链表基础:单向链表的定义、创建、遍历、插入、删除操作

说实话,很多初学者一听到「链表」两个字就头大。我当年也是这样,觉得指针绕来绕去,不如数组来得直接。但后来在项目中做动态内存管理时,才发现链表这东西,真香。

数组有个硬伤——长度固定。你定义了一个100个元素的数组,结果数据只有20个,浪费;如果来了120个,又装不下。链表就不一样了,它像一串珠子,想加一颗就加一颗,想取一颗就取一颗,灵活得很。

什么是单向链表

单向链表,说白了就是一组节点,每个节点里存着数据,还存着一个指针,指向下一个节点。最后一个节点的指针指向NULL,表示到头了。

每个节点长这样:

struct Node {
    int data;           // 数据域
    struct Node *next;  // 指针域,指向下一个节点
};

你想想看,这个结构体里有个指向自身类型的指针,是不是有点递归的味道?嗯,这就是链表的精髓。

核心概念:链表通过指针将分散在内存各处的节点串联起来,不要求物理连续。

链表的定义与创建

先定义节点类型:

typedef struct Node {
    int data;
    struct Node *next;
} Node, *LinkedList;

我个人习惯把 LinkedList 定义成指向头节点的指针,这样函数传参时更直观。

创建链表,一般从创建头节点开始。头节点不存有效数据,只用来标记链表的起点。

LinkedList createList() {
    LinkedList head = (LinkedList)malloc(sizeof(Node));
    if (head == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    head->next = NULL;
    return head;
}

我在项目中遇到过一个问题:malloc之后忘了检查返回值,结果在嵌入式设备上跑着跑着就崩了。从那以后,我每次malloc都必做判空处理,成了肌肉记忆。

链表的遍历

遍历链表,就是从头节点开始,顺着next指针一路往下走,直到遇见NULL。

void traverseList(LinkedList head) {
    Node *p = head->next;  // 跳过头节点
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

这里有个小细节:遍历时不要直接移动头指针,否则链表就丢了。一定要用一个临时指针p来走。

小技巧:遍历时如果既要访问数据又要修改节点,可以用二级指针或者返回新的头指针。

链表的插入操作

插入分三种情况:头插、尾插、中间插。我重点讲中间插入,因为这是最容易出bug的地方。

// 在指定位置之后插入新节点
int insertAfter(Node *prev, int data) {
    if (prev == NULL) return -1;

    Node *newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) return -1;

    newNode->data = data;
    newNode->next = prev->next;
    prev->next = newNode;

    return 0;
}

注意顺序:先让新节点指向后一个节点,再让前一个节点指向新节点。顺序反了,后面的节点就找不到了。

我曾经在调试一个通信协议栈时,就因为把这两行代码写反了,导致链表断链,数据包全部丢失。查了整整一个下午才找到原因……嗯,这种坑踩过一次就记住了。

链表的删除操作

删除节点,核心思路是:找到要删节点的前一个节点,让它跳过要删的节点,直接指向下一个。

int deleteNode(LinkedList head, int target) {
    Node *prev = head;
    Node *curr = head->next;

    while (curr != NULL) {
        if (curr->data == target) {
            prev->next = curr->next;
            free(curr);
            return 0;
        }
        prev = curr;
        curr = curr->next;
    }
    return -1;  // 没找到
}

删除后一定要free掉节点内存,否则内存泄漏。在嵌入式环境里,内存泄漏是慢性毒药,跑几天就崩了。

注意:删除头节点后面的第一个节点时,要特殊处理。如果链表带头节点,删除逻辑不变;如果不带头节点,删除第一个节点需要更新头指针。

知识体系图

下面这张图帮你理清单向链表的整体脉络:

单向链表知识体系 单向链表 定义:struct Node 创建:malloc + 判空 遍历:while(p != NULL) 插入:头插 / 尾插 / 中间插 删除:跳过 + free 注意:判空 / 顺序 / 内存泄漏

完整示例代码

把上面这些操作串起来,写一个完整的例子:

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node, *LinkedList;

LinkedList createList() {
    LinkedList head = (LinkedList)malloc(sizeof(Node));
    if (!head) return NULL;
    head->next = NULL;
    return head;
}

void traverseList(LinkedList head) {
    Node *p = head->next;
    while (p) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

int insertAfter(Node *prev, int data) {
    if (!prev) return -1;
    Node *newNode = (Node*)malloc(sizeof(Node));
    if (!newNode) return -1;
    newNode->data = data;
    newNode->next = prev->next;
    prev->next = newNode;
    return 0;
}

int deleteNode(LinkedList head, int target) {
    Node *prev = head;
    Node *curr = head->next;
    while (curr) {
        if (curr->data == target) {
            prev->next = curr->next;
            free(curr);
            return 0;
        }
        prev = curr;
        curr = curr->next;
    }
    return -1;
}

void freeList(LinkedList head) {
    Node *p = head;
    while (p) {
        Node *tmp = p;
        p = p->next;
        free(tmp);
    }
}

int main() {
    LinkedList list = createList();

    insertAfter(list, 10);
    insertAfter(list, 20);
    insertAfter(list, 30);

    printf("链表内容:");
    traverseList(list);

    deleteNode(list, 20);
    printf("删除20后:");
    traverseList(list);

    freeList(list);
    return 0;
}

常见问题与避坑指南

  • 忘记判空:malloc可能失败,尤其是嵌入式设备内存紧张时。不判空,后续操作就是野指针。
  • 插入顺序搞反:先连后断,这个口诀记牢。
  • 删除后没free:内存泄漏的根源。我见过一个项目,链表频繁增删但不free,跑了三天后系统直接挂掉。
  • 遍历时移动了头指针:链表丢了,找不回来。一定要用临时指针。

我的建议:刚开始学链表时,拿纸笔画一画指针的指向变化。画清楚了,代码自然就写对了。

单向链表是链表家族的基础。搞懂了它,双向链表、循环链表、甚至内核链表都不在话下。你想想看,是不是这个理?

专注资料整理