顺序表和链表2

目录

3、链表:

3.1 、链表的概念及结构 :

3.2、链表的分类 :

3.3、链表以文件形式进行实现 :

3.3.1、test.c源文件:

3.3.2、SList.c源文件:

3.3.3、SList.h头文件:

3.4、链表面试题:

3.4.1、链表面试题一:

3.4.2、链表面试题二:

3.4.3、链表面试题三:

3.4.4、链表面试题四:

3.4.5、链表面试题五:

3.4.6、链表面试题六:

3.4.7、链表面试题七:

3.4.8、链表面试题八:

3.4.9、链表面试题九:

3.4.10、链表面试题十:

3.4.11、链表面试题十一:


3、链表:

3.1 、链表的概念及结构

概念: 链表 是一种 物理存储结构上非连续、非顺序的存储结构 ,数据元素的 逻辑顺序 是通过链表中的 指针链接次序 实现的 、

数据结构中:

链表中,只需要知道第一个节点的位置,就可以通过指针把整个链表都进行访问,不存在扩容的代价不存在空间的浪费,因为是按需索取空

,在顺序表中的原地扩容时,一般也是直接扩容到原来大小的1.5或2倍,扩容时,如果后面的空间足够大,就会直接在原地进行扩容,也可能

会造成空间的浪费,除此之外,顺序表原地扩容时,其物理结构也是连续的,但是在链表中,当空间不够使用时,会进行申请空间,他是一个一

个的申请空间按需索取空间,所以不会造成空间的浪费,但是,这些空间的物理结构是不连续的,链表在一定程度上可以解决顺序表的缺陷,

链表在头部或者中间插入数据或者删除数据时,都不需要对数据进行挪动,通过改变指针就可以完成,但是链表也有一定的缺陷,因为链表是一

个一个的按需索取空间,如果要存储的数据量过大,就会频繁的开辟空间,除此之外,链表不支持随机访问,即按照下标进行访问,如果想随机

访问链表中任意一个位置的数据,就必须要先从头开始依次进行遍历访问,直到访问到所需的数据,而在顺序表中,由于其物理结构是连续的

这样就可以直接通过下标对顺序表中任意一个位置的数据进行随机访问,链表和顺序表相辅相成,不可以进行取代、

当使用链表时,按需一个一个索取空间的时候,就会一个一个的申请空间,该空间也是动态开辟出来的,由于链表的物理结构是不连续的,所以

当每一次申请空间的时候,不可以使用realloc函数来一个一个的增容空间,这是不可以的,因为使用realloc函数进行增容时,不管是原地还是异

地增容,都是在原来的空间后面进行增容的,即增容后的整个空间的物理结构是连续的,但是,链表的物理结构是不连续的,所以,当使用链表

时,不可以用realloc函数来一次增容一个空间,除此之外,已知,当使用realloc函数,若第一个参数为NULL空指针时,就相当于是malloc函数功

能,所以,当开辟链表中第一个空间的时候,可以使用realloc函数,但是要保证该函数的第一个参数要为空指针NULL,然后,后面再申请第

二, 第三个空间的时候,就只能使用malloc函数来开辟,就不能再使用realloc函数来开辟了,所以这样比较麻烦,因为,只有开辟链表中第一个

空间的时候,是可以使用realloc函数的,但是后面所有的空间的开辟都要使用malloc函数,为了避免这样的麻烦,在此,如果是使用链表,就直

使用malloc函数来动态开辟内存空间,不使用realloc函数、当顺序表异地扩容时性能消耗较大,原地扩容的时候,性能消耗较小,但也会有性

能消耗、

malloc函数是在堆区上动态开辟的内存空间,使用该函数动态开辟出来的内存空间是系统随机分配的,所以该空间的位置也是随机的链表中按

需索取空间顺序表中一个一个的扩容空间不一样的,前者除第一个空间外,必须每次都使用malloc函数来动态开辟内存空间,需要一个就开

辟一个,不需要一个就释放一个,而后者如果想要一个一个的增容,使用的则是realloc函数来扩容,这是因为顺序表要求物理结构是连续的,而

链表要求物理结构是不连续的、

链表中,会定义一个指针变量,即,指针变量phead或者pList指针名字并不代表是否是带头链表,用来存放链表中第一个节点的地址、

如果是无头链表,则指针变量phead或者plist指向链表中第一个节点的地址,即头节点的地址,也即:第一个有效节点的地址,如果是带头

表,则指向链表中第一个节点的地址,即头节点的地址,也即:链表中第一个节点,即不存储有效数据的节点的地址、

不管链表是否带头,头节点都是指链表中第一个节点,若是无头链表,则其头节点就是第一个有效节点,若是带头链表,则其头节点就是链表中

第一个节点,即不存储有效数据的节点、

所谓头指针即指,指向链表中第一个节点的地址,若是无头链表头指针则指向链表中第一个有效节点,若是带头链表,则头指针指向链表中第

一个节点,即不存储有效数据的节点、

现实中数据结构中:

3.2、链表的分类 :

实际中链表的结构非常多样,以下 情况组合起来就有 8种 链表结构
1、单向或者双向、
所谓 单向即指单链表 ,在其中, 一个节点中只存储一个指针 ,该指针指向下一个节点,单链表存在缺陷,如下图中第一种情况,它只能从d1找
d2,d2找d3,并 不能反向进行, 单链表中 最后一个节点中的指针一定指向空指针NULL;
所谓 双向即指双链表 ,在其中, 一个节点中存储了两个指针 ,后指针指向了下一个节点的地址,前指针指向了前一个节点的地址、
双向链表中有 前驱指针,也有 后继指针,而 单向链表中只有 后继指针

2、带头或者不带头、

以下则为:不带头单链表,带头单链表,其中带头的情况在尾插的时候会很方便、

哨兵位的头节点也是结构体类型,在其内部也会存在数据,可能是随机值,也可能是0,是什么值都不重要,是因为使用不到它的值、

 3、循环或者非循环、

以下则为:非循环单链表和循环单链表、

对于双向非循环链表而言,最后一个节点中的后继指针指向空指针NULL,同时头节点中的前驱指针也指向空指针NULL、

 

所谓非循环链表即指,链表中最后一个节点中存储的为空指针NULL,若链表中最后一个节点中的指针域中存储的为第一个节点的地址,则称为

循环链表

上述分类如果组合起来就有8种结构,虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1、无头单向非循环链表:
      
     结构简单, 一般不会单独用来存数据 ,实际中更多是作为其他数据结构的 子结构 ,如哈希桶、图的邻接表等等,另外这种结构在 笔试面试中出
现很多、
2、带头双向循环链表:
     
    结构最复杂, 一般用在单独存储数据 ,实际中使用的链表数据结构,都是 带头双向循环链表 ,另外这个结构虽然结构复杂,但是使用代码实现
以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了,这种链表在实际中是 最实用的 ,也是后期所学的STL,STL
是C++中的一个官方库,当中的list的结构,即,STL中有一个数据结构,叫做list结构,使用的就是该种结构、

3.3、链表以文件形式进行实现 :

3.3.1、test.c源文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

void TestSList1()
{
	SLTNode* plist = NULL;
	//尾插 
	//当某次尾插之前的plist如果为NULL,在调用函数内要改变*pplist,所以要使用传址调用,由于此时的plist初始化为NULL,所以当第一次尾插的时候一定要使用
	//传址调用,这是因为要在调用函数内部改变*pplist的值,如果按照下面的代码来看的话,连续进行尾插,第二次以及后面的尾插时,已经保证前面有了节点,这时按理说是可以通过传值调用即可,但是,还要再写出来
	//另外一个尾插函数,这是因为第一次尾插时必须传址,若后面的尾插采用传值的话,调用函数的形参部分就要发生改变,写出来两个尾插函数,比较麻烦,
	//其二就是,如果不是连续调用尾插函数的话,两次尾插之间如果有别的函数存在时,可能其他函数会改变*pplist的值,当再次尾插时,就不知道*pplist是否为NULL,要先进行判断,如果为NULL则使用传址调用,如果不为空,则使用传值,
	//这样就会非常麻烦,所以当每次尾插时都使用传址调用即可,这是因为传址调用能够满足传值调用的要求,但是传值满足不了传址调用的要求、
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);
	//头插
	//每一次头插的时候,在调用函数内部都需要改变*pplist,所以要使用传址调用、
	SListPushFront(&plist, 0);
	//打印
	//打印时不会在调用函数内部改变*pplist的值,所以直接传值调用即可、
	SListPrint(plist);
	//尾删
	SListPopBack(&plist);
	//打印
	SListPrint(plist);
	//尾删
	SListPopBack(&plist);
	//打印
	SListPrint(plist);
	//尾删

	SListPopBack(&plist);
	//打印
	SListPrint(plist);
	//尾删

	SListPopBack(&plist);
	//打印
	SListPrint(plist);
	//尾删
	SListPopBack(&plist);
	//打印
	SListPrint(plist);
	//尾删
	SListPopBack(&plist);
	//打印
	SListPrint(plist);
	//头删
	SListPopFront(&plist);
	//打印
	SListPrint(plist);

	//尾插
	SListPushBack(&plist, 1);
	//尾插
	SListPushBack(&plist, 2);
	//尾插
	SListPushBack(&plist, 3);
	//打印
	SListPrint(plist);
	//头删
	SListPopFront(&plist);
	//打印
	SListPrint(plist);

	//查找附带修改、
	SLTNode* pos = SListFind(plist, 2);
	if (pos == NULL)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找打了,其地址为:%p\n", pos);
		//查找功能附带修改功能、
		//pos->data = 30;
		//修改后进行打印、
		//printf("修改完之后进行打印为:\n");
		//SListPrint(plist);
		//单链表在pos位置之 后 插入数据200;

		SListInsertAfter(pos, 200);
		//打印
		SListPrint(plist);
		//单链表在pos位置之 前 插入数据300;
		SListInsertBefore(&plist, pos, 300);//传址
		//打印
		SListPrint(plist);
		//单链表删除pos位置之 后 的节点、
		SListEraseAfter(pos);
		//打印
		SListPrint(plist);
	}
	//尾删
	SListPopBack(&plist);
	//尾删
	SListPopBack(&plist);
	//打印
	SListPrint(plist);

	//查找附加修改、
	pos = SListFind(plist, 300);
	if (pos == NULL)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找打了,其地址为:%p\n", pos);
		//查找功能附带修改功能、
		//pos->data = 30;
		//修改后进行打印、
		//printf("修改完之后进行打印为:\n");
		//SListPrint(plist);

		//单链表删除pos所在位置的节点、
		SListEraseCur(&plist, pos);
		//打印
		SListPrint(plist);
	}

	//尾插
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);

	//打印
	SListPrint(plist);

	//查找附加修改、
	pos = SListFind(plist, 5);
	if (pos == NULL)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找打了,其地址为:%p\n", pos);
		//查找功能附带修改功能、
		//pos->data = 30;
		//修改后进行打印、
		//printf("修改完之后进行打印为:\n");
		//SListPrint(plist);

		//单链表删除pos所在位置的节点、
		SListEraseCur(&plist, pos);
		//打印
		SListPrint(plist);
	}
	//尾插
	SListPushBack(&plist,6);
	//打印
	SListPrint(plist);

	//销毁单链表、
	SListDestroy(&plist);
	//打印
	SListPrint(plist);
}

