目录
一、前情提要
上篇讲了双向链表的由来和功能,以及代码实现。它与单链表相对应。这篇文章主要讲一种特殊的链表——循环链表。循环链表之所以留到这里,是因为它有两种形式,一种是循环单链表、另外一种是循环双链表。下面会给出代码和分析。
二、循环单链表介绍
(一)循环单链表概念
Q:循环链表是个啥,它跟单链表有什么联系?
A:实际上循环单链表就是由单链表变来的。你可以将循环链表看做成,单链表的尾结点(最后一个结点)原本是指向NULL的。然后我们改变它的指向,最终指向头结点,这样就实现了循环的目的。
画个图理解一下:顺便预告一下
(这个图只是为了理解循环链表概念,后面写代码不是以这个为准,头尾指针的问题还会讨论)
Q:为什么循环链表还要保留一个没有数据的头结点?这样循环起来跳过这个点挺麻烦的。
A:说白了还是为了一致性。为了使空链表和非空链表一致。所以通常情况下会保留头结点。但是注意,并不是说循环链表一定要头结点,保留仅仅是为了一致,你当然也可选择不保留。
空的循环链表如下图:
Q:循环单链表的适用场景是哪种?
A:频繁在表头表尾操作,不在中间操作。
既然频繁在表头表尾操作,这就要求找表头表尾方便,从这里就开始涉及到头指针和尾指针的问题了。
(二)头指针和尾指针
Q:啥是尾指针?
A:头指针:指向头结点的指针。
尾指针:指向终端结点(一轮循环结束的标志)的指针。
非循环链表中:
假设此时我们是单链表,尾指针毫无意义。它就指向个NULL,要它有何用?在非循环链表中头指针决定着一切。有头指针找到表头容易,O(1)的复杂度就能够找到,但表尾需要遍历一遍,需要O(n)的复杂度,所以找表尾就有点慢。所以一般情况下我们习惯于在非循环链表中头插法,就是因为有头指针的存在。
(复杂度的概念自己上网查查吧,没啥好讲的)
循环单链表中:
回到循环链表中,适用场景决定了它必须快速找到表头和表尾!
假设我们有头指针,找到表头很容易,O(1)的复杂度。但是找表尾很麻烦,只能遍历,还是需要O(n)的时间复杂度。
假设我们有尾指针,找到表尾很容易,O(1)的复杂度。招表头一样也很容易,因为是循环的,表头在表尾下一位。所以Node->Next就是头结点。也是O(1)的时间复杂度就能找到。
综上所述:
1.如果是非循环链表,那肯定用头指针。插入头插法简单。
2.如果是循环链表,尾指针更容易找到表头和表尾,所以尾指针更适用。插入,头插和尾插都很简单。但尾插更简单适用,通常也是用尾插,毕竟尾指针直接指向尾部,更方便。
所以最终效果图是这样的
后面就是代码了,跟单链表差不多。。
注意:这次的代码会用到很多二级指针。因为尾插会改变尾指针的位置,改变指针的指向需要用二级指针!!
除此之外,我有些写法和之前不太一样。比如删除结点的代码,本质是一样的。你当然可以不模仿我写,总而言之能写出来,功能相同就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;
}
三、循环双链表介绍
知道了循环单链表,循环双链表就好理解了。不就是双向链表,最终节点指向头结点,头结点指向最终结点嘛。
这样的话就使得适用场景进一步扩大。
循环单链表:频繁在表头表尾操作,不在中间操作。
循环双链表:既可以频繁在表头表尾操作,也可以在中间操作。
由此可见,循环双链表就是一个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;
}