写在最前,
写文章的初衷只是为了复习与记录自己的成长,笔者本人也还是学生,文章中难免会出现许多问题与错误,文章内容仅供参考,有不足的地方还请大家多多包涵并指正,谢谢~
第二章:线性表
2.1 线性表的概念及其抽象数据类型定义
线性表(Linear List):线性表是n个类型相同数据元素的有限序列,对n>0,除第一元素无直接前驱,最后一个元素无直接后继外,其余的每个数据元素只有一个直接前驱和一个直接后继。数据元素之间具有一对一的关系。表中元素个数称为线性表的长度,线性表没有元素时称为空表,表起始位置称表头,表结束位置称表尾。
线性表的特点:
①同一性:线性表由同类数据元素组成
②有穷性:线性表由有限个数据元素组成,表长度就是表中数据元素的个数
③线性表中相邻数据元素之间存在着序偶关系
线性表的抽象数据类型定义:
ADT LinearList{
数据元素:可以是任意数据类型的数据,但必须属于同一个数据对象
结构关系:线性表中相邻元素之间存在序偶关系
基本操作:
①InitList(L)
操作前提:L为未初始化线性表
操作结果:将L初始化为空表
②ListLength(L)
操作前提:线性表L已存在
操作结果:如果L为空则返回0,否则返回表中的元素个数
③GetData(L,i)
操作前提:表L存在,且1<=i<=ListLength(L)
操作结果:返回线性表L中的第i个元素的值
④InsList(L,i,e)
操作前提:表L已存在,e为合法元素值且1<=i<=ListLength(l)+1
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
⑤DelList(L,i,e)
操作前提:表L已存在且非空,1<=i<=ListLength(L)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
⑥Locate(L,e)
操作前提:表L已存在,e为合法数据元素值
操作结果:如果L中存在数据元素e,则将当前指针指向数据元素e所在位置并返回TRUE,否则返回FALSE
⑦DestroyList(L)
操作前提:线性表L已存在
操作结果:将L销毁
⑧ClearList(L)
操作前提:线性表L已存在
操作结果:将L置为空表
⑨EmptyList(L)
操作前提:线性表L已存在
操作结果:如果L为空则返回TRUE,否则返回FALSE
}ADT LinearList
2.2 线性表的顺序存储
线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的数据元素存储在连续的物理存储单元中,即通过数据元素物理存储的连续性来反映数据元素之间逻辑上的相邻关系。采用顺序存储结构存储的线性表通常简称为顺序表。归纳为:关系线性化,结点顺序存
地址计算:假设线性表中有n个元素,每个元素占k个单元,第一个元素的地址为,则可计算出第i个元素的地址
:
线性表顺序存储的表示及操作:
#define MAXSIZE 100 /此处的宏定义常量表示线性表的最大长度
#define OK 1
#define ERROR 0
//用C语言定义线性表的顺序存储结构如下:
typedef struct SeqList *List;
typedef struct{
ElemType elem[MAXSIZE]; //线性表占用的数组空间
int last; //记录线性表中最后一个元素在数组elem[]中的位置(下标值),空表置为-1,last+1即为线性表的长度
}SeqList;
//顺序表的初始化:
List MakeEmpty(){
List L;
L=(List)malloc(sizeof(struct SeqList));
L->last=-1;
return L;
}
//顺序表的按序号查找运算,查找顺序表L中第i个数据元素
ElemType GetData(L,i){
return L.elem[i-1];
}
//顺序表的按内容查找运算:在顺序表L中查找与e相等的元素,若L.elem[i]=e,则找到该元素,并返回i+1;若找不到,则返回-1
int Locate(SeqList L,ElemType e){
int i=0; //i为扫描计数器,初值为0,即从第一个元素开始比较
while((i<=L.last)&&(L.elem[i]!=e)) //顺序扫描表,直到找到值为e的元素,或扫描到表尾而没找到则跳出循环
i++;
if(i<=L.last) //若找到值为e的元素,则返回其序号
return (i+1);
else //若没找到,则返回空序号
return (-1);
} //此算法的时间复杂度为O(n)
//顺序表的插入运算:在顺序表L中第i个元素之前插入一个元素e。i的合法取值范围是1<=i<=L->last+2
int InsList(SeqList *L,int i,ElemType e){
int k;
if((i<1)||(i>L->last+2)){ //首先判断插入位置是否合法
printf("插入位置i不合法!");
return (ERROR);
}
if(L->last>=MAXSIZE-1){
printf("表已满,无法插入!");
return (ERROR);
}
for(k=L->last;k>=i-1;k--) //为插入元素而移动位置
L->elem[k+1]=L->elem[k];
L->elem[i-1]=e; //在C语言中,第i个元素的下标为i-1
L->last++; //last任指向表尾
return(OK);
}
//顺序表的删除运算,在顺序表L中删除第i个数据元素,并用指针参数e返回其值。i的合法取值为1<=i<=L.last+1
int DelList(SeqList *L,int i,ElemType *e) {
int k;
if((i<1)||(i>L->last+1)){
printf("删除位置不合法!");
return (ERROR);
}
*e=L->elem[i-1]; //将删除元素放到e所指的变量中
for(k=i;i<=L->last;k++)
L->elem[k-1]=L->elem[k]; //将后面的元素依次前移
L->last--;
return (OK);
}
实例:有两个顺序表LA和LB,其元素均为非递减有序排列,编写算法,将他们合并成一个顺序表LC,要求LC也是非递减有序排列。例如:LA=(2,2,3),LB=(1,3,3,4),则LC=(1,2,2,3,3,3,4)。
算法思想:设表LC是一个空表,为使LC也是非递减有序排列,可设两个指针i,j分别指向表LA和LB中的元素,若LA.elem[i]>LB.elem[j],则先将LB.elem[j]插入到表LC中,若LA.elem[i]<=LB.elem[j],则先将LA.elem[i]插入到表LC中,如此进行循环,直到其中一个表被扫描完毕,然后再将未扫描完的表中剩余的所有元素放到LC中。
//线性表的合并运算:
void mergeList(SeqList *LA,SeqList *LB,SeqList *LC){
int i,j,k.l;
i=0;j=0;k=0;
while(i<=LA->last&&j<=LB->last)
if(LA->elem[i]<=LB->elem[i]){
LC->elem[k]=LA->elem[i];
i++;k++;
}
else{
LC->elem[k]=LB->elem[j];
j++;k++;
}
while(i<=LA->last){ /*当表LA有剩余元素时,则将表LA余下的元素赋给表LC*/
LC->elem[k]=LA->elem[i];
i++;k++;
}
while(j<=LB->last){ /*当表LB有剩余元素时,则将表LB余下的元素赋给表LC*/
LC->elem[k]=LB->elem[j];
j++;k++;
}
LC->last=LA->last+LB->last+1;
}
算法分析:由于两个待归并的表LA、LB本身是值有序表,且表LC的建立采用的是尾插法建表,插入时不需要移动元素,所以算法的时间复杂度O(LA->last+LB->last).
由上面的讨论可知,线性表顺序表示的优点如下:
①无须为表示结点间的逻辑关系而增加额外的存储空间(因为逻辑上相邻的元素其存储的物理位置也是相邻的)。
②可方便地随机存取表中的任一元素,如GetData(L,i)操作。
线性表顺序表示的缺点如下:
①插入或删除运算不方便,除表尾的位置外,在表的其他位置上进行插入或删除操作都必须移动大量的结点,其效率较低。
②由于顺序表要求占用连续的存储空间,存储分配只能预先进行静态分配。因此当表长变化较大是,难以确定合适的存储规模。若按可能达到的最大长度预先分配表空间,则可能造成一部分空间长期闲置而得不到充分利用;若实现对表长估计不足,则插入操作可能使表长超过预先分配的空间而造成溢出
2.3 线性表链式存储
链式存储是最常用的动态存储方法。为了克服顺序表的缺点,可以采用链式方式存储线性表。通常将采用链式存储结构的线性表称为线性链表。
从链接方式的角度看,链表可分为单链表、双链表和循环链表;
从实现角度看,链表可分为动态链表和静态链表。
接下来分别对其进行讨论。
单链表:与顺序表用一组连续的存储单元依次存放结点不同,链表用一组任意的存储单元来存放结点。这组存储可以是连续的,也可以是非连续的,甚至是零散分布在内存的任何位置上。因此,链表中结点的逻辑顺序和物理顺序不一定相同。为正确表示结点间的逻辑关系,必须在存储每个数据元素值得同时,存储其后继结点的位置信息。这两部分信息组成的存储映像称为结点(Node),如下图所示:
数据域存储值,指针域存储直接后继的地址。
每个结点只有一个next指针域,故这种链表称为单链表。
单链表中每个结点的存储地址存放在其前驱结点的指针域中,由于第一个结点无前驱,所以应设一个头指针H指向第一个结点,最后一个结点没有直接后继,则指定最后一个结点的指针域为空。
为了操作统一方便,可以在单链表的第一个结点之前附设一个头结点,其数据域可以存储一些关于线性表长度等附加信息,也可以不存储任何信息。其指针域存储第一个结点的存储位置,此时头指针就不再指向第一个结点而是指向头结点。如果线性表为空表,则头结点的指针域为空。
线性表链式存储的表示及操作:
注:后续文章中若无特殊说明,默认LinkList L为头指针!
//单链表的存储结构描述如下:
typedef struct Node{ /*结点类型定义*/
ElemType data; /*结点数据域*/
struct Node *next; /*结点指针域*/
}Node,*LinkList; /*LinkList为结构指针类型*/
/*说明:LinkList和Node *同为结构指针类型,通常习惯上用LinkList强调单链表的头指针变量。
如:LinkList L,强调L为单链表头指针,提高程序可读性。用Node * 定义指向单链表中结点的指针。*/
//初始化单链表
InitList(LinkList *L){
*L=(LinkList)malloc(sizrof(Node)); /*建立头结点*/
(*L)->next=NULL; /*建立空的单链表L*/
}
//建立单链表,这里提供头插法和尾插法两种方法
/*假定节点中的数据类型为字符,以“$”为输入结束标志符*/
//头插法:将新结点插入到当前链表的表头节点之后,直至读入结束标志。
//头插法得到的单链表逻辑顺序与输入元素顺序相反,所以也叫逆序建表法
void CreateFromHead(LinkList L){ /*L是带头结点的空链表头指针*/
Node *s;
char c;
int flag=1; /*flag初值为1,当输入$时,置flag为0,建表结束*/
while(flag){
c=getchar();
if(c!='$'){
s=(Node *)malloc(sizeof(Node)); //建立新结点
s->data=c;
s->next=L->next;
L->next=s;
}
else flag=0;
}
}
//尾插法:将新结点插入到当前链表的表尾上,为此需要增加一个尾指针r,使之指向当前单链表的表尾
void CreateFromTail(LinkList L){ /*L是带头结点的空链表头指针*/
Node *r,*s;
int flag=1; /*flag初值为1,当输入$时,置flag为0,建表结束*/
r=L; //r指针动态指向当前表尾,以便于做尾插入,其初值指向头结点
while(flag){ /*循环输入表中元素值,将建立新结点s插入表尾*/
c=getchar();
if(c!='$'){
s=(Node *)malloc(sizeof(Node));
s->data=c;
r->next=s;
r=s;
}
else{
flag=0;
r->next=NULL; //将最后一个结点的next链域置空,表示链表结束
}
}
}
//查找,这里提供按序号查找和按值查找两种方法,平均时间复杂度均为O(n)
/*按序号查找:查找带头结点的单链表中的第i个结点,需从头指针L出发,从头结点(L->next)开始
顺着链域扫描,用指针p指向当前扫描到的结点,初值指向头结点,用j做计数器,累计当前扫描过的
结点数,当j==i时,指针p所指的结点就是要找的第i个结点*/
Node *Get(LinkList L,int i){ //若找到(1<=i<=n),则返回结点的存储位置,否则返回NULL
int j;
Node *p;
if(i<=0) return NULL;
p=L;j=0; //从头结点开始扫描
while((p->next!=NULL)&&(j<i)){
p=p->next; //扫描下一个结点
j++; //已扫描结点计数器
}
if(i==j) return p; //找到并返回第i个结点
else return NULL; //找不到,i<=0或i>n
}
/*按值查找:在单链表中查找是否有值等于key的结点。需从头指针指向的头结点出发,顺链逐个将
结点值和给定值key做比较,返回查找结果*/
Node *Locate(LinkList L,ElemType key){
//若找到结点值等于key的第一个结点,则返回位置p,否则返回NULL
Node *p;
p=L->next; //从第一个结点开始
while(p!==NULL) //当前表未查完
if(p->data!=key)
p=p->next;
else break; //找到结点值等于key时退出循环
return p;
}
//求单链表长度,时间复杂度为O(n)
/*在单链表中,整个链表由头指针来表示,可在从头到尾遍历的过程中统计计数,从而得到单链表的长度*/
int ListLength(LinkList L){
Node *p;
p=L->next;
j=0;
while(p!=NULL){ //当前表未遍历完
p=p->next;
j++;
}
return j; //j为单链表的长度
}
//单链表的插入操作
/*在线性表的第i(1<=i<=n+1)个位置之前插入一个新元素e,插入过程分为三步:1.查找第i-1个结点并由
指针pre指示2.申请新结点s,将其数据域的值置为e。3.通过修改指针域将新结点s挂入单链表L*/
void InsList(LinkList L,int i,ElemType e){
Node *pre,*s;
int k;
if(i<=0) return ERROR;
pre=L; k=0; //从头开始查找第i-1个结点
while(pre!=NULL&&k<i-1){
pre=pre->next;
k=k+1;
}
if(pre==NULL){ //pre为空表示已找完,但还未找到第i个,说明插入位置不合理
printf("插入位置不合理!");
return ERROR;
}
s=(Node *)malloc(sizeof(Node)); //申请新结点
s->data=e; //将e置入s的数据域
s->next=pre->next; //修改指针,完成插入
pre->next=s;
return OK;
}
//单链表的删除操作
/*将线性表第i(1<=i<=n+1)个元素e删除。删除过程分为以下两步:1.通过计数方式查找第i-1个结点并由
指针pre指示。2.删除第i个结点并释放结点空间*/
int DelList(LinkList L,int i,ElemType *e){
/*在带头结点的单链表L中删除第i个元素,并将删除的元素保存到变量e中*/
Node *pre,*r;
int k;
pre=L; k=0;
while(pre->next!=NULL&&k<i-1){ //查找待删除结点i的前驱结点i-1,使pre指向它
pre=pre->next;
k=k+1;
}
if(pre->next==NULL){ /*while循环跳出是因为pre->next为空,没有找到合法的前驱位置*/
printf("删除结点的位置i不合理!");
return ERROR;
}
r=pre->next;
pre->next=r->next; //修改指针,删除结点r
*e=r->data;
free(r); //释放被删除的结点所占的内存空间
return OK;
}
说明:删除算法中的循环条件(pre->next!=NULL&&k<i-1)与前插算法中的循环条件(pre!=NULL&&k<i-1)不同。前插时的插入位置有m+1个(m为当前单链表中数据元素的个数).i=m+1是指在第m+1个位置前(即单链表末尾)插入。而删除操作中删除的合法位置只有m个,若使用与前插操作相同的循环条件则会出现指针指空的情况,使删除操作失败。
实例:有两个单链表LA和LB,其元素均为非递减有序排列,编写一个算法,将它们合并成一个单链表LC,要求LC也是非递减有序排列。要求:利用新表LC和现有的表LA和LB中的元素结点空间,不申请额外的结点空间。例如LA=(2,2,3),LB=(1,3,3,4),则LC=(1,2,2,3,3,3,4)。
算法思想:要求利用现有的表LA和LB中的元素结点空间来建立新表LC,可通过更改结点的next域来重新建立新的元素之间的线性关系。为保证新表仍然递增有序,可以利用尾插法建立单链表的方法,只是新建表中的结点不用malloc,而只要从表LA和LB中选择合适的点插入到新表LC中即可。
LinkList MergeList(LinkList LA,LinkList LB){
Node *pa,*pb,*r;
LinkList LC;
/*将LC初始置空表。pa和pb分别指向两个单链表LA和LB中的第一个结点
r初值为LC且r时钟指向LC的表尾*/
pa=LA->next;
pb=LB->next;
LC=LA;
LC->next=NULL;r=LC;
while(pa!=NULL&&pb!=NULL){
/*当两个表中均未处理完时,比较选择将较小值结点插入到新表LC中*/
if(pa->data<=pb->data){
r->next=pa;
r=pa;
pa=pa->next;
}
else{
r->next=pb;
r=pb;
pb=pb->next;
}
}
//跳出while循环则至少有一个表已处理完
if(pa) //若表LA未完,将表LA中后续元素链到新表LC表尾
r->next=pa;
else //若表LB未完,将表LB中后续元素链到新表LC表尾
r->next=pb;
free(LB);
return(LC);
}
循环链表(Circular Linked List)是一个首尾相接的链表。将单链表最后一个结点的指针域由NULL改为指向表头结点,就得到了单链形式的循环链表,并称为循环单链表。同样还可以有多重链的循环链表。
循环单链表中,所有结点都被链在一个环上,为使某些操作实现方便,也可设置一个头结点。空循环链表仅由一个自成循环的头结点表示。带头结点的循环单链表如下图所示:
带头结点的循环单链表的各种操作的实现算法与带头结点的单链表的实现算法类似,差别仅在于算法中判别当前节点p是否为表尾结点的条件不同。单链表中判别条件为p!=NULL或p->next!=NULL,而单循环链表的判别条件则是p!=L或p->next!=L。
在循环单链表中附设尾指针有时比附设头指针会使操作变得更简单。例如,在用头指针表示的循环单链表中,找开始结点的时间复杂度是O(1),然而要找到终端结点。则需要从头指针开始遍历整个链表,其时间复杂度是O(n)。如果用尾指针rear来表示循环单链表, 则查找开始结点和终端结点都很方便,它们的存储位置分别是rear->next->next 和rear, 显然,查找时间复杂度都是O(1)。因此, 实用中多采用尾指针表示循环单链表。
采用带头结点和头指针的循环单链表,初始化和建立循环单链表的算法如下:
//初始化循环单链表
InitClinkList(LinkList *CL){ //CL用来接收待初始化的循环单链表的头指针变量的地址
*CL=(LinkList)malloc(sizeof(Node)); //建立头结点
(*CL)->next=*CL; //建立空的循环单链表CL
}
//建立循环单链表
/*假设线性表中结点的数据类型是字符,逐个输入这些字符,并以$作为输入结束标志符*/
void CreatrClinkList(LinkList CL){
/*CL是已经初始化好的,带头结点的空循环链表的头指针,通过键盘输入元素值
利用尾插法建立循环单链表*/
Node *rear,*s;
char c;
rear=CL; //rear指针动态指向链表的当前表尾,其初值指向头结点
c=getchar();
while(c!='$'){
s=(Node*)malloc(sizeof(Node));
s->data=c;
rear->next=s;
rear=s;
c=getchar();
}
rear->next=CL;
}
实例:有两个带头结点的循环单链表LA、LB,编写算法,将两个循环单链表合并为一个循环单链表,其头指针为LA。
/*采用以下的方法,需要遍历链表,找到表尾,其执行时间是O(n)*/
LinkList merge_1(LinkList LA,LinkList LB){
//此算法将两个采用头指针的循环单链表的首尾连接起来
Node *p,*q;
p=LA;q=LB;
//找到表LA、LB的表尾,用p、q指向它们
while(p->next!=LA) p=p->next;
while(q->next!=LB) q=q->next;
//修改表LB的尾指针,使之指向表LA的头结点
q->next=LA;
//修改表LA的尾指针,使之指向表LB中的第一个结点
p->next=LB->next;
free(LB);
return (LA);
}
/*采用以下的方法,无须循环遍历找尾结点,只需要直接修改尾结点的指针域,其执行时间是O(1)*/
LinkList merge_2(LinkList RA,LinkList RB){
//此算法将两个采用尾指针的循环单链表的首尾连接起来
Node *p;
p=RA->next; //保存链表RA的头结点地址
RA->next=RB->next->next; //链表RB的开始结点链到链表RA的终端结点之后
free(RB->next); //释放链表RB的头结点
RB->next=p; //链表RA的头结点链到RB的终端结点之后
return RB; //返回新循环链表的尾指针
}
双向链表(Double Linked List):在单链表的每个结点基础上,再增加一个指向其前驱的指针域prior,这样形成的链表中就有两条方向不同的链。双链表的结点结构及双向循环链表的图示如下:
双链表的结点结构:
在双向链表中,只涉及后继指针的的算法,如求表长度、取元素、元素定位等,与单链表中相应的算法相同,但对于涉及前驱和后继两个方向指针变化的操作,则与单链表中的实现算法不同。
下面是双链表的结构定义与相关操作:
//双链表的结构定义如下
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DoubleList;
//双向链表的前插操作,即,在第i个结点之前插入一个新的结点
int DlinkIns(DoubleList L,int i,ElemType e){
DNode *s,*p;
int k;
if(i<=0) return ERROR; //i的取值范围为1<=i<=表长+1
p=L; k=0; //从头开始查找第i个结点
while(p!=NULL&&k<i){
p=p->next;
k=k+1;
}
if(p==NULL){ //p为空表示已找完,但还未找到第i个,说明插入位置不合理
printf("插入位置不合理!");
return ERROR;
}
s=(DNode*)malloc(sizeof(DNode));
if(s){
s->data=e;
s->prior=p->prior; //①
p->prior->next=s; //②
s->next=p; //③
p->prior=s; //④
return TRUE;
}
else return FALSE;
}
//双向链表的删除操作,即,删除双向链表的第i个结点
int DlinkDel(DoubleList,int i,ElemType *e){
DNode *p;
int k;
p=L; k=0;
while(p->next!=NULL&&k<i){ //查找待删除结点i,使p指向它
p=p->next;
k=k+1;
}
if(p->next==NULL){ /*while循环跳出是因为p->next为空,还没有找到结点*/
printf("删除结点的位置i不合理!");
return ERROR;
}
*e=p->data;
p->prior->next=p->next; //①
p->next->prior=p->prior; //②
free(p);
return TRUE;
}
双向链表的前插和删除操作图示如下:
静态链表:以上的各种链表都是使用指针类型实现的,链表中结点空间的分配和回收(释放)均由系统提供的标准函数malloc和free动态实现,故称为动态链表。在没有提供“指针”这种数据类型的高级语言中若仍想采用链表存储结构,则采用顺序存储结构数组模拟实现链表。在数组的每个表目中设置“游标(Cursor)”来模拟指针,由编程人员自己编写从数组中分配和回收结点的过程。这种方式被称为静态单链表(Static Linked List)。
用游标模拟实现链表的方法:定义一个较大的结构数组作为节点空间存储池。每个结点应有两个域,即data域和cursor域。data域存放结点的数据信息,cursor域存放的不再是指针而是游标,游标存放的是其后继结点在结构数组中的相对位置(即数组下标值)。数组的第0个分量可以设计成表的头结点,头结点的cursor域指示了表中的第一个结点的位置。表尾结点的cursor域为-1,表示静态单链表的结束。
以下是静态单链表的结构体数组表述和基本操作算法描述:
//静态单链表结构体数组描述
#define Maxsize 10 //链表可能达到的最大长度
typedef struct{
ElemType data;
int cursor;
}; Component,StaticList[Maxsize];
/*在已申请的大的存储空间中有一个已用的静态单链表,还有一个空闲结点链表。
设space为静态单链表存储空间的首地址,头指针为0。空闲结点静态单链表的头指针另设变量av存储*/
//初始化:将静态单链表初始化为一个空闲结点静态单链表
void initial(StaticList space,int *av){
int k;
/*设置已用静态单链表的头指针指向space空间位置0,space[0]相当于头结点*/
space[0].cursor=-1;
for(k=1;k<Maxsize-1;k++)
space[k].cursor=k+1; //连链
space[Maxsize-1].cursor=-1; //标记链尾
*av=1; //设置备用链表头指针初值
/*已用空间头指针此时可视为单链表的头结点,备用空间头指针av指向空闲结点静态链表中的第一个结点*/
}
/*分配结点空间:对系统而言,在空闲结点链表中分配结点空间相当于空闲结点链表中减少一个结点
对使用者而言,相当于申请到了一个可用的新结点*/
int getnode(StaticList space,int *av){
//从备用链表摘下一个结点空间,分配给待插入静态链表中的元素
int i;
i=*av;
*av=space[*av].cursor;
return i
}
/*回收结点空间:对系统而言,在空闲结点链表中回收结点空间相当于空闲结点链表中增加一个结点
对使用者而言,相当于释放了一个可用的新结点*/
void freenode(StaticList space,int *av,int k){
//从space备用链表中回收序号为k的结点,av为备用链表的头指针
space[k].cursor=*av;
*av=k;
}
2.4 顺序表与链表的综合比较
1.基于空间的考虑
顺序表的存储空间是静态分配的,在程序执行之前必须明确规定它的存储规模。若线性表的长度变化较大,则存储规模难以预先确定。估计过大则造成空间浪费,估计太小又可能导致空间溢出。在静态链表中,初始存储池虽然也是静态分配的,但若同时存在若干个结点类型相同的链表,则它们可以共享空间,使各链表之间相互调节余缺,减少溢出机会。动态链表的存储空间是动态分配的,只要内存空间尚有空闲,就不会产生溢出。因此,当线性表的长度变化较大、难以估计其存储规模时,采用动态链表作为存储结构较好。存储密度(Storage Density)是指结点数据本身所占的存储量和整个结点结构所占的存储量之比。链表中的每个结点,除了数据域外,还要额外设置指针(或游标)域,从存储密度来讲,这是不经济的。一般地,存储密度越大,存储空间的利用率就越高。显然,顺序表的存储密度为1,而链表的存储密度小于1。例如,单链表中各结点的数据均为整数,指针所占空间和整型量相同,则单链表的存储密度为50%。因此若不考虑顺序表中的备用结点空间,则顺序表的存储空间利用率为100%,而单链表的存储空间利用率为 50%。由此可知,当线性表的长度变化不大、易于事先确定其大小时,为了节约存储空间,宜采用顺序表作为存储结构。
2.基于时间的考虑
顺序表是由向量实现的.它是一种随机存取结构,对表中任一结点都可以在 O(1)时间内直接地存取,而链表中的结点则需从头指针起顺着链查找才能取得。因此,若线性表的操作主要是进行查找,很少做插入或删除操作,宜采用顺序表作为存储结构。在链表中的任何位置上进行插入或删除,都只需要修改指针。而在顺序表中进行插入或删除时,平均要移动表中近一半的结点,尤其是当每个结点的信息量较大时,移动结点的时间开销就相当可观。因此,对于频繁进行插入或删除操作的线性表,宜采用链表作为存储结构。若表的插人或删除操作主要发生在表的首尾两端,则宜采用带尾指针的单循环链表。
3.基于语言的考虑
在没有提供指针类型的高级语言环境中,若要采用链表结构,则可以使用游标来实现静态链表。虽然静态链表在存储分配上有不足之处,但它和动态链表一样,具有插入和删除方便的特点。值得一提的是,即使是对那些具有指针类型的语言,静态链表也有其用武之地。特别是当线性表的长度不变,仅需改变结点之间的相对关系时,静态链表比动态链表可能更方便。
线性表链式存储方式的比较:
链表名称\操作名称 | 找表中首元素结点 | 找表尾结点 | 找P结点的前驱结点 |
---|---|---|---|
带头结点单链表L | L->next 时间耗费O(1) | 一重循环 时间耗费O(n) | 顺P结点的next域无法找到P结点的前驱 |
带头结点循环单链表(头指针)L | L->next 时间耗费O(1) | 一重循环 时间耗费O(n) | 顺P结点的next域可以找到P结点的前驱 时间耗费O(n) |
带尾指针的循环单链表R | R->next->next 时间耗费O(1) | R 时间耗费O(1) | 顺P结点的next域可以找到P结点的前驱 时间耗费O(n) |
带头结点双向循环链表L | L->next 时间耗费O(1) | L->prior 时间耗费O(1) | P->prior 时间耗费O(1) |
总结:
写在最后,
因本系列文章主要为复习,故重点关注数据结构概念知识与理论知识,本章知识涉及一个案例实现,篇幅原因这里就不再展开,后续文章中会单独就实例的实现编写相关算法思想并上传源代码。笔记仅作为参考,若读者发现内容有误请私信指正,谢谢!