线性表
线性表按照存储结构分为 顺序表和 链表。
顺序表的特点:逻辑上相连的数据元素,其物理次序也是相邻的。
链表的特点:用任意的存储单元存储线性表的数据元素(可以连续,也可以不连续)
1. 顺序表
存储结构的分类:
- 随机存取(任意存取):知道起始位置就可以通过下标直接访问到元素的位置,与存储位置无关。
- 顺序存取:不能通过下标访问,在存取第N个数据时,必须先访问前(N-1)个数据 。
顺序表属于随机存取。
1.1 存储结构表示
顺序表的数据元素是按照数组一个接一个紧密排列的。
对顺序表进行操作时,需要一个“操作台”,使用这个操作台能够访问到顺序表中的任意一个元素,同时也要保存该顺序表的大小,方便用户了解顺序表的信息,确保不会越界操作。
上图的顺序表结构类型就是这个操作台。
typedef struct
{
ElemType *elem; //顺序表的基地址
int length; //顺序表当前总长度
}SqList;//顺序表结构类型
其中ElemType
代表顺序表存放的数据类型。
用顺序表的方式来存储一下图书数据:
ISBN | 书名 | 定价 |
---|---|---|
9787302257646 | 程序设计基础 | 25 |
9787302219972 | 单片机技术与应用 | 32 |
9787302203513 | 编译原理 | 46 |
每个图书的信息有ISBN、书名和定价3个元素组成,因此定义图书信息的结构体来包含这3个元素:
typedef struct
{
char no[20]; //ISBN
char name[50]; //书名
float price; //定价
}Book;
这个线性表的数据元素是Book
,因此可以确定对应“操作台”里面的数据元素:
typedef struct
{
Book *elem; //顺序表的基地址
int length; //顺序表当前总长度
}SqList;
将上面顺序表结构类型的
ElemType
换成Book
即可。
1.2 初始化
顺序表的初始化操作就是构造1个空的顺序表。
算法步骤:
- 为顺序表动态分配1个预定义大小的数组空间,再使“操作台”的
elem
指向这块空间的基地址 - 将“操作台”的
length
设置为0,因为空表还没有元素
#define MAXSIZE 100 //数据可能到达的长度
Status InitList(SqList *L)
{
L->elem = (ElemType*)malloc(sizeof(ElemType)*MAXSIZE); //为顺序表分配指定大小的空间
if (L->elem == NULL) exit(OVERFLOW); //内存分配失败就退出
L->length = 0; //空表长度为0
return OK;
}
其中,ElemType
是数据元素的类型,SqList
是操作台的类型,Status
是函数的返回值,代表函数的结果状态。
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
1.3 取值
获取顺序表第i
个数据元素的值。
时间复杂度O(1)
算法步骤:
- 判断序号
i
是否合理(1≤i≤length
),不合理就返回ERROR
- 合理就取出第
i
个数据元素的值,放到参数e
中
Status GetElem(const SqList *L, int i, ElemType *e)
{
if ( (i<1) || (i>(L->length)) ) return ERROR; //判断i是否合理,不合理返回ERROR
*e = (L->elem)[i-1]; //将elem[i-1]单元的数据赋给e L->elem代表数组首元素地址
return OK;
}
1.4 查找
查找指定元素e
。若查找成功,则返回该元素在顺序表的位置序号;若查找失败,则返回0。
时间复杂度O(n)
算法步骤:
- 从第1个元素起,依次和指定元素
e
比较,若查找成功,返回元素的序号+1 - 整个顺序表找完都没有找到指定元素
e
,则返回0
Status LocateElem(const SqList *L, ElemType e)
{
int i = 0;
for (i=0; i<L->length; i++)
{
if ((L->elem)[i] == e)
return i+1; //查找成功,返回序号+1
}
return 0; //找遍整个顺序表都没有找到,查找失败,返回0
}
1.5 插入
在顺序表的第i
个位置插入新的数据元素e
,同时顺序表的长度要+1。
时间复杂度O(n)
算法步骤:
- 判断插入的位置
i
是否合法(1≤i≤length+1
),不合理就返回ERROR
- 判断顺序表的存储空间是否已满,若满的话返回
ERROR
- 将原先的第
n
个至第i
个元素依次后移1个位置(i=n+1
时无需移动) - 在第
i
个位置插入新的数据元素e
- 顺序表的数据长度+1
Status ListInsert(SqList *L, int i, ElemType e)
{
int j = 0;
if ( (i<1) || (i>(L->length)+1) ) return ERROR; //1. 判断i是否合理,不合理返回ERROR
if (L->length == MAXSIZE) return ERROR; //2. 判断顺序表的存储空间是否已满
for (j=(L->length)-1; j>=i-1; j--)
{
(L->elem)[j+1] = (L->elem)[j]; //3. 插入位置及之后的元素后移
}
(L->elem)[i-1] = e; //4. 将新的数据元素e放到第i个位置
L->length++; //5. 表长加1
return OK;
}
1.6 删除
将顺序表的第i
个位置的数据元素删除,同时顺序表的长度要-1。
时间复杂度O(n)
算法步骤:
- 判断插入的位置
i
是否合法(1≤i≤length
),不合理就返回ERROR
- 将原先的第
i+1
个至第n
个元素依次前移1个位置(i=n
时无需移动) - 顺序表的数据长度-1
Status ListDelete(SqList *L, int i)
{
int j = 0;
if ( (i<1) || (i>(L->length)) ) return ERROR; //判断i是否合理,不合理返回ERROR
for (j=i; j<(L->length); j++)
{
(L->elem)[j-1] = (L->elem)[j]; //被删除元素之后的元素前移
}
L->length--; //表长减1
return OK;
}
顺序表缺点:在做插入或删除的操作时,需移动大量元素。
1.7 案例
给出一个简单的案例,将数据元素类型ElemType
设置为int
,所以这是一个数据元素为int
的顺序表。
包含上述5种操作,便于理解、应用。
下述代码运行环境:VS2019,其他环境可移植。
#include <stdio.h>
#define MAXSIZE 100 //数据可能到达的长度
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
typedef struct
{
int* elem; //顺序表的基地址
int length; //顺序表当前总长度
}SqList;//顺序表结构类型
// 顺序表的初始化
// 函数参数:L -> 操作台地址
// 返回值:1代表运行正常
Status InitList(SqList* L)
{
L->elem = (int*)malloc(sizeof(int) * MAXSIZE); //为顺序表分配指定大小的空间
if (L->elem == NULL) exit(OVERFLOW); //内存分配失败就退出
L->length = 0; //空表长度为0
return OK;
}
// 获取顺序表第i个数据元素的值
// 函数参数:L -> 操作台地址; i -> 获取第i个元素; e -> 存放元素的地址
// 返回值:1代表运行正常; 0代表运行有问题
Status GetElem(const SqList* L, int i, int* e)
{
if ((i < 1) || (i > (L->length))) return ERROR; //判断i是否合理,不合理返回ERROR
*e = (L->elem)[i - 1]; //将elem[i-1]单元的数据赋给e L->elem代表数组首元素地址
return OK;
}
// 查找指定元素e
// 函数参数:L -> 操作台地址; e -> 查找的指定元素
// 返回值:0代表运行有问题; 其他表示指定元素的编号
Status LocateElem(const SqList* L, int e)
{
int i = 0;
for (i = 0; i < L->length; i++)
{
if ((L->elem)[i] == e)
return i + 1; //查找成功,返回序号+1
}
return 0; //找遍整个顺序表都没有找到,查找失败,返回0
}
// 在顺序表的第i个位置插入新的数据元素e
// 函数参数:L -> 操作台地址; i -> 第i个位置; e -> 插入的新元素
// 返回值:1代表运行正常; 0代表运行有问题
Status ListInsert(SqList* L, int i, int e)
{
int j = 0;
if ((i < 1) || (i > (L->length) + 1)) return ERROR; //判断i是否合理,不合理返回ERROR
if (L->length == MAXSIZE) return ERROR; //判断顺序表的存储空间是否已满
for (j = (L->length) - 1; j >= i - 1; j--)
{
(L->elem)[j + 1] = (L->elem)[j]; //插入位置及之后的元素后移
}
(L->elem)[i - 1] = e; //将新的数据元素e放到第i个位置
L->length++; //表长加1
return OK;
}
// 将顺序表的第i个位置的数据元素删除
// 函数参数:L -> 操作台地址; i -> 第i个位置
// 返回值:1代表运行正常; 0代表运行有问题
Status ListDelete(SqList* L, int i)
{
int j = 0;
if ((i < 1) || (i > (L->length))) return ERROR; //判断i是否合理,不合理返回ERROR
for (j = i; j < (L->length); j++)
{
(L->elem)[j - 1] = (L->elem)[j]; //被删除元素之后的元素前移
}
L->length--; //表长减1
return OK;
}
int main()
{
SqList simpleData; //创建顺序表的操作台
int getData = 0; //保存第i个数据元素的值
int checkData = 1; //存储查找到元素的编号
//初始化操作台
InitList(&simpleData);
ListInsert(&simpleData, 1, 4); //顺序表插入数据,第1个位置插入4
ListInsert(&simpleData, 2, 7); //顺序表插入数据,第2个位置插入7
ListInsert(&simpleData, 3, 1); //顺序表插入数据,第3个位置插入1
GetElem(&simpleData, 1, &getData);
printf("%d\n", getData); //打印取到的元素
GetElem(&simpleData, 2, &getData);
printf("%d\n", getData); //打印取到的元素
GetElem(&simpleData, 3, &getData);
printf("%d\n\n", getData); //打印取到的元素
if (checkData = LocateElem(&simpleData, 4)) //在顺序表中查找元素,并记录其位置信息
{
GetElem(&simpleData, checkData, &getData); //获取对应位置信息的元素
printf("GetElem : %d\n\n", getData); //打印取到的元素
}
ListDelete(&simpleData, 2); //删除第2个元素
GetElem(&simpleData, 2, &getData);
printf("Change Elem : %d\n\n", getData); //打印取到的元素; 应该打印为原先第3个元素的值
//获取原先第3个元素的数值,因为删掉了,所以函数运行出错
if (GetElem(&simpleData, 3, &getData))
{
printf("GetElem Success!\n");
}
else
{
printf("Delete Success! GetElem Error!\n");
}
return 0;
}
2. 链表
存储结构的分类:
- 随机存取(任意存取):知道起始位置就可以通过下标直接访问到元素的位置,与存储位置无关。
- 顺序存取:不能通过下标访问,在存取第N个数据时,必须先访问前(N-1)个数据 。
链表属于顺序存取。
2.1 存储结构表示
对于链表而言,不仅要存储数据本身的信息,还需要存储一个指示其直接后继的信息,这就是结点的组成。
结点: 数据域+指针域
其中上图的L为头指针,指向链表的首元素。
链表的结点:
typedef struct LNode
{
ElemType data; //结点的数据域
struct LNode* next; //结点的指针域
}LNode, *LinkList; //Linklist为指向结构体LNode的指针类型
其中ElemType
代表顺序表存放的数据类型。
通常习惯用
Linklist
定义单链表的头指针(上图中的L)。用
LNode *
定义指向单链表中任意结点的指针变量。
为了方便处理,在单链表的第一个结点前会附设一个结点,称之为头结点,下图为添加头结点的链表:
明确上图中的概念:
-
头指针是指向链表中第一个结点的指针。
若链表有头结点,则头指针就指向链表的头结点;
若链表没有头结点,则头指针就指向链表的首元结点。
-
头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。
头结点的数据域可以不存储任何信息,也可以存储数据元素类型相同的其他附加信息——链表的长度等。
-
首元结点是链表中存储第一个数据元素 a 0 a_0 a0的结点。
增加头结点的好处:
-
首元结点的地址保存在头结点的指针域当中,此时对首元结点的操作和其他结点操作相同,无需特殊处理。
-
便于空表和非空表的统一处理。
-
无头结点时,头指针指向链表的首元结点;空链表时
L == NULL
,并不指向首元结点(因为没有) -
增加头结点后,头指针都是指向头结点的非空指针。空表的判断条件:
L->next == NULL
-
上图是非空链表。
上图是空链表。
2.2 初始化
下图就是链表初始化的顺序及结果:
算法步骤:
- 生成头结点,并用头指针指向头结点
- 头结点的指针域置空
Status InitList(LinkList* L)
{
*L = (LinkList)malloc(sizeof(LNode)); //生成头结点,并用头指针指向头结点
(*L)->next = NULL; //头结点的指针域置空
return OK;
}
2.3 取值
获取顺序表第i
个数据元素的值。
时间复杂度O(n)
算法步骤:
- 用指针
p
指向首元结点,用j
做计数器,初值为1 - 从首元结点开始依次顺着链域
next
向下寻找。只要p
不为NULL
(链表结束),并且还没有到第i
个结点,p
就指向下一个结点,同时计数器j
的值加1 - 退出查找后,如果指针
p
为NULL
(从头找到尾都没有找到),或者计数器j
大于i
(i≤0
),表示取值失败;否则取值成功,用e
来保存取到的数值
Status GetElem(const LinkList* L, int i, ElemType *e)
{
LinkList p = (*L)->next; //p指向首元结点
int j = 1; //设置计数器
while ( (p!=NULL) && (j<i) ) //循环结束标志:p为空指针,i<1(输入非法)
{
p = p->next; //p指向下一个结点
++j;
}
if (p==NULL || j>i) return ERROR; //从头找到尾都没有找到,或者输入的i非法
*e = p->data; //存储找到的数据
return OK;
}
2.4 查找
查找指定元素e
。若查找成功,则返回该结点的地址;若查找失败,则返回NULL
。
时间复杂度O(n)
算法步骤:
- 用指针
p
指向首元结点 - 从首元结点开始依次顺着链域
next
向下寻找。只要p
不为NULL
(链表结束),并且p指向结点的数据域不等于给定值e
,p
就指向下一个结点 - 返回
p
。若查找成功,p
就是该结点的地址;若查找失败,p
的值就是NULL
LNode* LocateElem(const LinkList* L, ElemType e)
{
LinkList p = (*L)->next; //p指向首元结点
while ( (p!=NULL) && (p->data!=e) ) //循环结束标志:p为空指针; 找到了e
{
p = p->next; //p指向下一个结点
}
return p;
}
2.5 插入
在链表的第i
个位置插入新的结点,其数据域为e
,即插入到结点
a
i
−
1
a_{i-1}
ai−1与结点
a
i
a_i
ai之间。下图为的过程,共分为5步。
时间复杂度O(n)
算法步骤:
- 查找到结点
a
i
−
1
a_{i-1}
ai−1,并用指针
p
指向该结点 - 生成一个新结点 s s s
- 将新结点
s
s
s的数据域设置为
x
- 将新结点 s s s的指针域指向结点 a i a_i ai
- 将 a i − 1 a_{i-1} ai−1的指针域指向新结点 s s s
Status ListInsert(const LinkList* L, int i ,ElemType e)
{
LinkList p = *L; //p指向头结点
int j = 0;
while ( (p!=NULL) && (j<i-1) ) //1. 查找第i-1个结点,p指向该结点
{
p = p->next; //p指向下一个结点
++j;
}
if (p==NULL || j>i-1) return ERROR; //i过大,或者输入的i过小
LinkList s = (LinkList)malloc(sizeof(LNode)); //2. 生成新结点s
s->data = e; //3. 设置新结点的数据域为e
s->next = p->next; //4. 设置新结点的指针域指向ai
p->next = s; //5. 设置ai-1的指针域指向新结点s
return OK;
}
2.6 删除
将链表的第i
个位置的结点删除。
时间复杂度O(n)
算法步骤:
- 查找到结点
a
i
−
1
a_{i-1}
ai−1,并用指针
p
指向该结点 - 临时保存结点 a i a_i ai的地址在q中,以备释放
- 将
p
的指针域指向结点 a i + 1 a_{i+1} ai+1 - 释放掉结点 a i a_i ai
Status ListDelete(const LinkList* L, int i)
{
LinkList p = *L; //p指向头结点
LinkList q = NULL;
int j = 0;
while ( (p->next!=NULL) && (j<i-1) ) //1. 查找第i-1个结点,p指向该结点
{
p = p->next; //p指向下一个结点
++j;
}
if (p->next!=NULL || j>i-1) return ERROR; //i过大,或者输入的i过小
q = p->next; //2. 临时保存结点ai的地址在q中,以备释放
p->next = q->next; //3. 将p的指针域指向结点ai+1
free(q); //4. 释放掉结点ai
return OK;
}
注:链表函数里面前3行的初始化和判断条件很重要!
p
的初始值:p=*L
还是p = (*L)->next
j
的初始值:j = 0
还是j = 1
- 判断条件:
p != NULL
还是p->next != NULL
这3者要配合着来看待,特别是针对首元结点的操作。
LinkList p = *L; //p指向头结点
int j = 0;
while ((p != NULL) && (j < i - 1)) //查找第i-1个结点,p指向该结点
{
p = p->next; //p指向下一个结点
++j;
}
2.7 案例
给出一个简单的案例,将数据元素类型ElemType
设置为int
,所以这是一个数据元素为int
的链表。
包含上述5种操作,便于理解、应用。
下述代码运行环境:VS2019,其他环境可移植。
#include <stdio.h>
#define MAXSIZE 100 //数据可能到达的长度
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
typedef struct LNode
{
int data; //结点的数据域
struct LNode* next; //结点的指针域
}LNode, * LinkList; //Linklist为指向结构体LNode的指针类型
// 链表的初始化
// 函数参数:L -> 链表头指针地址
// 返回值:1代表运行正常
Status InitList(LinkList* L)
{
*L = (LinkList)malloc(sizeof(LNode)); //生成头结点,并用头指针指向头结点
(*L)->next = NULL; //头结点的指针域置空
return OK;
}
// 获取链表第i个数据元素的值
// 函数参数:L -> 头指针地址; i -> 获取第i个元素; e -> 存放元素的地址
// 返回值:1代表运行正常; 0代表运行有问题
Status GetElem(const LinkList* L, int i, int* e)
{
LinkList p = (*L)->next; //p指向首元结点
int j = 1; //设置计数器
while ((p != NULL) && (j < i)) //循环结束标志:p为空指针,i<1(输入非法)
{
p = p->next; //p指向下一个结点
++j;
}
if (p == NULL || j > i) return ERROR; //从头找到尾都没有找到,或者输入的i非法
*e = p->data; //存储找到的数据
return OK;
}
// 查找指定元素e
// 函数参数:L -> 头指针地址; e -> 查找的指定元素
// 返回值:该元素所在结点的地址
LNode* LocateElem(const LinkList* L, int e)
{
LinkList p = (*L)->next; //p指向首元结点
while ((p != NULL) && (p->data != e)) //循环结束标志:p为空指针; 找到了e
{
p = p->next; //p指向下一个结点
}
return p;
}
// 在链表的第i个位置插入新的数据元素e
// 函数参数:L -> 头指针地址; i -> 第i个位置; e -> 插入的新元素
// 返回值:1代表运行正常; 0代表运行有问题
Status ListInsert(const LinkList* L, int i, int e)
{
LinkList p = *L; //p指向头结点
int j = 0;
while ((p != NULL) && (j < i - 1)) //1. 查找第i-1个结点,p指向该结点
{
p = p->next; //p指向下一个结点
++j;
}
if (p == NULL || j > i - 1) return ERROR; //i过大,或者输入的i过小
LinkList s = (LinkList)malloc(sizeof(LNode)); //2. 生成新结点s
s->data = e; //3. 设置新结点的数据域为e
s->next = p->next; //4. 设置新结点的指针域指向ai
p->next = s; //5. 设置ai-1的指针域指向新结点s
return OK;
}
// 将链表的第i个位置的数据元素删除
// 函数参数:L -> 头指针地址; i -> 第i个位置
// 返回值:1代表运行正常; 0代表运行有问题
Status ListDelete(const LinkList* L, int i)
{
LinkList p = *L; //p指向头结点
LinkList q = NULL;
int j = 0;
while ((p->next != NULL) && (j < i - 1)) //1. 查找第i-1个结点,p指向该结点
{
p = p->next; //p指向下一个结点
++j;
}
if (p->next != NULL || j > i - 1) return ERROR; //i过大,或者输入的i过小
q = p->next; //2. 临时保存结点ai的地址在q中,以备释放
p->next = q->next; //3. 将p的指针域指向结点ai+1
free(q); //4. 释放掉结点ai
return OK;
}
int main()
{
LinkList myList; //创建头指针
int getData = 0; //保存第i个数据元素的值
LNode* checkData = NULL; //存储查找到的元素所在结点的地址
//初始化操作台
InitList(&myList);
ListInsert(&myList, 1, 9); //链表插入数据,第1个位置插入9
ListInsert(&myList, 2, 8); //链表插入数据,第2个位置插入8
ListInsert(&myList, 3, 7); //链表插入数据,第3个位置插入7
GetElem(&myList, 1, &getData);
printf("%d\n", getData); //打印取到的元素
GetElem(&myList, 2, &getData);
printf("%d\n", getData); //打印取到的元素
GetElem(&myList, 3, &getData);
printf("%d\n\n", getData); //打印取到的元素
if (checkData = LocateElem(&myList, 8)) //在链表中查找元素,并记录其位置信息
{
printf("GetElem : %d\n\n", checkData->data); //打印取到的元素
}
ListDelete(&myList, 2); //删除第2个元素
GetElem(&myList, 2, &getData);
printf("Change Elem : %d\n\n", getData); //打印取到的元素; 应该打印为原先第3个元素的值
//获取原先第3个元素的数值,因为删掉了,所以函数运行出错
if (GetElem(&myList, 3, &getData))
{
printf("GetElem Success!\n");
}
else
{
printf("Delete Success! GetElem Error!\n");
}
return 0;
}
3. 顺序表和链表的比较
顺序表 | 链表 | |
---|---|---|
存储空间 | 预先分配,会导致空间闲置或溢出现象 | 动态分配,不会出现存储空间闲置或溢出现象 |
存储密度 | 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 |
存取元素 | 随机存取,按位置访问元素的时间复杂度为O(1) | 顺序存取,按位置访问元素的时间复杂度为O(n) |
插入、删除 | 平均移动约表中一半元素,时间复杂度为O(n) | 不需移动元素,确定插入、删除位置后,时间复杂度为O(1) |
适用情况 | 表长变化不大,且能事先确定变化的范围;很少进行插入或删除操作,经常按元素位置序号访问数据元素 | 长度变化较大;频繁进行插入或删除操作 |
4. 单链表的创建
4.1 头插法
将新结点逐个插入链表的头部(头结点之后)来创建链表,简而言之就是将新结点插入到头结点之后。
算法步骤:
- 创建一个只有头结点的空链表
- 循环n次,插入链表的元素
- 生成一个新结点p
- 将要插入的元素放到p的数据域中
- 将新结点p放入到头结点之后
void CreateList_H(const LinkList* L, int n)
{
LinkList L = (LinkList)malloc(sizeof(LNode)); //1. 创建一个只有头结点的空链表
L->next = NULL;
int i = 0;
for (i=0; i<n; ++i)
{
LinkList p = (LinkList)malloc(sizeof(LNode)); //2.1 生成一个新结点p
scanf("%EelemType",&(p->data)); //%ElemType是结点数据的类型,自行更改
//cin>>p->data; //2.2 将要插入的元素放到p的数据域中
p->next = L->next;
L->next = p; //2.3 将新结点p放入到头结点之后
}
}
4.2 尾插法
将新结点逐个插入链表的尾部来创建链表。相较于头插法,需要增加一个尾指针来指向链表的尾结点。
算法步骤:
- 创建一个只有头结点的空链表
- 初始化尾指针r,指向头结点
- 循环n次,插入链表的元素
- 生成一个新结点p
- 将要插入的元素放到p的数据域中
- 将新结点p放入到尾指针r指向的结点后
- 尾指针r指向新的尾结点p
void CreateList_R(const LinkList* L, int n)
{
LinkList L = (LinkList)malloc(sizeof(LNode)); //1. 创建一个只有头结点的空链表
L->next = NULL;
LinkList r = L;
int i = 0;
for (i=0; i<n; ++i)
{
LinkList p = (LinkList)malloc(sizeof(LNode)); //2.1 生成一个新结点p
scanf("%EelemType",&(p->data)); //%ElemType是结点数据的类型,自行更改
//cin>>p->data; //2.2 将要插入的元素放到p的数据域中
r->next = p; //2.3 将新结点p放入到尾指针r指向的结点后
p->next = NULL;
r = p; //2.4 尾指针r指向新的尾结点p
}
}
5. 循环链表
循环链表的特点:表中最后一个结点的指针域指向头结点,整个链表形成一个环。
由此,从表中任一结点出发均可找到表中其他结点。
下图为空的循环链表:
下图为非空的循环链表:
循环单链表的操作和单链表基本一致,差别仅在于:当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULL
或p->next!=NULL
,而循环单链表的判别条件为p!=L
或p->next!=L
。(L为头指针)
6. 双向链表
双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
双向链表的存储结构如下:
typedef struct DuLNode
{
Elemtype data; //数据域
struct DuLNode *prior; //直接前驱
struct DuLNode *next; //直接后继
}DuLNode, *DuLinkList;
空双向链表头结点的两个指针都指向自身。
其中,双向链表最后一个结点的直接后继指针指向头结点,形成了循环链表。
d为循环双向链表的任意一个结点,循环双向链表的特性可表示成:
d->next->prior = d->prior->next = d
6.1 双向链表的插入
下图为插入新结点时,双向链表的指针变化情况。
Status ListInsert_DuL(const DuLinkList* L, int i ,ElemType e)
{
DuLinkList p = NULL;
if (!(p=GetElem_DuL(L, i)) //在L中确定第i个元素的位置指针
return ERROR; //p为NULL时,第i个元素不存在
DuLinkList s = (DuLinkList)malloc(sizeof(DuLNode)); //生成新结点s
s->data = e; //设置新结点的数据域为e
s->prior = p->prior;//对应上图的第1步
p->prior->next = s; //对应上图的第2步
s->next = p; //对应上图的第3步
p->prior = s; //对应上图的第4步
return OK;
}
6.2 双向链表的删除
下图为删除指定结点时,双向链表的指针变化情况。
Status ListInsert_DuL(const DuLinkList* L, int i ,ElemType e)
{
DuLinkList p = NULL;
if (!(p=GetElem_DuL(L, i)) //在L中确定第i个元素的位置指针
return ERROR; //p为NULL时,第i个元素不存在
p->prior->next = p->next; //对应上图的第1步
p->next->prior = p->prior;//对应上图的第2步
free(p); //对应上图的第3步:释放被删除的结点空间
return OK;
}
7. 单链表、循环链表和双向链表的比较
L
表示头指针;R
表示尾指针;p
表示当前结点的指针
查找表头结点 | 查找表尾结点 | 查找结点*p 的前驱结点 | |
---|---|---|---|
带头结点的单链表L | L->next | 从L->next 依次向后遍历 | 通过p->next 无法找到其前驱 |
带头结点仅设头指针L的循环单链表 | L->next | 从L->next 依次向后遍历 | 通过p->next 可以找到其前驱 |
带头结点仅设尾指针R的循环单链表 | R->next | R | 通过p->next 可以找到其前驱 |
带头结点的双向循环链表L | L->next | L->prior | p->prior |