【嵌入式 C 语言实战】链表进阶全攻略:头插法 / 排序 / 循环链表 / 双向链表(附完整代码)
大家好,我是学嵌入式的小杨同学。在上一篇博客中,我们掌握了单链表的基础操作(尾插、查询、删除等),但嵌入式开发中,仅基础单链表无法满足所有场景 —— 比如需要快速在表头插入数据、对链表排序、实现环形数据存储等。今天就结合资料,深入讲解链表的进阶用法:头插法、链表排序、循环链表、双向链表,附完整实战代码,帮你解锁链表的全部核心技能,应对更复杂的嵌入式数据存储需求。
一、进阶基础:先理清核心前提
在开始进阶操作前,先回顾链表的核心本质,避免遗忘关键知识点:
- 存储逻辑:链式存储不依赖连续内存,通过指针串联节点,解决数组扩容难题;
- 节点结构:由 “数据域(存业务数据)+ 指针域(指向下一节点)” 组成,通过结构体定义;
- 内存管理:依赖
malloc(开辟节点)、memset/bzero(初始化)、free(释放节点),嵌入式开发中必须严格管理内存,避免泄漏; - 基础设计:本文所有进阶操作均基于 “带头节点” 的链表(头节点仅用于标识起始位置,不存有效数据),简化边界判断。
二、进阶操作 1:头插法(O (1) 高效新增节点)
基础单链表的 “尾插法” 需要遍历到链表末尾,时间复杂度 O (n);而 “头插法” 直接在头节点后插入新节点,时间复杂度 O (1),适用于需要 “先进后出” 的场景(如栈、串口数据缓存)。
1. 头插法核心逻辑
- 创建新节点并填充数据、初始化指针;
- 新节点的指针域指向头节点当前的后续节点(即原链表的第一个有效节点);
- 头节点的指针域指向新节点,完成插入。
2. 实战代码
c
运行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 单链表节点结构体
struct Node {
int data; // 数据域
struct Node *pnext;// 指针域
};
// 初始化链表(返回头节点)
struct Node *initLinkList() {
struct Node *phead = (struct Node *)malloc(sizeof(struct Node));
if (phead == NULL) {
printf("malloc error:头节点创建失败\n");
exit(0);
}
phead->pnext = NULL; // 头节点初始无后续节点
return phead;
}
// 创建单个节点
struct Node *createNode(int data) {
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
if (newNode == NULL) {
printf("malloc error:节点创建失败\n");
return NULL;
}
memset(newNode, 0, sizeof(struct Node)); // 初始化节点内存
newNode->data = data;
newNode->pnext = NULL;
return newNode;
}
// 头插法新增节点(核心进阶函数)
void addNodeHead(struct Node *phead, int data) {
if (phead == NULL) {
printf("链表未初始化\n");
return;
}
struct Node *newNode = createNode(data);
if (newNode == NULL) return;
// 核心步骤:新节点链接原链表,头节点链接新节点
newNode->pnext = phead->pnext;
phead->pnext = newNode;
printf("头插节点成功:%d\n", data);
}
// 打印链表(验证结果)
void printLinkList(struct Node *phead) {
if (phead == NULL || phead->pnext == NULL) {
printf("链表为空\n");
return;
}
struct Node *ptemp = phead->pnext;
printf("链表节点:");
while (ptemp != NULL) {
printf("%d → ", ptemp->data);
ptemp = ptemp->pnext;
}
printf("NULL\n");
}
3. 测试效果
c
运行
int main() {
struct Node *phead = initLinkList();
// 头插法新增3个节点
addNodeHead(phead, 10);
addNodeHead(phead, 20);
addNodeHead(phead, 30);
printLinkList(phead); // 输出:30 → 20 → 10 → NULL
return 0;
}
关键特点:头插法插入的节点始终在链表头部,实现 “后插先出”,效率远高于尾插法,是嵌入式高频使用的技巧。
三、进阶操作 2:链表排序(冒泡排序实战)
链表存储的无序数据(如传感器采集的随机值),需通过排序使其有序,嵌入式开发中常用 “冒泡排序”(简单易实现、无需额外空间),核心是通过指针遍历对比相邻节点数据。
1. 链表冒泡排序逻辑
- 外层循环控制排序轮次(最多 n-1 轮,n 为链表长度);
- 内层循环遍历链表,对比相邻节点的数据;
- 若前一节点数据大于后一节点,交换两者数据域;
- 优化:添加标志位,若某轮无数据交换,说明链表已有序,直接退出循环。
2. 实战代码
c
运行
// 获取链表长度(排序需用到)
int getLinkListLength(struct Node *phead) {
if (phead == NULL || phead->pnext == NULL) return 0;
int len = 0;
struct Node *ptemp = phead->pnext;
while (ptemp != NULL) {
len++;
ptemp = ptemp->pnext;
}
return len;
}
// 链表冒泡排序(升序)
void sortLinkList(struct Node *phead) {
int len = getLinkListLength(phead);
if (len <= 1) {
printf("链表长度≤1,无需排序\n");
return;
}
struct Node *p1 = NULL; // 外层循环指针(控制轮次)
struct Node *p2 = NULL; // 内层循环指针(对比相邻节点)
int temp = 0; // 临时变量,用于交换数据
int isSorted = 0; // 标志位:1表示已有序
for (p1 = phead->pnext; p1 != NULL && !isSorted; p1 = p1->pnext) {
isSorted = 1; // 假设本轮有序
for (p2 = phead->pnext; p2->pnext != NULL; p2 = p2->pnext) {
// 前一节点数据大于后一节点,交换数据
if (p2->data > p2->pnext->data) {
temp = p2->data;
p2->data = p2->pnext->data;
p2->pnext->data = temp;
isSorted = 0; // 有交换,说明未有序
}
}
}
printf("链表排序完成\n");
}
3. 测试效果
在main函数中添加排序调用:
c
运行
// 新增无序节点(尾插法,代码略,可参考基础篇)
addNodeTail(phead, 5);
addNodeTail(phead, 3);
addNodeTail(phead, 8);
printf("排序前:");
printLinkList(phead); // 输出:5 → 3 → 8 → NULL
sortLinkList(phead);
printf("排序后:");
printLinkList(phead); // 输出:3 → 5 → 8 → NULL
四、进阶结构 1:循环链表(环形数据存储)
循环链表是单链表的变种,尾节点的指针域不指向NULL,而是指向头节点,形成闭合环形,适用于需要 “循环遍历”“环形缓存” 的场景(如任务调度、数据轮转存储)。
1. 循环链表核心特点
- 尾节点指针域 = 头节点地址,无 “NULL” 终止符;
- 遍历无边界,需通过 “回到头节点” 判断结束;
- 新增、删除逻辑与单链表类似,仅尾节点判断条件改变(
ptemp->pnext != phead)。
2. 实战代码(核心操作)
c
运行
// 初始化循环链表(尾节点指向头节点)
struct Node *initCycleList() {
struct Node *phead = (struct Node *)malloc(sizeof(struct Node));
if (phead == NULL) {
printf("malloc error:头节点创建失败\n");
exit(0);
}
phead->pnext = phead; // 尾节点(初始头节点即尾节点)指向自身
return phead;
}
// 循环链表尾插法新增节点
void addCycleNodeTail(struct Node *phead, int data) {
if (phead == NULL || phead->pnext != phead) {
printf("循环链表未初始化\n");
return;
}
struct Node *newNode = createNode(data);
if (newNode == NULL) return;
// 找到尾节点(pnext == phead的节点)
struct Node *ptemp = phead;
while (ptemp->pnext != phead) {
ptemp = ptemp->pnext;
}
// 新节点指向头节点,尾节点指向新节点
newNode->pnext = phead;
ptemp->pnext = newNode;
printf("循环链表新增节点:%d\n", data);
}
// 打印循环链表(遍历1圈即可)
void printCycleList(struct Node *phead) {
if (phead == NULL || phead->pnext == phead) {
printf("循环链表为空\n");
return;
}
struct Node *ptemp = phead->pnext;
printf("循环链表节点:");
while (ptemp != phead) { // 回到头节点即结束
printf("%d → ", ptemp->data);
ptemp = ptemp->pnext;
}
printf("(回到头节点)\n");
}
五、进阶结构 2:双向链表(前后双向遍历)
双向链表的每个节点除了 “后继指针”(pnext),还增加 “前驱指针”(pbefore),支持向前、向后双向遍历,适用于需要频繁查找前驱 / 后继节点的场景(如文件系统目录、双向缓存)。
1. 双向链表节点结构与核心逻辑
(1)节点结构体定义
c
运行
// 双向链表节点结构体
struct DoubleNode {
int data; // 数据域
struct DoubleNode *pbefore; // 前驱指针(指向前一节点)
struct DoubleNode *pnext; // 后继指针(指向后一节点)
};
(2)核心操作逻辑(以尾插为例)
- 创建新节点,初始化
pbefore和pnext为NULL; - 找到尾节点(
pnext == NULL); - 尾节点的
pnext指向新节点,新节点的pbefore指向尾节点; - 新节点的
pnext置NULL,完成插入。
2. 实战代码(核心操作)
c
运行
// 初始化双向链表
struct DoubleNode *initDoubleList() {
struct DoubleNode *phead = (struct DoubleNode *)malloc(sizeof(struct DoubleNode));
if (phead == NULL) {
printf("malloc error:头节点创建失败\n");
exit(0);
}
phead->pbefore = NULL;
phead->pnext = NULL;
return phead;
}
// 创建双向链表节点
struct DoubleNode *createDoubleNode(int data) {
struct DoubleNode *newNode = (struct DoubleNode *)malloc(sizeof(struct DoubleNode));
if (newNode == NULL) {
printf("malloc error:节点创建失败\n");
return NULL;
}
memset(newNode, 0, sizeof(struct DoubleNode));
newNode->data = data;
newNode->pbefore = NULL;
newNode->pnext = NULL;
return newNode;
}
// 双向链表尾插法新增节点
void addDoubleNodeTail(struct DoubleNode *phead, int data) {
if (phead == NULL) {
printf("双向链表未初始化\n");
return;
}
struct DoubleNode *newNode = createDoubleNode(data);
if (newNode == NULL) return;
// 找到尾节点
struct DoubleNode *ptemp = phead;
while (ptemp->pnext != NULL) {
ptemp = ptemp->pnext;
}
// 双向链接:尾节点与新节点互指
ptemp->pnext = newNode;
newNode->pbefore = ptemp;
printf("双向链表新增节点:%d\n", data);
}
// 双向链表双向遍历
void printDoubleList(struct DoubleNode *phead) {
if (phead == NULL || phead->pnext == NULL) {
printf("双向链表为空\n");
return;
}
// 正向遍历(从前往后)
struct DoubleNode *ptemp = phead->pnext;
printf("正向遍历:");
while (ptemp != NULL) {
printf("%d → ", ptemp->data);
ptemp = ptemp->pnext;
}
printf("NULL\n");
// 反向遍历(从后往前)
ptemp = phead->pnext;
while (ptemp->pnext != NULL) { // 先回到尾节点
ptemp = ptemp->pnext;
}
printf("反向遍历:");
while (ptemp != phead) {
printf("%d → ", ptemp->data);
ptemp = ptemp->pbefore;
}
printf("(回到头节点)\n");
}
六、嵌入式开发关键注意事项
- 内存管理优先级:嵌入式设备内存有限,
malloc分配的节点必须用free释放,双向链表 / 循环链表删除节点时,需同时断开 “前驱 / 后继指针”,避免野指针; - 循环链表遍历终止条件:必须通过 “是否回到头节点” 判断,否则会陷入死循环;
- 双向链表指针同步:修改节点时,需同时更新
pbefore和pnext,确保双向链接正确,否则会导致链表断裂; - 静态内存替代:若设备不支持动态内存分配(如部分单片机),可预先定义节点数组(静态内存池),替代
malloc和free,避免内存碎片化; - 多线程安全:若在中断 / 多线程中操作链表,需添加互斥锁(如
pthread_mutex_t)或关中断保护,防止并发访问导致链表混乱。
七、总结
链表的进阶用法本质是 “指针操作的灵活扩展”,核心要点可总结为:
- 头插法:O (1) 高效插入,适用于栈、缓存场景;
- 链表排序:冒泡排序简单易实现,无需额外空间;
- 循环链表:环形结构,适用于循环遍历、轮转存储;
- 双向链表:双向遍历,适用于频繁查找前驱 / 后继节点的场景。
掌握这些进阶技能,能轻松应对嵌入式开发中复杂的数据存储需求,比如串口环形缓存、设备链表管理、文件系统目录等。链表作为嵌入式 C 语言的核心数据结构,其指针操作思维也能为后续学习二叉树、图等更复杂结构打下基础。
我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧!