【数据结构与算法】(6):带头结点的双向循环链表

🤡博客主页:醉竺

🥰本文专栏:《数据结构与算法》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


 ✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨  


目录

一.双向循环链表

二.双向循环链表基础操作 

1.存储结构 

2.生成新结点 

3.初始化链表 

4.判断链表是否为空 

5.打印链表 

三.双向循环链表进阶操作

1.尾插法

2.头插法

3.尾删法

4.头删法 

5.查找结点 

6.在pos位置之前插入结点 

7.删除pos位置的结点 

8.改进后的尾插法和头插法 

9.改进后的尾删法和头删法 

10.链表的销毁 

四.归纳总结 


一.双向循环链表

我们说双向循环链表,它也被称为循环双链表。在学习双向循环链表之前先来看一一下双链表的结构:

虽然双链表可以很方便地找到某个结点前后的所有结点,但寻找效率却不一定高。比如已经拿到了双链表中最后一个节点的指针,那我们要如何快速寻找第 1 个节点呢?双循环链表可以很好的解决这个问题。 

在双链表的基础上,我们将链表中最后一个结点的后继指针由指向 nullptr 修改为指向头结点,将链表头结点的前趋指针由指向 nullptr 修改为指向最后一个结点,也就构成了双循环链表。 

为了让实现代码简单,依旧在链表中引入头结点。当双循环链表为空时,头结点的前趋指针和后继指针都指向自身,如图所示: 

空双循环链表头结点的后继和前趋指针都指向自身

当双循环链表不为空时,最后一个结点的后继指针指向头结点,头结点的前趋指针指向最后一 个结点,如图所示: 

带头结点的双循环链表数据存储描述图 

从上图可以看到,双循环链表的所有后继指针形成了一个环,所有前趋指针也形成了一个环 (一共两个环)。这样的话,给定一个数据结点,无论访问链表的后继结点还是前趋结点,都非常灵活和方便。 


二.双向循环链表基础操作 

这里先给出以下双向循环链表各种操作的接口函数,以便查看所实现的功能:

// 存储结构
typedef int  LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType data;
}ListNode;

// 双向循环链表生成新结点
ListNode* ListCreate(LTDataType x);

// 双向循环链表初始化
ListNode* ListInit();

// 双向循环链表销毁
void ListDestroy(ListNode* phead); 

// 双向循环链表打印
void ListPrint(ListNode* phead);

//判断循环链表是否为空
bool ListEmpty(ListNode* phead);

// 双向循环链表尾插
void ListPushBack(ListNode* phead,  LTDataType x);
// 双向循环链表头插
void ListPushFront(ListNode* phead,  LTDataType x);

// 双向循环链表尾删
void ListPopBack(ListNode* phead);
// 双向循环链表头删
void ListPopFront(ListNode* phead);

// 双向循环链表结点查找
ListNode* ListFind(ListNode* phead,  LTDataType x);

// 双向循环链表在pos位置前面插入结点
ListNode* ListInsert(ListNode* pos,  LTDataType x);
// 双向循环链表删除pos位置结点
void ListErase(ListNode* pos);

1.存储结构 

双向循环链表的存储结构内容有一个前驱指针prev用来指向该结点的前一个结点;一个后继指针next指向后后一个结点;以及本结点存储的数据data。 

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

2.生成新结点 

为了方便后续对双向循环链表的各种操作,这里把malloc新结点封装成一个函数。 

// 双向循环链表生成新结点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

3.初始化链表 

首先创建一个头结点,初始化双向循环链表的时候, 头结点的前驱指针和后继指针都要指向头结点,这样做有后续操作有很大的优势。例如:更方便判断链表是否为空;新结点头插到链表时的操作可以统一等。

// 双向循环链表初始化
ListNode* ListInit()
{
	ListNode* phead = ListCreate(-1);
	phead->next = phead;
 	phead->prev = phead;

	return phead;
}

4.判断链表是否为空 

由上图也知,当头结点的next指针指向本身时,链表是空的。

// 双向循环链表判断是否为空 
bool ListEmpty(ListNode* phead)
{
	assert(phead);
	
	return phead->next == phead;
}

5.打印链表 

从链表的第一个结点(头结点后面的一个结点)开始往后遍历,每遍历到一个结点打印该结点的data数值。这里有一个问题,遍历带头结点的双向循环链表的终止条件是什么? 

我们知道双向循环链表的特点是链表的最后一个结点的next指针是指向头结点的,遍历结束最后一个结点时再往后遍历就回到了头结点。所以cur指针指向头结点时,整个链表从第一个结点到最后一个结点都已经遍历一遍了。

