第十九章:指针与双向链表——指针操作、插入与删除的指针修改

双向链表,说白了就是给每个节点多装了一根“回头箭”。

单向链表只能从头往后走,想找前一个节点?没门。双向链表就不一样了——每个节点里除了存数据的 data,还有两个指针:一个指向前驱(prev),一个指向后继(next)。这样你就能前后自由穿梭了。

我个人习惯把双向链表比作一条双向车道。单向链表是单行道,想掉头?你得从头再来。双向链表呢,随时可以调头,灵活得很。

19.1 双向链表的结构定义

先看结构体长什么样:

typedef struct DNode {
    int data;               // 数据域
    struct DNode *prev;     // 指向前驱节点
    struct DNode *next;     // 指向后继节点
} DNode, *DLinkList;

嗯,这里要注意:prevnext 都是指针,类型是 struct DNode *。你不能写成 DNode *,因为这时候 DNode 还没定义完呢。

小技巧: 我习惯把头节点的 prev 置为 NULL,尾节点的 next 置为 NULL。这样遍历时判断边界就方便了。

19.2 双向链表的插入操作

插入操作比单向链表麻烦一点,因为你要维护两个方向的指针。但好处是——如果你知道要插在哪个节点后面,你不需要遍历找前驱。

来看一个经典场景:在节点 p 后面插入新节点 s

// 在 p 节点之后插入 s 节点
s->prev = p;
s->next = p->next;
if (p->next != NULL) {
    p->next->prev = s;
}
p->next = s;

这里有个坑,我当年第一次写的时候踩过。你想想看,如果 p 是尾节点,p->next 是 NULL。这时候你直接写 p->next->prev = s,程序就崩了。所以一定要加个判断。

避坑指南: 我曾经在项目里因为没判断 p->next 是否为空,导致在链表尾部插入时直接段错误。排查了半天才发现是空指针解引用。记住:操作指针前,先问自己一句“它是不是 NULL?”

19.3 双向链表的删除操作

删除操作比插入更简单,因为你知道要删哪个节点,直接把它从链表中“摘”下来就行。

删除节点 p

// 删除节点 p
if (p->prev != NULL) {
    p->prev->next = p->next;
}
if (p->next != NULL) {
    p->next->prev = p->prev;
}
free(p);

你看,代码很对称。前驱的后继指向 p 的后继,后继的前驱指向 p 的前驱。这样 p 就被孤立了,然后 free 掉。

但要注意:如果 p 是头节点,p->prev 是 NULL,这时候你不能写 p->prev->next。同理,如果 p 是尾节点,p->next 是 NULL。

核心要点: 双向链表的插入和删除,本质就是修改四个指针(或两个指针)的指向。每次操作前,先画个图,把指针的“来龙去脉”理清楚,再动手写代码。

19.4 双向链表的遍历

双向链表遍历有两种方式:正向和反向。

正向遍历:

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

反向遍历:

DNode *p = tail;  // 假设 tail 指向尾节点
while (p != NULL) {
    printf("%d ", p->data);
    p = p->prev;
}

我个人觉得,双向链表最爽的地方就在这里。你想怎么走就怎么走,不用像单向链表那样只能一条道走到黑。

19.5 双向链表 vs 单向链表

特性 单向链表 双向链表
指针数量 1 个(next) 2 个(prev, next)
内存占用 较小 较大(多一个指针)
插入/删除 需遍历找前驱 直接操作,效率更高
反向遍历 不支持 支持
代码复杂度 中等(需维护两个指针)

你看,双向链表虽然多占了一个指针的内存,但换来了更灵活的操作。在嵌入式系统里,如果内存够用,我一般优先用双向链表——调试起来也方便,能前后看。

19.6 双向链表的核心逻辑图

下面这张图展示了双向链表的基本结构,以及插入和删除时指针的变化:

双向链表结构及插入/删除指针变化 prev (前驱) curr (当前) next (后继) next next prev prev 插入新节点 s 到 curr 之后 s (新节点) 插入后: s->prev = curr; s->next = curr->next; curr->next->prev = s; curr->next = s; 删除节点 curr curr->prev->next = curr->next; curr->next->prev = curr->prev; free(curr);

19.7 总结

双向链表的核心就是两个指针:prev 和 next。插入和删除时,记住一句话:先连后断。先把新节点的两个指针连好,再修改前后节点的指针。这样就不会丢失节点。

我在实际项目中,双向链表用得比单向链表多。尤其是在需要频繁插入删除、或者需要双向遍历的场景下,双向链表真的省心不少。当然,代价就是多占一点内存——但现在的嵌入式设备,这点内存真不算什么。

嗯,这一章就到这。记住:画图、画图、画图。指针操作,画图比写代码更重要。

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