int main()
{
	TestSList1();

	return 0;
}

//单链表结构适合头插,头删,  尾部若进行插入或删除是不适合的,对于尾插要进行遍历找尾,尾删的时候还要遍历找到倒数第二个节点的地址,然后
//再将其next置为空指针NULL,若在pos位置之前插入节点也是不方便的,需要先遍历找到pos位置之前的节点的地址,除此之外,删除pos位置所在的节点也是不方便的
//需要找到pos位置前面的一个节点的地址,使其与pos所在位置之后的节点或者空指针NULL链接起来、

//使用双向链表单独存储数据会更加方便,单链表主要用来作为复杂数据结构的子结构,如:图的邻接表或者是哈希桶、
//除此之外,单链表会在很多经典的练习题中使用、

3.3.2、SList.c源文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"


//打印链表
void SListPrint(SLTNode* plist)
{
	//在此处不可以直接断言plist,因为,链表可能为空链表,当链表为空链表的时候,打印出来就是空链表,只有NULL
	//若在此处对plist进行断言,如果链表为空链表的话,在此就会直接报错,而我们想要的是,若链表为空链表时,就打印出空链表,而不是直接报错、

	//在调用函数内部没改变 plist 的值,所以选用  传值  调用、
	SLTNode* cur = plist;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		//链表的遍历、
		cur = cur->next;
	}
	printf("NULL\n");
}


//创建新节点
SLTNode* BuySLTNode(SLTDataType x)
{
	//在堆区上按需索取空间、
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		//动态开辟空间失败、
		printf("malloc fail\n");
		return NULL;
	}
	else
	{
		//动态开辟内存空间成功、
		node->data = x;
		node->next = NULL;
		return node;
	}
}

//尾插
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);

	//创建新节点、
	SLTNode* newnode = BuySLTNode(x);

	//判断 *pplist是否为空、
	if (*pplist == NULL)
	{
		//空链表、
		//在调用函数内部改变了*pplist的值,所以选用 传址 调用、
		*pplist = newnode;
	}
	else
	{
		//非空链表、
		//先遍历链表进行找尾、
		SLTNode* tail = *pplist;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
//当使用带头链表进行尾插时,即使链表为空,此时链表中还有头节点,其不存有效数据,而plist就指向了该头节点的地址,所以plist不是空指针NULL
//当进行尾插时,直接在头节点后面进行链接节点即可,所以进行尾插时直接使用 传值 调用即可、


//头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);

	//创建新节点、
	SLTNode* newnode = BuySLTNode(x);

	//不需要考虑 *pplist 是否为空指针,在此,不管其是否为空指针NULL,都是可以的、
	newnode->next = *pplist; 
	//在调用函数内部改变了*pplist的值,所以选用 传址 调用、
	*pplist = newnode;
}
//当使用带头链表进行头插时,不是在头节点前面插,而是在所有的有效节点之前,头节点之后进行插入,此时plist仍指向的是头节点的地址,并没有被改变,所以
//直接使用传值调用即可、



//尾删
void SListPopBack(SLTNode** pplist)
{
	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);


	//也可以暴力检查链表为空的情况,当链表为空的时候就代表不能再进行尾删了,若再尾删就会报错、
	//assert(*pplist);

	//下面情况是非暴力检查,其代表当链表为空的时候,是可以进行尾删的,不会报错,只不过没有删掉任何节点,当再打印链表的时候
	//还是打印出空链表就行了、
	if (*pplist == NULL)
	{
		//链表中没有节点、
		//没有节点可以删除,所以就直接返回即可、
		return;
	}
	else if ((*pplist)->next == NULL)
	{
		//只有一个节点、
		//当有节点存在时,不管是以哪种方法删去,只要删除一个节点则就需要释放一个空间,但是由于此种情况只有一个节点,而且*pplist指向了第一个节点的地址,若把该空间释放后,那么*pplist所指的空间就再存在,则该指针即为野指针
		//所以要将其置为空指针NULL,此时就需要改变*pplist的值,只有通过传址调用才能使得调用函数外面的*pplist的值发生改变,所以在此选择 传址 调用、
		free(*pplist);
		//在调用函数内部改变了*pplist的值,所以选用 传址 调用、
		*pplist = NULL;
	}
	else
	{
		//多个节点、

		//记录倒数第二个节点的地址,通过该节点的地址,找到并把该节点中指针所指的地址置为NULL;
		//方法一:
		SLTNode* prev = NULL;
		SLTNode* tail = *pplist;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		//此时tail是一个局部变量,当free(tail)后再把tail置为空指针的原因是因为,当释放完空间之后,tail仍指向这块空间的地址,此时指针变量tail即为
		//野指针,为了防止不小心再次对该野指针进行解引用从而造成错误,所以就要将其置为空指针NULL,这是一个好习惯,此时即使不把指针变量taile置为空指针
		//NULL也是可以的,这是因为指针变量tail是一个局部变量,当出了调用函数后则局部变量就会被销毁,在调用函数外部是无法使用局部变量tail的,所以即使
		//再此不把tail置为空指针也是可以的,但是一般来说都置为空指针,这是一个好习惯、
		prev->next = NULL;


		方法二:
		//SLTNode* tail = *pplist;
		//while (tail->next->next != NULL)
		//{
		//	tail = tail->next;
		//}
		此时tail所在位置即为倒数第二个节点的地址、
		//free(tail->next);
		//tail->next = NULL;
	}
}
//当使用带头链表进行尾删时,即使链表全部删完,此时链表中还有头节点,头节点不会被删除,其不存有效数据,而plist就指向了该头节点的地址
//由于该头节点所占空间不会被释放,所以不需要把plist置为空指针NULL,所以当进行尾删时直接使用 传值 调用即可、



//头删
//每一次头删,当不是空链表时,plist的值就会发生改变,所以要使用传址调用,当是空链表时直接返回,虽然没有改变*pplist的值,但是为了使用同一个
//调用函数,直接使用 传址 调用即可、
void SListPopFront(SLTNode** pplist)
{
	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);

	//也可以暴力检查链表为空的情况,当链表为空的时候就代表不能再进行尾删了,若再尾删就会报错、
	//assert(*pplist);

	//下面情况是非暴力检查,其代表当链表为空的时候,是可以进行头删的,不会报错,只不过没有删掉任何节点,当再打印链表的时候
	//还是打印出空链表就行了,在OJ题中一般都是不采取暴力的、

	if (*pplist == NULL)
	{
		//空链表、
		//不再需要删除,直接返回即可、
		return;
	}
	else
	{
		//链表中一个或多个节点、

		//对于链表而言,如果直接把前一个节点释放掉的话,则后一个节点的地址就会被置为随机值,即,如果直接释放掉前一个节点的话,就找不到后一个节点了
		//所以,在释放前一个节点之前先把后一个节点的地址保存一份、

		//保存下一个节点的地址、
		//-> 的优先级高于 *
		SLTNode* next = (*pplist)->next;
		free(*pplist);
		//在调用函数内部改变了*pplist的值,所以选用 传址 调用、
		*pplist = next;
		//当链表中只有一个节点时,则结构体指针变量next中存储的就是空指针NULL,现要释放*pplist所指的空间,之后则指针变量*pplist就变成了
		//野指针,要将其置为空指针NULL,此时,结构体指针变量next的值就是空指针NULL,将其赋值给*pplist,则就会把*pplist置为空指针NULL、
	}
}



//查找附加修改、
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	//当链表为空时,plist等于NULL,此时查找的话想得到的结果就是找不到即可,并不需要直接断言从而报错,太暴力、
	//assert(plist);

	//该调用函数返回值为查找到的节点的地址、
	//在调用函数内部没有改变plist的值,则只需要 传值 调用即可、
	SLTNode* cur = plist;
	//如果为空链表,则plist等于NULL,所以,cur等于NULL,不进循环、
	//如果不是空链表,则进入循环、
	while (cur)	
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	//到此为止,只有两种情况,要么是空链表不进入循环从而没找到,要么就是进入循环遍历完链表之后也没有找到、
	return NULL;
}
//像查找和打印一样的功能,使用的都是传值调用,在这两个调用函数内部,按理说也可以不进行SLTNode* cur = plist; 直接使用plist即可,这是因为,两个调用函数
//采用的都是传值调用,形参是实参的一份临时拷贝,在调用函数内部改变形参不会改变实参的值,所以在调用函数内部直接使用plist也是可以的,即使改变了它的值,在调用函数
//外部的plist的值仍没有发生改变,但是通常进行SLTNode* cur = plist操作,是一种好的习惯,并其代码的可读性较高,但是如果采用的是传址调用的话,这里就不能不进行赋值操作
//否则在调用函数内部改变plist的值,调用函数外部的plist的值也会随之改变、




