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

链表

链表有双向的、单向的,带头的、不带头的,循环的、非循环的,这次介绍的双向链表是带头循环双向列表,虽然该链表结构复杂,但是实现起来并不复杂。
该链表的第一个节点是不存储有效数据的头节点。具体是如何实现带头循环双向链表的,将在定义链表结构体部分详细讲述。
在这里插入图片描述

定义链表的结构体

定义数据类型

首先我们要确定链表中的数据类型,可以选int,long, double, char等类型,具体的类型可以根据我们的具体情况来选取,下面的代码是将int作为链表存储的数据类型。

typedef int LTDateType;

定义链表的结构体

我们在结构体中定义了两个结构体指针,其中结构体指针prev表示该节点的前一个节点的地址,结构体指针next表示该节点的后一个节点的地址,因此我们可以通过访问该节点找到它的上一个节点和下一个节点,由此实现链表的双向
我们将链表的头节点的prev指向尾节点,将链表的尾结点的next指向头节点,由此实现链表的循环

typedef struct ListNode
{
	LTDateType data;
	struct ListNode* prev;
	struct ListNode* next;

}LTNode;

初始化链表

以下操作是创建了一个由不包含有效数据的头节点构成的链表,由于链表是循环且双向的,所以此时头节点的prev指向自己,next指向自己。最后返回新创建的头指针。

LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));

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

	return phead;
}

链表的打印函数

我们之所以先写链表的打印函数,是为了更好的观察其它关于链表函数实现结果。
为什么要使用assert()函数?
提示:assert()函数中条件为真什么事也不发生,程序继续执行,如果条件为假,则会终止程序运行并报错。
如果我们传入的phead为空时,该函数在下面使用phead的时候将会对phead进行解引用,这将会导致程序运行错误,使用assert()函数可以帮我们快速地找到问题所在。
由于链表是循环的,控制打印的停止将是一个问题。
这里我们通过循环迭代更新cur指向链表的下一个节点,当cur == phead 时就应该停止打印。
提示:不打印phead节点的值是因为phead中不存储有效数据。

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

创建一个新的节点

使用malloc()函数给新的节点分配一个空间,并将新的节点的数据值初始化我们想要的值,并且应该将新节点中的next和prev指针初始化为NULL,要养成好习惯,否则,以后可能会出现野指针的问题。

LTNode* BuyListNode(LTDateType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));

	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

链表的尾插

先创建一个新的节点。
再创建一个结构体指针记录当前的尾节点,这样做的好处是,逻辑清晰,且后续链接节点的时候不用考虑顺序问题
尾结点的查找,由于该链表是循环的,所以头节点的prev所指向的节点就是尾节点。
接下来就是链接部分,第一步是链接当前的尾节点和新的节点,第二步是链接新的节点和头节点,这两步之间没有顺序。

void ListPushBack(LTNode* phead, LTDateType x)
{
	assert(phead);
	LTNode* NewNode = BuyListNode(x);

	LTNode* tail = phead->prev;
	
	tail->next = NewNode;
	NewNode->prev = tail;

	phead->prev = NewNode;
	NewNode->next = phead;
}

链表的尾删

带头的链表一般不删头节点,所以第二个assert()函数所起到的作用就是防止尾删删掉头节点,当头节点的prev指向的是头节点,说明链表只有一个节点,且这个节点是头节点,我们不应该删掉,assert()函数将起到作用。
多定义一个指向尾节点的指针tail可以让逻辑更清晰,且不用考虑顺序问题。
注意:尾节点要最后释放,否则,将会出现野指针问题。

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

	free(tail);
}

链表的头插

同样是先要获得一个新的节点,然后定义一个结构指针指向头结点的下一个节点,使逻辑清晰,链接过程中不用考虑顺序问题。

void ListPushFront(LTNode* phead, LTDateType x)
{
	assert(phead);
	
	LTNode* NewNode = BuyListNode(x);
	LTNode* PheadNext= phead->next;

	NewNode->next = PheadNext;
	PheadNext->prev = NewNode;

	NewNode->prev = phead;
	phead->next = NewNode;
}

链表的头删

