数据结构--带头双向循环链表(C语言实现)

一、前言:为什么要学带头双向循环链表?

在数据结构的学习中,链表是线性表的核心实现之一,而带头双向循环链表是链表家族中最实用、最优雅的结构:

  1. 带头:拥有一个不存储有效数据的头节点,极大简化插入 / 删除逻辑,无需处理头节点为空的边界情况;
  2. 双向:每个节点同时保存前驱指针(prev)和后继指针(next),支持正向、反向双向遍历;
  3. 循环:尾节点的后继指针指向头节点,头节点的前驱指针指向尾节点,无NULL 指针,避免空指针访问异常。

它完美解决了单链表查找前驱节点困难、尾插 / 尾删效率低的痛点,是实际开发中(如 LRU 缓存、底层队列)的首选链表结构。

本篇博客将基于标准 C 语言实现,逐行解析代码,精准讲解每个接口的功能、实现逻辑,带你彻底掌握带头双向循环链表。

二、基础结构定义

在实现链表功能前,我们先定义节点结构和数据类型,为后续操作打下基础:

c 复制代码
#pragma once  // 防止头文件重复包含
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

// 重定义链表存储的数据类型,方便后期修改类型(如改为char/double)
typedef int ListDataType;

// 带头双向循环链表节点结构
typedef struct LTNode
{
    ListDataType data;       // 节点存储的数据
    struct LTNode* prev;     // 前驱指针:指向当前节点的上一个节点
    struct LTNode* next;     // 后继指针:指向当前节点的下一个节点
}LTNode;

核心结构说明

  1. ListDataType:使用typedef重定义数据类型,后期只需修改这一行,就能让链表存储任意类型数据;
  2. prev指针:双向链表核心,让每个节点能快速找到前驱节点;
  3. next指针:基础指针,用于向后遍历节点。

三、工具函数:创建新节点

所有插入操作都需要先创建新节点,我们封装一个独立工具函数:

c 复制代码
// 功能:创建一个新节点,初始化数据和指针,返回新节点地址
// 参数:x - 新节点要存储的数据
// 返回值:新节点的指针
LTNode* BuyLTNode(ListDataType x)
{
    // 申请节点内存
    LTNode* NewNode = (LTNode*)malloc(sizeof(LTNode));
    // 内存申请失败处理
    if (NewNode == NULL)
    {
        perror("malloc failed!");  // 打印错误信息
        exit(-1);                  // 终止程序
    }
    // 初始化节点数据
    NewNode->data = x;
    // 新节点默认前驱、后继指针置空
    NewNode->next = NewNode->prev = NULL;
    return NewNode;
}

实现逻辑

  1. 内存申请 :使用malloc为新节点分配堆内存,大小为LTNode结构体大小;
  2. 异常判断:检查内存申请结果,失败则打印错误并退出程序,避免空指针操作;
  3. 节点初始化:给节点数据域赋值,将前驱、后继指针初始化为NULL
  4. 返回节点:返回创建完成的新节点指针,供后续插入操作使用。

四、核心接口完整解析

1. 创建并初始化带头节点的双向循环链表

c 复制代码
// 功能:创建一个带头节点的双向循环链表,初始化头节点
// 返回值:链表头节点的指针
LTNode* ListCreateHead()
{
    // 创建头节点,数据域填-1(标识为头节点,无实际意义)
    LTNode* Head = BuyLTNode(-1);
    // 双向循环核心:空链表时,头节点的prev和next都指向自己
    Head->next = Head->prev = Head;
    return Head;
}

实现逻辑

  1. 创建头节点:调用BuyLTNode创建头节点,数据域填充标识值;
  2. 循环初始化:空链表状态下,头节点的prevnext指针都指向自身,满足双向循环特性;
  3. 返回头节点:返回链表头指针,作为整个链表的操作入口。

2. 判断链表是否为空

c 复制代码
// 功能:判断链表是否为空链表(仅存头节点)
// 参数:phead - 链表头节点指针
// 返回值:空链表返回true,非空返回false
bool ListEmpty(LTNode* phead)
{
    assert(phead);  // 断言:头节点指针不能为空
    // 核心判断:头节点的next指向自己,说明无有效数据节点
    return phead->next == phead;
}

实现逻辑

  1. 合法性校验:用assert断言头节点非空,防止空指针访问;
  2. 空链表判断:带头双向循环链表中,仅头节点存在时即为空,判断条件为phead->next == phead
  3. 返回结果:返回bool值表示链表是否为空。

3. 正向遍历打印链表

c 复制代码
// 功能:正向遍历链表,打印所有有效数据节点
// 参数:phead - 链表头节点指针
void ListPrintForward(LTNode* phead)
{
    // 断言:链表非空,禁止打印空链表
    assert(!ListEmpty(phead));
    // 正向遍历起始点:头节点的下一个节点
    LTNode* pcur = phead->next;
    printf("PrintForward: ");
    // 循环遍历:直到回到头节点停止
    while (pcur != phead)
    {
        printf("%d", pcur->data);
        // 非尾节点时打印分隔符,美化输出
        if (pcur->next != phead)
        {
            printf(" -> ");
        }
        pcur = pcur->next;  // 指针后移
    }
    printf("\n");
}

实现逻辑

  1. 合法性校验:断言链表非空,避免空链表打印;
  2. 遍历初始化:正向遍历从phead->next(第一个有效数据节点)开始;
  3. 循环打印:循环条件为pcur != phead,遍历到尾节点(回到头节点)时停止;
  4. 格式优化:非尾节点时打印分隔符,保证输出格式美观;
  5. 指针移动:每次循环通过next指针向后移动遍历指针。