//单链表在pos位置之后插入x;
//在调用函数内部不会改变pos的值,故 传值 调用即可、
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	//不需要通过在调用函数内部改变pos的值从而改变外面的pos的值,所以直接 传值 调用即可、

	//断言,保证pos位置的地址是有效的、
	assert(pos);
	//在此也可以不对pos进行断言,具体见  单链表在pos位置之 前 插入x 里面、
	//个人感觉这一步也可以省略是因为,如果程序能够执行到该调用函数的话,说明了pos一定不等于空指针NULL,若等于空指针根本就不能执行该调用函数,所以这一部省略也是可以的
	//加上也不影响,同时当调用到该函数时,pos一定不是空指针NULL,,那么又间接说明了链表肯定不是空链表,所以综合下来只需要进行第一个断言即可,加上pos的断言也是没有关系的、
	//这是鉴于在main函数里已经把关系捋清楚之后的情况,即把SListEraseCur,SListEraseAfter,SListInsertBefore,SListInsertAfter函数的调用放在找到pos的模块内,即pos不等于NULL模块内是可以省略pos的断言
	//但是如果在main函数内没有把这些调用函数放在其中的话,就需要断言一下pos防止出现问题,所以,我们在这里最好要进行断言一下才会更加安全、

	//当代码执行到该调用函数时,pos一定不是空指针NULL,所以其一定是有效地址,所以pos一定指向链表中有效节点的地址、

	//创建新节点、
	SLTNode* newnode = BuySLTNode(x);

	//方法一:
	/*newnode->next = pos- >next;
	pos->next = newnode; */

	//方法二:
	//在把新节点插入链表中之前把pos位置的下一个节点的地址保存一份到next中、
	SLTNode* next = pos->next;
	pos->next = newnode; 
	newnode->next = next;
}


单链表在pos位置之 前 插入x;
//void SListInsertBefore(SLTNode* pos, SLTDataType x)
//{
//	不需要通过在调用函数内部改变pos的值从而改变外面的pos的值,所以直接 传值 调用即可、

//	//断言,保证pos位置的地址是有效的、
//	assert(pos);

//	//创建新节点、
//	SLTNode* newnode = BuySLTNode(x);
//	
//	//此时,可以通过:newnode->next=pos,,这样就可以把新节点与pos位置的节点进行链接起来,但是,新节点要想与原来pos位置的前一个节点链接
//	//起来是不容易的,因为,pos位置的前一个节点的地址是不知道的,又因是单链表,所以也无法通过后一个节点推出前一个节点的地址,所以目前来看
//	//pos位置的前一个节点的地址是不知道的,就没有办法把pos位置的前一个节点与新节点进行链接、
//	//这就是单链表的缺陷,如果是双链表的话,就可以直接通过pos找到pos位置的前一个节点的地址了,所以,使用单链表在pos位置之前插入数据是比较麻烦的、
//
//	//但是使用单链表的话也是可以解决的,只不过比较麻烦,现在就是不知道pos位置的前一个节点的地址,所以要想办法去寻找,只能从单链表起始位置一直找才可以,
//	//所以该调用函数的形参部分要进行改动,我们要从单链表的起始位置开始往后找节点,那么就需要在该调用函数的形参部分来接收指向单链表起始位置(第一个节点)的指针。
//}


//单链表在pos位置之 前 插入x;
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);

	//断言,保证pos位置的地址是有效的、
	assert(pos);
	//个人感觉这一步也可以省略是因为,如果程序能够执行到该调用函数的话,说明了pos一定不等于空指针NULL,若等于空指针根本就不能执行该调用函数,所以这一部省略也是可以的
	//加上也不影响,同时当调用到该函数时,pos一定不是空指针NULL,,那么又间接说明了链表肯定不是空链表,所以综合下来只需要进行第一个断言即可,加上pos的断言也是没有关系的、
	//这是鉴于在main函数里已经把关系捋清楚之后的情况,即把SListEraseCur,SListEraseAfter,SListInsertBefore,SListInsertAfter函数的调用放在找到pos的模块内,即pos不等于NULL模块内是可以省略pos的断言
	//但是如果在main函数内没有把这些调用函数放在其中的话,就需要断言一下pos防止出现问题,所以,我们在这里最好要进行断言一下才会更加安全、

	//可以暴力检查链表为空的情况,这是因为,已经默认当调用该函数时链表不可能为空,但是当*pplist为空指针NULL时说明肯定出现了错误,可以暴力检查、
	//assert(*pplist);
	//这一步可以省略,是因为前面已经断言了pos,当前面pos的断言通过之后,间接的断言了链表不是空链表,所以可以省略这一步、

	//该调用函数是用来在pos位置之前插入数据x,当执行到该调用函数时,pos的值肯定不是空指针NULL,这个时候那么链表肯定是不空链表,
	//所以在此不考虑链表为空的情况、

	//不需要通过在调用函数内部改变pos的值从而改变外面的pos的值,所以直接 传值 调用即可、


	//当pos的位置恰好为第一个节点的位置时,此时pos等于cur,不进入循环,出循环再执行prev->next = newnode;的时候
	//由于此时的prev中还是空指针NULL,所以就会出错,所以要加if语句过滤,如果pos的位置恰好为第一个节点的位置时就相当于是头插,


	方法一:

	创建新节点、
	//SLTNode* newnode = BuySLTNode(x);

	判断pos所在的位置是否为第一个节点、
	//if (pos == *pplist)
	//{
	//	//pos所在位置是第一个节点的位置、

	//	//头插即可、
	//	newnode->next = pos;
	//	//在调用函数内部会改变*pplist的值所以要选择 传址 调用、
	//	*pplist = newnode;
	//}
	//else
	//{
	//	//pos所在位置不是第一个节点的位置、
	//	SLTNode* prev = NULL;
	//	SLTNode* cur = *pplist;
	//	while (cur != pos)
	//	{
	//		prev = cur;
	//		cur = cur->next;
	//	}
	//	prev->next = newnode;
	//	newnode->next = pos;
	//}


	//方法二:
	//判断pos所在的位置是否为第一个节点、
	if (pos == *pplist)
	{
		//pos所在位置是第一个节点的位置、
		//头插即可、
		SListPushFront(pplist, x);
	}
	else
	{
		//pos所在位置不是第一个节点的位置、
		SLTNode* prev = *pplist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//创建新节点、
		SLTNode* newnode = BuySLTNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}
//但是如果不告诉指向单链表的起始位置(第一个节点)的指针应该怎么办呢?
//应该先在pos所在位置的后面插入一个新的节点,插入完毕后,再把新节点和pos所在位置的节点中的data进行交换即可、


//单链表删除pos位置之 后 的节点、
void SListEraseAfter(SLTNode* pos)
{
	//不需要通过在调用函数内部改变pos的值从而改变外面的pos的值,所以直接 传值 调用即可、

	//断言,保证pos位置的地址是有效的、
	assert(pos);
	//个人感觉这一步也可以省略是因为,如果程序能够执行到该调用函数的话,说明了pos一定不等于空指针NULL,若等于空指针根本就不能执行该调用函数,所以这一部省略也是可以的
	//加上也不影响,同时当调用到该函数时,pos一定不是空指针NULL,,那么又间接说明了链表肯定不是空链表,所以综合下来只需要进行第一个断言即可,加上pos的断言也是没有关系的、
	//这是鉴于在main函数里已经把关系捋清楚之后的情况,即把SListEraseCur,SListEraseAfter,SListInsertBefore,SListInsertAfter函数的调用放在找到pos的模块内,即pos不等于NULL模块内是可以省略pos的断言
	//但是如果在main函数内没有把这些调用函数放在其中的话,就需要断言一下pos防止出现问题,所以,我们在这里最好要进行断言一下才会更加安全、

	//也可以暴力检查pos所在位置后面是否为空的情况,当为空的时候就代表不能再删除了,若再尾删就会报错、
	//assert(*pplist);

	//也可以进行温柔检查,当pos所在位置后面为空时,则不需要删除直接返回就行,不让其报错、

	//当代码执行到该调用函数时,pos一定不是空指针NULL,所以其一定是有效地址,所以pos一定指向链表中有效节点的地址、

	//判断pos所在位置后面是否还有节点、
	if (pos->next == NULL)
	{ 
		//pos所在位置后面没有节点、
		//不需要删除,直接返回即可、
		return;
	}
	else
	{
		//pos所在位置后面有节点,无论是pos所在位置后面有一个还是多个节点时均适用、
		SLTNode* next = pos->next;
		pos->next=next->next;
		free(next);
		next = NULL;
	}
}



