带头双向循环链表的实现-增删查改

目录

链表的8种结构

1.链表的分类

2.链表8种结构

顺序表、不带头不循环单链表、带头双向循环链表等各种数据结构的创建和初始化的比较

一、1个数据结构的初始化取决于这个数据结构最初的状态是什么

二、数据结构——顺序表的初始化

1.顺序表的创建和初始化的思路

2.利用顺序表的初始化函数SLInit把顺序表sl设置为空顺序表的原理

3.顺序表sl的创建和把顺序表sl的初始化为空顺序表的代码:(注意想要创建顺序表的话必须要声明结构体类型)

三、数据结构——不带哨兵位头结点不循环的单链表的初始化

1. 不带哨兵位头结点不循环的单链表的创键和初始化的思路:(为什么不带哨兵位头结点不循环的单链表不需要初始化函数的原因)

2.不带哨兵位头结点不循环的单链表的的创建和把不带哨兵位头结点不循环的单链表初始化为空链表的代码

四、数据结构——带哨兵位头结点循环的双向链表的初始化

4.1知识点

4.2带哨兵位头结点循环的双向链表的创建和初始化的思路

带头循环双向链表的整个工程代码

一、List.h头文件

二、List.c源文件

三、test.c源文件(测试源文件)

对带头循环双向链表的各个功能函数进行解析

一、对尾插函数LTPushBack进行解析

1.带头循环双向链表尾插的两种情况:

情况1:要尾插的带头循环双向链表的有效结点数为0。

情况2: 要尾插的带头循环双向链表的有效结点数至少有1个。

2.代码:

2.1.对assert(phead)代码进行解析

//以下是对带头循环双向链表的尾插函数的写法一进行解析:

2.2对LTNode* newnode = BuyListNode(x)进行解析

2.3对LTNode* tail = phead->prev代码进行解析

2.4.以下是把新创建的结点和哨兵位头结点链接起来和新创建的结点和尾结点链接起来的过程。

(1)以下是新创建的结点和尾结点链接起来的过程

(2)以下是新创建的结点和哨兵位头结点链接起来的过程

尾插函数写法1的小结

//以下是对带头循环双向链表的尾插函数的写法二进行解析:

2.5对LTInsert(phead,x)函数进行解析

二、对尾删函数LTPopBack代码进行解析

1.带头循环双向链表尾删的两种情况

情况1:要尾删的带头循环双向链表的有效结点数为1。

情况2: 要尾删的带头循环双向链表的有效结点数至少有2个。

2.代码

2.1.注意

三、对头插函数LTPushFront代码进行解析

1.带头循环双向链表头插的两种情况:

情况1:要头插的带头循环双向链表的有效结点数为0。

情况2: 要头插的带头循环双向链表的有效结点数至少有1个。

2.代码:

四、对头删函数进行解析

1.带头循环双向链表头删的两种情况

2.代码:

带头双向循环链表的总结

1.带头循环双向链表可以正着遍历和倒着遍历

(1)正着遍历的方式

(2)倒着走遍历的方式

2.如何在10min之内写一个链表

3.为什么带头循环双链表没有提供在链表pos位置之后插入一个结点的函数,而不带头不循环单链表却提供了在链表pos位置之后插入一个结点的函数的原因:

4.对利用LTEmpty函数判断指针phead指向的双向链表是否是空链表的补充:

5.为什么哨兵位头结点中的成员变量data不能用来存放链表的有效长度的原因(假设链表的有效长度用size表示)

6.


链表的8种结构

1.链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
(1)单向或者双向

(2) 带头或者不带头

(3)循环或者非循环

(4)虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

① 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
②带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。

③在实际当中一般很少用循环链表,大多数用的是不循环链表。由于带哨兵位头结点的优势是尾插,所以一般再对链表进行尾插之前把链表变成带哨兵位头结点的链表进行尾插会使得尾插非常方便,因为当链表只有一个哨兵位头结点进行尾插时是不需要考虑头插进而改变头指针的值的(这里可以对比带头双向循环链表和不带头不循环链表的尾插得出带头双向循环链表方便尾插)

2.链表8种结构

(1)单链表有4种结构:

单链表是带哨兵位头结点的循环链表、单链表是带哨兵位头结点的不循环链表、

单链表是不带哨兵位头结点的循环链表、单链表是不带哨兵位头结点的不循环链表

(2)双链表有4种结构:

双链表是带哨兵位头结点的循环链表、双链表是带哨兵位头结点的不循环链表、

