双向带头循环链表-实现思路+图解

目录

一.何为双向循环链表?

1.何为'双向'?

2.何为'带头'?

3.何为'循环'?

 二.如何实现双向带头循环链表?

1.基本结构-结点的创建

2.创建哨兵位结点

3.链表的增删查改


一.何为双向循环链表?

在学习双向带头循环链表之前,我们一定都学习过最基础的单链表。单链表是一种线性的数据结构,由若干个动态内存分配的结点相互链接而形成,其中每一个结点拥有一个指向下一个结点的指针。因此,通过头节点进行顺序访问,我们可以遍历这个单链表获取数据。单链表图示如下:

然而,由于结点只存有下一个结点的指针,使得我们在需要对单链表进行尾部操作(如尾插,尾删)时,需要先遍历单链表找到尾部,其时间复杂度为O(N)。那么有没有一个更好的数据结构能帮我们提高效率呢? 双向带头循环链表由此而来。

那么首先介绍一下何为双向带头循环链表。其中'双向','带头','循环'均为链表的特性。

1.何为'双向'?

与之相对的概念便是'单向','单向'指的是对于每个结点,其内部有一个next指针指向下一个结点。单链表便指的是单向链表。

而双向链表,其内部有prev和next两个指针,分别指向上一个结点和下一个结点,因此支持向上访问结点,也更加灵活。

2.何为'带头'?

一般来说,我们需要对链表进行管理,方便对其进行增删查改各种操作。这里便存在两种管理方式:1.不带头,用头指针进行管理       2.带头,即用不存放数据的一个'空结点'进行管理,此节点又称为'哨兵位结点'。

带头链表具有的优势在单链表中体现为尾部操作(即尾插,尾删)时更加简单,不用区分插入或删除时上一个元素是头指针还是结点。而在双向循环链表中,其更是极大的方便了增删查改的操作,后面我们将进行介绍。

3.何为'循环'?

循环最大的特点便是'头尾相连',即尾结点的下一个结点不指向NULL,而是指向头部结点。形成一个环状结构。此概念较为简单,此处不多赘述。

明白了上面的概念之后,我们就应该了解了双向带头循环链表的结构,图示如下:

 二.如何实现双向带头循环链表?

1.基本结构-结点的创建

// 带头+双向+循环链表
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

LTDataType为存放数据类型,_prev指向前一个结点,而_next指向后一个结点,和前面我们认知的结构完全一样。

2.创建哨兵位结点

由于我们需要用哨兵位结点对链表进行管理,因此需要先创建此节点。

ListNode* ListCreate()
{
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head == NULL)
	{
		printf("create error\n");
		exit(-1);
	}
	head->_prev = head;
	head->_next = head;
	return head;
}

实现非常简单,动态开辟一个结点,进行初始化后作为函数的返回值返回即可。并命名哨兵位为head-头结点。

3.链表的增删查改

和单链表的实现一样,首先需要实现一个函数创建新结点,其实现与哨兵位结点的创建相似,只是初始化时需要将数据赋值。

ListNode* CreateNewNode(LTDataType x)
{
	ListNode* ret = (ListNode*)malloc(sizeof(ListNode));
	if (ret == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}
	ret->_data = x;
	ret->_prev = NULL;
	ret->_next = NULL;
	return ret;
}

接下来便是增删查改操作的实现,我们需要实现以下功能:

// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

 和单链表相比,双向带头循环链表插入和删除的操作甚至更为简单,下面给出思路:

假设插入的位置为p2之前,即已知p2地址(特别的,头插为head->next,尾插为head)

1.创建新结点。  2.定义变量存储p1(即为p2->prev)和p2的地址。  3.链接newnode和p1,p2。

 所有的插入删除都可以这样操作,此处不多赘述,下面直接给出代码以供参考。

1.头插

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newNode = CreateNewNode(x);

	//创建成功,开始插入新结点
	ListNode* next = pHead->_next;

	newNode->_prev = pHead;
	pHead->_next = newNode;
	newNode->_next = next;
	next->_prev = newNode;
}

 2.尾插

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newNode = CreateNewNode(x);

	ListNode* tail = pHead->_prev;

	newNode->_prev = tail;
	tail->_next = newNode;
	newNode->_next = pHead;
	pHead->_prev = newNode;
}

3.头删

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->_next != pHead);

	ListNode* front = pHead->_next;
	ListNode* next = front->_next;
	
	free(front);
	front = NULL;
	pHead->_next = next;
	next->_prev = pHead;

	ListErase(pHead->_next);
}

4.尾删

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->_next != pHead);

	ListNode* tail = pHead->_prev;
	ListNode* prev = tail->_prev;

	free(tail);
	tail = NULL;
	pHead->_prev = prev;
	prev->_next = pHead;
	ListErase(pHead->_prev);
}

在实现指定位置插入和删除之前,为了获取位置,我们先要实现Find函数来查找对应数据的结点位置。实现如下:

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		if (cur->_data == x)
			return cur;
		cur = cur->_next;
	}
	return NULL;
}

若找到返回对应结点地址,否则返回NULL。此处只需注意遍历的条件:哨兵位结点下一个结点为第一个有效数据结点,从此处开始遍历,直到变量再次指向哨兵位结点时跳出循环。后面链表的打印和销毁遍历时同理。

5.查找

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		if (cur->_data == x)
			return cur;
		cur = cur->_next;
	}
	return NULL;
}

6.指定位置前插入

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newNode = CreateNewNode(x);
	ListNode* prev = pos->_prev;

	newNode->_next = pos;
	pos->_prev = newNode;
	newNode->_prev = prev;
	prev->_next = newNode;
}

7.指定位置删除

void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->_prev;
	ListNode* next = pos->_next;

	free(pos);
	pos = NULL;
	prev->_next = next;
	next->_prev = prev;
}

8.链表打印

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		printf("%d  ", cur->_data);
		cur = cur->_next;
	}
	printf("\n");
}

9.链表销毁

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		ListNode* next = cur->_next;
		free(cur);
		cur = next;
	}
	free(pHead);
}

由于哨兵位结点head作为循环结束的标志,因此先不进行销毁,从下一个结点开始遍历结点并销毁,最后再释放head结点。双向带头循环链表操作由此完成。

只要明白结构和大体思路,并对单链表有一定了解,便能轻松的实现双向带头循环链表,操作非常简单,大家也可以动手尝试。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值