13、动态链表的实现:单链表、双链表的内存管理、节点创建与销毁

动态链表,说白了就是「用指针串起来的一堆结构体」。

我刚开始学C语言时,总觉得数组够用了。直到有一次做嵌入式项目,需要动态管理传感器节点——今天接3个,明天拆2个,后天又加5个。数组?根本没法搞。链表才是正解。

这一章,咱们就聊聊单链表和双链表的内存管理。重点放在:节点怎么创建、怎么销毁、内存怎么才不会泄漏。

13.1 单链表:最简单的动态结构

单链表长什么样?每个节点里存一个数据,再加一个指向下一个节点的指针。最后一个节点指向NULL。

核心要点:单链表只能从头往后走,不能回头。所以遍历、插入、删除都得小心维护头指针。

13.1.1 节点结构定义

typedef struct Node {
    int data;           // 数据域
    struct Node *next;  // 指针域
} Node;

嗯,这里要注意:struct Node *next 不能写成 Node *next,因为typedef还没生效。我见过不少新手在这栽跟头。

13.1.2 创建节点

创建节点,说白了就是malloc一块内存,填上数据,把next置NULL。

Node* createNode(int data) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        fprintf(stderr, "内存分配失败\n");
        exit(EXIT_FAILURE);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

我的习惯:每次malloc后立刻检查返回值。别偷懒。我在项目中遇到过malloc返回NULL导致程序崩溃的惨案——嵌入式设备内存本来就紧张,不检查就是给自己埋雷。

13.1.3 销毁节点

销毁节点就一个原则:free之前,先把它的next指针存下来。不然链表就断了。

void destroyList(Node *head) {
    Node *current = head;
    Node *next;
    while (current != NULL) {
        next = current->next;  // 先存下一个
        free(current);          // 再释放当前
        current = next;         // 移动到下一个
    }
}

我曾经犯过的错:直接free(head)然后head = head->next。你想想看,free之后那块内存已经还给系统了,再去访问next?未定义行为!

13.2 双链表:能往前走也能往后走

双链表比单链表多一个prev指针。好处是:删除节点时不用找前驱,直接就能操作。代价是:每个节点多占一个指针的内存。

13.2.1 节点结构定义

typedef struct DNode {
    int data;
    struct DNode *prev;
    struct DNode *next;
} DNode;

双链表的头节点,prev指向NULL。尾节点的next指向NULL。这是规矩。

13.2.2 创建双链表节点

DNode* createDNode(int data) {
    DNode *newNode = (DNode*)malloc(sizeof(DNode));
    if (newNode == NULL) {
        fprintf(stderr, "内存分配失败\n");
        exit(EXIT_FAILURE);
    }
    newNode->data = data;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

创建过程和单链表差不多。区别在于:插入时要同时维护prev和next两个指针。

13.2.3 销毁双链表

void destroyDList(DNode *head) {
    DNode *current = head;
    while (current != NULL) {
        DNode *next = current->next;
        free(current);
        current = next;
    }
}

销毁逻辑和单链表一样。因为双链表虽然多了一个prev,但销毁时只需要沿着next走就行。

13.3 插入与删除:内存管理的核心操作

插入和删除,说白了就是「改指针 + 管内存」。我总结了一个口诀:先接后断,先存后删

13.3.1 单链表头部插入

void insertAtHead(Node **head, int data) {
    Node *newNode = createNode(data);
    newNode->next = *head;
    *head = newNode;
}

这里用了二级指针。为什么?因为要修改头指针本身。我建议你养成用二级指针的习惯,不然函数里改了头指针,外面不知道。

13.3.2 单链表删除指定节点

void deleteNode(Node **head, int target) {
    if (*head == NULL) return;

    Node *current = *head;
    Node *prev = NULL;

    // 找目标节点
    while (current != NULL && current->data != target) {
        prev = current;
        current = current->next;
    }

    if (current == NULL) return;  // 没找到

    if (prev == NULL) {
        // 删除的是头节点
        *head = current->next;
    } else {
        prev->next = current->next;
    }

    free(current);  // 释放内存
}

避坑指南:我曾经在删除节点后忘了free,结果内存泄漏。嵌入式设备跑几天就挂了。排查了好久才发现是链表节点没释放。从那以后,我每次写删除操作,都会在代码里加个注释:// 别忘了free!

13.3.3 双链表尾部插入

void insertAtTail(DNode **head, int data) {
    DNode *newNode = createDNode(data);
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    DNode *current = *head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = newNode;
    newNode->prev = current;
}

双链表插入时,一定要把prev和next都设置好。少设一个,遍历时就可能出问题。

13.4 内存管理要点总结

操作 单链表 双链表
节点大小 data + 1个指针 data + 2个指针
插入复杂度 O(1)(头插)/ O(n)(尾插) O(1)(头插)/ O(n)(尾插)
删除复杂度 O(n)(需找前驱) O(1)(已知节点指针)
内存开销 较小 较大(多一个prev)
遍历方向 单向 双向

选单链表还是双链表?我的经验是:如果删除操作多,而且经常需要反向遍历,用双链表。如果只是简单存数据、顺序访问,单链表就够了。

13.5 知识体系图

下面这张图,把单链表和双链表的核心操作串起来了。你一看就明白。

动态链表内存管理核心知识体系 单链表 节点结构:data + next 创建:malloc → 赋值 → next=NULL 销毁:先存next → free → 循环 头插:新节点→next = 原头 删除:找前驱 → 改指针 → free ⚠ 注意:删除时先存next再free 💡 适用场景:顺序访问、内存敏感 双链表 节点结构:prev + data + next 创建:malloc → 赋值 → prev=next=NULL 销毁:沿next遍历 → free 尾插:找尾 → 改next和prev 删除:直接操作prev和next → free ⚠ 注意:插入/删除要维护两个指针 💡 适用场景:频繁删除、双向遍历

我的建议:刚开始学链表时,先在纸上画图。把每个节点画成方框,指针画成箭头。画清楚了再写代码。我当年就是这么过来的,比直接敲代码效率高得多。

好了,单链表和双链表的内存管理就聊到这儿。记住三个核心:malloc检查、free不遗漏、指针不乱指。做到这三点,链表这块基本稳了。

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