在 C 语言的数据结构学习中,链表是仅次于数组的重要线性结构。它通过指针将离散的内存节点串联起来,克服了数组固定大小、插入删除效率低的缺点。本文将深入讲解单链表与双向链表的原理、实现及应用场景,帮助读者彻底掌握这两种基础数据结构。
一、链表基础概念
链表是由多个节点组成的线性数据结构,每个节点包含两部分:
- 数据域:存储节点的实际数据(如 int、char、自定义结构体等)
- 指针域:存储下一个(或上一个)节点的内存地址,实现节点间的关联
与数组相比,链表的核心优势在于:
- 内存动态分配,无需预先指定大小
- 插入 / 删除操作无需移动大量元素,仅需修改指针指向
- 有效利用碎片化内存空间
二、单链表详解
2.1 单链表结构特点
单链表是最简单的链表形式,每个节点仅包含一个指针域,指向下一个节点。链表的操作需通过头指针(指向第一个节点)开始,尾节点的指针域为NULL,表示链表结束。
2.2 单链表节点定义
首先定义单链表节点结构体,包含数据域和指针域:
#include <stdio.h>
#include <stdlib.h>
// 单链表节点结构体
typedef struct Node {
int data; // 数据域(存储整型数据)
struct Node *next; // 指针域(指向后续节点)
} Node, *LinkedList; // Node:节点类型;LinkedList:节点指针类型
2.3 单链表核心操作实现
2.3.1 初始化链表
初始化操作创建一个头节点(不存储实际数据,仅用于简化操作),并将头节点的next指针设为NULL:
// 初始化单链表(返回头节点指针)
LinkedList initLinkedList() {
// 分配头节点内存
LinkedList head = (LinkedList)malloc(sizeof(Node));
if (head == NULL) {
printf("内存分配失败!\n");
exit(1); // 退出程序
}
head->next = NULL; // 初始状态下无后续节点
return head;
}
2.3.2 尾插法添加节点
在链表末尾插入新节点,适用于顺序创建链表:
// 尾插法添加节点(head:头节点;data:待插入数据)
void insertAtTail(LinkedList head, int data) {
// 1. 创建新节点
LinkedList newNode = (LinkedList)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL; // 新节点为尾节点,next设为NULL
// 2. 找到当前尾节点(从头节点开始遍历)
LinkedList p = head;
while (p->next != NULL) {
p = p->next; // 移动到下一个节点
}
// 3. 插入新节点(尾节点的next指向新节点)
p->next = newNode;
}
2.3.3 遍历链表
从头部开始依次访问每个节点,输出数据:
// 遍历单链表(head:头节点)
void traverseLinkedList(LinkedList head) {
LinkedList p = head->next; // 跳过头节点,从第一个数据节点开始
if (p == NULL) {
printf("链表为空!\n");
return;
}
printf("链表内容:");
while (p != NULL) {
printf("%d ", p->data); // 输出当前节点数据
p = p->next; // 移动到下一个节点
}
printf("\n");
}
2.3.4 删除指定节点
根据数据值删除节点,需处理 “删除头节点后第一个节点”“删除中间节点”“删除尾节点” 三种场景:
// 删除指定数据的节点(head:头节点;target:待删除数据)
void deleteNode(LinkedList head, int target) {
LinkedList p = head; // p指向当前节点的前驱节点
LinkedList q = head->next; // q指向当前节点
// 1. 查找待删除节点
while (q != NULL && q->data != target) {
p = q; // p跟随q移动
q = q->next;
}
// 2. 处理查找结果
if (q == NULL) {
printf("未找到数据为%d的节点!\n", target);
return;
}
// 3. 删除节点(修改前驱节点的指针,跳过待删除节点)
p->next = q->next;
free(q); // 释放待删除节点的内存(避免内存泄漏)
printf("成功删除数据为%d的节点!\n", target);
}
2.4 单链表完整示例
int main() {
// 1. 初始化链表
LinkedList head = initLinkedList();
// 2. 添加节点
insertAtTail(head, 10);
insertAtTail(head, 20);
insertAtTail(head, 30);
traverseLinkedList(head); // 输出:10 20 30
// 3. 删除节点
deleteNode(head, 20);
traverseLinkedList(head); // 输出:10 30
// 4. 尝试删除不存在的节点
deleteNode(head, 40); // 输出:未找到数据为40的节点
return 0;
}
三、双向链表详解
3.1 双向链表结构特点
双向链表在单链表基础上增加了前驱指针域(指向前一个节点),每个节点包含三个部分:
- 前驱指针域(prev):指向当前节点的前一个节点
- 数据域(data):存储实际数据
- 后继指针域(next):指向当前节点的后一个节点
双向链表的优势在于:
- 可双向遍历,访问前驱节点无需从头遍历
- 删除节点时无需查找前驱节点(单链表需查找)
- 插入操作更灵活(支持前插、后插)
3.2 双向链表节点定义
// 双向链表节点结构体
typedef struct DNode {
int data; // 数据域
struct DNode *prev; // 前驱指针域(指向前一个节点)
struct DNode *next; // 后继指针域(指向后一个节点)
} DNode, *DLinkedList;
3.3 双向链表核心操作实现
3.3.1 初始化双向链表
与单链表类似,创建头节点,头节点的prev和next均设为NULL:
// 初始化双向链表(返回头节点指针)
DLinkedList initDLinkedList() {
DLinkedList head = (DLinkedList)malloc(sizeof(DNode));
if (head == NULL) {
printf("内存分配失败!\n");
exit(1);
}
head->prev = NULL;
head->next = NULL;
return head;
}
3.3.2 插入节点(前插法)
在指定节点p的前面插入新节点,步骤如下:
- 创建新节点并赋值
- 新节点的prev指向p的前驱节点
- 若p的前驱节点不为空,更新其next指向新节点
- 新节点的next指向p
- p的prev指向新节点
// 前插法:在节点p前插入数据为data的节点
void insertBefore(DNode *p, int data) {
if (p == NULL) {
printf("目标节点为空,无法插入!\n");
return;
}
// 1. 创建新节点
DLinkedList newNode = (DLinkedList)malloc(sizeof(DNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(1);
}
newNode->data = data;
// 2. 调整指针(关键步骤)
newNode->prev = p->prev; // 新节点的前驱 = p的前驱
if (p->prev != NULL) {
p->prev->next = newNode; // p的前驱的后继 = 新节点
}
newNode->next = p; // 新节点的后继 = p
p->prev = newNode; // p的前驱 = 新节点
}
3.3.3 双向遍历链表
支持正向遍历(从头部到尾部)和反向遍历(从尾部到头部):
// 正向遍历双向链表
void traverseForward(DLinkedList head) {
DLinkedList p = head->next;
if (p == NULL) {
printf("双向链表为空!\n");
return;
}
printf("正向遍历:");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 反向遍历双向链表
void traverseBackward(DLinkedList head) {
DLinkedList p = head;
// 先找到尾节点
while (p->next != NULL) {
p = p->next;
}
if (p == head) { // 链表为空(只有头节点)
printf("双向链表为空!\n");
return;
}
printf("反向遍历:");
while (p != head) {
printf("%d ", p->data);
p = p->prev; // 反向移动
}
printf("\n");
}
3.3.4 删除指定节点
由于双向链表可直接访问前驱节点,删除操作无需遍历查找,步骤如下:
- 若待删除节点p的前驱不为空,更新其next指向p的后继
- 若待删除节点p的后继不为空,更新其prev指向p的前驱
- 释放p的内存
// 删除双向链表中的节点p
void deleteDNode(DNode *p) {
if (p == NULL) {
printf("待删除节点为空!\n");
return;
}
// 1. 调整前驱节点的指针
if (p->prev != NULL) {
p->prev->next = p->next;
}
// 2. 调整后继节点的指针
if (p->next != NULL) {
p->next->prev = p->prev;
}
// 3. 释放节点内存
free(p);
printf("节点删除成功!\n");
}
3.4 双向链表完整示例
int main() {
// 1. 初始化双向链表
DLinkedList head = initDLinkedList();
// 2. 尾插法添加节点(借助前插法实现)
DNode *tail = head; // 尾节点初始为头节点
insertBefore(tail->next, 10); // 在尾节点后插入(即尾插)
tail = tail->next; // 更新尾节点
insertBefore(tail->next, 20);
tail = tail->next;
insertBefore(tail->next, 30);
tail = tail->next;
// 3. 遍历链表
traverseForward(head); // 输出:10 20 30
traverseBackward(head); // 输出:30 20 10
// 4. 删除中间节点(数据为20的节点)
DNode *p = head->next->next; // 指向数据为20的节点
deleteDNode(p);
traverseForward(head); // 输出:10 30
return 0;
}
四、单链表与双向链表对比
|
对比维度 |
单链表 |
双向链表 |
|
节点结构 |
1 个指针域(next) |
2 个指针域(prev+next) |
|
内存占用 |
较小(每个节点少一个指针) |
较大(每个节点多一个指针) |
|
遍历方向 |
仅正向(从头到尾) |
双向(正向 + 反向) |
|
插入操作效率 |
需查找前驱节点(O (n)) |
直接访问前驱(O (1)) |
|
删除操作效率 |
需查找前驱节点(O (n)) |
直接访问前驱(O (1)) |
|
实现复杂度 |
简单 |
较复杂(指针调整步骤多) |
|
适用场景 |
仅需正向遍历、内存有限场景 |
需双向遍历、频繁插入删除场景 |
五、常见问题与注意事项
- 内存泄漏问题:链表节点通过malloc分配内存,删除节点或销毁链表时必须用free释放,否则会导致内存泄漏。
- 空指针访问:操作前需判断指针是否为NULL(如头节点、待删除节点),避免程序崩溃。
- 头节点作用:头节点不存储实际数据,仅用于简化操作(如避免插入第一个节点时判断头指针是否为空)。
- 循环链表:链表尾节点的next指向头节点(单链表)或头节点的prev指向尾节点(双向链表),形成循环结构,适用于需要循环访问的场景(如约瑟夫环问题)。
六、总结
单链表和双向链表是 C 语言中最基础的链表结构,掌握它们是学习更复杂数据结构(如链表哈希表、二叉树)的基础。选择哪种链表需根据实际需求:
- 若内存资源有限、仅需正向遍历,优先选择单链表
- 若需频繁双向遍历、插入删除操作频繁,优先选择双向链表
通过本文的讲解和代码示例,相信读者已掌握两种链表的核心操作。建议结合实际问题(如链表反转、链表排序、链表合并)进行练习,进一步巩固知识点。
1679

被折叠的 条评论
为什么被折叠?