4. 反向遍历打印链表

c 复制代码
// 功能:反向遍历链表,打印所有有效数据节点
// 参数:phead - 链表头节点指针
void ListPrintBackward(LTNode* phead)
{
    // 断言:链表非空
    assert(!ListEmpty(phead));
    // 反向遍历起始点:头节点的前驱节点(最后一个有效数据节点)
    LTNode* pcur = phead->prev;
    printf("PrintBackward: ");
    // 循环遍历:直到回到头节点停止
    while (pcur != phead)
    {
        printf("%d", pcur->data);
        // 非首节点时打印分隔符
        if (pcur->prev != phead)
        {
            printf(" -> ");
        }
        pcur = pcur->prev;  // 指针前移
    }
    printf("\n");
}

实现逻辑

  1. 合法性校验:断言链表非空;
  2. 遍历初始化:反向遍历从phead->prev(最后一个有效数据节点)开始;
  3. 循环打印:循环条件为pcur != phead,遍历到第一个节点(回到头节点)时停止;
  4. 格式优化:非首节点时打印分隔符;
  5. 指针移动:每次循环通过prev指针向前移动遍历指针。

5. 销毁链表(释放所有内存)

c 复制代码
// 功能:销毁整个链表,释放所有节点内存,头节点置空
// 参数:pphead - 链表头节点的二级指针(需要修改头指针本身)
void ListDestroy(LTNode** pphead)
{
    assert(pphead);  // 断言:二级指针不能为空
    // 空链表直接返回,无需释放
    if (ListEmpty(*pphead))
    {
        return;
    }
    // 从第一个数据节点开始遍历释放
    LTNode* pcur = (*pphead)->next;
    while (pcur != (*pphead))
    {
        LTNode* del = pcur;    // 记录待释放节点
        pcur = pcur->next;     // 指针后移,防止断链
        free(del);             // 释放节点内存
        del = NULL;            // 指针置空,防止野指针
    }
    free(*pphead);    // 释放头节点内存
    *pphead = NULL;    // 头指针置空,彻底销毁链表
}

实现逻辑

  1. 合法性校验:断言二级指针非空;
  2. 空链表处理:若链表为空,直接返回;
  3. 遍历释放数据节点:从第一个数据节点开始,逐个释放内存,释放前先移动指针,避免断链;
  4. 释放头节点:数据节点释放完成后,释放头节点内存;
  5. 头指针置空:通过二级指针将外部头指针置空,防止野指针。

6. 清空链表(保留头节点)

c 复制代码
// 功能:清空链表所有有效数据节点,仅保留头节点
// 参数:phead - 链表头节点指针
void ListKeepHead(LTNode* phead)
{
    assert(phead);  // 断言:头节点非空
    // 空链表无需清空
    if (ListEmpty(phead))
    {
        return;
    }
    // 遍历释放所有数据节点
    LTNode* pcur = phead->next;
    while (pcur != phead)
    {
        LTNode* del = pcur;
        pcur = pcur->next;
        free(del);
        del = NULL;
    }
    // 恢复空链表状态:头节点指针指向自己
    phead->prev = phead->next = phead;
}

实现逻辑

  1. 合法性校验:断言头节点非空;
  2. 空链表处理:空链表直接返回;
  3. 释放数据节点:遍历并释放所有有效数据节点;
  4. 恢复空链表结构:将头节点的prevnext指针重新指向自身,回到初始空链表状态。

7. 头插法(在头节点后插入新节点)

c 复制代码
// 功能:头插法,在头节点后插入新节点(链表最前端)
// 参数:phead - 头节点指针,x - 待插入数据
void ListPushFront(LTNode* phead, ListDataType x)
{
    assert(phead);  // 断言:头节点非空
    LTNode* NewNode = BuyLTNode(x);  // 创建新节点
    
    // 第一步:新节点绑定前后指针
    NewNode->next = phead->next;  // 新节点后继指向原第一个数据节点
    NewNode->prev = phead;        // 新节点前驱指向头节点

    // 第二步:原链表节点绑定新节点
    phead->next->prev = NewNode;  // 原第一个节点前驱指向新节点
    phead->next = NewNode;        // 头节点后继指向新节点
}

实现逻辑

  1. 合法性校验:断言头节点非空;
  2. 创建新节点:调用BuyLTNode创建待插入节点;
  3. 新节点指针绑定:新节点next指向原第一个数据节点,prev指向头节点;
  4. 原链表节点绑定:原第一个数据节点的prev指向新节点,头节点的next指向新节点;
  5. 核心优势:带头节点无需处理空链表,时间复杂度 O (1)

8. 尾插法(在链表尾部插入新节点)

c 复制代码
// 功能:尾插法,在链表尾部插入新节点
// 参数:phead - 头节点指针,x - 待插入数据
void ListPushBack(LTNode* phead, ListDataType x)
{
    assert(phead);  // 断言:头节点非空
    LTNode* NewNode = BuyLTNode(x);  // 创建新节点

    // 第一步:新节点绑定前后指针
    NewNode->next = phead;        // 新节点后继指向头节点(循环特性)
    NewNode->prev = phead->prev;  // 新节点前驱指向原尾节点

    // 第二步:原链表节点绑定新节点
    phead->prev->next = NewNode;  // 原尾节点后继指向新节点
    phead->prev = NewNode;        // 头节点前驱指向新节点(更新尾节点)
}

