一.带头双向循环链表
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;
}
解析如下:
**申请节点:**用 malloc 在堆上开辟一块空间,并检查申请是否成功。
**初始化数据:**把数据存进去,前后指针先置成 NULL。
**返回节点:**把新节点地址返回给调用者
注意:大家在写项目时 malloc 一定要检查返回值,不然申请失败了还继续用,程序就崩了。
2. 创建返回链表的头节点(初始化)
方式一:返回头节点指针(推荐使用)
cpp
// 创建并初始化链表的头节点(哨兵位)
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(0); // 哨兵位数据随便给,反正不用
phead->prev = phead;
phead->next = phead;
return phead;
}
解析如下:
调用 BuyListNode 申请头节点:哨兵位不存有效数据,传 0 进去只是占个位置,这个值没有实际意义。
自己指向自己:把新节点的 prev 和 next 都指向它自己,这样空链表就形成了闭环。后面不管是头插还是尾插,都基于这个循环结构操作。
返回头节点地址:把哨兵位的地址返回给调用者,之后所有操作都通过这个头节点来定位链表。
方式二:传二级指针初始化
cpp
// 初始化链表(传二级指针版)
void ListInit(ListNode** pphead)
{
assert(pphead); // 防止传空指针
*pphead = BuyListNode(0);
(*pphead)->prev = *pphead;
(*pphead)->next = *pphead;
}
解析如下:
为什么要传二级指针:这个函数要改变外面指针的指向(让外面的 plist 指向新创建的头节点),传一级指针只是值拷贝,改不了外面的变量,所以得传指针的地址进来,也就是二级指针。
先断言防止传空:
assert(pphead)确保传进来的不是 NULL,防止后面解引用崩掉。申请节点并初始化:申请一个哨兵位节点,让 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; // 把外面的指针置空,防止野指针
}
解析如下:
先断言防止崩掉:进来先检查 pphead 和 *pphead,确保传进来的不是 NULL,头节点也确实存在。这俩条件不满足程序就直接报错终止,避免后面解引用空指针导致程序崩得更难看。
遍历释放有效节点:从第一个有效节点开始,一个一个往后走,每次先把下一个节点保存起来(因为等会儿 free 掉当前节点后就找不到它了),然后释放当前节点,再移到下一个。循环终止条件是走回到头节点,说明整圈都释放完了。
最后释放头节点并置空:有效节点全部释放完之后,把哨兵位也 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; // 这行没用,外面的指针还是指向已释放的内存
}
解析如下:
能释放内存但置不了空:一级指针版本可以遍历释放所有节点,包括哨兵位,内存确实被回收了,但函数内部的
phead只是外部指针的一份拷贝,最后phead = NULL这一行只把拷贝置空了,外部的plist还是指向那块已经被释放的内存。野指针隐患:调用完
ListDestroy(plist)后,plist里的地址值没变,但指向的内存已经被系统回收了,这时候plist就成了野指针。如果后面不小心再解引用它,程序就会崩。需要调用者手动置空:用一级指针版本的话,调用者必须自己记得在函数调用后写一句
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");
}
解析如下:
从第一个有效节点开始打印:
cur = phead->next跳过哨兵位,从真正存数据的第一个节点开始。哨兵位不打印,只作为起点和终点的标志。循环遍历直到回到头节点:每次打印完当前节点的数据,就把 cur 移到下一个节点。循环条件是
cur != phead,因为链表是循环的,等 cur 走回到哨兵位就说明已经把所有有效节点都打印完了,再走就重复了。打印格式直观显示循环结构:开头打印 "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;
}
解析如下:
找尾节点:因为链表是循环的,头节点的 prev 指向的就是最后一个节点,所以
phead->prev直接拿到尾节点,不需要遍历去找,O(1) 就搞定了。建立链接:新节点的 prev 指向尾节点,尾节点的 next 指向新节点,新节点的 next 指向头节点,头节点的 prev 指向新节点。四步指针操作,把新节点插到尾节点和头节点之间。
画个图就好懂了:插入前是 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);
}
解析如下:
断言检查:先确保头节点存在,再确保链表不为空。
phead->next != phead这个条件就是说如果头节点的 next 指向自己,说明链表里没有有效节点,这时候再删就出问题了。找到尾节点和它的前驱:
phead->prev就是尾节点,tail->prev是尾节点前面的那个节点。删尾节点之前先把这两个都记下来。绕过尾节点重新链接:让尾节点的前驱直接和头节点链接起来,相当于把尾节点从链表中摘出去,然后 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;
}
解析如下:
记下原来的第一个节点:
first = phead->next保存原来的第一个有效节点,因为新节点要插在它前面,不保存的话等会儿改完指针就找不到了。四步建立链接:phead 的 next 指向新节点,新节点的 prev 指向 phead,这样头节点和新节点就接上了;新节点的 next 指向原来的 first,first 的 prev 指向新节点,这样新节点和原来的第一个节点也接上了。前后都连好,新节点就成功插到了最前面。
结果:插入前是 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);
}
解析如下:
断言检查:先保证头节点存在,再确保链表不为空,删之前必须保证有东西可删。
找到头两个节点:
first是要删的第一个有效节点,second是它后面的节点。把这两个都记下来,防止断链。绕过第一个节点直接链接:让头节点直接指向
second,second的 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;
}
解析如下:
从第一个有效节点开始逐个往后找,直到走回头节点说明遍历完了。
如果当前节点的数据和要找的值相等,直接返回这个节点的地址。
遍历完都没找到就返回 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指向新节点
}
解析如下:
断言保证 pos 有效:传进来的位置指针不能为空,这是插入操作的前提。
保存前驱节点:
posPrev = pos->prev先把 pos 前面的节点记下来,因为后面改指针的时候需要用到它。四步链接:先让前驱节点的 next 指向新节点,再让新节点的 prev 指向前驱节点,然后新节点的 next 指向 pos,最后 pos 的 prev 指向新节点。四步走完,新节点就稳稳地插在了 pos 的前面。记住一个原则:先搞定新节点和前后节点的链接,最后再动 pos 的 prev,这样就不会丢节点。
注意 :这个函数不依赖头节点,只要传一个有效节点指针
pos就能在它前面插入,非常灵活。ListPushBack和ListPushFront都可以复用这个函数。


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节点
}
解析如下:
断言确保 pos 有效:要删除的位置必须是一个有效的节点,传空指针进来是不行的。
保存前后节点:先把 pos 的前驱和后继都记下来,防止等会儿指针改了之后找不到它们。
绕过 pos 重新链接:让 posPrev 的 next 直接指向 posNext,posNext 的 prev 直接指向 posPrev,相当于把 pos 从链表中摘了出去,最后 free 掉就完事了。
注意:
ListPopBack和ListPopFront都可以复用这个函数。删除后要把外面对应的指针置空,防止野指针。


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 类型数据的格式说明符。