本文的所有代码均由C++编写
4 双链表、循环链表和静态链表
文章目录
- 4 双链表、循环链表和静态链表
- 4.1 双链表
- 4.1.1 双链表的定义
- 4.1.2 双链表的初始化
- 4.1.2 双链表的后插操作
- 4.1.3 双链表的后删操作
- 4.1.4 双链表的销毁操作
- 4.2 循环链表
- 4.2.1 循环链表的概念
- 4.2.2 循环单链表
- 4.2.2.1 循环单链表的初始化
- 4.2.2.2 循环单链表判空
- 4.2.2.3 判断表尾结点
- 4.2.3 循环双向链表
- 4.2.3.1 循环双链表的初始化
- 4.2.3.2 循环双链表的判空
- 4.2.3.3 循环双链表的插入
- 4.2.3.4 循环双链表的删除
- 4.3 静态链表
- 4.3.1 静态链表的定义
- 4.3.2 静态链表的初始化
- 4.3.3 静态链表的插入
- 4.3.4 静态链表的删除
- 4.3.5 静态链表后话
4.1 双链表
4.1.1 双链表的定义
在前面的知识中我们曾经说过,由于单链表中每个数据元素分为两部分——数据域和指针域,所以单链表只能寻找下一个元素而无法寻找上一个元素,即无法逆向检索,为了解决这个问题,我们引入了双链表。
双链表的节点用DNode(D指的是Double)命名,其在原有的数据域和指针域的基础上加了一个前置指针域(prior)
。定义如下所示:
typedef struct DNode{ElemType data;struct DNode *prior,*next;
}DNode,*DLinklist;
4.1.2 双链表的初始化
双链表对于单链表来说,初始化就有所不同了。
bool InitDLinkList(DLINKLIST &L){L = new DNode;if(L == NULL)return false;L->prior = NULL;L->next = NULL;return true;
}
4.1.2 双链表的后插操作
同样地,对于单链表熟悉插入操作的同学,双链表插入也会很熟悉,但是需要注意一点的同样是修改指针顺序问题。
bool InsertNextDNode(DNode *p,DNode *s){if(p == NULL || s== NULL)retur false;s->next = p->next;if(p->next != NULL)p->next->prior = s;s->prior = p;p->next = s;return true;
}
4.1.3 双链表的后删操作
bool DeleteNextDNode(DNode *p){if(p == NULL)return false;DNode *q = p->next;if(q == NULL)return false;p->next = q->next;if(q->next != NULL)q->next->prior = p;delete(q);return true;
}
4.1.4 双链表的销毁操作
void DestoryList(DLinklist &L)
{while(L->next != NULL)DeleteNextDNode(L);delete(L);L=NULL;
}
4.2 循环链表
4.2.1 循环链表的概念
在开篇,先让我们了解一下接下来要讲的循环列表
的概念。
循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。
需要注意的是,由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。
//循环条件 单链表: p! = NULL p->next != NULL//循环单链表: p! = L p->next! = L
4.2.2 循环单链表
在前面学习单链表的时候我们知道,如果对于某一个结点p,其前驱结点是不知道在哪的。但是对于循环单链表来说,从表中任一结点出发均可找到表中其他结点,因为表是循环的。
对于普通的单链表来说,从头结点找到尾部,时间复杂度是O(n);对于循环链表来说同样如此,但是如果你是从尾部开始找到头部,那么时间复杂度实际为O(1)。所以对于很多操作如果需要频繁地对表头表尾动手,那你可以在初始化工作
的时候让L指针指向表尾元素。
4.2.2.1 循环单链表的初始化
bool InitList(LinkList &L){L = new LNode; //分配一个头结点if(L == NULL) //内存不足分配失败return false;L->next = L; //头结点next指向头结点return true;
}
4.2.2.2 循环单链表判空
bool Empty(LinkList L){if(L->next == L)return true;elsereturn false;
}
4.2.2.3 判断表尾结点
bool isTail(LinkList L,LNode *p){if(p->next == L)return true;elsereturn false;
}
4.2.3 循环双向链表
虽然循环单链表可以从任意一个结点开始找到任意一个结点,但是如果要查找的结点刚好是自己所处的前一个结点,那岂不是浪费时间兜圈子?为此,和单链的循环表类似,双向链表也可以有循环表,我们叫做循环双向表
。如果一旦发生上述情况,直接通过前置指针即可访问。
4.2.3.1 循环双链表的初始化
对于其定义,我们上个小节谈论过了,如下:
//循环双链表的定义
typedef struct DNode
{int data;struct DNode* prior,*next;
}DNode,*DLinklist;
对于其初始化如下:
//初始化循环双链表
bool InitDList(DLinklist& L)
{L = new DNode;if (L == NULL)return false;L->prior = L;L->next = L;return true;
}
对于初始化双向链表来说,首先要注意内存空间不足的问题,这是在前面的单链表初始化中也需要注意的;第二个不一样的点就是:由于是循环链表,其后继指针域指向的不是空,而需要指自身;同样地,前驱指针域也是如此。
4.2.3.2 循环双链表的判空
如果要判断双链表是否为空,那无非就是判断是否像初始化时一样,前驱指针
和后继指针
都指向头结点
,如下:
//判断循环双链表是否为空
bool Empty(DLinklist L)
{if (L->next == L)return true;elsereturn false;
}
4.2.3.3 循环双链表的插入
如果要对循环双链表做插入操作,需要注意改结点不止改一根指针了。如果要在循环双链表中的结点p后插入结点s,如下所示:
bool InsertNextDNode(DNode* p, DNode* s)
{s->next = p->next;p->next->prior = s;s->prior = p;p->next = s;
}
4.2.3.4 循环双链表的删除
对于删除,和插入一样:
//删除p->next = q->next;q->next->prior = p;free(q);
2.5.2、两个链表合并
如何将两个带有尾指针的链表合并
思路分析:
操作分析:
//用p存表头结点p = Ta->next;//Tb表头连接Ta表尾Ta -> next = Tb ->next ->next; //释放Tb表头结点delete Tb->next;//修改指针Tb -> next = p;
具体代码:
LinkList Counnect(LinkList Ta,LinkList Tb){//假设Ta、Tb都是非空的单循环链表p = Ta->nextl//p存表头结点Ta->next = Tb->next->next;//Tb连Ta表尾delete Tb->next;//释放Tb表头结点Tb->next = p;/return Tb;
}
4.3 静态链表
在考研中极少考察静态链表的代码实现。所以考研人可以跳步地观看这一小节。
在早期的编程环境中,并没有像C语言一样这么高级的指针机制,如果失去了指针这一工具,我们前面讲的链表结构就失效了。为此,人们想出了用数组
来代替指针来描述单链表。
我们让数组的元素由两个数据域组成,data和cur。也就是说,数组的索引下标对应一个data和一个cur。数据域data用来存放数据元素,而游标cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。如果用一幅图描述如下所示:
我们把这种用数组描述的链表叫做静态链表
,有的书上也叫做游标实现法
。
4.3.1 静态链表的定义
对于静态链表来说,由于其实际上是用数组去存放的,所以需要一整片连续的空间,存储密度高;并且由于数组具有静态的特点,所以为了方便插入数据,我们通常会把数组建立得大一些,以便插入空闲空间可以方便插入时不至于溢出。
#define MAXSIZE 100
typedef struct
{ElemType data;int cur
}Component,StaticLinkList [MAXSIZE];
4.3.2 静态链表的初始化
在单链表的初始化中,我们是将头结点的指针域置空。对应到静态链表来,我们可以将其游标设为-1,表示数组中没有索引可以给其指向。
Status InitList(StaticLinkList space)
{int i;for(i = 0;i<MAXSIZE-1;i++)space[i].cur = -2;space[MAXSIZE-1].cur = 0;return OK;
}
针对上面的代码实际上我是做了部分改进的,for循环的作用是为数组中每个元素的游标附上一个-2,这是因为内存中本来就含有脏数据,如果我们不对游标做初始化,里面还是有数据,在添加元素的时候就无法判别哪个数组元素是空位置。当然如果你愿意,-2也可以是其他负数或是一些特定的数字。
4.3.3 静态链表的插入
对于静态链表来说,由于其不像单链表一样可以生成结点,也没有可以释放结点的功能,所以这两个功能我们必须自己实现。
对于插入位序为i的结点来说,我们可以分以下步骤进行:
- 找到一个空的结点,存入数据元素
- 从头结点除法找到位序为i-1的结点
- 修改新结点的next
- 修改i-1号结点的next
4.3.4 静态链表的删除
如果想要删除一个节点,可以分以下步骤进行:
- 从头结点出发找到前驱结点
- 修改前驱结点的游标
- 被删除结点的next设为-2(设为初始化值即可)
4.3.5 静态链表后话
静态链表的优点和缺点都显而易见。优点是在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了顺序存储结构中插入和删除操作需要移动大量元素的缺点。
而其缺点是:其虽然是顺序存储,却硬要实现链表,这就导致了其失去了随机存取的特性,而且由于数组长度固定不变,导致表长不能扩展。(虽然可以用动态数组,但那也太麻烦了)
总的来说,静态链表还是在一些地方适用的,比如在一些不支持指针的低级语言和一些数组元素数量固定不变的场景(如操作系统的文件分配表FAT)还是比较适用的。