3.1 线性表的定义
定义:零个或者多个数据元素的有限序列
-
第一个元素无前驱,最后一个元素无后继
-
线性表元素的个数n(n>=0)定义为线性表的长度,n=0时为空表
-
排列的必须是相同的数据类型
-
在较复杂的线性表中,一个数据元素可以由若干个数据项组成
-
元素之间的是一对一的关系
3.2 线性表的抽象数据类型
ADT 线性表(List)
Data 每个元素的类型均为DataType
Operation
InitList(*L):初始化操作,建立一个空的线性表
ListEmpty(L):若线性表为空,返回true,否则返回false
CleanList(*L):清空线性表
GetElem(L,i,*e):线性表中的第i个位置元素值返回给e
LocateElem(L,e):在线性表中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号表示成功。否则返回0,失败
ListInsert(*L,i,e):在线性表L中的第i个位置插入新元素e
ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值
ListLength(L):返回线性表L的元素个数
end ADT
3.3 线性表的顺序存储结构
定义:用一段地址连续的存储单元依次存储线性表的数据元素
数据元素强调相同数据类型
3.3.2 顺序存储方式
可以用一维数组来实现
why?cuz线性表每个数据元素的类型都相同
//顺序存储的结构代码
#define MAXSIZE 20 //存储空间的初始分配量
typedef int ElemType; //类型根据具体情况而定
typedef struct{
ElemType data[MAXSIZE]; //数组存储元素,最大值为MAXSIZE
int length; //线性表当前的长度
}SqList;
数组长度与线性表长度区别
- 数组的长度是存放线性表的存储空间长度
- 线性表的长度是线性表中数据元素的个数,根据插入,删除动态分配
- 任何时刻,线性表的长度<=数组的长度
存取时间性能为O(1),通常把具有此类特定的存储结构成为随机存取结构
3.3.3 顺序存储结构的插入和删除
- 获得元素操作
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;/*Status是函数类型,其值是函数结果的状态代码,如OK*/
/*Status是一个整型,返回1或者0*/
/*初始条件:顺序线性表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;
}
-
插入元素操作
算法思路:
* 如果插入的位置不合理,抛出异常
* 如果线性表的长度大于数组的长度,抛出异常或者动态增加容量
* 从最后一个元素向前遍历到第i个位置,分别将它们后移一个位置
* 把元素插入第i个位置
* 表长+1
/*初始条件:顺序线性表L已存在,1<=i<ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的元素,表长+1*/
Status ListInsert(Sqlist *L,int i,ElemType e)
{
int k;
if(L->length==MAXSIZE)
return ERROR; /*顺序线性表已经满*/
if(i < 1 || i > L->length+1)
return ERROR;/*插入位置不合适*/
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;
}
-
删除元素操作
算法思路
*如果删除位置不正确,抛出异常
*取出删除元素
*如果删除的不是最后一个位置从删除元素位置起向后遍历到最后一个元素位置,将它们往前移动一个位置
*表长-1
/*初始条件:顺序线性表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;
}
一个小插曲
在学习线性表的时候,我在书上写下,为什么插入要(E)
时间复杂度分析
存,读数据为O(1),插入删除为O(n)
线性表顺序存储的优缺点
优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快去存取表中任一位置的元素
缺点:
- 插入和删除操作需要移动大量的元素
- 当线性表长度变化较大的时候,难以确定存储空间
- 造成存储空间的碎片,造成存储空间浪费
3.4 线性表的链式存储结构
n个node链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个node只包含一个指针域,所以叫单链表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7i0rC7Vp-1635231668869)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316171059107.png)]
头指针:链表中第一个node的存储位置叫头指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWKrtTLp-1635231668873)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316193855223.png)]
为了对链表进行更方便的操作,会在单链表的第一个node前附设一个头node
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M3EzzKtv-1635231668876)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316194335689.png)]
头指针与头结点node的异同
头指针:* 指链表指向第一个node的指针,若链表有头node,则是指头node的指针
* 头指针具有标识作用,所以常用头指针冠以链表的名字
* 无论链表是否为空,头指针都不为空
头结点:* 为了操作的统一和方便而设立的,放在第一元素的node之前,其数据域一般无意义,也可存放链表的长度
* 对第一元素结点node前插入node和删除node操作统一
* 头结点node不一定是链表的必须要素
我们主要关心的是数据元素和数据元素之间的逻辑关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19M3isPb-1635231668880)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316195835877.png)]
单链表,结构指针代码如下
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;/*定义LinkList*/
node由存放数据元素的数据域和存放后继node的指针域组成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-11ExHLEu-1635231668881)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316200939686.png)]
3.4.1 单链表的基本操作
- 单链表的读取
算法思路
- 声明一个指针p指向链表的第一个node,初始化j从1开始
- 当j<i时,遍历链表,让p的指针向后移动,不断指向下一个node,j+1
- 若到链表末尾p为空,则说明第i个node不存在
- 否则查找成功,返回node p的数据
算法的时间复杂度取决于i的位置。
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p;
p = L->next;/*声明一个指针p*/
j=1; /*j为一个计数器*/
while (p && j<i)
{
p = p->next;
++j;
}
if(!p || j>i)
return ERROR;
*e = p->data;
return OK;
}
为什么用while循环不用for循环?
cuz单链表结构中没有定义表长,不知道循环多少次,不方便用for来控制
- 单链表的插入和删除
插入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wuMyVutV-1635231668883)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316203331175.png)]
s->next = p->next;
p->next = s;
p的后继node改为s的后继node,再把p->next改成s
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ARaLqNDu-1635231668885)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316203402489.png)]
插入算法思路
- 声明一指针p指向链表头结点,初始化j从1开始
- 当j<i时,遍历链表,让p指针往后移,不断指向下一个node,j+1
- 若链表的末尾p为空,则说明第i个结点不存在
- 否则查找成功,在系统生成一个空node结点s
- 将数据e赋值给s->data
- 单链表插入标准语句 s->next = p->next; p->next = s;
- 返回成功
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度+1*/
Status ListInsert(LinkLikst *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j < i);/*寻找第i-1个node*/
{
p = p->next;
++j;
}
if(!p || j>i)
return ERROR;
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
- 删除
把p的后继node,改成p的后继的后继的node
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G44LRtnD-1635231668886)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210316205513844.png)]
q = p->next;
p->next = q->next;
删除算法思路
- 声明一指针p指向链表头指针,初始化j从1开始
- 当j<i时,遍历链表,让p指针往后移,不断指向下一个node,j+1
- 若链表的末尾p为空,则说明第i个结点不存在
- 否则查找成功,将欲删除的结点 p->next赋值给q
- 单链表的删除标准语句 p->next = q->next
- 将q结点的数据赋值给e,作为返回
- 释放结点q;
- 返回成功
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个结点,并用e返回其值,L的长度域-1*/
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList q,p;
p = *L;
j = 1;
while(p->next && j<i)
{
p = p->next;
++j;
}
if(!(p->next) || j>i)
return ERROR;
q = p->next;
*e = q->data;
free(q);/*C语言的标准函数free,它的作用就是让系统回收一个Node,释放内存*/
return OK;
}
对于插入和删除数据越频繁的操作,单链表的效率优势是明显。
- 单链表的创建==动态生成链表的过程,空表开始建立
算法思路
-
声明一指针p和计数器变量i
-
初始化一空链表L
-
让L的头结点的指针指向NULL,即建立一个带头结点的单链表
-
循环 * 生成一新结点赋值给p
* 随机生成一个数字赋值给p的数据域p->data
* 将p插入到头结点与前一新结点之间
头插法
/*随机产生n个元素的值,建立带头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L,int n)
{
LinkList p;
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;
}
}
尾插法
/*随机产生n个元素的值,建立带头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for(i = 0;i < n;i++)
{
p = (Node *)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;
}
单链表的整表删除
算法思路
- 声明一个结点p和q
- 将第一个结点赋值给p
- 循环
- 将下一个结点赋值给q
- 释放p
- 将q赋值给p
/*初始条件:顺序线性表已存在*/
/*将L重置为空表*/
Status CleanList(LinkList *L)
{
LinkList p,q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
为什么要设置变量q?
cuz:在释放free§的时候,p包括指针域和数据域,释放的时候把指针域也释放了,下一个结点无法找到,所以需要q。
单链表结构与顺序存储结构的优缺点
1存储分配方式
- 顺序存储结构用一段连续的存储单元依次存储数据元素。
- 单链表采用链式存储结构,用任意一组存储单元存放线性表的数据元素,开辟空间不连续
2时间性能
- 查找:顺序存储结构0(1),单链表0(n)
- 插入和删除:顺序存储结构O(n),单链表O(1)
3空间性能
- 顺序存储结构需要预先分配存储空间,容易造成空间浪费,也容易发生上溢
- 单链表动态分配存储空间。
3.5静态链表
定义:用数组描述的链表叫做静态链表(游标实现法,data,cur)
/*线性表的静态链式存储结构*/
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;/*游标curson,为0时无指向*/
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-laoqZXfE-1635231668888)(C:\Users\Gabrielle\AppData\Roaming\Typora\typora-user-images\image-20210317154701193.png)]
以上图示相当于初始化的数组状态
/*将一维数组space中分量链成一备用表*/
/*sapce[0].cur为头指针,"0"表示空指针*/
Status InitLink(StaticLinkList space)
{
int i;
for(i = 0;i < MAXSIZE-1;i++)
space[i].cur = i + 1;
space[MAXSIZE-1].cur = 0;/*目前链表为0,最后一个数据元素的cur为0*/
return OK;
}
在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现。静态链表操作的是数组,静态链表的插入和删除,需要编写实现函数。
静态链表其实是为了给没有指针的高级语言设计的一种实现
插入
/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(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 = MAX_SIZE-1; /*k是最后一个元素的下标*/
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++) /*找到第i个元素之前的位置*/
k = L[k].cur;
L[j].cur = L[k].cur; /*把第i个元素之前的cur赋值给新的元素*/
L[k].cur = j; /*把新元素的下标赋值给第i个元素之前元素的cur*/
return OK;
}
return ERROR;
}
删除
/*删除在L中第i个数据元素,相当于free()*/
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(i < 1 || i > ListLength(L))
return ERROR;
k = MAX_SIZE-1;
for(j = 1;j<= i-1;j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Fred_SSL(L,j);
return OK;
}
void Free_SSL(StaticLinkList space,int k)
{
space[k].cur = space[0].cur;
space[0].cur = k;
}
静态链表的优缺点
优点
在插入和删除操作的时候不需要移动元素,改进顺序存储结构中进行插入和删除操作的时候需要移动大量元素的缺点
缺点
表长难以确定,无法解决分配空间带来的问题,可能造成空间浪费
没有顺序存储结构随机存取的特性
3.6 循环链表
3.6.1 定义—什么是循环链表
将单链表中终端结点的指针端由空指针改成指向头结点,使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)
单链表和循环链表的区别
最突出的是在循环的判断条件上。单链表判断p->next是否为空,循环链表判断p->next是否等于头结点。
3.6.2 循环链表
- 空循环链表
通常设置一个头结点(但是不是每一个空的循环链表都需要一个头结点),如图
- 非空循环链表
3.7 双向链表
定义—什么是双向链表
双向链表(double linked list)是在单链表的每一个结点中,再设置一个指向其前驱结点的指针域。在双向链表中,有两个指针域,一个指向直接后继,一个指向直接前驱。
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DulNode *prior;/*直接前驱指针*/
struct DulNode *next;/*直接后继指针*/
}DulNode,*DulinkList;
插入
在操作插入的时候代码的顺序很重要
(1)插入S结点的前驱(prior)
(2)插入S结点的后继(next)
(3)后结点的前驱(prior)
(3)后结点的后继(next)
s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s;
删除
删除只需要两步,相比较插入简单一点
(1)前结点的后继(next)
(2)后结点的前驱(prior)
p->prior->next = p->next;
p->next->prior = p->prior;
总结