1.1 线性表定义和基本操作
1.1.1线性表的定义(逻辑结构):
具有相同数据类型的n(n≥0)个数据元素的有限序列。
PS:
n为表长,n=0为空表,位序(从1开始),表头元素,表尾元素,直接前驱,直接后继。
1.1.2线性表的特点:
①个数有限。
②逻辑上顺序(先后次序)。
③都是数据元素(单个元素)。
④数据类型相同(存储空间同)。
1.1.3线性表的基本操作(基本操作):
①从无到有,从有到无:
InitList(&L):初始化表。构造一个空的线性表,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,释放内存空间。
②增删元素:
ListInsert(&L,i,e):插入操作,在表L的第i个位置插入指定元素e.
ListDelete(&L,i,&e):删除操作,删除表L的第i个位置元素,并用e返回删除元素的值。
③查找元素:
LocateElem(L,e):按值查找操作,在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作,获取表L中的第i个位置的元素的值。
④其他常用操作:
Length(L):求表长,返回表L的长度(元素个数)。
PrintList(L):输出操作,按前后顺序输出表L所有值。
Empty(L):判空操作,若L为空表,返回true,否则返回false。
PS:
①对数据的操作——创销、增删改(也要查)查
②若需要将修改的程序“带回来”则在参数前加上"&"引用符号。
③存储结构不同,运算实现的方式不同。
1.2顺序表(顺序存储[物理/存储结构],线性表[逻辑结构])
1.2.1顺序表的定义(线性表的顺序存储):
逻辑上相邻的元素存储在物理位置上也相邻的存储单元(逻辑和物理顺序相同)。
1.2.2顺序表的内存地址:
问:如何知道一个数据元素大小? 答:sizeof(ElemType)
1.2.3顺序表的特点:
①随机访问,即在O(1)内找到第i个元素。
②存储密度高,每个节点只能存储数据元素。
③扩展容量不方便。(动态时间复杂度高,要求大片连续空间)
④插、删操作不便,需移动大量数据元素。
1.3顺序表的实现:
1.3.1静态分配:
存储空间是静态的,一开始确定后就无法更改。
定义一个静态顺序表:
#define MaxSize 50 /定义线性表的最大长度
typedef struct{
ElemType data[MaxSize]; /用静态的“数组”存放数据元素
int length; /顺序表的当前长度
}SqList; /顺序表类型定义
====》使用一维数组进行静态分配,ElemType为元素类型。若存满后则无法继续存储。
====》typedef struct{ } name;
name就是该结构体的名字。
struct
是结构体的关键字,是用来定义结构体的。
typedef
是定义自定义类型的关键字。可以定义自定义类型。(改名字)
struct test
{
int a;
};
/定义一个结构体,名字是test,这样就可以使用struct test 来定义变量。 如:struct test a;
typedef struct test T;
/定义一个自定义类型T,其代表含义为struct test. T a;和之前的struct test a;一个效果。
/两个可以合并。
typedef struct test
{
int a;
}T;
基本操作——初始化静态顺序表:
void InitList(sqList &L){
for(int i = 0;i<MaxSize;i++)
L.data[i]=0; /将所有数据元素设置为默认初始值
L.length=0; /顺序表初始长度为0
}
====》①该程序中对所有数据元素设置为默认初始值是为了防止输出内存中遗留的“脏数据”,实际可以省略,因为正常只会输出存放的数据,没有存放的数据正常不会进行输出。
②长度初始化是不可省略的,因为不确定是否有“脏数据”,也不会对它在进行赋值。
基本操作——按位查找:
ElemType GetElem(sqList L,int i){
/可以加一些判断语句增加健壮性
return L.data[i-1];
}
1.3.2动态分配:
存储空间在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就会另外开辟更大空间,来替换原有空间。
定义一个动态顺序表:
#define InitSize 50 /定义线性表的表长初始值
typedef struct{
ElemType *data; /指示动态分配数组的指针
int MaxSize,length; /数组的最大容量和当前长度
}SqList; /顺序表类型定义
====》动态分配并不是链式存储,同样属于顺序存储结构,只是分配的空间大小可在运行时决定。
1.3.3动态分配语句:
C的初始动态分配语句(申请:malloc,释放:free,包含在stdlib.h的头文件中):
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
====》malloc函数:会申请一整片的存储空间,并会返回该片存储空间的起始地址的指针,故:
①需强制转型为定义的数据元素类型指针,即:(ElemType*)。
②多大存储空间由参数决定,即:sizeof(ElemType)*InitSize决定。
C++的初始动态分配语句(申请:new 释放:delete):
L.data==new ElemType[InitSize];
1.3.4基本操作:
基本操作——初始化动态顺序表:
void InitList(sqList &L){
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
//这边的类型要与定义顺序表中一致(ElemType *data;)
L.length=0;
L.MaxSize=InitSize;
}
基本操作——增加动态数组长度:
void IncreaseSize(sqList &L,int len){
int *p=L.data; /将p指向原来的内存空间,用于数据复制和用free来释放该空间
L.data=(ElemType*)malloc(sizeof(ElemType)*(L.MaxSize+len));
for(int i=0;i<L.length;i++){
L.data[i]=p[i]; /将原来数据复制到新区域
}
L.MaxSize=InitSize+len; /表长增加len
free(p); /释放原来内存空间
}
====>可以发现IncreaseSize函数与InitList函数的差别在于增加了malloc中参数(增加的大小),定义新指针,将原有数据复制到新区域和释放原有空间,且表长成功增加。
很明显,虽然成功动态分布内存,并不是在原有基础上扩充,而是重新申请一片区域,再而转移带新区域,这样的时间开销很明显大了很多。
基本操作——插入操作:
ListInsert(&L,i,e):插入操作,在表L的第i个位置插入指定元素e.(注意这边的i位置是位序)
void ListInsert(sqList &L,int i,int e){ /int就是上面的ElemType
for(int j=L.length;j>=i;j--) /将第i个元素及之后的元素后移
L.data[j]=L.data[j-1]; /这边要注意,数组下标比位序要小1
L.data[i-1]=e; /在位置i处放入e,这边要注意,数组下标比位序要小1
L.length++; /长度加1
}
===》以上代码实现插入操作,但是若输入数据不符合要求怎么办?是不是缺少了健壮性,故可修改为:
bool ListInsert(sqList &L,int i,int e){ /int就是上面的ElemType
if(i<1||i>L.length+1) /判断i的范围是否有效
return false;
if(L.length>=Maxsize) /当前存储空间已满,不能插入
return false;
for(int j=L.length;j>=i;j--) /将第i个元素及之后的元素后移
L.data[j]=L.data[j-1]; /这边要注意,数组下标比位序要小1
L.data[i-1]=e; /在位置i处放入e,这边要注意,数组下标比位序要小1
L.length++; /长度加1
return true;
}
===》时间复杂度:
最好情况:O(1),表尾; 最坏情况:O(n),表头;
基本操作——删除操作:
ListDelete(&L,i,&e):删除操作,删除表L的第i个位置元素,并用e返回删除元素的值。
bool ListDelete(sqList &L,int i,int &e){ /int就是上面的ElemType
//注意这边e在主函数上,定义类型要与表一致
if(i<1||i>L.length) /判断i的范围是否有效
return false;
e=L.data[i-1]; /这边要注意,数组下标比位序要小1
for(int j=i;j<L.length;j++) /将第i个元素之后的元素前移
L.data[j-1]=L.data[j]; /这边要注意,数组下标比位序要小1
L.length--; /长度加1
return true;
}
===》在删除操作中,移动元素:先移动前面元素,再移动后面元素。
在插入操作中,移动元素:先移动后面元素,在移动柜前面元素
===》时间复杂度:
最好情况:O(1),表尾; 最坏情况:O(n),表头;
基本操作——查找操作:
GetElem(L,i):按位查找操作,获取表L中的第i个位置的元素的值。
LocateElem(L,e):按值查找操作,在表L中查找具有给定关键字值的元素。
int GetElem(sqList L,int i){ /int就是上面的ElemType
//可以加一些判断语句增加健壮性
return L.data[i-1];
}
====》可以发现与静态相同,虽然动态中定义的是:ElemType *data;,但实际上静态中的定义的数组也是指针。他们都是指向相同的位置,且它们的数据类型相同。
====》时间复杂度:O(1),很好解释了“随机存取”特性。
int LocateElem(sqList L,int e){ /int就是上面的ElemType
for(int i=0;i<L.length;i++)
if(L.data[i] == e)
return i+1; /数组下标为i的元素值等于e,返回其位序i+1
return 0; /退出循环,说明查找失败
}
====》if(L.data[i] == e)中,对比的是整型变量的值是否相等,故可以实现,若是结构型变量也可以么?
====》不可以,应该要分别对结构体中的变量进行分别比较然后在用&&来连接。
====》时间复杂度:
最好情况:O(1),表尾; 最坏情况:O(n),表头;
1.4链表(链式存储[物理/存储结构],线性表[逻辑结构])
逻辑上相邻的元素存储在物理位置上不相邻的存储单元(逻辑和物理顺序不同)。
故:插入和删除操作无需移动元素,只需修改指针,但失去“随机存取”特性。
1.4.1链表分类:
单链表、双链表、循环链表、静态链表。
1.4.2单链表的定义:
通过一组任意存储单元来存储线性表中数据元素,故:
单链表结点结构:自身信息(数据域)+后继指针(指针域)。
单链表结点类型描述:(语句①)
struct LNode{ /定义单链表结点类型
ElemType data; /数据域,每个节点存放一个数据元素
struct LNode *next; /指针域,指向下一个节点
};
====》定义了一个struct结构体,用来表示一个结点,
添加一个新的结点,并用指针p指向该结点:
struct LNode*p=(struct LNode*)malloc(sizeof(struct LNode)); /struct LNode可不可简写?
====》使用typedef关键字(数据类型重命名),对struct LNode进行简化。
用法:typedef <数据类型> <别名>
eg:(语句②) typedef struct LNode LNode; //用LNode来表示 struct LNode
(语句③)typedef struct LNode* LinkList; //用LinkList来表示 struct LNode *(指针)
====》上面语句①②③可写成下面简单形式(推荐):
单链表结点类型描述且进行简化了LNode类型和LinkList指向LNode指针
typedef struct LNode{ /定义单链表结点类型
ElemType data; /数据域,每个节点存放一个数据元素
struct LNode *next; /指针域,指向下一个节点
}LNode,*LinkList;
由语句③可知下面两条语句相同(强调点不同):
要表示一个单链表时,只需声明一个头指针,指向单链表的第一个结点:
LNote*L //声明一个指向单链表第一个结点的指针
强调返回的是一个结点
Linklist L //声明一个指向单链表第一个结点的指针
强调这是一个单链表
1.4.3单链表的特点:
①不要求大片连续空间,改变容量方便。
②插、删操作方便。
③不可随机存取(失去随机存取特性)。
④耗费一定空间存放指针。
1.5单链表的实现
1.5.1不带头结点
基本操作——初始化单链表:
bool InitList(LinkList &L){
L=NULL; /空表,暂时还没有任何结点(防止脏数据)
return true;
}
====》形参中&符号不要忘记,否则修改的将“带不回来”
L为主函数中声明的一个指向单链表的指针(并没有创建一个结点)LinkList L;
基本操作——判断单链表是否为空:
bool Empty(LinkList L){
return (L==NULL);
}
基本操作——按位序插入:
ListInsert(&L,i,e):插入操作,在表L的第i个位置插入指定元素e.
(找到第i-1个结点(修改next指针)将新结点插入,若是第一个位置,则可改变头指针中的next)
与带头结点主要区别在于第一个结点的插入需要格外声明:
if(i==1){ /插入第一个结点的操作与其他结点操作不同
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e; /将插入结点的值赋给新结点
s->next = L;
L = s; /头指针指向新结点
return true; /插入成功
}
1.5.2带头结点(推荐)
基本操作——初始化单链表:
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeof(LNode)); /分配一个头结点,没有数据
if(L==NULL) /内存不足,分配失败
return false;
L—>next=NULL; /头指针之后暂时还没有节点
return true;
}
====》形参中&符号不要忘记,否则修改的将“带不回来”
L为主函数中声明的一个指向单链表的指针(并没有创建一个结点)LinkList L;
基本操作——判断单链表是否为空:
bool Empty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
基本操作——按位序插入操作:
ListInsert(&L,i,e):插入操作,在表L的第i个位置插入指定元素e.
(找到第i-1个结点(修改next指针)将新结点插入,若是第一个位置,则可改变头指针中的next)
bool ListInsert(LinkList &L,int i,ElemType e){
if (i < 1) /i是位序,这判断i是否合法
return false;
LNode*p; /指针p指向当前扫描到的结点
int j = 0; /当前p指向的是第几个结点
p = L; /L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i-1){ /循环找到第i-1个结点
p=p->next;
j++;
}
//有后插操作,以下语句直接用return InsertNextNode(p,e);替代。
if (p == NULL) /i值不合法,值太大,找不到
return false;
LNode *s = (LNode*)malloc(sizeof(LNode));/插入结点,所以要定义一个新的结点
s->data = e; /将插入结点的值赋给新结点
s->next = p->next; /将结点p指向下一个结点的next地址交给结点s中的next
p->next = s; /将结点s连到p之后
return true; /插入成功
}
====》注意,最后第三句和最后第二句语句顺序不可颠倒。
====》学过下面这条语句后,可将上面代码下面部分直接用后插操作代替即可。
基本操作——指定结点的后插操作:
bool InsertNextNode(LNode *p,ElemType e){
if (p == NULL)
return false;
LNode *s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) /内存分配失败(如:内存不足)
return false;
s->data = e; /将插入结点的值赋给新结点
s->next = p->next; /将结点p指向下一个结点的next地址交给结点s中的next
p->next = s; /将结点s连到p之后
return true; /插入成功
}
基本操作——指定结点的前插操作:
前插是比较难实现的,因为你找不到前面的数据,除非你有头指针,但复杂度为O(n).
下面同样实现前插,但是复杂度为O(1).
实际上,这个还是后插(通过后插实现前插),将新插入的结点存放前面的数据,前面的结点再存放新数据。
bool InsertPriorNode(LNode *p,ElemType e){
if (p == NULL)
return false;
LNode *s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) /内存分配失败(如:内存不足)
return false;
s->next = p->next;
p->next = s; /新结点s连到p之后
//上面两句其实就是后插操作
s->data = p->data; /将p中元素复制到s中
p->data = e; /p中元素覆盖为e
return true;
}
基本操作——删除操作(按位序):
ListDelete(&L,i,&e):删除操作,删除表L的第i个位置元素,并用e返回删除元素的值。
(找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点)
最好时间复杂度为O(1),最坏、平均复杂度为O(n).
bool ListDelete(LinkList &L,int i,ElemType &e){
if (i < 1) /i是位序,这判断i是否合法
return false;
LNode*p; /指针p指向当前扫描到的结点
int j = 0; /当前p指向的是第几个结点
p = L; /L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i-1){ /循环找到第i-1个结点
p=p->next;
j++;
}
if (p == NULL) /i值不合法,值太大,找不到
return false;
//以上与插入操作相同,找到前驱结点
if (p->next == NULL) /第i—1个结点之后已无其他结点
return false;
LNode *q =p->next; /令q指向被删除结点
e = q->data ; /用e返回元素的值
p->next = q->next; /将*q结点从链中“断开”
free(p); /释放结点的存储空间
return true; /删除成功
}
基本操作——删除操作(指定结点):
删除结点需要修改(找到)其前驱结点的next指针,很明显,不能直接找到,方法与之前相同。如:
方法一:传入头指针,循环寻找p的前驱结点
方法二:偷天换日(类似结点前插的实现)时间复杂度为O(1).
bool DeleteNode(LNode *p){
if (p == NULL)
return false;
LNode *q = p->next; /令q指向*p的后继结点
p->data = p->next->data; /和后继结点交换数据域
p->next = q->next; /将*q结点从链中“断开”
free(q); /释放后继结点的存储空间
return true;
}
====》这边其实有个错误的情况,那就是如果p是最后一个结点,q就是一个空指针,是有问题的。想要解决这个方法,只能使用方法一。
同时我们现在能很深刻的认识的单链表的局限性:无法逆向检索,有时候不太方便。
基本操作——查找操作(按位查找):
GetElem(L,i):按位查找操作,获取表L中的第i个位置的元素的值。
(在按位插入/按位删除中,已经实现了按位查找的功能,故重复代码可直接用LNode *p=GetElem(L,i-1); )
(当i<0或超出范围时,指针的值都是NULL,平均时间复杂度:O(n))
LNode*GetElem(LinkList L,int i){
if (i < 1) //i是位序,这判断i是否合法
return NULL;
LNode*p; //指针p指向当前扫描到的结点
int j = 0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i){ //循环找到第i个结点
p=p->next;
j++;
}
return p;
}
基本操作——查找操作(按值查找):
LocateElem(L,e):按值查找操作,在表L中查找具有给定关键字值的元素。
LNode*LocateElem(LinkList L,ElemType e){
LNode* = p->next;
//从第1个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p=p->next;
}
return p; //找到后返回该结点指针,否则返回NULL
}
====》时间复杂度为:O(n)
基本操作——求表长:
Length(L):求表长,返回表L的长度(元素个数)。
int Length((LinkList L){
int len = 0; //统计表长
LNode *p = L;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
====》时间复杂度为:O(n)
基本操作——单链表的建立(尾插法):
可对之前编写的“按位序插入操作”进行补充。如:
初始化单链表;
设置变量len记录链表长度;
while循环{
每次取一个数据e ;
ListInsert(L,len+1,e)插到尾部 ;
len++;
}
====》复杂度为:O(n^2); 有更加简单的方法,直接添加一个尾指针。
LinkList List_TailInsert(LinkList &L){ //正向建立单链表
int x; //设ElemType为整型
L = (LinkList)malloc(sizeof(LNode));//建立头结点
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
while (x != 9999){ //输入9999表示结束
s= (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指向新的表尾结点(永远保持r指向最后一个结点)
scanf("%d",&x);
}
r->next = NULL; //尾结点指针置空
return L;
}
====》时间复杂度为:O(n)
基本操作——单链表的建立(头插法):
可对之前编写的“后插操作”进行补充。如:
初始化单链表;
设置变量len记录链表长度;
while循环{
每次取一个数据e ;
insertNextNode(L,e);
}
====》复杂度为:O(n^2); 有更加简单的方法,直接添加一个头指针。
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x; //设ElemType为整型
L = (LinkList)malloc(sizeof(LNode));//建立头结点
L->next = NULL; //初始为空链表(必须初始化,养成好习惯)
scanf("%d",&x); //输入结点的值
while (x != 9999){ //输入9999表示结束
s= (LNode *)malloc(sizeof(LNode)); //建立新结点
s->data = x;
s->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
====》时间复杂度为:O(n),重要应用,链表的逆置。
1.6双链表
1.6.1双链表的定义:
在单链表的基础上,在增加一个指针域(存储前驱结点),弥补了单链表无法逆向检索,可进可退,存储密度更低一丢丢。
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域,每个节点存放一个数据元素
struct DNode *prior,*next; //指针域,前驱和后继节点
}DNode,*DinkList;
====》按值查找的操作和单链表的相同。双链表在插入和删除操作的实现上,与单链表有着较大的不同,这是因为“链”变化时也需要对prior指针做出修改,其关键是保证在修改的过程中不断链。此外,双链表可以很方便地找到其前驱结点,因此,插入、删除操作的时间复杂度仅为O(1).
1.6.2双链表的实现(带头结点):
基本操作——初始化双链表:
bool InitDLinkList(DLinkList &L){
L=(DNode*)malloc(sizeif(DNode)); //分配一个头结点,没有数据
if(L==NULL) //内存不足,分配失败
return false;
L—>prior=NULL; //头指针的prior永远指向NULL
L—>next=NULL; //头指针之后暂时还没有节点
return true;
}
====》可以发现与单链表大致相同,形参中&符号不要忘记,否则修改的将“带不回来”
L为主函数中声明的一个指向单链表的指针(并没有创建一个结点)DLinkList L;
基本操作——判断双链表是否为空:
bool Empty(DLinkList L){
if (L->next == NULL)
return true;
else
return false;
}
基本操作——插入操作:
在p结点之后插入s结点。
bool InsertNextDNode(DNode *p,DNode *s){
s->next = p->next; //将结点*s插入到结点*p之后
p->next ->prior= s; //下一个结点的前驱指向s,(若没有下个结点,则出错)
s->prior = p;
p->next = s;
}
====》把等号读成指向,如最后一句:p->next指向s。
本段代码不太严谨,在当p结点为最后一个结点的话,第二句会出现空指针的错误,可修改为:
bool InsertNextDNode(DNode *p,DNode *s){
if(p = NULL||s == NULL) //非法参数
return false;
s->next = p->next; //将结点*s插入到结点*p之后
if(p -> next!= NULL) //若p结点后有后继结点
p->next ->prior= s;
s->prior = p;
p->next = s;
}
====》边界情况:新插入结点在最后一个位置,需要特殊处理。
基本操作——删除操作:
bool DeleteNextDNode(DNode *p){
if(p = NULL) return false;
DNode *q = p->next; //找到p的后继结点q
if(q = NULL) return false; //p没有后继
p->next = q->next;
if(q ->next!= NULL) //若p结点后有后继结点
q->next ->prior= p;
free(q); //释放结点空间
return true;
}
====》边界情况:若被删除结点在最后一个位置,需要特殊处理。
基本操作——销毁双链表:
bool DestroyList(DLinklist &L){
//循环释放各个数据结点
while(L->next!= NULL)
DeleteNextDNode(L);
free(L); //释放头结点
L = NULL; //头结点指向NULL
}
基本操作——遍历双链表
<1>后向遍历
while(p!= NULL){
//对结点p做相应处理,如打印
p = p->next;
}
<2>前向遍历
while(p!= NULL){
//对结点p做相应处理,如打印
p = p->prior;
}
<2>前向遍历(跳过头结点)
while(p->prior!= NULL){
//对结点p做相应处理,如打印
p = p->prior;
}
====》双链表不可随机存取,按位查找、安值查找操作都只能用遍历的方式实现。时间复杂度O(n);
1.7循环链表
1.7.1循环链表的定义:
在单链表和双链表的基础上,进行改进,形成循环单链表和循环双链表。
单链表:表尾结点的next指针指向NULL。
从一个结点出发只能找到后续的各个结点
循环单链表:表尾结点的next指针指向头结点。
从一个结点出发可以找到任何一个结点。
双链表:表头结点的prior指向NULL。
表尾结点的next指向NULL。
循环双链表:表头结点的prior指向表尾结点。
表尾结点的next指向头结点。
====》循环单链表的判空条件不是头结点指针是否为空,而是它是否等于头指针。 有时对单链表常做的操作是在表头和表尾进行的,此时对循环单链表不设头指针而仅设尾指针,从而使得操作效率更高。其原因是,若设的是头指针,对表尾操作需要O(n)的时间复杂度,而若设的是尾指针r,r->next即为头指针,对表头和表尾进行操作都只需要O(1)的时间复杂度。
1.7.2循环单链表的实现(带头结点):
基本操作——初始化循环单链表:
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeif(LNode)); //分配一个头结点,没有数据
if(L==NULL) //内存不足,分配失败
return false;
L—>next=L; //头结点next指向头结点
return true;
}
基本操作——判断循环单链表是否为空:
bool Empty(LinkList L){
if(L—>next==L)
return true;
else
return false;
}
基本操作——判断结点p是否为循环单链表的表尾结点:
bool isTail(LinkList L,LNode *p){
if(p—>next==L)
return true;
else
return false;
}
1.7.2循环双链表的实现(带头结点):
基本操作——初始化循环双链表:
bool InitDLinkList(DLinkList &L){
L=(DNode*)malloc(sizeif(DNode)); //分配一个头结点,没有数据
if(L==NULL) //内存不足,分配失败
return false;
L—>prior=L; //头结点的prior指向头结点
L—>next=L; //头结点next指向头结点
return true;
}
基本操作——判断循环双链表是否为空:
bool Empty(DLinkList L){
if(L—>next==L)
return true;
else
return false;
}
基本操作——判断结点p是否为循环双链表的表尾结点:
bool isTail(LinkList L,LNode *p){
if(p—>next==L)
return true;
else
return false;
}
基本操作——循环双链表的插入:
在p结点之后插入s结点。
bool InsertNextDNode(DNode *p,DNode *s){
s->next = p->next; //将结点*s插入到结点*p之后
p->next ->prior= s; //下一个结点的前驱指向s,(若没有下个结点,则出错)
s->prior = p;
p->next = s;
}
====》这个是之前“双链表的插入”代码中,为表尾结点的话,则可能出现错误,但是,在这里是没有错的,应该不会出现空指针的错误。
基本操作——删除操作:
bool DeleteNextDNode(DNode *p){
if(p = NULL) return false;
DNode *q = p->next; //找到p的后继结点q
p->next = q->next;
q->next ->prior= p;
free(q); //释放结点空间
return true;
}
====》可以知道,双链表中的边界问题在循环双链表中是没有的。
1.8静态链表
1.8.1静态链表的定义:
单链表:各个结点在内存中星罗棋布、散落天涯。
包含:数据元素、指向下一个结点的指针(地址)
表尾的话:指针指向NULL.
静态链表:分配一整片连续的内存空间,各个结点集中安置。
包含:数据元素、下一个结点的数组下标(游标)
在静态链表中,数组下标为0的结点,充当“头结点”
表尾的话:游标为-1.
代码定义静态链表:
#denfine Maxsize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
};
====》用数组作为静态链表:struct Node a[Maxsize];
课本上的写法:
#denfine Maxsize 10 //静态链表的最大长度
typedef struct{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
}SLinkList[MaxSize];
等价于
#denfine Maxsize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
}SLinkList[MaxSize];
typedef struct Node SLinkList[MaxSize];
====》可用SLinkList定义“一个长度为MaxSize的Node型数组”
====》SLinkList a 等价于 struct Node a[MaxSize]
====》a是一个静态链表 等价于 a是一个Node型数组
1.8.2静态链表的实现:
基本操作——初始化静态链表:
与单链表相比,单链表中头指针L.next是指向NULL的。
在静态链表中,我们需要:
①把a[0] (静态链表)的next设为-1;
②将空闲的结点,把其他结点的next设为一个特殊值用来表示结点空闲,好判别是否为空闲的位置。
基本操作——静态链表的查找操作:
从头结点出发挨个往后遍历结点,直到想要的结点为止
====》时间复杂度为:O(n);
基本操作——静态链表的插入操作:
①找到一个空的结点,存入数据元素。
②从头结点出发找到位序为i-1的结点。
③修改新结点的next。
④修改i-1号结点的next。
1.8.3静态链表的优缺点和适用场景:
优点:增、删操作不需要大量移动元素。
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变。
适用场景:
①不支持指针的低级语言;
②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
1.9顺序表VS链表
1.9.1逻辑结构:
都属于顺序结构。
1.9.2存储结构:
顺序表的优缺点:
优点:支持随机存取、存储密度高。
缺点:大片连续空间分配不方便,改变容量不方便。
链表的优缺点:
优点:离散的小空间分配方便,改变容量方便。
缺点:不可随机存取,存储密度低。
1.9.3基本操作:
创销、增删改查。
基本操作 | 顺序表 | 链表 | 优胜者 |
创建 | 需要预分配大片连续空间,若分配空间过小,则之后不方便拓展容量;若分配过大,则浪费内存空间。静态分配:静态数组(容量不可改变),动态分配:容量可改变,但需移动大量元素,时间代价高。 | 只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展。 | 链表 |
销毁 | 修改length =0;静态分配:静态数组(系统自动回收空间),动态分配:动态数组(malloc/free,需手动free) | 依次删除各个结点(free) | (malloc和free必须成对出现) |
增删 | 插入/删除元素需要将后续元素都后移/前移。时间复杂度O(n),时间开销来自移动元素。(若数据元素很大,则移动时间代价很高) | 插入/删除元素只需修改指针即可。时间复杂度O(n),时间开销来自查找目标元素(查找元素时间代价更低)。 | 链表 |
查找 | 按位查找:O(1)。按值查找:O(n),若有序,可在O(log_2n)时间内找到。 | 按位/值查找O(n) | 顺序表 |
适用场景:
表长难以预估、经常要增加/删除元素 ——链表
表长可预估、查询(搜索)操作较多 ——顺序表
总结:
线性表:同种数据类型,有限长。
逻辑结构:线性结构。根据不同的存储结构分为:顺序表和链表。
顺序表:存储结构:顺序存储(定义数组,连续空间)
静态数组:
直接使用数组。
动态数组:
使用数组指针,在使用malloc函数开辟空间。
====》malloc用来开辟空间,返回该空间的头指针(故前面需要(ElemType))。
====》故:L.data=(ElemType *)malloc(InitSize*sizeof(ElemType));
====》ElemType为数据类型,sizeof计算该数据类型大小,InitSize是数组的大小
链表:存储结构:链式存储(定义单个结点)
单链表:单个结点包含:数据域(存储数据)和指针域(指向下一个结点)
LNode *p=(LNode *)malloc(sizeof(LNode));
带头结点:
不带头结点: