C语言带头双向循环链表实现指南

陈默
陈默
发布时间:2026-10-16| 6人浏览过
原创

在学习C语言数据结构时,带头双向循环链表因其结构的复杂性往往让初学者感到困惑,但其强大的扩展性和对称性使其成为许多底层容器(如PHP Hash表、Redis列表)的基础。本文将通过分步代码解析与图示,带你从零构建一个完整的带头双向循环链表,重点攻克节点指针悬挂与内存管理难点。

为什么选择带头双向循环链表

在常见的链表结构中,无头单向非循环链表结构简单但缺乏双向遍历能力,通常仅作为其他数据结构的子组件;而带头双向循环链表虽然设计最为复杂,却具备诸多优势:首尾访问效率高、插入删除操作时间复杂度稳定为O(1)(已知节点位置时)、且通过哨兵位(Dummy Head)简化了边界条件的判断逻辑。了解其实现原理,对深入理解STL中的list或std::deque等容器大有裨益。

第1步:定义节点结构体

首先定义链表的节点。为了代码的可维护性,使用 typedef 对数据类型进行重命名。每个节点包含指向前驱节点的 prev、指向后继节点的 next 以及存储数据的 val。

typedef int LNDataType;

typedef struct ListNode
{
	struct ListNode* prev;   // 前驱指针
 	struct ListNode* next;   // 后继指针
      LNDataType val;          // 数据域
}LN;

第2步:封装节点申请函数

创建新节点时应封装独立函数 BuynewNode。必须检查 malloc 是否成功,防止因内存不足导致程序崩溃。新节点初始状态为空,需显式将指针置为NULL。

// 申请一个新节点并初始化
LN* BuynewNode(LNDataType x)
{
	LN* newNode = (LN*)malloc(sizeof(LN));
	if (newNode == NULL)
	{
		printf("malloc fail\n");
		exit(-1); // 终止进程
	}
	newNode->next = NULL;
	newNode->prev = NULL;
	newNode->val = x;
	return newNode;
}

第3步:创建哨兵位节点(初始化)

链表初始化即创建哨兵位。注意,因为要修改外部传入的指针 plist 本身的指向,所以函数参数需要传递二级指针 LN** pphead。哨兵位本身不存有效业务数据,但其 next 和 prev 均指向自身,形成闭环。

// 初始化节点
void ListNodeInit(LN** pphead)
{
	LN* newNode = BuynewNode(0);
	*pphead = newNode;
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;
}

调用示例:

// test.c
	LN* plist = NULL;
	ListNodeInit(&plist); 

第4步:实现尾插操作

尾插即在最后一个有效数据和哨兵位之间插入新节点。由于只需修改节点内部字段而不改变链表头指针,故传一级指针即可。使用 assert(phead) 确保链表存在。插入过程涉及四个指针关系的调整:

  1. 找到当前尾部节点 tail = phead->prev。
  2. 将旧尾部的 next 指向新节点。
  3. 将新节点的 prev 指向旧尾部。
  4. 将新节点的 next 指向哨兵位,并将哨兵位的 prev 指向新节点。
// 尾插
void ListNodePushBack(LN* phead, LNDataType x)
{
	assert(phead);
	LN* newNode = BuynewNode(x);
	LN* tail = phead->prev;
	
	// 连接旧尾部与新节点
	tail->next = newNode;
	newNode->prev = tail;
	
	// 连接新节点与哨兵位
	newNode->next = phead;
	phead->prev = newNode;
}

第5步:打印链表数据

遍历时应从第二个节点(phead->next)开始,直到回到哨兵位为止,避免打印哨兵位的伪数据。

// 打印链表
void ListNodePrint(LN* phead)
{
	assert(phead);
	LN* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

第6步:实现尾删操作

尾删即将最后一个有效节点移除。关键点是:绝对不要free掉哨兵位。因此必须断言链表不止有哨兵位一个节点(phead->next != phead)。删除步骤:

  1. 定位尾部节点 tail 及其前驱 tailPrev。
  2. 断开尾部节点的链接:将哨兵位的 prev 指向 tailPrev,将 tailPrev 的 next 指向哨兵位。
  3. 释放 tail 内存并置空。
// 尾删
void ListNodePopBack(LN* phead)
{
	assert(phead);
	assert(phead->next != phead); // 防止删掉头结点
	
	LN* tail = phead->prev;
	LN* tailPrev = tail->prev;
	
	// 重新建立连接
	phead->prev = tailPrev;
	tailPrev->next = phead;
	
	free(tail);
	tail = NULL;
}

第7步:实现头插操作

头插在哨兵位和第一个有效节点之间插入新节点。同样只需修改局部指针指向:

// 头插
void ListNodePushFront(LN* phead, LNDataType x)
{
	assert(phead);
	LN* newNode = BuynewNode(x);
	LN* next = phead->next; // 暂存原第一个节点
	
	// 哨兵位与新节点连接
	phead->next = newNode;
	newNode->prev = phead;
	
	// 新节点与原第一个节点连接
	newNode->next = next;
	next->prev = newNode;
}

第8步:在指定位置前插入节点

该函数通用性极强,既可用于中间插入,也可用于头插。逻辑是找到 pos 的前驱,将新节点夹在它们中间。

// 在 pos 前面插入节点
void ListNodeInsert(LN* pos, LNDataType x)
{
	if (pos == NULL)
	{
		printf("没有找到这个数\n");
		return;
	}
	LN* newNode = BuynewNode(x);
	LN* tailPrev = pos->prev;
	
	// 连接前驱与新节点
	tailPrev->next = newNode;
	newNode->prev = tailPrev;
	
	// 连接新节点与pos
	newNode->next = pos;
	pos->prev = newNode;
}

第9步:删除指定位置节点

删除 pos 节点时,需确保 pos 不是哨兵位,否则会导致破坏链表基础结构。操作逻辑为跨越 pos,将其前后节点直接相连,然后释放 pos。

// 删除指定位置节点
void ListNodeErease(LN* phead, LN* pos)
{
	if (pos == phead || pos == NULL)
	{
		printf("pos指向头,或为空\n");
		return;
	}
	LN* posPrev = pos->prev;
	LN* posNext = pos->next;
	
	// 跨越删除
	posPrev->next = posNext;
	posNext->prev = posPrev;
	
	free(pos);
	pos = NULL;
}

第10步:销毁整个链表

程序结束时必须释放所有节点以防止内存泄漏。遍历所有节点进行 free 操作。注意:最后还需手动 free 哨兵位。形参指针置空无法影响外部实参,这是C语言值传递的特性。

// 销毁链表
void ListNodeDestroy(LN* phead)
{
	assert(phead);
	LN* cur = phead->next;
	while (cur != phead)
	{
		LN* next = cur->next;
		free(cur);
		cur = next;
	}
	// 别忘了释放哨兵位
	free(phead);
	// phead = NULL; // 此处无法改变外部变量
}

以上就是C语言实现带头双向循环链表的详细内容,更多关于C语言数据结构链表实战的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。