源代码:
cpp
#include <stdio.h>
#include <stdlib.h>
//函数结果状态代码
#define OK 1
#define ERROR 0
typedef int Status;//函数返回状态,ok,error
typedef int Elemtype;//链表元素为整形
typedef struct Dulnode//定义结构体
{
Elemtype data;//数据域
struct Dulnode* next;
struct Dulnode* prior;//指针域(前后都要有)
}DulLnode,*LinkList;//单个结点,整个链表(指向结点的指针)
//初始化链表
Status InitLinkList(LinkList* L){
*L=(DulLnode*)malloc(sizeof(DulLnode));
if((*L)==NULL){
return ERROR;//判断是否分配成功
}
(*L)->next=NULL;//前指针为空
(*L)->prior=NULL;//后指针为空
return OK;
}
//判断链表是否为空
Status IsEmptyLinkList(const LinkList* L){
if((*L)->next==NULL && (*L)->prior==NULL){
printf("该链表为空!\n");
return ERROR;
}else{
return OK;
}
}
//判断链表长度
Status LenLinkList(const LinkList* L){
IsEmptyLinkList(L);
DulLnode* p;
int i=0;
p=(*L)->next;
while (p!=NULL)
{
i++;
p=p->next;
}
printf("该链表的长度为:%d\n",i);
return OK;
}
//清空链表
Status ClearLinkList(LinkList* L){
DulLnode* p;
DulLnode* q;
p=(*L)->next;
while (p!=NULL)
{
q=p;
p=p->next;
free(q);
}
(*L)->next = NULL;
printf("该链表已清空!\n");
return OK;
}
Status DestoryLinkList(LinkList* L) {
ClearLinkList(L); // 1. 先清空数据节点
free(*L); // 2. 释放头节点内存
*L = NULL; // 3. 头节点指针置空
printf("该链表已销毁!\n");
return OK;
}
//双向链表插入,头插法
Status CreateLinkList_H(LinkList* L,int n){
for(int i=0;i<n;i++){
DulLnode* newLnode;
newLnode=(DulLnode*)malloc(sizeof(DulLnode));
if(newLnode==NULL){
return ERROR;//判断是否分配成功
}
printf("请输入第%d个数据:\n",i+1);
scanf("%d",&newLnode->data);
newLnode->next = (*L)->next;
newLnode->prior = (*L);
// 如果原链表非空,更新原第一个节点的前驱
if ((*L)->next != NULL) {
(*L)->next->prior = newLnode;
}
(*L)->next = newLnode;
}
return OK;
}
//双向链表插入,尾插法
Status CreateLinkList_R(LinkList* L,int n){
for(int i=0;i<n;i++){
int j=1;
DulLnode* newLnode;
newLnode=(DulLnode*)malloc(sizeof(DulLnode));
if(newLnode==NULL){
return ERROR;//判断是否分配成功
}
DulLnode* p=(*L);
while (p->next!=NULL)
{
j++;
p=p->next;
}
printf("请输入第%d个数据:\n",j+1);
scanf("%d",&newLnode->data);
newLnode->prior=p;
newLnode->next=p->next;
p->next=newLnode;
p=newLnode;
}
return OK;
}
//查看当前链表
Status ShowLinkList(const LinkList* L){
IsEmptyLinkList(L);
DulLnode* p;
p=(*L)->next;
int i=1;
printf("该链表的数据为:\n");
while (p!=NULL)
{
printf("%d : %d\n", i, p->data); // 打印序号和数据
i++;
p=p->next;
}
return OK;
}
int main(){
LinkList mylist;
mylist=NULL;
InitLinkList(&mylist);
CreateLinkList_H(&mylist,4);
LenLinkList(&mylist);
CreateLinkList_R(&mylist,4);
ShowLinkList(&mylist);
LenLinkList(&mylist);
ClearLinkList(&mylist);
DestoryLinkList(&mylist);
}
C语言双向链表完全解析
一、双向链表基础概念
1.1 什么是双向链表?
双向链表是一种链式存储结构,每个节点包含三个部分:
-
数据域:存储具体数据。
-
前驱指针(prior):指向前一个节点的地址。
-
后继指针(next):指向后一个节点的地址。
图示:
头节点 → [数据节点1] ↔ [数据节点2] ↔ [数据节点3] → NULL
-
特点:
-
可以从头节点正向遍历到尾节点。
-
可以从尾节点逆向遍历回头节点。
-
插入和删除操作需要维护前驱和后继指针。
-
1.2 双向链表 vs 单向链表
特性 | 单向链表 | 双向链表 |
---|---|---|
遍历方向 | 只能单向(从头到尾) | 可以双向遍历 |
内存占用 | 每个节点少一个指针 | 每个节点多一个指针 |
删除节点效率 | O(n)(需找到前驱节点) | O(1)(直接通过prior访问) |
适用场景 | 只需单向操作 | 需要双向操作(如浏览器历史记录) |
二、代码逐行解析与图解
2.1 头文件与宏定义
#include <stdio.h> // 输入输出函数(如printf、scanf)
#include <stdlib.h> // 内存管理函数(如malloc、free)
#define OK 1 // 操作成功状态码
#define ERROR 0 // 操作失败状态码
知识点:
-
#include
:预处理指令,引入外部库的功能。 -
#define
:定义常量,提高代码可读性。
2.2 类型定义
typedef int Status; // 函数返回状态类型(OK/ERROR)
typedef int Elemtype; // 链表元素类型为整型
typedef struct Dulnode { // 双向链表节点结构体
Elemtype data; // 数据域(存储具体数值)
struct Dulnode* next; // 指向下一个节点的指针
struct Dulnode* prior; // 指向前一个节点的指针
} DulLnode, *LinkList; // DulLnode是节点类型,LinkList是头节点指针类型
图解:
DulLnode结构体:
+--------+--------+--------+
| prior | data | next |
+--------+--------+--------+
知识点:
-
typedef
:为类型定义别名,简化代码。 -
LinkList
:指向头节点的指针,代表整个链表。
2.3 初始化链表 InitLinkList
Status InitLinkList(LinkList* L) {
*L = (DulLnode*)malloc(sizeof(DulLnode)); // 1. 创建头节点
if (*L == NULL) return ERROR; // 2. 内存分配失败检查
(*L)->next = NULL; // 3. 头节点的next指针初始化为空
(*L)->prior = NULL; // 4. 头节点的prior指针初始化为空
return OK; // 5. 初始化成功
}
步骤详解:
-
分配内存 :使用
malloc
为头节点分配内存空间。 -
错误处理 :如果内存不足,返回
ERROR
。 -
初始化指针 :头节点的
next
和prior
均设为NULL
,表示空链表。
图解:
初始化前:mylist → NULL
初始化后:mylist → [头节点] (next=NULL, prior=NULL)
常见问题:
-
为什么头节点的prior要设为NULL?
头节点是链表的逻辑起点,没有前驱节点,因此
prior
始终为NULL
。
2.4 判断链表是否为空 IsEmptyLinkList
Status IsEmptyLinkList(const LinkList* L) {
if ((*L)->next == NULL && (*L)->prior == NULL) { // 判断头节点的指针
printf("该链表为空!\n");
return ERROR; // 空链表返回ERROR
} else {
return OK; // 非空返回OK
}
}
逻辑分析:
-
如果头节点的
next
和prior
均为NULL
,说明链表为空。 -
注意 :在标准双向链表中,头节点的
prior
始终为NULL
,因此只需检查next
是否为空即可。
示例:
-
空链表:
头节点 → NULL
-
非空链表:
头节点 → [数据节点1] ↔ [数据节点2]
2.5 计算链表长度 LenLinkList
Status LenLinkList(const LinkList* L) {
IsEmptyLinkList(L); // 1. 先检查链表是否为空
DulLnode* p; // 2. 遍历指针
int i = 0; // 3. 计数器
p = (*L)->next; // 4. p指向第一个数据节点
while (p != NULL) { // 5. 遍历直到链表末尾
i++;
p = p->next;
}
printf("该链表的长度为:%d\n", i);
return OK;
}
图解:
链表结构:头节点 → [10] ↔ [20] ↔ [30] → NULL
遍历过程:
- p = 10 → i=1
- p = 20 → i=2
- p = 30 → i=3
- p = NULL → 结束
最终输出:该链表的长度为:3
知识点:
- 遍历链表 :从头节点的
next
开始,逐个访问节点,直到next
为NULL
。
2.6 清空链表 ClearLinkList
Status ClearLinkList(LinkList* L) {
DulLnode* p = (*L)->next; // 1. p指向第一个数据节点
DulLnode* q; // 2. 临时指针用于释放内存
while (p != NULL) { // 3. 遍历所有数据节点
q = p; // 4. 记录当前节点
p = p->next; // 5. p移动到下一个节点
free(q); // 6. 释放当前节点内存
}
(*L)->next = NULL; // 7. 头节点的next重置为NULL
printf("该链表已清空!\n");
return OK;
}
图解:
清空前:头节点 → [10] ↔ [20] ↔ [30] → NULL
清空后:头节点 → NULL
关键点:
- 重置头节点指针 :释放所有数据节点后,必须将头节点的
next
设为NULL
,避免悬垂指针。
2.7 销毁链表 DestoryLinkList
Status DestoryLinkList(LinkList* L) {
ClearLinkList(L); // 1. 先清空数据节点
free(*L); // 2. 释放头节点内存
*L = NULL; // 3. 头节点指针置空
printf("该链表已销毁!\n");
return OK;
}
销毁过程:
-
清空数据节点 :调用
ClearLinkList
释放所有数据节点。 -
释放头节点 :使用
free
释放头节点内存。 -
置空指针 :将链表指针
*L
设为NULL
,防止野指针。
示例:
销毁前:mylist → 头节点 → NULL
销毁后:mylist → NULL
2.8 头插法插入节点 CreateLinkList_H
Status CreateLinkList_H(LinkList* L, int n) {
for (int i = 0; i < n; i++) {
DulLnode* newLnode = (DulLnode*)malloc(sizeof(DulLnode)); // 1. 创建新节点
if (newLnode == NULL) return ERROR;
printf("请输入第%d个数据:\n", i+1);
scanf("%d", &newLnode->data); // 2. 输入数据
newLnode->next = (*L)->next; // 3. 新节点next指向原第一个节点
newLnode->prior = *L; // 4. 新节点prior指向头节点
if ((*L)->next != NULL) { // 5. 如果原链表非空
(*L)->next->prior = newLnode; // 原第一个节点的prior指向新节点
}
(*L)->next = newLnode; // 6. 头节点的next指向新节点
}
return OK;
}
图解(插入数据10和20):
初始链表:头节点 → NULL
插入10后:
头节点 → [10] (prior=头节点, next=NULL)
插入20后:
头节点 → [20] (prior=头节点) ↔ [10] (prior=20)
步骤详解:
-
创建新节点:动态分配内存。
-
输入数据:用户输入节点值。
-
链接新节点 :新节点的
next
指向原第一个节点。 -
设置前驱 :新节点的
prior
指向头节点。 -
更新原节点 :如果原链表非空,原第一个节点的
prior
指向新节点。 -
更新头节点 :头节点的
next
指向新节点。
常见错误:
- 空指针崩溃 :如果原链表为空,
(*L)->next->prior
会导致崩溃,因此需要条件判断。
2.9 尾插法插入节点 CreateLinkList_R
Status CreateLinkList_R(LinkList* L, int n) {
for (int i = 0; i < n; i++) {
DulLnode* newLnode = (DulLnode*)malloc(sizeof(DulLnode)); // 1. 创建新节点
if (newLnode == NULL) return ERROR;
DulLnode* p = *L; // 2. p指向头节点
while (p->next != NULL) { // 3. 找到尾节点
p = p->next;
}
printf("请输入第%d个数据:\n", i+1);
scanf("%d", &newLnode->data); // 4. 输入数据
newLnode->prior = p; // 5. 新节点的prior指向尾节点
newLnode->next = p->next; // 6. 新节点的next设为NULL
p->next = newLnode; // 7. 尾节点的next指向新节点
}
return OK;
}
图解(插入数据30和40):
初始链表:头节点 → [10] ↔ [20] → NULL
插入30后:
头节点 → [10] ↔ [20] ↔ [30] → NULL
插入40后:
头节点 → [10] ↔ [20] ↔ [30] ↔ [40] → NULL
步骤详解:
-
创建新节点:动态分配内存。
-
定位尾节点 :从头节点出发,遍历到
next
为NULL
的节点。 -
链接新节点:
-
新节点的
prior
指向尾节点。 -
新节点的
next
指向NULL
(即p->next
的值)。 -
尾节点的
next
指向新节点。
-
常见错误:
- 未找到尾节点 :若链表为空,
p->next
为NULL
,直接插入到头节点之后。
2.10 显示链表 ShowLinkList
Status ShowLinkList(const LinkList* L) {
IsEmptyLinkList(L); // 1. 检查链表是否为空
DulLnode* p = (*L)->next; // 2. p指向第一个数据节点
int i = 1;
printf("该链表的数据为:\n");
while (p != NULL) { // 3. 遍历链表
printf("%d : %d\n", i, p->data); // 4. 打印序号和数据
i++;
p = p->next; // 5. 移动到下一个节点
}
return OK;
}
输出示例:
该链表的数据为:
1 : 20
2 : 10
3 : 30
4 : 40
三、主函数流程解析
int main() {
LinkList mylist; // 定义链表指针
mylist = NULL; // 初始化为NULL
InitLinkList(&mylist); // 初始化链表(创建头节点)
CreateLinkList_H(&mylist, 4); // 头插法插入4个节点
LenLinkList(&mylist); // 计算长度
CreateLinkList_R(&mylist, 4); // 尾插法再插入4个节点
ShowLinkList(&mylist); // 显示链表数据
ClearLinkList(&mylist); // 清空数据节点
DestoryLinkList(&mylist); // 销毁链表(包括头节点)
}
执行流程:
-
初始化 :创建头节点,链表结构为
头节点 → NULL
。 -
头插法插入4个节点:数据按逆序插入,如输入顺序为1,2,3,4,链表顺序为4,3,2,1。
-
计算长度:输出链表长度为4。
-
尾插法再插入4个节点:数据按顺序追加,链表变为4,3,2,1,5,6,7,8。
-
显示链表:打印所有节点数据。
-
清空链表:释放所有数据节点,头节点保留。
-
销毁链表:释放头节点,链表指针置空。
四、常见问题与调试技巧
4.1 内存泄漏检测
-
工具:使用Valgrind(Linux)或Visual Studio内存调试器。
-
示例:未释放节点会导致内存泄漏,通过工具可定位未释放的内存块。
4.2 空指针崩溃
-
场景:在空链表上执行删除或访问操作。
-
预防:在操作前检查链表是否为空。
4.3 指针操作错误
-
示例 :未正确设置
prior
指针,导致逆向遍历失败。 -
调试 :逐步打印每个节点的
prior
和next
值,验证指针是否正确。
五、总结与拓展
5.1 核心知识点
-
双向链表结构:前驱和后继指针的维护。
-
内存管理:动态分配与释放,避免泄漏。
-
边界条件处理:空链表、头尾节点操作。
5.2 拓展应用
-
双向循环链表 :尾节点的
next
指向头节点,头节点的prior
指向尾节点。 -
LRU缓存淘汰算法:利用双向链表快速移动节点。
5.3 学习建议
-
动手实践:手动绘制链表操作图示。
-
代码调试:通过调试器观察指针变化。
-
阅读源码 :研究Linux内核中的链表实现(
list.h
)。
单链表教程:
(C语言)单链表(2.0)数据结构(指针,单链表教程)-CSDN博客
运行结果:
cpp
请输入第1个数据:
10
请输入第2个数据:
20
请输入第3个数据:
30
请输入第4个数据:
40
该链表的长度为:4
请输入第6个数据:
1
请输入第7个数据:
2
请输入第8个数据:
3
请输入第9个数据:
4
该链表的数据为:
1 : 40
2 : 30
3 : 20
4 : 10
5 : 1
6 : 2
7 : 3
8 : 4
该链表的长度为:8
该链表已清空!
该链表已销毁!
请按任意键继续. . .
注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!