【嵌入式 C 语言实战】链表进阶全攻略:头插法 / 排序 / 循环链表 / 双向链表(附完整代码)

20 阅读9分钟

【嵌入式 C 语言实战】链表进阶全攻略:头插法 / 排序 / 循环链表 / 双向链表(附完整代码)

大家好,我是学嵌入式的小杨同学。在上一篇博客中,我们掌握了单链表的基础操作(尾插、查询、删除等),但嵌入式开发中,仅基础单链表无法满足所有场景 —— 比如需要快速在表头插入数据、对链表排序、实现环形数据存储等。今天就结合资料,深入讲解链表的进阶用法:头插法、链表排序、循环链表、双向链表,附完整实战代码,帮你解锁链表的全部核心技能,应对更复杂的嵌入式数据存储需求。

一、进阶基础:先理清核心前提

在开始进阶操作前,先回顾链表的核心本质,避免遗忘关键知识点:

  • 存储逻辑:链式存储不依赖连续内存,通过指针串联节点,解决数组扩容难题;
  • 节点结构:由 “数据域(存业务数据)+ 指针域(指向下一节点)” 组成,通过结构体定义;
  • 内存管理:依赖malloc(开辟节点)、memset/bzero(初始化)、free(释放节点),嵌入式开发中必须严格管理内存,避免泄漏;
  • 基础设计:本文所有进阶操作均基于 “带头节点” 的链表(头节点仅用于标识起始位置,不存有效数据),简化边界判断。

二、进阶操作 1:头插法(O (1) 高效新增节点)

基础单链表的 “尾插法” 需要遍历到链表末尾,时间复杂度 O (n);而 “头插法” 直接在头节点后插入新节点,时间复杂度 O (1),适用于需要 “先进后出” 的场景(如栈、串口数据缓存)。

1. 头插法核心逻辑

  1. 创建新节点并填充数据、初始化指针;
  2. 新节点的指针域指向头节点当前的后续节点(即原链表的第一个有效节点);
  3. 头节点的指针域指向新节点,完成插入。

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. 链表冒泡排序逻辑

  1. 外层循环控制排序轮次(最多 n-1 轮,n 为链表长度);
  2. 内层循环遍历链表,对比相邻节点的数据;
  3. 若前一节点数据大于后一节点,交换两者数据域;
  4. 优化:添加标志位,若某轮无数据交换,说明链表已有序,直接退出循环。

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)核心操作逻辑(以尾插为例)
  1. 创建新节点,初始化pbeforepnextNULL
  2. 找到尾节点(pnext == NULL);
  3. 尾节点的pnext指向新节点,新节点的pbefore指向尾节点;
  4. 新节点的pnextNULL,完成插入。

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");
}

六、嵌入式开发关键注意事项

  1. 内存管理优先级:嵌入式设备内存有限,malloc分配的节点必须用free释放,双向链表 / 循环链表删除节点时,需同时断开 “前驱 / 后继指针”,避免野指针;
  2. 循环链表遍历终止条件:必须通过 “是否回到头节点” 判断,否则会陷入死循环;
  3. 双向链表指针同步:修改节点时,需同时更新pbeforepnext,确保双向链接正确,否则会导致链表断裂;
  4. 静态内存替代:若设备不支持动态内存分配(如部分单片机),可预先定义节点数组(静态内存池),替代mallocfree,避免内存碎片化;
  5. 多线程安全:若在中断 / 多线程中操作链表,需添加互斥锁(如pthread_mutex_t)或关中断保护,防止并发访问导致链表混乱。

七、总结

链表的进阶用法本质是 “指针操作的灵活扩展”,核心要点可总结为:

  1. 头插法:O (1) 高效插入,适用于栈、缓存场景;
  2. 链表排序:冒泡排序简单易实现,无需额外空间;
  3. 循环链表:环形结构,适用于循环遍历、轮转存储;
  4. 双向链表:双向遍历,适用于频繁查找前驱 / 后继节点的场景。

掌握这些进阶技能,能轻松应对嵌入式开发中复杂的数据存储需求,比如串口环形缓存、设备链表管理、文件系统目录等。链表作为嵌入式 C 语言的核心数据结构,其指针操作思维也能为后续学习二叉树、图等更复杂结构打下基础。

我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧!