C语言链表详解,新手也能看懂! ——从入门到精通的完整教程

前言

链表是C语言中最重要的数据结构之一,也是面试中的高频考点。很多初学者在刚接触链表时,常常被指针、内存分配等概念搞得晕头转向。本文将以最通俗易懂的方式,带你从零开始学习链表,包含完整的代码示例和详细的注释说明。

读完本文,你将掌握:链表的基本概念、结构体定义、所有基本操作(创建/遍历/插入/删除/查找/销毁),以及常见的链表面试题解法。

第1章 链表的基本概念

1.1 什么是链表

链表是一种动态数据结构,由一系列节点(Node)组成,每个节点包含两部分:

• 数据域:存储实际的数据(如整型、字符型等)

• 指针域:存储下一个节点的内存地址

与数组不同,链表的节点在内存中不是连续存储的,而是通过指针相互连接,形成一个链式结构。就像一串糖葫芦,每颗山楂代表一个节点,连接山楂的竹签就是指针。

1.2 链表与数组的对比

为了更好地理解链表的优势,我们来对比一下链表和数组的区别:

|-------------|------------|------------|
| 对比项 | 数组 | 链表 |
| 内存存储 | 连续存储 | 离散存储 |
| 大小固定 | 是,声明时确定 | 否,动态分配 |
| 访问元素 | O(1),随机访问 | O(n),顺序访问 |
| 插入/删除 | O(n),需移动元素 | O(1),只需改指针 |

总结:数组适合频繁访问元素的场景,链表适合频繁插入删除的场景。

第2章 单链表的结构体定义

在C语言中,我们使用结构体来定义链表的节点。每个节点包含数据域和指针域。

2.1 节点结构体定义

// 定义链表节点结构体

typedef struct Node {

int data; // 数据域:存储数据

struct Node* next; // 指针域:指向下一个节点

} Node;

代码解释:

• typedef:给结构体起别名,方便使用

• int data:数据域,这里以int为例,也可以是其他类型

• struct Node* next:指针域,指向同类型的下一个节点

• 注意:结构体内部不能使用Node* next,因为Node还没定义完成

2.2 头指针的概念

链表通过一个头指针(head)来访问整个链表。头指针指向链表的第一个节点。如果链表为空,头指针的值为NULL。

// 定义头指针,初始化为空链表

Node* head = NULL;

第3章 链表的基本操作

本章将详细讲解链表的所有基本操作,每个操作都配有完整的代码和详细注释。

3.1 创建节点

在进行任何操作之前,我们首先需要学会如何创建一个新的节点。

// 创建新节点

Node* createNode(int data) {

// 1. 分配内存空间

Node* newNode = (Node*)malloc(sizeof(Node));

// 2. 检查内存分配是否成功

if (newNode == NULL) {

printf("内存分配失败!\n");

exit(1); // 退出程序

}

// 3. 初始化节点数据

newNode->data = data; // 设置数据域

newNode->next = NULL; // 指针域初始化为空

return newNode;

}

注意事项:

• 使用malloc分配内存后,一定要检查是否分配成功

• 新节点的next指针一定要初始化为NULL

• 用完节点后要用free释放内存,避免内存泄漏

3.2 遍历链表

遍历链表就是从头节点开始,依次访问每个节点,直到链表末尾(next为NULL)。

// 遍历打印链表

void printList(Node* head) {

// 检查链表是否为空

if (head == NULL) {

printf("链表为空!\n");

return;

}

// 使用临时指针遍历,不要直接修改head

Node* current = head;

printf("链表内容:");

// 循环遍历,直到当前节点为空

while (current != NULL) {

printf("%d -> ", current->data);

current = current->next; // 移动到下一个节点

}

printf("NULL\n");

}

关键点:

• 一定要用临时指针current遍历,不要直接修改头指针head

• 循环条件是current != NULL,不是current->next != NULL

• 遍历前先判断链表是否为空

3.3 插入操作

链表的插入操作有三种:头插法、尾插法、指定位置插入。

3.3.1 头插法

头插法就是将新节点插入到链表的最前面,成为新的头节点。

// 头插法:在链表头部插入节点

void insertAtHead(Node** head, int data) {

// 1. 创建新节点

Node* newNode = createNode(data);

// 2. 新节点的next指向原头节点

newNode->next = *head;

// 3. 更新头指针指向新节点

*head = newNode;

printf("成功头插:%d\n", data);

}

注意:这里使用二级指针Node** head,因为我们需要修改头指针本身的值。如果使用一级指针,修改的只是指针的副本,不会影响原来的头指针。

3.3.2 尾插法

尾插法就是将新节点插入到链表的最后面。

// 尾插法:在链表尾部插入节点

void insertAtTail(Node** head, int data) {

// 1. 创建新节点

Node* newNode = createNode(data);

// 2. 如果链表为空,新节点就是头节点

if (*head == NULL) {

*head = newNode;

printf("成功尾插:%d\n", data);

return;

}

// 3. 找到最后一个节点

Node* current = *head;

while (current->next != NULL) {

current = current->next;

}

// 4. 将新节点链接到最后

current->next = newNode;

printf("成功尾插:%d\n", data);

}