双链表是不带哨兵位头结点的循环链表、双链表是不带哨兵位头结点的不循环链表

顺序表、不带头不循环单链表、带头双向循环链表等各种数据结构的创建和初始化的比较

一、1个数据结构的初始化取决于这个数据结构最初的状态是什么

1.顺序表的最初的状态是空顺序表——这个最初状态导致顺序表应初始化为空顺序表。

2.不带头不循环单链表的最初的状态是个空链表——这个最初状态导致不带头不循环单链表应初始化为空链表。

3.带头双向循环链表的最初的状态是个只有一个哨兵位头结点的带头双向循环链表——这个最初状态导致带头双向循环链表初始化为只有一个结点的带头双向循环链表而且这个结点还是哨兵位头结点。

二、数据结构——顺序表的初始化

(注意:①顺序表本质是个结构体,而且这个结构体是不存放顺序表的数据data的,但是这个结构体却存放顺序表数据data所在堆区中的地址a;②这里的顺序表sl表示结构体变量sl)

1.顺序表的创建和初始化的思路

由于顺序表的本质是个结构体,而且顺序表最初的状态是个空顺序表同时也可以认为顺序表最初的状态是个结构体,所以一开始可以直接定义1个结构体变量sl表示1个顺序表而且不用对结构体变量sl进行初始化(注意:变量sl的类型为结构体SL;这里把变量s简称为顺序表sl),然后利用顺序表的初始化函数SLInit把顺序表sl设置为空顺序表。

2.利用顺序表的初始化函数SLInit把顺序表sl设置为空顺序表的原理

不管顺序表sl中的成员变量指针SLDatyp*a和成员变量int capacity的值是多少,只要一开始顺序表sl中的成员变量int size的值是0即size = 0就可以说明结构体变量sl是个空顺序表,所以要想利用顺序表初始化函数SLInit把顺序表sl设置为空顺序表的话,则必须在SLInit函数的内部把顺序表sl中的成员变量int size的值设置0来表示顺序表sl是个空链表。

总的来说,由于顺序表sl是个空顺序表,所以一般SLInit函数把顺序表sl的成员变量指针a设置为NULL,把成员变量capacity设置为0。

小结:总的来说,不管结构体类型SL中的成员变量指针a和成员变量capacity的值是多少,只要保证结构体类型SL中的成员变量size的值为0就可以说明这个结构体变量sl是个空顺序表。

注意:

(1)这个顺序表sl是不存放顺序表sl的数据data的,因为这个顺序表sl是存放在栈区的,而顺序表sl的数据data是存放在堆区的,而且顺序表sl只是存放了顺序表sl的数据data所在堆区中的地址a。

(2)不管顺序表sl中的成员变量有没有被顺序表的初始化函数SLInit进行初始化即不管有没有让结构体变量sl中的成员变量指针a指向顺序表数据data所在的动态空间,只要结构体变量sl被定义了就说明了结构体变量sl是个顺序表。

(3)对顺序表sl进行初始化为空顺序表的两种方式:

方式1:size = 0,capacity = 0,a = NULL。

图形解析:

方式2:size = 0,capacity != 0,a != NULL。

图形解析:

3.顺序表sl的创建和把顺序表sl的初始化为空顺序表的代码:(注意想要创建顺序表的话必须要声明结构体类型)

三、数据结构——不带哨兵位头结点不循环的单链表的初始化

注意:顺序表最初的状态是空顺序表,而要想把顺序表初始化为空顺序表的话,则必须要先定义一个结构体并用这个定义的结构体创建一个顺序表,而要想把刚创建好的顺序表初始化为空顺序表的话则必须把结构体中的size设置0。总的来说,有结构体,size为0,则顺序表就是空链表

1. 不带哨兵位头结点不循环的单链表的创键和初始化的思路:(为什么不带哨兵位头结点不循环的单链表不需要初始化函数的原因)

由于不带哨兵位头结点不循环的单链表的最初状态是个空链表而且把不带哨兵位头结点不循环的单链表设置为空链表的方式很简单,所以不需要创建一个初始化函数对不带哨兵位头结点不循环的单链表进行初始化,而把不带哨兵位头结点不循环的单链表设置为空链表的方式是:想要把主调函数中的不带哨兵位头结点不循环的单链表初始化为空链表的话,只需在主调函数中把指向不带哨兵位头结点不循环的单链表头结点的指针plist指向为NULL即让指针plist = NULL即可。总的来说,想要单链表为空链表只需有一个单链表头结点的指针指向NULL即可。