实现逻辑

  1. 合法性校验:断言头节点非空;
  2. 创建新节点:创建待插入节点;
  3. 新节点指针绑定:新节点next指向头节点,prev指向原尾节点;
  4. 原链表节点绑定:原尾节点next指向新节点,头节点prev指向新节点;
  5. 核心优势:双向循环链表无需遍历找尾节点,时间复杂度 O (1)

9. 头删(删除第一个数据节点)

c 复制代码
// 功能:头删,删除链表第一个有效数据节点
// 参数:phead - 头节点指针
void ListPopFront(LTNode* phead)
{
    assert(phead);             // 断言:头节点非空
    assert(!ListEmpty(phead)); // 断言:链表非空,禁止删除空链表

    LTNode* del = phead->next;  // 记录待删除的第一个数据节点

    // 重新绑定链表指针,断开待删除节点
    phead->next = phead->next->next;  // 头节点后继指向第二个数据节点
    phead->next->prev = phead;        // 第二个数据节点前驱指向头节点

    free(del);    // 释放节点内存
    del = NULL;   // 指针置空
}

实现逻辑

  1. 双重校验:断言头节点非空、链表非空;
  2. 记录待删节点:保存第一个数据节点地址;
  3. 指针重绑定:头节点指向第二个数据节点,第二个数据节点指向头节点;
  4. 释放内存:释放待删节点,指针置空防止野指针。

10. 尾删(删除最后一个数据节点)

c 复制代码
// 功能:尾删,删除链表最后一个有效数据节点
// 参数:phead - 头节点指针
void ListPopBack(LTNode* phead)
{
    assert(phead);             // 断言:头节点非空
    assert(!ListEmpty(phead)); // 断言:链表非空

    LTNode* del = phead->prev;  // 记录待删除的尾节点

    // 重新绑定链表指针
    phead->prev = phead->prev->prev;  // 头节点前驱指向倒数第二个节点
    phead->prev->next = phead;        // 倒数第二个节点后继指向头节点

    free(del);    // 释放内存
    del = NULL;   // 指针置空
}

实现逻辑

  1. 双重校验:断言头节点非空、链表非空;
  2. 记录待删节点:通过phead->prev直接找到尾节点;
  3. 指针重绑定:头节点指向倒数第二个节点,倒数第二个节点指向头节点;
  4. 释放内存:释放尾节点,指针置空。

11. 查找节点(按值查找)

c 复制代码
// 功能:查找第一个值为x的节点,返回节点地址,找不到返回NULL
// 参数:phead - 头节点指针,x - 待查找数据
// 返回值:找到返回节点指针,否则返回NULL
LTNode* ListFind(LTNode* phead, ListDataType x)
{
    assert(phead);  // 断言:头节点非空
    LTNode* pcur = phead->next;  // 从第一个数据节点开始遍历

    // 遍历整个链表
    while (pcur != phead)
    {
        if (pcur->data == x)  // 找到目标节点
        {
            return pcur;
        }
        pcur = pcur->next;  // 指针后移
    }
    return NULL;  // 未找到返回NULL
}

实现逻辑

  1. 合法性校验:断言头节点非空;
  2. 遍历初始化:从第一个数据节点开始查找;
  3. 循环匹配:遍历链表,对比节点数据,匹配则返回节点指针;
  4. 未找到处理:遍历结束未找到,返回NULL

12. 指定节点前插入(通用插入)

c 复制代码
// 功能:在指定节点pos前插入新节点(最通用的插入接口)
// 参数:phead - 头节点指针,pos - 指定节点,x - 待插入数据
void ListInsertBefore(LTNode* phead, LTNode* pos, ListDataType x)
{
    assert(phead);     // 断言:头节点非空
    assert(pos);       // 断言:指定节点非空
    LTNode* NewNode = BuyLTNode(x);  // 创建新节点

    // 绑定新节点与pos前驱节点
    pos->prev->next = NewNode;
    NewNode->prev = pos->prev;

    // 绑定新节点与pos节点
    NewNode->next = pos;
    pos->prev = NewNode;
}

实现逻辑

  1. 合法性校验:断言头节点、指定节点均非空;
  2. 创建新节点:创建待插入节点;
  3. 前驱节点绑定:pos 的前驱节点后继指向新节点,新节点前驱指向 pos 的前驱节点;
  4. 目标节点绑定:新节点后继指向 pospos前驱指向新节点;
  5. 通用性:头插、尾插都可通过该函数实现。

13. 指定节点后插入

c 复制代码
// 功能:在指定节点pos后插入新节点
// 参数:phead - 头节点指针,pos - 指定节点,x - 待插入数据
void ListInsertAfter(LTNode* phead, LTNode* pos, ListDataType x)
{
    assert(phead);     // 断言:头节点非空
    assert(pos);       // 断言:指定节点非空
    LTNode* NewNode = BuyLTNode(x);

    // 绑定新节点与pos后继节点
    pos->next->prev = NewNode;
    NewNode->next = pos->next;

    // 绑定新节点与pos节点
    NewNode->prev = pos;
    pos->next = NewNode;
}

实现逻辑

  1. 合法性校验:断言头节点、指定节点均非空;
  2. 创建新节点:创建待插入节点;
  3. 后继节点绑定:pos 的后继节点前驱指向新节点,新节点后继指向pos 的后继节点;
  4. 目标节点绑定:新节点前驱指向pospos 后继指向新节点。

14. 获取链表有效节点个数

