news 2026/4/21 3:35:16

单向循环链表 + 尾指针:让插入删除更高效的秘密武器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单向循环链表 + 尾指针:让插入删除更高效的秘密武器

你还在用头指针遍历整个链表来尾部插入吗?加上一个尾指针,时间复杂度从 O(n) 直接降到 O(1)!

今天我们来聊一个链表中的“小优化大智慧”——单向循环链表配合尾指针。别看只是多存了一个指针,它能让尾部插入、头部删除、链表拼接等操作变得异常高效。


一、什么是单向循环链表?

单向循环链表:在普通单向链表的基础上,将最后一个节点的next指针指向头节点(而不是None),形成一个闭环。

尾指针:除了传统的head指针外,再维护一个tail指针,始终指向链表的最后一个节点。

为什么加尾指针?

  • 没有尾指针时,要在尾部插入新节点,需要从头遍历到尾部,O(n)。
  • 有了尾指针,直接tail.next = new_node,然后更新tail,O(1)。

二、结构图示

普通单向链表(带头指针)

head → [A] → [B] → [C] → None

尾部插入需要遍历到C才能操作。

单向循环链表(带尾指针)

head → [A] → [B] → [C] ──┐ ↑ │ └────────────┘ tail ────────────────────┘
  • tail.next指向head,形成环。
  • 插入、删除时,同时维护headtail

三、插入操作详解(带尾指针)

1. 头部插入(在第一个节点之前)

步骤

  1. 创建新节点new_node
  2. new_node.next = head(新节点指向原头节点)。
  3. tail.next = new_node(尾节点的 next 指向新头)。
  4. head = new_node(更新头指针)。

时间复杂度:O(1)

图示

原链表:head → [A] → [B] → [C] ─┐ tail ────────────────────┘ 插入 new 到头部后: head → [new] → [A] → [B] → [C] ─┐ tail ────────────────────┘

2. 尾部插入(在最后一个节点之后)

步骤

  1. 创建新节点new_node
  2. new_node.next = head(新节点指向头,保持循环)。
  3. tail.next = new_node(原尾节点指向新节点)。
  4. tail = new_node(更新尾指针)。

时间复杂度:O(1) —— 因为直接通过tail定位。

图示

原链表:head → [A] → [B] → [C] ─┐ tail ────────────────────┘ 插入 new 到尾部: head → [A] → [B] → [C] → [new] ─┐ tail ────────────────────┘

3. 中间插入(已知某节点之后)

与普通单向链表一样,需要先找到插入位置的前驱节点(O(n)),然后修改指针,并注意如果插入位置是尾部,需要更新tail


四、删除操作详解(带尾指针)

1. 删除头节点

步骤

  1. 如果链表只有一个节点:head == tail,则删除后链表为空,设置head = tail = None
  2. 否则:
    • head = head.next(移动头指针)。
    • tail.next = head(保持循环)。

时间复杂度:O(1)

图示

删除前:head → [A] → [B] → [C] ─┐ tail ────────────────────┘ 删除后:head → [B] → [C] ─┐ tail ─────────────┘

2. 删除尾节点

关键:删除尾节点需要找到它的前驱节点(倒数第二个节点),因为单链表无法直接获取前驱。所以即使有tail指针,删除尾节点仍需要遍历到倒数第二个节点,时间复杂度 O(n)。

步骤

  1. head开始遍历,找到节点prev使得prev.next == tail
  2. prev.next = head(跳过尾节点,指向头)。
  3. tail = prev(更新尾指针)。
  4. 如果链表只有一个节点,则删除后置空。

时间复杂度:O(n)

特殊优化:如果经常需要删除尾节点,可以考虑使用双向循环链表,那样可以 O(1) 删除尾部。

3. 删除中间节点

需要先找到前驱节点(O(n)),然后prev.next = curr.next。注意如果删除的是最后一个节点(即curr == tail),需要更新tailprev


五、时间复杂度总结表

操作单向循环链表(带尾指针)普通单向链表(仅头指针)
头部插入O(1)O(1)
尾部插入O(1)O(n)
中间插入(已知前驱)O(1)O(1)
头部删除O(1)O(1)
尾部删除O(n)O(n)(需遍历到倒数第二)
中间删除(已知前驱)O(1)O(1)
查找元素O(n)O(n)

结论:尾指针主要优化了尾部插入操作,从 O(n) 降为 O(1)。


六、Python 实现(带详细注释)

