【数据结构初阶】带头双向循环链表 (DSList) --增删查改完整实现详解

一.带头双向循环链表

1.定义

带头双向循环链表 是一种链式存储结构,每个节点包含三个部分:

  • 数据域(data):存储实际数据元素

  • 前驱指针(prev):指向当前节点的前一个节点

  • 后继指针(next):指向当前节点的后一个节点

带头 :存在一个哨兵位头节点(不存储有效数据 ),作为链表的统一入口和边界标识

双向 :每个节点既有前驱指针又有后继指针,支持双向遍历

循环 :头节点的 prev 指向尾节点,尾节点的 next 指向头节点,形成闭环


C语言描述如下:

cpp 复制代码
// 定义双向链表的节点
typedef struct ListNode
{
    LDTDataType data;      // 数据域:存储实际数据
    struct ListNode* prev; // 前驱指针:指向前一个节点
    struct ListNode* next; // 后继指针:指向后一个节点
} ListNode;

2.结构

带头双向循环链表是**"结构最复杂"的链表,这种链表并不是"单独"用来存数据的,恰恰相反,它是实际工程中应用最广泛的链表结构**,像Linux内核、数据库缓存、消息队列等底层 都在大量使用。虽然它结构看起来复杂(有哨兵位、双向指针、还首尾相连),但这些设计反而带来了很多优势:空链表和非空链表操作逻辑完全统一,不用写一堆if判断边界,尾插尾删还能O(1)搞定,所以代码实现起来反而比普通链表更简单、更不容易出bug。总的来说就是"结构复杂但用起来真香"。


二.带头循环双向链表的接口实现

1.创建文件

写这个链表一般需要建三个文件:

  • List.h(头文件):放结构体定义、函数声明、还有需要用到的头文件(比如stdio.h、stdlib.h、assert.h)

  • List.c(源文件):放各个接口函数的具体实现代码

  • test.c(测试文件):写main函数,调用List.c里的接口来测试功能正不正常

2.Lits.h头文件的代码

cpp 复制代码
// List.h
// 带头双向循环链表 ------ 接口声明

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>  bool类型需要这个头文件

typedef int LTDataType;

// 双向链表节点结构
typedef struct ListNode
{
    LTDataType data;          // 数据域
    struct ListNode* prev;    // 前驱指针,指向前一个节点
    struct ListNode* next;    // 后继指针,指向后一个节点
} ListNode;

//  接口函数声明

// 动态申请一个新节点(内部使用,但也可以对外暴露)
ListNode* BuyListNode(LTDataType x);

// 创建并初始化链表的头节点(哨兵位)
ListNode* ListCreate();

// 销毁整个链表(释放所有节点)
void ListDestroy(ListNode* phead);

// 打印链表所有有效数据(便于调试)
void ListPrint(ListNode* phead);

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

// 尾删:删除链表尾部节点
void ListPopBack(ListNode* phead);

// 头插:在哨兵位之后插入新节点
void ListPushFront(ListNode* phead, LTDataType x);

// 头删:删除哨兵位之后的第一个有效节点
void ListPopFront(ListNode* phead);

// 查找:根据数据值查找对应节点,返回节点指针,找不到返回NULL
ListNode* ListFind(ListNode* phead, LTDataType x);

// 在pos位置之前插入新节点(pos由ListFind获得)
void ListInsert(ListNode* pos, LTDataType x);

// 删除pos位置的节点(pos由ListFind获得)
void ListErase(ListNode* pos);

// 判空:链表为空(只有哨兵位)返回true,否则返回false
bool ListEmpty(ListNode* phead);

// 获取链表有效节点的个数(不包含哨兵位)
size_t ListSize(ListNode* phead);

3.List.c完成这个函数接口的实现

1. 动态申请一个新节点

cpp 复制代码
// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    if (newnode == NULL)   // 补充:malloc失败要检查
    {
        perror("malloc fail");
        exit(-1);
    }
    newnode->data = x;
    newnode->prev = NULL;
    newnode->next = NULL;
    return newnode;
}

解析如下:

  1. **申请节点:**用 malloc 在堆上开辟一块空间,并检查申请是否成功。

  2. **初始化数据:**把数据存进去,前后指针先置成 NULL。

  3. **返回节点:**把新节点地址返回给调用者

