


点击下面查看作者专栏 🔥🔥C语言专栏🔥🔥 🌊🌊编程百度🌊🌊 🌠🌠如何获取自己的代码仓库🌠🌠
🌐索引与导读
- 💻链表分类
- 💻双向链表的作用
- 💻双向链表的概念和结构
- 分文件编写双向链表
-
- listNode.c
- listNode.h
- test.c
-
- ❓测试尾插和尾删(附带销毁)
- ❓测试头插和头删(附带销毁)
- [❓测试 查找、任意位置插入(LTInsert)、任意位置删除(LTErase)](#❓测试 查找、任意位置插入(LTInsert)、任意位置删除(LTErase))
- ❓测试主函数
- 希望读者多多三连
- 给小编一些动力
- 蟹蟹啦!
💻链表分类
💻双向链表的作用
选择双向链表 场景分析 需要双向遍历 频繁中间操作 需要快速删除 浏览器历史
文本编辑器 LRU缓存
内存管理 游戏对象管理
GUI系统
💻双向链表的概念和结构

🚩双向链表是一种链表数据结构
每个节点除了包含数据域(用于存储数据) 之外,还包含两个指针域 ,一个指向前一个节点(prev),另一个指向后一个节点(next)
最后一个节点有指向开头的指针next,开头的节点有指向结尾的指针prev,形成循环
📶双向链表的头节点
单链表的头节点和双向链表的头节点不是一个概念
带头链表的头节点实际上是哨兵位,不存储任何有效数据,只是在这里放哨的
📶双向链表节点的组成部分
初始定义
c
struct ListNode {
int data;
struct ListNode* next; //指向下一个节点
struct ListNode* prev; //指向上一个节点
};
重新定义数据类型后
c
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next; //指向下一个节点
struct ListNode* prev; //指向上一个节点
}LTNode;
初始情况下:
plist(头节点)为空,next指针和prev指针都指向自己
分文件编写双向链表
test.c 接口声明 lisNode.c 功能实现 listNode.h 函数的声明与头文件定义
listNode.c
🌠双向链表节点的申请
一个新的节点的申请,结构如下:
前驱指针和后驱指针都要指向自己
c
/*双向链表节点的申请*/
LTNode* LTBuyNode(LTDataType x){
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL) {
perror("malloc fail!");
exit(-1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
🌠双向链表的初始化
c
void LTInit(LTNode** pphead) {
assert(pphead);
*pphead = LTBuyNode(-1);
}
🔥🔥🔥🔥讲解代码要点:
- ❗注意 :我们需要二级指针(
LTNode**)的唯一场景是:我们需要修改头指针本身的值
判断双向链表是否为空
c
bool LTEmpty(LTNode* phead){
assert(phead);
return (phead->next == phead && phead->prev == phead);
}
🔥🔥🔥🔥讲解代码要点:
return (phead->next == phead && phead->prev == phead);
- 如果链表的头节点的前驱指针和后继指针都指向自己,说明链表为空
assert(phead)防范的是链表没有初始化 或者传参传错了
简单来说,就是保证指针有效不为空assert(!LTEmpty(phead))保证链表还有数据不会只剩下头节点(不然头删尾删操作会把头节点删掉)
🌠双向链表的头插

❗插入的数据是在head与d1之间❗
c
void LTPushFront(LTNode* phead, LTDataType x) {
assert(phead);
//申请一个新节点
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
🔥🔥🔥🔥讲解代码要点:
- 连接秘诀:
先连接新节点
再重置头节点
🌠双向链表的头删
c
void LTPopFront{LTNode* phead}{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
🔥🔥🔥🔥讲解代码要点:
LTNode* del = phead->next;
- 链表的头删是删除phead的下一个节点
为何不直接assert(phead);
assert(phead)防范的是链表没有初始化 或者传参传错了
简单来说,就是保证指针有效不为空assert(!LTEmpty(phead))保证链表还有数据不会只剩下头节点(不然头删尾删操作会把头节点删掉)
🌠双向链表的尾插
c
void LTPushBack(LTNode* phead, LTDataType X) {
assert(phead);
LTNode* newnode = LTBuyNode(X);
newnode->prev = phead->prev;
newnode->next = phead;
//此时phead的前驱指针还是指向原来的节点,需要进行修改
phead->prev->next = newnode;
phead->prev = newnode;
}
由于是带头循环
每个节点都有4条逻辑线连接着,通常先修改两个节点间的链接,再修改循环的链接
🌠双向链表的尾删

c
void LTPopBack(LTNode* phead){
assert(!LTEmpty(phead));
LTNode* del = phead->prev;
del->prev->next = phead; //让d2指向phead
phead->prev = del->prev;
free(del);
del = NULL;
}
🔥🔥🔥🔥讲解代码要点:
链表的prev与next的顺序不要搞错了
- 如上图 :
head的prev节点是d3,d3的prev节点是d2
head的next节点是d1,d3的next节点是head
🌠双向链表的遍历打印
c
void LTPint(LTNode* phead){
assert(phead);
LTNode* pcur = phead->next;
while(pcur != phead){
printf("%d ->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
🌠双向链表的查找
c
LTNode* LTFind(LTNode* phead, LTDataType x){
assert(phead);
LTNode* pcur = phead->next;
while(pcur != phead) {
if(pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
🌠双向链表在pos之前插入节点

c
void LTInsert(LTNode* pos, LTDataType x) {
assert(pos);
//申请一个新节点
LTNode* newnode = LTBuyNode(x);
LTNode* prev = pos->prev;
//1. 处理 prev 和 newnode 的关系
prev->next = newnode;
newnode->prev = prev;
//2. 处理 newnode 和 pos 的关系
newnode->next = pos;
pos->prev = newnode;
}
🤔代码核心逻辑🤔
建立 prev <-> newnode <-> pos 的连接
🌠双向链表任意位置的删除
c
void LTErase(LTNode* pos){
assert(pos);
assert(phead);
assert(phead != pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
assert(phead != pos);
确保phead不等于pos
🌠双向链表的销毁
c
/*双向链表的销毁*/
void LTDestroy(LTNode** pphead) {
assert(pphead);
assert(*pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead) {
LTNode* next = pcur->next;
free(pcur); //pcur只是内存释放,还可以改变指针指向
pcur = next;
}
free(*pphead);
*pphead = NULL;
}
🔥🔥🔥🔥讲解代码要点:
- 重点搞清楚
二级指针与free的操作原理
二级指针只有在修改头文件的值的时候才使用
free释放的指针指向的数据内存,但是原指针指向的未知的内存,会成为悬空指针
listNode.h
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next; //指向下一个节点
struct ListNode* prev; //指向上一个节点
}LTNode;
//双向链表的初始化
/*要改变头节点plist*/
void LTInit(LTNode** pphead);
//双向链表初始化的另一种方式
LTNode* LTInit2();
//双向链表节点的申请
LTNode* LTBuyNode(LTDataType x);
//双向链表的尾插
//我们需要二级指针(LTNode** )的唯一场景是:我们需要修改"头指针"本身的值
void LTPushBack(LTNode* phead, LTDataType);
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType);
//双向链表的尾删
void LTPopBack(LTNode* phead);
//双向链表的头删
void LTPopFront(LTNode* phead);
//双向链表的头删
void LTPopFront(LTNode* phead);
//判断双向链表是否为空
bool LTEmpty(LTNode* phead);
//双向链表的遍历
void LTPrint(LTNode* phead);
//双向链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//双向链表在pos之前插入节点
void LTInsert(LTNode* pos, LTDataType x);
//双向链表任意位置的删除
void LTErase(LTNode* phead, LTNode* pos);
//双向链表的销毁
void LTDestroy(LTNode** pphead);
test.c
test.c的文件负责对函数功能进行测试记得包含头文件!!!
#include "ListNode.h"
❓测试尾插和尾删(附带销毁)
c
//1.测试尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
printf("插入1、2、3、4后:");
LTPrint(plist);
//2.测试尾删
LTPopBack(plist);
printf("尾删一次后: ");
LTPrint(plist); // 预期: 1 ->2 ->3 ->
LTPopBack(plist);
LTPopBack(plist);
printf("再尾删两次后: ");
LTPrint(plist); // 预期: 1 ->
// 3. 测试双向链表的删除
LTDestroy(&plist);
printf("销毁链表后: ");
LTPrint(plist); // 触发 assert(phead)
❓测试头插和头删(附带销毁)
c
// 1. 测试头插
LTPushFront(plist, 100);
LTPushFront(plist, 200);
LTPushFront(plist, 300);
printf("头插100,200,300后: ");
LTPrint(plist); // 预期: 300 ->200 ->100 ->
// 2. 测试头删
LTPopFront(plist);
printf("头删一次后: ");
LTPrint(plist); // 预期: 200 ->100 ->
// 3. 测试判空
LTPopFront(plist);
LTPopFront(plist);
if (LTEmpty(plist)) {
printf("链表当前为空 (Correct)\n");
}
else {
printf("链表判空逻辑错误\n");
}
//4. 测试销毁
LTDestroy(&plist);
printf("销毁链表后: ");
LTPrint(plist); // 触发 assert(phead)
❓测试 查找、任意位置插入(LTInsert)、任意位置删除(LTErase)
c
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
printf("初始链表: ");
LTPrint(plist);
// 1. 测试查找 + 插入
// 需求:在 2 的前面插入 20
LTNode* pos = LTFind(plist, 2);
if (pos) {
LTInsert(pos, 20);
printf("在2前面插入20: ");
LTPrint(plist); // 预期: 1 ->20 ->2 ->3 ->
}
else {
printf("未找到节点 2\n");
}
// 2. 测试查找 + 删除
// 需求:删除节点 2
pos = LTFind(plist, 2);
if (pos) {
LTErase(plist, pos);
pos = NULL; // 防止非法访问
printf("删除节点2后: ");
LTPrint(plist); // 预期: 1 ->20 ->3 ->
}
❓测试主函数
c
int main(){
//TestList1();
//TestList2();
//TestList3();
return 0;
}
三个测试函数不可以一起调用,因为**assert会报错**
希望读者多多三连
给小编一些动力
蟹蟹啦!