2.不带哨兵位头结点不循环的单链表的的创建和把不带哨兵位头结点不循环的单链表初始化为空链表的代码

四、数据结构——带哨兵位头结点循环的双向链表的初始化

4.1知识点

注意:下面是带头双向循环结点的结构体类型

1.循环单链表中的循环指的是链表的尾结点中的成员变量next存放链表头结点的地址。

图形解析:

2.循环双向链表中的循环指的是链表头结点中的成员变量prev存放链表尾结点的地址而

链表尾结点中的成员变量next存放链表头结点的地址。循环双向链表中的双向指的是每个结点都有两个指针,其中一个指针prev存放当前结点位置的上一个结点的地址,其中一个指针next存放当前结点位置的下一个结点的地址。

图形解析:

3.不循环双向链表的特点:

(1) 不循环双向链表中的双向指的是每个结点都有两个指针。而在头结点和尾结点之间的所以结点中的两个指针的作用是其中一个指针prev存放当前结点位置的上一个结点的地址,其中一个指针next存放当前结点位置的下一个结点的地址。

(2) 不循环双向链表的头结点的成员变量指针prev的值为NULL而成员变量指针next存放的是头结点位置的下一个结点的地址。

(3) 不循环双向链表的尾结点的成员变量指针next的值为NULL而成员变量指针prev存放的是尾结点位置的上一个结点的地址。

图形解析:

4.带哨兵位头结点循环的双向链表的特点(注意:链表总结点数至少大于等于1的链表)

(1)链表头结点的成员变量指针prev存放链表尾结点的地址而成员变量指针next存放链表当前结点位置的下一个结点的地址。

(2)链表尾结点的成员变量prev存放链表当前结点位置的上一个结点的地址而成员变量next存放链表头结点的地址。

(3)在链表头结点和尾结点之间的每个结点中的成员变量指针prev存放的是链表当前结点位置的上一个结点的地址而成员变量指针next存放的是链表当前结点位置的下一个结点的地址

图形解析:

5. 带哨兵位头结点循环的双向链表而且链表只有一个结点的链表,而且这个唯一的结点还是哨兵位头结点的特点

由于这个带哨兵位头结点循环的双向链表只有一个结点而且这个结点还是哨兵位头结点,所以这个链表会认为哨兵位头结点既是链表的头结点也是链表的尾结点,由于循环双向链表中的循环特性使得这个链表的哨兵位头结点中的成员变量指针prev存放哨兵位头结点的地址,而这个链表的哨兵位头结点中的成员变量指针next存放哨兵位头结点的地址。总的来说,这个链表的哨兵位头结点中的成员变量指针prev和成员变量指针next存放的都是哨兵位头结点自己的地址即让指针prev和指针next都指向自己。

图形解析:

4.2带哨兵位头结点循环的双向链表的创建和初始化的思路

由于带哨兵位头结点循环的双向链表最初的状态是只带一个结点的双向循环链表而且这个结点还是哨兵位头结点。而把创建的带哨兵位头结点循环的双向链表初始化为只带一个结点的双向循环链表而且这个结点还是哨兵位头结点的方式:

带头循环双向链表的整个工程代码

一、List.h头文件

List.h

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

//typedef char LTDataType;
//typedef double LTDataType;

//链表存储的数据类型
typedef int LTDataType;

//带头双向循环链表结点的结构体类型
typedef struct ListNode
{
	struct ListNode* next;//指针next指向当前结点的下一个结点
	struct ListNode* prev;//指针prev指向当前结点的上一个结点
	LTDataType data;//链表存储的数据
}LTNode;

//创建结点
LTNode* BuyListNode(LTDataType x);

//初始化函数
LTNode* LTInit();

//打印函数
void LTPrint(LTNode* phead);

//尾插函数
void LTPushBack(LTNode* phead, LTDataType x);

//尾删函数
void LTPopBack(LTNode* phead);

//头插函数
void LTPushFront(LTNode* phead, LTDataType x);

//头删函数
void LTPopFront(LTNode* phead);

//查找函数
LTNode* LTFind(LTNode* phead, LTDataType x);

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

// 删除pos位置
void LTErase(LTNode* pos);

//判断带头双向循环链表是否为空链表(注意:这里的空链表指的是链表有效结点的个数为0)
bool LTEmpty(LTNode* phead);

