C语言数据结构:链表进阶(环形链表、双向链表、内核链表)与队列详解

上一篇讲解了单向链表基础:结构体封装、头插、尾插、尾删、valgrind 内存泄漏检测。本篇继续拓展环形链表判环、环长、环入口、双向链表、Linux 内核链表、链式队列,配套原理推导、核心逻辑,适合期末复习、嵌入式 C 面试准备。

一、单向链表 —— 环形链表问题

单向链表尾结点不再指向NULL,指向链表内部某个结点,就形成带环链表。常见三道面试题:判断是否有环、求环的长度、求环的入口点,核心算法:快慢指针(双指针)

1. 判断链表是否有环:快慢指针法

  • 定义两个指针:pfast快指针,pslow慢指针,都从链表头出发。

  • 慢指针pslow每次走 1 步;快指针pfast每次走 2 步

  1. 如果快指针走到NULL,链表无环;

  2. 如果快慢指针在链表中相遇,链表一定存在环。

原理:环内,快指针速度大于慢指针,快指针会不断追赶慢指针,环内一定会相遇;无环链表快指针会率先抵达末尾NULL

2. 求有环链表的环长

环长:环形部分包含结点数量

  1. 快慢指针得到相遇点;

  2. 指针从相遇点开始遍历,循环计数;

  3. 当指针再次回到相遇点,统计得到结点个数就是环的长度

3. 求环形链表环的入口结点(经典数学推导)

设:

  • l:链表头到环入口的结点距离;

  • a:环入口到快慢指针相遇点距离;

  • b:相遇点回到环入口的距离; 环总长:L = a + b

相遇时:

  • 慢指针路程:s = l + a

  • 快指针路程:2s = l + a + k*(a+b),k 为快指针在环内绕的圈数

联立化简,得到关键结论:l = b

✅数学结论:链表起点到环入口距离 = 相遇点到环入口距离

算法实现步骤:

  1. 得到快慢指针相遇结点;

  2. 一个指针从相遇点出发,另一个指针从链表头部出发;

  3. 两个指针每次都只走一步;

  4. 两个指针第一次相遇的结点,就是环形链表的环入口

⚠️注意:必须两个指针都一次走一步,不能继续快慢速度。

二、双向链表

单向链表结点只有后继指针pnext,只能向后遍历;双向链表每个结点增加前驱指针ppre,既可以向后遍历,也可以向前回溯。

1. 双向链表结构体定义

//数据域,可以自定义存储任意业务数据 typedef struct stu { char name[32]; int age; int score; }Data_t; //双向链表结点:前驱+后继+数据 typedef struct dnode { Data_t data; struct dnode *ppre; //指向前驱结点 struct dnode *pnext; //指向后继结点 }DNode_t; //双向链表管理对象,封装头指针与链表长度 typedef struct dlink { DNode_t *phead; int clen; }DLink_t;

2. 双向链表优缺点

✅优点

  1. 支持正向、反向双向遍历;

  2. 已知某结点,可以直接找到它的前驱结点,单向链表必须从头遍历;

  3. 删除当前结点时,不需要遍历找前驱。

❌缺点

  1. 每个结点多一个指针域,内存开销变大

  2. 插入、删除结点,要维护两个指针(ppre、pnext),代码逻辑比单链表复杂,指针顺序容易写错。

3. 双向链表 API 接口清单

  1. 创建双向链表

  2. 结点插入(头插、尾插、按位置插入)

  3. 结点删除

  4. 结点查找

  5. 结点数据修改

  6. 正向 / 反向遍历链表

  7. 链表销毁(循环 free 所有结点,释放链表对象,防止内存泄漏)

💡易错提醒:双向链表插入删除,要同时修改新结点、相邻结点的pprepnext,指针赋值顺序不能乱。

三、Linux 内核链表

内核链表本质:双向循环链表,Linux 内核大量使用。

和普通双向链表核心区别

  1. 普通链表:结点内部包含数据域结点把业务数据直接包在结构体里面,一旦写死数据类型,链表就只能存这一种数据。如果要存新的数据类型,需要重新写一套链表代码。

  2. 内核链表:链表节点嵌入业务结构体

链表的pprepnext指针不包裹数据;把链表小结构体,嵌入到你自己业务结构体内部。 同一套链表代码,可以挂载任意不同类型业务结构体,代码复用性极强。

核心两个内核宏:

  1. offsetof(TYPE, MEMBER)获取结构体成员,距离结构体起始地址的字节偏移量

  2. container_of(ptr, type, member)已知嵌入的链表成员地址,结合偏移量,反向得到整个业务结构体的首地址

工作流程:通过链表指针 → container_of + offsetof → 拿到外层完整业务结构体。

内核链表没有 data 域,只负责串起结构体;业务数据放在外层结构体。一套链表算法适配多种数据类型,驱动、内核模块广泛使用。

四、队列(Queue)

1. 队列基础概念

队列是线性结构,特性:FIFO 先进先出

  • 队尾:执行插入操作,叫入队

  • 队头:执行删除操作,叫出队

类比排队:先排队的人,先离开队伍。

2. 队列分类

  1. 顺序队列:数组实现,存在假溢出问题,一般优化为循环队列

  2. 链式队列:链表实现,本篇重点

链式队列管理结构体,一般保存:

  • phead:队头指针(出队在这里删结点)

  • ptail:队尾指针(入队在这里新增结点)

  • clen:当前队列元素个数

3. 链式队列核心操作

  1. 入队(队尾插入结点)新结点加到ptail后面,更新队尾指针ptail指向新结点;队列为空时,pheadptail都指向新结点。

  2. 出队(队头删除结点)删除phead指向的队头结点,更新队头指针; ⚠️边界:删除之后队列为空,需要将ptailNULL,避免野指针。

4. 队列 API

  1. 创建队列

  2. 入队

  3. 队列遍历

  4. 判断队列是否为空

  5. 出队

  6. 获取队头元素(只读取,不删除)

  7. 销毁队列:释放全部结点、释放队列管理对象

5. 队列典型应用场景

数据缓冲、任务排队、消息队列,生产者消费者模型。

知识点总结思维导图

  1. 环形单向链表

  • 判环:快慢指针;有环则相遇,无环 fast 走到 NULL

  • 环长:相遇点循环计数回到原点

  • 环入口:头指针、相遇点指针,同速步进,相遇即入口,数学推导l=b

  1. 双向链表每个结点:ppre前驱指针 +pnext后继指针;双向遍历;插入删除维护两组指针,内存开销增大。

  2. 内核双向循环链表链表结点嵌入业务结构体;offsetof求偏移,container_of反向获取结构体首地址;一套链表操作支持多种数据类型。

  3. 队列 FIFO 先进先出队尾入队,队头出队;链式队列维护头指针、尾指针;常用于缓冲、消息队列。


拓展思考(面试常考)

  1. 快慢指针为什么快指针每次走 2 步,走 3 步行不行?

可以,但 2 步是最简单;步长过大,会增加错过相遇的概率,2 步是最优。

  1. 双向链表删除结点相比单向链表优势?

单向链表删除当前结点,需要从头遍历找前驱;双向链表直接node->ppre拿到前驱。

  1. 内核链表相比普通链表最大优势是什么?

代码复用,不需要为每种数据类型重写一套链表插入删除。

  1. 链式队列出队后,什么时候 ptail 要置 NULL?

删除之后队列变空,如果不置空,ptail 会变成野指针,下次入队会产生逻辑错误。下一篇可以完整实现:环形链表判环代码、双向链表全套接口、内核链表模拟实现、链式队列完整 C 代码。