关键点:找尾节点的循环条件是current->next != NULL,这样current停在最后一个节点上,而不是NULL。

3.3.3 指定位置插入

在指定位置(第pos个位置,从1开始计数)插入新节点。

// 指定位置插入:在第pos个位置插入节点

void insertAtPosition(Node** head, int data, int pos) {

// 1. 位置合法性检查

if (pos < 1) {

printf("位置不合法!\n");

return;

}

// 2. 如果插入位置是1,相当于头插

if (pos == 1) {

insertAtHead(head, data);

return;

}

// 3. 创建新节点

Node* newNode = createNode(data);

Node* current = *head;

// 4. 找到第pos-1个节点(前驱节点)

for (int i = 1; i < pos - 1 && current != NULL; i++) {

current = current->next;

}

// 5. 检查位置是否超出链表长度

if (current == NULL) {

printf("位置超出链表长度!\n");

free(newNode); // 释放已分配的内存

return;

}

// 6. 完成插入:先连后,再连前

newNode->next = current->next;

current->next = newNode;

printf("在位置%d成功插入:%d\n", pos, data);

}

插入技巧口诀:先连后,再连前。这样就不会丢失后面的节点。

3.4 删除操作

链表的删除操作也有三种:头删、尾删、指定位置删除。

3.4.1 头删法

// 头删法:删除第一个节点

void deleteAtHead(Node** head) {

// 1. 检查链表是否为空

if (*head == NULL) {

printf("链表为空,无法删除!\n");

return;

}

// 2. 保存要删除的节点

Node* temp = *head;

// 3. 更新头指针

*head = (*head)->next;

// 4. 释放内存

printf("成功删除头节点:%d\n", temp->data);

free(temp);

}

3.4.2 尾删法

// 尾删法:删除最后一个节点

void deleteAtTail(Node** head) {

// 1. 检查链表是否为空

if (*head == NULL) {

printf("链表为空,无法删除!\n");

return;

}

// 2. 如果只有一个节点

if ((*head)->next == NULL) {

printf("成功删除尾节点:%d\n", (*head)->data);

free(*head);

*head = NULL;

return;

}

// 3. 找到倒数第二个节点

Node* current = *head;

while (current->next->next != NULL) {

current = current->next;

}

// 4. 删除最后一个节点

printf("成功删除尾节点:%d\n", current->next->data);

free(current->next);

current->next = NULL;

}

3.4.3 指定位置删除

// 指定位置删除:删除第pos个位置的节点

void deleteAtPosition(Node** head, int pos) {

// 1. 检查链表是否为空

if (*head == NULL) {

printf("链表为空,无法删除!\n");

return;

}

// 2. 位置合法性检查

if (pos < 1) {

printf("位置不合法!\n");

return;

}

// 3. 如果删除位置是1,相当于头删

if (pos == 1) {

deleteAtHead(head);

return;

}

// 4. 找到第pos-1个节点(前驱节点)

Node* current = *head;

for (int i = 1; i < pos - 1 && current->next != NULL; i++) {

current = current->next;

}

// 5. 检查位置是否超出链表长度

if (current->next == NULL) {

printf("位置超出链表长度!\n");

return;

}

// 6. 保存要删除的节点

Node* temp = current->next;

// 7. 重新链接,跳过要删除的节点

current->next = temp->next;

// 8. 释放内存

printf("在位置%d成功删除:%d\n", pos, temp->data);

free(temp);

}

3.5 查找节点

// 查找节点:查找值为data的节点是否存在

int searchNode(Node* head, int data) {

Node* current = head;

int position = 1;

while (current != NULL) {

if (current->data == data) {

printf("找到值%d,在位置%d\n", data, position);

return position; // 找到,返回位置

}

current = current->next;

position++;

}

printf("未找到值%d\n", data);

return -1; // 未找到,返回-1

}

3.6 销毁链表

链表使用完后一定要记得销毁,释放所有节点的内存,否则会造成内存泄漏!

// 销毁链表:释放所有节点的内存

void destroyList(Node** head) {

Node* current = *head;

Node* next;

while (current != NULL) {

next = current->next; // 先保存下一个节点

free(current); // 释放当前节点

current = next; // 移动到下一个节点

}

*head = NULL; // 头指针置空

printf("链表已成功销毁!\n");

}

3.7 完整可运行的测试程序

下面是一个完整的测试程序,包含了所有的操作,你可以直接复制运行:

#include <stdio.h>

#include <stdlib.h>

// 定义链表节点结构体

typedef struct Node {

int data;

struct Node* next;

} Node;

// 这里放入上面所有的函数声明...

int main() {

Node* head = NULL; // 创建空链表

printf("===== 链表测试程序 =====\n");

// 测试尾插

insertAtTail(&head, 10);

insertAtTail(&head, 20);

insertAtTail(&head, 30);

printList(head);

// 测试头插

insertAtHead(&head, 5);

printList(head);

// 测试指定位置插入

insertAtPosition(&head, 15, 3);

printList(head);

// 测试查找

searchNode(head, 15);

searchNode(head, 99);

// 测试删除

deleteAtPosition(&head, 3);

printList(head);

deleteAtHead(&head);

deleteAtTail(&head);

printList(head);

// 测试销毁

destroyList(&head);

printList(head);

return 0;

}

