ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

递增有序链表插入的完整实现与边界避坑指南

递增有序链表插入的完整实现与边界避坑指南 1. 题目分析与核心思路拆解1.1 这道习题到底在考什么“习题2.4 递增的整数序列链表的插入”是数据结构课程里非常经典的一道题。题目本身描述很朴素有一个已经按递增升序排列好的单链表现在要插入一个新节点让插入后的链表依然保持递增有序。听起来是不是特别简单不就是找个位置插进去嘛。但我带学生做这道题的时候发现十个里面有七八个会翻车翻车点五花八门但又高度集中。要么是没有考虑链表为空的情况一上来就解引用空指针要么是遍历条件写错跳出循环后指针指偏了要么是头插的情况丢掉了头节点链表从头就断了。这些错误本质上指向同一个问题——你真的是在用“链表思维”思考还是在用“数组思维”硬套这道题真正考察的不是“会不会写插入”,而是以下几层能力第一链表遍历的终止条件设计。数组里找一个位置你只需要记住下标 i然后 arr[i] 和 arr[i1] 之间的关系是天然的。但链表里没有下标你手里只有两个指针在挪什么时候停、停在哪稍微想岔了插入位置就偏了。第二边界情况的完整覆盖。空链表、新节点最小头插、新节点最大尾插、新节点夹在中间中间插这四种情况全都要照顾到。很多同学只把中间插的代码写出来了其他三种情况要么没写要么写了但是逻辑是错的。第三指针操作的正确顺序。链表插入最关键的一句就是 newNode-next pre-next 和 pre-next newNode这两句话的顺序不能反。先断链还是先挂新节点结果完全不同。顺序错了后面的节点就丢了就会造成内存泄漏。第四动态内存管理的意识。新节点从哪来malloc 分配。分配失败怎么办用完要不要 free这些在考试题里经常不做要求但在实际工程项目里每一条都是命门。所以我说这道题是链表入门的“试金石”。你要是能把这道题的边界情况捋明白后面学双向链表、循环链表其实都是顺手的事因为核心思维完全一样。1.2 为什么选链表而不是数组在动手写代码之前值得花一分钟想一个问题题目为什么要用链表来存这个递增序列而不是用数组答案其实很简单因为插入操作是链表的“主场”。数组的插入最坏情况下要把后面所有元素都往后挪一格时间复杂度是 O(n)而且如果数组一开始就开满了还得扩容扩容又是一次 O(n) 的复制。链表不需要链表在找到插入位置之后只需要改两个指针的指向时间复杂度是 O(1)。这个过程可以用一个生活化的例子来理解数组就像一条板凳上坐满了人现在来了个新同学要按个子高低坐到中间那从中间开始的所有人都得站起来往后挪一个位置。链表就像一列火车车厢每节车厢之间是用挂钩连接的现在要加一节车厢只需要在对应的位置把挂钩摘开把新车厢挂上去再重新挂好即可后面的车厢一节都不用动。当然链表也不是没有代价。数组支持随机访问我想知道第 5 个元素是谁直接 arr[4] 就行了O(1)。链表不行哪怕你想找第 2 个元素也得从头节点开始next 一次才知道。这就是链表在“查找”上的短板。所以链表的插入虽然在“插入”这一步是 O(1)但前提是“你已经找到了插入位置”而找这个位置本身要遍历链表是 O(n)。这就引出一个非常重要的结论链表插入的复杂度是“查找的 O(n)”加上“插入的 O(1)”整体上仍然是 O(n)。那是不是说链表就没优势了不是。链表的优势在于如果你的场景是“频繁在中间位置插入删除而且每次插入删除时位置已经知道比如通过迭代器定位好了”那数组是顶不住的链表才是正解。另外链表的存储空间是动态的这个月存 100 个节点下个月存 10000 个它都能自适应数组的容量是静态的你开大了浪费内存开小了又不够用。1.3 先画图再写代码找插入点的核心逻辑我教这道题的时候会强制要求学生先画图再写代码。不是形式主义的画图是真的把链表结构和指针变化画在草稿纸上。为什么因为链表的所有操作本质上都是“指针的重新指向”而人脑对于“指针指向哪”这件事天生就不太擅长直接凭空想象。画图能把这个过程可视化大大减少犯错的概率。假设我们的链表长这样head - [3] - [5] - [8] - [11] - NULL现在要插入一个值为 7 的新节点。我们的目标很明确希望它站在 5 的后面、8 的前面因为 5 7 8。那走查一下这个过程。我们需要两个指针一个叫 preprevious前驱节点一个叫 curcurrent当前节点。一开始pre head也就是指向值为 3 的节点cur head-next也就是指向值为 5 的节点。我们要做的是让 pre 和 cur 不断地往后挪直到 cur 指向的节点的值大于等于 7。当 cur 指向值为 8 的节点时循环就该停了。此时 pre 站在值为 5 的节点上cur 站在值为 8 的节点上而我们要插入的新节点就刚好插在 pre 和 cur 中间。好那循环的条件怎么写换成代码语言就是while (cur ! NULL cur-data newData) { pre cur; cur cur-next; }注意这个条件里的两个关键点。第一为什么是cur-data newData就继续走因为如果当前节点的值比新值小说明新值应该插在它后面所以 pre 和 cur 继续往后挪。反过来如果 cur-data 大于等于 newData说明新值应该插在 cur 前面也就是 pre 的后面循环终止。第二为什么还要判断cur ! NULL因为新节点有可能比链表里所有节点都大。比如链表的尾节点是 11新值是 20那 cur 会一直挪到 NULL。此时循环必须停下来否则你再访问 cur-data 就是解引用空指针程序直接崩溃。这个cur ! NULL就是边界条件保护。循环结束后newNode-next 应该指向 curpre-next 应该指向 newNode。完事。你会发现在这个过程中核心逻辑其实就三行。但很多同学栽就栽在逻辑想清楚了代码却写成了先pre-next newNode再newNode-next cur这样就斩断了后续链表newNode 后面就没接上原来的后续节点了。所以画图为什么重要因为只要你在图上把箭头一画顺序就能看出来。2. 完整实现从数据结构到插入函数2.1 链表节点的定义与创建既然要写代码第一步肯定是定义链表节点。这里的场景是整数序列所以数据域就是一个 int。稍微有工作经验的读者肯定会想这未免有点教学气真实项目中哪有人直接用 int 当链表数据确实真实项目里链表节点的数据域千奇百怪可能是个结构体可能是个字符串也可能是任意复杂类型的指针。但作为一道习题int 是最干净的载体能把“链表的机制”完整呈现出来又不至于让类型问题干扰理解。#include stdio.h #include stdlib.h typedef struct Node { int data; struct Node *next; } Node, *List;这里的 typedef 比较方便后面写代码的时候Node *和List可以混用。注意List本质上就是Node *通常用来表示一个链表的头指针。当然也可以不用List这个别名直接写Node *head更直白新手可能更习惯这种写法。接下来是创建新节点的辅助函数。每次插入都要 malloc 一个新节点如果每次都写一遍 malloc 和判空代码会显得很啰嗦所以封装成一个函数是很有必要的。Node* createNode(int data) { Node *newNode (Node*)malloc(sizeof(Node)); if (newNode NULL) { printf(内存分配失败 ); return NULL; } newNode-data data; newNode-next NULL; return newNode; }这里有几个细节值得讲一下。首先是 malloc 的返回值。在 C 语言中malloc 返回类型是void*所以在 C 里需要强制转换成Node*但如果在纯 C 环境下编译void*到Node*的转换是隐式的不需要强转。不过为了兼容性我一般还是会写上显式转换反正不亏。其次是内存分配失败的检查。很多学生的代码里是不查的malloc 完直接就用。这在刷题平台上的确大概率没问题因为内存几乎不会分不出来。但在嵌入式环境或长期运行的服务器程序里内存分配失败是真实可能发生的如果不对 NULL 做处理下一步newNode-data data就是对空指针解引用程序崩溃而且崩溃得毫无预兆排查起来很痛苦。第三新手容易忽略的一个点newNode-next一定要初始化为 NULL。为什么不初始化会出事因为 malloc 分配的内存里面是随机值不是你想象中的 0。如果你把 newNode-next 默认成 NULL而它实际是个随机地址那后面遍历链表的时候就会跑到一个野地址去轻则读到垃圾数据重则段错误。这种 bug 极其隐蔽你单步调试都不一定能发现。2.2 insertSorted 函数的完整实现与逐行解读在一切准备就绪之后核心的插入函数来了。我先给出一个完全版本再带大家一行一行拆。void insertSorted(List *head, int newData) { Node *newNode createNode(newData); if (newNode NULL) { return; } // 情况一链表为空或新节点应该插在头节点之前 if (*head NULL || (*head)-data newData) { newNode-next *head; *head newNode; return; } // 情况二中间插入或尾部插入 Node *pre *head; Node *cur pre-next; while (cur ! NULL cur-data newData) { pre cur; cur cur-next; } // 循环结束后newNode 应该插在 pre 和 cur 之间 newNode-next cur; pre-next newNode; }等一下有同学会问为什么这个函数接收的是List *head而不是List head换句话说为什么传的是头指针的地址这是这道题里非常关键的一个设计点。想一个问题如果链表是空的我们要插入第一个节点。那就得修改头指针让 head 指向这个新节点。如果你函数的形参只是List head那你在函数里改 head 的值改的是形参的副本函数一返回原链表头指针纹丝不动等于没插。所以凡是需要“修改头指针本身”的场景都必须传头指针的地址也就是二级指针。如果实在不想用二级指针也有别的办法比如让链表带头节点dummy head这个我们下一节专门讲。总而言之传二级指针是在不带头节点的情况下必须做的选择。现在逐行看逻辑。先看if (*head NULL || (*head)-data newData)。这里处理的是两种特殊情况一种是链表为空另一种是新节点比原来所有节点都小得插在头部。这里用了一个巧妙的合并写法*head NULL把空链表的情况涵盖了(*head)-data newData把新节点最小的情况涵盖了。为什么用因为题目里只说“递增”没说是否允许相等。如果序列里有重复元素新值又相等插在头节点之前并没有破坏单调性——相等的也算非递减嘛。当然如果你想严格“递增无重复”那这里应该用也就是只有当新值小于头节点值时才头插。具体用哪个取决于你对题目“递增”的理解。这个细节我们后面还会展开。如果头插不用处理那就进入第二个阶段从左往右找插入点。pre一开始指向头节点cur指向pre-next。循环条件cur ! NULL cur-data newData的意思是只要当前节点不为空而且当前节点的值小于新值我就接着往后找。这里再强调一次循环条件的顺序。cur ! NULL必须写在左边cur-data newData写在右边。原因很简单C 语言里是短路求值左边为假右边根本不执行。如果把条件写成cur-data newData cur ! NULL当 cur 为 NULL 时第一步就会先解引用空指针去取cur-data然后程序当场崩溃。这个顺序问题刷题的时候不常见但写工程项目时分分钟遇到养成习惯很重要。最后是插入操作。循环结束后cur 要么停在某个值大于等于 newData 的节点上要么停在 NULL 上pre 就在它的前一个位置。于是newNode-next cur; pre-next newNode;这两步的顺序是死规矩不能反。很多人问过先写pre-next newNode再写newNode-next cur行不行答案是行不行要看具体情况但这个顺序是危险操作。如果你先让 pre-next 指向 newNode此时原来的 cur 就“断链”了你手里还拿着 cur 的地址吗拿着的Node *cur这个变量还在所以如果你马上执行newNode-next cur其实结果也对。但问题是如果代码不是这么简单中间隔着几行其他操作或者函数被改了很容易出问题。更稳妥的习惯是先接后面再接前面这样即便中途被打断链表的后半部分也还挂在别的地方不容易丢。这属于“防御性编程”的范畴我用这个顺序用了很多年很少因为插入操作丢过节点。2.3 带头节点与不带头节点的区别写链表的场景里一直有一个“派系之争”到底用不用头节点dummy head带头节点的链表它的头指针指向一个真正的节点但这个节点不存储有效数据只是为了方便操作而存在。插入、删除的时候你永远不需要修改头指针本身因为它始终指向那个固定的 dummy 节点。这样一来insertSorted 函数的签名就变成了void insertSorted(List head, int newData)注意这里不需要二级指针了因为函数体内的头插逻辑不会改变 head 变量的指向head 始终指向 dummy 节点变的只是 head-next。那带头节点的实现长什么样核心逻辑变成这样void insertSorted(List head, int newData) { Node *newNode createNode(newData); if (newNode NULL) { return; } Node *pre head; Node *cur head-next; while (cur ! NULL cur-data newData) { pre cur; cur cur-next; } newNode-next cur; pre-next newNode; }对比一下不带头节点的版本你会发现代码变得更短了更统一了没有了那个单独处理“头插”的分支。为什么因为 dummy 节点永远站在链表的第一个位置新节点再小也只会插到 dummy 的后面而 pre 和 cur 只需要从 dummy 出发往下走就行。这样空链表、头插、中间插、尾插全部被统一成一种操作。这就是 dummy head 的最大价值——消除边界分支让代码逻辑变得扁平。代价是什么呢多了一个节点的内存开销。另外遍历链表输出的时候要记得跳过头节点再输出否则会把 dummy 当作有效数据打出来。这两种风格在工业代码里都很常见。Linux 内核里的链表实现用的是另一种思路侵入式链表但那是更进阶的话题。就这道题而言如果你是在刷数据结构考研题通常默认不带头节点如果你在实际写代码我更推荐带头节点。这个选择不用纠结理解清楚各自的原理代码怎么都能写对。3. 边界条件与内存管理的实战避坑3.1 空链表插入最容易被忽略的入口先说一个让我印象非常深刻的场景。有一次我让学生们在课下实现这个插入函数第二天收上来的代码里有将近三分之一是“在链表为空时会崩溃”的。他们的代码长这样Node *pre *head; Node *cur pre-next; // 如果 *head NULL这一步就崩溃了 while (...) { ... }为什么崩溃因为链表为空时*head 是 NULL这时候pre-next就是在对 NULL 解引用属于 100% 的违法行为。哪怕编译器没报警告运行起来也必现段错误。正确的做法就是把空链表当作一个独立的分支来处理或者在进入“常规插入逻辑”之前就完成对空链表的覆盖。我在 2.2 的代码里做的处理是把空链表和头插合并到了同一个分支里if (*head NULL || (*head)-data newData) { newNode-next *head; *head newNode; return; }当 *head NULL 时newNode-next 被赋值为 NULL因为 *head 是 NULL然后 *head 指向 newNode完美实现“空链表插第一个节点”。这一行代码同时干了三件事挂空指针、更新头指针、返回。代码是简洁的但我建议你自己写的时候也可以拆开写先把空链表单独处理然后再处理头插这样可读性更好逻辑也更直白。还有一个容易犯的错误只判断了 pre 为 NULL 的情况没有判断 cur 为 NULL 的情况。循环写到一半当新值是最大值时cur 一直往后走最终走到 NULL此时如果循环条件里没有cur ! NULL的保护代码会在下一次判断 cur-data 时崩溃。这不是什么高深的坑就是一个边界意识但确确实实是高频错误。3.2 头插、中间插、尾插三种情况怎么合并有的教科书会把插入分成三种情况大讲特讲写三个函数或三个分支。但我要说的是只要边界条件写对了头插、中间插、尾插本质上根本不需要分开写它们共用同一段循环和同一段插入代码。回顾一下我们的循环退出后无非三种情况头插循环条件在一开始就满足了*head 不是 NULL但 head-data 已经 newData。此时 pre *head也就是原来的头节点cur pre-next。然后 newNode 插在 pre 后面指针变化是newNode-next pre-nextpre-next newNode代码完全走的是同一段逻辑。但注意在 2.2 的代码里头插是被 if 单独拦下来的。如果你把那个 if 删掉直接走下面的通用循环pre 初始得是 *headcur 是 pre-next那当链表非空且 head-data 已经大于等于 newData 时循环一次都不会执行pre 还是站在头节点上然后 newNode 就被插到了头节点后面。头节点本身没变新节点成了第二个节点。这跟“头插”语义不符——我们需要的是新节点成为新的头节点除非用 dummy head。中间插遍历中途停下pre 和 cur 都指向合法节点newNode 插在它们中间。尾插cur 挪到 NULL循环结束pre 指向原链表的最后一个节点。此时newNode-next NULLpre-next newNode完美。所以你看除了“新节点要成为新头节点”这个特殊情况需要单独处理外中间插和尾插是完全统一的。这也是为什么带头节点的链表能简化代码——因为在 dummy head 面前“头插”已经被转化成了“中间插”再也没有特殊分支了。3.3 指针悬挂、内存泄漏与崩溃的常见原因链表题除了解析逻辑最容易被忽略的就是内存问题。很多刷题网站不查内存泄漏程序跑完就算赢但真实工程项目不是这样。最常见的三大内存问题第一malloc 成功但没初始化 next 字段。前面说过malloc 返回的内存是不清零的如果你忘了给 newNode-next 赋值那么它是“悬空”的。如果恰好你又在尾插newNode 成了链表的最后一个节点那这个“尾巴”的 next 一定得是 NULL否则遍历链表时会被带飞到不知道什么地方去。所以newNode-next NULL这行代码不是摆设是生命线。第二插入时指针顺序反了导致链表断裂。比如这段错误代码pre-next newNode; newNode-next cur; // 有问题如果 pre-next 指向了 newNode那么原来的 cur 节点就被“孤立”了。虽然因为你还存着 cur 变量马上又把 newNode-next 接上 cur看起来结果是对的。但是如果这两行之间插了别的逻辑比如你先 free(cur) 了那 newNode-next 就指向一块已经释放的内存这就是典型的悬垂指针之后调用 free 和访问都会出问题。所以万能口诀是先让新节点指好它后面要接的东西再让前面的节点放弃旧连接指向新节点。第三忘记 free 导致内存泄漏。严格来说插入操作本身不会引起泄漏因为新节点是新建的插进去后它就是链表的一部分程序结束时由外部统一释放。但如果你写删除操作时不 free或者插入失败比如 createNode 返回 NULL就退出那就可能泄漏。养成好习惯写完一个函数先问自己这个函数的每一条分支是“拥有”了某个资源还是“借”了某个资源拥有的一方必然有释放的职责。3.4 测试用例设计怎么证明你的代码是对的这一部分是最能拉开“做题家”和“工程派”差距的。很多人代码写完往 OJ 一交Accepted 就完事。但我建议你养成设计测试用例的习惯哪怕就是一道习题。针对这道题我的测试用例清单是这样的测试场景输入链表插入值期望结果空链表插入NULL5链表变成 [5]头插[3, 7, 9]1[1, 3, 7, 9]中间插[3, 7, 9]5[3, 5, 7, 9]尾插[3, 7, 9]10[3, 7, 9, 10]重复值插前面[3, 7, 7, 9]7[3, 7, 7, 7, 9]重复值插后面[3, 7, 7, 9]7[3, 7, 7, 7, 9]顺序取决于实现插入非常量[5]5[5, 5] 或 [5, 5]取决于判头逻辑除了这些常规的我一定会做两个测试第一个是大数据量测试。比如先手动构造一个 10000 个节点的递增链表然后随机生成 100 个插入值插入后遍历一遍检查整个链表是否仍然严格递增。这个测试能一次性暴露你在循环边界上的大部分问题。第二个是内存检测。用 Valgrind 跑一遍看有没有 invalid read / invalid write / malloc leak。尤其是 invalid read很多时候代码能跑出正确结果但 Valgrind 会告诉你你其实在偷偷读了一块不该读的内存只是运气好没崩。这种东西在面试里被问到会非常加分。这两步做下来才敢说你的插入函数是可靠的。4. 复杂度分析与从这道题延伸出去的内容4.1 时间复杂度的两面性查找O(n)、插入O(1)在做复杂度分析的时候新人最容易犯的毛病就是背结论链表插入是 O(1)。这句话当然不算错但它有一个非常重要的前提就是位置已经找到了。如果给你一个“无序链表”你就直接插在头节点后面那确实 O(1)但本题是插入后要维持递增有序那就必须从头遍历找插入点这一步的时间复杂度是 O(n)。具体分析一下。最好的情况是插入位置就在头节点或者链表为空此时只需要执行常数次操作时间复杂度 O(1)。最坏的情况是插入位置在尾部pre 和 cur 要一个节点一个节点挪到链表末尾假设链表长度为 n那么循环体执行 n 次O(n)。平均情况如果插入值均匀分布平均要遍历一半的链表也就是 n/2 次仍然是 O(n)。空间复杂度方面这个算法只需要一个新建节点外加两个临时指针变量 pre 和 cur所以空间复杂度是 O(1)。换句话说无论链表多长这个函数额外占用的内存是固定的。再往深层想一步如果你在维护一个动态有序集合并且插入频率极高单纯用单链表做插入O(n) 的平均复杂度可能扛不住。这时候就该考虑用跳表Skip List或者二叉搜索树BST了当然再往上还有红黑树这类平衡结构。为什么会有那些高级结构本质上就是它们把“查找插入位置”这个 O(n) 的步骤优化成了 O(log n)。所以一道基础题背后引出的复杂度思维能帮你理解整个数据结构体系的演化脉络。4.2 循环链表、双向链表与有序表合并这道题做完了建议你趁热打铁把几个变体也写一遍收益会非常大。第一个变体是循环单链表。如果把尾节点的 next 指向 head而不是 NULL链表就成了循环链表。循环链表的遍历终止条件不再是cur ! NULL而是cur ! 头节点或者用 do-while 结构先走一步再判断。插入的时候尾插和头插的判断会变得绕因为你没有一个“天然的 NULL”来表示链表结束得靠“回到起点”来判断。这个变体的典型应用是操作系统进程调度里的时间片轮转队列以及一些游戏引擎里的循环动画列表。我当年学内核时接触到的很多环形缓冲区实现本质上就是循环链表的思想。第二个变体是双向链表。多了一个前驱指针 prev插入一个新节点需要操作四个指针newNode-prev pre; newNode-next cur; if (cur ! NULL) { cur-prev newNode; } pre-next newNode;注意到区别了吗双向链表里找到插入位置后要更新前一个节点的 next、后一个节点的 prev、新节点的 prev 和 next。这正好四步。顺序上依然要记住先接新节点和后续节点再接新节点和前驱节点最后改后续节点的 prev。双向链表的好处是支持双向遍历删除节点的时候不需要知道前驱直接用 cur-prev 就能找到它复杂度从查找前驱的 O(n) 降到了 O(1)。第三个变体是有序链表合并。给你两个已经递增的单链表要求合并成一个仍然递增的单链表。核心思路就是双指针同时遍历两个链表谁小就把谁摘下来接到新链表后面这个操作其实和“插入”非常相似都是基于对有序性的利用。合并的代码我建议你也写一遍因为它是归并排序在链表上的核心操作理解了它你就会发现“把两个有序数组归并”和“把两个有序链表归并”其实是同一种思想在不同载体上的体现。4.3 通用化设计用函数指针支撑任意数据类型的插入最后聊一个偏进阶的点。目前我们写的 insertSorted 是专门给 int 用的那如果链表里存的是 float、字符串、结构体呢难道每种类型都要重写一个插入函数C 语言的正统解法是用 void* 数据域配合函数指针。把节点的数据域改成 void*然后让调用方传入一个比较函数返回负数、零或正数表示大小关系这样插入函数本身就不需要关心数据到底长什么样了。typedef struct Node { void *data; struct Node *next; } Node; typedef int (*compare_fn)(const void *a, const void *b);然后插入函数大概长这样void insertSortedGeneric(List *head, void *newData, size_t dataSize, compare_fn cmp) { Node *newNode createNode(newData, dataSize); if (newNode NULL) return; Node *pre *head; Node *cur pre ? pre-next : NULL; while (cur ! NULL cmp(cur-data, newData) 0) { pre cur; cur cur-next; } newNode-next cur; if (pre ! NULL) { pre-next newNode; } else { *head newNode; } }其中 createNode 需要根据 dataSize 动态申请内存然后把新数据拷贝进去。这个模式其实就是标准库 qsort 里“自定义比较函数”那一套思路的链表版。说到这插一句很多嵌入式领域的热插拔设备管理、内核链表的节点遍历用的都是类似的设计模式因为设备的节点不是 int而是一个包含厂商 ID、设备类型、总线编号的结构体。所以别看这是一道普通的习题它的设计思想能一路通到内核代码里。最后再分享一个小技巧我过去带新人时发现一个特别有意思的现象初学者写链表代码特别喜欢对着屏幕一行一行盯企图用肉眼找到 bug。这其实是效率最低的方式。链表这种结构靠眼睛盯是盯不出来的因为链表的 bug 往往是结构性的是“链接关系错乱”导致的整条链的样子和你脑补的完全不一样。我的建议是写一个“调试辅助函数”每次插入完之后把整个链表从头到尾打印一遍void printList(List head) { Node *cur head; int count 0; while (cur ! NULL) { printf(%d - , cur-data); cur cur-next; count; if (count 100) { // 防止循环链表死循环 printf(CYCLIC LINK DETECTED! ); return; } } printf(NULL ); }然后每操作一步就调用一次观察输出是否符合预期。这个习惯看起来笨但在排查链表问题时远比调试器好用因为你直接看到的是“逻辑视图”而不是机器层面的内存视图。这个 count 100 的防御性判断也建议保留万一你的链表在哪个环节不小心被改成了循环链表这个打印函数能帮你立刻发现而不是让程序陷入死循环。回到这道题本身。能把这个递增序列链表的插入写出、写对、写出边界完备的版本你对链表指针操作的基本功就已经算是扎实了。恭喜你过了这一关接下来再遇到什么样的链表题心里都不会虚了。
返回列表