1.定义:循环链表是另一种形式的链式存储结构。它的特征是表中最后一个节点的指针域指向头节点,整个链表形成一个环。如果循环链表是个空表,那头节点的next指针直接指向自己
一条蛇咬住自己的尾巴,那么一只蚂蚁从身体的任何一个位置开始爬,都能爬边蛇的全身,同样,从循环链表中任何一个节点出发都能找到表中的其他节点。
如果我们要访问尾节点,需要从头指针开始,一个节点一个节点到走,然后走到尾节点,时间复杂度尾O(n);
如果我们要合并A、B两个线性表,需要进行的操作是,让A的尾节点next指针指向B的首元节点,再让B的尾节点next指针指向A的头节点,然后释放B的头节点。其中要访问两个的尾节点,时间复杂度尾O(Length(A) + Length(B))
改进形式:不设置头指针,设置尾指针指向尾节点
这样访问尾节点一步到位,访问头节点直接tail->next
,首元节点tail->next->next
,时间复杂度都是O(1)
再考虑合并线性表A、B,同样的操作,时间复杂度变成了O(1),代码如下
p = tailA->next; /*保存A表的头节点*/
tailA->next = tailB->next->next; /*让A的尾节点的next指针指向B的首元节点*/
q = tailB->next; /*保存B表的头节点*/
tailB->next = p; /*让B的尾节点的next指针指向A的头节点*/
free(q); /*释放B的头节点*/
可见,设置尾指针取代头指针有很多便利,同样,抓住了尾指针,整个循环链表都能拎起来
C语言描述循环链表代码如下
typedef struct CNode {
Elemtype data;
struct CNode *next;
}CNode, *TailCLinkList;
TailCLinkList 为CNode *类型,也就是指向节点的指针类型,而且把它固定为尾指针,普通节点的指针用CNode * ,以示区分
2.循环链表的实现
2.1初始化循环链表
/*建立一个空的循环链表*/
Status InitCLinkList(TailCLinkList *L) {
*L = (CNode *)malloc(sizeof(CNode));
/*创建头节点(空链表只有一个头节点,同时是特殊的尾节点)*/
if (!(*L)) exit(OVERFLOW);
(*L)->next = *L;
/*让头节点的next指针域指向自己*/
return OK;
}
2.2清空循环链表
void ClearCLinkList(TailCLinkList *L)
{
CNode *p, *q;
p = (*L)->next->next; /*让p指向首元节点*/
while (p != (*L)->next) {
/*当p没有循环到头节点*/
q = p->next;
free(p);
p = q;
}
*L = p;
/*让*L指回头节点那片空间*/
(*L)->next = *L;
/*让头节点的next指针域指回自己*/
}
2.3销毁循环链表
void DestroyCLinkList(TailCLinkList *L)
{
CNode *p, *q;
/*p指向首元节点*/
p = (*L)->next->next;
/*若p还没到头节点,则执行下面的循环*/
while (p != (*L)->next) {
q = p->next;
free(p);
p = q;
}
/*释放完普通节点的空间后,释放头节点的空间*/
*L = p;
free(*L);
/*修改*L指向为NULL*/
*L = NULL;
}
2.4返回循环链表中元素的个数
int CLinkListLength(TailCLinkList L)
/*注意是元素个数,头节点数据域中没有存储元素*/
{
int i = 0;
CNode *p;
p = L->next->next;
while(p != (*L)->next) {
++i;
p = p->next;
}
return i;
}
2.5返回循环链表中第i个元素的值
Status GetElem(TailCLinkList L, int i, ElemtType *e)
{
int j = 1;
CNode *p;
p = L->next->next;
while (p != (*L)->next && j < i)
{
p = p->next;
++j;
}
if (p = (*L)->next || j > i)
return ERROR;
/*上面的if判断说明第i个元素不存在*/
/*即表长小于 i 或者 i < 1*/
*e = p->data;
return OK;
}
2.6确定和e相等的元素在循环链表中的位置
#define NOT_FOUND 0
int LocateElem(TailCLinkList L, ElemType e)
/*找到的话返回是第几个元素,否则返回0*/
{
int i = 0;
CNode *p;
p = L->next->next;
while (p != (*L)->next && p->data != e)
{
++i;
p = p->next;
}
if (p = (*L)->next) return NOT_FOUND;
return i;
}
2.7向循环链表插入元素
/*将元素插在第i位之前,i == 1时插在头节点之后,i = length + 1时插在尾节点之后*/
Status CLinkListInsert(TailCLinkList *L, int i, ElemType e)
{
int j = 1;
CNode *p, *s;
p = (*L)->next;
/*p从头节点开始是为了使从首元节点前插入和从其他节点前插入统一处理*/
while (p != *L && j < i)
{
p = p->next;
++j;
}
if (j == i) {
/*同时包括了i == 1 和 i == length + 1 的情况*/
s = (CNode *)malloc(sizeof(CNode));
/*以下代码插入新节点s,即使是在首元节点前插入*/
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
return ERROR;
}
2.8删除循环链表中的元素
Status CLinkListInsert(TailCLinkList *L, int i, ElemType *e)
{
int j = 1;
CNode *p, *q;
p = *L->next;
while (p != *L && j < i) {
p = p->next;
++j;
}
if (p = *L || j > i)
/*表长小于i 或者 i < 1*/
return ERROR;
/*p->next指示要删除的节点位置*/
q = p->next;
/*获取节点数据元素*/
*e = q->data;
/*删除节点*/
p->next = q->next;
/*释放节点*/
free(q);
return OK;
}
2.9遍历循环链表中的元素
void CLinkListTraverse(TailCLinkList L)
{
CNode *p;
p = L->next->next;
while (p != L->next) {
/*元素之间用空格分隔,纯属个人习惯*/
printf("%c ", p->data);
p = p->next;
}
printf("\n");
}
2.10尾插法创建循环链表
/*这里假设数据元素为字符类型,并且存储空间充足*/
typedef char ElemType
void CreateCLinkList_Tail(TailCLinkList *L, int n)
{
CNode *p;
int i;
char ch;
/*初始化,建立一个只有头节点的空表*/
*L = (CNode *)malloc(sizeof(CNode));
(*L)->next = *L;
/*依次生成新结点,插入到表尾*/
for (i = 0; i < n; i++) {
scanf("%c", &ch);
/*也可以用getchar()读取字符*/
p = (CNode *)malloc(sizeof(CNode));
p->data = ch;
p->next = (*L)->next;
(*L)->next = p;
/*p插入后变成尾节点*/
*L = p;
}
}
2.10头插法创建循环链表
/*这里假设数据元素为字符类型,并且存储空间充足*/
typedef char ElemType
void CreateCLinkList_Head(TailCLinkList *L, int n)
{
CNode *p;
int i;
char ch;
/*初始化,建立一个只有头节点的空表*/
*L = (CNode *)malloc(sizeof(CNode));
(*L)->next = *L;
/*先插入一个节点固定为循环链表的尾巴*/
scanf("%c", &ch);
p = (CNode *)malloc(sizeof(CNode));
p->data = ch;
p->next = *L;
(*L)->next = p;
*L = p;
/*依次生成新结点,插入到表头*/
for (i = 1; i < n; i++) {
scanf("%c", &ch);
/*也可以用getchar()读取字符*/
p = (CNode *)malloc(sizeof(CNode));
p->data = ch;
/*让p的next指针指向首元节点*/
p->next = (*L)->next->next;
/*p插入后变成首元节点*/
(*L)->next->next = p;
}
}
好了,循环链表就先介绍到到这里,如果觉得上面的代码不好理解,你可以在每个函数里声明一个CNode *head
作为循环链表的头节点,在初始化的时候head = *L
那个时候尾巴就是头,头就是尾巴,其他时候head = *L->next
头在尾巴后面,这样代换之后就比较容易理解了
接下来介绍双向链表
1.定义:双向链表是在单链表的每个节点中,再设置一个指向其前驱的指针域。所以双向链表中的每个节点有两个指针域,一个next指针指向直接后继,一个prior指针指向直接前驱
为什么要学习双向链表呢?设想这样一种情况,老板问你,某单链表中第i位员工的信息,你从头节点开始往后访问,直到第i个元素节点,你说是张三,然后老板问你,那前一个是谁,你又要从头节点出发访问,到第i - 1位元素节点,哦,是李四,老板又问你,李四前面呢,你大为光火,心中暗暗的咒骂老板,想着干脆遍历全部打印出来,那可是50万人哦,打印出来老板二话不说把你炒了!
双向链表为查访某元素的前一个元素而生,只需要pos->prior,O(1)的时间!
双向链表的C语言描述如下:
typedef struct DulNode {
ElemType data;
struct DulNode *prior, *next;
}DulNode, *DuLinkList;
这里依然按照之前的约定,约定DuLinkList为头指针,普通节点的指针用DulNode *加以区分
2.双向链表的实现
2.1双向链表的初始化
/*创建一个空的双向链表*/
Status InitDuLinkList(DuLinklist *L)
{
*L = (DuLinkList)malloc(sizeof(DulNode));
if(!(*L)) exit(OVERFLOW);
/*空链表只有一个头节点,且指针域均为NULL*/
(*L)->prior = NULL;
(*L)->next = NULL;
return OK;
}
清空双向链表、销毁双向链表、返回双向链表中元素的个数、返回双向链表中第i个元素的值、确定和e相等的元素值在双向链表中的位置、遍历双向链表中的元素节点这些操作和在单链表中的一样,只需要从头结点出发,顺着next指针域往下即可,prior指针域用不上,如果对单链表的相关操作不熟悉可以看我之前的文章
2.2插入元素
Status DuLinkListInsert(DuLinkList *L, int i, ElemType e)
{
int j = 1;
DulNode *p, *s;
p = *L;
/*这里是从头节点开始,j为表长加 1*/
while (p && j < i)
{
p = p->next;
++j;
}
if (!p || j > i)
return ERROR;
/*表长 + 1 < i 或者 i < 1*/
/*在表尾的插入比较特殊,因为tail->next == NULL,不存在tail->next->prior*/
if (p->next == NULL) {
s = (DulNode *)malloc(sizeof(DulNode));
s->data = e;
s->prior = p;
s->next = NULL;
p->next = s;
return OK;
}
s = (DulNode *)malloc(sizeof(DulNode));
/*以下代码插入新节点s,即使是在首元节点前插入*/
s->data = e;
s->prior = p;
s->next = p->next;
p->next->prior = s;
/*注意上面两步都用到了p->next,一定在下面这步之前*/
p->next = s;
return OK;
}
2.3删除第i个元素的节点,用e接收其数据元素
Status DuLinkListInsert(LinkList *L, int i, ElemType *e)
{
int j = 1;
DulNode *p, *q;
p = *L;
while (p->next && j < i) {
p = p->next;
++j;
}
if (!(p->next) || j > i)
/*表长小于i 或者 i < 1*/
return ERROR;
/*p->next指示要删除的节点位置,记为q,有q->prior == p*/
q = p->next;
/*删除表尾元素比较特殊*/
if (q->next = NULL) {
*e = q->data;
p->next = NULL;
free(q);
return OK;
}
/*获取节点数据元素*/
*e = q->data;
/*删除节点*/
p->next = q->next;
q->next->prior = p;
/*释放节点*/
free(q);
return OK;
}
2.4头插法创建双向链表
/*这里假设数据元素为字符类型,并且存储空间充足*/
typedef char ElemType;
void CreateDuLinkList_Head(DuLinkList *L, int n)
{
DulNode *p;
int i;
char ch;
/*初始化,建立一个只有头节点的空表*/
*L = (DuLinkList)malloc(sizeof(DulNode));
(*L)->prior = NULL;
(*L)->next = NULL;
/*第一个节点作为表尾插入操作比较特殊*/
scanf("%c", &ch);
p = (DulNode *)malloc(sizeof(DulNode));
p->data = ch;
p->prior = *L;
p->next = NULL;
(*L)->next = p;
/*接下来依次生成新结点,插入到表头*/
for (i = 1; i < n; i++) {
scanf("%c", &ch);
/*也可以用getchar()读取字符*/
p = (DulNode *)malloc(sizeof(DulNode));
p->data = ch;
p->proir = *L;
p->next = (*L)->next;
(*L)->next->prior = p;
(*L)->next = p;
}
}
2.5尾插法创建双向链表
/*这里假设数据元素为字符类型,并且存储空间充足*/
typedef char ElemType;
void CreateDuLinkList_Tail(DuLinkList *L, int n)
{
DulNode *p, *q;
int i;
char ch;
/*初始化,建立一个只有头节点的空表*/
*L = (LinkList)malloc(sizeof(Node));
p = *L;
/*p始终为指向尾节点的指针*/
for (i = 0; i < n; i++) {
scanf("%c", &ch);
/*也可以用getchar()读取字符*/
q = (Node *)malloc(sizeof(Node));
q->data = ch;
q->prior = p;
q->next = NULL;
/*p的next指针指向新节点,新节点加入表中,成为表尾*/
p->next = q;
/*之后p指针移动到新节点的位置,即表尾的位置*/
p = q;
}
/*链表创建结束*/
p->next = NULL;
}
至此,双向链表的各种操作也已经全部实现了,也可以把循环链表和双向链表组合起来得到双向循环链表,双向循环链表在已经实现了的循环链表的基础上很容易就能构建,自己动手试试吧!下一章,将要学习链表的相关应用