//单链表删除当前pos所在位置的节点、
void SListEraseCur(SLTNode** pplist, SLTNode* pos)
{
	//不需要通过在调用函数内部改变pos的值从而改变外面的pos的值,所以直接 传值 调用即可、

	//该调用函数是用来删除pos所在位置的节点,当执行到该调用函数时,pos的值一定不是空指针NULL,这个时候那么链表肯定是不空链表,所以在此不考虑链表为空的情况、

	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);

	//断言,保证pos位置的地址是有效的、
	assert(pos);
	//个人感觉这一步也可以省略是因为,如果程序能够执行到该调用函数的话,说明了pos一定不等于空指针NULL,若等于空指针根本就不能执行该调用函数,所以这一部省略也是可以的
	//加上也不影响,同时当调用到该函数时,pos一定不是空指针NULL,,那么又间接说明了链表肯定不是空链表,所以综合下来只需要进行第一个断言即可,加上pos的断言也是没有关系的、
	//这是鉴于在main函数里已经把关系捋清楚之后的情况,即把SListEraseCur,SListEraseAfter,SListInsertBefore,SListInsertAfter函数的调用放在找到pos的模块内,即pos不等于NULL模块内是可以省略pos的断言
	//但是如果在main函数内没有把这些调用函数放在其中的话,就需要断言一下pos防止出现问题,所以,我们在这里最好要进行断言一下才会更加安全、

	//可以暴力检查链表为空的情况,这是因为,已经默认当调用该函数时链表不可能为空,但是当*pplist为空指针NULL时说明肯定出现了错误,可以暴力检查、
	//assert(*pplist);
	//这一步可以省略,是因为前面已经断言了pos,当前面pos的断言通过之后,间接的断言了链表不是空链表,所以可以省略这一步、


	//方法一:
	//如果pos的位置恰好为第一个节点的位置时就相当于是头删、
	//判断pos所在的位置是否为第一个节点、
	if (pos == *pplist)
	{	
		//pos所在位置是第一个节点的位置、
		//头删即可、

		//链表中有一个或多个节点、
		//对于链表而言,如果直接把前一个节点释放掉的话,则后一个节点的地址就会被置为随机值,即,如果直接释放掉前一个节点的话,就找不到后一个节点了
		//所以,在释放前一个节点之前先把后一个节点的地址保存一份、
		//保存下一个节点的地址、
		//-> 的优先级高于 *
		SLTNode* next = (*pplist)->next;
		free(*pplist);
		//在调用函数内部改变了*pplist的值,所以选择 传址 调用、
		*pplist = next;
		//当链表中只有一个节点时,则结构体指针变量next中存储的就是空指针NULL,现要释放*pplist所指的空间,之后则指针变量*pplist就变成了
		//野指针,要将其置为空指针NULL,此时,结构体指针变量next的值就是空指针NULL,将其赋值给*pplist,则就会把*pplist置为空指针NULL、
	}
	else
	{
		//pos所在位置不是第一个节点的位置、
		SLTNode* prev = NULL;
		SLTNode* cur = *pplist;
		while (cur != pos)
		{
			prev = cur;
			cur = cur->next;
		}
		//无论pos所在位置后面有节点,还是没有节点的情况均适用、
		prev->next = pos->next;
		free(pos);
		pos= NULL;
		//当pos的位置恰好为第一个节点的位置时,此时pos等于cur,不进入循环,出循环再执行prev->next = newnode;的时候
		//由于此时的prev中还是空指针NULL,所以就会出错,所以要加if语句过滤,如果pos的位置恰好为第一个节点的位置时就相当于是头删、
	}


	方法二:
	如果pos的位置恰好为第一个节点的位置时就相当于是头删、
	判断pos所在的位置是否为第一个节点、
	//if (pos == *pplist)
	//{
	//	//pos所在位置是第一个节点的位置、
	//	//头删即可、
	//	SListPopFront(pplist);
	//}
	//else
	//{
	//	//pos所在位置不是第一个节点的位置、
	//	SLTNode* prev = *pplist;
	//	while (prev->next != pos)
	//	{
	//		prev = prev->next;
	//	}
	//	//无论pos所在位置后面有节点,还是没有节点的情况均适用、
	//	prev->next = pos->next;
	//	free(pos);
	//	pos = NULL;
	//}
}




//销毁单链表(释放空间)、
void SListDestroy(SLTNode** pplist)
{
	//即使链表为空链表,即结构体指针变量plist为空指针NULL,但是结构体指针变量plist的 地址 一定不是空指针NULL,并且该结构体指针变量
	//plist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*pplist相当于对空指针NULL进行解引用,会出现错误、
	assert(pplist);

	//当链表为空链表时,链表中没有节点,就不需要对节点进行释放,此时*pplist为空指针NULL,所以指针变量cur也是空指针NULL,不进入循环,
	//再把空指针NULL赋值给*pplist,程序结束、

	//注意,此时不要对*pplist进行断言,若为空链表时不需要直接报错,只是不对任何节点进行释放空间即可,只需要使得*pplist置为空指针NULL即可,但不需要直接报错、

	//链表一个一个的对节点进行释放、
	SLTNode* cur = *pplist;
	while (cur != NULL)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//在调用函数内部改变了*pplist的值,所以选择 传址 调用、
	*pplist = NULL;
}

3.3.3、SList.h头文件:

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

//存储数据的载体叫做节点,一个节点中至少要有一个数据域和一个指针域,前一个节点里的指针域中的指针变量指向下一个节点、

//单链表、
//在顺序表中,一上来就直接有一个结构体,即使顺序表中一个值也没有,那么也要存在一个结构体,结构体中的结构成员变量的值都是随机值
//所以要对其进行初始化,而对于链表而言,在最初的时候,如果是空链表,即没有一个节点时,此时是没有结构体的,只有一个指针变量,当需要往
//链表中链接节点的时候,就需要创建新的节点(结构体)并对其结构体成员变量进行赋值,所以,链表不需要进行初始化、

//单向不带头不循环
//定义一个节点、
typedef int SLTDataType;

typedef struct SListNode
{
	//数据域
	SLTDataType data;
	//指针域,该节点中指针域中的指针变量指向下一个节点,一个节点就是一个结构体、
	struct SListNode* next;//在这里一定不要把struct SListNode*写成SLTNode*、
}SLTNode; 

//链表也是线性表,也要关注增删查改、

//尾插
void SListPushBack(SLTNode** pplist, SLTDataType x);


//头插
void SListPushFront(SLTNode** pplist, SLTDataType x);


//尾删
void SListPopBack(SLTNode** pplist);



//头删
void SListPopFront(SLTNode** pplist);


//打印
void SListPrint(SLTNode* plist);


//查找附加修改、
SLTNode* SListFind(SLTNode* plist, SLTDataType x);


//单链表在pos位置之 后 插入x、			
void SListInsertAfter(SLTNode* pos, SLTDataType x);


单链表在pos位置之 前 插入x、
//void SListInsertBefore(SLTNode* pos, SLTDataType x);

//单链表在pos位置之 前 插入x、
void SListInsertBefore(SLTNode** pplist,SLTNode* pos, SLTDataType x);


// 单链表删除pos所在位置之后的节点、
void SListEraseAfter(SLTNode* pos);


// 单链表删除当前pos所在位置的节点、
void SListEraseCur(SLTNode** pplist,SLTNode* pos);


//销毁单链表
void SListDestroy(SLTNode** pplist);



双链表、
定义一个节点、
//struct ListNode
//{
//	//数据域
//	int data;
//	//指针域,该节点中指针域中的指针变量指向下一个节点,一个节点就是一个结构体、
//	struct SListNode* next;
//	//指针域,该节点中指针域中的指针变量指向上一个节点,一个节点就是一个结构体、
//	struct SListNode* prev;
//};


3.4、链表面试题:

3.4.1、链表面试题一:

删除 链表中等于给 定值 val 的所有节点 力扣
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

//无头链表,头节点即指链表中第一个节点,即第一个有效节点、
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode*prev=NULL;
    struct ListNode*cur=head;
    while(cur)
    {
        if(cur->val!=val)
        {
            prev=cur;
            cur=cur->next;
        }
        else
        {
            struct ListNode*next=cur->next;
            if(prev==NULL)
            {
                //不是常规情况,头节点中存储的是val值,无论在链表中存储val值的节点是不是连续的,均适用、
                //相当于头删、
                //此时,头指针的值发生了改变,为什么不使用传址调用呢,这是因为,该调用函数返回值为新的头结点的地址,所以在链表中当在调用函数内部改变了头指针的话,要想使得调用函数外部的头指针的值随之改变,要么是用传址调用,要么使用传值调用再把新的头节点的地址返回来即可,但是后者通常不使用,一般使用的是前者、
                //若使用后者,则不管头指针有没有在调用函数内部发生改变,都以返回来的新头节点的地址为主即可、
                head=next;
                free(cur);
                //不要看到free(cur)就直接把cur置为空指针NULL,要考虑周全、
                cur=next;
            }
            else
            {
                //常规情况,头节点中存储的不是val值,无论在链表中存储val值的节点是不是连续的,均适用、
                //方法一:
                free(cur);
                //不要看到free(cur)就直接把cur置为空指针NULL,要考虑周全、
                prev->next=next;
                cur=next;
                //cur=prev->next;

                //方法二:
                /*prev->next=cur->next;
                free(cur);
                //不要看到free(cur)就直接把cur置为空指针NULL,要考虑周全、
                cur=prev->next;*/
            }
        }
    }
    return head;
}



//在VS下的测试代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>

struct ListNode
{
	int val;
	struct ListNode *next;
};

//无头链表,头节点即指链表中第一个节点,即第一个有效节点、
struct ListNode* removeElements(struct ListNode* head, int val)
{
	struct ListNode*prev = NULL;
	struct ListNode*cur = head;
	while (cur)
	{
		if (cur->val != val)
		{
			prev = cur;
			cur = cur->next;
		}
		else
		{
			struct ListNode*next = cur->next;
			if (prev == NULL)
			{
				//不是常规情况,头节点中存储的是val值,无论在链表中存储val值的节点是不是连续的,均适用、
				//相当于头删、
				//此时,头指针的值发生了改变,为什么不使用传址调用呢,这是因为,该调用函数返回值为新的头结点的地址,所以在链表中当在调用函数内部改变了头指针的话,要想使得调用函数外部的头指针的值随之改变,要么是用传址调用,要么使用传值调用再把新的头节点的地址返回来即可,但是后者通常不使用,一般使用的是前者、
				//若使用后者,则不管头指针有没有在调用函数内部发生改变,都以返回来的新头节点的地址为主即可、
				head = next;
				free(cur);
				//不要看到free(cur)就直接把cur置为空指针NULL,要考虑周全、
				cur = next;
			}
			else
			{
				//常规情况,头节点中存储的不是val值,无论在链表中存储val值的节点是不是连续的,均适用、
				//方法一:
				free(cur);
				//不要看到free(cur)就直接把cur置为空指针NULL,要考虑周全、
				prev->next = next;
				cur = next;
				//cur=prev->next;

				//方法二:
				/*prev->next=cur->next;
				free(cur);
				//不要看到free(cur)就直接把cur置为空指针NULL,要考虑周全、
				cur=prev->next;*/
			}
		}
	}
	return head;
}
int main()
{
	//以不通过的测试用例为例:
	struct ListNode* node1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* node2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* node3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* node4 = (struct ListNode*)malloc(sizeof(struct ListNode));

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	node1->val = 7;
	node2->val = 7;
	node3->val = 7;
	node4->val = 7;

	//把调用函数的返回值,即新节点的地址放在node1中,此时node1中放的就是返回来的新节点的地址,然后拿着该地址再去实现下面的函数、
	node1 = removeElements(node1, 7);

	//
	//
	//.....
	return 0;
}

