循环链表
什么是循环链表?
- 单链表(仅含一个指针域,每个节点存在直接后继)
- 尾指向头(形成一个循环)
单链表的话只能从链表头开始遍历,才能访问到所有的节点,而循环链表,由于形成了一个圈,从任意一个节点出发我们都可以访问到所有的元素。
所以定义循环链表类型时,我们可以用一个指针标识头,一个指针标识尾,在之后的操作中添加一条语句,将tail->next = head; 添加这样一条语句,我们即可完成一个循环链表的构建。不过感觉这样做好像有点没意思?。。。
其实用一个指针标识尾就可以了,不过这样做的话必须添加一个头结点,这个头结点中不含数据,它只指向真正的表头(注意是头结点,不是头指针,为什么添加?方便表示。)。也就是说无论链表是否为空,这个头结点始终是存在的,这是一个标识头结点,如果链表为空,那么头结点的next就要指向自己,这样代表一个空的循环链表。而如果没有头结点,空循环链表其实是没法表示的,只能用某种特定的描述代表一个空循环链表,(比如说像普通单链表一样,头尾指针相等都为空,来代表空循环链表;头尾指针不等,那么这个循环链表必定不空,那么需要将此表表示成循环链表,链接尾和头)
特点
循环链表,区别于单链表的最大特点:尾巴指向头,形成一个圈。
实现(添加头结点法)
结点类型以及循环链表类型定义及描述:
typedef struct _node{
ElemType data;
struct _node * next;
}Node;
typedef struct _looplist{
//循环链表只需要标识其尾部即可,因为头部其实就是rear->next->next,因为包含一个头结点,头结点的下一个才是真正意义的链表头
Node * rear;
int size;
}LoopList;
一、初始化
由于增加了头结点,也就意味着我们初始化的时候需要先申请一个头结点:
void InitLpList(LoopList * lplist)
{
//申请头结点
Node * pnew = (Node * pnew) malloc(sizeof(Node));
//自己指向自己
pnew->next = pnew;
//更新链表信息,此时尾指针指向的是头结点
lplist->rear = pnew;
lplist->size = 0;
}
二、是否为空
相应的是否为空循环链表的判断方法也有变化:
bool LpListIsEmpty(LoopList * lplist)
{
return lplist->rear->next == lplist->rear;
//也可以return lplist->size == 0;
}
三、添加
添加的话,还是根据单链表的思路进行,添加第一个,添加后续:
bool AddToLpList(LoopList * lplist, ElemType data)
{
Node * pnew = (Node *)malloc(sizeof(Node));
if(pnew == NULL)
return false;
CopyToNode(pnew,data);
pnew->next = lplist->rear->next;//无论是第一个还是后续的,新节点next指向头结点就可以了
if(LpListIsEmpty(lplist)) //添加第一个
lplist->rear->next->next = pnew;//头结点指向第一个新节点
else //添加后续的
lplist->rear->next = pnew;//添加到尾
lplist->rear = pnew;//无论是第一个还是后续的添加,都需要更新链表尾
return true;
}
四、插入
也类似于单链表(插入前我们先要判断是否是一个空循环链表):
- 插在第一个位置的话实际上就是插在头结点之后;
- 插在最后一个位置的话,需要将这个结点的next指向头结点,同时需要更新尾巴,其实就是添加一个结点到非空循环链表的操作(不妨偷个懒,当用户要插在最后一个位置,直接调用AddToLpList()函数);
- 根据用户指定的位置,插在中间的话,循环链表的rear信息不需要更改,只需找到用户指定位置,插入即可;
bool InsertLpList(LoopList * lplist, int index, ElemType data)
{
//先判断用户的index是否合法
if(index < 1 || index > (lplist->size + 1))
return false;
if(index == lplist->size + 1)//插在尾
{
AddToLpList(lplist,data);//就是添加操作
lplist->size++;
return true;
}
Node * pnew = (Node *) malloc(sizeof(Node));
//未分配到,提前返回
if(pnew == NULl)
return false;
CopyToNode(pnew,data);
if(index == 1 && lplist->size >= 1)//插在头
{
pnew->next = lplist->rear->next->next;//指向之前的第一个结点
lplist->rear->next->next = pnew;//头结点指向新的第一个节点
}else if(index > 1 && index < lplist->size + 1){//插在中间
//找到要插入的位置的前一个指针
Node * temp;
int i = 1;
for(temp = lplist->rear->next->next;i != index - 1;temp = temp->next)
continue;
pnew->next = temp->next;
temp->next = pnew;
}
lplist->size++;
return true;
}
五、删除
也差不多像链表一样考虑:
- 删除的是否是最后一个元素,是的话更新链表为空;
- 删除的如果不是最后一个,考虑删除的是第一个位置,或者中间位置,或者最后的尾;相应的操作也差不多类似于插入的步骤;
这里就省略这个删除的实现了。。。写的有点多,其实也就和单链表差不多的,当加深下印象吧。。。
总结
主要还是考虑问题的思维方式,只要理解了精髓,具体实现都是可以完成的。只不过有的人实现可能更简洁(如果定义类型的方式上有优势,那么其他部分就相应的会轻松很多,也会更简洁),有的麻烦。。(比如说我。。),但是核心都是差不多的。