注意:大家在写项目时 malloc 一定要检查返回值,不然申请失败了还继续用,程序就崩了。


2. 创建返回链表的头节点(初始化)

方式一:返回头节点指针(推荐使用)

cpp 复制代码
// 创建并初始化链表的头节点(哨兵位)
ListNode* ListCreate()
{
    ListNode* phead = BuyListNode(0);  // 哨兵位数据随便给,反正不用
    phead->prev = phead;
    phead->next = phead;
    return phead;
}

解析如下:

  1. 调用 BuyListNode 申请头节点:哨兵位不存有效数据,传 0 进去只是占个位置,这个值没有实际意义。

  2. 自己指向自己:把新节点的 prev 和 next 都指向它自己,这样空链表就形成了闭环。后面不管是头插还是尾插,都基于这个循环结构操作。

  3. 返回头节点地址:把哨兵位的地址返回给调用者,之后所有操作都通过这个头节点来定位链表。


方式二:传二级指针初始化

cpp 复制代码
// 初始化链表(传二级指针版)
void ListInit(ListNode** pphead)
{
    assert(pphead);                    // 防止传空指针
    *pphead = BuyListNode(0);
    (*pphead)->prev = *pphead;
    (*pphead)->next = *pphead;
}

解析如下:

  1. 为什么要传二级指针:这个函数要改变外面指针的指向(让外面的 plist 指向新创建的头节点),传一级指针只是值拷贝,改不了外面的变量,所以得传指针的地址进来,也就是二级指针。

  2. 先断言防止传空assert(pphead) 确保传进来的不是 NULL,防止后面解引用崩掉。

  3. 申请节点并初始化:申请一个哨兵位节点,让 plist 指向它,再把 prev 和 next 都指向自己,形成一个空链表。

两种方式的区别

  • 方式一用起来简单ListNode* plist = ListCreate();

  • 方式二调用时得传地址:ListInit(&plist);

初始化带头双向循环链表,首先动态申请一个头结点头结点的前驱和后继指针都指向自己,形成一个循环****如图所示:


3.双向链表的销毁

cpp 复制代码
// 双向链表销毁(二级指针)
void ListDestroy(ListNode** pphead)
{
    assert(pphead);      // 防止传进来的是空指针(比如传了NULL)
    assert(*pphead);     // 确保链表头节点存在

    // 从第一个有效节点开始遍历释放
    ListNode* cur = (*pphead)->next;
    while (cur != *pphead)
    {
        ListNode* next = cur->next;   // 先保存下一个节点
        free(cur);
        cur = next;
    }

    // 最后释放哨兵位头节点
    free(*pphead);
    *pphead = NULL;      // 把外面的指针置空,防止野指针
}

解析如下:

  1. 先断言防止崩掉:进来先检查 pphead 和 *pphead,确保传进来的不是 NULL,头节点也确实存在。这俩条件不满足程序就直接报错终止,避免后面解引用空指针导致程序崩得更难看。

  2. 遍历释放有效节点:从第一个有效节点开始,一个一个往后走,每次先把下一个节点保存起来(因为等会儿 free 掉当前节点后就找不到它了),然后释放当前节点,再移到下一个。循环终止条件是走回到头节点,说明整圈都释放完了。

  3. 最后释放头节点并置空:有效节点全部释放完之后,把哨兵位也 free 掉,然后把外面的指针置成 NULL。这一步很关键,防止外面那个指针还在指向已经被释放的内存,变成野指针。

问题来了为啥用二级指针?

因为最后要置空外面的 plist传一级指针只能释放节点,但没法把外面的指针设为NULL。用二级指针就能直接从函数内部把 plist 置空,调用完直接用就安全了

这个是一级指针写法(我个人不太推荐):

cpp 复制代码
void ListDestroy(ListNode* phead)
{
    assert(phead);

    ListNode* cur = phead->next;
    while (cur != phead)
    {
        ListNode* next = cur->next;
        free(cur);
        cur = next;
    }

    free(phead);
    // phead = NULL;  // 这行没用,外面的指针还是指向已释放的内存
}

