数据结构与算法(五)——线性表(下)循环链表篇

本文介绍了循环链表的概念,特别是循环单链表和循环双链表。循环单链表是通过将单链表的尾节点指向头节点形成,适用于频繁在表头表尾操作的场景。文章讨论了头指针和尾指针在不同链表类型中的作用,并提供了C语言实现的代码示例,包括插入、删除等操作。循环双链表则允许在链表中间进行操作,具有更大的灵活性。
摘要由CSDN通过智能技术生成

目录

一、前情提要

二、循环单链表介绍

三、循环双链表介绍


一、前情提要

        上篇讲了双向链表的由来和功能,以及代码实现。它与单链表相对应。这篇文章主要讲一种特殊的链表——循环链表。循环链表之所以留到这里,是因为它有两种形式,一种是循环单链表、另外一种是循环双链表。下面会给出代码和分析。

二、循环单链表介绍

(一)循环单链表概念

        Q:循环链表是个啥,它跟单链表有什么联系?

        A:实际上循环单链表就是由单链表变来的。你可以将循环链表看做成,单链表的尾结点(最后一个结点)原本是指向NULL的。然后我们改变它的指向,最终指向头结点,这样就实现了循环的目的。

        画个图理解一下:顺便预告一下

      (这个图只是为了理解循环链表概念,后面写代码不是以这个为准,头尾指针的问题还会讨论)

6e448390c119496682d9452c09e7593e.png

     Q:为什么循环链表还要保留一个没有数据的头结点?这样循环起来跳过这个点挺麻烦的。

     A:说白了还是为了一致性。为了使空链表和非空链表一致。所以通常情况下会保留头结点。但是注意,并不是说循环链表一定要头结点,保留仅仅是为了一致,你当然也可选择不保留。

空的循环链表如下图:

9f69585a83574fdb992501a71d7a7a90.png

   

  Q:循环单链表的适用场景是哪种?

  A:频繁在表头表尾操作,不在中间操作。

       既然频繁在表头表尾操作,这就要求找表头表尾方便,从这里就开始涉及到头指针和尾指针的问题了。

(二)头指针和尾指针

  Q:啥是尾指针?

  A:头指针:指向头结点的指针。                   

      尾指针:指向终端结点(一轮循环结束的标志)的指针。

1498f6034c074c109a523b68c7c12e66.png

非循环链表中:

        假设此时我们是单链表,尾指针毫无意义。它就指向个NULL,要它有何用?在非循环链表中头指针决定着一切。有头指针找到表头容易,O(1)的复杂度就能够找到,但表尾需要遍历一遍,需要O(n)的复杂度,所以找表尾就有点慢。所以一般情况下我们习惯于在非循环链表中头插法,就是因为有头指针的存在。

(复杂度的概念自己上网查查吧,没啥好讲的)

循环单链表中:

        回到循环链表中,适用场景决定了它必须快速找到表头和表尾!

        假设我们有头指针,找到表头很容易,O(1)的复杂度。但是找表尾很麻烦,只能遍历,还是需要O(n)的时间复杂度。

        假设我们有尾指针,找到表尾很容易,O(1)的复杂度。招表头一样也很容易,因为是循环的,表头在表尾下一位。所以Node->Next就是头结点。也是O(1)的时间复杂度就能找到。

        综上所述:

        1.如果是非循环链表,那肯定用头指针。插入头插法简单。

        2.如果是循环链表,尾指针更容易找到表头和表尾,所以尾指针更适用。插入,头插和尾插都很简单。但尾插更简单适用,通常也是用尾插,毕竟尾指针直接指向尾部,更方便。

        

所以最终效果图是这样的

f0f637a2e0744d829f6aa3e8224e9b42.png

       后面就是代码了,跟单链表差不多。。

       注意:这次的代码会用到很多二级指针。因为尾插会改变尾指针的位置,改变指针的指向需要用二级指针!!

       除此之外,我有些写法和之前不太一样。比如删除结点的代码,本质是一样的。你当然可以不模仿我写,总而言之能写出来,功能相同就OK。

       这次会有很多特判,需要多画图,没办法口述清楚,注释想办法写详细了。还看不懂的看下单链表,或者网上代码吧。          

  

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

typedef struct NodeList
{
	int Data;
	struct NodeList* Next;
}Node,*LinkList;

//注意LinkList是一级指针,LinkList*是二级指针!! 
LinkList InitList()
{
	LinkList TailPtr=(Node*)malloc(sizeof(Node)); //尾指针 
	if(TailPtr==NULL)
	{
		printf("申请内存失败");
		return NULL; 
	}
	else
	{
		TailPtr->Next=TailPtr;   //尾指针暂时先指向头结点 
	}
	return TailPtr;
}

//尾插 
void Tail_Insert(LinkList *TailPtr,int Data)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));
	if(New_Node==NULL)
	{
		printf("申请内存失败");
		return;
	}
	else
	{
		//按照单链表的插法插入
		New_Node->Data=Data;
		New_Node->Next=(*TailPtr)->Next;  //还是看不懂就去看单链表的代码吧。 
		(*TailPtr)->Next=New_Node;
		
		*TailPtr=(*TailPtr)->Next;//尾指针像后面移一位。因为要指向终端结点嘛。 
	}
}

