三、线性表
1.线性表的定义
线性表(List):零个或多个数据元素的有限序列。
线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
在复杂的线性表中,一个数据元素可以由若干个数据项组成。
2.线性表的抽象数据类型
线性表的抽象数据类型定义:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,......an},每个元素的类型均为DataType。其中,除第一个元素a1
外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元
素。数据元素之间的关系是一对一的关系。
Operation
InitList(*L): 初始化操作,建立一个空的线性表L
ListEmpty(L): 若线性表为空,返回true,否则返回false
ClearList(*L): 将线性表清空
GetElem(L, i, *e): 将线性表L中的第i个位置元素值返回给e
LocateElem(L, e): 在线性表L中查询与给定值e相等的元素,如果查询成功,返回该元素在表中序号表示成功;否则,返回0表示失败
ListInsert(*L, i, e): 在线性表L中的第i个位置插入新元素e
ListDelete(*L, i, e): 删除线性表L中第i个位置元素,并用e返回其值
ListLength(L): 返回线性表L的元素个数
endADT
对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂的操作,完全可以用这些基本操作的组合来实现。
比如,要实现两个线性表集合A和B的并集操作。这里假设La表示集合A,Lb表示集合B,代码如下:
/* 将所有在线性表 Lb中但不在 La中的数据元素插入到 La中*/
void unionL(List *La, List Lb) {
int La_len,lb_len,i;
ElemType e; // 声明与La和Lb相同的数据元素e
La_len = ListLength(*La); // 求线性表的长度
Lb_len = ListLength(Lb);
for (i=1; i<=Lb_len; i++) {
GetElem(Lb, i, &e); // 取Lb中第i个数据元素赋给e
if (!LocateElem(*La, e)) // La中不存在和e相同数据元素
ListInsert(La, ++La_len, e); // 插入
}
}
3.线性表的顺序存储结构
3.1 顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
3.2 顺序存储方式
由于线性表的每个数据元素的类型都相同,我们可以用一维数组来实现顺序结构存储,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。
线性表的顺序存储结构代码如下:
#define MAXSIZE 20 // 存储空间初始分配量
typedef int ElemType; // ElemType 类型根据实际情况而定,这里假定为int
typedef struct {
ElemType data[MAXSIZE]; // 数组存储数据元素,最大值为MAXSIZE
int length; // 线性表当前长度
}SqList;
这里发现,描述顺序存储结构需要三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置
- 线性表的最大存储容量:数组长度MAXSIZE
- 线性表的当前长度:length
3.3 数组长度与线性表长度的区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作而改变。
在任意时刻,线性表的长度都应该小于等于数组的长度。
3.4 地址计算方法
假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)
LOC(ai+1) = LOC(ai) + c
所以对于第i个数据元素 ai 的存储位置可以由 a1 推算得出:
LOC(ai) = LOC(a1) + (i - 1) * c
3.5 获得元素操作
要实现GetElem操作,即将线性表L中的第i个位置元素值返回,就是把数组第i-1下标的值返回。
/* 顺序存储结构的代码定义
* #define MAXSIZE 20
* typedef int ElemType;
* typedef struct {
* ElemType data[MAXSIZE];
* int length;
* }SqList;
*/
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(SqList L, int i, ElemType *e) {
if (L.length==0 || i<1 || i>L.length)
return ERROR;
*e = L.data[i-1];
return OK;
}
3.6 插入操作
插入算法的思路:
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置
- 将要插入元素填入位置处
- 表长加1
代码如下:
/* 顺序存储结构的代码定义
* #define MAXSIZE 20
* typedef int ElemType;
* typedef struct {
* ElemType data[MAXSIZE];
* int length;
* }SqList;
*/
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:在L中第i个位置前插入新的数据元素e,L的长度加1 */
Status ListInsert(SqList *L, int i, ElemType e) {
int k;
if (L->length == MAXSIZE) // 顺序线性表已经满了
return ERROR;
if (i<1 || i>L->length+1) // 当i不在范围内时
return ERROE;
if (i<=L->length) { // 若插入数据位置不在表尾
for (k=L->length-1; k>=i-1; k--)
L->data[k+1]=L->data[k];
}
L->data[i-1]=e; // 插入新元素
L->length++;
return OK;
}
3.7 删除操作
删除算法的思路:
- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
- 表长减1
代码如下:
/* 顺序存储结构的代码定义
* #define MAXSIZE 20
* typedef int ElemType;
* typedef struct {
* ElemType data[MAXSIZE];
* int length;
* }SqList;
*/
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 */
Status ListDelete(SqList *L, int i, ElemType *e) {
int k;
if (L->length == 0) // 线性表为空
return ERROR;
if (i<1 || i>L->length) // 删除位置不正确
return ERROR;
*e=L->data[i-1];
if (i<L->length) { // 如果删除不是最后位置
for (k=i; k<L->length; k++) // 将删除位置后继元素前移
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
小结:线性表的顺序存储结构,在存、读数据时,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。这说明,它比较适合元素个数不太变化,而更多是存取数据的应用。
3.8 线性表顺序存储结构的优缺点
优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 能够快速地存取表中任一位置的元素
缺点:
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的“碎片”
4.线性表的链式存储结构
4.1 顺序存储结构的不足
线性表的顺序存储结构最大的缺点就是插入和删除时需要移动大量元素,比较耗时,可以采用链式存储结构来解决。
4.2 线性表的链式存储结构定义
为了表示每个数据元素 ai 与其直接后继数据元素 ai+1 之间的逻辑关系,对数据元素 ai 来说,除了存储本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素 ai 的存储映像,称为结点。
n个结点(ai 的存储映像)链结成一个链表,即为线性表(a1,a2,···,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
链表中第一个结点的存储位置叫做头指针。
有时,为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。
4.3 头指针与头结点的异同
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
- 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了
- 头结点不一定是链表的必须要素
4.4 线性表链式存储结构代码描述
typedef struct Node {
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList; // 定义LinkList
此处对第5行代码有疑惑的同学,看这篇文章。
由此看出,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
4.5 单链表的读取
获得链表第i个数据的算法思路:
- 声明一个指针p指向链表第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个结点不存在
- 否则查找成功,返回结点p的数据
代码如下:
/* 链式存储结构的代码定义
* typedef int ElemType;
* typedef struct Node {
* ElemType data;
* struct Node *next;
* }Node;
* typedef struct Node *LinkList;
*/
/* 初始条件:L已存在,1<=i<=ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(LinkList L, int i, ElemType *e) {
int j = 1; // 计数器
LinkList p; // 声明一个指针p
p = L->next;
while (p && j<i) {
p = p-> next;
j++;
}
if (!p || j>i)
return ERROR;
*e = p->data;
return OK;
}
最坏情况时间复杂度为O(n)
4.6 单链表的插入
想要在p和p->next之间插入s,只需
s->next=p->next; p->next=s;
注:两者顺序不能反
单链表第i个数据插入结点的算法思路:
- 声明一指针p指向链表头结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个结点不存在
- 否则查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给s->data
- 单链表的插入标准语句s->next = p->next,p->next = s
- 返回成功
代码如下:
/* 链式存储结构的代码定义
* typedef int ElemType;
* typedef struct Node {
* ElemType data;
* struct Node *next;
* }Node;
* typedef struct Node *LinkList;
*/
/* 初始条件:L已存在,1<=i<=ListLength(L) */
/* 操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度加1 */
Status ListInsert(LinkList *L, int i, ElemType e) {
int j=1;
LinkList p,s;
p = *L;
while (p && j<i) { // 寻找第i-1个结点
p = p->next;
j++;
}
if (!p || j>i)
return ERROR; // 第i个结点不存在
s = (LinkList)malloc(sizeof(Node)); // 生成新结点(c标准函数)
s->data = e;
s->next = p->next; // 将p的后继结点赋值给s的后继
p->next = s; // 将s赋值给p的后继
return OK;
}
4.7 单链表的删除
要删除p的下一个结点,只需
q=p->next; p->next=q->next;
单链表第i个数据删除结点的算法思路:
- 声明一指针p指向链表头指针,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个结点不存在
- 否则查找成功,将欲删除的结点p->next赋值给q
- 单链表的删除标准语句p->next=q->next
- 将q结点中的数据赋值给e,作为返回
- 释放q结点
- 返回成功
算法如下:
/* 链式存储结构的代码定义
* typedef int ElemType;
* typedef struct Node {
* ElemType data;
* struct Node *next;
* }Node;
* typedef struct Node *LinkList;
*/
/* 初始条件:L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个结点,并用e返回其值,L的长度减1 */
Status ListDelete(LinkList *L, int i, ElemType *e) {
int j=1;
LinkList p,q;
p = *L;
while (p->next && j<i) { // 寻找第i-1个结点
p = p->next;
j++;
}
if (!(p->next) || j>i)
return ERROR; // 第i个结点不存在
q = p->next;
p->next = q->next; // 将q的后继赋值给p的后继
*e = q->data; // 将q结点中的数据给e
free(q); // 让系统回收此结点,释放内存
return OK;
}
相比线性表的顺序存储结构,对于插入或删除数据越频繁的操作,单链表的效率优势越明显。
4.8 单链表的整表创建
单链表整表创建的算法思路:
- 声明一指针p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环:
- 生成一个新结点赋值给p
- 随机生成一个数字赋值给p的数据域p->data
- 将p插入到头结点与前一个新结点之间
代码如下:
/* 链式存储结构的代码定义
* typedef int ElemType;
* typedef struct Node {
* ElemType data;
* struct Node *next;
* }Node;
* typedef struct Node *LinkList;
*/
/* 随机产生n个元素的值,建立带头结点的单链线性表L(头插法) */
void CreateListHead(LinkList *L, int n) {
LinkList p;
int i;
srand(time(0)); // 初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; // 先建立一个带头结点的单链表
for (i=0; i<n; i++) {
p = (LinkList)malloc(sizeof(Node)); // 生成新结点
p->data = rand()%100+1; // 随机生成100以内的数字
p->next = (*L)->next;
(*L)->next = p; // 插入到表头
}
}
此处对传入的参数或第15行代码有疑惑的同学,看这篇文章。
除了头插法,我们还可以采用尾插法,代码如下:
/* 链式存储结构的代码定义
* typedef int ElemType;
* typedef struct Node {
* ElemType data;
* struct Node *next;
* }Node;
* typedef struct Node *LinkList;
*/
/* 随机产生n个元素的值,建立带头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n) {
LinkList p,r;
int i;
srand(time(0)); // 初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));
r=*L; // r为指向尾部的结点
for (i=0; i<n; i++) {
p = (LinkList)malloc(sizeof(Node)); // 生成新结点
p->data = rand()%100+1; // 随机生成100以内的数字
r->next = p; // 将表尾终端结点的指针指向新结点
r = p; // 再次指向尾结点
}
r->next = NULL; // 表示当前链表结束
}
4.9 单链表的整表删除
单链表整表删除的算法思路:
- 声明结点p和q
- 将第一个结点赋值给p
- 循环:
- 将下一结点赋值给q
- 释放p
- 将q赋值给p
代码如下:
/* 链式存储结构的代码定义
* typedef int ElemType;
* typedef struct Node {
* ElemType data;
* struct Node *next;
* }Node;
* typedef struct Node *LinkList;
*/
/* 初始条件:L已存在 */
/* 操作结果:将L重置为空表 */
Status ClearList(LinkList *L) {
LinkList p,q;
p = (*L)->next; // p指向第一个结点
while (p) { // 遍历到表尾
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; // 头结点指针域为空
return OK;
}
4.10 单链表结构与顺序存储结构比较
- 存储分配方式
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
- 时间性能
- 查找
- 顺序存储结构O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,为O(n)
- 单链表得到某个位置的指针后,插入和删除为O(1)
- 查找
- 空间性能
- 顺序存储结构需要预分配存储空间,分大了,浪费,分小了,易发生上溢
- 单链表不需要分配存储空间,元素个数不受限制
5.静态链表
5.1 静态链表的定义
用数组描述的链表叫做静态链表,这种描述方法还叫做游标实现法。
/* 线性表的静态链表存储结构 */
#define MAXSIZE 1000 // 假设链表的最大长度为1000
typedef struct {
ElemType data;
int cur; // 游标(Cursor),为0时表示无指向
} Component, StaticLinkList[MAXSIZE];
另外我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下表;而数组的最后一个元素的cur,则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0。如图:
此时的图示相当于初始化的数组状态,代码如下:
/* 将一维数组space中各分量链成一个备用链表 */
/* space[0].cur 为头指针,“0”表示空指针 */
Status InitList(StaticLinkList space) {
int i;
for (i=0; i<MAXSIZE-1; i++) {
space[i].cur = i+1;
}
space[MAXSIZE-1].cur = 0; // 目前静态链表为空,最后一个元素的cur为0
return OK;
}
假如将“甲”,“乙”,“丙”,“丁”,“戊”,“己”,“庚”存入,得
注意下标为0(第一个元素)和下标为999(最后一个元素)的cur
5.2 静态链表的插入操作
代码如下:
// 获得插入的下标
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space) {
int i = space[0].cur; // 第一个备用链表的下标
if (space[0].cur)
space[0].cur = space[i].cur; //由于备用链表的第一个元素被赋值,将备用链表的“头结点”后移一位
return i;
}
// 插入元素
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e) {
int j, k ,l;
k = MAXSIZE - 1; // k取最后一个元素的下标,因为最后一个元素的cur是静态链表第一个元素的下标
if (i<1 || i>ListLength(L)+1)
return ERROR;
j = Malloc_SSL(L); // 调用上面获取插入的下标的方法
if (j) {
L[j].data = e;
for (l=1; l<=i-1; l++)
k = L[k].cur; // 获得第i个元素之前的位置
L[j].cur = L[k].cur;
L[k].cur = j;
return OK;
}
return ERROR;
}
就拿上面例子来说,想要再插入元素“丙”:
5.3 静态链表的删除操作
代码如下:
/* 删除在L中第i个数据元素e */
Status ListDelete(StaticLinkList L, int i) {
int j, k;
if (i<1 || i>ListLength(L))
return ERROR;
k = MAXSIZE-1;
for (j=1; j<=i-1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
/* 将下标为i的空闲结点回收到备用链表 */
/* 即在备用链表的表头插入要删除的元素 */
void Free_SSL(StaticLinkList space, int i) {
space[i].cur = space[0].cur; // 把第一个元素的cur值赋值给要删除的分量的cur
space[0].cur = i; // 把要删除的分量下标赋值给第一个元素的cur
}
还拿之前的例子来说:
另:静态链表也有相应的其他操作,如ListLengh
int ListLength(StaticLinkList L) {
int j=0;
int i=L[MAXSIZE-1].cur;
while (i) {
i=L[i].cur;
j++;
}
return j;
}
5.4 静态链表的优缺点
优点:
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
缺点:
- 没有解决连续存储分配带来的表长难以确定的问题
- 失去了顺序存储结构随机存取的特性
小结:静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。
6.循环链表
将链表中终端结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相连的单链表称为单循环链表,简称循环链表(circular linked list)。
此时要访问最后一个结点需要O(n)时间,我们可以改造一下使其变为O(1)。
这里,我们不用头指针,改用指向终端结点的尾指针来表示循环链表。
由此,访问最后结点的时间就是O(1)了。
另外,此方法也有利于合并两个循环链表。
合并:
代码如下:
p = rearA->next; // 保存A表的头结点
rearA->next = rearB->next->next; // 将本是指向B表的第一个结点(不是头结点)赋值给rearA->next
q = rearB->next; // 记录下B表的头结点,方便后续释放
rearB->next = p; // 将原A表的头结点赋值给rearB->next
free(q); // 释放q
7.双向链表
7.1 双向链表的代码描述
双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
/* 线性表的双向链表存储结构 */
typedef struct DulNode {
ElemType data;
struct DulNode *prior; // 直接前驱指针
struct DulNode *next; // 直接后继指针
} DulNode, *DulLinkList;
7.2 双向链表的插入操作
假设存储元素e的结点为s,要实现将结点s插入到结点p和p->next之间需要下面几步。
/* 先把s结点的前驱和后继连上去,再修改原来的p->next和p的前驱和后继结点 */
s->prior = p; // 把p值赋值给s的前驱,如图中①
s->next = p->next; // 把p->next赋值给s的后继,如图中②
p->next->prior = s; // 把s赋值给p->next的前驱,如图中③
p->next = s; // 把s赋值给p的后继,如图中④
7.3 双向链表的删除操作
如果要删除结点p,只需要以下几步。
p->prior->next = p->next;
p->next->prior = p->prior; // 这两步可以反过来
free(p); // 释放结点