解析如下:

  1. 能释放内存但置不了空:一级指针版本可以遍历释放所有节点,包括哨兵位,内存确实被回收了,但函数内部的 phead 只是外部指针的一份拷贝,最后 phead = NULL 这一行只把拷贝置空了,外部的 plist 还是指向那块已经被释放的内存。

  2. 野指针隐患:调用完 ListDestroy(plist) 后,plist 里的地址值没变,但指向的内存已经被系统回收了,这时候 plist 就成了野指针。如果后面不小心再解引用它,程序就会崩。

  3. 需要调用者手动置空:用一级指针版本的话,调用者必须自己记得在函数调用后写一句 plist = NULL,但人很容易忘,所以不如直接用二级指针版本,让函数内部一次性把释放和置空都搞定,更安全。

一级指针的问题:函数里的 phead 是外面 plist 的一份拷贝,置空它影响不了外面的 plist。调用完后 plist 就成了野指针,容易出问题。所以建议用二级指针


4.双向链表的打印

cpp 复制代码
// 打印双向链表
void ListPrint(ListNode* phead)
{
    assert(phead);

    ListNode* cur = phead->next;  // 从第一个有效节点开始
    printf("head <-> ");

    while (cur != phead)
    {
        printf("%d <-> ", cur->data);
        cur = cur->next;
    }

    printf("head\n");
}

解析如下:

  1. 从第一个有效节点开始打印cur = phead->next 跳过哨兵位,从真正存数据的第一个节点开始。哨兵位不打印,只作为起点和终点的标志。

  2. 循环遍历直到回到头节点:每次打印完当前节点的数据,就把 cur 移到下一个节点。循环条件是 cur != phead,因为链表是循环的,等 cur 走回到哨兵位就说明已经把所有有效节点都打印完了,再走就重复了。

  3. 打印格式直观显示循环结构:开头打印 "head <-> ",中间每个节点数据后面跟 " <-> ",结尾再打印 "head\n",一看就知道这是个带头循环的链表,也能看清数据的前后关系。空链表时打印出来就是 "head <-> head"。


5.双向链表的尾插

写法一:直接操作(容易理解)

cpp 复制代码
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
    assert(phead);

    ListNode* newnode = BuyListNode(x);
    ListNode* tail = phead->prev;  // 头节点的前驱就是尾节点

    // tail <-> newnode <-> head
    tail->next = newnode;
    newnode->prev = tail;
    newnode->next = phead;
    phead->prev = newnode;
}

解析如下:

  1. 找尾节点:因为链表是循环的,头节点的 prev 指向的就是最后一个节点,所以 phead->prev 直接拿到尾节点,不需要遍历去找,O(1) 就搞定了。

  2. 建立链接:新节点的 prev 指向尾节点,尾节点的 next 指向新节点,新节点的 next 指向头节点,头节点的 prev 指向新节点。四步指针操作,把新节点插到尾节点和头节点之间。

  3. 画个图就好懂了:插入前是 head <-> tail,插入后变成 head <-> ... <-> tail <-> newnode <-> head,newnode 成了新的尾。


写法二:复用 ListInsert(更简洁)

cpp 复制代码
// 双向链表尾插(复用ListInsert)
void ListPushBack(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListInsert(phead, x);  // 在头节点前面插入-->相当于尾插
}

有个问题,为啥在 phead 前面插入就是尾插?

因为带头双向循环链表里,头节点的 prev 指向的就是尾节点,在 phead 前面插入等价于在尾节点后面插入新节点。


6.双向链表的尾删

写法一:直接操作

cpp 复制代码
// 双向链表尾删
void ListPopBack(ListNode* phead)
{
    assert(phead);
    assert(phead->next != phead);  // 链表为空就不能再删了

    ListNode* tail = phead->prev;      // 尾节点
    ListNode* tailPrev = tail->prev;   // 尾节点的前驱

    tailPrev->next = phead;
    phead->prev = tailPrev;
    free(tail);
}

解析如下:

  1. 断言检查:先确保头节点存在,再确保链表不为空。phead->next != phead 这个条件就是说如果头节点的 next 指向自己,说明链表里没有有效节点,这时候再删就出问题了。

  2. 找到尾节点和它的前驱phead->prev 就是尾节点,tail->prev 是尾节点前面的那个节点。删尾节点之前先把这两个都记下来。

  3. 绕过尾节点重新链接:让尾节点的前驱直接和头节点链接起来,相当于把尾节点从链表中摘出去,然后 free 掉尾节点,链表还是保持循环。