c 复制代码
// 功能:获取链表中有效数据节点的总个数
// 参数:phead - 头节点指针
// 返回值:有效节点数量
int ListSize(LTNode* phead)
{
    assert(phead);  // 断言:头节点非空
    LTNode* pcur = phead->next;
    int count = 0;  // 计数器

    // 遍历统计节点数量
    while (pcur != phead)
    {
        count++;
        pcur = pcur->next;
    }
    return count;
}

实现逻辑

  1. 合法性校验:断言头节点非空;
  2. 初始化计数器:定义变量统计节点数;
  3. 遍历统计:遍历所有有效数据节点,每遍历一个节点计数器 + 1
  4. 返回结果:返回有效节点总数。

15. 按位置插入(前插)

c 复制代码
// 功能:在第pos个数据节点前插入(pos从1开始)
// 参数:phead - 头节点指针,pos - 插入位置,x - 待插入数据
void ListInsertByPosBefore(LTNode* phead, int pos, ListDataType x)
{
    assert(phead);                      // 头节点非空
    assert(!ListEmpty(phead));          // 链表非空
    assert(pos >= 1 && pos <= ListSize(phead));  // 位置合法

    LTNode* pcur = phead->next;
    int count = 1;
    // 遍历找到第pos个节点
    while (count < pos)
    {
        count++;
        pcur = pcur->next;
    }
    ListInsertBefore(phead, pcur, x);  // 调用通用前插函数
}

实现逻辑

  1. 三重校验:校验头节点、链表非空、插入位置合法;
  2. 定位目标节点:遍历找到第pos个数据节点;
  3. 复用接口:调用ListInsertBefore完成插入,代码复用。

16. 按位置插入(后插)