// 双向循环链表打印
void ListPrint(ListNode* phead)
{
	assert(phead);

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

三.双向循环链表进阶操作

1.尾插法

// 双向循环链表尾插方式1:
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);
	ListNode* tail = phead->prev; //找到记录尾结点

	tail->next = newnode; //尾结点的next指针指向要插入的新结点
	newnode->prev = tail; //新结点的前驱指针指向尾结点
	newnode->next = phead; //新结点的后继指针指向头结点
	phead->prev = newnode; //头结点的前驱指针指向新结点

    // 最后形成了一个新的双向循环链表
}

2.头插法

头插法的两种方式,基本原理是一样的。一种没有记录第一个结点,操作过程中顺序不可以改变,否则头插就会失败。另外一种用first指针记录第一个结点,这样顺序随意更自由。

第一种: 

// 双向循环链表头插方式1:step1和step2顺序不能变
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);

	//step1:
	newnode->next = phead->next;
	phead->next->prev = newnode;
	//step2:
	phead->next = newnode;
	newnode->prev = phead;
}

第二种: 

// 双向循环链表头插方式 2:step1和step2顺序随意
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);
	ListNode* first = phead->next; //用指针first记录首元结点,即链表的第一个有效结点

	//step1:
	phead->next = newnode;
	newnode->prev = phead;
	//step2:
	newnode->next = first;
	first->prev = newnode;
}

3.尾删法

// 双向循环链表尾删方式1:
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

4.头删法 

// 双向循环链表头删方式1:
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

	//ListNode* first = phead->next;
	
	//phead->next = first->next;
	//phead->next->prev = phead;
	//free(first);

	//phead->next = first->next;
	//first->next->prev = phead;
	//free(first);

	ListNode* first = phead->next;
	ListNode* second = first->next;

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

5.查找结点 

遍历链表,找到data为X的结点,找到的话返回这个结点的地址,找不到返回NULL。 

// 双向循环链表结点查找
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.在pos位置之前插入结点 

// 双向循环链表在pos位置之前插入结点
ListNode* ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	
	//方式1:
	/*ListNode* newnode = ListCreate(x);
	
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	newnode->next = pos;
	pos->prev = newnode;*/

	//方式2:
	ListNode* newnode = ListCreate(x);
	ListNode* posPrev = pos->prev;

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

7.删除pos位置的结点 

// 双向循环链表删除pos位置的结点
void ListErase(ListNode* pos)
{
	assert(pos);

	//方式1:
	/*
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	*/
	//方式2:
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;

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

8.改进后的尾插法和头插法 

由方法6在pos位置之前插入结点 ,可以改进尾插法和头插法。

尾插法:是在最后一个结点后面位置插入新的结点,就相当于头结点位置的前一个,所以pos相当于头结点。 

头插法:就相当于在第一个结点位置前插入新的结点,所以pos相当于第一个结点。 


尾插法: 

// 双向循环链表尾插方式2:
void ListPushBack(ListNode* phead, LTDataType x)
{
	ListInsert(phead, x);
}

 头插法:

// 双向循环链表头插方式3:
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

9.改进后的尾删法和头删法 

由方法7删除pos位置的结点 ,可以改进尾删法和头删法。

尾删法:就是删除最后一个结点位置的结点,就相当于头结点前一个位置的结点,所以pos相当于头结点前一个。 

头删法:就是删除在第一个结点位置的结点,所以pos相当于第一个结点。


尾删法:

// 双向循环链表尾删方式2:
void ListPopBack(ListNode* phead)
{
	assert(phead);
	
	ListEras(phead->prev);
}

头删法:

// 双向循环链表头删方式2:
void ListPopFront(ListNode* phead)
{
	assert(phead);

	ListErase(phead->next);
}

10.链表的销毁 

// 双向循环链表销毁
void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;

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

四.归纳总结 

这里提供所有代码,可以直接运行。创建三个文件,分别是List.h,List.c,Test.c。

List.h:

#define _CRT_SECURE_NO_WARNINGS 1

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

// 2、带头+双向+循环链表增删查改实现

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

//  双向循环链表生成新结点
ListNode* ListCreate(LTDataType x);

// 双向循环链表初始化
ListNode* ListInit();

// 双向循环链表销毁
void ListDestroy(ListNode* phead); 

// 双向循环链表打印
void ListPrint(ListNode* phead);

//判断循环链表是否为空
bool ListEmpty(ListNode* phead);

// 双向循环链表尾插
void ListPushBack(ListNode* phead,  LTDataType x);
// 双向循环链表头插
void ListPushFront(ListNode* phead,  LTDataType x);

// 双向循环链表尾删
void ListPopBack(ListNode* phead);
// 双向循环链表头删
void ListPopFront(ListNode* phead);

// 双向循环链表结点查找
ListNode* ListFind(ListNode* phead,  LTDataType x);

// 双向循环链表在pos位置前面插入结点
ListNode* ListInsert(ListNode* pos,  LTDataType x);
// 双向循环链表删除pos位置结点
void ListErase(ListNode* pos);

List.c:

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

// 双向循环链表生成新结点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

// 双向循环链表初始化
ListNode* ListInit()
{
	ListNode* phead = ListCreate(-1);
	phead->next = phead;
 	phead->prev = phead;

	return phead;
}

// 双向循环链表打印
void ListPrint(ListNode* phead)
{
	assert(phead);

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

// 双向循环链表判断是否为空 
bool ListEmpty(ListNode* phead)
{
	assert(phead);
	
	return phead->next == phead;
}

// 双向循环链表尾插方式1:
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);
	ListNode* tail = phead->prev;

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

// 双向循环链表尾插方式2:
//void ListPushBack(ListNode* phead, LTDataType x)
//{
//	ListInsert(phead, x);
//}

// 双向循环链表头插方式1:step1和step2顺序不能变
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);

	//step1:
	newnode->next = phead->next;
	phead->next->prev = newnode;
	//step2:
	phead->next = newnode;
	newnode->prev = phead;
}