写法二:复用 ListErase

cpp 复制代码
// 双向链表尾删(复用ListErase)
void ListPopBack(ListNode* phead)
{
    assert(phead);
    assert(phead->next != phead);
    ListErase(phead->prev);  // 删掉尾节点
}

尾删就是删掉头节点的前驱节点(即尾节点),所以直接 ListErase(phead->prev) 搞定,代码简洁不容易出错。


7.双向链表的头插

写法一:直接操作

cpp 复制代码
// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
    assert(phead);

    ListNode* newnode = BuyListNode(x);
    ListNode* first = phead->next;  // 原来的第一个有效节点

    // phead <-> newnode <-> first
    phead->next = newnode;
    newnode->prev = phead;
    newnode->next = first;
    first->prev = newnode;
}

解析如下:

  1. 记下原来的第一个节点first = phead->next 保存原来的第一个有效节点,因为新节点要插在它前面,不保存的话等会儿改完指针就找不到了。

  2. 四步建立链接:phead 的 next 指向新节点,新节点的 prev 指向 phead,这样头节点和新节点就接上了;新节点的 next 指向原来的 first,first 的 prev 指向新节点,这样新节点和原来的第一个节点也接上了。前后都连好,新节点就成功插到了最前面。

  3. 结果:插入前是 head <-> first,插入后变成 head <-> newnode <-> first,newnode 成了新的第一个有效节点。

写法二:复用 ListInsert

cpp 复制代码
// 双向链表头插(复用ListInsert)
void ListPushFront(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListInsert(phead->next, x);  // 在第一个有效节点前面插入 --> 相当于头插
}

8.双向链表的头删

写法一:直接操作

cpp 复制代码
// 双向链表头删
void ListPopFront(ListNode* phead)
{
    assert(phead);
    assert(phead->next != phead);  // 链表为空就不能再删了

    ListNode* first = phead->next;      // 第一个有效节点
    ListNode* second = first->next;     // 第二个有效节点

    // phead <-> second
    phead->next = second;
    second->prev = phead;
    free(first);
}

解析如下:

  1. 断言检查:先保证头节点存在,再确保链表不为空,删之前必须保证有东西可删。

  2. 找到头两个节点first 是要删的第一个有效节点,second 是它后面的节点。把这两个都记下来,防止断链。

  3. 绕过第一个节点直接链接:让头节点直接指向 secondsecond 的 prev 指向头节点,相当于把 first 从链表中摘出去,最后 free 掉就完事了。如果链表里本来只有一个有效节点,那 second 就是头节点自己,删除后链表就回到了空链表的状态。

写法二:复用 ListErase(推荐)

cpp 复制代码
// 双向链表头删(复用ListErase)
void ListPopFront(ListNode* phead)
{
    assert(phead);
    assert(phead->next != phead);
    ListErase(phead->next);  // 删掉第一个有效节点
}

头删就是删掉头节点的后继节点 (即第一个有效节点),所以直接 ListErase(phead->next) 搞定。


9.查找双向链表中的元素

cpp 复制代码
// 在双向链表中查找元素,返回节点指针,找不到返回NULL
ListNode* ListFind(ListNode* phead, LTDataType x)
{
    assert(phead);

    ListNode* cur = phead->next;
    while (cur != phead)
    {
        if (cur->data == x)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

解析如下:

  1. 从第一个有效节点开始逐个往后找,直到走回头节点说明遍历完了。

  2. 如果当前节点的数据和要找的值相等,直接返回这个节点的地址。

  3. 遍历完都没找到就返回 NULL,调用者通过判空就知道存不存在。


10.在指定pos位置之前插入元素

cpp 复制代码
void ListInsert(ListNode* pos, LTDataType x)
{
    assert(pos);  // pos不能为空

    ListNode* newnode = BuyListNode(x);  // 申请新节点
    ListNode* posPrev = pos->prev;       // 先保存pos的前驱节点

    // 四步链接:posPrev <-> newnode <-> pos
    posPrev->next = newnode;  // 1:前驱的next指向新节点
    newnode->prev = posPrev;  // 2:新节点的prev指向前驱
    newnode->next = pos;      // 3:新节点的next指向pos
    pos->prev = newnode;      // 4:pos的prev指向新节点
}

解析如下:

  1. 断言保证 pos 有效:传进来的位置指针不能为空,这是插入操作的前提。

  2. 保存前驱节点posPrev = pos->prev 先把 pos 前面的节点记下来,因为后面改指针的时候需要用到它。

  3. 四步链接:先让前驱节点的 next 指向新节点,再让新节点的 prev 指向前驱节点,然后新节点的 next 指向 pos,最后 pos 的 prev 指向新节点。四步走完,新节点就稳稳地插在了 pos 的前面。记住一个原则:先搞定新节点和前后节点的链接,最后再动 pos 的 prev,这样就不会丢节点。

注意 :这个函数不依赖头节点,只要传一个有效节点指针 pos 就能在它前面插入,非常灵活。ListPushBackListPushFront 都可以复用这个函数。


11.删除指定pos位置的元素

cpp 复制代码
void ListErase(ListNode* pos)
{
    assert(pos);  // pos不能为空

    ListNode* posPrev = pos->prev;  // 保存pos的前驱
    ListNode* posNext = pos->next;  // 保存pos的后继

    // 跳过pos:posPrev <──> posNext
    posPrev->next = posNext;  // 前驱的next指向后继
    posNext->prev = posPrev;  // 后继的prev指向前驱
    free(pos);                // 释放pos节点
}

解析如下:

  1. 断言确保 pos 有效:要删除的位置必须是一个有效的节点,传空指针进来是不行的。

  2. 保存前后节点:先把 pos 的前驱和后继都记下来,防止等会儿指针改了之后找不到它们。

  3. 绕过 pos 重新链接:让 posPrev 的 next 直接指向 posNext,posNext 的 prev 直接指向 posPrev,相当于把 pos 从链表中摘了出去,最后 free 掉就完事了。

注意:ListPopBackListPopFront 都可以复用这个函数。删除后要把外面对应的指针置空,防止野指针。


12.双向链表的判空

cpp 复制代码
bool ListEmpty(ListNode* phead)
{
    assert(phead);
    return phead->next == phead;
}

带头双向循环链表在空链表时,头节点的 next 指向自己。所以 phead->next == phead 就说明链表里没有有效节点,返回 true;否则返回 false。

判空主要配合删除操作使用,防止在空链表上执行删除导致程序崩溃。用断言还是 if 判断看情况,调试阶段用 assert 更合适,能直接定位问题


13.获取双向链表的元素个数

cpp 复制代码
size_t ListSize(ListNode* phead)
{
    assert(phead);

    size_t size = 0;
    ListNode* cur = phead->next;
    while (cur != phead)
    {
        size++;
        cur = cur->next;
    }
    return size;
}

从第一个有效节点开始遍历,每经过一个节点计数加一,直到走回头节点为止。返回的 size 不包含哨兵位头节点。

这个操作的时间复杂度是 O(n),因为链表不像数组可以直接算长度,必须遍历一遍才知道有多少个节点。如果频繁需要获取长度,可以考虑在链表结构体里单独维护一个 size 变量,插入删除时同步更新

返回值用 size_t 是因为它表示大小,和 sizeof 的返回值类型一致,范围也比 int 更大,更合理。


三.代码整合

List.h

cpp 复制代码
// List.h
// 带头+双向+循环链表增删查改实现
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int LTDataType;

// 定义双向链表的节点
typedef struct ListNode
{
    LTDataType data;          // 数据域
    struct ListNode* prev;    // 前驱指针
    struct ListNode* next;    // 后继指针
} ListNode;

// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x);

// 创建返回链表的头结点
ListNode* ListCreate();

// 双向链表销毁
void ListDestroy(ListNode** pphead);

// 双向链表打印
void ListPrint(ListNode* phead);

// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x);

// 双向链表尾删
void ListPopBack(ListNode* phead);

// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x);

// 双向链表头删
void ListPopFront(ListNode* phead);

// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x);

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

// 双向链表的判空
bool ListEmpty(ListNode* phead);

// 获取双向链表的元素个数
size_t ListSize(ListNode* phead);

List.c

cpp 复制代码
// List.c
// 带头+双向+循环链表接口实现

#include "List.h"

// 1. 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    if (newnode == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    newnode->data = x;
    newnode->prev = NULL;
    newnode->next = NULL;
    return newnode;
}

// 2. 创建返回链表的头结点(哨兵位)
ListNode* ListCreate()
{
    ListNode* phead = BuyListNode(0);  // 哨兵位数据随便给
    phead->prev = phead;
    phead->next = phead;
    return phead;
}

// 3. 双向链表销毁
void ListDestroy(ListNode** pphead)
{
    assert(pphead);
    assert(*pphead);

    ListNode* cur = (*pphead)->next;
    while (cur != *pphead)
    {
        ListNode* next = cur->next;
        free(cur);
        cur = next;
    }

    free(*pphead);
    *pphead = NULL;
}

// 4. 双向链表打印
void ListPrint(ListNode* phead)
{
    assert(phead);

    ListNode* cur = phead->next;
    printf("head <-> ");
    while (cur != phead)
    {
        printf("%d <-> ", cur->data);
        cur = cur->next;
    }
    printf("head\n");
}

// 5. 双向链表尾插(复用ListInsert)
void ListPushBack(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListInsert(phead, x);
}

// 6. 双向链表尾删(复用ListErase)
void ListPopBack(ListNode* phead)
{
    assert(phead);
    assert(!ListEmpty(phead));
    ListErase(phead->prev);
}

// 7. 双向链表头插(复用ListInsert)
void ListPushFront(ListNode* phead, LTDataType x)
{
    assert(phead);
    ListInsert(phead->next, x);
}

// 8. 双向链表头删(复用ListErase)
void ListPopFront(ListNode* phead)
{
    assert(phead);
    assert(!ListEmpty(phead));
    ListErase(phead->next);
}

// 9. 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
    assert(phead);

    ListNode* cur = phead->next;
    while (cur != phead)
    {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}

// 10. 在pos位置之前插入新节点
void ListInsert(ListNode* pos, LTDataType x)
{
    assert(pos);

    ListNode* newnode = BuyListNode(x);
    ListNode* posPrev = pos->prev;

    posPrev->next = newnode;
    newnode->prev = posPrev;
    newnode->next = pos;
    pos->prev = newnode;
}

// 11. 删除pos位置的节点
void ListErase(ListNode* pos)
{
    assert(pos);

    ListNode* posPrev = pos->prev;
    ListNode* posNext = pos->next;

    posPrev->next = posNext;
    posNext->prev = posPrev;
    free(pos);
}

// 12. 双向链表判空
bool ListEmpty(ListNode* phead)
{
    assert(phead);
    return phead->next == phead;
}

// 13. 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{
    assert(phead);

    size_t size = 0;
    ListNode* cur = phead->next;
    while (cur != phead)
    {
        size++;
        cur = cur->next;
    }
    return size;
}

test.c

cpp 复制代码
// Test.c
// 带头双向循环链表功能测试

#include "List.h"


//测试尾插和尾删
void TestPushBackAndPopBack()
{
    ListNode* plist = ListCreate();

    ListPushBack(plist, 10);
    ListPushBack(plist, 20);
    ListPushBack(plist, 30);
    ListPushBack(plist, 40);
    ListPushBack(plist, 50);
    printf("尾插5个节点: ");
    ListPrint(plist);
    printf("长度: %zu\n\n", ListSize(plist));

    ListPopBack(plist);
    ListPopBack(plist);
    ListPopBack(plist);
    printf("尾删3个节点: ");
    ListPrint(plist);
    printf("长度: %zu\n\n", ListSize(plist));

    ListDestroy(&plist);
}


//测试头插和头删
void TestPushFrontAndPopFront()
{
    ListNode* plist = ListCreate();

    ListPushFront(plist, 1);
    ListPushFront(plist, 2);
    ListPushFront(plist, 3);
    ListPushFront(plist, 4);
    ListPushFront(plist, 5);
    printf("头插5个节点: ");
    ListPrint(plist);
    printf("长度: %zu\n\n", ListSize(plist));

    ListPopFront(plist);
    ListPopFront(plist);
    printf("头删2个节点: ");
    ListPrint(plist);
    printf("长度: %zu\n\n", ListSize(plist));

    ListDestroy(&plist);
}