c 复制代码
// 功能:在第pos个数据节点后插入(pos从1开始)
void ListInsertByPosAfter(LTNode* phead, int pos, ListDataType x)
{
    assert(phead);
    assert(!ListEmpty(phead));
    assert(pos >= 1 && pos <= ListSize(phead));

    LTNode* pcur = phead->next;
    int count = 1;
    while (count < pos)
    {
        count++;
        pcur = pcur->next;
    }
    ListInsertAfter(phead, pcur, x);
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空、插入位置合法;
  2. 定位目标节点:遍历找到第pos个数据节点;
  3. 复用接口:调用ListInsertAfter完成插入。

17. 删除指定节点

c 复制代码
// 功能:删除指定节点pos
// 参数:phead - 头节点指针,pos - 待删除节点
void ListPopPos(LTNode* phead, LTNode* pos)
{
    assert(phead);     // 头节点非空
    assert(pos);       // 待删节点非空

    // 重新绑定指针,跳过待删节点
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;

    free(pos);    // 释放内存
    pos = NULL;   // 指针置空
}

实现逻辑

  1. 合法性校验:断言头节点、待删节点非空;
  2. 指针断链重连:待删节点的前驱节点直接指向其后继节点,完成断链;
  3. 释放内存:释放待删节点,指针置空。

18. 删除第一个值为 x 的节点

c 复制代码
// 功能:删除第一个值为x的有效数据节点
void ListPopFirstX(LTNode* phead, ListDataType x)
{
    assert(phead);
    assert(!ListEmpty(phead));
    // 查找目标节点
    LTNode* del = ListFind(phead, x);
    // 找到则删除
    if (del != NULL)
    {
        ListPopPos(phead, del);
    }
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空;
  2. 查找节点:调用ListFind找到第一个值为x的节点;
  3. 删除节点:找到节点后调用ListPopPos删除,未找到则不操作。

19. 删除所有值为 x 的节点

c 复制代码
// 功能:删除链表中所有值为x的节点
void ListPopAllX(LTNode* phead, ListDataType x)
{
    assert(phead);
    assert(!ListEmpty(phead));
    LTNode* pcur = phead->next;

    while (pcur != phead)
    {
        if (pcur->data == x)
        {
            LTNode* del = pcur;    // 记录待删节点
            pcur = pcur->next;     // 指针先后移,防止断链
            ListPopPos(phead, del);
        }
        else
        {
            pcur = pcur->next;
        }
    }
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空;
  2. 遍历判断:遍历链表,判断节点数据是否等于目标值;
  3. 安全删除:匹配到目标值时,先移动指针再删除,避免断链;
  4. 循环执行:直到遍历完整个链表,删除所有目标节点。

20. 按位置删除节点

c 复制代码
// 功能:删除第pos个数据节点(pos从1开始)
void ListEraseByPos(LTNode* phead, int pos)
{
    assert(phead);
    assert(!ListEmpty(phead));
    assert(pos >= 1 && pos <= ListSize(phead));

    int count = 1;
    LTNode* pcur = phead->next;
    // 定位到第pos个节点
    while (count != pos)
    {
        count++;
        pcur = pcur->next;
    }
    ListPopPos(phead, pcur);  // 删除节点
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空、删除位置合法;
  2. 定位节点:遍历找到第pos个数据节点;
  3. 复用接口:调用ListPopPos完成删除。

21. 按位置修改节点值

c 复制代码
// 功能:修改第pos个节点的数据为x
void ListModifyByPos(LTNode* phead, int pos, ListDataType x)
{
    assert(phead);
    assert(!ListEmpty(phead));
    assert(pos >= 1 && pos <= ListSize(phead));

    int count = 1;
    LTNode* pcur = phead->next;
    // 定位目标节点
    while (count != pos)
    {
        count++;
        pcur = pcur->next;
    }
    pcur->data = x;  // 修改数据
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空、位置合法;
  2. 定位节点:遍历找到第pos个数据节点;
  3. 修改数据:直接修改节点数据域的值。

22. 修改第一个值为 oldX 的节点

c 复制代码
// 功能:将第一个值为oldX的节点修改为newX
void ListModifyX(LTNode* phead, ListDataType oldX, ListDataType newX)
{
    assert(phead);
    assert(!ListEmpty(phead));
    // 新旧值相同,无需修改
    if (oldX == newX)
    {
        return;
    }
    // 查找目标节点
    LTNode* Modify = ListFind(phead, oldX);
    if (Modify != NULL)
    {
        Modify->data = newX;
    }
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空;
  2. 冗余判断:新旧值相同直接返回,提升效率;
  3. 查找修改:找到第一个目标节点后修改数据,未找到则不操作。

23. 修改所有值为 oldX 的节点

c 复制代码
// 功能:将所有值为oldX的节点修改为newX
void ListModifyAllX(LTNode* phead, ListDataType oldX, ListDataType newX)
{
    assert(phead);
    assert(!ListEmpty(phead));
    if (oldX == newX)
    {
        return;
    }
    LTNode* pcur = phead->next;
    // 遍历所有节点
    while (pcur != phead)
    {
        if (pcur->data == oldX)
        {
            pcur->data = newX;
        }
        pcur = pcur->next;
    }
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空;
  2. 冗余判断:新旧值相同直接返回;
  3. 遍历修改:遍历链表,所有匹配节点数据全部修改。

24. 获取第一个数据节点

c 复制代码
// 功能:获取链表第一个有效数据节点
// 返回值:第一个数据节点的指针
LTNode* ListGetFront(LTNode* phead)
{
    assert(phead);
    assert(!ListEmpty(phead));
    return phead->next;
}
  1. 实现逻辑
  2. 合法性校验:校验头节点、链表非空;
  3. 直接返回:第一个数据节点就是phead->next,直接返回指针。

25. 获取最后一个数据节点

c 复制代码
// 功能:获取链表最后一个有效数据节点
// 返回值:最后一个数据节点的指针
LTNode* ListGetBack(LTNode* phead)
{
    assert(phead);
    assert(!ListEmpty(phead));
    return phead->prev;
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空;
  2. 直接返回:尾节点就是phead->prev,直接返回指针。

26. 按位置获取节点

c 复制代码
// 功能:获取第pos个有效数据节点
// 返回值:第pos个节点的指针
LTNode* ListGetNodeByPos(LTNode* phead, int pos)
{
    assert(phead);
    assert(!ListEmpty(phead));
    assert(pos >= 1 && pos <= ListSize(phead));

    int count = 1;
    LTNode* pcur = phead->next;
    // 定位目标节点
    while (count != pos)
    {
        count++;
        pcur = pcur->next;
    }
    return pcur;
}

实现逻辑

  1. 合法性校验:校验头节点、链表非空、位置合法;
  2. 定位节点:遍历找到第pos个数据节点;
  3. 返回节点:返回目标节点指针。

五、所有代码

List.h

c 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef int ListDataType;
typedef struct LTNode
{
	ListDataType data;
	struct LTNode* prev;
	struct LTNode* next;

}LTNode;

// 创建并初始化带头节点的双向循环链表
LTNode* ListCreateHead();

// 判断链表是不是空链表
bool ListEmpty(LTNode* phead);

// 打印链表(正向遍历)
void ListPrintForward(LTNode* phead);

// 打印链表(反向遍历)
void ListPrintBackward(LTNode* phead);

// 链表销毁
void ListDestroy(LTNode** pphead);

// 清空链表:保留头节点,删除所有数据节点
void ListKeepHead(LTNode* phead);

// 头插法:在头节点后插入新节点
void ListPushFront(LTNode* phead, ListDataType x);

// 尾插法:在链表尾部插入新节点
void ListPushBack(LTNode* phead, ListDataType x);

// 头删:删除第一个数据节点
void ListPopFront(LTNode* phead);

// 尾删:删除最后一个数据节点
void ListPopBack(LTNode* phead);

// 查找第一个值为 x 的节点,返回节点地址,找不到返回NULL
LTNode* ListFind(LTNode* phead, ListDataType x);

// 在指定节点 pos 前插入(最通用的插入)
void ListInsertBefore(LTNode* phead, LTNode* pos, ListDataType x);

// 在指定节点 pos 后插入
void ListInsertAfter(LTNode* phead, LTNode* pos, ListDataType x);

// 获取链表有效数据节点个数
int ListSize(LTNode* phead);

// 按位置插入(在第 pos 个数据节点后插入,pos从1开始)
void ListInsertByPosAfter(LTNode* phead, int pos, ListDataType x);

// 按位置插入(在第 pos 个数据节点前插入,pos从1开始)
void ListInsertByPosBefore(LTNode* phead, int pos, ListDataType x);

// 删除指定节点 pos
void ListPopPos(LTNode* phead, LTNode* pos);

// 删除第一个值为 x 的节点
void ListPopFirstX(LTNode* phead, ListDataType x);

// 删除所有值为 x 的节点
void ListPopAllX(LTNode* phead, ListDataType x);

// 按位置删除(删除第 pos 个数据节点,pos 从1开始)
void ListEraseByPos(LTNode* phead, int pos);

// 修改第 pos 个节点的值
void ListModifyByPos(LTNode* phead, int pos, ListDataType x);

// 修改第一个值为 oldX(旧数据) 的节点为 newX(新数据)
void ListModifyX(LTNode* phead, ListDataType oldX, ListDataType newX);

// 修改所有值为 oldX(旧数据) 的节点为 newX(新数据)
void ListModifyAllX(LTNode* phead, ListDataType oldX, ListDataType newX);

// 获取第一个数据节点
LTNode* ListGetFront(LTNode* phead);

// 获取最后一个数据节点
LTNode* ListGetBack(LTNode* phead);

// 获取第 pos 个节点
LTNode* ListGetNodeByPos(LTNode* phead, int pos);

List.c

c 复制代码
#include "List.h"

// 功能:创建一个新的链表节点
// 参数:x - 节点存储的数据
// 返回:新节点的指针
LTNode* BuyLTNode(ListDataType x)
{
    // 为新节点申请堆内存,大小为链表节点结构体大小
    LTNode* NewNode = (LTNode*)malloc(sizeof(LTNode));
    // 判断内存申请是否失败
    if (NewNode == NULL)
    {
        // 打印malloc失败的原因
        perror("malloc failed!");
        // 退出程序,终止运行
        exit(-1);
    }
    // 给新节点的数据域赋值
    NewNode->data = x;
    // 新节点前驱指针初始化为NULL
    NewNode->prev = NULL;
    // 新节点后继指针初始化为NULL
    NewNode->next = NULL;
    // 返回创建好的新节点
    return NewNode;
}

// 功能:创建并初始化一个带头节点的双向循环链表
// 返回:链表的头节点指针
LTNode* ListCreateHead()
{
    // 创建头节点,数据域填-1作为标记,不存储有效数据
    LTNode* Head = BuyLTNode(-1);
    // 空链表:头节点的next指向自己
    Head->next = Head;
    // 空链表:头节点的prev指向自己
    Head->prev = Head;
    // 返回初始化完成的头节点
    return Head;
}

// 功能:判断链表是否为空
// 参数:phead - 链表头节点
// 返回:空返回true,非空返回false
bool ListEmpty(LTNode* phead)
{
    // 断言保证头节点不为空
    assert(phead);
    // 双向循环链表空的条件:头节点next指向自己
    return phead->next == phead;
}

// 功能:正向打印链表(从头节点后第一个节点开始遍历)
// 参数:phead - 链表头节点
void ListPrintForward(LTNode* phead)
{
    // 断言:链表不能为空,否则不能打印
    assert(!ListEmpty(phead));
    // 正向遍历指针,从头节点后第一个节点开始
    LTNode* pcur = phead->next;
    // 打印提示信息
    printf("PrintForward: ");
    // 循环遍历:没有回到头节点就继续
    while (pcur != phead)
    {
        // 打印当前节点的数据
        printf("%d", pcur->data);
        // 如果当前节点不是最后一个节点,打印分隔符
        if (pcur->next != phead)
        {
            printf(" -> ");
        }
        // 遍历指针向后移动
        pcur = pcur->next;
    }
    // 换行
    printf("\n");
}

// 功能:反向打印链表(从尾节点开始遍历)
// 参数:phead - 链表头节点
void ListPrintBackward(LTNode* phead)
{
    // 断言:链表不能为空
    assert(!ListEmpty(phead));
    // 反向遍历指针,从最后一个节点开始
    LTNode* pcur = phead->prev;
    // 打印提示信息
    printf("PrintBackward: ");
    // 循环遍历:没有回到头节点就继续
    while (pcur != phead)
    {
        // 打印当前节点数据
        printf("%d", pcur->data);
        // 如果不是第一个节点,打印分隔符
        if (pcur->prev != phead)
        {
            printf(" -> ");
        }
        // 遍历指针向前移动
        pcur = pcur->prev;
    }
    // 换行
    printf("\n");
}

// 功能:销毁整个链表,释放所有内存
// 参数:pphead - 二级指针,用来修改外部头指针
void ListDestroy(LTNode** pphead)
{
    // 断言:二级指针不能为空
    assert(pphead);
    // 如果链表为空,直接返回
    if (ListEmpty(*pphead))
    {
        return;
    }
    // 从第一个数据节点开始遍历
    LTNode* pcur = (*pphead)->next;
    // 没有回到头节点就继续释放
    while (pcur != (*pphead))
    {
        // 记录当前要释放的节点
        LTNode* del = pcur;
        // 遍历指针先向后移动,防止断链
        pcur = pcur->next;
        // 释放节点内存
        free(del);
        // 指针置空,避免野指针
        del = NULL;
    }
    // 释放头节点
    free(*pphead);
    // 外部头指针置空
    *pphead = NULL;
}

// 功能:清空链表,只保留头节点
// 参数:phead - 链表头节点
void ListKeepHead(LTNode* phead)
{
    // 断言:头节点不能为空
    assert(phead);
    // 如果链表为空,无需清空
    if (ListEmpty(phead))
    {
        return;
    }
    // 从第一个数据节点开始遍历
    LTNode* pcur = phead->next;
    // 遍历释放所有数据节点
    while (pcur != phead)
    {
        // 记录待释放节点
        LTNode* del = pcur;
        // 指针后移
        pcur = pcur->next;
        // 释放节点
        free(del);
        // 指针置空
        del = NULL;
    }
    // 恢复空链表状态
    phead->next = phead;
    phead->prev = phead;
}

// 功能:头插,在头节点后插入新节点
// 参数:phead - 头节点,x - 插入数据
void ListPushFront(LTNode* phead, ListDataType x)
{
    // 断言:头节点不能为空
    assert(phead);
    // 创建新节点
    LTNode* NewNode = BuyLTNode(x);
    // 新节点next指向原第一个数据节点
    NewNode->next = phead->next;
    // 新节点prev指向头节点
    NewNode->prev = phead;
    // 原第一个节点prev指向新节点
    phead->next->prev = NewNode;
    // 头节点next指向新节点
    phead->next = NewNode;
}

// 功能:尾插,在链表尾部插入新节点
// 参数:phead - 头节点,x - 插入数据
void ListPushBack(LTNode* phead, ListDataType x)
{
    // 断言:头节点不能为空
    assert(phead);
    // 创建新节点
    LTNode* NewNode = BuyLTNode(x);
    // 新节点next指向头节点
    NewNode->next = phead;
    // 新节点prev指向原尾节点
    NewNode->prev = phead->prev;
    // 原尾节点next指向新节点
    phead->prev->next = NewNode;
    // 头节点prev指向新节点(更新尾节点)
    phead->prev = NewNode;
}

// 功能:头删,删除第一个数据节点
// 参数:phead - 头节点
void ListPopFront(LTNode* phead)
{
    // 断言:头节点不能为空
    assert(phead);
    // 断言:链表不能为空,不能删除空链表
    assert(!ListEmpty(phead));
    // 记录要删除的第一个节点
    LTNode* del = phead->next;
    // 头节点next指向第二个节点
    phead->next = phead->next->next;
    // 第二个节点prev指向头节点
    phead->next->prev = phead;
    // 释放删除节点
    free(del);
    // 指针置空
    del = NULL;
}

// 功能:尾删,删除最后一个数据节点
// 参数:phead - 头节点
void ListPopBack(LTNode* phead)
{
    // 断言:头节点不能为空
    assert(phead);
    // 断言:链表不能为空
    assert(!ListEmpty(phead));
    // 记录要删除的尾节点
    LTNode* del = phead->prev;
    // 头节点prev指向倒数第二个节点
    phead->prev = phead->prev->prev;
    // 倒数第二个节点next指向头节点
    phead->prev->next = phead;
    // 释放节点
    free(del);
    // 指针置空
    del = NULL;
}

// 功能:查找值为x的节点
// 参数:phead - 头节点,x - 查找目标
// 返回:找到返回节点地址,找不到返回NULL
LTNode* ListFind(LTNode* phead, ListDataType x)
{
    // 断言:头节点不能为空
    assert(phead);
    // 从第一个数据节点开始遍历
    LTNode* pcur = phead->next;
    // 遍历整个链表
    while (pcur != phead)
    {
        // 如果当前节点数据等于目标值
        if (pcur->data == x)
        {
            // 返回节点地址
            return pcur;
        }
        // 继续向后遍历
        pcur = pcur->next;
    }
    // 遍历结束没找到,返回NULL
    return NULL;
}

// 功能:在指定节点pos前插入新节点
// 参数:phead - 头节点,pos - 指定位置,x - 插入数据
void ListInsertBefore(LTNode* phead, LTNode* pos, ListDataType x)
{
    // 断言:头节点不能为空
    assert(phead);
    // 断言:链表不能为空
    assert(!ListEmpty(phead));
    // 断言:指定位置节点不能为空
    assert(pos);
    // 创建新节点
    LTNode* NewNode = BuyLTNode(x);
    // pos的前一个节点next指向新节点
    pos->prev->next = NewNode;
    // 新节点prev指向pos的前一个节点
    NewNode->prev = pos->prev;
    // 新节点next指向pos
    NewNode->next = pos;
    // pos的prev指向新节点
    pos->prev = NewNode;
}

// 功能:在指定节点pos后插入新节点
// 参数:phead - 头节点,pos - 指定位置,x - 插入数据
void ListInsertAfter(LTNode* phead, LTNode* pos, ListDataType x)
{
    // 断言:头节点不能为空
    assert(phead);
    // 断言:链表不能为空
    assert(!ListEmpty(phead));
    // 断言:指定节点不能为空
    assert(pos);
    // 创建新节点
    LTNode* NewNode = BuyLTNode(x);
    // pos的后一个节点prev指向新节点
    pos->next->prev = NewNode;
    // 新节点next指向pos的后一个节点
    NewNode->next = pos->next;
    // 新节点prev指向pos
    NewNode->prev = pos;
    // pos的next指向新节点
    pos->next = NewNode;
}

// 功能:获取链表有效节点个数
// 参数:phead - 头节点
// 返回:有效节点数量
int ListSize(LTNode* phead)
{
    // 断言:头节点不能为空
    assert(phead);
    // 从第一个数据节点开始遍历
    LTNode* pcur = phead->next;
    // 节点计数器
    int count = 0;
    // 遍历所有有效节点
    while (pcur != phead)
    {
        // 计数+1
        count++;
        // 指针后移
        pcur = pcur->next;
    }
    // 返回总数
    return count;
}

// 功能:按位置前插,在第pos个节点前插入
// 参数:phead - 头节点,pos - 位置,x - 数据
void ListInsertByPosBefore(LTNode* phead, int pos, ListDataType x)
{
    // 断言:头节点非空
    assert(phead);
    // 断言:链表非空
    assert(!ListEmpty(phead));
    // 断言:位置合法(1~节点总数)
    assert((pos >= 1) && (pos <= ListSize(phead)));
    // 从第一个节点开始找
    LTNode* pcur = phead->next;
    // 位置计数器
    int count = 1;
    // 找到第pos个节点
    while (count < pos)
    {
        count++;
        pcur = pcur->next;
    }
    // 调用通用前插函数
    ListInsertBefore(phead, pcur, x);
}

// 功能:按位置后插,在第pos个节点后插入
void ListInsertByPosAfter(LTNode* phead, int pos, ListDataType x)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 断言位置合法
    assert((pos >= 1) && (pos <= ListSize(phead)));
    // 从第一个节点开始找
    LTNode* pcur = phead->next;
    int count = 1;
    // 找到第pos个节点
    while (count < pos)
    {
        count++;
        pcur = pcur->next;
    }
    // 调用通用后插函数
    ListInsertAfter(phead, pcur, x);
}

// 功能:删除指定节点pos
// 参数:phead - 头节点,pos - 待删节点
void ListPopPos(LTNode* phead, LTNode* pos)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 断言待删节点非空
    assert(pos);
    // pos的前一个节点next指向pos的后一个节点
    pos->prev->next = pos->next;
    // pos的后一个节点prev指向pos的前一个节点
    pos->next->prev = pos->prev;
    // 释放pos节点
    free(pos);
    // 指针置空
    pos = NULL;
}

// 功能:删除第一个值为x的节点
void ListPopFirstX(LTNode* phead, ListDataType x)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 查找值为x的节点
    LTNode* del = ListFind(phead, x);
    // 如果找到
    if (del != NULL)
    {
        // 删除该节点
        ListPopPos(phead, del);
    }
}

// 功能:删除所有值为x的节点
void ListPopAllX(LTNode* phead, ListDataType x)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 从第一个节点开始遍历
    LTNode* pcur = phead->next;
    // 遍历整个链表
    while (pcur != phead)
    {
        // 如果当前节点数据等于x
        if (pcur->data == x)
        {
            // 记录待删节点
            LTNode* del = pcur;
            // 指针先向后移动,防止断链
            pcur = pcur->next;
            // 删除节点
            ListPopPos(phead, del);
        }
        else
        {
            // 不匹配则继续向后
            pcur = pcur->next;
        }
    }
}

// 功能:按位置删除第pos个节点
void ListEraseByPos(LTNode* phead, int pos)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 断言位置合法
    assert((pos >= 1) && (pos <= ListSize(phead)));
    // 计数器
    int count = 1;
    // 从第一个节点开始
    LTNode* pcur = phead->next;
    // 找到第pos个节点
    while (count != pos)
    {
        count++;
        pcur = pcur->next;
    }
    // 删除该位置节点
    ListPopPos(phead, pcur);
}

// 功能:按位置修改节点数据
void ListModifyByPos(LTNode* phead, int pos, ListDataType x)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 断言位置合法
    assert((pos >= 1) && (pos <= ListSize(phead)));
    int count = 1;
    // 从第一个节点开始找
    LTNode* pcur = phead->next;
    // 找到第pos个节点
    while (count != pos)
    {
        count++;
        pcur = pcur->next;
    }
    // 修改数据
    pcur->data = x;
}

// 功能:修改第一个值为oldX的节点为newX
void ListModifyX(LTNode* phead, ListDataType oldX, ListDataType newX)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 如果新旧值相同,无需修改
    if (oldX == newX)
    {
        return;
    }
    // 查找旧值节点
    LTNode* Modify = ListFind(phead, oldX);
    // 如果找到
    if (Modify != NULL)
    {
        // 修改为新值
        Modify->data = newX;
    }
}

// 功能:修改所有值为oldX的节点为newX
void ListModifyAllX(LTNode* phead, ListDataType oldX, ListDataType newX)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 新旧值相同直接返回
    if (oldX == newX)
    {
        return;
    }
    // 从第一个节点遍历
    LTNode* pcur = phead->next;
    while (pcur != phead)
    {
        // 匹配旧值
        if (pcur->data == oldX)
        {
            // 修改为新值
            pcur->data = newX;
        }
        // 继续向后
        pcur = pcur->next;
    }
}