3.4.2、链表面试题二:

反转一个单链表, 力扣
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 //方法一:三个指针反转方向:
//若想要改变形参从而改变实参的话,即改变头指针的位置,可以传址调用也可以把新的头节点的地址返回即可、
/*struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
    {
        //空链表、
        return NULL;
    }
    else
    {
        //非空链表、
        struct ListNode*n1=NULL;
        struct ListNode*n2=head;
        struct ListNode*n3=n2->next;
        while(n2)
        {
            n2->next=n1;
            n1=n2;
            n2=n3;
            if(n3)
            n3=n3->next;
        }
        return n1;
    }   
}*/

//方法二:头插法:
//要求在原链表中进行反转,而不是创建一个新的链表、
//此处所谓的头插即把原链表中的节点拿来头插,而不是创建新的节点进行头插、
//若使用冒泡降序排序的来交换节点中的数值的话可以达到目的但是时间复杂度过高,是O(N^2),所以不采用该方法、
//遍历原链表,把每个节点都拿出来进行头插、
//定义指针变量cur指向原链表中的头节点,再定义一个指针变量newHead作为反转后的链表的头指针,但是该指针先指向反转后的链表的尾,最后到达反转后的链表的头部即可,所以指针变量newHead初始化为NULL、
//指针变量cur取原链表的节点头插到指针变量newHead上即可,最终形成反转后的链表、
/*struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
    {
        return NULL;
    }
    else
    {
        struct ListNode*newHead=NULL;
        struct ListNode*cur=head;
        //先保存原链表中指针变量cur所在位置的下一个节点的地址、
        struct ListNode*next=cur->next;
        while(cur)
        {
            //头插
            cur->next=newHead;
            newHead=cur;
            cur=next;
            if(cur)
            next=cur->next;
        }
        return newHead;
    }
}*/


struct ListNode* reverseList(struct ListNode* head)
{
    //此种方法不考虑是否为空链表的情况、
    struct ListNode*newHead=NULL;
    struct ListNode*cur=head;
        while(cur)
        {
            //先保存原链表中指针变量cur所在位置的下一个节点的地址、
            struct ListNode*next=cur->next;
            //头插
            cur->next=newHead;
            newHead=cur;
            cur=next;
        }

        return newHead;
}

3.4.3、链表面试题三:

给定一个带有头结点 head 的非空单链表,返回链表的中间结点,如果有两个中间结点,则返回第二个中间结点, 力扣
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

//若为1 2 3 4 5 6 其中,3和4都叫做中间节点,3叫做上中位数,4叫做下中位数、
//附加要求:只能遍历一遍链表、

//方法一:
//当链表中节点个数为奇数时,有唯一的中间节点,如:1 2 3 4 5 ,此时节点个数为5个,在程序中,5/2=2,,所以中间节点所在位置的下标即为2、
//当链表中的节点个数为偶数时,此时中间节点个数为两个,但是要取两者中后面的节点作为中间节点,如:1 2 3 4 5 6,此时节点个数为6个,在程序中,6/2=3,所以,中间节点所在位置的下标即为3、
//奇数:第一次遍历求出来链表中节点的个数,然后用该个数除以2得到2,然后再进行遍历链表找出来下标为2的节点即为中间节点、
//偶数:第一次遍历求出来链表中节点的个数,然后用该个数除以2得到3,然后再进行遍历链表找出来下标为3的节点即为中间节点、
//上述方法虽然能够行得通,但是对链表遍历了两边,若要求只能对链表遍历一遍的话,就不可以采用该方法、

//方法二:快慢指针
//定义两个指针变量,分别为:slow,fast,这两者在最初都指向链表的头节点,区别是,指针变量slow一次走一步,指针变量fast一次走两步,fast的速度是slow的二倍,若节点个数为奇数个,当fast走到最后一个节点时,slow所在位置就是中间节点,其原理就是,fast的速度是slow的二倍,当fast走到最后一个节点时,此时中间节点就是slow所在的位置、
//当节点个数为偶数时,当fast走完所有的节点到NULL所在的位置时,此时,slow所在位置就是中间节点、
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode*slow,*fast;
    slow=head;
    fast=head;
    //错误写法
    //while(fast->next && fast)
    //上述写法中,先执行&&之前的语句,为真则执行后面的,为假直接停止,后面的不计算,若节点为偶数个的话,当fast到达NULL位置时,fast->next 程序先执行&&前面的语句,此时就崩溃了,只能写成下面这样、
    //当写成下面时,fast又作为了fast->next的前提,当fast为真时,执行&&后面的语句,此时,fast为真,则说明fast一定不是空指针NULL,然后再执行fast->next就不会出错了、
    while(fast && fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

3.4.4、链表面试题四:

输入一个链表,输出该链表中 倒数第k个 结点, 链表中倒数第k个结点_牛客题霸_牛客网

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 *
 * C语言声明定义全局变量请加上static,防止重复定义
 */

/**
 * 
 * @param pListHead ListNode类 
 * @param k int整型 
 * @return ListNode类
 */

//方法一:
//先遍历一遍链表求出来链表中节点的个数,假设为N个,已知输入的数值为K,则再遍历一遍链表找到下标为N-K的
//位置就是所求的结果,但是这种方法也要遍历两边链表,所以不采用该方法、

//方法二:
//采用变形的快慢指针法,假设K=2,让fast先走K步,然后slow和fast再同时走,当fast走到NULL所在的位置时,
//此时slow所在的位置就是所求的节点、
//或者先让fast先走K-1步,然后slow和fast再同时走,当fast走到最后一个节点的位置时,此时slow所在的位置
//就是所求的位置,两种方法不同之处就是判断停止的条件不同、
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    struct ListNode*slow,*fast;
    slow=fast=pListHead;
    //fast先走K步
    while(k--)
    {
        if(fast == NULL)
        {
          //K大于链表的节点个数、
            return NULL;
        }
          //k小于等于链表节点个数、
        fast=fast->next;
    }
    
    while(fast)
    {
         slow=slow->next;
         fast=fast->next;
    }
    return slow;
}
//牛客使用的gcc编译器、

3.4.5、链表面试题五:

两个有序链表合并为一个新的有序链表并返回, 新链表是通过拼接给定的两个链表的所有节点组成的, 力扣
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

//在链表中,只要把两个有序的链表合并成一个有序的链表,思路都是一样的, 即,每次取存储的数据较小的节点下来进行尾插到新链表中即可、

//假设两个链表的长度都是N,如果要进行尾插,首先要进行找尾,当尾插第一个节点时,遍历节点0次,当尾插第二个节点时,遍历节点一次,...当尾插第2N个节点时,遍历节点2N-1次,执行次数为:1+2+3+...+2N-1,,等差数列,这只是找尾所需要的执行次数,再遍历两个链表时还需要执行2N次,所以时间复杂度为:O(N^2),如果要求时间复杂度为:O(N)的话,再定义一个尾指针遍历tail,这样就不需要每次进行找尾了,现在有2N个值拿下来进行尾插,则时间复杂度就是:O(N),满足要求、
//现在两个指针变量list1和list2分别指向两个链表的头节点,进行比较两个头节点中的数据大小,把小的拿到新链表中进行尾插,相等的话就随便哪一个即可,然后list往后走一个节点,再次比较大小,继续把小的拿到新链表中去尾插,再更新tail,然后对应的list再往后找一个节点,当list1和list2其中一个为空指针NULL时,循环结束,再把另外一个list所指的节点以及后面的所有的节点直接拷贝到新链表中即可,这是因为两个链表在最初就已经排序结束了,不需要再一个个节点就行拷贝了,直接把更新后的尾指针与这个节点的地址链接起来即可,此时tail就不需要再更新了,记录tail只是用来方便尾插的,当尾插结束时,不需要返回tail的值,所以,不需要再更新tail了,不需要返回新的尾指针、\