//头插
void Head_Insert(LinkList *TailPtr,int Data)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));
	if(New_Node==NULL)
	{
		printf("申请内存失败");
		return;
	}
	
	Node* HeadNode=(*TailPtr)->Next; //先找到头结点位置 
	//下面就是新结点插在头结点后面的操作 
	New_Node->Data=Data;         
	New_Node->Next=HeadNode->Next;
	HeadNode->Next=New_Node; 
	if(HeadNode==(*TailPtr))  //这里需要特判,因为当链表为空的时候头插。 
	{                           //尾指针必须要往后移一位到新的结点上。 
		*TailPtr=New_Node;  //如果不为空则不需要。实在不懂可以画图。 
	} 
} 

//在值为k的结点后面,插入结点
void Val_Insert(LinkList *TailPtr,int k,int Data) 
{
	Node* New_Node=(Node*)malloc(sizeof(Node));
	if(New_Node==NULL)
	{
		printf("请内存失败");
		return;
	}
	
	Node* HeadNode=(*TailPtr)->Next;
	for(Node* i=HeadNode->Next;i!=HeadNode;i=i->Next)//从头结点下一个位置开始查询
	{                                                //直到是头结点结束 
	
		if(i->Data==k)
		{
			New_Node->Data=Data;
			//和单链表一样的插法 
			New_Node->Next=i->Next;
			i->Next=New_Node;
			if(k==(*TailPtr)->Data) //如果该结点是尾指针所指向的,那么尾指针就得向后移一位。 
			(*TailPtr)=(*TailPtr)->Next;//这样就可以保持尾指针持续指向终端节点(一轮循环结束的标志)。 
			return; 
		}                                                
	}
	printf("没有值为k的结点");//到这步还没返回的话说明没有值为k的结点。 
	 
}

//查询
Node* Find(LinkList TailPtr,int Data)
{
	Node* HeadNode=TailPtr->Next;
	for(Node* i=HeadNode->Next;i!=HeadNode;i=i->Next)//从头结点下一个位置开始查询
	{                                                    //直到是头结点结束
		if(i->Data==Data)
		{
			return i;
		}
	}
	return NULL;
}

//删除值为Data的结点 
void Delete(LinkList *TailPtr,int Data)
{
	Node* HeadNode=(*TailPtr)->Next;
	for(Node* i=HeadNode;i->Next!=HeadNode;i=i->Next)//遍历查询
	{                                               
		if(i->Next->Data==Data)
		{
			Node* Tar_Node=i->Next->Next;
			if((*TailPtr)!=i->Next) free(i->Next); //这里要特判一下,因为如果删除的是尾指针。 
			else                                   //尾指针要退一位。 
			{
				*TailPtr=i;
				free(i->Next);
			}
			i->Next=Tar_Node; 
			return;
		}
	}
	printf("没有值为Data的结点");
} 

//清空链表 
void MK_Empty(LinkList *TailPtr)
{
	if((*TailPtr)->Next==(*TailPtr))
	{
		printf("空链表");
		return;
	}
	
	Node* HeadNode=(*TailPtr)->Next;
	Node* Last=HeadNode->Next;
	Node* Front;
	while(Last!=HeadNode)
	{
		Front=Last->Next;

		free(Last);
		Last=Front;
	}
	*TailPtr=HeadNode;   //删完之后,要将尾指针初始化,使其指向头结点。不然它指向的是一块未知区域。  
	(*TailPtr)->Next=*TailPtr;
	return;
} 

//输出
void PrintList(LinkList TailPtr)
{
	if(TailPtr->Next==TailPtr)
	{
		printf("此表为空\n");
		return; 
	}
	Node* HeadNode=TailPtr->Next;
	for(Node* i=HeadNode->Next;i!=HeadNode;i=i->Next)
	{
		printf("%d ",i->Data);
	}
	printf("\n");
} 
int main()
{
	LinkList TailPtr=InitList();
	Head_Insert(&TailPtr,1);
	Head_Insert(&TailPtr,2);
	Head_Insert(&TailPtr,3);
	Head_Insert(&TailPtr,4);
	PrintList(TailPtr);
	
	Tail_Insert(&TailPtr,5);
	PrintList(TailPtr);
	
	Val_Insert(&TailPtr,3,3);
	PrintList(TailPtr);
	
	Delete(&TailPtr,5);
	PrintList(TailPtr);
	
	MK_Empty(&TailPtr);
	PrintList(TailPtr);
    
    printf("%x %x",TailPtr,TailPtr->Next);//检验清空后,尾指针是否初始化成功。 
	return 0;
}

42908cc1b6af4c5faa27af079f00a5c6.png

三、循环双链表介绍

       知道了循环单链表,循环双链表就好理解了。不就是双向链表,最终节点指向头结点,头结点指向最终结点嘛。

b4ddd983acc74ee29ac8702033609f63.png