classNode:def__init__(self,val):self.val=val self.next=NoneclassCircularLinkedList:def__init__(self):self.head=None# 头指针self.tail=None# 尾指针defis_empty(self):returnself.headisNone# 头部插入definsert_at_head(self,val):new_node=Node(val)ifself.is_empty():self.head=new_node self.tail=new_node new_node.next=new_node# 指向自己,形成循环else:new_node.next=self.head self.tail.next=new_node self.head=new_node# 尾部插入(O(1))definsert_at_tail(self,val):new_node=Node(val)ifself.is_empty():self.head=new_node self.tail=new_node new_node.next=new_nodeelse:new_node.next=self.head self.tail.next=new_node self.tail=new_node# 删除头节点defdelete_head(self):ifself.is_empty():returnNoneremoved_val=self.head.valifself.head==self.tail:# 只有一个节点self.head=Noneself.tail=Noneelse:self.head=self.head.nextself.tail.next=self.headreturnremoved_val# 删除尾节点(需要遍历,O(n))defdelete_tail(self):ifself.is_empty():returnNoneremoved_val=self.tail.valifself.head==self.tail:# 只有一个节点self.head=Noneself.tail=Nonereturnremoved_val# 找到倒数第二个节点curr=self.headwhilecurr.next!=self.tail:curr=curr.next# curr 现在是倒数第二个节点curr.next=self.head self.tail=currreturnremoved_val# 删除第一个值为 val 的节点defdelete_by_value(self,val):ifself.is_empty():returnFalse# 如果头节点就是要删除的ifself.head.val==val:self.delete_head()returnTrue# 遍历查找prev=self.head curr=self.head.nextwhilecurr!=self.head:ifcurr.val==val:prev.next=curr.nextifcurr==self.tail:# 删除的是尾节点self.tail=prevreturnTrueprev=curr curr=curr.nextreturnFalse# 遍历打印(从 head 开始,绕一圈)defdisplay(self):ifself.is_empty():print("空链表")returnresult=[]curr=self.headwhileTrue:result.append(str(curr.val))curr=curr.nextifcurr==self.head:breakprint(" -> ".join(result)+" -> (回到头)")# 测试if__name__=="__main__":cll=CircularLinkedList()cll.insert_at_tail(10)cll.insert_at_tail(20)cll.insert_at_head(5)cll.display()# 5 -> 10 -> 20 -> (回到头)cll.delete_head()cll.display()# 10 -> 20 -> (回到头)cll.insert_at_tail(30)cll.display()# 10 -> 20 -> 30 -> (回到头)cll.delete_tail()cll.display()# 10 -> 20 -> (回到头)cll.delete_by_value(20)cll.display()# 10 -> (回到头)

七、图解辅助理解(ASCII 艺术)

插入尾部(带尾指针)

初始状态(只有一个节点 5): head ──→ [5] ←── tail ↑ │ └─┘ 插入 10 到尾部: head ──→ [5] → [10] ←── tail ↑ │ └───────────┘

尾指针直接让5.next = 1010.next = headtail = 10,一步到位。

删除尾部

删除前: head ──→ [5] → [10] → [20] ←── tail ↑ │ └─────────────────┘ 删除尾节点 20: 需要找到前驱节点 10: head ──→ [5] → [10] ←── tail ↑ │ └────────┘

10.next = headtail = 10


八、实际应用场景

  • 约瑟夫环问题:循环链表天然适合模拟围成一圈的人。
  • 操作系统进程调度:时间片轮转调度算法中,就绪队列常用循环链表。
  • 缓冲池/对象池:需要循环利用固定数量资源时。
  • 游戏开发中的回合制战斗:角色按顺序行动,循环链表可轻松实现。

九、总结

  • 单向循环链表 + 尾指针的核心优势是尾部插入 O(1)
  • 头部插入/删除、尾部插入都是 O(1),但尾部删除仍然是 O(n)。
  • 空间上只多了一个指针,换来了尾部操作的高效。
  • 适合需要频繁在尾部添加元素的场景(如消息队列、日志收集)。

思考题:如果既要尾部插入 O(1),又要尾部删除 O(1),应该使用什么数据结构?
(提示:双向循环链表,或者用 Python 的collections.deque


如果觉得有用,欢迎点赞、收藏、转发~
下期我们讲“双向循环链表的实现与应用”,敬请期待!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 3:29:21

MapGIS转Shapefile一键转换工具|高效精准地理数据格式迁移方案

温馨提示:文末有联系方式一、MapGIS数据秒变Shapefile:极简操作,零学习成本 本工具专为GIS用户设计,实现MapGIS标准格式(如WL、WP、WT、WV等)到ESRI Shapefile(.shp)的全自动、一键式…

作者头像 李华
网站建设 2026/4/21 3:27:18

Unity入门

目录 一、项目 1.1 创建新项目 1.2 目录含义 1.3 添加项目 二、游戏场景 2.1 新建游戏场景 2.2 保存游戏场景 2.3 本质 三、窗口 3.1 窗口布局 3.2 Hierarchy 窗口 3.3 Scene 窗口 3.3.1 常用工具 3.3.2 世界坐标轴 3.3.3 快捷键 3.4 Game 窗口 3.5 Project 窗…

作者头像 李华
网站建设 2026/4/21 3:23:57

专家视角看JVM是如何让所有高速运行的线程“瞬间静止”

专家视角看JVM是如何让所有高速运行的线程“瞬间静止”前言当 GC 发生时,JVM 是如何让所有正在高速运行的线程“瞬间静止”的1. 核心指挥官:SafepointSynchronize::begin()2. 线程如何感应:主动轮询机制(Polling)A. 解…

作者头像 李华
网站建设 2026/4/21 3:22:14

园区网络实验作业

一、实验拓扑二、实验要求需求: 1、按照图示的VLAN及IP地址需求,完成相关配置 2、要求SW1为VLAN 2/3的主根及主网关SW2为vlan 20/30的主根及主网关SW1和SW2互为备份 3、可以使用super vlan(但是会出现双主)可忽略 4、上层通过静态…

作者头像 李华