双向链表、循环链表和静态链表
上节我们实现了单链表的基本操作,在实现过程中可以发现,单链表的逻辑关系仅由一个向后的单向指针表示,使得单链表可以很容易地获取其后继结点,而对于其前驱结点,则只能从头遍历链表来获取。这个缺点让单链表使用起来不太方便,因此我们引入几个功能更强大的链式存储结构:双向链表、循环链表和静态链表。
1 双向链表 Double Linked List
1.1 双向链表的定义
顾名思义,双向链表的结点中包含两个指针,分别指向其前驱结点和后继结点。单链表可以非常容易地找到下一个结点,但是回到上一个结点就比较麻烦,而且单链表很多操作必须要借助前驱节点才能完成(例如删除和插入),使用双向链表可以更轻松地解决这些问题。
1.2 双向链表的实现
1.2.1 双向链表结点的定义
/********** 双向链表结点 **********/
typedef int elemType;
typedef struct DNode {
elemType Data; // 数据域
struct DNode *pre; // 指向前驱的指针
struct DNode *next; // 指向后继的指针
} DNode, *DLinkedList;
1.2.2 主要操作的实现
双链表的主要操作包括:
- 头插法建立双链表
- 尾插法建立双链表
- 按位查找结点操作
- 按值查找结点操作
- 插入结点操作
- 删除结点操作
其中第 1-4 步都只需要从头按顺序往后执行即可,只需要涉及一个方向的指针,因此和单链表的操作无异,时间复杂度也相同。只是要注意双链表增加了一个指向前驱的指针,所以代码略有不同。双链表和单链表的主要区别在于插入操作和删除操作。
大部分双链表的实现操作和单链表大同小异,最重要的问题是避免出现链表断裂的情况。
1.2.2.1 插入结点操作
大体上和单链表的插入操作一致,需要注意的是分情况处理前驱结点和后继结点的指针。
下图中黑线为旧有关系,红线为新建立的关系:
需要注意两个问题:
- 观察旧有关系(即黑线1, 2),我们唯一能访问到后继结点 next 的方式就是通过前驱结点 pre 的后继指针(即黑线1)。因此,在确立新结点 n 和后继结点 next 之间的关系(红线2和3)前,一定不能过早地删除 pre 指向 next 的指针(即黑线1) 。简单来说,就是红线2和3一定要在红线4之前建立。
- 还需要注意不是任何时候后继结点 next 都存在,所以需要考虑 next = NULL 时的情况。
/*
* Function: 插入操作
* ----------------------------
* 在指定位置i插入结点。
*/
DNode *listInsert(DLinkedList &L, elemType e, int i){
DLinkedList tmp = getElem(L, i-1); // 首先使用按位查找检测第i-1个结点是否存在,
if (tmp==NULL){ // 即检查插入位置的前驱结点是否存在
return NULL;
}
DNode *n = new DNode; // 创建新结点
n->data = e;
n->next = tmp->next; // 将新结点插入第i-1个结点之后
n->pre = tmp;
if (tmp->next!=NULL){ // 当tmp不是最后一个结点时,
tmp->next->pre = n; // 才需要考虑后继结点问题
}
tmp->next = n;
return n;
}
时间复杂度分析
分析和单链表的插入操作一致,时间复杂度为
O
(
n
)
O(n)
O(n) 。
1.2.2.2 删除结点操作
大体上和单链表的删除操作一致,需要注意分情况处理前驱结点和后继结点的指针。
/*
* Function: 删除操作
* ----------------------------
* 删除在指定位置i的结点。
*/
elemType listDelete(DLinkedList &L, int i){
DLinkedList tmp = getElem(L, i-1); // 检查删除位置的前驱结点是否存在
if (tmp==NULL || tmp->next==NULL){ // tmp不存在或者tmp就是最后一个结点,
return -1; // 则不需要执行删除操作,直接返回
}
DLinkedList q = tmp->next; // 指针q指向待删除结点
elemType e = q->data; // e用来保存删除结点的值
tmp->next = q->next; // 断开q与其他结点在链表中连接关系
if (q->next!=NULL){
q->next->pre = tmp;
}
free(q); // 释放结点的存储空间
return e;
}
时间复杂度分析
分析和单链表的删除操作一致,时间复杂度为
O
(
n
)
O(n)
O(n) 。
1.2.3 关于双链表时间复杂度为 O ( 1 ) O(1) O(1) 的说明
根据前文我们的分析,不论是单链表还是双链表,插入和删除操作的时间复杂度都为 O ( n ) O(n) O(n) 。但是有些地方却显示他们的插入操作时间复杂度为 O ( 1 ) O(1) O(1),双链表的删除操作时间复杂度也为 O ( 1 ) O(1) O(1) 。这主要是因为在两种不同的说法中,对于删除和插入的具体操作有不同的要求。
我们之前提到的插入和删除操作都是针对序号来操作的,即只知道要删除的结点的位置或是保存的数值。这就要求我们首先遍历来找到在这个位置的结点的前驱,然后再进行插入单个结点或删除单个结点的操作。不论是否具有前向指针,这个操作都无法避免。其中,按位查找的时间复杂度为 O ( n ) O(n) O(n) ,插入单个节点和删除单个节点的时间复杂度为 O ( 1 ) O(1) O(1) 。因此,针对序号的插入或删除操作的时间复杂度为 O ( n ) O(n) O(n) 。
而另外一种插入或删除操作是直接针对结点来操作的,即已经知道待插入或删除的结点(且有办法直接获取)。
-
如果是插入操作且是向后插入,那么两种链表都可以直接找到后继结点,时间复杂度为 O ( 1 ) O(1) O(1) ;如果是向前插入操作,由于双链表具有前向指针,所以可以直接获取前驱结点,时间复杂度为 O ( 1 ) O(1) O(1) ,而单链表没有前向指针无法直接获取前驱结点,所以还是需要遍历来找到前驱结点,故时间复杂度为 O ( n ) O(n) O(n)
-
如果是删除操作,单链表只知道待删除的结点是没用的,所以还是需要遍历链表来找到前驱结点,时间复杂度为 O ( n ) O(n) O(n) ;而双链表可以直接获取前驱,时间复杂度为 O ( 1 ) O(1) O(1) 。
因此,对于链表操作的时间复杂度需要看清楚该操作到底有什么具体要求才能判断。
2 循环链表 Circular Linked List
2.1 循环链表的定义
循环链表的特点是表尾结点的后继指针指向头结点或是第一位结点(如果不存在头结点),整个链表形成一个环。以没有头结点的单向循环链表为例:
循环链表的特点在于其所有结点的指针域都不会等于 NULL ,这就让表尾结点和其它结点有了一致性,不需要额外检查表尾指针是否为空。为了继续利用这一优势,循环链表经常不包含头结点转而使用头、尾指针,这样就可以使表中所有元素等价,真正形成一个环,环内的所有元素都可以执行相同的操作。
对于单向循环链表来说,尾指针比头指针更实用。因为链表的很多操作都是在表头或表尾进行的,如果采用头指针,那么想要访问表尾结点还是需要遍历整个链表;但是采用尾指针的话,尾指针的下一位就是表头,所以不论访问表头还是表尾,都只需要 O ( 1 ) O(1) O(1) 的时间复杂度。
2.2 循环链表的实现
2.2.1 单向循环链表的结点定义
单向循环链表的结点定义和单链表一样,只是在链表的构建上有区别。
/********** 单向循环链表结点 **********/
typedef int elemType;
typedef struct LNode{
elemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkedList;
2.2.2 主要操作的实现
下列代码采用的是仅含尾指针的单向循环量表。
单向循环链表的操作基本与单链表一致,只是需要主要链表结束的判断条件并不是结点指针为空,而是是否等于尾指针或头指针。
详细操作见附录。
2.2.3 双向循环链表
了解了单向循环链表和双向链表就不难推出双向循环链表的特征,表中结点首尾相连,每个结点都包含两个指针。
3 静态链表
待补充
相关章节
第一节 【绪论】数据结构的基本概念
第二节 【绪论】算法和算法评价
第三节 【线性表】线性表概述
第四节 【线性表】线性表的顺序表示和实现
第五节 【线性表】线性表的链式表示和实现
第六节 【线性表】双向链表、循环链表和静态链表
第七节 【栈和队列】栈
第八节 【栈和队列】栈的应用
第九节 【栈和队列】栈和递归
第十节 【栈和队列】队列
附录
双向链表的实现
/*
* File name: DoubleLinkedList.h
* -----------------------
* Using struct Node to implement double linked list.
*/
#ifndef _DOUBLE_LINKED_LIST_h_
#define _DOUBLE_LINKED_LIST_h_
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
/********** 双向链表结点 **********/
typedef int elemType;
typedef struct DNode {
elemType data;
struct DNode *pre; // 指向前驱的指针
struct DNode *next; // 指向后继的指针
} DNode, *DLinkedList;
/********** 主要操作的实现 **********/
/*
* Function: 头插法建立双链表
* ----------------------------
* 从空表开始生成新结点,并将读取到的数据存放到新结点的数据域中,
* 然后将新结点插入到头结点之后。
*/
DLinkedList headInsert(DLinkedList &L){
DNode *n; // 先声明一个结点指针n,用来指向将来生成的新结点
elemType e; // 元素e,用来接收输入的元素数据
L = new DNode; // 创建头结点
L->pre, L->next = NULL; // 初始为空链表
scanf("%d", &e);
while(e!=-1){
n = new DNode; // 创建新结点
n->data = e;
n->next = L->next; // 将新结点插入头结点之后
n->pre = L;
if (L->next!=NULL){ // 当tmp不是最后一个结点时,
L->next->pre = n; // 才需要考虑后继结点问题
}
L->next = n;
scanf("%d", &e);
}
return L; // 返回头结点
}
/*
* Function: 尾插法建立双链表
* ----------------------------
* 从空表开始生成新结点,并将读取到的数据存放到新结点的数据域中,
* 然后将新结点插入到链表的表尾。
*/
DLinkedList tailInsert(DLinkedList &L){
DNode *n; // 声明新的结点n
DLinkedList t; // 声明尾指针t
elemType e;
L = new DNode; // 创建头结点
L->pre, L->next = NULL;
t = L; // 将指针t指向表尾,此时链表为空,表尾就是头结点
scanf("%d", &e);
while (e!=-1){
n = new DNode; // 创建新结点
n->data = e;
t->next = n; // 完善尾结点和新结点之间的双向指针
n->pre = t;
t = n; // 将尾指针t指向新结点n
scanf("%d", &e);
}
t->next = NULL; // 将新的链表的尾结点指针置空
return L;
}
/*
* Function: 判空操作
* ----------------------------
* 判断链表是否为空。
*/
bool listEmpty(DLinkedList L){
return !L->next; // 如果只有头结点,则链表为空
}
/*
* Function: 求表长操作
* ----------------------------
* 计算单链表中结点的个数,不包括头结点。
*/
int listLength(DLinkedList L){
int count=0; // 用来计算结点个数
DLinkedList tmp=L;
while (tmp->next!=NULL){
tmp = tmp->next;
count++;
}
return count;
}
/*
* Function: 按位查找结点
* ----------------------------
* 在双链表中从第一个结点出发,直到找到第i个结点为止,
* 否则返回最后一个结点指针域NULL。
*/
DNode *getElem(DLinkedList L, int i){
if (i<0){ // 检查序号值是否合法
return NULL;
}
DLinkedList tmp=L; // 创建临时指针,并指向头结点
for (int j=0;j<i;j++){
if (!tmp->next){ // 如果临时指针的指针域为空, 则代表
return NULL; // 临时指针已位于表尾且仍未查到,返回NULL
}
tmp = tmp->next;
}
return tmp; // 如果for循环正常退出,
// 则代表找到,返回临时指针
}
/*
* Function: 按值查找结点
* ----------------------------
* 在双链表中从第一个结点出发,直到找到某个结点的数据域
* 等于目标值为止,否则返回返回NULL。
*/
DNode *locateElem(DLinkedList L, elemType e, int &count){
DLinkedList tmp=L->next; // 创建临时指针,指向头结点指针域
count=1; // 记录查找次数
while (tmp!=NULL && tmp->data!=e){ // 如果临时指针不为空且还未找到目标值,
tmp = tmp->next; // 则继续while循环
count++;
}
return tmp;
}
/*
* Function: 插入操作
* ----------------------------
* 在指定位置i插入结点。
*/
DNode *listInsert(DLinkedList &L, elemType e, int i){
DLinkedList tmp = getElem(L, i-1); // 首先使用按位查找检测第i-1个结点是否存在,
if (tmp==NULL){ // 即检查插入位置的前驱结点是否存在
return NULL;
}
DNode *n = new DNode; // 创建新结点
n->data = e;
n->next = tmp->next; // 将新结点插入第i-1个结点之后
n->pre = tmp;
if (tmp->next!=NULL){ // 当tmp不是最后一个结点时,
tmp->next->pre = n; // 才需要考虑后继结点问题
}
tmp->next = n;
return n;
}
/*
* Function: 删除操作
* ----------------------------
* 删除在指定位置i的结点。
*/
elemType listDelete(DLinkedList &L, int i){
DLinkedList tmp = getElem(L, i-1); // 检查删除位置的前驱结点是否存在
if (tmp==NULL || tmp->next==NULL){ // tmp不存在或者tmp就是最后一个结点,
return -1; // 则不需要执行删除操作,直接返回
}
DLinkedList q = tmp->next; // 指针q指向待删除结点
elemType e = q->data; // e用来保存删除结点的值
tmp->next = q->next; // 断开q与其他结点在链表中连接关系
if (q->next!=NULL){
q->next->pre = tmp;
}
free(q); // 释放结点的存储空间
return e;
}
/*
* Function: 输出操作
* ----------------------------
* 按顺序从头到尾输出单链表的元素
*/
void listPrint(DLinkedList L){
DLinkedList tmp=L;
while (tmp->next!=NULL){
tmp = tmp->next;
printf("%d ", tmp->data);
}
printf("\n");
}
#endif // _DOUBLE_LINKED_LIST_h_
单向循环链表的实现
/*
* File name: CircularCLinkedList.h
* -----------------------
* Using struct Node to implement single circular linked list.
* Single circular linked list only possess tail pointer.
*/
#ifndef _CIRCULAR_LINKED_LIST_h_
#define _CIRCULAR_LINKED_LIST_h_
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
/********** 单向循环链表结点 **********/
typedef int elemType;
typedef struct LNode{
elemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *CLinkedList;
/********** 主要操作的实现 **********/
/*
* Function: 头插法建立单向循环链表
* ----------------------------
* 从空表开始生成新结点,并将读取到的数据存放到新结点的数据域中,
* 然后将新结点插入到头结点之后。
*/
CLinkedList headInsert(CLinkedList &L){ // L是尾指针
L = NULL;
LNode *n; // 先声明一个结点指针n,用来指向将来生成的新结点
elemType e; // 元素e,用来接收输入的元素数据
scanf("%d", &e);
while(e!=-1){
n = new LNode; // 创建新结点
n->data = e;
if (!L){
L = n;
L->next = n;
} else {
n->next = L->next->next;
L->next->next = n;
}
scanf("%d", &e);
}
return L; // 返回头结点
}
/*
* Function: 尾插法建立单向循环链表
* ----------------------------
* 从空表开始生成新结点,并将读取到的数据存放到新结点的数据域中,
* 然后将新结点插入到链表的表尾。
*/
CLinkedList tailInsert(CLinkedList &L){ // L这里就是尾指针
L = NULL;
LNode *n; // 声明新的结点指针n
elemType e;
scanf("%d", &e);
while (e!=-1){
n = new LNode; // 创建新结点n
n->data = e;
if (!L){
L = n;
L->next = n;
} else {
n->next = L->next; // 令新结点n指向表头结点
L->next = n; // 将新结点n加入链表
L = n; // 将尾指针指向新结点n
}
scanf("%d", &e);
}
return L;
}
/*
* Function: 求表长操作
* ----------------------------
* 计算单向循环链表中结点的个数。
*/
int listLength(CLinkedList L){
int count=1; // 用来计算结点个数
CLinkedList tmp=L;
while (tmp->next!=L){
tmp = tmp->next;
count++;
}
return count;
}
/*
* Function: 按位查找结点
* ----------------------------
* 在单向循环链表中从第一个结点出发,直到找到第i个结点为止,
* 否则返回最后一个结点指针域NULL。
*/
LNode *getElem(CLinkedList L, int i){
if (i<=0){ // 检查序号值是否合法
return NULL;
}
CLinkedList tmp=L->next; // 创建临时指针,并指向第一个结点
for (int j=1;j<i;j++){
if (tmp==L){ // 如果临时指针的指向表尾结点, 则代表
return NULL; // 临时指针已位于表尾且仍未查到,返回NULL
}
tmp = tmp->next;
}
return tmp; // 如果for循环正常退出,
// 则代表找到,返回临时指针
}
/*
* Function: 按值查找结点
* ----------------------------
* 在单向循环链表中从第一个结点出发,直到找到某个结点的数据域
* 等于目标值为止,否则返回返回NULL。
*/
LNode *locateElem(CLinkedList L, elemType e, int &count){
CLinkedList tmp=L->next; // 创建临时指针,指向第一个结点
count=1; // 记录查找次数
while (true){
if (tmp->data==e){ // 如果找到目标结点,则直接返回tmp
return tmp;
}
if (tmp==L){ // 如果临时指针回到表尾且还未找到目标值,
return NULL; // 则直接返回NULL
}
tmp = tmp->next;
count++;
}
}
/*
* Function: 插入操作
* ----------------------------
* 在指定位置i插入结点。
*/
LNode *listInsert(CLinkedList &L, elemType e, int i){
CLinkedList tmp = getElem(L, i-1); // 首先使用按位查找检测第i-1个结点是否存在,
if (tmp==NULL){ // 即检查插入位置的前驱结点是否存在
return NULL;
}
LNode *n = new LNode; // 创建新结点
n->data = e;
if (tmp==L){ // 确保尾指针指向最后一个结点
L=n;
}
n->next = tmp->next; // 将新结点插入第i-1个结点之后
tmp->next = n;
return tmp;
}
/*
* Function: 删除操作
* ----------------------------
* 删除在指定位置i的结点。
*/
elemType listDelete(CLinkedList &L, int i){
CLinkedList tmp = getElem(L, i-1); // 检查删除位置的前驱结点是否存在
if (tmp==NULL || tmp->next==NULL){ // tmp不存在或者tmp就是最后一个结点,
return -1; // 则不需要执行删除操作,直接返回
}
CLinkedList q = tmp->next; // 指针q指向待删除结点
elemType e = q->data; // e用来保存删除结点的值
if (q==L){ // 如果删除尾结点,则需先移动尾指针
L = tmp;
}
tmp->next = q->next; // 断开q其他结点在链表中连接关系
free(q); // 释放结点的存储空间
return e;
}
/*
* Function: 输出操作
* ----------------------------
* 按顺序从头到尾输出单向循环链表的元素
*/
void listPrint(CLinkedList L){
CLinkedList tmp=L->next;
while (true){
printf("%d ", tmp->data);
if (tmp==L){
break;
}
tmp = tmp->next;
}
printf("\n");
}
#endif // _CIRCULAR_LINKED_LIST_h_