同样是为了防止传过来的头节点是NULL,和防止删掉头节点,我们使用了两次assert()函数。
我们同样新创建一个指向头节点下一个节点的指针,使之后不用考虑顺序问题
然后把头节点的下一个节点的下一个节点链接到头节点上,最后释放原来的头节点的下一个节点。

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* PheadNext = phead->next;
	phead->next = PheadNext->next;
	PheadNext->next->prev = phead;

	free(PheadNext);
}

链表查找

整体思路是:如果查找到x的话,返回x的地址;如果在链表中查找完一遍查找不到x,则返回NULL。
先创建一个新的指针cur指向头节点的下一个节点,while循环的条件是cur不指向头节点。当循环结束时即当cur指向头节点时,说明链表已经查找完一遍了,且在链表中未查找到有数据值为x的节点,此时返回NULL。

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

链表任意位置的插入

这里链表的插入说的是在pos位置前(由于链表是双向的,所以查找pos的前一个位置非常方便)插入一个新的节点,这里先创建一个新的结构体,再创建一个结构体指针指向pos的前一个节点,之后将新节点链接上pos位置处的节点,将指向pos的前一个节点的指针链接上新节点链表的插入就完成了。

void ListInsert(LTNode* pos, LTDateType x)
{
	assert(pos);
	LTNode* NewNode = BuyListNode(x);
	LTNode* PosPrev = pos->prev;
	
	NewNode->prev = PosPrev;
	PosPrev->next = NewNode;

	NewNode->next = pos;
	pos->prev = NewNode;

}

链表任意位置的删除

在这里,我们新创建两个结构体指针分别指向pos节点的前一个节点,pos节点的后一个节点,我们只要将新创建的节点链接上,即可在链表中删除pos节点,但一定不要忘了释放pos位置处结构体。

void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* PosNext = pos->next;
	LTNode* PosPrev = pos->prev;

	PosPrev->next = PosNext;
	PosNext->prev = PosPrev;
	
	free(pos);
}

链表的销毁

销毁原链表的时候,要记得把主函数里面的头指针置为NULL,否则,可能会出现野指针的现象。
将主函数里面的头指针置为NULL有三种方法,一种方法是通过传头指针的地址,在函数中修改头指针为NULL;第二种方法就是通过函数返回值将头指针置为NULL;第三种方法就是在调用完销毁函数后在主函数中将头指针置为NULL.
我这里是采用的第三种方法。

void ListDestroy(LTNode* phead)
{
	assert(phead);
	
	LTNode* cur = phead->next;

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

测试

注意要包含所需要的头文件。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

尾插尾删测试

int main()
{
	LTNode* list = ListInit();
	ListPrint(list);
	ListPushBack(list, 1);
	ListPushBack(list, 2);
	ListPushBack(list, 3);
	ListPushBack(list, 4);
	ListPrint(list);
	ListPopBack(list);
	ListPopBack(list);
	ListPopBack(list);
	ListPrint(list);
	list = NULL;
	return 0
}

测试结果
测试结果

头插头删测试

int main()
{
	LTNode* list = ListInit();
	ListPrint(list);
	
	ListPushFront(list, 1);
	ListPushFront(list, 2);
	ListPushFront(list, 3);
	ListPushFront(list, 4);
	ListPrint(list);

	ListPopFront(list);
	ListPrint(list);
	list = NULL;
	return 0;
}

测试结果
测试结果

查找、插入、删除测试

int main()
{
	LTNode* list = ListInit();
	ListPrint(list);
	ListPushFront(list, 1);
	ListPushFront(list, 2);
	ListPushFront(list, 3);
	ListPushFront(list, 4);
	ListPrint(list);

	ListPopFront(list);
	ListPrint(list);
	printf("%p\n",ListFind(list, 1));
	ListInsert(ListFind(list, 1),5);
	ListPrint(list);
	ListErase(ListFind(list, 5));
	ListPrint(list);
	list = NULL;
	return 0;
}

测试结果
测试结果

总结

由于带头双向循环列表特殊的结构,让我们可以快速的找到尾节点,和指定位置的前一个节点,虽然结构复杂,但是能带来性能上很大的提升。


文章中如有错误,欢迎请大佬们指出,我会立即更改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小吴cc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值