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

你还在用头指针遍历整个链表来尾部插入吗?加上一个尾指针,时间复杂度从 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 实现(带详细注释)

python 复制代码
class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

class CircularLinkedList:
    def __init__(self):
        self.head = None   # 头指针
        self.tail = None   # 尾指针

    def is_empty(self):
        return self.head is None

    # 头部插入
    def insert_at_head(self, val):
        new_node = Node(val)
        if self.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))
    def insert_at_tail(self, val):
        new_node = Node(val)
        if self.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.tail = new_node

    # 删除头节点
    def delete_head(self):
        if self.is_empty():
            return None
        removed_val = self.head.val
        if self.head == self.tail:  # 只有一个节点
            self.head = None
            self.tail = None
        else:
            self.head = self.head.next
            self.tail.next = self.head
        return removed_val

    # 删除尾节点(需要遍历,O(n))
    def delete_tail(self):
        if self.is_empty():
            return None
        removed_val = self.tail.val
        if self.head == self.tail:  # 只有一个节点
            self.head = None
            self.tail = None
            return removed_val
        # 找到倒数第二个节点
        curr = self.head
        while curr.next != self.tail:
            curr = curr.next
        # curr 现在是倒数第二个节点
        curr.next = self.head
        self.tail = curr
        return removed_val

    # 删除第一个值为 val 的节点
    def delete_by_value(self, val):
        if self.is_empty():
            return False
        # 如果头节点就是要删除的
        if self.head.val == val:
            self.delete_head()
            return True
        # 遍历查找
        prev = self.head
        curr = self.head.next
        while curr != self.head:
            if curr.val == val:
                prev.next = curr.next
                if curr == self.tail:  # 删除的是尾节点
                    self.tail = prev
                return True
            prev = curr
            curr = curr.next
        return False

    # 遍历打印(从 head 开始,绕一圈)
    def display(self):
        if self.is_empty():
            print("空链表")
            return
        result = []
        curr = self.head
        while True:
            result.append(str(curr.val))
            curr = curr.next
            if curr == self.head:
                break
        print(" -> ".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


如果觉得有用,欢迎点赞、收藏、转发~

下期我们讲"双向循环链表的实现与应用",敬请期待!

相关推荐
2401_883600252 小时前
如何配置Oracle的外部口令存储_安全外部密码库Wallet自动登录
jvm·数据库·python
2401_897190552 小时前
如何在MongoDB中实现连表查询_group与累计求和操作
jvm·数据库·python
justjinji2 小时前
PHP源码运行是否受硬盘转速影响_7200转vs5400转对比【指南】
jvm·数据库·python
2301_796588502 小时前
如何用 error 事件全局捕获页面图片或脚本加载失败状态
jvm·数据库·python
曲幽2 小时前
FastAPI 生产环境避坑指南:用 Alembic 管理数据库迁移,别再手动改表结构了!
python·fastapi·web·async·sqlalchemy·env·alembic·migration
qq_413847402 小时前
JavaScript中利用Range对象实现复杂的文本选择操作
jvm·数据库·python
qq_654366982 小时前
Vue.js组件通信Emit处理长列表滚动到底部后的数据请求
jvm·数据库·python
用户0332126663672 小时前
使用 Python 提取 PDF 文件中的文本、表格、图片
python
qq_654366982 小时前
CSS3 按钮悬停时显示手型光标(cursor- pointer)的正确写法
jvm·数据库·python