第22章 线性表(三):双向链表与循环链表、链表的应用(多项式相加)、静态链表的概念

各位同学,咱们继续聊链表。前面两章我们把单链表的基本操作讲透了,但实际项目中你会发现,单链表有个“硬伤”——只能从头往后走。你想往回找前一个节点?没门儿。这就引出了我们今天的主角:双向链表和循环链表。

22.1 双向链表:能进能退,才是真本事

双向链表,说白了就是在每个节点里多存一个指针,指向前一个节点。结构体长这样:

typedef struct DNode {
    int data;
    struct DNode *prev;  // 前驱指针
    struct DNode *next;  // 后继指针
} DNode, *DLinkList;

你想想看,单链表里删除一个节点,你得先找到它的前驱,否则没法删。双向链表就没这烦恼——我拿着当前节点,直接通过 prev 就能找到前一个。

核心优势:双向链表的插入和删除操作,时间复杂度从 O(n) 降到了 O(1)(前提是你已经定位到了目标节点)。

我在项目中做过一个内存缓存池,里面用双向链表管理空闲块。每次回收内存时,需要快速把相邻的空闲块合并。用双向链表,我直接通过 prevnext 检查前后邻居,代码写起来特别顺手。

22.1.1 双向链表的插入操作

插入操作分四种情况:头插、尾插、中间插、在指定节点前插。我重点讲一下在节点 p 之前插入新节点 s

s->prev = p->prev;
s->next = p;
if (p->prev != NULL)
    p->prev->next = s;
p->prev = s;

我曾经踩过的坑:写双向链表插入时,最容易忘掉处理 p->prev->next = s 这一步。结果链表就断了,调试了半天才发现。记住:先接好新节点的两个指针,再断开旧连接。

22.1.2 双向链表的删除操作

删除节点 p 就简单多了:

if (p->prev != NULL)
    p->prev->next = p->next;
if (p->next != NULL)
    p->next->prev = p->prev;
free(p);

嗯,这里要注意:如果 p 是头节点,p->prev 是 NULL,直接跳过就行。

22.2 循环链表:首尾相连,循环往复

循环链表就是把单链表的尾节点指向头节点,形成一个环。为什么要这么做?我举个例子你就明白了。

在操作系统的进程调度中,时间片轮转算法就用到了循环链表。每个进程跑完一个时间片,调度器就移到下一个进程。如果到了末尾,自动回到开头继续。用循环链表实现,你根本不用判断“是不是最后一个节点”——直接 p = p->next 就行了。

22.2.1 循环链表的遍历

遍历循环链表时,终止条件不再是 p == NULL,而是 p == head

DNode *p = head->next;
while (p != head) {
    printf("%d ", p->data);
    p = p->next;
}

小技巧:我习惯在循环链表里加一个“哨兵节点”(也叫头节点),不存数据,只用来标记起点和终点。这样空链表和非空链表的操作就能统一了。

22.3 链表的经典应用:多项式相加

多项式相加是链表应用的经典案例。每个多项式可以看作由若干“项”组成的链表,每项包含系数和指数。比如:

3x^5 + 2x^3 + x + 7

用链表表示就是:

typedef struct Term {
    float coef;  // 系数
    int expn;    // 指数
    struct Term *next;
} Term, *Polynomial;

两个多项式相加的算法,说白了就是合并两个有序链表——按指数降序排列,指数相同的系数相加,指数不同的直接插入。

22.3.1 多项式相加的核心逻辑

Polynomial AddPolynomial(Polynomial A, Polynomial B) {
    Polynomial C = (Polynomial)malloc(sizeof(Term));
    Term *pa = A->next, *pb = B->next, *pc = C;
    
    while (pa != NULL && pb != NULL) {
        if (pa->expn > pb->expn) {
            pc->next = pa; pa = pa->next;
        } else if (pa->expn < pb->expn) {
            pc->next = pb; pb = pb->next;
        } else {
            // 指数相等,系数相加
            float sum = pa->coef + pb->coef;
            if (sum != 0) {
                Term *t = (Term*)malloc(sizeof(Term));
                t->coef = sum;
                t->expn = pa->expn;
                pc->next = t;
                pc = t;
            }
            pa = pa->next;
            pb = pb->next;
            continue;
        }
        pc = pc->next;
    }
    pc->next = (pa != NULL) ? pa : pb;
    return C;
}

注意:系数相加为0时,这一项要直接丢弃,不能留在结果里。我在做数值计算项目时,就因为没处理这个细节,导致结果里多了一堆“0x^N”的项,被同事笑话了。

22.4 静态链表:用数组模拟链表

有些场景下,你不能用动态内存分配(比如嵌入式系统里没有堆),但又想用链表的灵活性。怎么办?静态链表就是答案。

静态链表用数组来存储节点,每个节点除了数据,还有一个“游标”(cur),指向下一个节点的数组下标。最后一个节点的游标为 -1 或 0,表示结束。

#define MAXSIZE 100
typedef struct {
    int data;
    int cur;  // 游标,相当于 next 指针
} StaticNode, StaticList[MAXSIZE];

静态链表的插入和删除,本质上是在数组里维护一个“空闲链表”。你删除一个节点,就把它的下标放回空闲链表;插入时,从空闲链表取一个下标。

我个人习惯:用数组下标 0 作为空闲链表的头节点,下标 1 作为数据链表的头节点。这样管理起来特别清晰。

22.4.1 静态链表的优缺点

优点 缺点
不需要动态内存分配,适合嵌入式环境 数组大小固定,不能动态扩展
插入删除不需要移动元素 访问元素仍需遍历,不支持随机访问
内存连续,缓存友好 实现比动态链表复杂

我在做单片机项目时用过静态链表管理传感器数据缓冲区。因为单片机内存只有 2KB,用 malloc 容易碎片化,静态链表正好解决了这个问题。

22.5 本章知识体系

下面这张图帮你理清本章的知识脉络:

线性表(三)链表进阶 双向链表 prev + next 双指针 插入/删除 O(1) 循环链表 尾节点指向头节点 适合轮转调度 多项式相加 系数+指数结构体 合并有序链表 静态链表 数组+游标 嵌入式常用 核心思想:用指针(或游标)将离散的节点串联起来 灵活插入删除 vs 随机访问能力弱 典型应用场景 操作系统进程调度 | 内存管理 | 多项式运算 | 嵌入式缓冲区管理 LRU缓存 | 文本编辑器撤销功能 | 图论邻接表

好了,这一章的内容就到这里。双向链表和循环链表是单链表的自然延伸,多项式相加是链表应用的经典案例,静态链表则是在受限环境下的巧妙替代方案。把这些吃透了,你就能在合适的场景下选择合适的链表结构。


公众号:蓝海资料掘金营,微信 deep3321