链表 - 用指针连接的动态线性结构

揭开链条的秘密:链表入门指南

📰 5W1H 发明者故事

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

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

  • 纽厄尔(1927-1992):物理学家出身,后来转向人工智能,RAND公司研究员
  • 肖(1922-1991):计算机科学家,与纽厄尔合作开发程序
  • 西蒙(1916-2001):诺贝尔经济学奖得主,认知科学和人工智能先驱

当时的处境:1950年代中期,三人在RAND公司和卡内基理工学院合作,试图用计算机模拟人类的问题解决过程。他们需要一个能表示动态变化的符号结构的数据结构。

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

时间:1955-1956年
时代背景:

  • 计算机内存增长到可以存储数千个"字"(word)
  • FORTRAN语言刚诞生,但主要用于数值计算
  • 人工智能作为一个领域刚刚被命名(1956年达特茅斯会议)
  • IPL(Information Processing Language)语言正在开发中
  • 还没有"数据结构"这个术语,计算机科学作为独立学科尚未形成

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

地点:

  • 美国加利福尼亚州圣莫尼卡的RAND公司(研究与发展公司)
    环境:
  • 冷战时期的美国,政府资助大量军事和基础研究
  • RAND是一个智库,聚集了顶尖的科学家和数学家
  • 纽厄尔和西蒙共用一间办公室,经常讨论到深夜
  • 计算机是IBM 701,需要通过穿孔卡片输入程序

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

数据结构:链表(Linked List)
核心概念:不像数组那样把所有元素排成一排存在一起,链表让每个元素记住"下一个在哪里"。就像寻宝游戏,每个线索指向下一个线索的位置。
关键突破:

  • 指针概念:不直接存数据本身,而是存"去哪找数据"的地址
  • 动态增长:不需要预先知道要存多少元素,需要时就分配新节点
  • 灵活连接:节点可以插入、删除、重排,不需要移动其他元素
  • 表处理语言(List Processing):专门为这种结构设计的IPL语言

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

要解决的问题:

  1. 逻辑表达式表示:在定理证明程序(Logic Theorist)中,需要表示长度不定的逻辑表达式
  2. 动态数据增长:数组必须预先定义大小,但定理证明过程中符号表达式的长度根本无法预测
  3. 符号操作:不同于数值计算,AI需要操作符号、表达式、树状结构

当时的挑战:

  • IBM 701只有4096个字(word)的内存
  • 没有高级语言,需要用汇编操作内存地址
  • 每次内存分配都需要程序员手动管理
  • 没有垃圾回收,内存泄漏是严重问题

动机:纽厄尔在做定理证明时意识到,人类的推理过程涉及大量临时假设和回溯,需要一种能动态增长、灵活连接的存储方式。数组的连续存储方式太过僵硬。

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

实现思路:

  • 每个节点分成两部分:数据区 + 指针区
  • 数据区存储实际内容
  • 指针区存储下一个节点的内存地址
  • 用NULL(或0)表示链表的结束
  • 用一个头指针记住链表的开始位置

技术方案:

内存布局示例:
地址100: [数据A] [地址200]  --→  
地址200: [数据B] [地址300]  --→  
地址300: [数据C] [NULL]     --→  结束

头指针 = 地址100

历史影响:

  • 链表是动态数据结构的开端,开启了计算机科学的新篇章
  • 启发了Lisp语言(1958年),至今仍是AI领域的重要语言
  • 成为更复杂结构(树、图、哈希表)的基础组件
  • 今天的几乎所有编程语言都支持链表或类似的引用结构

今天的使用:

  • 操作系统内核的进程管理
  • 内存分配器的空闲块管理
  • 文件系统的目录结构
  • 高级语言的垃圾回收实现
  • 数据库的索引结构(B+树等)

名言:纽厄尔后来说,“我们当时并没有意识到自己在发明一种基础数据结构,我们只是想解决定理证明的问题。”


📝 自然语言需求定义

需求名称:实现单向链表,支持动态插入、删除和遍历

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

  1. 创建节点:创建一个新节点,存储指定数据

    • 输入:整数数据
    • 操作:分配内存,存储数据,next指针置为NULL
    • 输出:新节点指针,分配失败返回NULL
  2. 头插法插入:在链表头部插入新节点

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

    • 输入:链表头指针的指针,整数数据
    • 操作:如果链表为空则头插;否则遍历到最后,新节点接在最后
    • 输出:成功返回true
  4. 按值删除:删除第一个值为指定数据的节点

    • 输入:链表头指针的指针,目标值
    • 操作:找到该节点,调整前驱的next指针,释放该节点内存
    • 输出:成功删除返回true,未找到返回false
  5. 按位置查找:获取第N个节点的数据(从0开始计数)

    • 输入:链表头指针,位置索引
    • 操作:从头遍历N次
    • 输出:成功返回true,数据通过指针返回;越界返回false
  6. 遍历打印:遍历并打印所有节点数据

    • 输入:链表头指针
    • 操作:从头开始,依次访问每个节点,打印数据
    • 输出:无
  7. 释放链表:释放整个链表的所有节点内存

    • 输入:链表头指针
    • 操作:依次释放每个节点,避免内存泄漏
    • 输出:无

约束条件

  • 使用单向指针(只指向下一个节点)
  • 头指针可能为NULL(空链表)
  • 所有malloc必须有对应的free
  • 删除节点时必须正确处理头节点的情况
  • 位置索引从0开始,越界必须返回错误

验收标准(必须可验证)

编号 测试场景(自然语言描述) 预期结果 验证方式
1 创建包含单个节点10的链表 链表非空,头节点数据为10 头插10,检查头指针和数据
2 头插法插入10,20,30 链表顺序为30→20→10 依次头插,遍历验证顺序
3 尾插法插入10,20,30 链表顺序为10→20→30 依次尾插,遍历验证顺序
4 删除头节点(值为30) 新头节点为20,链表大小减1 删除后遍历验证
5 删除中间节点(值为20) 链表变为10→30 删除后遍历验证
6 删除不存在的值 返回false,链表不变 尝试删除100,检查返回值和链表
7 查找第1个节点 返回20 创建10→20→30,查找索引1
8 查找越界位置 返回false 尝试查找索引10
9 释放链表后无内存泄漏 valgrind显示无泄漏 创建长链表,释放,valgrind检测

AI 生成提示

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

要求:
1. 使用标准C99
2. 节点结构体:包含int data和struct Node* next
3. 包含完整错误处理(空指针、malloc失败、越界)
4. 内存安全(所有malloc必须有free)
5. 代码必须有详细注释
6. 在main函数中实现所有9个验收标准的测试用例
7. 测试通过输出 "✓ 测试X通过",失败输出 "✗ 测试X失败"

核心函数:
- create_node(data) - 创建节点
- insert_head(&head, data) - 头插
- insert_tail(&head, data) - 尾插
- delete_by_value(&head, value) - 按值删除
- get_by_index(head, index, &value) - 按位置查找
- traverse(head) - 遍历打印
- free_list(head) - 释放链表

💻 C语言实现文件

对应文件: linked_list.c

编译运行:

gcc -o linked_list_test linked_list.c
./linked_list_test

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

核心函数:

  • create_node(data) - 创建节点
  • insert_head(&head, data) - 头插法
  • insert_tail(&head, data) - 尾插法
  • delete_by_value(&head, value) - 按值删除
  • get_by_index(head, index, &value) - 按位置查找
  • traverse(head) - 遍历打印
  • free_list(head) - 释放链表
Logo

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

更多推荐