循环链表
循环单链表把最后一个节点的 next 指回第一个节点,而不是 null。本课同时保存 head、tail,维护非空时的不变量 tail.next == head。
沿 next 前进会回到起点,不会遇到链尾空引用。因此遍历不能等 null,而要在完成一圈、curr == head 时停止。漏掉这个条件,搜索不存在的值就会无限循环。
循环链表如何工作
可以把节点看成围成一圈的接力队员,每人只知道下一位。跑道没有终点标志,需要识别回到起点的时刻。
insert_head 与 insert_tail 都是 O(1),因为保存了两端。头插让新节点指向旧头,再更新 head 与 tail.next。尾插让新节点指向头,旧尾指向新节点,再推进 tail。每次操作都重新满足 tail.next = head,画布的回绕箭头显示这次变化。尾插无需遍历的原因是 tail,单链表也可保存尾指针。
search 从头逐个比较。命中时在环中返回索引;未命中时从尾部绕回头,推进后检查 curr == head,返回未找到。成功与失败的停止原因不同。
delete 用同样的一圈边界定位目标和前驱,再让前驱跳过目标。查找为 O(n),改线为 O(1)。删除头时修改回绕目标,删除尾时把 tail 退到前驱,删除唯一节点时把环清空。
容量滑块 4–10、默认 7,是画布限制,代码以 max 对应;链表结构本身没有固定容量。
逐步演示
从空环开始:
- 头插 5:
5.next = 5,形成自环,head、tail都指向 5。状态5 ↺。 - 头插 8:
8.next = 5,再令旧尾5.next = 8,head改为 8。状态8 → 5 → 回到 8。 - 尾插 3:
tail已在 5。令3.next = 8、5.next = 3,tail改为 3。状态8 → 5 → 3 → 回到 8。 - 尾插 7:令
7.next = 8、3.next = 7,tail改为 7。状态8 → 5 → 3 → 7 → 回到 8。 - 搜索 3:索引 0 比较
8 ≠ 3,索引 1 比较5 ≠ 3,索引 2 比较3 = 3,返回 2,在环中停止。 - 搜索 9:依次比较
8 ≠ 9、5 ≠ 9、3 ≠ 9、7 ≠ 9。游标越过尾部,沿回绕箭头回到头。curr == head成立,停止并返回未找到,全程没有遇到 null。
画布把普通 next 放在节点行中,尾到头的回绕线放在下方。失败搜索的游标沿弧线返回起点,可以看到完整的一圈。
时间与空间复杂度
| 操作 | 成本 | 原因 |
|---|---|---|
| 索引访问 | O(n) | 从头沿 next 前进 |
| 头插 | O(1) | 新节点接旧头,更新回绕 |
| 尾插 | O(1) | 已有 tail |
| 按值删除 | O(n) | 最多找一圈,再常数次改线 |
| 搜索 | O(n) | 命中或走完一圈 |
| 空间 | O(n) | 每节点一个值、一个指针 |
每节点存储与单链表相同,另有链表级别的尾指针。环形结构适合轮转调度或循环缓冲的链式实现;其代价是必须维护环和停止条件。循环缓冲也可用数组实现,不必使用链表。
边界情况
- 单节点环:
node.next = node,head == tail,画布显示自环。 - 删除头节点:多节点时先
head = node.next,再tail.next = head。 - 删除尾节点:多节点时先
tail = prev,再tail.next = head。 - 删除唯一节点:
head、tail一起清空,不能继续维护指向旧节点的环。 - 值不存在:恰好遍历一圈,回到头后返回
-1/False,不等待 null。 - 空环搜索:
if not self.head直接返回,不进入遍历。
常见错误
- 用空引用停止。 非空环中的
while curr:、while curr.next:不会结束。应先处理和推进,再检查是否回到head。默认搜索 9 就是检验这个条件。 - 回绕指针过期。 插入或删除端点后要恢复
tail.next = head。漏接旧尾可能断链,也可能形成跳过新节点的环。 - 删尾后不更新
tail。 后续插入会连接到已移出环的节点。 - 忽略唯一节点。
head == tail时,删除要清空两者;空环插入的第一节点要指向自己。假设总有两个节点会破坏这个场景。