C语言实现带头双向循环链表功能

 更新时间:2026年08月31日 14:12:20   作者:不灭的黄金瞳123  
本文用C语言手把手带你实现,从哨兵位、节点结构到插入删除、查找修改和销毁,每个操作都有详细代码和测试,无论你是初学者还是想复习,都能快速掌握双向链表的精髓,提升数据结构功底

双向链表的结构

有了之前不带头单链表的基础,现在再来看看双向链表,全称带头双向循环链表

这里的带头并不是指的之前单链表中的头节点,实际称为哨兵位。哨兵位节点不存储任何有效数据,只是用于避免遍历链表时进入死循环。

带头双向循环链表的实现

本文手写的带头双向循环链表存储的数据类型为 int

带头双向循环链表的结构

// 重命名方便书写和修改
typedef int LDataType;
typedef struct ListNode
{
	LDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

prev 指向的是前一个节点的, next 指向的是后一个节点, data 用于存放数据

带头双向循环链表的方法

头文件包含

#include <stdio.h> //标准输入输出
#include <stdlib.h> // 主要使用动态开辟内存
#include <assert.h> // 主要使用断言

带头双向循环链表节点的申请

申请的一个节点要让他自循环

ListNode* BuyNode(LDataType data)
{
	// 动态申请空间
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (!node)
	{
		// 没有开辟成功
		perror("malloc error");
		exit(1);
	}
	node->data = data;
	// 要让这个节点自循环
	node->prev = node;
	node->next = node;
	return node;
}

动态申请的内存空间存放于堆区,不主动释放出函数后不会被系统收回

测试

int main()
{
	// 测试节点的申请
	ListNode* n = BuyNode(1);
	return 0;
}

带头双向循环链表的插入

带头双向循环链表数据的打印

打印整个链表数据就涉及到遍历链表,需要找到尾节点,尾节点的下一个节点是哨兵位

void Print_List(ListNode* phead)
{
	// 打印链表
	// 判空,传进来的 phead 不能为 NULL
	assert(phead);
	if (phead->next == phead)
	{
		// 处理只有一个哨兵位的情况
		printf("NULL\n");
		return;
	}
	ListNode* pcur = phead-> next; // 用 pcur 去进行遍历
	while (pcur != phead)
	{
		// 当 pcur 走到 phead 的位置时说明整个链表都已经走完了
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}
void Reverse_Print_List(ListNode* phead)
{
	// 逆序打印链表
	// 判空,传进来的 phead 不能为 NULL
	assert(phead);
	if (phead->prev == phead)
	{
		// 处理只有一个哨兵位的情况
		printf("NULL\n");
		return;
	}
	ListNode* pcur = phead->prev; // 用 pcur 去进行遍历
	while (pcur != phead)
	{
		// 当 pcur 走到 phead 的位置时说明整个链表都已经走完了
		printf("%d->", pcur->data);
		pcur = pcur->prev;
	}
	printf("\n");
}

头插

让新节点的 next 指向原头节点(哨兵位的 next 指向的节点), prev 指向哨兵位。原头节点(哨兵位的 next 指向的节点)的 prev 指向新节点,哨兵位的 next 指向这个新节点。

void PushFront(ListNode* phead, LDataType data)
{
	// 双向链表的头插
	// 因为哨兵位不用存放有效数据,且哨兵位本身不能被修改,所以用传值调用即可
	// 判空,传进来的 phead 不能为 NULL
	assert(phead);
	// 调用申请节点函数
	ListNode* newnode = BuyNode(data);
	// 让新节点的 next 指向原头节点(哨兵位的 next 指向的节点)
	newnode->next = phead->next;
	// 让新节点的 prev 指向哨兵位
	newnode->prev = phead;
	//原头节点(哨兵位的 next 指向的节点)的 prev 指向新节点
	phead->next->prev = newnode;
	// 哨兵位的 next 指向这个新节点
	phead->next = newnode;
}

测试

int main()
{
	// 测试头插和打印
	ListNode* head = BuyNode(-1);// 申请哨兵位
	Print_List(head);
	Reverse_Print_List(head);
	PushFront(head, 2);
	PushFront(head, 3);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

尾插

让新节点的 prev 指向原尾节点,next 指向哨兵位,原尾节点的 next 指向新节点,哨兵位的 prev 指向新节点。

void PushBack(ListNode* phead, LDataType data)
{
	// 尾插
	// 因为哨兵位不用存放有效数据,且哨兵位本身不能被修改,所以用传值调用即可
	// 判空,传进来的 phead 不能为 NULL
	assert(phead);
	// 调用申请节点函数
	ListNode* newnode = BuyNode(data);
	// 让新节点的 prev 指向原尾节点
	newnode->prev = phead->prev;
	// 让新节点的 next 指向哨兵位
	newnode->next = phead;
	// 原尾节点的 next 指向新节点
	phead->prev->next = newnode;
	// 哨兵位的 prev 指向新节点
	phead->prev = newnode;
}

测试

int main()
{
	// 测试尾插
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	PushBack(head, 2);
	PushBack(head, 3);
	PushBack(head, 4);
	PushBack(head, 5);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

带头双向循环链表的查找

遍历链表查找节点,找到了返回,找不到返回 NULL

ListNode* Find(ListNode* phead, LDataType data)
{
	// 双向链表的查找
	// 传进来的 phead 不能是 NULL
	assert(phead);
	if (phead->prev == phead)
	{
		// 处理只有一个哨兵位的情况,这是个空链表
		printf("当前链表为空!\n");
		return NULL;
	}
	ListNode* pcur = phead->next;// 用 pcur 去遍历
	while (pcur != phead)
	{
		if (pcur->data == data)
		{
			// 找到了就返回
			return pcur;
		}
		pcur = pcur->next;
	}
	// 出来说明遍历完了链表都没找到
	return NULL;
}

指定位置前插入

让新节点的 prev 指向原节点的 prev 指向的节点,next 指向原节点,原节点的 prev 指向新节点,原节点的 next 指向的节点指向新节点。

void Push_Pos_Before(ListNode* pos, LDataType data)
{
	// 指定为之前插入
	// 判空,传进来的 pos 不能为 NULL
	assert(pos);
	ListNode* prev = pos->prev; // 用 prev 来记住 pos 前的节点
	// 调用申请节点函数
	ListNode* newnode = BuyNode(data);
	//让新节点的 prev 指向原节点的 prev 指向的节点
	newnode->prev = prev;
	// 让新节点的 next 指向原节点
	newnode->next = pos;
	// 原节点的 prev 指向新节点
	pos->prev = newnode;
	// 原节点的 next 指向的节点指向新节点
	prev->next = newnode;
}

测试

int main()
{
	// 测试指定位置插入
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	Push_Pos_Before(head, 1);
	Push_Pos_Before(head, 2);
	Push_Pos_Before(head, 3);
	Print_List(head);
	Reverse_Print_List(head);
	ListNode* find = Find(head, 2);
	Push_Pos_Before(find, 4);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

带头双向循环链表的删除

节点都是动态申请的空间,删除需要用到 free

尾删

让尾节点的 prev 指向的节点的 next 指向头节点,头节点的 prev 指向新的尾节点,再释放尾节点

void PopBack(ListNode* phead)
{
	// 双向链表尾删
	// 判空,传进来的 phead 不能为 NULL
	assert(phead);
	if (phead->next == phead)
	{
		// 只有一个哨兵位,说明链表是空的
		printf("当前链表为空!\n");
		return;
	}
	ListNode* pcur = phead; // 让 pcur 去遍历链表
	ListNode* prev = pcur->prev; // 用 ptail 去记录 pcur->next 指向的节点
	while (pcur->next != phead)
	{
		// pcur->next 为 phead 时说明 pcur 已经到尾节点了
		pcur = pcur->next;
		prev = pcur->prev;
	}
	// 让尾节点的 prev 指向的节点的 next 指向头节点
	prev->next = phead;
	// 让头节点的 prev 指向新的尾节点
	phead->prev = prev;
	// 释放原尾节点
	free(pcur);
}

测试

int main()
{
	// 测试尾删
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	Push_Pos_Before(head, 1);
	Push_Pos_Before(head, 2);
	Push_Pos_Before(head, 3);
	Print_List(head);
	Reverse_Print_List(head);
	PopBack(head);
	Print_List(head);
	Reverse_Print_List(head);
	PopBack(head);
	Print_List(head);
	Reverse_Print_List(head);
	PopBack(head);
	Print_List(head);
	Reverse_Print_List(head);
	PopBack(head);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

头删

哨兵位的 next 指向要删除节点的 next 指向的节点,要删除节点的 next 指向的节点的 prev 指向哨兵位,最后释放要删除的节点。

void PopFront(ListNode* phead)
{
	// 头删
	// 判空,传进来的 phead 不能为 NULL
	assert(phead);
	if (phead->next == phead)
	{
		// 只有一个哨兵位,说明链表是空的
		printf("当前链表为空!\n");
		return;
	}
	// 记录要删除的节点
	ListNode* del = phead->next;
	// 哨兵位的 next 指向要删除节点的 next 指向的节点
	phead->next = del->next;
	// 要删除节点的 next 指向的节点的 prev 指向哨兵位
	del->next->prev = phead;
	// 释放要删除的节点
	free(del);
}

测试

int main()
{
	// 测试头删
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	Push_Pos_Before(head, 1);
	Push_Pos_Before(head, 2);
	Push_Pos_Before(head, 3);
	Print_List(head);
	Reverse_Print_List(head);
	PopFront(head);
	Print_List(head);
	Reverse_Print_List(head);
	PopFront(head);
	Print_List(head);
	Reverse_Print_List(head);
	PopFront(head);
	Print_List(head);
	Reverse_Print_List(head);
	PopFront(head);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

指定位置删除

void Pop_Pos_Del(ListNode* phead, ListNode* pos)
{
	// 指定位置删除
	// 判空,传进来的 pos 和 phead 不能为空
	assert(phead && pos);
	if (pos == phead)
	{
		printf("哨兵位不能删除!\n");
		return;
	}
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	// 让 pos 前的节点的 next 指向 pos 后的节点
	prev->next = next;
	// 让 pos 后的节点的 prev 指向 pos 前的节点
	next->prev = prev;
	// 释放节点
	free(pos);
}

测试

int main()
{
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	Push_Pos_Before(head, 1);
	Push_Pos_Before(head, 2);
	Push_Pos_Before(head, 3);
	Push_Pos_Before(head, 4);
	Push_Pos_Before(head, 5);
	Print_List(head);
	Reverse_Print_List(head);
	ListNode* find1 = Find(head, 1);
	ListNode* find2 = Find(head, 2);
	ListNode* find3 = Find(head, 5);
	//测试删中间
	Pop_Pos_Del(head, find2);
	Print_List(head);
	Reverse_Print_List(head);
	//测试删头
	Pop_Pos_Del(head, find1);
	Print_List(head);
	Reverse_Print_List(head);
	//测试删尾
	Pop_Pos_Del(head, find3);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

带头双向循环链表值的修改

void Pos_Edit(ListNode* phead, ListNode* pos, LDataType data)
{
	// 链表值的修改
	// 判空,传进来的 phead 和 pos 不允许修改
	assert(phead && pos);
	if (phead == pos)
	{
		printf("哨兵位不允许修改!\n");
		return;
	}
	// 修改
	pos->data = data;
}

测试

int main()
{
	// 测试修改
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	Push_Pos_Before(head, 1);
	Push_Pos_Before(head, 2);
	Push_Pos_Before(head, 3);
	Print_List(head);
	Reverse_Print_List(head);
	ListNode* find = Find(head, 2);
	Pos_Edit(head, find, 4);
	Print_List(head);
	Reverse_Print_List(head);
	find = Find(head, 3);
	Pos_Edit(head, find, 5);
	Print_List(head);
	Reverse_Print_List(head);
	return 0;
}

带头双向循环链表的销毁

从头节点开始释放,最后释放尾节点

void Destory(ListNode** pphead)
{
	// 链表的销毁
	// 判空,传进来的 phead 不能为 NULL
	// 因为这里会销毁哨兵位所以传二级指针
	assert(pphead && *pphead);
	ListNode* pcur = (*pphead)->next;// 用 pcur 来遍历,从头节点开始释放
	ListNode* ptail = pcur->next; // 用 ptail 来记录 pcur 的下一个节点
	while (pcur != *pphead)
	{
		// 释放当前节点
		free(pcur);
		// 走向下一个节点
		// 当 pcur 走到哨兵位的时候, 哨兵位还没有被释放
		// 所以 ptail 还可以向前走
		pcur = ptail;
		ptail = ptail->next;
	}
	//最后释放哨兵位
	free(*pphead);
	*pphead = NULL;
}

测试

int main()
{
	// 测试销毁链表
	ListNode* head = BuyNode(-1); // 申请哨兵位
	Print_List(head);
	Push_Pos_Before(head, 1);
	Push_Pos_Before(head, 2);
	Push_Pos_Before(head, 3);
	Print_List(head);
	Reverse_Print_List(head);

	Destory(&head);
	return 0;
}

到此这篇关于C语言实现带头双向循环链表的文章就介绍到这了,更多相关C语言带头双向循环链表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言 深入理解动态规划之计数类DP

    C语言 深入理解动态规划之计数类DP

    动态规划可谓是大名鼎鼎,笔试面试中的高频考点,也是重点难点,动态规划类型题目灵活多变,难度系数也相对较高,往往我们做不好动态规划的题目就会与心仪的offer失之交臂,本篇文章我们就一起来研究一下动态规划的计数类DP
    2022-04-04
  • C语言实现大数据文件的内存映射机制

    C语言实现大数据文件的内存映射机制

    这篇文章主要介绍了C语言实现大数据文件的内存映射机制的相关资料,需要的朋友可以参考下
    2017-01-01
  • Qt QCompleter自动补全的实现

    Qt QCompleter自动补全的实现

    本文主要介绍了Qt QCompleter自动补全的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • 利用C语言实现顺序表的实例操作

    利用C语言实现顺序表的实例操作

    顺序表是线性表中的一种重要的数据结构,也是最基础的数据结构,所以他不仅是学习中的重点,也是应用开发非常常用的一种数据结构。这篇文章介绍如何利用C语言实现顺序表。
    2016-08-08
  • Qt数据库应用之实现通用数据库请求

    Qt数据库应用之实现通用数据库请求

    这篇文章主要为大家介绍了Qt中是如何实现通用数据库请求的,文中的示例代码讲解详细,对我们学习Qt有一定帮助,感兴趣的小伙伴可以了解一下
    2022-03-03
  • C++模拟实现时间轮模式(推荐)

    C++模拟实现时间轮模式(推荐)

    文章介绍了时间轮(TimerWheel)的原理和实现方式,这是一种高效的定时器管理算法,常用于操作系统内核,时间轮通过类似钟表的机制管理定时任务,每过一秒,秒针移动一格,触发相应任务,感兴趣的朋友跟随小编一起看看吧
    2025-11-11
  • 基于c++中的默认拷贝函数的使用详解

    基于c++中的默认拷贝函数的使用详解

    本篇文章对c++中默认拷贝函数的使用进行了详细的分析介绍。需要的朋友参考下
    2013-05-05
  • C++ STL入门教程(2) list双向链表使用方法(附程序代码)

    C++ STL入门教程(2) list双向链表使用方法(附程序代码)

    这篇文章主要为大家详细介绍了C++ STL入门教程第二篇,list双向链表使用方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • C语言实现万年历小功能

    C语言实现万年历小功能

    这篇文章主要为大家详细介绍了C语言实现万年历小功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-03-03
  • DSP中浮点转定点运算--定点数模拟浮点数运算及常见的策略

    DSP中浮点转定点运算--定点数模拟浮点数运算及常见的策略

    本文主要讲解DSP中定点数模拟浮点数运算及常见的策略,具有参考价值,需要的朋友可以参考一下。
    2016-06-06

最新评论