//判断带头双向循环链表的有效结点个数(注意:有效结点指的是存储数据的结点,带头双向循环链表的哨兵位头结点是不存储数据的)
size_t LTSize(LTNode* phead);

//带头双向循环链表的销毁函数(注意:这个销毁函数包括销毁带头双向循环链表的哨兵位头结点,所以在使用该函数后一定要把主调函数中指向带头双向循环链表的哨兵位头结点指针设置为空指针NULL)
void LTDestroy(LTNode* phead);

二、List.c源文件

List.c

#include "List.h"

//创建结点
LTNode* BuyListNode(LTDataType x)
{
	//创建结点
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//初始化
	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

//初始化函数
LTNode* LTInit()
{
	//创建哨兵位头结点
	LTNode* phead = BuyListNode(-1);
	//初始化
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

//打印函数
void LTPrint(LTNode* phead)
{
	//由于指针phead指向的带头循环双向链表一定不为空链表(因为带头双向循环链表至少是带有哨兵位头结点这一个结点的),
	//所以一定要判断指针phead指向的链表是否为空链表,若是为空链表则说明传参不小心传了个空指针。
	assert(phead);
	//由于哨兵位头结点是不存储链表的数据的,所以cur要从链表第一个有效结点的位置phead->next开始遍历链表。
	LTNode* cur = phead->next;//我们一般在遍历链表时是不会用头结点指针phead来遍历链表的,一般是创建临时指针变量cur来遍历链表,这样做的目的是要有一个指针始终指向链表的头结点以便我们可以迅速找到整个链表。
	while (cur != phead)//当cur = phead时说明此时cur指向链表的哨兵位头结点位置,则此时我们会结束遍历链表。
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//尾插函数
void LTPushBack(LTNode* phead, LTDataType x)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);

	//尾插函数写法1:

	创建结点
	//LTNode* newnode = BuyListNode(x);

	找尾结点
	//LTNode* tail = phead->prev;

	链接
	新创建的结点与尾结点链接
	//tail->next = newnode;
	//newnode->prev = tail;
	新创建的结点与头结点链接
	//phead->prev = newnode;
	//newnode->next = phead;

	//尾插函数写法2:
	LTInsert(phead, x);//由于LTInsert函数的功能是在pos的前一个位置插入一个结点,所以要想LTInsert有尾插的功能则必须传哨兵位头结点的地址phead给LTInsert函数。
}

//尾删函数
void LTPopBack(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	//判断链表除了哨兵位头结点外是否还有可以删除的元素
	assert(phead->next != phead);

	//尾删函数写法1:
	
	找尾结点
	//LTNode* tail = phead->prev;
	找尾结点的上一个结点
	//LTNode* tailPrev = tail->prev;
	把哨兵位头结点与尾结点的上一个结点链接起来
	//phead->prev = tailPrev;
	//tailPrev->next = phead;
	删除尾结点
	//free(tail);

	//尾删函数写法2:
	LTErase(phead->prev);//由于LTErase函数的功能是删除pos位置的结点,所以要想LTErase有尾删的功能则必须传尾结点的地址phead->prev给LTErase函数。
}

//头插函数
void LTPushFront(LTNode* phead, LTDataType x)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);

	//头插函数写法1:

	创建结点
	//LTNode* newnode = BuyListNode(x);
	
	链接方式1: 新创建的结点先链接第一个有效结点,再链接哨兵位头结点(注意:链接方式1必须是按这个顺序进行链接)
	把新创建的结点与第一个有效结点链接起来
	/*newnode->next = phead->next;
	phead->next->prev = newnode;

	把哨兵位头结点与新创建的结点链接起来
	phead->next = newnode;
	newnode->prev = phead;*/
	 
	链接方式2:(注意:由于我们存储了链表第一个有效结点的地址,所以我们可以不用关心新创建的结点与哨兵位头结点和第一个有效结点的链接顺序)
	找第一个有效结点新创建的结点先链接哨兵位头结点,再链接第一个有效结点。
	//LTNode* first = phead->next;
	把哨兵位头结点与新创建的结点链接起来
	//phead->next = newnode;
	//newnode->prev = phead;
	把新创建的结点与第一个有效结点链接起来
	//first->prev = newnode;
	//newnode->next = first;

	//头插函数写法2:
	LTInsert(phead->next, x);//由于LTInsert函数的功能是在pos的前一个位置插入一个结点,所以要想LTInsert有头插的功能则必须传链表第一个有效结点的地址phead->next给LTInsert函数。
}

