2.3、循环链表实现
2.3.1、循环链表说明
- 循环链表(circular linked list)是另一种形式的链式存储结构,它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他的结点。
- 单链表中,从一已知结点出发,只能访问到该结点及其后续结点,无法找到该结点之前的其它结点。而在单循环链表中,从任一结点出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现。
- 单循环链表的操作和单链表基本一致,差别在于算法中的循环条件不是操作指针 p 或 p->next 是否为 NULL ,而是它们是否等于头指针。
- 双循环链表的操作和双链表基本一致。
2.3.2、循环链表的优缺点与适用情况
敬请期待
2.3.3、单循环链表实现
(1)单循环链表存储结构定义与头文件声明
- 存储结构定义:为了单链表的头尾操作更便捷,这里增设一个链表类型,其中包括链表的头指针 head (始终指向头结点)、尾指针 tail (始终指向链表最后一个结点)以及一个用于记录结点个数的计数器。增设尾指针 tail 使得尾插、合并操作更加便利,时间复杂度更小。
- 头文件声明:头文件声明包括函数结果状态代码 OK、ERROR,数据元素类型 ElemType ,单链表的基本操作的函数原型声明,以及需要的标准库函数头文件。
注:由于C语言不存在引用,需用传址。对于一些需要更改链表内容的操作,需要传递地址。即,形参声明为指针类型,实参用取址符
#ifndef SING_CIRCULAR_LinkList_H
#define SING_CIRCULAR_LinkList_H
#define OK 1
#define ERROR 0
#include <stdio.h>
#include <stdlib.h>
#include<stdbool.h>
#include <assert.h>
typedef int Status; //自定义函数类型,其值是函数结果的状态代码
typedef int ElemType; //自定义数据类型
typedef struct LNode
{
//结点类型,同单链表
ElemType data;
struct LNode* next; //注意类型
} LNode, *PNode;
typedef struct SCLinkList
{
PNode head; //指向头结点
PNode tail; //指向尾结点
int len;
} SCLinkList;
//顺序表的基本操作的函数原型说明
Status InitSCLinkList(SCLinkList *L);
//构造一个空顺序表
PNode Buynode(ElemType e);
//开辟一个数据域为e的新结点
Status push_back(SCLinkList *L, ElemType e);
//尾部插入新元素e
Status push_front(SCLinkList *L, ElemType e);
//头部插入新元素e
Status pop_back(SCLinkList *L);
//尾部删除
Status pop_front(SCLinkList *L);
//头部删除
Status Destory_List(SCLinkList *L);
//销毁顺序表
Status Clear_List(SCLinkList *L);
//置空队列 Q
bool Is_empty(SCLinkList L);
//判断队列是否为空
int Length_List(SCLinkList L);
//返回队列 Q 的元素个数
PNode find(SCLinkList L, ElemType e);
//返回e元素所在结点
Status Insert_val(SCLinkList *L, ElemType e);
//在L中第i个位置前插入新的元素e
Status Delete_val(SCLinkList *L, ElemType e);
//删除L中的第i个元素,并用e返回其值
Status Sort(SCLinkList *L);
//升序排序
Status Traverse_List(SCLinkList Q /*,visit()*/);
//从队头到队尾依次调用函数 visit(),这里简化为输出
#endif
(2)单循环链表主要操作实现与具体思路
- 初始化 InitSCLinkList()
对于有头结点的单循环链表初始化操作就是开辟一个头结点,并给头指针、尾指针赋初值。此时只有一个头结点,则头指针与尾指针都应指向头结点,且尾结点的 next 应指向头结点。单链表长度记录 len 初始化为 0 (无数据的头结点不计入数据结点个数)。
Status InitSCLinkList(SCLinkList *L)
{
PNode s = (PNode)malloc(sizeof(LNode)); //开辟一个结点空间
assert(s != NULL);
L->head = L->tail = s; //此时仅有一个结点,头尾指针均指向该结点
L->tail->next = L->head; //让尾结点的指针域指向头结点,实现循环
L->len= 0;
return OK;
}
- 结点申请操作 Buynode()
为了减少代码的重复,特设此函数。该函数开辟一个新结点空间,并完成结点数据域、指针域的初始化,最后返回这个结点的地址。
PNode Buynode(ElemType e)
{
PNode s = (PNode)malloc(sizeof(LNode));
assert(s != NULL);
s->data = e;
s->next = NULL;
return s;
}
- 尾插 push_back()
即将元素 e 插入到当前单循环链表的尾结点后。首先申请一个结点(Buynode()操作),其次将该结点链结到当前链表的尾部,然后更新尾指针指向,最后单链表长度加 1 。插入删除操作均要更改链表内容,故形参需要传递地址。
Status push_back(SCLinkList *L, ElemType e)
{
//尾部插入新元素e
PNode s = Buynode(e);
L->tail->next = s; //实现新结点s接在当前链表的尾结点之后
L->tail = s; //更新当前尾结点,即当前尾结点为s
L->tail->next = L->head; //实现新的尾结点指向头结点
L->len++;
return OK;
}
- 头插 push_front()
即将新结点插入到单循环链表头部(头结点之后,第一个数据结点之前)。首先要申请一个结点,其次,要链结到第一个数据结点的前端,再链结到头结点的后端(不可颠倒,如果先将新结点链结到头结点的后端,会导致其他结点丢失)。最后应区分头插的新结点是不是当前第一个数据结点,如果是,那么需要更新尾结点。
Status push_front(SCLinkList *L, ElemType e)
{
//头部插入新元素e
PNode s = Buynode(e);
//先链接结点尾部
s->next = L->head->next;
L->head->next = s;
//以上两行对于s为第一个结点时,仍插入成功,但tail指针指向不正确 ,故如下
if (L->head == L->tail) //两者相等即新插入的结点为第一个结点
{
L->tail = s; //更新尾结点,此时新插的结点是尾结点
L->tail->next = L->head;//实现循环
}
L->len++;
return OK;
}
- 尾删 pop_back()
首先判断当前链表是否为空,若为空直接返回。要删除尾结点,则必须先获取尾结点的直接前驱。获取尾结点前驱需要操作指针 p 从头结点开始,依次向后遍历,直至 p 的next 域指向尾结点(即 p ->next == L->tail ),则此时 p 便指向尾结点的前驱。获取前驱后,直接释放尾结点,最后更新尾结点指向即可。
Status pop_back(SCLinkList *L)
{
//尾部删除
if (L->len== 0)
return ERROR;
PNode p = L->head; //要先寻找尾结点的前驱
while (p->next != L->tail)
{
//从第一个元素结点位置向后遍历,直至p->next==L->tail,即p为尾结点的前驱
p = p->next;
}
free(L->tail);
L->tail = p; //更新尾结点
L->tail->next = L->head; //更新尾结点的指针域
L->len--;
return OK;
}
- 头删 pop_front()
同样,头删要考虑当前链表是否为空,若为空,直接返回。由于头结点的指针域指向第一个数据结点,所以不需要像尾删那样,遍历到尾结点的直接前驱。只需先获取第一个元素结点,然后将第二个元素结点链结到头结点的 next 域即可,最后释放结点。该操作要考虑,头删后链表是否仅剩一个头结点,如果是,则应更新尾指针 tail ,使其指向头结点。
Status pop_front(SCLinkList *L)
{
//头部删除
if (L->len== 0)
return ERROR;
PNode p = L->head->next;
L->head->next = p->next;
free(p);
if (L->len== 1)
{
//如果当前只有一个结点(即删除的是尾结点lsat),需要更改尾结点的指向
//此时删除后,仅剩一个头结点
L->tail = L->head;
L->tail->next = L->head;//实现循环
}
L->len--;
return