//本题中只有尾插,记录尾指针就可以解决找尾的麻烦,但是在单链表的实现中,就算有尾指针tail记录也是解决不了尾删的问题,因为不能倒着找地址,还要从头开始找,比较麻烦,和这里不一样,单链表中不止有尾插,还有尾删等等操作、
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    if(list1==NULL)
        return list2;
        //若链表2为空链表的话,则list2==NULL,所以返回空指针NULL,说明两个空链表合并还是空链表,返回空指针NULL正确,若链表2不是空链表的话,则list2不是空指针,所以空链表1和非空链表2合并后还是非空链表2,返回链表2的头指针即可,符合、
    if(list2==NULL)
        return list1;
        //若链表1为空链表的话,则list1==NULL,所以返回空指针NULL,说明两个空链表合并还是空链表,返回空指针NULL正确,若链表1不是空链表的话,则list1不是空指针,所以空链表2和非空链表1合并后还是非空链表1,返回链表1的头指针即可,符合、

    //程序到此则说明list1和list2都不是空指针NULL、

    //当list1和list2中若一个为空指针NULL,另外一个不是空指针的话,则while都不进入,并且while下面的两个if语句,一个进入,一个不进入,不管那个进入,都会有tail->next的操作,而不进while的话,tail==NULL,所以会崩溃、
    //头指针、
    struct ListNode* head=NULL;
    //尾指针、
    struct ListNode* tail=NULL;
    //当两者都不是NULL时,循环继续,如果有一个等于NULL,则循环结束、
    while(list1 && list2)
    {
        //比较大小、
        if(list1->val < list2->val)
        {
            //取较小值进行尾插、
            //当新链表为空链表时,此时,tail==NULL,head==NULL,两者都是空指针,一旦新链表中存在节点时,head和tail都不是空指针NULL了,所只对两者中其一进行判断即可、
            if(tail==NULL)
            {
                //新链表为空链表时、
                head=tail=list1;
            }
            else
            {
                //新链表中存在节点、
                tail->next=list1;
                //更新tail尾指针、
                tail=list1;
            }
            list1=list1->next; 
        }
        else
        {
            //当两者数值相等时,随便取一个即可,所以相等在情况在else内部、
            //取较小值进行尾插、
            //当新链表为空链表时,此时,tail==NULL,head==NULL,两者都是空指针,一旦新链表中存在节点时,head和tail都不是空指针NULL了,所只对两者中其一进行判断即可、
             if(tail==NULL)
            {
                //新链表为空链表时、
                head=tail=list2;
            }
            else
            {
                //新链表中存在节点、
                tail->next=list2;
                //更新tail尾指针、
                tail=list2;
            }
            list2=list2->next;
        }
    }
    //到此为止,while循环结束,说明list1和list2其中一个为空指针NULL,不可能都不为空指针NULL、
    //两者也不可能同时都为空指针,因为程序中对于list1和list2都是单路遍历,没有同时往后移动,所以两者不可能同时为空指针,也不可能同时都不为空指针,程序到此,一定是一个为空,一个不为空、
    if(list1)
    {
        //此时list2等于NULL,新链表要把list1所在位置的节点及后面的节点都要链接到新链表内、
        tail->next=list1;
    }
    if(list2)
    {
        //此时list1等于NULL,新链表要把list2所在位置的节点及后面的节点都要链接到新链表内、
        tail->next=list2;
    }
    return head;
}



//上述代码中,合并后的新链表使用的是 不带头 链表方法,若改写成 带头链表方法 则应是:
//由于题目中没有明确告诉链表1和链表2以及合并后的链表是带头链表,所以则默认这三者都是不带头链表,此时题目要求把合并后的新链表的头指针返回去,则该题有两种方法,其一就是上述代码,要注意的是,在此所谓的带不带头主要是针对于合并后的新链表是否带头,若采用不带头链表,即为上述例子,合并后返回的是head,但是若采用带头链表的话,此时head指向的是新链表中的哨兵位的头节点,而题目要求返回链表是不带头的,所以,就不能直接把head返回去,而是要把head->next返回即可、
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    //头指针、
    struct ListNode* head=NULL;
    //尾指针、
    struct ListNode* tail=NULL;
    //合并后的新链表带哨兵位、
    head=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
    head->next=NULL;
    while(list1 && list2)
    {
        //比较大小、
        if(list1->val < list2->val)
        {
            //取较小值进行尾插、
            //此时就不需要判断新链表是否为空链表了,因为有哨兵位的头节点存在,所以新链表不可能为空链表,所以此时的head和tail一定不是空指针NULL,直接把从两个要合并的链表中拿下来的存储有效数据的节点链接到哨兵位的头节点后面即可、
            tail->next=list1;
            //更新tail尾指针、
            tail=list1;
            list1=list1->next; 
        }
        else
        {
            //当两者数值相等时,随便取一个即可,所以相等在情况在else内部、
            //取较小值进行尾插、
            tail->next=list2;
            //更新tail尾指针、
            tail=list2;
            list2=list2->next;
        }
    }
    //到此为止,while循环结束,说明list1和list2其中一个为空指针NULL,不可能都不为空指针NULL、
    //两者也不可能同时都为空指针,因为程序中对于list1和list2都是单路遍历,没有同时往后移动,所以两者不可能同时为空指针,也不可能同时都不为空指针,程序到此,一定是一个为空,一个不为空、
    if(list1)
    {
        //此时list2等于NULL,新链表要把list1所在位置的节点及后面的节点都要链接到新链表内、
        tail->next=list1;
    }
    if(list2)
    {
        //此时list1等于NULL,新链表要把list2所在位置的节点及后面的节点都要链接到新链表内、
        tail->next=list2;
    }
    //在此如果直接释放head,那么就找不到合并后的新链表中的第一个存储有效数据的节点的地址了,其地址会被置为随机值,先保存该地址再释放动态开辟的内存空间、
    struct ListNode* list=head->next;
    free(head);
    head=NULL;
    return list;
    //该方法无论list1和list2同时为空指针NULL,或者两者一个为空指针,另外一个不是空指针,或者两者都不为空的情况都是满足的、
}

链表的OJ题中,如果不是指明链表是带头的,则就默认其为不带头链表、

3.4.6、链表面试题六:

编写代码,以给定值 x 为基准将链表分割成两部分,所有小于 x 的结点排在大于或等于 x的结点之前, 链表分割_牛客题霸_牛客网

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        //没有特别指明链表是带头链表这就默认为是无头链表、
        //例如:链表为:3  5  1  6  3  4,x=4,,,则执行完后的结果为: 3  1  3  5  6  4、
        //遍历原链表,把小于x的节点按照相对不变的顺序插入到链表1,把大于等于x的节点按照相对不变的顺序
        //插入到链表2,再把链表1和链表2链接起来、
        //操作链表1时,定义两个指针变量,其中lessHead作为链表1的头指针,由于要把小于x的节点按照相对不变的顺序插入到链表1
        //要进行尾插,所以定义一个变量lessTail来作为链表1的尾指针,避免每次尾插时找尾,在此操作链表1时使用的是带头链表、
        //操作链表2时,定义两个指针变量,其中greaterHead作为链表2的头指针,由于要把大于等于x的节点按照相对不变的顺序插入到链表2
        //要进行尾插,所以定义一个变量greaterTail来作为链表2的尾指针,避免每次尾插时找尾,在此操作链表2时使用的是带头链表、
        //最后再把两个链表连接起来,即lessTail->next=greaterHead->next,返回lessHead即可、
        struct ListNode*lessHead,*lessTail,*greaterHead,*greaterTail;
        lessHead=lessTail=(struct ListNode*)malloc(sizeof(struct ListNode));
        //lessHead->next=NULL;
        greaterHead=greaterTail=(struct ListNode*)malloc(sizeof(struct ListNode));
        //greaterHead->next=NULL;
        lessTail->next=greaterTail->next=NULL;
        struct ListNode*cur=pHead;
        //由于最后返回新链表的头指针,所以在此传值调用即可、
        while(cur)
        {
            if(cur->val < x)
            {
                lessTail->next=cur;
                lessTail=cur;
                //lessTail=lessTail->next;
            }
            else
            {
                greaterTail->next=cur;
                greaterTail=cur;
                //greaterTail=greaterTail->next;
            }    
            cur=cur->next;
        }
        lessTail->next=greaterHead->next;
        greaterTail->next=NULL;
        struct ListNode*list=lessHead->next;
        free(lessHead);
        lessHead=NULL;
        free(greaterHead);
        greaterHead=NULL;
        return list;
    }
    
    //当原链表中最后一个节点存储的数据大于等于x时,此时,该节点会被分配到链表2中,再把链表1和链表2链接起来后,那么
    //该节点就成了新链表的最后一个节点,又因为在原链表中,该节点中的指针指向了NULL,所以新链表中最后一个节点就
    //相当于默认指向了空指针NULL、
    //当原链表中最后一个节点存储的数据小于x时,此时,该节点会被分配到链表1中,再把链表1和链表2链接起来后,那么
    //该节点在新链表中就不是最后一个节点了,比如:原链表为: 3  5  1  6  3  ,,x=4,,此时:
    //链表1:  3  1  3                  原链表:3  5  1  6  3  NULL、  x=4、
    //链表2:  5  6
    //那么新链表中最后一个节点存储的数据是6,当把链表1和2串起来时,发现新链表中最后一个节点中的指针
    //指向了链表1中的最后一个3,这是因为在原链表中节点6就指向了原链表中最后一个节点3,所以这就造成了死循环,
    //所以当出现这种情况时要手动将greaterTail->next置为空指针NULL,若要是这样的话,还要进行判断原链表中最后一个数据和x之间的
    //关系,为了更加方便,不管原链表中最后一个数据和x是什么样的关系,都要手动把greaterTail->next置为空指针NULL即可、
    //即新链表中的最后一个节点,不管该节点中的指针在原链表中是否指向空指针NULL,都再次重新手动置空、
    
    //内存超限的情况一般都是造成死循环了、
    
    //1、所有的值都比x小、
    //2、所有的值都比x大、
    //3、比x大和小的都有,最后一个值比x大、
    //4、比x大和小的都有,最后一个值比x小、

3.4.7、链表面试题七:

所谓 链表的回文结构就是链表的对称结构,即对链表的 正反读取的结果都是一样的,若是 双向链表或者是数组的话,就会比较简单,即左端和右
端各给一个指针,分别比较两指针所指数据是否相同,依次向内部递进,主要是因为 双向链表结构和数组结构既可以正着走也可以倒着走,但是
对于 单链表而言只能正着走而不能倒着走、
对于该题而言,已知链表长度小于等于900,可以直接定义一个元素个数为900的数组,遍历链表,把链表中的数据都放进数组中去,然后再通过
数组进行判断是否为回文结构,由于直接开辟了 常数个空间,则 空间复杂度就是:O(1),满足题目要求,该方法在牛客下是可以通过的,但是方
法比较投巧,如果题目不告诉链表长度小于等于900,仍按照该方法来实现的话,该方法是不符合题意的, 链表多长就应该开辟多大的数组,则
需要遍历链表得出链表的长度,然后根据该长度动态开辟数组,但是由于链表在每次执行用例中都不相同,即不清楚链表的长度,所以假设链表
长度为N,则动态开辟数组时也要开辟N个空间,所以 空间复杂度就是:O(N),不符合题目要求,
思路:
先找到原链表的 中间节点,然后从中间节点进行 逆置,同时定义指针变量rHead作为逆置后的新链表的头指针,然后指针变量A和rHead分别往后
遍历,如果对应的 数值不相等则代表是不回文结构,return false当两个指针变量有一个指向了空指针NULL,则代表是回文结构,返回true、
不管原链表中的节点个数是奇数个还是偶数个,都是适用的、
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
    //寻找中间节点、
    struct ListNode* middleNode(struct ListNode* head)
    {
        struct ListNode*slow,*fast;
        slow=head;
        fast=head;
        //错误写法
        //while(fast->next && fast)
        //上述写法中,先执行&&之前的语句,为真则执行后面的,为假直接停止,后面的不计算,若节点为偶数个的话,当fast到达NULL位置时,fast->next 程序先执行&&前面的语句,此时就崩溃了,只能写成下面这样、
        //当写成下面时,fast又作为了fast->next的前提,当fast为真时,执行&&后面的语句,此时,fast为真,则说明fast一定不是空指针NULL,然后再执行fast->next就不会出错了、
        while(fast && fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;
        }
            return slow;
    }
    
    //逆置链表、
    struct ListNode* reverseList(struct ListNode* head)
    {
        //此种方法不考虑是否为空链表的情况、
        struct ListNode*newHead=NULL;
        struct ListNode*cur=head;
        while(cur)
            {
                //先保存原链表中指针变量cur所在位置的下一个节点的地址、
                struct ListNode*next=cur->next;
                //头插
                cur->next=newHead;
                newHead=cur;
                cur=next;
            }
        return newHead;
}
    
    bool chkPalindrome(ListNode* A) 
    {
        //寻找中间节点、
        struct ListNode* mid=middleNode(A);
        //逆置链表、
        struct ListNode* rHead=reverseList(mid);
        //比较、
        //当两个指针变量有一个指向了空指针NULL,则是回文结构、
        while(A && rHead)
        {
            if(A->val == rHead->val)
            {
                //相等则继续往后走、
                A=A->next;
                rHead=rHead->next;
            }
            else
            {
                //不是回文结构、
                return false;  //0、
            }
        }
        return true;  //1、
        //bool类型使用%d进行打印、
    }
};
//在vs2019下,如果某一个指针为空指针NULL,若没有对该指针进行检查,直接进行使用则就会报错、
//在寻找中间节点的算法中,假设链表长度为N,由于快指针一次走两个节点,所以执行次数就是N/2,此时执行次数只需要看
//fast即可,因为总是fast先到达空指针NULL,即fast控制着循环结束,逆置链表时需要遍历原链表,则执行次数为N,而在最后的
//判断部分执行次数大概为N/2,则总的执行次数为2N,所以时间复杂度就是:O(N),空间复杂度就是:O(1),满足要求、

3.4.8、链表面试题八:

输入两个链表,找出它们的第一个公共结点, 力扣

 思路一:

依次比较,拿着 链表1中的第一个节点的地址 分别与 链表2中的每一个节点的地址进行比较 ,看进行比较的两个地址是否相同,然后再拿着链表1
中的第二个节点的地址分别与链表2中的每一个节点的地址进行比较,看进行比较的两个地址是否相同,直到拿着链表1中的 最后一个节点的地址
分别与链表2中的每一个节点的地址进行比较,看进行比较的两个地址是否相同 ,在此过程中, 若相同则该地址就是相交节点的地址 ,当链表1中
所有节点的地址都与链表2中的每一个节点的地址进行比较,并且都不相等的话,则代表两个链表不相交,即,链表1中的每个节点的地址都去跟
链表2中每一个节点的地址进行比较,若存在相等,则就相交, 第一个相等的地址就是第一个交点的地址 ,因为 可能存在多个交点,在此只取第
一个交点即可 ,假设链表1的长度为M,链表2的长度为N,考虑 最坏 的情况下,即 比较完全部的情况 ,则时间复杂度就是:O(M*N),即,当链表
1中的第一个节点与链表2中每一个节点进行比较时,要循环N次,链表1中第二个,第三个直到第M个节点与链表2中的每一个节点进行地址比较
时,循环次 次,所以执行次数就是M个N相加,则执行次数就是M*N次, 时间复杂度就是:O(M*N),空间复杂度就是:O(1) ,该方法 不仅能够判
断出来两个链表是否相交,还能够得出来两个链表第一个交点的地址 ,要注意, 此时比较的是地址,不可以使用val值进行比较,因为不同节点中
可能存储的val值是相等的、
思路二:
求出 两个链表的长度 ,分别为 L1和L2 ,在 求两个链表的长度时,就可以分别找到两个链表的尾节点 ,若这两个尾节点的地址是 相同 的,则两个链
表是 相交 的, 若不相同,则是不相交的 每一个节点的地址是唯一的,不可能两个节点的地址是一样的 ,,当两个链表的 尾节点 的地址是 相同
时候,再让长的链表 先走L1-L2的绝对值 步, 再一起走,找交点 ,第一个相等的地址就是链表的第一个交点,即所求的交点,,,在该方法中,
分别求链表的长度时, 要遍历两个链表,则执行次数为M+N次 ,然后先让长链表走,再一起走,又是M+N次,所以执行次数为2*(M+N),则 时间
复杂度就是:O(M+N),空间复杂度就是:O(1), 注意, 不可以逆置,若是相交的链表,逆置的话,会出现一对多,即某一个节点指向了后面两
个节点的情况 ,这是肯定不对的,因为每一个节点 后指针域 中只能存储一个地址、
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    //不要使用头指针headA,headB来迭代,当找到尾之后再进行两个节点地址比较时,还要从头开始,这样就找不到头节点的位置了,所以不要直接使用headA和headB进行迭代、
    struct ListNode* tailA=headA;
    struct ListNode* tailB=headB;
    //计算两个链表的长度、
    //在此不考虑两个链表为空链表的情况,即默认两个链表中都至少有一个节点、
    int lenA=1;
    int lenB=1;
    //找尾、
    while(tailA->next)
    {
        tailA=tailA->next;
        lenA++;
    }
    while(tailB->next)
    {
        tailB=tailB->next;
        lenB++;
    }
    if(tailA != tailB)
    {
        //两个链表不相交、
        return NULL;
    }
    else
    {
        //两个链表相交,继续找交点、
        //长链表先走差距步,再同时走找交点、

        /*struct ListNode*longList=NULL;
        struct ListNode*shortList=NULL;
        if(lenA > lenB)
        {
            //链表A比链表B长、
            longList=headA;
            shortList=headB;
        }
        else
        {
            //链表A的长度小于等于链表B的长度、
            longList=headB;
            shortList=headA;
        }*/
               
        //假设链表A短,链表B长,同时当链表A和B的长度相等时,也按照A短B长的代码来即可,即当链表A的长度小于等于链表B时、
        struct ListNode* shortList=headA;
        struct ListNode* longList=headB;
        if(lenA > lenB)
        {
            //链表A比链表B长、
            longList=headA;
            shortList=headB;
        }

        //求两个链表长度的差距的值、
        int gap=abs(lenA-lenB);//求绝对值、
        //让长链表先走gap步、
            while(gap--)
            {
                longList=longList->next;
            }
            //同时走、
            //while(longList)
            //while(shortList)
            //因为longList和shortList肯定同时到达NULL,所以即使只判断其中一个也是可以的、
            while(longList && shortList)
            {
                if(shortList == longList)
                {
                   //return shortList;
                    return longList;
                }
                shortList=shortList->next;
                longList=longList->next;
            }
    }
    //理论上讲,在此不写return也是对的,但是编译器编译的是语法逻辑,它不关心具体的执行逻辑,编译器不能分析语法的具体执行,他站在语法逻辑的角度去看,我们已知代码肯定会从上面的两个if语句中进行返回,即在执行逻辑中,肯定会从上面的两个if语句中进行返回,所以,理论上代码不会执行下面的return,这是站在执行逻辑去看的,但是编译器是站在语法逻辑的角度来看,他会认为上面用来返回的两个if语句不一定会进入其中,if语句只是用来判断的,也有不满足条件而不进入if语句的情况,当不进入上面用来返回的两个if语句时,代码走到此处如果没有return返回的话,语法上就错了,所以,即使这里的return实际上用不到,也要写上,随便返回一个值就行了,要注意与返回类型相对应,,防止编译器报错、
    return NULL; 
}

3.4.9、链表面试题九:

给定一个 链表 ,判断链表中 是否有环 力扣

若链表中最后一个节点中的指针域中存储的为第一个节点的地址,则称为循环链表,循环链表也属于带环链表、

对于循环链表而言,定义头指针head,再把该头指针赋值给指针变量cur,现在使cur逐次遍历,使用do-while循环,在指针变量cur不等于空指针

NULL的情况下,如果,cur等于head时,则遍历完一圈了,并且是循环链表,所以他属于带环链表,像循环链表这种情况而言,在循环内部肯定

能够使得cur等于head如果当cur等于空指针NULL时,则链表即为不带环链表

当链表中的最后一个节点中的指针指向了该链表其他位置的的一个节点,包括最后一个节点指向了本身时而不是头节点的时候,就不再是循环

链表了,但是属于带环链表、

我们应该怎么判断该链表是否带环呢?

此时不可以像判断循环链表那样,通过遍历来与某一个节点的地址进行比较,这是因为,不清楚该节点是否在环内,如果在环内,可以通过该节