//头删函数
void LTPopFront(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	//判断链表除了哨兵位头结点外是否还有可以删除的元素
	assert(phead->next != phead);

	//头删函数写法1:

	找第一个有效结点
	//LTNode* first = phead->next;
	找第二个有效结点
	//LTNode* second = first->next;

	头删第一个有效结点
	//free(first);

	把哨兵位头结点与第二个有效结点链接起来
	//phead->next = second;
	//second->prev = phead;

	//头删函数写法2:
	//注意:头删是一定不能删除哨兵位头结点的。头删实际删除的时有效的结点(注意:有效结点指的是用来实际存储链表数据的结点,而哨兵位头结点是不存储链表的数据的),所以头删删除的是链表的第一个有效结点。
	LTErase(phead->next);//由于LTErase函数的功能是删除pos位置的结点,所以要想LTErase有头删的功能则必须传第一个有效结点的地址phead->next给LTErase函数。
}

//查找函数
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	LTNode* cur = phead->next;
	//查找
	while (cur != phead)//遍历整个链表
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

// 在pos之前插入x
void LTInsert(LTNode* pos, LTDataType x)
{
	//判断带头双向循环链表是否存在地址为pos的结点
	assert(pos);
	//创建结点
	LTNode* newnode = BuyListNode(x);
	//找pos位置的前一个结点
	LTNode* prev = pos->prev;
	//链接
	//把新创建的结点和pos位置的前一个结点链接起来
	prev->next = newnode;
	newnode->prev = prev;
	//把新创建的结点和pos位置的结点链接起来
	newnode->next = pos;
	pos->prev = newnode;

}

// 删除pos位置
void LTErase(LTNode* pos)//注意:传参是一定不要传哨兵位头结点的地址,不然会删除哨兵位头结点的进而使得我们找不到链表
{
	//判断带头双向循环链表是否存在地址为pos的结点
	assert(pos);
	//找pos位置的前一个结点
	LTNode* prev = pos->prev;
	//找pos位置的后一个结点
	LTNode* next = pos->next;
	//链接
	prev->next = next;
	next->prev = prev;
	//删除pos位置的结点
	free(pos);
}

//判断带头双向循环链表是否为空链表(注意:空链表的特征是除了哨兵位头结点,链表有效结点的个数为0)
bool LTEmpty(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	写法1:
	//if (phead->next != phead)//当phead->next = phead时说明链表有效结点的个数为0。
	//	return false;//不是空链表
	//return true;//是空链表
	//写法2:
	return phead->next == phead;
}

//判断带头双向循环链表的有效结点个数(注意:有效结点指的是存储数据的结点,带头双向循环链表的哨兵位头结点是不存储数据的)
size_t LTSize(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	size_t size = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

//带头双向循环链表的销毁函数(注意:这个销毁函数包括销毁带头双向循环链表的哨兵位头结点,
//所以在使用该函数后一定要把主调函数中指向带头双向循环链表的哨兵位头结点指针设置为空指针NULL)
void LTDestroy(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	LTNode* cur = phead;
	while (cur != phead)
	{
		//先保存cur位置的下一个结点的地址
		LTNode* next = cur->next;
		//删除cur位置的结点
		free(cur);
		cur = next;
	}
	//删除哨兵位头结点
	free(phead);
	//phead = NULL;//注意:这里把phead设置为空指针实际是不会改变实参的值的,所以这里把phead置不置为空指针是可有可无的。
}

三、test.c源文件(测试源文件)

test.c(测试函数)

#include"List.h"

//测试尾插尾删函数
void TestList1()
{
	LTNode* plist = LTInit();
	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPushBack(plist, 5);
	LTPrint(plist);

	//尾删
	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	//LTPopBack(plist);
}

//测试头插头删函数
void TestList2()
{
	LTNode* plist = LTInit();
	//头插
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPushFront(plist, 5);
	LTPrint(plist);

	//头删
	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);
}

//测试查找函数
void TestList3()
{
	LTNode* plist = LTInit();
	//头插
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPushFront(plist, 5);
	LTPrint(plist);

	//指定修改pos位置结点的值
	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		//修改pos位置结点的值
		pos->data *= 10;
	}
	LTPrint(plist);

	LTDestroy(plist);
	//由于LTDestroy函数会删除带头双向循环链表的哨兵位头结点但是在LTDestroy函数内部时无法把头结点指针phead设置为空指针的,
	//所以在使用完LTDestroy函数之后一定要在主调函数中把头结点指针phead设置为空指针以此来避免对野指针进行非法访问
	plist = NULL;//注意:此时头结点指针phead是个野指针,所以必须把phead设置为空指针。
}