这样的话就使得适用场景进一步扩大。

循环单链表:频繁在表头表尾操作,不在中间操作。

循环双链表:既可以频繁在表头表尾操作,也可以在中间操作。

由此可见,循环双链表就是一个Buff叠满的产物。

用头指针或尾指针也不用纠结,因为有Pre和Next指针,用哪个都可以快速访问表头表尾。

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

typedef struct NodeList
{
	int Data;
	struct NodeList* Next;
	struct NodeList* Pre; 
}Node,*LinkList;


LinkList InitList()
{
	LinkList Ptr=(Node*)malloc(sizeof(Node));
	if(Ptr==NULL)
	{
		printf("申请内存失败");
		return NULL;
	}
	else
	{
		Ptr->Next=Ptr;//头指针的Next和Pre都先只指向自己 
		Ptr->Pre=Ptr;
	}
}
//头插 
void Head_Insert(LinkList Ptr,int Data)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));
	if(New_Node==NULL)
	{
		printf("申请内存失败");
		return;
	}
	
	New_Node->Data=Data;
	//下面是双向链表的插法,不懂的去看双向链表。
	New_Node->Next=Ptr->Next;
	New_Node->Pre=Ptr;
	Ptr->Next->Pre=New_Node;
	Ptr->Next=New_Node; 
}

//尾插
void Tail_Insert(LinkList Ptr,int Data)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));
	if(New_Node==NULL)
	{
		printf("申请内存失败");
		return;
	}
	
	New_Node->Data=Data;
	//先找到表尾
	Node* TailPtr=Ptr->Pre;
	//下面是双向链表的插法,不懂的去看双向链表。
	New_Node->Next=TailPtr->Next;
	New_Node->Pre=TailPtr;
	TailPtr->Next->Pre=New_Node;
	TailPtr->Next=New_Node; 
}

//查询
Node* Find(LinkList Ptr,int Data)
{
	if(Ptr->Next==Ptr&&Ptr->Pre==Ptr)
	{
		printf("此表为空");
		return NULL;
	}
	else    //这些操作重复很多次了就不赘述了 
	{
		for(Node* i=Ptr->Next;i!=Ptr;i=i->Next)
		{
			if(i->Data==Data)
			{
				return i;
			}
		}
	}
	printf("未找到值为Data的结点");
	return NULL;
} 

//在值为k的结点后面插
void Val_Insert(LinkList Ptr,int k,int Data)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));
	if(New_Node==NULL)
	{
		printf("申请内存失败");
		return;
	}
	
	New_Node->Data=Data;
	//先找到目标结点 
	Node* Tar_Node=Find(Ptr,k);
	if(Tar_Node==NULL) return;
	//在目标结点后插入,插入步骤和双向链表一样
	New_Node->Next=Tar_Node->Next; 
	New_Node->Pre=Tar_Node;
	Tar_Node->Next->Pre=New_Node;
	Tar_Node->Next=New_Node;
}
 
//删除值为k的结点
void Delete(LinkList Ptr,int Data)
{
	Node* Tar_Node=Find(Ptr,Data);//先找到目标结点。 
	if(Tar_Node==NULL) return;
	
	//找到目标结点前一个结点 
	Node* Pre_Target=Ptr->Next;
	while(Pre_Target->Next!=Tar_Node)
	{
		Pre_Target=Pre_Target->Next;
	}
	//改变其指向 
	Pre_Target->Next=Tar_Node->Next;
	free(Tar_Node); 
}

//清空链表
void Mk_Empty(LinkList Ptr)
{
	if(Ptr->Next==Ptr&&Ptr->Pre==Ptr)
	{
		printf("此表为空");
		return;
	}
	else
	{
		Node* Last=Ptr->Next;
		Node* Front;     //用for循环挨个删也可以,写法多样。 
		while(Last!=Ptr) //删的只剩头结点 
		{
			Front=Last->Next;
			free(Last);
			Last=Front;
		}
		Ptr->Next=Ptr;  //指针的Next要初始化,不然它指向的是一块未知区域。 
		Ptr->Pre=Ptr;
	}
} 

//输出链表
void PrintList(LinkList Ptr)
{
	if(Ptr->Next==Ptr&&Ptr->Pre==Ptr)
	{
		printf("此表为空");
		return ;
	}
	else
	{
		for(Node* i=Ptr->Next;i!=Ptr;i=i->Next)
		{
			printf("%d ",i->Data);
		}
		printf("\n");
	} 
}
int main()
{
	LinkList Ptr=InitList();
	Head_Insert(Ptr,1);
	Head_Insert(Ptr,2);
	Head_Insert(Ptr,3);
	Head_Insert(Ptr,4);
	PrintList(Ptr);
	
	Tail_Insert(Ptr,5);
	PrintList(Ptr);
	
	Val_Insert(Ptr,3,3);
	PrintList(Ptr);
	
	Delete(Ptr,3);
	PrintList(Ptr);
	
	Mk_Empty(Ptr);
	PrintList(Ptr);
	return 0;
} 

e7c7ff2b86d549a49c0515f2a3c3cf10.png

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值