点的地址等于cur得出来属于带环链表,但是如果不在环内的话,即使是带环链表,也不能够判断出来,会造成死循环、

思路:

快慢指针慢指针一次走一步,快指针一次走两步,两个指针都从链表的起始位置开始运行,如果链表带环,则一定会在环内相遇,否则,快指

针则率先走到链表的末尾、

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {
    struct ListNode * slow,*fast;
    slow=fast=head;
    //当链表不带环时,链表中节点的个数可能为奇数个,也可能为偶数个、
    while(fast && fast->next)
    {
        //当在循环内部时,slow一次走一步,fast一次走两步的情况下,一定可以追的上、
        slow=slow->next;
        fast=fast->next->next;
        if(slow == fast)
        {
            //带环链表、
            return true;
        }
    }
    //不带环、
    //带环链表时,fast和fast->next都不可能等于空指针NULL,程序到此处则肯定是不带环链表、
    return false;    
}

 ​​​

3.4.10、链表面试题十:

给定一个链表,返回链表开始入环的第一个节点,如果链表无环,则返回 NULL, 力扣
结论:
让一个指针从链表 起始位置 开始遍历链表,同时让一个指针从 判环时相遇点的位置 开始绕环运行,两个指针都是 每次均走一步 ,最终肯定会在环
入口点的位置相遇、

方法一: 

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head)
{
    struct ListNode * slow,*fast;
    slow=fast=head;
    //当链表不带环时,链表中节点的个数可能为奇数个,也可能为偶数个、
    while(fast && fast->next)
    {
        //当在循环内部时,slow一次走一步,fast一次走两步的情况下,一定可以追的上、
        slow=slow->next;
        fast=fast->next->next;
        if(slow == fast)
        {
            //带环链表、
            struct ListNode * meet=fast;
            //struct ListNode * meet=slow;
            while(meet!=head)
            {
                //每一次走一步、
                meet=meet->next;
                head=head->next;
            }
            return head;
            //return meet;
        }
    }
    //不带环、
    //带环链表时,fast和fast->next都不可能等于空指针NULL,程序到此处则肯定是不带环链表、
    return NULL;    
}

  方法二:

struct ListNode *detectCycle(struct ListNode *head)
{
    struct ListNode * slow,*fast;
    slow=fast=head;
    //当链表不带环时,链表中节点的个数可能为奇数个,也可能为偶数个、
    while(fast && fast->next)
    {
        //当在循环内部时,slow一次走一步,fast一次走两步的情况下,一定可以追的上、
        slow=slow->next;
        fast=fast->next->next;
        if(slow == fast)
        {
            //带环链表、
            struct ListNode* meet=fast;
            //struct ListNode * meet=slow;
            struct ListNode* headB=meet->next;
            meet->next=NULL;
            struct ListNode *headA=head;
            struct ListNode* tailA=headA;
            struct ListNode* tailB=headB;
            //计算两个链表的长度、
            //在此不考虑两个链表为空链表的情况,即默认两个链表中都至少有一个节点、
            int lenA=1;
            int lenB=1;
            //找尾、
            while(tailA->next)
            {
                tailA=tailA->next;
                lenA++;
            }
            while(tailB->next)
            {
                tailB=tailB->next;
                lenB++;
            }
            if(tailA != tailB)
            {
                //两个链表不相交、
                return NULL;
            }
            else
            {
                //两个链表相交,继续找交点、
                //长链表先走差距步,再同时走找交点、

                /*struct ListNode*longList=NULL;
                struct ListNode*shortList=NULL;
                if(lenA > lenB)
                {
                    //链表A比链表B长、
                    longList=headA;
                    shortList=headB;
                }
                else
                {
                    //链表A的长度小于等于链表B的长度、
                    longList=headB;
                    shortList=headA;
                }*/
                    
                //假设链表A短,链表B长,同时当链表A和B的长度相等时,也按照A短B长的代码来即可,即当链表A的长度小于等于链表B时、
                    struct ListNode* shortList=headA;
                    struct ListNode* longList=headB;
                    if(lenA > lenB)
                    {
                        //链表A比链表B长、
                        longList=headA;
                        shortList=headB;
                    }

                    //求两个链表长度的差距的值、
                    int gap=abs(lenA-lenB);//求绝对值、
                    //让长链表先走gap步、
                        while(gap--)
                        {
                            longList=longList->next;
                        }
                        //同时走、
                        //while(longList)
                        //while(shortList)
                        //因为longList和shortList肯定同时到达NULL,所以即使只判断其中一个也是可以的、
                        while(longList && shortList)
                        {
                            if(shortList == longList)
                            {
                            //return shortList;
                                return longList;
                            }
                            shortList=shortList->next;
                            longList=longList->next;
                        }
            }
        }
    }
    //理论上讲,在此不写return也是对的,但是编译器编译的是语法逻辑,它不关心具体的执行逻辑,编译器不能分析语法的具体执行,他站在语法逻辑的角度去看,我们已知代码肯定会从上面的两个if语句中进行返回,即在执行逻辑中,肯定会从上面的两个if语句中进行返回,所以,理论上代码不会执行下面的return,这是站在执行逻辑去看的,但是编译器是站在语法逻辑的角度来看,他会认为上面用来返回的两个if语句不一定会进入其中,if语句只是用来判断的,也有不满足条件而不进入if语句的情况,当不进入上面用来返回的两个if语句时,代码走到此处如果没有return返回的话,语法上就错了,所以,即使这里的return实际上用不到,也要写上,随便返回一个值就行了,要注意与返回类型相对应,,防止编译器报错、

    //不带环、
    //带环链表时,fast和fast->next都不可能等于空指针NULL,程序到此处则肯定是不带环链表、
    return NULL;    
}

3.4.11、链表面试题十一:

给定一个 链表 ,每个节点包含一个 额外增加的随机指针 ,该指针可以指向链表中的 任何节点或空节点 包括可以指向它自身 ,要求返回这个链表
的深度拷贝, 力扣
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */

struct Node* copyRandomList(struct Node* head) 
{
    struct Node* cur=head;
    //1、把拷贝的节点链接到原节点后面、
    while(cur)
    {
        //在OJ题中,不需要对malloc等进行检查、
        struct Node* copy=(struct Node*)malloc(sizeof(struct Node));

        //拷贝数值、
        copy->val=cur->val;

        //方法一:
        //把拷贝节点链接到原节点后面、
        copy->next=cur->next;
        cur->next=copy;

        //移动指针变量cur、
        cur=cur->next->next;
        //cur=copy->next;

        /*//方法二:
        //把拷贝节点链接到原节点后面、
        struct Node* next=cur->next;
        cur->next=copy;
        copy->next=next;

        //移动指针变量cur、
        cur=next; */
    }

    //2、链接每个拷贝节点的random指针、
    //上述过程中已经进行了原链表的第一次遍历,在该过程中是没有办法操作random的,因为random指针所指的位置是随机的,当上述过程还在遍历前几个原节点时,若random指针指向了原链表后面的节点,比如,由于拷贝节点是链接到原节点的后面的,当random指针指向了原链表后面的节点时,那么新开辟的前几个拷贝节点就要指向后面的拷贝节点,此时有可能后面的拷贝节点还没开辟到,再者就是,由于cur还没遍历完一遍,而random指针所指位置又是随机的,当前几个原节点中的random指针如果指向了cur还没遍历到的节点,此时该random所指的位置也是不确定的,所以这样是完不成的,要进行第二次遍历,那么就需要再让cur从头开始,并其还要让copy从第一个拷贝节点开始、
    cur=head;
    while(cur)
    {
        struct Node* copy=cur->next;
        if(cur->random==NULL)
        {
            //当某一个原节点的random指向了NULL,则需要将对应的拷贝节点的random手动置为空指针NULL、
            copy->random=NULL;
        }
        else
        {
            //当某一个原节点的random不指向NULL,则按照规律来、
            copy->random=cur->random->next;
        }
        cur=cur->next->next; 
    }

    //3、把拷贝节点都从原链表上面分割下来,再把每个拷贝节点都链接起来、
    //为了简单,要进行第三次遍历,就算第2,3步可以在一次遍历内搞定,也最好不要这样,会比较麻烦,并且多遍历一次也不会影响时间复杂度,所以尽量分散来写,而把每个拷贝节点从原链表上分割下来,则要再从原链表起始位置开始,依次把每个拷贝节点都分割下来,最后再把所有的拷贝节点链接起来即可、
    //需要再让cur从头开始,并其还要让copy从第一个拷贝节点开始、
    cur=head;
    struct Node* copyHead=NULL;
    struct Node* copyTail=NULL;
    while(cur)
    {
        //在该过程中对于拷贝节点,应该是解下来一个,链接上一个,解下来一个,就把解下来的拷贝的节点链接到前面的一个拷贝节点上去,所谓链接在此就是尾插,可以使用带头链表,也可使用不带头链表,在此以不带头链表为例,因为不管哪种,影响都不大、
        //在此过程中最好定义三个指针,否则不太好定位、
        struct Node* copy=cur->next;

        //struct Node* next=copy->next;
        struct Node* next=cur->next->next;

        //把原链表中的箭头恢复、
        //最好恢复一下、
        cur->next=next;

        //if(copyHead==NULL)
        if(copyTail==NULL)
        {
            //使用无头链表进行尾插时若该无头链表中没有节点存在、
            copyHead=copy;
            copyTail=copy;
        }
        else
        {
            //使用无头链表进行尾插时若该无头链表中有节点存在、
            copyTail->next=copy;    
            copyTail=copy;
            //copyTail=copyTail->next;
        }
        cur=next;
    }   
    //最后一个拷贝节点的next已经指向了NULL,所以在此直接返回新链表的头指针即可,不需要再手动把copyTail->next置为空指针NULL了、
    return copyHead;
}

关于双向链表的知识点会在后续更新,谢谢大家!

猜你喜欢

转载自blog.csdn.net/lcc11223/article/details/123418217