在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

循环链表

循环单链表把最后一个节点的 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 对应;链表结构本身没有固定容量。

逐步演示

从空环开始:

  1. 头插 5:5.next = 5,形成自环,head、tail 都指向 5。状态 5 ↺。
  2. 头插 8:8.next = 5,再令旧尾 5.next = 8,head 改为 8。状态 8 → 5 → 回到 8。
  3. 尾插 3:tail 已在 5。令 3.next = 8、5.next = 3,tail 改为 3。状态 8 → 5 → 3 → 回到 8。
  4. 尾插 7:令 7.next = 8、3.next = 7,tail 改为 7。状态 8 → 5 → 3 → 7 → 回到 8。
  5. 搜索 3:索引 0 比较 8 ≠ 3,索引 1 比较 5 ≠ 3,索引 2 比较 3 = 3,返回 2,在环中停止。
  6. 搜索 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 时,删除要清空两者;空环插入的第一节点要指向自己。假设总有两个节点会破坏这个场景。