6、双向链表基础:双向链表的结构定义、创建与遍历

单向链表我们聊完了,今天来聊聊它的“升级版”——双向链表。

说实话,我刚学链表那会儿,觉得单向链表已经够用了。直到我在一个嵌入式项目里,需要频繁地反向遍历数据——用单向链表做这个操作,那叫一个痛苦。每次都得从头结点开始,一个一个往后找。后来我改成双向链表,代码量反而少了,性能也上去了。

嗯,这就是双向链表的魅力。

6.1 双向链表的结构定义

双向链表,说白了就是每个节点里多了一个指针,指向前一个节点。

单向链表只有一个 next 指针,指向后面。双向链表呢,多了一个 prev 指针,指向前面。这样一来,你既可以往前走,也可以往后走。

结构定义长这样:

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

你看,和单向链表比,就多了一个 prev 指针。但就是这个小小的指针,让很多操作变得简单了。

核心区别一句话:单向链表只能从头走到尾,双向链表可以两头走。

6.2 双向链表的创建

创建双向链表,我习惯用头插法。为什么?因为头插法代码写起来顺手,而且很多场景下我们需要频繁在头部插入数据。

来看代码:

DLinkList createDLinkList() {
    DLinkList head = (DLinkList)malloc(sizeof(DNode));
    if (head == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    head->prev = NULL;
    head->next = NULL;
    head->data = 0;  // 头结点数据域可以不用,这里随便给个值

    int value;
    printf("请输入节点值(输入-1结束):");
    scanf("%d", &value);

    while (value != -1) {
        DNode *newNode = (DNode*)malloc(sizeof(DNode));
        if (newNode == NULL) {
            printf("内存分配失败\n");
            break;
        }
        newNode->data = value;

        // 头插法:新节点插入到头结点后面
        newNode->next = head->next;
        newNode->prev = head;
        if (head->next != NULL) {
            head->next->prev = newNode;
        }
        head->next = newNode;

        printf("请输入下一个值(输入-1结束):");
        scanf("%d", &value);
    }

    return head;
}

小技巧:插入新节点时,一定要先处理新节点的 prev 和 next,再修改前后节点的指针。顺序搞反了,链表就断了。我曾经在这个坑里栽过跟头,调试了半天才发现是顺序问题。

6.3 双向链表的遍历

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

正向遍历:

void forwardTraverse(DLinkList head) {
    DNode *p = head->next;
    printf("正向遍历:");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

反向遍历:

void backwardTraverse(DLinkList head) {
    // 先找到最后一个节点
    DNode *p = head->next;
    if (p == NULL) {
        printf("链表为空\n");
        return;
    }
    while (p->next != NULL) {
        p = p->next;
    }

    printf("反向遍历:");
    while (p != head) {
        printf("%d ", p->data);
        p = p->prev;
    }
    printf("\n");
}

你看,反向遍历在单向链表里几乎不可能高效实现,但在双向链表里,就是几行代码的事。

6.4 双向链表 vs 单向链表

我整理了一个对比表,方便你直观理解:

对比项 单向链表 双向链表
节点结构 data + next prev + data + next
内存占用 较小(每个节点少一个指针) 较大(每个节点多一个指针)
正向遍历 支持 支持
反向遍历 不支持(效率极低) 支持(高效)
删除节点 需要知道前驱节点 不需要,直接通过 prev 找到
插入节点 需要处理前驱的 next 需要处理前后两个节点的指针
代码复杂度 较低 稍高(多了一个指针要维护)

注意:双向链表虽然方便,但每个节点多了一个指针,内存开销增加了。在嵌入式系统里,如果内存非常紧张,单向链表可能更合适。我做过一个传感器数据采集的项目,节点数量上万,用双向链表内存直接爆了,后来改成单向链表才搞定。

6.5 双向链表的核心逻辑图

下面这张图展示了双向链表的核心结构,以及正向、反向遍历的路径:

双向链表结构示意图 头结点 prev data: 10 prev data: 20 prev data: 30 NULL next next next next prev prev prev 正向遍历(next方向) 反向遍历(prev方向) 头结点的 prev 指向 NULL,尾结点的 next 指向 NULL 双向链表 vs 单向链表 单向链表:只能从头到尾(一条路) 双向链表:可以从头到尾,也可以从尾到头(两条路)

6.6 避坑指南

我做了这么多年嵌入式开发,用双向链表踩过的坑不少。这里分享几个典型的:

  • 指针初始化:创建新节点时,一定要把 prev 和 next 都初始化为 NULL。我曾经漏掉一个,结果野指针导致系统崩溃。
  • 删除节点时:记得把被删除节点的 prev 和 next 都置为 NULL,防止悬空指针。
  • 头结点的处理:头结点的 prev 永远是 NULL,不要试图修改它。尾结点的 next 也永远是 NULL。
  • 插入和删除的顺序:先处理新节点的指针,再处理前后节点的指针。这个顺序错了,链表就断了。

总结一下:双向链表比单向链表多了一个 prev 指针,换来的是反向遍历和节点删除的便利。代价是每个节点多占 4 或 8 字节(看平台),代码也稍微复杂一点。但说实话,在大多数场景下,这点代价是值得的。

好了,双向链表的基础就聊到这里。代码你多写几遍,自然就熟了。


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