尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

单链表操作详解:建表、插入、删除与逆序

发布时间:2026/9/29 16:11:23

资讯中心
01
ARTICLE

单链表操作详解:建表、插入、删除与逆序

单链表操作详解:建表、插入、删除与逆序
很多朋友第一次接触数据结构第一个觉得“有点意思”的东西就是单链表。我当年学C语言的时候写一个带插入、删除的单链表能把全班一半人劝退。后来用Python重新把这些操作写了一遍才发现核心逻辑其实就那么几条找前驱、改指针、防断链。单链表是后面学习栈、队列、图、哈希表的基石很多进阶数据结构里都藏着“节点指针引用”的影子。这篇文章我想把单链表从建表到清空、从逆序到循环链表的所有常见操作按我自己实践的路数重新讲一遍重点放在边界处理、内存释放和调试技巧上。不论你是刚接触数据结构的在校生还是刷算法题准备面试的开发者又或者只是工作中偶尔需要用Python封装一个简单的链表结构这篇文章都适合你而且我会尽量用让人听得懂的话把每一步的“为什么”讲清楚。1. 先从底层逻辑看单链表为什么它值得认真学1.1 数组在插入和删除时的“搬砖”困境很多人学链表之前都在用数组数组的优点是连续内存、随机访问快按下标拿元素是O(1)时间。但它最大的短板就是插入和删除太贵。你可以想象一排座位坐满了人有人想坐到第三排中间那么后面所有人都得站起来挪一个位置。数组的插入就是这种“挪位置”的操作在中间插入一个元素后续元素全部后移删除中间元素后续元素全部前移。数据量小的时候无所谓几万个元素的时候一次插入就可能引发大量内存拷贝性能瞬间变差。链表就是用来解决这个问题的。它不要求元素在内存里连续存放而是每个节点自己带着“下一个节点的地址”像一串珠子一样用线串起来。这样在中间插入或删除时只需要改动前后两个节点的指针其他节点完全不动。这就是链表最核心的价值用牺牲随机访问的代价换来插入删除的高效率。1.2 节点的基本组成与“头”的三种角色单链表的最小单位叫节点在C语言里通常用结构体定义在Python里可以用类或者简单的两个属性来模拟。每个节点只有两部分数据域和指针域。数据域存实际内容指针域存下一个节点的引用地址。最后一个节点的指针指向空表示链表结束。这里必须把三个容易混的概念讲清楚头指针、头结点、首元结点。头指针是指向链表第一个节点的变量它是链表存在的标志头指针为None就表示空链表。头结点是在首元结点之前额外添加的一个虚拟节点它不存实际数据或者只存链表长度等辅助信息。首元结点则是链表中第一个真正存数据的节点。很多人一开始分不清“头结点”和“首元结点”其实只要记住头结点可有可无有了它空链表和非空链表在处理上可以统一代码写起来会少很多if分支。1.3 带头结点和不带头结点的单链表怎么选不带头结点的写法是最直白的头指针直接指向第一个数据节点空链表时头指针为None。插入到第一个位置时必须特殊处理因为要修改头指针本身删除第一个节点时也一样得把头指针往后移动一个节点。这些特殊处理容易忘忘一次就出现空指针异常。带头结点的写法则是额外创建一个dummy节点让它作为头结点真正的第一个数据节点是dummy.next。这样“插入到第一个位置”就变成了“在dummy之后插入”和其他位置的操作完全一致删除第一个节点也变成了“删除dummy的下一个节点”逻辑统一。代价是多用一个节点但对代码清晰度的提升非常明显。我个人在做算法题时几乎都给单链表加一个虚拟头结点这里分享一个小结论大多数链表修改操作虚拟头结点都能帮你省掉一半的边界判断。对比项不带头结点带头结点空链表状态head为Nonehead指向头结点头结点.next为None插入头位置需要修改head在头结点后插入即可删除头位置需要修改head删除头结点的后继即可存储开销少一个节点多一个节点可存辅助信息代码复杂度分支多分支少更统一这两种写法在实际项目中都很常见Python中由于没有指针通常用“类模拟节点”的方式同样可以灵活选择带不带虚拟头结点。我建议入门阶段两种都写一遍能加深理解。2. 基本操作拆解建表、遍历、插入、删除2.1 建立单链表头插法和尾插法的取舍建立单链表最常用的方式有两种头插法和尾插法。头插法是每次把新节点插到链表头部也就是让新节点的next指向当前head再把head指向新节点。这种写法的好处是时间复杂度O(1)不需要遍历链表找尾节点但缺点是最终链表顺序和输入顺序相反。比如依次输入1、2、3头插法构建后访问顺序是3、2、1。尾插法则是每次把新节点接到链表末尾需要先走到链表尾部再接入因此普通实现是O(n)的时间。如果数据量很大可以额外用一个尾指针来记录最后一个节点这样也能做到O(1)插入。尾插法得到的链表顺序和输入顺序一致更符合我们的直觉。用Python实现一个不带头结点的尾插法代码如下class ListNode: def __init__(self, val0, nextNone): self.val val self.next next def build_linked_list(arr): if not arr: return None head ListNode(arr[0]) cur head for val in arr[1:]: cur.next ListNode(val) cur cur.next return head代码里用cur这个“移动指针”来代表当前链表的尾部每接入一个新节点就把cur移动到新节点上。这个方法我建议所有初学者手写十遍因为后面所有链表遍历操作的基础都是这套“从头走到尾”的思路。2.2 在指定位置插入一个节点最容易翻车的边界处理“在指定位置插入建立单链表”是很多实验课必做的题目也是热词里出现频率最高的一项。这里的“指定位置”一般有两种理解一种是指定下标从0开始另一种是指定某个节点之后。我们在算法题里最常见的是前者。插入的核心思想是找到位置index处的前驱节点prev然后让新节点new_node.next指向prev.next再把prev.next指向new_node。顺序千万别写反如果把prev.next先改了原来的后继节点就丢了这个错误我第一次写的时候也犯过调试了很久。下面是带虚拟头结点的Python实现它可以避免插入头位置时修改head的额外分支def insert_at_index(head, index, val): dummy ListNode(0, head) prev dummy # 移动prev到index位置的前一个节点 for _ in range(index): if prev.next is None: raise IndexError(Index out of range) prev prev.next new_node ListNode(val) new_node.next prev.next prev.next new_node return dummy.next这个函数里如果index为0那么pre指向的就是dummy插入之后新节点成为真正的新头结点。如果index大于链表长度则进入循环时prev.next为None抛出异常。边界处理的关键点就是我们循环终止时prev停在哪里以及在修改指针时是否保存了原后继节点。我通常在草稿纸上把“当前链表状态”画出来再写代码可以极大概率避免翻车。2.3 删除节点找到前驱比找到节点本身更重要删除操作和插入操作很像也需要先找到目标节点的前驱节点。如果要删除的是下标为index的节点实际上就是“跳过”这个节点让前驱的next直接指向目标节点的next。在Python中被跳过的节点如果没有其他引用Python的垃圾回收机制会自动回收但如果是在C语言里必须手动free否则就内存泄漏了。def delete_at_index(head, index): dummy ListNode(0, head) prev dummy for _ in range(index): if prev.next is None: raise IndexError(Index out of range) prev prev.next if prev.next is None: raise IndexError(Index out of range) # 要删除的节点是 prev.next prev.next prev.next.next return dummy.next有一个很容易被忽略的坑删除最后一个节点时prev.next是最后一个节点prev.next.next是None执行prev.next None是正常操作但如果删除倒数第二个节点prev.next.next指向最后一个节点执行后链表长度减一正确。问题往往出在“删头”和“删尾”两个边界上删头时如果没有虚拟头结点就必须单独处理head的更新删尾时要注意索引越界的判断。用虚拟头结点之后两种情况都被统一了这也是我在所有删除代码里都加dummy的原因。2.4 查找、修改与链表长度的统计查找操作同样需要遍历。最典型的场景就是“判断某个值是否在链表中”以及“返回第一个匹配节点的下标”。注意链表的查找复杂度是O(n)没有数组那样的随机访问能力这是它的固有缺点。查找代码很简单但要提醒一点遍历时循环条件用cur is not None而不是用cur.next is not None否则你就漏掉了最后一个节点。很多人写查找时犯这种错结果最后一个元素永远找不到。修改操作通常结合查找来做先找到节点再改value。这里不需要修改指针只需要给数据域赋值相对简单。计算链表长度时可以用循环计数也可以用递归。我强烈建议用循环因为递归写的长度计算在链表很长时很容易触发Python的递归深度上限而在C语言中也可能导致栈溢出。工程应用中链表节点达到十万级并不稀奇递归不是好选择。3. 高频进阶操作清空、逆序、循环单链表3.1 清空链表别一上来就断掉头指针清空链表这个操作看起来简单做起来有讲究。最直白但最危险的做法就是直接head None。在Python里如果整个链表没有其他引用确实会被垃圾回收看起来效果也不错但这会掩盖一个重要问题如果链表节点还被其他变量引用或者你是在C语言里写那么这些节点就永久泄漏了。正确的清空思路是遍历链表逐个断开引用。在C语言里要逐个free节点在Python中至少要保证所有节点不再被引用。如果你正在用类封装链表类成员变量还保存着head那么把head置为None就会让整条链失去根引用进而被回收但这依赖GC机制并不适合用来训练对内存管理的认识。我推荐的做法是如果需要清空一个单链表用一个指针cur遍历保存下一个节点然后断开当前节点的next直到链表结束。虽然Python里是“多此一举”但这能帮你建立正确的内存管理意识将来写C、C或者Rust时会感谢现在这些练习。在一个自定义的LinkedList类中清空后还要记得把size重置为0。3.2 Python单链表逆序的三板斧“python单链表逆序”是搜索热度非常高的关键词也是面试手撕代码的高频题。核心要求是把链表的指针方向全部反转也就是原来的第一个节点变成最后一个节点最后一个节点变成第一个节点。注意这里不能重新建一条新链表否则空间复杂度就变成了O(n)违背了原题通常要求的O(1)空间。第一个方法迭代反转最推荐。用三个指针prev、cur、next_temp遍历链表时先保存cur.next再把cur.next指向prev然后三个指针整体后移。最终head指向原来的尾节点。代码如下def reverse_list(head): prev None cur head while cur is not None: next_temp cur.next cur.next prev prev cur cur next_temp return prev这个方法需要记住一个关键点next_temp cur.next必须在修改cur.next之前完成顺序不能变。我正式面试时遇到过好几个人在这一点上卡住一紧张就把next_temp忘了。第二个方法递归反转。递归版本代码特别短但理解起来需要绕一下。它的思路是“先反转后面所有的节点再把当前节点接到反转结果的末尾”。不过递归在链表很长时有栈溢出风险面试时可以用工程中要谨慎。第三个方法栈辅助反转。先遍历链表把所有节点压入栈中再逐一弹出并重新链接。这个办法最简单但空间复杂度O(n)只适合对空间不敏感的场景。实战中我优先使用迭代法因为时间O(n)、空间O(1)逻辑也是最直观的。3.3 循环单链表环形结构让边界问题消失一半循环单链表是指链表最后一个节点的next不再指向None而是指向第一个节点形成一个环。这种结构非常适合那些需要“周而复始”访问的场景比如操作系统的进程调度轮转、约瑟夫环问题。在循环单链表中最大的变化是遍历的终止条件。普通单链表用cur is None判断结尾循环链表不行你得记录起始节点当cur再次回到起始节点时停止。因此循环链表一般保留头指针或尾指针尾指针指向最后一个节点这样最后一个节点访问第一个节点就很方便插入到末尾也直接通过尾指针O(1)完成。循环单链表的插入删除操作和普通单链表相似但要注意不能把链表遍历到None。如果你在某个节点后面插入了一个新节点需要判断该节点是否是最后一个节点是的话还要更新尾指针。删除操作也一样如果删除的是尾节点别忘了把尾指针往前移一个节点。很多初学者第一次写循环链表时都会因为循环条件写错而出现死循环。我的经验是先画出链表结构标出头和尾再写代码基本不会错。下面是一个简单的循环链表构造示例def build_circular_linked_list(arr): if not arr: return None head ListNode(arr[0]) cur head for val in arr[1:]: cur.next ListNode(val) cur cur.next cur.next head # 尾节点指向头结点形成循环 return head注意这个head是首元结点不是虚拟头结点。循环链表里你同样可以使用dummy节点但使用时要小心dummy节点也在环里遍历时需要跳过。4. 单链表基本操作实验设计从零到可运行的测试4.1 实验的题目设计与模块划分很多学校的“单链表的基本操作实验”一般是要求你实现初始化、插入、删除、查找、遍历、清空等功能并写一个菜单程序来演示。这里我建议不要只写一个main函数堆到底而是把操作封装成类或者独立函数再写一个测试模块。我常用的做法是定义一个LinkedList类包含head指针和size计数提供append、insert、delete、search、reverse、clear等方法。然后写一个简单的单元测试函数每次操作后都打印当前链表内容和长度。如果你用pytest直接写几个test用例更好但初学者阶段用普通断言加打印也够了。一个典型的实验流程可以是这样的从数组初始化链表并打印。在头部、中间、尾部各插入一个节点打印验证。删除头部、中间、尾部的节点打印验证。查找一个存在和一个不存在的值打印结果。反转链表并打印。清空链表查验长度是否变成0。这套流程几乎涵盖了所有基本操作做完一遍单链表的理解基本就到位了。实验代码不求花哨但每操作一步都要能看到链表的状态变化这是调试自己写的链表最好的方式。4.2 新手的五个典型错误第一个错误插入时先把前驱的next指向新节点再去设置新节点的next导致原后继节点丢失。因为前驱的next已经被改变你再也没有办法拿到原来的后继了。正确的顺序是先把新节点和后继连起来再让前驱指向新节点。第二个错误删除时没有判断链表为空。在空链表上执行删除prev.next是None访问None的next属性就直接抛异常。所以任何删除操作前都要考虑空链表的情况最好用防御性写法。第三个错误遍历循环条件多写一个等号或漏掉最后节点。比如用while cur.next is not None作为循环条件会漏掉最后一个节点用while cur is not None则正好。写遍历时把这两种条件在脑海里过一遍能少踩一半坑。第四个错误反转链表时丢失后续节点。很多人知道反转是“指回头”但只顾着把cur指向前驱忘了保存原来的后继结果链就断了。这就是我前面反复强调的next_temp必须先保存的原因。第五个错误索引和计数边界差一。比如删除第index个节点循环应该走index步还是index-1步取决于前驱的初始位置。最好的解决方式是在纸上画出dummy节点、前驱、目标节点、后继节点用箭头标出走几步。画图不是浪费时间是真正高效的debug方式。4.3 调试单链表的高效率工具与技巧调试链表最怕的就是“脑子里跑代码”。哪怕经验再丰富链表指针一复杂脑子也会打结。我的方法是给链表写一个打印函数输出类似1 - 2 - 3 - None的格式并且在每一步修改操作前后都调用打印函数。别嫌麻烦它能在几分钟内帮你定位问题。另外刷题网站和IDE里通常支持断点调试你要善用“watch”面板观察prev、cur、next_temp三个变量的值。尤其是反转链表这种多指针操作你可以在每次循环后把三个变量的值记录下来找出出错的那一步。还有一个实用的技巧用最小测试用例验证。比如插入操作测试空链表插入、在头插入、在尾插入、在中间插入这四类用例覆盖了所有边界。删除操作同理删除头、删除尾、删除中间节点加上空链表和越界情况。你把这些用例都跑一遍代码的边界条件基本就没有bug了。我个人在实际操作中的体会是单链表最想训练的不是“背代码”而是建立一种对“引用/指针”修改的直觉。凡是涉及链表的修改操作你先在纸上把前驱和后继画出来再动手写代码基本不会出错调试时再用打印函数把每一步的链表状态输出来有问题也藏不住。这套方法不仅对单链表有效后面学习双向链表、循环链表、二叉树时一样能用得上。最后再分享一个小技巧给链表类加上一个length字段时刻维护它的大小而不是每次现算这样很多判断越界的逻辑会简单一个量级。希望这篇单链表入门能帮你把基础打得扎实一点少走一些我当年走过的弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。