【数据结构】链表中的GOAT——带头+双向+循环链表(结尾附源码)

1 带头双向循环链表的概念

带头双向循环链表,是众多链表结构中最复杂的一种结构。顾名思义,带头就是指带有头结点;双向表示链表可以从左往右走,也可以从右往左走;循环则是字面意思,及此类链表可以构成一个环。

其逻辑结构如下:

对于带头双向循环链表,每个节点有两个指针,分别指向当前节点的上一个和下一个结点,我们将其定义为prev和next。

头结点的prev指向尾结点,尾结点的next指向头结点,这样做就形成了一个循环,我们可以很快地通过指针找到尾结点,链表的插入删除会更加快捷。

由于链表中始终有一个结点(头结点),因此在对链表插入删除的过程中,无需再像单链表一样分多种情况考虑。

一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。尽管带头双向循环链表看上去,结构比常见的无头单向非循环链表要复杂,但是使用代码实现以后会发现结构会带来很多优势,接口实现和维护反而会变得更简单。

2 链表实现

2.1 结点的结构

对于带头双向循环链表,每个节点都应该有两个指针以及存放数据的部分。这里我们假设节点当中只存储一个数据。

typedef int LTDatatype;
typedef struct ListNode
{
	LTDatatype data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

2.2 新结点的创建

由于每一次添加新节点都需要开辟新的空间,于是我们编写一个创建新结点的函数:

LTNode* BuyLTNode(LTDatatype x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;

	return newnode;
}

2.3 链表初始化

由于该链表是带头的,因此最好是将链表初始化一下,将最初的结点置为头结点。

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

这里需要注意,在链表中只有头结点单个结点时,为了符合双向循环的条件,需要将头结点的next和prev都指向自身,此时头结点既是头,也是尾。

一般对于有头结点的链表,头结点最好不要用来存放有效数据。这里的-1并不是有效数据,只是为了方便创建结点,没有实际意义。

2.4 链表尾插

回想一下单链表的尾插,我们需要循环遍历找到原链表的尾结点后才能执行插入操作,而这时带头双向循环链表的优势就体现出来了,由于头结点中存放了尾结点的地址,就可以很快速的找到尾结点。

同时,尾插的代码量是非常小的,没有多余的判断,只需要改变四个指针。

void LTPushBack(LTNode* phead, LTDatatype x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;
}

2.5 链表头插

对于头插也是同理,只需要改变四个指针。、

void LTPushFront(LTNode* phead, LTDatatype x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);
	LTNode* cur = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = cur;
	cur->prev = newnode;
}

2.6 判断链表是否为空

当链表中只有头结点一个节点时,即链表当中没有存储有效数据,那么我们认为这种链表是空链表。

当链表为空以后,如果我们进行删除操作,显然是不符合逻辑,会产生bug的。因此,可以设计一个判断链表为空的函数,在后续配合删除函数使用。

我们可以用布尔值来编写这个函数。

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

这里有一个巧妙地设计,当return中的条件成立的时候,链表中只有头结点一个节点,所以是空间表,返回true;反之就返回false。

这样的设计避免了一部分代码量,同时也容易理解。

2.7 链表尾删

我们先通过头结点的prev找到尾结点,然后通过尾结点的prev找到尾结点的上一个结点(即将成为新的尾结点),将该节点直接与头结点建立循环联系,最后再将原来的尾结点free掉即可。

同时,这里可以增加一句断言,与上面编写的判断链表为空的函数配合使用,避免对空链表进行删除操作引起程序bug。

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* first = tail->prev;
	first->next = phead;
	phead->prev = first;
	free(tail);
}

2.8 链表头删 

这里的逻辑跟单链表的头删比较类似,先将头结点与待删结点的下一个结点建立联系,再free掉待删节点。

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

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

2.9 链表元素查找

查找操作与单链表也非常类似,通过循环遍历来查找匹配的结点。

由于循环链表的特性,如果整个链表完全遍历一遍后回到头结点,就说明链表中不存在想要查找的结点。

查找到后返回该节点的地址。

LTNode* LTFind(LTNode* phead, LTDatatype x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

2.10 任意位置插入

任意位置的插入函数,一般是要与查找函数配合使用的。这是因为想要在任意位置处插入一个元素,首先需要有一个确切的位置。

一般是通过查找函数先找到想要插入的位置,再使用插入函数。

void LTInsert(LTNode* pos, LTDatatype x)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

对于带头双向循环链表的任意位置插入,一般是插入到指定位置pos之前。 

2.11 任意位置删除

这里指的删除,是直接删除掉指定位置pos处的结点,具体逻辑也跟头删尾删类似。

void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

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

2.12 对于头尾插入、删除的改良

有了任意位置的插入、删除函数,那么对于头尾结点这样特殊位置的插入、删除,是否也可以使用任意位置插入删除函数来进行操作呢?

答案是可以。

既然可以,那我们就可以将这个函数复用以下,减少头尾插入、删除函数的代码量。

头插:

void LTPushFront(LTNode* phead, LTDatatype x)
{
	assert(phead);

	LTInsert(phead->next, x);
}

尾插:

void LTPushBack(LTNode* phead, LTDatatype x)
{
	assert(phead);

	LTInsert(phead, x);
}

尾插是在指定位置pos之前的位置插入,对于phead, 它之前的位置就是原来的尾结点,因此这里将phead作为参数。

头删:

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->next);
}

尾删: 

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->prev);
}

 2.13 链表的销毁

我认为销毁操作和初始化操作应该是所有链表当中操作最简单,最容易理解的操作,就是将所开辟的空间依次释放掉即可。

void LTDestory(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
}

3 源码

3.1 List.h 

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <stdbool.h>

typedef int LTDatatype;
typedef struct ListNode
{
	LTDatatype data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

LTNode* LTInit();

void LTPrint(LTNode* phead);

void LTPushBack(LTNode* phead, LTDatatype x);

void LTPushFront(LTNode* phead, LTDatatype x);

void LTPopFront(LTNode* phead);

void LTPopBack(LTNode* phead);

LTNode*  LTFind(LTNode* phead);

//在pos之前插入
void LTInsert(LTNode* pos, LTDatatype x);

void LTErase(LTNode* pos);

void LTDestory(LTNode* phead);

 3.2 List.c

#include "List.h"

LTNode* BuyLTNode(LTDatatype x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;

	return newnode;
}

void LTPrint(LTNode* phead)
{
	assert(phead);

	printf("guard <==> ");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d <==> ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

void LTPushBack(LTNode* phead, LTDatatype x)
{
	assert(phead);

	/*LTNode* newnode = BuyLTNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;*/

	LTInsert(phead, x);
}

void LTPushFront(LTNode* phead, LTDatatype x)
{
	assert(phead);

	/*LTNode* newnode = BuyLTNode(x);
	LTNode* cur = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = cur;
	cur->prev = newnode;*/

	LTInsert(phead->next, x);
}

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	/*LTNode* first=phead->next;
	LTNode* second = first->next;
	phead->next = second;
	second->prev = phead;
	free(first);*/
	LTErase(phead->next);
}

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	/*LTNode* tail = phead->prev;
	LTNode* first = tail->prev;
	first->next = phead;
	phead->prev = first;
	free(tail);*/
	LTErase(phead->prev);
}

LTNode* LTFind(LTNode* phead, LTDatatype x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

void LTInsert(LTNode* pos, LTDatatype x)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

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

void LTDestory(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
}

猜你喜欢

转载自blog.csdn.net/fbzhl/article/details/130714546