【数据结构】带头双向循环链表


前言

数据结构的常见线性表,分别是顺序表,链表,栈,队列
本篇给大家带来带头双向循环链表的实现和讲解


一、带头双向循环链表的结构

1.1、概念

在这里插入图片描述

  • 带头:不像单链表头节点就存放数据的,这里的作用主要作哨兵位,就不需要传二级指针,因为对头部没有修改。
  • 双向:有两个指针,分别是prev和next,分别存放前一个节点和后一个节点`的地址
  • 循环:头节点的prev尾,尾的next是头节点,意思是头节点前一个尾下一个头节点

综上所述,这就是链表的最完美的结构,主要用来存储数据,但是单链表的存在并非没有意义,单链表主要做复杂数据结构的子结构和OJ,无论是尾插,头插等等,都是非常方便,而且实现也比较简单

二、带头双向链表的实现

2.1、结构定义

单个结点包含三个部分:前指针(prev)数据(data)后指针(next)
在这里插入图片描述

// 链表的结构
typedef int ListDataType;
// 链表的结构
typedef struct ListNode
{
	ListDataType data;		// 数据域
	struct ListNode* prev;	// 前一个结点
	struct ListNode* next;	// 后一个结点

}ListNode;

2.2、初始化

注意初始化是这里返回类型,我们是通过函数来完成初始化的,说明我们调用函数把头节点给修改,但是并没有用二级指针,这里我用的另外一个方法是函数的返回

该链表的结构十分的巧妙,只有一个节点的时候,就是哨兵位节点,不存储有效数据,而是做一个标志(哨兵)方便对第一个数据修改,而已他的next和prev都是指向自己,这是由于循环的特性,在删除和插入的是方便统一操作。

// 初始化
ListNode* ListInit()
{
	// 申请一块空间
	ListNode* phead = (ListNode*)malloc(sizeof(struct ListNode));
	assert(phead);		// 防止误操作
	
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

我就应该这样调用
在这里插入图片描述

初始化后的结点如图

在这里插入图片描述

2.2、创建一个节点

每次插入的时候,我们都需要写很多重复的代码,所以可以把创建节点写成一个函数,后面方便调用。

// 创建一个节点
ListNode* ListCreateNode(ListDataType x)
{
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	assert(newNode);
	newNode->data = x;
	newNode->prev = NULL;
	newNode->next = NULL;
	
	return newNode;
}

2.3、尾插

在这里插入图片描述

大家可以把代码代入图中(走读代码)就可以很清晰的明白这个过程

// 尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
	// 实参不能传NULL
	assert(phead);

	ListNode* newNode = ListCreateNode(x);
	ListNode* tail = phead->prev;

	// phead  tail  newNode
	tail->next = newNode;
	newNode->prev = tail;
	newNode->next = phead;
	phead->prev = newNode;
}

2.4、头插

在这里插入图片描述

// 头插
void ListPushFront(ListNode* phead, ListDataType x)
{
	// 实参不能传NULL
	assert(phead);

	ListNode* newNode = ListCreateNode(x);
	ListNode* headNext = phead->next;
	
	// phead  newNode  headNext 
	newNode->next = headNext;
	headNext->prev = newNode;
	phead->next = newNode;
	newNode->prev = phead;
}

2.5、尾删

要注意只有哨兵位不能删除,我这里加的assert(断言),为假就报错,所以phead->next != phead

在这里插入图片描述

// 尾删
void ListPopBack(ListNode* phead)
{
	// 实参不能传NULL
	assert(phead);	
	// 就哨兵位一个不能删除
	assert(phead->next != phead);
	
	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;
	
	// phead tailPrev tail
	tailPrev->next = phead;
	phead->prev = tailPrev;

	free(tail);
}

2.6、头删

要注意只有哨兵位不能删除,我这里加的assert(断言),为假就报错,所以phead->next != phead

在这里插入图片描述

// 头删
void ListPopFront(ListNode* phead)
{
	// 实参不能传NULL
	assert(phead);	
	// 就哨兵位一个不能删除
	assert(phead->next != phead);

	ListNode* head = phead->next;
	ListNode* headNext = head->next;
	
	// phead head headNext
	phead->next = headNext;
	headNext->prev = phead;

	free(head);
}

2.7、打印

由于循环的特性,所以没有NULL,所以我们打印的停止条件也要改变,从phead的下一个开始打印,因为phead不存有效数据,打印一圈回到phead即可。

// 打印
void ListPrint(ListNode* phead)
{
	// 实参不能传NULL
	assert(phead);
	
	ListNode* cur = phead->next;
	while (cur != phead)
	{	
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

同时我们在测试下刚才写的插入和删除
在这里插入图片描述

2.8、查找

找到还是返回节点,可以和pos配合或者直接修改那个节点的数据,找不到就返回NULL。

// 查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
	// 防止实参传NULL过来
	assert(phead);
	
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	
	return NULL;
}

2.9、pos位置(pos前)插入

这里就不需要传链表的头

  1. 带哨兵位的原因,不会修改头节点
  2. 由于双向的特性,可以直接找到前后节点,不需在遍历

在这里插入图片描述

// pos处插入
void ListInsert(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newNode = ListCreateNode(x);
	ListNode* posPrev = pos->prev;
	
	// prevPos newNode pos
	posPrev->next = newNode;
	newNode->prev = posPrev;
	newNode->next = pos;
	pos->prev = newNode;
}

2.10、pos位置删除

这里就不需要传链表的头

  1. 带哨兵位的原因,不会修改头节点
  2. 由于双向的特性,可以直接找到前后节点,不需在遍历

在这里插入图片描述

// pos处删除
void ListErase(ListNode* pos)
{
	assert(pos);
	
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;

	// posPrev pos posNext
	posPrev->next = posNext;
	posNext->prev = posNext;
	
	free(pos);
}

测试下pos相关操作
在这里插入图片描述

2.11、利用pos复用插入和删除

  • 尾插:哨兵位前一个就是尾
// 尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
	// 哨兵位前一个就是尾
	ListInsert(phead, x);
}
  • 头插:哨兵位下一个就是头
// 头插
void ListPushFront(ListNode* phead, ListDataType x)
{
	// 哨兵位下一个就是头
	ListInsert(phead->next, x);
}
  • 尾删:哨兵位前一个就是尾
// 尾删
void ListPopBack(ListNode* phead)
{
	// 哨兵位前一个就是尾
	ListErase(phead->prev);
}

  • 头删:哨兵位下一个是头
// 头删
void ListPopFront(ListNode* phead)
{
	// 哨兵位下一个是头
	ListErase(phead->next);
}

测试结果,和我们之前写的测试结果一模一样,所以想要快速实现双向带头循环链表功能,可以直接写这两个函数即可。
在这里插入图片描述

2.12、销毁

由于函数接口的一致性,我们最好传一级指针,还是这有一个问题,就是phead会野指针,我们无法在函数内把phead置为NULL,所以就只能在主函数里置为NULL,在函数体内也是需要手动置空,所以不一定要在函数体内置NULL,可以在外面置空

在这里插入图片描述

// 销毁
void ListDestroy(ListNode* phead)
{
	ListNode* cur = (phead)->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

主函数调用销毁后,手动在置空
在这里插入图片描述


总结

带头双向循环链表,虽然结构复杂但是实现起来特别简单,如果想快速实现功能可以直接写两个函数,pos的插入和删除,带头双向循环链表主要还是主要存储数据。

  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值