循环链表 - 首尾相连的环形动态结构

循环思考:循环链表入门

📰 5W1H 发明者故事

Who(何人)- 发明者是谁?

发明者:艾伦·纽厄尔(Allen Newell)、克利夫·肖(Cliff Shaw)和赫伯特·西蒙(Herbert Simon)
背景

  • 纽厄尔(1927-1992):人工智能先驱,图灵奖得主(1975年)
  • 肖(1922-1991):系统程序员,擅长底层实现
  • 西蒙(1916-2001):诺贝尔经济学奖得主,人类认知科学奠基人

当时的处境:在开发IPL(Information Processing Language)语言时,他们发现普通链表无法高效地表示"循环引用"结构,即一组对象互相引用、没有明确起点和终点的场景,例如棋盘上所有合法走步的循环枚举。

When(何时)- 什么时候发明的?

时间:1956年
时代背景

  • IPL-II语言正式发布,这是链表思想的直接载体
  • 人工智能的第一次"黄金时代"刚刚开始
  • 计算机内存仍以磁鼓和磁芯为主,容量极小
  • 程序员需要手工管理每一个字节的内存
  • 没有操作系统的内存管理,程序自己分配和回收空间

Where(何地)- 在哪里发明的?

地点:美国加利福尼亚州圣莫尼卡的RAND公司
环境

  • 冷战军备竞赛推动了大量基础计算研究
  • RAND公司拥有当时最先进的IBM 701计算机
  • 纽厄尔和西蒙的合作跨越了RAND公司和卡内基理工学院
  • 研究氛围自由,鼓励跨学科探索

What(何事)- 发明了什么?

数据结构:循环链表(Circular Linked List)
核心概念:在普通链表基础上,将最后一个节点的next指针指回头节点(而非NULL),形成一个"环"。就像钟表的表盘,从12点出发,沿着时针方向走,最终回到12点。
关键突破

  • 无终点遍历:从任意节点出发,都能访问到所有节点
  • 头尾等价:头节点和尾节点不再有本质区别,只需要维护一个指针
  • 循环问题建模:天然适合"轮询"、"约瑟夫环"等循环场景
  • 哨兵节点:Knuth在TAOCP中推广用虚拟头节点简化边界处理

Why(何因)- 为什么发明?

要解决的问题

  1. 循环枚举:某些算法需要反复循环遍历一组元素(如进程调度轮转)
  2. 无终点连接:普通链表的NULL尾部在循环场景中是一个"伪终点",需要特殊处理
  3. 双向访问:可以从任意节点向前追溯整个序列

当时的挑战

  • 普通链表需要额外的"尾指针"才能高效尾插;循环链表只需维护尾节点即可同时O(1)访问头和尾
  • 循环引用导致垃圾回收困难(这也推动了标记-清扫GC的发明)

动机:约瑟夫问题(Josephus Problem)是一个古老的数学题,需要循环遍历一组人并逐一淘汰。普通链表处理这类问题极其繁琐,而循环链表使之变得自然直观。

How(何果)- 如何实现?有什么影响?

实现思路

  • 最后节点的next指向头节点(而非NULL)
  • 通常维护tail指针(尾节点),通过tail->next即可得到头节点
  • 遍历时以"回到起点"作为终止条件

技术方案

循环链表示意:
┌─────────────────────────┐
↓                         │
[头:A] → [B] → [C] → [尾:D]

tail = D, tail->next = A(头节点)

历史影响

  • 操作系统的进程调度(Round-Robin算法)天然使用循环链表
  • Linux内核的list_head结构本质上是循环双向链表
  • 约瑟夫问题成为算法竞赛的经典题目,用循环链表解决优雅简洁
  • Knuth在TAOCP第1卷2.2.4节专门讨论循环链表及其与普通链表的对比

今天的使用

  • Linux内核调度器(CFS)的任务队列
  • 音频/视频播放器的循环播放列表
  • 网络协议的令牌环(Token Ring)
  • 数据库连接池的连接复用

名言:Knuth写道,“循环链表的美妙之处在于,你永远不需要判断是否到达链表末尾——因为根本没有末尾。”


📝 自然语言需求定义

需求名称:实现循环链表,支持首尾插入、任意删除、循环遍历和约瑟夫环模拟

