Bootstrap

DS线性表之带头双向循环链表的讲解和实现(3)


前言

  前文所讲的是不带头单向不循环链表,三个条件都相反的带头循环链表就是我们今天所要讲的内容

  它也很常用,所以我们有必要好好了解它一下


一、带头双向循环链表的理解

在这里插入图片描述

头节点的认识

我们可以发现,所谓的“带头”就是比单链表多一个看似“无用”的头节点,该节点不存储任何信息或存储任何有效元素,起到"放哨"作用,作用是减少了对一个节点是否为空的判断

所以,带头双向循环链表也是要初始化的

二、带头双向循环链表的实现

  嗯,理解就这么结束了,乐~

节点成员

因为不仅有后驱指针,也有前驱指针,才能达到所需要的双向,所以我们的节点内容要稍微修改一下:

typedef int LTDataType;//定义数据类型,可以根据需要更改
typedef struct ListNode
{
	LTDataType data;      //数据域 存储数据
	struct ListNode* next;//指针域 存储指向下一个结点的指针
	struct ListNode* prev;//指针域 存储指向前一个结点的指针
}ListNode;

申请一个新节点

我们先实现一个申请新节点函数,而不是先考虑初始化,这是因为初始化其实也要申请一个新的节点
在这里插入图片描述

ListNode* BuyList(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

初始化

此链表的初始化就是创建一个头节点,注意因为是双向循环的,所以对于只有一个哨兵节点的链表来说,不管是前驱指针还是后驱指针,都是指向自己的,这可能需要你结合下图来理解一下:
在这里插入图片描述

ListNode* ListInit()
{
	ListNode* phead = BuyList(0); // 数字随便取,不重要,指针指对就行

	phead->next = phead;//构成循环
	phead->prev = phead;//构成循环

	return phead;
}

销毁

尤其要注意这个循环的条件,不是cur所指向空,cur在这里由于循环的缘故永远不会指向为空的,而是cur不为头节点的时候,就说明还有节点没被销毁,而出了循环之后,头节点也是节点,也要手动销毁并置空指针

先将除哨兵位之外的空间释放,最后在释放哨兵位

void ListDestory(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL; //养成好习惯,释放之后手动置为NULL
}

尾插

好,这个时候你就会发现循环的优势了,之前我们要从头节点开始遍历到尾部,而这里我们直接通过哨兵节点的前驱指针直接来到了最后一个节点,这时候再插入就很方便了
在这里插入图片描述
在这里插入图片描述

void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
	//1.创建结点
	ListNode* newnode = BuyList(x);
	ListNode* tail = phead->prev;//先找到尾结点
    
    //2.链接next
	tail->next = newnode;
	newnode->prev = tail;
	
    //3.链接prev
	newnode->next = phead;
	phead->prev = newnode;
}

头插

这也很方便,因为有哨兵节点的缘故,即使我们的链表没有一个有效数据,下列代码也适用
在这里插入图片描述
在这里插入图片描述

void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* newnode = BuyList(x);
	ListNode* first = phead->next;
	
	phead->next = newnode;
	newnode->prev = phead;

	newnode->next = first;
	first->prev = newnode;
}

头删

这里就不仅要检查一下哨兵节点的存在,也要检验一下是否有有效数据了,不然的话,就要把哨兵位给删掉了,这是我们所不愿看到的
在这里插入图片描述
在这里插入图片描述

void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//没有数据则报错
	ListNode* first = phead->next;
	ListNode* second = first->next;

	phead->next = second;
	second->prev = phead;
	free(first);
	first = NULL;
}

尾删

同样要检查一下是否真的有有效数据
在这里插入图片描述

void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	ListNode* tail = phead->prev;
	ListNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;

	free(tail);
	tail = NULL;
}

查找

遍历一遍链表,如果该结点的data等于x则返回该结点的地址,遍历一遍没有找到则返回NULL,跟后面在pos位置插入函数结合起来用

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;
}

在pos之前插入

跟头插尾插思想差不多,可以自己画图理解理解,先ListFind找到所要插入的位置,再执行插入操作

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = BuyList(x);
	ListNode* prev = pos->prev;

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

删除pos位置

也是要先ListFind找到节点的地址

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = pos->next;
	next->prev = prev;
}

判断链表是否为空

还是要注意这里的判断条件不是cur是否指向空,而是是否指向自身

bool ListEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;//相等则为真,不相等则为假
}

总结

  我们发现此链表虽然听起来复杂,但是实现起来竟然比单链表好很多,这其实启发我们不要被事物的表面所吓到,说不定再撑一会儿,就拨云见日了呢~

;