欢迎拜访:雾里看山-CSDN博客
本篇主题:数据结构之带头双向循环链表
发布时间:2025.7.1
隶属专栏:数据结构
目录
概念与结构
带头双向循环链表(Doubly Circular Linked List with Head Node)是链表中最复杂但功能最完善的变体,它结合了四种特性:
- 带头节点:包含一个不存储数据的哨兵节点(头节点)
- 双向:每个节点同时包含前驱和后继指针
- 循环:头尾节点相互连接形成环状结构
- 链表:动态分配的非连续存储结构
核心优势
- 统一操作逻辑:头节点消除边界条件处理
- 高效双端操作:头插/头删/尾插/尾删均为O(1)
- 任意位置操作:已知节点时插入/删除为O(1)
- 完整遍历能力:从任意节点开始均可遍历整个链表
- 内存利用率高:动态分配,无容量限制
应用场景
- 操作系统内核:
- 进程调度队列
- 内存页管理
- 文件描述符表
- 数据库系统
- 事务日志管理
- 缓存淘汰算法实现(如LRU)
- 索引结构实现
- 图形用户界面
- 控件层级管理
- 撤销/重做历史记录
- 事件处理队列
- 游戏开发
- 游戏对象管理
- 动画序列控制
- 粒子系统管理
- 网络协议栈
- TCP连接管理
- 数据包重组队列
- 路由表实现
带头双向循环链表的实现
创建结构体
结构体需要包含两个指针和一个数据域。
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;// 指向前一个节点
struct ListNode* next;// 指向后一个节点
LTDataType value; // 保存节点数据
}LTNode;
基本功能 接口实现
初始化
创建头结点,建立循环。
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
销毁
遍历数组,释放每一个节点的空间。
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* tmp = cur;
cur = cur->next;
free(tmp);
}
free(phead);
}
创建节点
动态开辟一个节点,为其他接口提供一个已经开辟好空间的节点。
LTNode* BuyLTNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->value = x;
node->prev = NULL;
node->next = NULL;
return node;
}
打印
打印出链表中所有的节点,方便我们进行查看
void LTPrint(LTNode* phead)
{
assert(phead);
printf("phead<=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->value);
cur = cur->next;
}
printf("NULL\n");
}
链表长度
遍历链表一遍,计算出链表的长度
int LTSize(LTNode* phead)
{
assert(phead);
int size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
增删查改 接口实现
增
头插
直接将节点插入到头结点后面即可
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* first = phead->next;
newnode->prev = phead;
phead->next = newnode;
newnode->next = first;
first->prev = newnode;
}
尾插
直接将节点插入到头结点之前即可
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
newnode->next = phead;
phead->prev = newnode;
newnode->prev = tail;
tail->next = newnode;
}
在pos位置插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->prev = prev;
pos->prev = newnode;
newnode->next = pos;
}
删
头删
确保链表中有数据即可
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* first = phead->next;
LTNode* second = first->next;
free(first);
phead->next = second;
second->prev = phead;
}
尾删
同样需要判断链表中是否存有数据
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
free(tail);
tailprev->next = phead;
phead->prev = tailprev;
}
删除pos位置
找到pos位置的前一个节点和后一个节点即可。
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
free(pos);
posprev->next = posnext;
posnext->prev = posprev;
}
查
遍历链表,找到正确的位置,返回其节点指针,如果数据不存在,则返回空指针。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->value == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
改
并没有太多的技术含量,只是对节点内的数据修改一下。实现的目的是为了统一接口。
void LTModify(LTNode* pos, LTDataType x)
{
assert(pos);
pos->value = x;
}
整体代码展示
包含三个文件,分别是:List.h
、List.c
和main.c
List.h
:
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType value;
}LTNode;
LTNode* BuyLTNode(LTDataType x);
LTNode* LTInit();
void LTDestory(LTNode* phead);
void LTPrint(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
int LTSize(LTNode* phead);
LTNode* LTFind(LTNode* phead, LTDataType x);
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
void LTModify(LTNode* pos, LTDataType x);
List.c
:
#include "List.h"
LTNode* BuyLTNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->value = x;
node->prev = NULL;
node->next = NULL;
return node;
}
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* tmp = cur;
cur = cur->next;
free(tmp);
}
free(phead);
}
void LTPrint(LTNode* phead)
{
assert(phead);
printf("phead<=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->value);
cur = cur->next;
}
printf("NULL\n");
}
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
newnode->next = phead;
phead->prev = newnode;
newnode->prev = tail;
tail->next = newnode;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
free(tail);
tailprev->next = phead;
phead->prev = tailprev;
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* first = phead->next;
newnode->prev = phead;
phead->next = newnode;
newnode->next = first;
first->prev = newnode;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* first = phead->next;
LTNode* second = first->next;
free(first);
phead->next = second;
second->prev = phead;
}
int LTSize(LTNode* phead)
{
assert(phead);
int size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->value == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->prev = prev;
pos->prev = newnode;
newnode->next = pos;
}
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
free(pos);
posprev->next = posnext;
posnext->prev = posprev;
}
void LTModify(LTNode* pos, LTDataType x)
{
assert(pos);
pos->value = x;
}
main.c
:
#include"List.h"
void TestList1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTPushFront(plist, 10);
LTPushBack(plist, 10);
LTPrint(plist);
}
void TestList2()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTPopBack(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
//LTPopFront(plist);
LTPrint(plist);
}
void TestList3()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTPushFront(plist, 10);
LTPushFront(plist, 20);
LTPushFront(plist, 30);
LTPushFront(plist, 40);
LTPrint(plist);
}
void TestList4()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
}
int main()
{
TestList4();
return 0;
}
总结
带头双向循环链表作为链表结构的终极形态,具有以下核心价值:
- 操作统一性:头节点消除了所有边界条件
- 极致效率:所有位置插入删除均为O(1)操作
- 遍历灵活性:支持双向遍历和循环访问
- 内存安全性:明确的内存管理边界
尽管实现相对复杂,但在需要高性能和灵活性的系统级开发中(如操作系统内核、数据库引擎),带头双向循环链表是不可替代的基础数据结构。Linux内核中的list_head
结构就是其典型应用,证明了这种数据结构在工业级系统中的重要价值。
⚠️ 写在最后:以上内容是我在学习以后得一些总结和概括,如有错误或者需要补充的地方欢迎各位大佬评论或者私信我交流!!!