int main()
{
	//测试函数
	TestList1();
	TestList2();
	TestList3();

	return 0;
}

对带头循环双向链表的各个功能函数进行解析

注意:①这个循环双向链表方便尾插和尾删,而不循环单链表的尾插和尾删很麻烦因为要找尾结点。②双向循环链表的每个结点中的所有指针都不存在空指针。

一、对尾插函数LTPushBack进行解析

1.带头循环双向链表尾插的两种情况:

注意:有效结点指的是存放链表有效数据data的结点而且有效结点不包括哨兵位头结点,因为哨兵位头结点是不存放链表的有效数据data的。

情况1:要尾插的带头循环双向链表的有效结点数为0。

图形解析:

情况2: 要尾插的带头循环双向链表的有效结点数至少有1个。

图形解析:

2.代码:

//尾插函数
void LTPushBack(LTNode* phead, LTDataType x)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);

	//尾插函数写法1:

	创建结点
	//LTNode* newnode = BuyListNode(x);

	找尾结点
	//LTNode* tail = phead->prev;

	链接
	新创建的结点与尾结点链接
	//tail->next = newnode;
	//newnode->prev = tail;
	新创建的结点与头结点链接
	//phead->prev = newnode;
	//newnode->next = phead;

	//尾插函数写法2:
	LTInsert(phead, x);//由于LTInsert函数的功能是在pos的前一个位置插入一个结点,所以要想LTInsert有尾插的功能则必须传哨兵位头结点的地址phead给LTInsert函数。
}

2.1.对assert(phead)代码进行解析

若指针phead指向的链表不是个空链表的话,则要用暴力查找的方式对指针phead进行断言即判断指针phead指向的链表是否是空链表。

(总的来说,头结点指针phead指向的链表一定不会为空的话就要对头结点指针进行断言。)

//以下是对带头循环双向链表的尾插函数的写法一进行解析:
2.2对LTNode* newnode = BuyListNode(x)进行解析

利用BuyListNode(x)函数创建一个要进行尾插的结点,并用指针newnode指向这个新创建的结点。

2.3对LTNode* tail = phead->prev代码进行解析

定义一个中间变量指针tail遍历带头循环双向链表,即用指针tail访问带头循环双向链表的每个结点。此时指针phead指向带头循环双向链表的哨兵位头结点,而指针phead->prev是带头循环双向链表尾结点的地址,而把尾结点的地址phead->prev赋值给指针tail的目的是让指针tail指向链表的尾结点,这样就可以方便尾插了。

2.4.以下是把新创建的结点和哨兵位头结点链接起来和新创建的结点和尾结点链接起来的过程。

图形解析:

(1)以下是新创建的结点和尾结点链接起来的过程

tail->next = newnode;

newnode->prev = tail;

(2)以下是新创建的结点和哨兵位头结点链接起来的过程

newnode->next = phead;

phead->prev = newnode;

尾插函数写法1的小结

该尾插函数的写法一可以处理带头循环双向链表只有一个头结点而且这个头结点还是哨兵位头结点的尾插情况:

图形解析:

//以下是对带头循环双向链表的尾插函数的写法二进行解析:
2.5对LTInsert(phead,x)函数进行解析

当在pow位置之前插入1个结点的函数LTInsert的第一个参数是带头循环双向链表的哨兵位头结点的地址phead->next时,此时LTInsert函数的功能是对带头循环双向链表进行尾插。

二、对尾删函数LTPopBack代码进行解析

1.带头循环双向链表尾删的两种情况

注意:有效结点指的是存放链表有效数据data的结点而且有效结点不包括哨兵位头结点,因为哨兵位头结点是不存放链表的有效数据data的。

情况1:要尾删的带头循环双向链表的有效结点数为1。

图形解析:

情况2: 要尾删的带头循环双向链表的有效结点数至少有2个。

图形解析:

2.代码

