外强中干——双向带头循环链表

前言:众所周知,链表有八种结构,由单向或双向,有头或无头,循环或不循环构成。在本篇,将介绍8种链表结构中最复杂的——双向带头循环链表。听着名字或许挺唬人的,但实际上双向带头循环链表实现起来比结构最简单的单向不带头不循环链表简单的多,是个“外强中干”的链表。这里只是说它名字唬人哦,实际上它是最优的链表结构,可以达到再任意位置插入,删除数据复杂度都是O(1)。(文末附有完整代码)

首先用你喜欢的IDE创建一个头文件和两个源文件,不同的文件具有以下不同的功能:

文件作用
list.h接口函数的声明
list.c接口函数的实现——链表的主体,文章的核心内容
test.c测试链表的运行逻辑
在这里插入图片描述下文将实现的11个链表接口
在这里插入图片描述双向循环链表图

接口

0.开始前的准备

头文件的包含;结构体的创建;.c文件引用.h文件

list.h

#include<stdio.h>
#include<assert.h>  //assert断言所需头文件
#include<stdlib.h>  //malloc动态开辟内存空间所需头文件
#include<stdbool.h> //在c语言中使用bool类型所需头文件,下文实现的ListEmpty函数用到

typedef int LTDataType; //因不知链表中存储的是什么数据类型,所以用重命名,要修改数据类型时直接将int换成其它的即可
typedef struct ListNode  
{
	LTDataType data;
	struct ListNode* next;  //指向后一个结点的指针
	struct ListNode* prev;  //指向前一个结点的指针
}ListNode;      //将结构体struct ListNode 重命名为 ListNode,方便操作

list.c

#include"list.h"  //引用头文件

test.c

#include"list.h"  //引用头文件

1.创建返回链表的头结点

这里的头结点就是我们常说的哨兵位,需要注意的是,哨兵位中不存储数据(即不给data赋值)。

list.h

//1.创建返回链表的头结点.
ListNode* ListInit();  //在头文件中声明

list.c

//要创建一个链表的结点,就需要开辟一个结构体,因此在.c文件中创建一个开辟新结点的函数,
//在下文中需要创建新结点时直接引用此函数即可
ListNode* ListCreate(LTDataType x) 
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode)); //malloc一个结构体
	if (newnode == NULL) //此处if判断malloc是否成功,常规情况下都会成功,毕竟失败了咱们链表不就创建失败了吗?
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x; //给数据赋值
	newnode->next = NULL;  //next指针置空
	newnode->prev = NULL;  //prev指针置空
	return newnode;
}

//1.创建返回链表的头结点
ListNode* ListInit()
{
	ListNode* pHead = ListCreate(-1); //这里的头结点随便给一个值,开头时提过头结点不存储数据,
//这样不是前后矛盾吗?不用担心,后文中的打印函数实现时并不会打印头结点。
	pHead->next = pHead;  //因为链表只有一个头结点,所以前后指针都指向自己
	pHead->prev = pHead;
	return pHead;
}

这里是引用

test.c

int main()
{
	ListNode* plist = ListInit();  
	return 0;
}

2.链表销毁

将每个创建的结点通过free函数释放,需要注意的是,因为传参我们传的是一级指针,所以需要在使用完销毁函数后手动置空。

list.h

//2.链表销毁
void ListDestory(ListNode* pHead);

list.c

//2.链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);  //断言一下pHead是否传有效数据进来
	ListNode* cur = pHead->next; //存储头结点的后一个结点
	while (cur != pHead)  //从头结点的后一个结点遍历到头结点停止循环
	{
		ListNode* next = cur->next;  //存储后一个结点
		free(cur);  //释放当前结点
		cur = next; //将cur指向后一个结点,使循环继续
	}
	free(pHead);
	//pHead = NULL; 置空无效,因为形参的改变不影响实参,在主函数中置空。
}

test.c

#include"list.h"
int main()
{
	ListNode* plist = ListInit();
	ListDestory(plist);
	plist = NULL;  //手动置空
	return 0;
}

3.链表打印

从头到尾遍历打印,注意不要打印头结点(哨兵位)

list.h

//3.链表打印
void ListPrint(ListNode* pHead);

list.c


//3.链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	//打印头结点
	printf("guard<==>"); //为了打印的美观,哨兵位这样表示
	ListNode* cur = pHead->next; //存储头结点的后一个结点
	while (cur != pHead)  //从头结点的后一个结点遍历到头结点停止循环
	{
		printf("%d<==>", cur->data);  //打印输出
		cur = cur->next;  //将cur指向后一个结点,使循环继续
	}
}

test.c