第4章 链表常见面试题总结

4.1 反转链表

题目:将单链表反转,例如 1->2->3->4->5 反转为 5->4->3->2->1。

解法:三指针迭代法。

Node* reverseList(Node* head) {

Node* prev = NULL; // 前一个节点

Node* curr = head; // 当前节点

while (curr != NULL) {

Node* next = curr->next; // 保存下一个节点

curr->next = prev; // 反转指针

prev = curr; // prev前移

curr = next; // curr前移

}

return prev; // prev成为新的头节点

}

4.2 判断链表是否有环

题目:判断链表中是否存在环。

解法:快慢指针法(龟兔赛跑算法)。慢指针每次走1步,快指针每次走2步,如果有环,它们一定会相遇。

int hasCycle(Node* head) {

if (head == NULL || head->next == NULL) {

return 0; // 没有环

}

Node* slow = head;

Node* fast = head;

while (fast != NULL && fast->next != NULL) {

slow = slow->next; // 慢指针走1步

fast = fast->next->next; // 快指针走2步

if (slow == fast) {

return 1; // 相遇,有环

}

}

return 0; // 没有环

}

4.3 求链表的中间节点

题目:找到链表的中间节点,如果有两个中间节点,返回第二个。

解法:还是快慢指针!快指针走两步,慢指针走一步,当快指针到末尾时,慢指针正好在中间。

Node* middleNode(Node* head) {

Node* slow = head;

Node* fast = head;

while (fast != NULL && fast->next != NULL) {

slow = slow->next;

fast = fast->next->next;

}

return slow;

}

4.4 删除链表的倒数第N个节点

题目:删除链表的倒数第n个节点,并返回链表的头节点。

解法:双指针法。先让快指针走n步,然后快慢指针一起走,当快指针到末尾时,慢指针正好指向倒数第n个节点的前驱。

Node* removeNthFromEnd(Node* head, int n) {

// 创建哑节点,简化头节点删除处理

Node dummy = {0, head};

Node* fast = &dummy;

Node* slow = &dummy;

// 快指针先走n+1步

for (int i = 0; i <= n; i++) {

fast = fast->next;

}

// 一起走

while (fast != NULL) {

slow = slow->next;

fast = fast->next;

}

// 删除节点

Node* temp = slow->next;

slow->next = slow->next->next;

free(temp);

return dummy.next;

}

4.5 合并两个有序链表

题目:将两个升序链表合并为一个新的升序链表。

Node* mergeTwoLists(Node* l1, Node* l2) {

// 创建哑节点

Node dummy = {0, NULL};

Node* current = &dummy;

while (l1 != NULL && l2 != NULL) {

if (l1->data <= l2->data) {

current->next = l1;

l1 = l1->next;

} else {

current->next = l2;

l2 = l2->next;

}

current = current->next;

}

// 接上剩余部分

current->next = (l1 != NULL) ? l1 : l2;

return dummy.next;

}

第5章 总结

通过本文的学习,相信你已经掌握了C语言链表的所有基本操作。让我们回顾一下重点:

• 链表是动态数据结构,适合频繁插入删除的场景

• 每个节点包含数据域和指针域,通过指针连接

• 插入操作口诀:先连后,再连前

• 删除操作:先保存,再链接,后释放

• 永远记得检查空指针的情况

• 使用完链表一定要销毁,避免内存泄漏

• 面试高频考点:反转链表、快慢指针判断环、找中间节点

链表是数据结构的基础,掌握了链表,后续学习栈、队列、树等数据结构就会容易很多。建议大家多动手写代码,不要只看不练,真正理解指针的运作原理。

------ 全文完 ------

相关推荐
ffqws_2 小时前
Spring Boot 配置读取全解析:从 application.yml 到 Java 对象的完整链路
java·数据库·spring boot
clear sky .2 小时前
【TCP】TCP数据粘包/分包问题
java·服务器·网络
孬甭_2 小时前
文件操作详解
c语言
云烟成雨TD2 小时前
Spring AI 1.x 系列【29】Embedding Model(嵌入模型)
java·人工智能·spring
CSCN新手听安2 小时前
【Qt】Qt窗口(五)QDialog对话框的使用,点击按钮弹出新的对话框,自定义对话框界面,模态对话框model
开发语言·c++·qt
晴夏。2 小时前
c++调用lua的方法
c++·游戏引擎·lua·ue
幸福巡礼2 小时前
【 LangChain 1.2 实战(四)】构建一个模块化的天气查询 Agent
java·前端·langchain
Lhan.zzZ9 小时前
笔记_2026.4.28_004
c++·ide·笔记·qt
wuminyu11 小时前
专家视角看Java字节码加载与存储指令机制
java·linux·c语言·jvm·c++