功能需求(用精确的中文描述)

  1. 创建空循环链表:初始化一个空的循环链表

    • 输入:无
    • 操作:分配哨兵/头节点,将其next指向自身
    • 输出:循环链表指针
  2. 头部插入:在链表头部(第一个有效节点位置)插入新节点

    • 输入:链表指针,整数数据
    • 操作:创建新节点,插入到头节点之后
    • 输出:成功返回true
  3. 尾部插入:在链表尾部插入新节点

    • 输入:链表指针,整数数据
    • 操作:找到当前最后一个节点,在其后插入新节点
    • 输出:成功返回true
  4. 删除节点:删除第一个值为target的节点

    • 输入:链表指针,目标值
    • 操作:遍历找到目标节点,调整前驱指针,释放内存
    • 输出:成功返回true,未找到返回false
  5. 循环遍历:从头节点出发,打印所有节点,回到头节点停止

    • 输入:链表指针
    • 操作:从第一个有效节点遍历,走完一圈
    • 输出:打印所有数据
  6. 从任意节点出发遍历:给定一个节点指针,沿next方向遍历整圈

    • 输入:任意节点指针,头节点指针(用于判断终止)
    • 操作:沿next遍历,回到原节点停止
    • 输出:打印经过的所有节点数据
  7. 约瑟夫环:n个人围成一圈,每数到k淘汰一人,返回最后幸存者

    • 输入:人数n,间隔k
    • 操作:构建循环链表,模拟约瑟夫过程
    • 输出:最后幸存者的编号
  8. 搜索节点:查找第一个值为target的节点

    • 输入:链表指针,目标值
    • 操作:从头遍历,找到则返回节点指针
    • 输出:找到返回节点指针,未找到返回NULL
  9. 释放链表:释放所有节点内存

    • 输入:链表指针
    • 操作:遍历所有节点,逐一释放
    • 输出:无

约束条件

  • 使用虚拟头节点(哨兵节点)简化边界处理
  • 链表始终保持循环性(最后节点的next始终指向头节点)
  • 空循环链表中,哨兵节点的next指向自身
  • 所有malloc必须有对应的free
  • 遍历终止条件:回到起点节点(不是NULL)

验收标准(必须可验证)

编号 测试场景(自然语言描述) 预期结果 验证方式
1 创建空循环链表 哨兵节点的next指向自身,链表有效 检查head->next == head
2 头插1,2,3后遍历 遍历顺序为3,2,1 依次头插,验证遍历结果
3 尾插1,2,3后遍历 遍历顺序为1,2,3 依次尾插,验证遍历结果
4 从任意节点出发遍历完整圈 访问到所有节点,不多不少 从中间节点出发,计数验证
5 删除头部第一个有效节点 链表正确缩短,仍保持循环 删除后验证循环性和节点数
6 删除不存在的值 返回false,链表不变 尝试删除999,检查返回值
7 约瑟夫环n=7,k=3 最后幸存者为编号4 模拟7人数3淘汰,验证结果
8 搜索存在的值 返回正确节点指针,数据匹配 插入10,20,30,搜索20
9 搜索不存在的值 返回NULL 在链表中搜索999
10 释放后内存无泄漏 valgrind无泄漏报告 创建100节点链表,释放检测

AI 生成提示

基于以上需求和验收标准,用标准C语言实现循环链表。

要求:
1. 使用标准C99
2. 使用虚拟头节点(哨兵节点)设计,head->next是第一个有效节点
3. 包含完整错误处理(空指针、malloc失败)
4. 内存安全(所有malloc必须有free)
5. 代码必须有详细注释
6. 在main函数中实现所有10个验收标准的测试用例
7. 测试通过输出 "✓ 测试X通过",失败输出 "✗ 测试X失败"

核心函数:
- create_circular_list() - 创建空循环链表(含哨兵节点)
- insert_front(list, data) - 头部插入
- insert_back(list, data) - 尾部插入
- delete_node(list, target) - 按值删除
- traverse(list) - 循环遍历打印
- search(list, target) - 搜索节点
- josephus(n, k) - 约瑟夫环问题
- free_circular_list(list) - 释放内存

💻 C语言实现文件

对应文件: circular_linked_list.c

编译运行:

gcc -o circular_linked_list_test circular_linked_list.c
./circular_linked_list_test

# 内存泄漏检测
valgrind --leak-check=full ./circular_linked_list_test

核心函数:

  • create_circular_list() - 创建空循环链表(哨兵节点)
  • insert_front(list, data) - 头部插入
  • insert_back(list, data) - 尾部插入
  • delete_node(list, target) - 按值删除
  • traverse(list) - 循环遍历
  • search(list, target) - 搜索节点
  • josephus(n, k) - 约瑟夫环模拟
  • free_circular_list(list) - 释放链表
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