#include"list.h"
int main()
{
	ListNode* plist = ListInit();
	ListPrint(plist);
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时链表中只有头结点,显示台窗口如下:
在这里插入图片描述

4.链表尾插

链表中的重要接口之一,在单链表中,进行尾插操作的话需要从头找到尾结点,而在双向链表中,头结点的前一个结点就是尾结点(pHead->prev)。从这里就可以看出双向带头循环链表实现起来的简单之处。

list.h

//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);

list.c

//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x) //插入拥有两个参数,第二个参数为你要插入的数值
{
	assert(pHead); //断言
	ListNode* newnode = ListCreate(x); //插入需要创建新结点,这里我们开头第1部分创建的ListCreate函数就有了用武之地。
	struct ListNode* tail = pHead->prev; //存储原来的尾结点,注意这个结点的存储会让你的尾插实现写的很舒服
	newnode->next = pHead;  //实现链接
	newnode->prev = tail;   
	tail->next = newnode;   
	pHead->prev = newnode;  //注意这里的pHead->prev不要再用tail代替了,因为改变tail并不会改变pHead->prev。
}

在这里插入图片描述

以上图为例,进行尾插操作的话,需要将新结点与d3,head链接起来,同时断开head和原尾结点d3。
上面的代码使用tail结构体指针存储了pHead->prev,这样的话下面实现链接的4行代码就可以是任意顺序,若是没有tail的话,必须在结尾改变pHead->prev,因为前面几行代码需要用到pHead->prev,若是改了它,则代码不能实现。

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 4);  //尾插4
	ListPushBack(plist, 3);  //尾插3
	ListPushBack(plist, 2);  //尾插2
	ListPushBack(plist, 1);  //尾插1
	ListPrint(plist);
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:
在这里插入图片描述

5.链表判空

很简单的一个接口,返回一个bool类型的值。因在链表尾删中需要用到,所以将其放在尾删前

list.h

//5.链表判空
bool ListEmpty(ListNode* pHead);

list.c

bool ListEmpty(ListNode* pHead)
{
	assert(pHead); //断言
	return pHead->next == pHead; // 若是头结点的后一个结点还是头结点,说明链表中只有头结点,
	//而头结点不存放有效数据,因此只有头结点的链表就是空链表。 
	//该语句为真,则返回true,若为假,则返回false
}

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	if (ListEmpty(plist))
	{
		printf("链表为空\n");
	}
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:
在这里插入图片描述

6.链表尾删

链表中的重要接口之一,双向链表找尾很方便,即pHead->prev。删除操作比起插入操作实现起来更简单。

list.h

//6.链表尾删
void ListPopBack(ListNode* pHead);

list.c

//6.链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);  //断言
	assert(!ListEmpty(pHead));
	ListNode* tail = pHead->prev;
	tail->prev->next = pHead;
	pHead->prev = tail->prev;
	free(tail);
	tail = NULL;
}

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListPrint(plist);  //第一行打印
	ListPopBack(plist);
	printf("\n");
	ListPrint(plist);  //第二行打印
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:尾删了一个0
在这里插入图片描述

7.链表头插

list.h

//7.链表头插
void ListPushFront(ListNode* pHead, LTDataType x);

list.c

//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead); //断言
	ListNode* newnode = ListCreate(x);  //创建新结点
	ListNode* head = pHead->next;   //存放头结点的后一个结点
	newnode->next = head;
	head->prev = newnode;
	pHead->next = newnode;
	newnode->prev = pHead;
}

这里是引用

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListPushFront(plist, 3);
	ListPrint(plist);  //第二行打印
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:头插一个3
在这里插入图片描述

8.链表头删

跟头删一样,删除操作都要进行判空,若是空链表,则不能再进行删除操作。

list.h


list.c

//链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));  
	ListNode* head = pHead->next;
	head->next->prev = pHead;
	pHead->next = head->next;
	free(head);
	head = NULL;
}

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListPrint(plist);  //第一行打印
	printf("\n");
	ListPopFront(plist); //头删
	ListPrint(plist);  //第二行打印
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:头删一个2
在这里插入图片描述

9.链表查找

查找链表中是否存在指定的数,若存在,则返回这个结点,若不存在,则返回NULL。

list.h

//9.链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);

list.c

//9.链表查找
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
}

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListNode* pplist = ListFind(plist,2);
	if (pplist)
	{
		printf("链表中包含2");
	}
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:查找链表中有没有2

在这里插入图片描述

10.在链表pos位置前插入

list.h

//10.在链表pos位置前插入
void ListInsert(ListNode* pos, LTDataType x);

list.c

//10.在链表pos位置前插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos); //断言
	ListNode* newnode = ListCreate(x); //创建新结点
	newnode->next = pos;  
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	pos->prev = newnode;
}

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListNode* pos = ListFind(plist,2);
	if (pos)
	{
		ListInsert(pos, 3);
	}
	ListPrint(plist);
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:在2的前面插入3
在这里插入图片描述

11.删除链表pos位置的结点

list.h

//11.删除链表pos位置的结点
void ListErase(ListNode* pos);

list.c

//11.删除链表pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos); //断言
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

在这里插入图片描述

test.c

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListPrint(plist);  //第一行打印
	printf("\n");
	ListNode* pos = ListFind(plist,2);
	if (pos)
	{
		ListErase(pos);  //删除2
	}
	ListPrint(plist);  //第二行打印
	ListDestory(plist);
	plist = NULL;
	return 0;
}