//尾删函数
void LTPopBack(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	//判断链表除了哨兵位头结点外是否还有可以删除的元素
	assert(phead->next != phead);

	//尾删函数写法1:
	
	找尾结点
	//LTNode* tail = phead->prev;
	找尾结点的上一个结点
	//LTNode* tailPrev = tail->prev;
	把哨兵位头结点与尾结点的上一个结点链接起来
	//phead->prev = tailPrev;
	//tailPrev->next = phead;
	删除尾结点
	//free(tail);

	//尾删函数写法2:
	LTErase(phead->prev);//由于LTErase函数的功能是删除pos位置的结点,所以要想LTErase有尾删的功能则必须传尾结点的地址phead->prev给LTErase函数。
}
2.1.注意

(1)当尾删函数LTPopBack把带头循环双向链表删除到还剩二个结点即此时带头循环双向链表只剩下一个哨兵位头结点和哨兵位头结点位置的下一个结点时,此时尾删函数LTPopBack还可以再删除一个结点。

(2)断言assert(表达式)的好处是:在debug的版本下,若assert(表达式)中的表达式是为假(即表达是出错)的话,则程序会报告assert(表达式)中的表达式在那个文件里的哪一行出现错误,这样的话就可以不用调试就可以知道assert(表达式)中的表达式在哪里出现错误即不用调试就可以找到错误。总的来说,若形参是指针的话,则指针一定不为空指针的话就必须用assert进行断言。(对该句话进行解析:链表头结点的指针phead指向的链表一定不是空链表的话就必须用暴力检查的方式assert(phead)进行断言即用assert(phead)检查指针phead指向的链表是否为空链表,这样做是为了防止程序员在传参时传错参了导致链表头结点的指针phead指向的链表是个空链表,若用assert(phead)进行断言的话就可以发现程序是在代码的什么位置传错参数的即assert(phead)可以判断指针phead在代码中出错的位置)

小结:不带头不循环的单链表的尾删函数把单链表删除到还剩一个结点的时候,此时要另外处理单链表被删到还剩一个结点的情况,但是带头循环双向链表的尾删函数再把带头循环双向链表删到只剩下一个哨兵位头结点和哨兵位头结点位置的下一个结点时还可以再删除一个结点并不需要做出另外处理。

图形解析:

三、对头插函数LTPushFront代码进行解析

1.带头循环双向链表头插的两种情况:

注意:有效结点指的是存放链表有效数据data的结点而且有效结点不包括哨兵位头结点,因为哨兵位头结点是不存放链表的有效数据data的。

情况1:要头插的带头循环双向链表的有效结点数为0。

图形解析:

情况2: 要头插的带头循环双向链表的有效结点数至少有1个。

图形解析(注意:头插有两种链接方式,如下图所示)

2.代码:

//头插函数
void LTPushFront(LTNode* phead, LTDataType x)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);

	//头插函数写法1:

	创建结点
	//LTNode* newnode = BuyListNode(x);
	
	链接方式1: 新创建的结点先链接第一个有效结点,再链接哨兵位头结点(注意:链接方式1必须是按这个顺序进行链接)
	把新创建的结点与第一个有效结点链接起来
	/*newnode->next = phead->next;
	phead->next->prev = newnode;

	把哨兵位头结点与新创建的结点链接起来
	phead->next = newnode;
	newnode->prev = phead;*/
	 
	链接方式2:(注意:由于我们存储了链表第一个有效结点的地址,所以我们可以不用关心新创建的结点与哨兵位头结点和第一个有效结点的链接顺序)
	找第一个有效结点新创建的结点先链接哨兵位头结点,再链接第一个有效结点。
	//LTNode* first = phead->next;
	把哨兵位头结点与新创建的结点链接起来
	//phead->next = newnode;
	//newnode->prev = phead;
	把新创建的结点与第一个有效结点链接起来
	//first->prev = newnode;
	//newnode->next = first;

	//头插函数写法2:
	LTInsert(phead->next, x);//由于LTInsert函数的功能是在pos的前一个位置插入一个结点,所以要想LTInsert有头插的功能则必须传链表第一个有效结点的地址phead->next给LTInsert函数。
}

            

四、对头删函数进行解析

1.带头循环双向链表头删的两种情况

情况1:要头删的带头循环双向链表的有效结点数为1。

图形解析:

情况2: 要头删的带头循环双向链表的有效结点数至少有1个。

图形解析:

2.代码:

