资讯动态

Python链表实现详解:从节点定义到增删改查,掌握数据结构核心

发布时间:2026/8/21 3:33:47 来源:尧图企业网站定制
链表是数据结构中的核心概念也是浙江高中信息技术选修一《数据与数据结构》课程的重点与难点。很多同学在学习时感觉链表比数组抽象操作起来也更复杂。本文将以Python语言为载体彻底拆解链表的基础操作从节点定义到增删改查提供可直接运行的代码和清晰的逻辑图解帮你把“酷学科教”中的知识点变成自己能够灵活运用的编程技能。我们将重点关注链表在Python中的具体实现逻辑、内存管理特点以及它解决的实际问题。与数组相比链表在插入和删除操作上具有理论上的时间复杂度优势但这种优势如何体现在代码中又会带来哪些额外的开销通过本文的逐步实现与对比分析你将能清晰地回答这些问题。1. 核心能力速览链表是什么能做什么在深入代码之前我们先通过一个表格快速建立对链表的整体认知明确学习目标。能力项说明与Python视角核心定义一种线性数据结构元素节点在内存中非连续存储依靠指针引用连接。与数组对比数组连续内存通过索引直接访问插入删除需移动元素。链表非连续内存通过指针顺序访问插入删除仅需修改指针。主要类型单链表每个节点包含数据和指向下一个节点的指针。双向链表节点包含指向前后节点的指针支持双向遍历。循环链表尾节点指向头节点形成环。核心操作遍历、查找、头部/尾部插入、任意位置插入、删除节点。时间复杂度访问O(n)插入/删除已知位置O(1)查找O(n)Python实现关键使用类的实例表示节点用实例属性next模拟指针。无需手动管理内存由GC负责。适用场景频繁的插入和删除操作、不确定数据规模、实现栈、队列等高级数据结构。学习重点指针引用的操作、边界条件处理空链表、头尾节点、虚拟头节点的技巧。2. 链表解决了什么问题为什么需要它数组是一种极其高效的数据结构特别是在随机访问通过索引直接获取元素时。然而这种高效源于其在内存中的连续存储。这也成为了它的“阿喀琉斯之踵”大小固定在多数静态语言如C、Java中数组大小需预先声明难以动态扩容。Python的list虽然是动态数组但扩容resize涉及申请新内存和复制所有元素是一个O(n)操作。插入删除成本高在数组中间插入或删除一个元素需要将该位置之后的所有元素向后移动或向前移动平均时间复杂度为O(n)。链表通过“用空间换时间”和“改变数据组织方式”来应对上述挑战动态大小每个节点独立分配内存可以随时创建新节点加入链表理论上没有容量限制受限于总内存。高效插入/删除只要找到目标位置插入或删除节点仅需修改相邻节点的指针引用时间复杂度为O(1)。注意这里的“已知位置”通常指已经持有目标节点或其前驱节点的引用查找这个位置的过程仍然是O(n)。简单来说当你需要频繁在序列中间进行添加或移除操作且随机访问的需求不高时链表是一个比数组或Python list更优的选择。许多复杂数据结构的基础都是链表例如图邻接表、哈希表链地址法解决冲突等。3. 环境准备与思维转换学习链表你不需要复杂的AI模型或GPU环境但需要完成两个关键的“环境准备”一是编程环境二是思维方式。3.1 编程环境准备Python环境确保安装Python 3.6或更高版本。在终端输入python --version检查。代码编辑器任何你熟悉的编辑器即可如VS Code、PyCharm甚至IDLE。关键库链表的核心实现不依赖任何第三方库纯Python即可。但为了可视化或测试我们可能会用到matplotlib或graphviz来画图这不是必须的。3.2 思维方式转换从“索引”到“引用”这是理解链表最困难也最重要的一步。在数组中我们通过list[3]直接访问第4个元素。在链表中没有索引。你必须从头节点head开始一个节点一个节点地“跟随”next指针走下去直到目标位置。你需要建立这样的心智模型节点Node是一个包含数据data和下一个节点的地址next的盒子。链表LinkedList是一串通过next指针连接起来的节点盒子。你只直接拥有第一个盒子头节点的地址。遍历从第一个盒子开始打开它拿到里面的next下一个盒子的地址走到下一个盒子重复此过程。插入创建一个新盒子让它的next指向原来位置的盒子然后让前一个盒子的next指向这个新盒子。删除让前一个盒子的next直接指向后一个盒子然后被跳过的那个盒子如果没有其他引用会被Python的垃圾回收器清理。4. 从零实现一个单链表我们将遵循“定义节点 - 定义链表类 - 实现基本操作”的顺序每一步都提供完整代码和逻辑图解。4.1 第一步定义节点类Node节点是链表的基石。在Python中我们用类来定义它。class ListNode: 单链表节点类 def __init__(self, data0, next_nodeNone): 初始化一个链表节点 :param data: 节点存储的数据 :param next_node: 指向下一个节点的引用默认为None表示没有下一个节点 self.data data # 节点数据域 self.next next_node # 节点指针域指向下一个节点 def __repr__(self): 自定义打印输出便于调试 return fListNode(data{self.data})关键点self.next存储的是下一个ListNode对象的引用内存地址而不是下一个节点的数据。next None表示这是链表的最后一个节点尾节点。__repr__方法不是必须的但它能让调试时节点的显示更清晰。4.2 第二步定义单链表类SinglyLinkedList与初始化链表类负责管理整个链表它通常只保存一个头节点head的引用。class SinglyLinkedList: 单链表类 def __init__(self): 初始化一个空链表 self.head None # 头指针指向链表的第一个节点。空链表时指向None。 def is_empty(self): 判断链表是否为空 return self.head is None def __repr__(self): 将链表打印为类似 list 的形式便于观察 nodes [] current self.head while current: nodes.append(repr(current.data)) # 使用节点的数据 current current.next return SinglyLinkedList([ , .join(nodes) ])初始状态创建一个SinglyLinkedList对象后其head属性为None表示这是一个空链表。4.3 第三步实现遍历与查找操作遍历是几乎所有链表操作的基础。def traverse(self): 遍历链表并打印所有元素 if self.is_empty(): print(链表为空) return current self.head while current: # 处理当前节点这里我们打印它 print(current.data, end - if current.next else \n) current current.next # 关键步骤移动到下一个节点 def length(self): 获取链表的长度节点个数 count 0 current self.head while current: count 1 current current.next return count def search(self, target_data): 在链表中查找是否存在某个值返回True或False current self.head while current: if current.data target_data: return True current current.next return False def get_node_at_index(self, index): 获取指定索引位置的节点索引从0开始 if index 0: return None current self.head current_index 0 while current and current_index index: current current.next current_index 1 return current # 如果index超出范围current会是None遍历的核心逻辑设置一个current变量初始指向head。只要current不是None就处理当前节点打印、比较等。将current更新为current.next即“走到下一个节点”。重复步骤2-3直到current为None走到链表尾部。4.4 第四步实现插入操作插入操作有三种常见情况头部插入、尾部插入、在指定节点后插入。def insert_at_head(self, data): 在链表头部插入新节点。时间复杂度 O(1)。 new_node ListNode(data) # 1. 创建新节点 new_node.next self.head # 2. 新节点指向原头节点 self.head new_node # 3. 更新头指针指向新节点 return new_node def insert_at_tail(self, data): 在链表尾部插入新节点。时间复杂度 O(n)因为需要先找到尾部。 new_node ListNode(data) if self.is_empty(): # 特殊情况空链表 self.head new_node return new_node current self.head while current.next: # 找到最后一个节点current.next为None current current.next current.next new_node # 原尾节点的next指向新节点 return new_node def insert_after_node(self, prev_node, data): 在给定的节点 prev_node 之后插入新节点。时间复杂度 O(1)。 if prev_node is None: print(给定的前一个节点不能为空) return None new_node ListNode(data) new_node.next prev_node.next # 新节点指向原prev_node的下一个节点 prev_node.next new_node # prev_node指向新节点 return new_node头部插入图解初始: head - [A|next] - [B|next] - None 步骤1: 创建新节点 [New|?] 步骤2: new.next head (即指向A) [New|next] - [A|next] - [B|next] - None 步骤3: head new head - [New|next] - [A|next] - [B|next] - None4.5 第五步实现删除操作删除操作需要找到待删除节点的前驱节点。def delete_at_head(self): 删除链表头节点。时间复杂度 O(1)。 if self.is_empty(): print(链表为空无法删除) return None deleted_node self.head self.head self.head.next # 头指针直接指向第二个节点 deleted_node.next None # 可选断开旧头节点的引用 return deleted_node.data # 返回被删除的数据 def delete_by_value(self, target_data): 删除链表中第一个值为 target_data 的节点。时间复杂度 O(n)。 if self.is_empty(): print(链表为空无法删除) return False # 特殊情况删除头节点 if self.head.data target_data: self.head self.head.next return True # 一般情况找到待删除节点的前一个节点 current self.head while current.next and current.next.data ! target_data: current current.next # 循环结束后current.next 可能是None没找到或者是待删除节点 if current.next is None: print(f未找到值为 {target_data} 的节点) return False else: # current.next 是要删除的节点 node_to_delete current.next current.next node_to_delete.next # 跳过待删除节点 # node_to_delete.next None # 可选帮助GC return True删除中间节点图解初始: ... - [Prev|next] - [Target|next] - [Next|next] - ... 目标删除Target 操作Prev.next Target.next 结果... - [Prev|next] - [Next|next] - ... [Target|next] (失去引用将被GC回收)5. 功能测试与效果验证现在让我们将上面的代码整合并运行验证链表的各项功能。我们将模拟一个简单的学生成绩管理场景。# 将前面所有类定义代码复制到这里然后运行以下测试代码 if __name__ __main__: print( 1. 创建空链表 ) score_list SinglyLinkedList() print(f链表是否为空: {score_list.is_empty()}) print(f链表内容: {score_list}) print() print( 2. 头部插入学生成绩 ) score_list.insert_at_head(85) # 插入第一个学生成绩 score_list.insert_at_head(92) # 头部插入92会成为新的第一个 score_list.insert_at_head(78) print(f链表内容: {score_list}) print(f链表长度: {score_list.length()}) print() print( 3. 尾部插入学生成绩 ) score_list.insert_at_tail(90) score_list.insert_at_tail(88) print(f链表内容: {score_list}) print() print( 4. 遍历链表 ) print(遍历输出: , end) score_list.traverse() print() print( 5. 查找操作 ) search_score 90 print(f查找成绩 {search_score}: {存在 if score_list.search(search_score) else 不存在}) search_score 100 print(f查找成绩 {search_score}: {存在 if score_list.search(search_score) else 不存在}) print() print( 6. 在指定节点后插入 ) # 假设我们要在成绩92的节点后插入一个成绩95 # 首先需要找到数据为92的节点 node_92 score_list.head.next # 已知78是头92是第二个节点 if node_92 and node_92.data 92: score_list.insert_after_node(node_92, 95) print(f在92后插入95后的链表: {score_list}) print() print( 7. 按索引访问 ) index 2 node score_list.get_node_at_index(index) print(f索引 {index} 处的节点数据是: {node.data if node else 索引无效}) print() print( 8. 删除操作 ) print(f删除头节点被删除的数据: {score_list.delete_at_head()}) print(f删除后链表: {score_list}) print(f尝试删除成绩 88: {成功 if score_list.delete_by_value(88) else 失败}) print(f删除后链表: {score_list}) print(f尝试删除不存在的成绩 100: {成功 if score_list.delete_by_value(100) else 失败}) print() print( 9. 最终状态 ) print(f链表是否为空: {score_list.is_empty()}) print(f链表长度: {score_list.length()}) print(f链表内容: {score_list}) print(遍历输出: , end) score_list.traverse()预期输出 1. 创建空链表 链表是否为空: True 链表内容: SinglyLinkedList([]) 2. 头部插入学生成绩 链表内容: SinglyLinkedList([78, 92, 85]) 链表长度: 3 3. 尾部插入学生成绩 链表内容: SinglyLinkedList([78, 92, 85, 90, 88]) 4. 遍历链表 遍历输出: 78 - 92 - 85 - 90 - 88 5. 查找操作 查找成绩 90: 存在 查找成绩 100: 不存在 6. 在指定节点后插入 在92后插入95后的链表: SinglyLinkedList([78, 92, 95, 85, 90, 88]) 7. 按索引访问 索引 2 处的节点数据是: 95 8. 删除操作 删除头节点被删除的数据: 78 删除后链表: SinglyLinkedList([92, 95, 85, 90, 88]) 尝试删除成绩 88: 成功 删除后链表: SinglyLinkedList([92, 95, 85, 90]) 尝试删除不存在的成绩 100: 失败 9. 最终状态 链表是否为空: False 链表长度: 4 链表内容: SinglyLinkedList([92, 95, 85, 90]) 遍历输出: 92 - 95 - 85 - 90通过这个测试我们验证了链表从创建、增删改查到遍历的完整生命周期。每一步操作后链表状态的变化都清晰地反映了指针next引用是如何被修改以重组链表结构的。6. 进阶技巧虚拟头节点Dummy Node观察前面的删除操作删除头节点和删除其他节点需要分开处理代码不够统一。虚拟头节点是一个常用的技巧可以简化边界条件判断。虚拟头节点是一个数据域无意义的节点它始终位于链表的最前面head指针指向它。这样原链表的第一个有效节点就变成了dummy_head.next。class SinglyLinkedListWithDummy: 使用虚拟头节点的单链表 def __init__(self): self.dummy_head ListNode(0) # 虚拟头节点数据任意这里用0 # 链表头在逻辑上是 self.dummy_head.next def insert_at_head(self, data): 在链表头部插入。现在头部是dummy_head.next new_node ListNode(data) new_node.next self.dummy_head.next self.dummy_head.next new_node # 与普通链表逻辑完全一致只是把 self.head 换成了 self.dummy_head.next def delete_by_value(self, target_data): 删除指定值的节点。无需单独处理头节点 prev self.dummy_head # 从虚拟头节点开始 while prev.next: if prev.next.data target_data: # prev.next 是要删除的节点 node_to_delete prev.next prev.next node_to_delete.next # 可以在这里返回 node_to_delete.data 或 True return True prev prev.next return False # 没找到 def traverse(self): 遍历跳过虚拟头节点 nodes [] current self.dummy_head.next # 从第一个真实节点开始 while current: nodes.append(repr(current.data)) current current.next print(SinglyLinkedListWithDummy([ , .join(nodes) ]))优势代码统一所有节点包括原第一个节点都有一个“前驱节点”插入和删除操作逻辑完全一致无需特殊判断head。减少错误避免了因忘记处理头节点而导致的空指针或逻辑错误。代价多使用了一个节点的极小内存空间。在获取链表真实头节点时需要额外一步self.dummy_head.next。在解决链表相关算法题时虚拟头节点是极其有用的工具。7. 链表 vs Python List性能实测对比理论需要实践验证。我们来设计一个简单的实验对比在列表中间频繁插入操作时链表和Python内置list的性能差异。import time def test_list_insert(n10000): 测试Python list在头部插入的性能 test_list [] start time.perf_counter() for i in range(n): test_list.insert(0, i) # 总是在头部插入这是list最慢的操作之一 end time.perf_counter() return end - start def test_linked_list_insert(n10000): 测试自实现单链表在头部插入的性能 linked_list SinglyLinkedList() start time.perf_counter() for i in range(n): linked_list.insert_at_head(i) # 链表头部插入是O(1) end time.perf_counter() return end - start if __name__ __main__: test_sizes [1000, 5000, 10000, 20000] print(操作在序列头部插入N个元素) print(次数\tPython List耗时(秒)\t链表耗时(秒)\t链表优势倍数) print(- * 70) for size in test_sizes: t_list test_list_insert(size) t_ll test_linked_list_insert(size) ratio t_list / t_ll if t_ll 0 else float(inf) print(f{size}\t{t_list:.6f}\t\t\t{t_ll:.6f}\t\t{ratio:.2f})预期结果分析 随着数据量N的增大list.insert(0, ...)操作的时间会近似呈O(N²)增长因为每次插入都需要移动所有现有元素。而我们自实现的链表insert_at_head操作是严格的O(1)耗时几乎与N成线性关系。因此在N较大时链表的性能优势会非常明显。注意Python的list是高度优化的动态数组对于尾部追加append和根据索引访问[i]操作其性能是链表无法比拟的。这个测试只是为了突出两者在设计哲学上的根本差异数组强于随机访问链表强于动态插入/删除。8. 常见问题与排查方法在实现和使用链表时初学者常会遇到以下几个典型问题问题现象可能原因排查方式解决方案遍历时进入无限循环链表存在环某个节点的next指向了之前的节点。1. 打印节点ID或使用调试器逐步执行。2. 使用“快慢指针”算法检测环。检查插入和删除逻辑确保尾节点的next始终为None且不会形成意外的循环引用。AttributeError: NoneType object has no attribute next试图访问None值的next属性。通常在while current:循环中对current.next操作前未判断current是否为空。在访问current.next或current.data之前确认current不是None。在循环条件或内部增加if current is None: break或if current:的判断。删除节点后链表长度或内容显示异常1. 删除逻辑错误未正确更新前驱节点的next指针。2. 在删除头节点时未更新链表的head指针。在删除操作后立即调用traverse()或length()函数观察链表状态。1. 画图理清指针修改顺序。2. 使用虚拟头节点统一删除逻辑。3. 单步调试观察指针变化。插入操作后新节点没有出现在预期位置1. 插入位置的前驱节点prev_node找错。2. 指针修改顺序错误导致链表断裂。在插入操作前后打印链表或使用调试器查看节点间的引用关系。牢记插入标准步骤1.new_node.next prev_node.next2.prev_node.next new_node顺序不能颠倒否则会丢失原prev_node.next的引用。内存占用过高对于极大链表Python中每个节点对象都有额外的内存开销如类型信息、引用计数。对于超大规模数据链表的内存效率低于数组。使用sys.getsizeof()粗略查看对象大小。对于海量数据考虑使用array模块或numpy数组。理解数据结构的权衡。如果数据量极大且需要频繁随机访问应优先考虑基于数组的结构。9. 最佳实践与学习建议画图是王道在纸上或白板上画出节点和指针手动模拟插入、删除过程。这是理解链表操作最直观有效的方法。先想后写在编码前务必先想清楚指针需要如何变动并用伪代码或注释描述步骤。特别是处理next指针时修改的顺序至关重要。善用虚拟头节点在解决复杂链表问题如删除所有特定值节点、链表反转、合并链表时先考虑引入虚拟头节点是否能简化逻辑。边界条件测试务必测试你的链表实现空链表时的操作插入、删除、遍历。只有一个节点的链表。操作头节点和尾节点。操作不存在的节点或值。理解Python特性在Python中变量是对象的引用。a b意味着a和b指向同一个对象。理解这一点对操作next指针至关重要。不要重复造轮子但要懂原理在实际Python开发中几乎不会自己实现链表list、collections.deque双端队列等内置数据结构已经足够优秀且高效。学习链表的目的在于理解其思想这是学习更复杂数据结构树、图和应对算法面试的基石。链表的学习是一个从抽象到具体再从具体回归抽象的过程。开始时觉得指针绕来绕去很正常多实现几遍多画几次图当你能清晰地在大脑中推演指针的变化时你就真正掌握了它。这份理解将是你学习“数据结构与算法”这门课程乃至应对更复杂编程挑战的坚实一步。建议将本文的代码自己手动敲一遍并尝试完成“反转单链表”、“检测环”、“合并两个有序链表”等经典练习巩固所学。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价