此时显示台窗口如下:查找链表中是否有2,若有,则删除2(仅删除一个)
在这里插入图片描述

至此,双向带头循环链表的11个接口就写完了。怎么样,是不是感觉实现起来非常简单,说它"外强中干"丝毫为过吧。

完整代码

list.h

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

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//1.创建返回链表的头结点.
ListNode* ListInit();
//2.链表销毁
void ListDestory(ListNode* pHead);
//3.链表打印
void ListPrint(ListNode* pHead);
//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
//5.链表判空
bool ListEmpty(ListNode* pHead);
//6.链表尾删
void ListPopBack(ListNode* pHead);
//7.链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
//8.链表头删
void ListPopFront(ListNode* pHead);
//9.链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
//10.链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
//11.链表删除pos位置的结点
void ListErase(ListNode* pos);

list.c 核心代码——接口的实现

#include"list.h"  //引用头文件

//要创建一个链表的结点,就需要开辟一个结构体,因此在.c文件中创建一个开辟新结点的函数,
//在下文中需要创建新结点时直接引用此函数即可
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode)); //malloc一个结构体
	if (newnode == NULL) //此处if判断malloc是否成功,常规情况下都会成功,毕竟失败了咱们链表不就创建失败了吗?
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x; //给数据赋值
	newnode->next = NULL;  //next指针置空
	newnode->prev = NULL;  //prev指针置空
	return newnode;
}

//1.创建返回链表的头结点
ListNode* ListInit()
{
	ListNode* pHead = ListCreate(-1); //这里的头结点随便给一个值,开头时提过头结点不存储数据,
	//这样不是前后矛盾吗?不用担心,后文中的打印函数实现时并不会打印头结点。
	pHead->next = pHead;  //因为链表只有一个头结点,所以前后指针都指向自己
	pHead->prev = pHead;
	return pHead;
}

//2.链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next; //存储头结点的后一个结点
	while (cur != pHead)  //从头结点的后一个结点遍历到头结点停止循环
	{
		ListNode* next = cur->next;  //存储后一个结点
		free(cur);  //释放当前结点
		cur = next; //将cur指向后一个结点
	}
	free(pHead);
	//pHead = NULL; 置空无效,因为形参的改变不影响实参,在主函数中置空。
}

//3.链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	printf("guard<==>"); //为了打印的美观,哨兵位这样表示
	ListNode* cur = pHead->next; //存储头结点的后一个结点
	while (cur != pHead)  //从头结点的后一个结点遍历到头结点停止循环
	{
		printf("%d<==>", cur->data);  //打印输出
		cur = cur->next;  //将cur指向后一个结点,使循环继续
	}
}

//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x) //插入拥有两个参数,第二个参数为你要插入的数值
{
	assert(pHead); //断言
	ListNode* newnode = ListCreate(x); //插入需要创建新结点,这里我们开头第1部分创建的ListCreate函数就有了用武之地。
	struct ListNode* tail = pHead->prev;  //存储原来的尾结点
	newnode->next = pHead;  //实现链接
	tail->next = newnode;
	pHead->prev = newnode;  //注意这里的pHead->prev不要再用tail代替了,因为改变tail并不会改变pHead->prev。
	newnode->prev = tail;
	
}

//5.链表判空
bool ListEmpty(ListNode* pHead)
{
	assert(pHead);
	return pHead->next == pHead; //该语句为真,则返回true,若为假,则返回false
}

//6.链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);  //断言
	assert(!ListEmpty(pHead));
	ListNode* tail = pHead->prev;
	tail->prev->next = pHead;
	pHead->prev = tail->prev;
	free(tail);
	tail = NULL;
}

//7.链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead); //断言
	ListNode* newnode = ListCreate(x);  //创建新结点
	ListNode* head = pHead->next;   //存放头结点的后一个结点
	newnode->next = head;
	head->prev = newnode;
	pHead->next = newnode;
	newnode->prev = pHead;
}

//8.链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));
	ListNode* head = pHead->next;
	head->next->prev = pHead;
	pHead->next = head->next;
	free(head);
	head = NULL;
}

//9.链表查找
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
}

//10.在链表pos位置前插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos); //断言
	ListNode* newnode = ListCreate(x); //创建新结点
	newnode->next = pos;
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	pos->prev = newnode;
}

//11.删除链表pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos); //断言
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

test.c 该文件随大家喜欢测试

#include"list.h"  //引用头文件
int main()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 2);
	ListPushBack(plist, 1);
	ListPushBack(plist, 0);
	ListPrint(plist);  //第一行打印
	printf("\n");
	ListNode* pos = ListFind(plist,2);
	if (pos)
	{
		ListErase(pos);  //删除2
	}
	ListPrint(plist);  //第二行打印
	ListDestory(plist);
	plist = NULL;
	return 0;
}

文末BB:对哪里有问题的朋友,可以在评论区留言,若哪里写的有问题,也欢迎朋友们在评论区指出,博主看到后会第一时间确定修改。最后,制作不易,如果对朋友们有帮助的话,希望能给点点赞和关注.
在这里插入图片描述

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

溪读卖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值