//头删函数
void LTPopFront(LTNode* phead)
{
	//判断指针phead指向的链表是否为空链表
	assert(phead);
	//判断链表除了哨兵位头结点外是否还有可以删除的元素
	assert(phead->next != phead);

	//头删函数写法1:

	找第一个有效结点
	//LTNode* first = phead->next;
	找第二个有效结点
	//LTNode* second = first->next;

	头删第一个有效结点
	//free(first);

	把哨兵位头结点与第二个有效结点链接起来
	//phead->next = second;
	//second->prev = phead;

	//头删函数写法2:
	//注意:头删是一定不能删除哨兵位头结点的。头删实际删除的时有效的结点(注意:有效结点指的是用来实际存储链表数据的结点,而哨兵位头结点是不存储链表的数据的),所以头删删除的是链表的第一个有效结点。
	LTErase(phead->next);//由于LTErase函数的功能是删除pos位置的结点,所以要想LTErase有头删的功能则必须传第一个有效结点的地址phead->next给LTErase函数。
}

带头双向循环链表的总结

1.带头循环双向链表可以正着遍历和倒着遍历

(1)正着遍历的方式

LTNode*cur = phead->next;

While(cur != phead)

{

    cur = cur->next;

}

(2)倒着走遍历的方式

LTNode*cur = phead->prev;

While(cur != phead->prev)

{

    cur = cur->prev;

}

2.如何在10min之内写一个链表

(1)先声明链表结点的结构体类型

(2)声明并定义BuyListNode函数

(3)声明并定义LTInit函数

(4)声明并定义LTFiind函数

(5)声明并定义LTInsret函数和LTErase函数,并用LTInsret函数和LTErase函数代替头插函数、尾插函数、头插函数、尾删函数

(6)定义并声明LTEmpty函数

(7)定义并声明LTSize函数

(8) 定义并声明LTDestroy函数

3.为什么带头循环双链表没有提供在链表pos位置之后插入一个结点的函数,而不带头不循环单链表却提供了在链表pos位置之后插入一个结点的函数的原因:

(1)由于不带头不循环单链表的在链表pos位置之前插入一个结点的函数的效率有点低,所以才会提供一个在链表pos位置之后插入一个结点的函数给单链表

(2)在单链表中实现在pos位置之前插入1个结点的函数和删除pos位置结点的函数的功能有点困难,但是在带头循环双向链表中实现在pos位置之前插入1个结点的函数和删除pos位置结点的函数的功能却很容易。

原因:这两个函数的实现在单链表中和在带头循环双向链表中实现的方式是不一样的:在单链表中这两个函数都要区分函数是否是处于对链表进行头插或头删的情况,由于带头循环双向链表始终有一个哨兵位头结点使得在带头循环双向链表中这两个函数不需要区分函数是否是处于对链表进行头插或头删的情况。

4.对利用LTEmpty函数判断指针phead指向的双向链表是否是空链表的补充:

(1)我们一般认为只有哨兵位头结点这一个结点的链表是个空链表。

(2)由于LTEmpty函数的返回值的数据类型是布尔值bool,而C语言是默认不支持布尔值的,但是这个布尔值是包含在头文件stdbool.h中,所以只要C程序利用#include包含头文件stdbool.h的话就可在C语言的代码中使用布尔值了,只有这样LTEmpty函数的返回值才能用布尔值bool表示。

5.为什么哨兵位头结点中的成员变量data不能用来存放链表的有效长度的原因(假设链表的有效长度用size表示)

已知:链表所有结点的类型——结构体类型

typedf  int  LTDataType;//链表中所有结点的成员变量数据data的类型都是LTDataType,但是数据data的类型的大小不一定是int类型,而有效结点的成员变量数据data是用来存放链表中的有效数据的。

typedf  structure  ListNode

{

   struct ListNode* prev;

   struct ListNode* next;

   LTDataType data;

}LTNode;

(1) 不能用哨兵位头结点中的成员变量data存放链表的有效长度size的最大的问题是:由于这个链表存放的数据data的数据类型LTDataType不一定是int,而且链表的有效长度size的数据类型是必须整形。若链表中的数据data的数据类型被改为为char或者float或者double的话则此时哨兵位头结点中的成员变量data就不适合用来存放链表的有效长度size的值,因为当链表的有效长度size的值很大时哨兵位头结点中的char类型的成员变量data是存放不下size的值的而且最终有可能会导致链表有效长度size为负值,而且链表的有效长度size的值必须是整形数据,所以更不可能用float或者double类型的成员变量data存放链表有效长度size的值,所以不能使用哨兵位头结点中的成员变量data来存放链表的有效长度size的值。

6.

带头双向循环链表比不带头不循环单链表的结构优势大。一般使用带头双向循环链表存储数据,而不是使用不带头不循环单链表存储数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值