//测试查找和插入
void TestFindAndInsert()
{
    ListNode* plist = ListCreate();

    ListPushBack(plist, 10);
    ListPushBack(plist, 20);
    ListPushBack(plist, 30);
    ListPushBack(plist, 40);
    ListPushBack(plist, 50);
    printf("原始链表: ");
    ListPrint(plist);

    ListNode* pos = ListFind(plist, 30);
    if (pos)
    {
        ListInsert(pos, 99);
        printf("在30前插入99: ");
        ListPrint(plist);
    }

    pos = ListFind(plist, 50);
    if (pos)
    {
        ListInsert(pos, 66);
        printf("在50前插入66: ");
        ListPrint(plist);
    }

    printf("长度: %zu\n\n", ListSize(plist));
    ListDestroy(&plist);
}

//测试查找和删除
void TestFindAndErase()
{
    ListNode* plist = ListCreate();

    ListPushBack(plist, 5);
    ListPushBack(plist, 15);
    ListPushBack(plist, 25);
    ListPushBack(plist, 35);
    ListPushBack(plist, 45);
    printf("原始链表: ");
    ListPrint(plist);

    ListNode* pos = ListFind(plist, 25);
    if (pos)
    {
        ListErase(pos);
        printf("删除25: ");
        ListPrint(plist);
    }

    pos = ListFind(plist, 5);
    if (pos)
    {
        ListErase(pos);
        printf("删除5: ");
        ListPrint(plist);
    }

    pos = ListFind(plist, 45);
    if (pos)
    {
        ListErase(pos);
        printf("删除45: ");
        ListPrint(plist);
    }

    printf("长度: %zu\n\n", ListSize(plist));
    ListDestroy(&plist);
}


//综合测试
void TestComprehensive()
{
    ListNode* plist = ListCreate();

    printf("链表是否为空: %s\n", ListEmpty(plist) ? "是" : "否");

    ListPushBack(plist, 1);
    ListPushBack(plist, 2);
    ListPushFront(plist, 0);
    ListPushBack(plist, 3);
    ListPushFront(plist, -1);
    printf("混合插入: ");
    ListPrint(plist);

    ListPopBack(plist);
    ListPopFront(plist);
    printf("头删尾删: ");
    ListPrint(plist);

    ListNode* pos = ListFind(plist, 2);
    if (pos)
    {
        ListInsert(pos, 99);
        printf("在2前插入99: ");
        ListPrint(plist);
    }

    pos = ListFind(plist, 1);
    if (pos)
    {
        ListErase(pos);
        printf("删除1: ");
        ListPrint(plist);
    }

    printf("长度: %zu\n", ListSize(plist));
    printf("是否为空: %s\n", ListEmpty(plist) ? "是" : "否");

    ListDestroy(&plist);
}

int main()
{
   //TestPushBackAndPopBack();
   //TestPushFrontAndPopFront();
    //TestFindAndInsert();
    //TestFindAndErase();
    //TestComprehensive();
    return 0;
}

%zu 是 C 语言里用来打印 size_t 类型数据的格式说明符。

相关推荐
石像鬼₧魂石1 小时前
贵州商会管理系统 —— 商协会数字化运营一站式平台
大数据·数据结构·物联网·eclipse·数据库架构
Java面试题总结1 小时前
区间树:反向映射的前置数据结构
java·前端·数据结构
想做小南娘,发现自己是女生喵11 小时前
【无标题】
数据结构·算法
凤凰院凶涛QAQ16 小时前
《Java版数据结构 & 集合类剖析》栈与队列:“push/pop 是栈的灵魂,offer/poll 是队列的骨架——四组 API,两种人生”
java·开发语言·数据结构
郝学胜-神的一滴16 小时前
算法实战:最小k个数——大顶堆的优雅解法
开发语言·数据结构·c++·python·程序人生·算法·排序算法
Irissgwe16 小时前
算法滑动窗口
数据结构·算法
无相求码18 小时前
【C语言】字符串的本质是什么,为什么 strlen 会读到野内存
c语言·开发语言
LuminousCPP20 小时前
C 语言集中实践全记录:顺序表 + 单链表原理实现与通讯录项目实战【附可运行源码】
c语言·开发语言·数据结构·经验分享·笔记
兰令水20 小时前
hot100【acm版】【2026.7.13打卡-java版本】
java·开发语言·数据结构·算法·leetcode·面试