(C语言)双向链表(教程)(指针)(数据结构)

源代码:

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:指向头节点的指针,代表整个链表。


复制代码
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. 初始化成功
}

步骤详解

  1. 分配内存 :使用malloc为头节点分配内存空间。

  2. 错误处理 :如果内存不足,返回ERROR

  3. 初始化指针 :头节点的nextprior均设为NULL,表示空链表。

图解

复制代码
初始化前:mylist → NULL
初始化后:mylist → [头节点] (next=NULL, prior=NULL)

常见问题

  • 为什么头节点的prior要设为NULL?

    头节点是链表的逻辑起点,没有前驱节点,因此prior始终为NULL


复制代码
Status IsEmptyLinkList(const LinkList* L) {
    if ((*L)->next == NULL && (*L)->prior == NULL) { // 判断头节点的指针
        printf("该链表为空!\n");
        return ERROR; // 空链表返回ERROR
    } else {
        return OK;    // 非空返回OK
    }
}

逻辑分析

  • 如果头节点的nextprior均为NULL,说明链表为空。

  • 注意 :在标准双向链表中,头节点的prior始终为NULL,因此只需检查next是否为空即可。

示例

  • 空链表:头节点 → NULL

  • 非空链表:头节点 → [数据节点1] ↔ [数据节点2]


复制代码
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开始,逐个访问节点,直到nextNULL

复制代码
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,避免悬垂指针。

复制代码
Status DestoryLinkList(LinkList* L) {
    ClearLinkList(L); // 1. 先清空数据节点
    free(*L);         // 2. 释放头节点内存
    *L = NULL;        // 3. 头节点指针置空
    printf("该链表已销毁!\n");
    return OK;
}

销毁过程

  1. 清空数据节点 :调用ClearLinkList释放所有数据节点。

  2. 释放头节点 :使用free释放头节点内存。

  3. 置空指针 :将链表指针*L设为NULL,防止野指针。

示例

复制代码
销毁前:mylist → 头节点 → NULL
销毁后:mylist → NULL

复制代码
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)

步骤详解

  1. 创建新节点:动态分配内存。

  2. 输入数据:用户输入节点值。

  3. 链接新节点 :新节点的next指向原第一个节点。

  4. 设置前驱 :新节点的prior指向头节点。

  5. 更新原节点 :如果原链表非空,原第一个节点的prior指向新节点。

  6. 更新头节点 :头节点的next指向新节点。

常见错误

  • 空指针崩溃 :如果原链表为空,(*L)->next->prior会导致崩溃,因此需要条件判断。

复制代码
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

步骤详解

  1. 创建新节点:动态分配内存。

  2. 定位尾节点 :从头节点出发,遍历到nextNULL的节点。

  3. 链接新节点

    • 新节点的prior指向尾节点。

    • 新节点的next指向NULL(即p->next的值)。

    • 尾节点的next指向新节点。

常见错误

  • 未找到尾节点 :若链表为空,p->nextNULL,直接插入到头节点之后。

复制代码
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); // 销毁链表(包括头节点)
}

执行流程

  1. 初始化 :创建头节点,链表结构为头节点 → NULL

  2. 头插法插入4个节点:数据按逆序插入,如输入顺序为1,2,3,4,链表顺序为4,3,2,1。

  3. 计算长度:输出链表长度为4。

  4. 尾插法再插入4个节点:数据按顺序追加,链表变为4,3,2,1,5,6,7,8。

  5. 显示链表:打印所有节点数据。

  6. 清空链表:释放所有数据节点,头节点保留。

  7. 销毁链表:释放头节点,链表指针置空。


四、常见问题与调试技巧

4.1 内存泄漏检测

  • 工具:使用Valgrind(Linux)或Visual Studio内存调试器。

  • 示例:未释放节点会导致内存泄漏,通过工具可定位未释放的内存块。

4.2 空指针崩溃

  • 场景:在空链表上执行删除或访问操作。

  • 预防:在操作前检查链表是否为空。

4.3 指针操作错误

  • 示例 :未正确设置prior指针,导致逆向遍历失败。

  • 调试 :逐步打印每个节点的priornext值,验证指针是否正确。


五、总结与拓展

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
该链表已清空!
该链表已销毁!

请按任意键继续. . .

注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!

相关推荐
the sun343 分钟前
数据结构---跳表
数据结构
小黑屋的黑小子12 分钟前
【数据结构】反射、枚举以及lambda表达式
数据结构·面试·枚举·lambda表达式·反射机制
LJianK113 分钟前
array和list在sql中的foreach写法
数据结构·sql·list
mahuifa15 分钟前
(2)VTK C++开发示例 --- 绘制多面锥体
c++·vtk·cmake·3d开发
xiongmaodaxia_z717 分钟前
python每日一练
开发语言·python·算法
Chandler2431 分钟前
Go:接口
开发语言·后端·golang
Jasmin Tin Wei32 分钟前
css易混淆的知识点
开发语言·javascript·ecmascript
&白帝&32 分钟前
java HttpServletRequest 和 HttpServletResponse
java·开发语言
ErizJ33 分钟前
Golang|Channel 相关用法理解
开发语言·后端·golang
automan0233 分钟前
golang 在windows 系统的交叉编译
开发语言·后端·golang