// 功能:获取第一个数据节点
LTNode* ListGetFront(LTNode* phead)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 第一个节点就是头节点next
    return phead->next;
}

// 功能:获取最后一个数据节点
LTNode* ListGetBack(LTNode* phead)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 尾节点就是头节点prev
    return phead->prev;
}

// 功能:按位置获取第pos个节点
LTNode* ListGetNodeByPos(LTNode* phead, int pos)
{
    // 断言头节点非空
    assert(phead);
    // 断言链表非空
    assert(!ListEmpty(phead));
    // 断言位置合法
    assert((pos >= 1) && (pos <= ListSize(phead)));
    int count = 1;
    // 从第一个节点开始
    LTNode* pcur = phead->next;
    // 找到目标位置
    while (count != pos)
    {
        count++;
        pcur = pcur->next;
    }
    // 返回节点指针
    return pcur;
}
相关推荐
freshman_y5 小时前
一篇介绍C语言中二级指针和二维数组的文章
c语言·开发语言
weixin_413920616 小时前
LVGL仪表显示项目
c语言
gumichef7 小时前
算法的时间复杂度和空间复杂度
数据结构
小柯博客7 小时前
STM32MP2安全启动技术深度解析
c语言·c++·stm32·嵌入式硬件·安全·开源·github
cpp_25018 小时前
P1832 A+B Problem(再升级)
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
爱编码的小八嘎8 小时前
C语言完美演绎9-1
c语言
꧁细听勿语情꧂8 小时前
合并两个有序表、判断链表的回文结构、相交链表、环的链表一和二
c语言·开发语言·数据结构·算法
大肥羊学校懒羊羊8 小时前
完数与盈数的计算题解
数据结构·c++·算法
气宇轩昂固执狂8 小时前
01-初识C语言
c语言·开发语言
我要升天!9 小时前
C语言连接 MySQL:libmysqlclient 获取方式详解
c语言·开发语言·数据库·mysql·adb