一、单链表的定义与节点结构

链表这东西,说白了就是一组节点串在一起。每个节点里存着数据,还存着下一个节点的地址。我刚开始学的时候,总觉得它比数组麻烦多了——数组多简单啊,下标一取就完事了。但后来做项目才发现,链表的动态特性太香了。

先看节点怎么定义:

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

这里有个细节——struct Node *next 为什么不能写成 Node *next?因为在 typedef 还没执行完的时候,Node 这个别名还不存在。嗯,这个坑我踩过。

核心要点:

  • 数据域:存实际数据,可以是 int、char,也可以是结构体
  • 指针域:存下一个节点的地址,最后一个节点指向 NULL
  • 头指针:指向第一个节点的指针,链表没了它就找不着北了

你想想看,数组在内存里是连续的一块,而链表的节点可以东一个西一个。每个节点通过指针串起来,就像一串珍珠,线断了就全散了。

二、单链表的创建与遍历

2.1 创建节点

创建节点其实就是动态分配内存。我个人习惯用 malloc,记得检查返回值:

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

注意:malloc 之后一定要判断是否成功。我在项目中遇到过内存泄漏,就是因为忘了检查返回值,结果程序跑着跑着就崩了。

2.2 头插法创建链表

头插法,就是每次新节点都插在头部。这样创建出来的链表,顺序和输入顺序是反的:

Node* createListByHead(int arr[], int n) {
    Node* head = NULL;
    for (int i = 0; i < n; i++) {
        Node* newNode = createNode(arr[i]);
        newNode->next = head;
        head = newNode;
    }
    return head;
}

2.3 尾插法创建链表

尾插法更符合直觉——新节点加在末尾,顺序和输入一致。但需要维护一个尾指针:

Node* createListByTail(int arr[], int n) {
    Node* head = NULL;
    Node* tail = NULL;
    for (int i = 0; i < n; i++) {
        Node* newNode = createNode(arr[i]);
        if (head == NULL) {
            head = newNode;
            tail = newNode;
        } else {
            tail->next = newNode;
            tail = newNode;
        }
    }
    return head;
}

2.4 遍历链表

遍历说白了就是从头走到尾,每到一个节点就干点事:

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

小技巧:遍历时用临时指针 current,别直接动 head。head 丢了,整个链表就找不回来了——我曾经犯过这种低级错误,调试了一下午。

三、单链表的插入与删除

3.1 插入操作

插入分三种情况:头部插入、中间插入、尾部插入。核心思路就两步——先找到位置,再改指针。

// 在第 pos 个位置插入(从1开始计数)
int insertNode(Node** head, int pos, int data) {
    Node* newNode = createNode(data);
    if (newNode == NULL) return -1;

    if (pos == 1) {
        newNode->next = *head;
        *head = newNode;
        return 0;
    }

    Node* prev = *head;
    for (int i = 1; i < pos - 1 && prev != NULL; i++) {
        prev = prev->next;
    }

    if (prev == NULL) {
        printf("位置无效\n");
        free(newNode);
        return -1;
    }

    newNode->next = prev->next;
    prev->next = newNode;
    return 0;
}

插入操作的关键:

  • 先连后断——先把新节点的 next 指向后一个节点,再让前一个节点的 next 指向新节点
  • 顺序不能反,否则会丢失后续节点
  • 头节点插入需要修改头指针,所以传的是二级指针

3.2 删除操作

删除比插入简单点,找到目标节点的前驱,跳过它就行了:

int deleteNode(Node** head, int pos) {
    if (*head == NULL) return -1;

    Node* temp;
    if (pos == 1) {
        temp = *head;
        *head = (*head)->next;
        free(temp);
        return 0;
    }

    Node* prev = *head;
    for (int i = 1; i < pos - 1 && prev != NULL; i++) {
        prev = prev->next;
    }

    if (prev == NULL || prev->next == NULL) {
        printf("位置无效\n");
        return -1;
    }

    temp = prev->next;
    prev->next = temp->next;
    free(temp);
    return 0;
}

注意:删除节点后一定要 free 掉,不然内存泄漏。我曾经在一个嵌入式项目里,频繁增删节点忘了释放,跑了三天后系统直接卡死——教训深刻。

四、单链表的反转

反转链表是面试高频题,也是很多人的噩梦。其实核心就三句话:记住下一个、指向前一个、往前走。

Node* reverseList(Node* head) {
    Node* prev = NULL;
    Node* current = head;
    Node* next = NULL;

    while (current != NULL) {
        next = current->next;   // 1. 记住下一个
        current->next = prev;   // 2. 指向前一个
        prev = current;         // 3. prev 往前走
        current = next;         // 4. current 往前走
    }
    return prev;  // prev 就是新头
}

为什么会这样?你想想看,反转的本质就是把每个节点的 next 指向它的前驱。原来指向后面的,现在指向前面的。头变成尾,尾变成头。

我的经验:刚开始学反转时,我总在纸上画图。画个三五个节点的链表,一步步走指针,比看十遍代码都管用。建议你也试试。

知识体系总览

下面这张图把单链表的操作串起来了,你可以对照着看:

单链表知识体系 单链表 定义与节点结构 创建与遍历 插入与删除 反转 数据域 指针域 头插法 尾插法 头部插入 中间插入 三指针法 核心操作:先连后断、指针移动、内存管理

单链表的核心操作就这些。说白了,链表操作就是玩指针——指来指去,别指丢了就行。刚开始可能会觉得绕,多画图、多写代码,慢慢就熟了。

本章总结:

  • 节点由数据域和指针域组成,指针域存放下一个节点的地址
  • 创建链表有头插法和尾插法,头插法顺序反转,尾插法顺序不变
  • 插入和删除都要先找到位置,再修改指针,注意内存管理
  • 反转链表用三指针法,记住下一个、指向前一个、往前走

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