// 双向循环链表头插方式2:step1和step2顺序随意
//void ListPushFront(ListNode* phead, LTDataType x)
//{
//	assert(phead);
//	ListNode* newnode = ListCreate(x);
//	ListNode* first = phead->next; //用指针first记录首元结点,即链表的第一个有效结点
//
//	//step1:
//	phead->next = newnode;
//	newnode->prev = phead;
//	//step2:
//	newnode->next = first;
//	first->prev = newnode;
//}

// 双向循环链表头插方式3:
//void ListPushFront(ListNode* phead, LTDataType x)
//{
//	assert(phead);
//	ListInsert(phead->next, x);
//}



// 双向循环链表尾删方式1:
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

// 双向循环链表尾删方式2:
//void ListPopBack(ListNode* phead)
//{
//	assert(phead);
//	
//	ListEras(phead->prev);
//}

// 双向循环链表头删方式1:
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

	//ListNode* first = phead->next;
	
	//phead->next = first->next;
	//phead->next->prev = phead;
	//free(first);

	//phead->next = first->next;
	//first->next->prev = phead;
	//free(first);

	ListNode* first = phead->next;
	ListNode* second = first->next;

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

// 双向循环链表头删方式2:
//void ListPopFront(ListNode* phead)
//{
//	assert(phead);
//
//	ListErase(phead->next);
//}


// 双向循环链表结点查找
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位置之前插入结点
ListNode* ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	
	//方式1:
	/*ListNode* newnode = ListCreate(x);
	
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	newnode->next = pos;
	pos->prev = newnode;*/

	//方式2:
	ListNode* newnode = ListCreate(x);
	ListNode* posPrev = pos->prev;

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

// 双向循环链表删除pos位置的结点
void ListErase(ListNode* pos)
{
	assert(pos);

	//方式1:
	/*
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	*/
	//方式2:
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;

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

// 双向循环链表销毁
void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;

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

Test.c:

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

void TestList()
{
	ListNode* plist = ListInit();
	printf("尾插1,2:\n");
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPrint(plist);

	printf("头插3,4,5:\n");
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);
	ListPrint(plist);

	printf("尾删一个元素:\n");
	ListPopBack(plist);
	ListPrint(plist);

	printf("头删一个元素:\n");
	ListPopFront(plist);
	ListPrint(plist);

	int i = 0;
	printf("请输入你要查找的结点:\n");
	scanf("%d", &i);
	ListNode* pos = ListFind(plist, i);
	if (pos)
	{
		printf("查找data为%d的结点并打印其data:\n", i);
		printf("%d\n", pos->data);
	}
	else
	{
		printf("没有data为%d的结点\n",i);
	}

	printf("请输入两个结点,一是插入的位置前面,二是插入的结点:\n");
	int k = 0, j = 0;
	scanf("%d%d", &k, &j);
	pos = ListFind(plist, k);
	if (pos)
	{
		ListInsert(pos, j);
		printf("在data为%d的结点前插入一个值为%d的结点后的链表:\n", k, j);
		ListPrint(plist);
	}
	else
	{
		printf("没有data为%d的结点,插入失败\n",k);
	}
	
	printf("请输入你要删除的结点:\n");
	int m = 0;
	scanf("%d", &m);
	pos = ListFind(plist, m);
	if (pos)
	{
		ListErase(pos);
		printf("删除结点%d后的链表:\n", m);
		ListPrint(plist);
	}
	else
	{
		printf("没有data为%d的结点,删除失败\n",m);
	}

	ListDestroy(plist);
	plist = NULL;
}
 
int main()
{
	TestList();
	return 0;
}
  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

醉竺

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

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

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

打赏作者

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

抵扣说明:

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

余额充值