线性表
定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。若用L命名线性表,则其一般表示为:
-
每个数据所占空间一样大。
-
a(i)是线性表中的“第i个”元素线性表中的位序。
-
a(i)是表头元素,a(n)是表尾元素。
-
除了第一个元素之外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继。
-
位序从一开始,数组从零开始。
知识总览:
基本操作
- InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
- DestroyList(&L):销毁操作。销毁线性表,并释放线性表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的长度,即L中数据元素的个数。
- PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
- Empty(L):判空操作。若L为空表,则返回ture,否则返回false。
思维导图
顺序表
定义
顺序表——用顺序存储的方式实现顺序表线性存储。
顺序表的实现——静态分配
-
示例:
-
代码举例:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #define Maxsize 10//定义最大长度 typedef struct {//本例中,数据元素的类型(ELemtype)是int int data[Maxsize];//用静态的数组存放元素 int length;//顺序表的当前长度 }SqList;//顺序表的类型定义 //基本操作——初始化一个顺序表 void InitList(SqList& L) { for (int i = 0; i < Maxsize; i++) L.data[i] = 0;//将所有数据元素设置位默认初始值 L.length = 0;顺序表初始长度为0 } int main() { SqList L;//声明一个顺序表 InitList(L);//初始化顺序表 //.....未完待续,后续操作 return 0; } //这个代码不用多说,是很基础的,但是有一个小的疑问,就是在对C语言的学习中,要想使用函数来改变函数外面的东西,必须在传值的时候,传的是地址,而不能是值,但是这里为什么可以,这是因为虽然这里传的是值,但是它的接收东西是加了一个&,这个符号和之前传地址的时候,用指针来接受是一样的,这可以实现一样的效果,当然,这个题目可以使用指针来做,然后再传的参数的时候,传地址就行了!
图片解析
-
没有设置数据元素的默认初始值导致的结果如下
-
顺序表的实现——动态分配
-
动态内存分配举例
-
代码举例
#include <stdlib.h>//malloc和free函数的头文件! #define InitSize 10//默认的最大长度 typedef struct { int* data;//指示动态分配数组的指针 int MaxSize;//顺序表的最大容量 int length;//顺序表的当前长度 }SeqList; void InitList(SeqList &L) { //用malloc函数申请一片连续的存储空间 L.data=(int*)malloc(InitSize*sizeof(int)); L.length=0; L.MaxSize=InitSize; } //增加动态数组的长度 void IncreaseSize(SeqList &L,int len) { int *p=L.data; L.data=(int*)malloc((L.MaxSize+len)*sizeof(int)); for(int i=0;i<L.length;i++) { L.data[i]=p[i];//将数据复制到新区域(时间开销大) } L.MaxSize=L.MaxSize+len;//顺序表最大长度增加len free(p);//释放原来的内存空间 }//注意:realloc函数也可以实现,但建议初学者使用malloc和free可以更好地理解背后过程! int main() { SeqList L;//声明一个顺序表 InitList(L);//初始化顺序表 //.....往顺序表中随便插入几个元素 IncreaseSize(L,5); return 0; }
特点
- 随机访问。即可以在O(1)时间内找到第i个元素。
- 存储密度高。每个节点只存储数据元素。
- 拓展元素容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
- 插入,删除操作不方便,需要移动大量元素。
图形解释
思维导图
插入和删除
知识总览
插入
- 在进行插入操作的时候,是把那些元素依次往后移一位,但是是先把后面的元素往后移,然后再移前面的元素。
//顺序表的基本操作--插入
#define MaxSize 10 //定义最大长度
typedef struct
{
int data[MaxSize]; //用静态的“数组”存放数组元素
int length; //顺序表的当前长度
}Sqlist; //顺序表的类型定义
bool ListInsert(SqList &L,int i,int e)
{
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];
}
L.data[i-1]=e; //在位置i处放入e
L.length++; //长度加1
return true;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//此处省略一些代码.....插入几个元素
ListInsert(L,3,3);
return 0;
}
插入时间复杂度
删除
- 在进行删除操作的时候,是把那些元素都往前移一位,是现移动前面的元素,再移动后面的元素。
bool ListDelete(SqList &L,int i,int &e)
{
if(i<1||i>L.length)
{
return false;
}
e=L.data[i-1];
for(int j=i;j<L.length;j++)
{
L.data[j-1]=L.data[j];
}
L.length--;
return true;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//此处省略一些代码.....插入几个元素
int e=-1; //用变量e把删除的元素“带回来”
if(ListDelete(L,3,e))
printf("已删除第3个元素,删除元素值为=%d\n",e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
删除时间复杂度
思维导图
查找
知识总览
按位查找
代码
#define MaxSize 10 //定义最大长度
typedef struct
{
ElemType data[MaxSize]; //用静态的“数组”存放数组元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
ElemType GetElem(SqList L,int i)
{
return L.data[i-1];
}
#define MaxSize 10 //顺序表的初始长度
typedef struct
{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(动态分配方式)
ElemType GetElem(SqList L,int i)
{
return L.data[i-1]; //和访问普通数组的方法一样
}//如果换一个类型的指针,指向同一个地址
-
拓展
用某一种类型的指针加上数组下标的这种方式来访问数据的话,那么系统在背后为你取数据的时候,一次取几个字节,其实和你指针所指向的类型有关!这也就是为什么使用malloc开辟动态内存空间的时候,要使用强制类型转换!
时间复杂度
按值查找
代码
#define InitSize 10//顺序表的初始长度
typedef struct
{
ElemType *data;//指示动态分配数组的指针
int MaxSize;//顺序表的当前容量
int length;//顺序表的当前长度
}SeqList;//顺序表的类型定义(动态分配方式)
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateELem(SeqList L,ElenType e)
{
for(int i=0;i<L.length;i++)
{
if(L.data[i]==e)
{
return i+1;//数组下标为i的元素值等于e,返回其位序i+1
}
}
return 0;//退出循环,说明查找失败!
}
-
结构类型的数据元素是不可以使用==来比较的!
-
解决办法
注意:C语言中,结构体的比较不能直接使用“==”
时间复杂度
思维导图
单链表
定义
代码
typedef struct LNode//定义单链表节点类型
{
ElenType data;//每个节点存放一个数据元素
struct LNode* next;//指针指向下一个节点
}LNode,*LinkList;
struct LNode//定义单链表节点类型
{
ElemType data;//每个节点存放一个数据元素
struct LNode *next;//指针指向下一个节点
};
typedef struct LNode LNode;
typedef struct LNode* LinkLIst;
//要表示一个单链表时,只需要声明一个头指针L,指向单链表的第一个节点
LNode* L;//声明一个指向单链表第一个节点的指针
//或者:
LinkList L;//声明一个指向单链表第一个节点的指针(代码可读性更强)
-
补充说明typedef的另一种的方式(结构体中的小知识):
-
就是在使用typedef对结构体改名字的时候,可以同时定义2个类型,一个是结构体变量类型,还有一个是结构体变量指针类型,这在上面的代码中是有所体现的。
typedef struct LNode//定义单链表结点类型
{
ElemType data;//每个节点存放一个数据元素
struct LNode* next;//指针指向下一个结点
}LNode,*LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L)
{
L=NULL;//空表,暂时还没有任何结点(防止脏数据)
return true;
}
//判断单链表是否为空
bool Empty(LinkList L)
{
if(L==NULL)
return true;
else
return false;
}
//或者
//bool Empty(LinkList L)
//{
// return (L==NULL);
//}
void test()
{
LinkList L;//声明一个指向单链表的指针(注意此处并没有创建一个结点)
//初始化一个空表
InitList(L);
//.....后续代码.....
}
typedef struct LNode//定义单链表结点类型
{
ElemType data;//每个节点存放一个数据元素
struct LNode* next;//指针指向下一个结点
}LNode,*LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
L=(LNode*)malloc(sizeof(LNode));//分配一个头结点(不存储数据)
if(L==NULL)//内存不足,分配失败
return false;
L->next=NULL://头结点之后暂时还没有节点
return true;
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
if(L->next==NULL)
return true;
else
return false;
}
void test()
{
LinkList L;//声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//.....后续代码.....
}
- 注意:头节点是不存储数据的!
思维导图
插入和删除
-
按位序插入(带节点)
-
关于这里有一些说明就是,先不管那些传值还是传址的东西有没有错,试一下不就知道了吗,然后将节点转接一定要学会,这是很重要的!
//在第i个位置插入元素e(带头节点)
bool ListInsert(LinkList &L, int i, ElemType e)
{
if(i<1)
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;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next=p->next;
p->next=s; //将节点s连到p之后
return true; //插入成功
}
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
- 注意:在上面的代码中,最后将节点进行重新来连接,从而实现按位序插入的操作
- 按位序插入(不带节点)
bool ListInsert(LinkList &L, int i, ElemType e)
{
if(i<1)
return false;
if(i==1) //插入第1个节点的操作与其他节点操作不同
{
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next=L;
L=s; //头指针指向新起点
return true;
}
LNode *p; //指针p指向当前扫描到的节点
int j = 1; //当前p指向的是第几个节点
p = L; //p指向第1个节点(注意:不是头节点)
while(p != NULL && j < i-1) //循环找到第i-1个节点
{
p = p->next;
j++
}
if(p==NULL) //i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next=p->next;
p->next=s; //将节点s连到p之后
return true; //插入成功
}
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
-
代码逻辑:为了考虑i=1的特殊情况,又因为它不带头节点,所以要单独进行考虑,所以单独开辟了一个新的节点,在创建好一个新的节点之后,就要进行指针节点的操作,这里的逻辑是,首先将L所指向的那个a1(第一个节点)的节点给了s所指向的next,再把给L,这样就实现了节点之间的转换(注意单链表不带头节点的时候,头指针L指向的是a1(第一个节点))
-
相关重要的概念的解释
- 当链表的每个节点只包含一个指针域时,我们称之为单链表。
- 头节点:在单链表的第一个节点(有效元素)之前附近设的一个节点,称之为头节点。
- 头指针:指向头节点的指针称为头指针。
- 首节点(首元节点):首节点就是第一个元素(头元素)的节点,是头节点后面的第一个节点。
- 在线性表的链式存储结构中,头指针是指向第一个节点的指针,若链表有头节点,则头指针就是指向链表头节点的指针。
- 头指针有标识的作用,常用头指针作为链表的名字。
- 无论链表是否为空,头指针都不为空。头指针是链表的必要元素。
- 头节点可有可无,它不是链表所必需的。
-
指定节点的后插操作
//后插操作:在p节点之后插入元素e bool ListNextNode(LNode *p, ElemType e) { if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s==NULL) //内存分配失败 return false; s->data = e; //用节点s保存数据元素e s->next = p->next; p->next = s; //将节点s连到p之后 return true; } typedef struct LNode { ElemType data; struct LNode *next; }LNode, *LinkList;
-
指定节点的前插操作
- 方法1
-
方法2
-
总代码
//前插操作:在p节点之前插入元素e 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; }
-
上面这幅图中的代码逻辑是将新开辟的s节点传到一个函数里面,然后将p中的指针交给s中的指针,接着将s连在p的后面,在创建一个变量,将p中的数据复制过来,然后再将s中的数据复制到p里面去,此时s中是有你所想要插入的元素的,接着在将变量中的数据传给s,这样就完成了指定节点的前插操作。
-
按位序删除(带头节点)
//按位序删除(带头结点) bool ListDelete(LinkList &L,int i,ElemType &e) { if(i<1) 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(q);//释放节点的存储空间 return true;//删除成功 } typedef struct LNode { ElemType data; struct LNode* next; }LNode,*LinkList;
-
指定节点的删除
-
这个代码的逻辑是,目的是要实现对指定节点的删除操作,这里是要删除p这个节点,那么这里的逻辑就是令q指向p的后继节点,然后再和后面的节点交换数据域,最后把q节点从链中进行断开,但这里是删除了一个指定位置的节点,实际上就是将两个节点交换了一下位置而已。
-
当删除的是最后一个节点时,如果还是按照这个方法去计算的话,那么是会出错的,因为p后面的指针已经是空指针了,所以再去里面找数据是找不到的。
//删除指定节点p 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是最后一个节点的话,那么只能够从表头开始遍历来寻找到p的前驱!
- 这时候就只能使用从表头来依次进行寻找p的前驱来实现对最后一个节点的删除,所以在针对不同的情况要找不一样的方法去进行解决!
-
思维导图
-
封装的好处
- 例如上图中就是将一个后插的操作,封装成一个函数,这样写的代码更加的简洁,代码逻辑更加地清晰!
查找
按位查找
//按位查找,返回第i个元素(带头结点)
LNode* GetElem(LinkList L,int i)
{
if(i<0)
{
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;
}
//王道书版本
LNode* GetElem(LinkList L,int i)
{
int j=1;
LNode* p=L->next;
if(i==0)
{
return L;
}
if(i<1)
{
return NULL;
}
while(p!=NULL&&j<i)
{
p=p->next;
j++;
}
return p;
}
- 右边代码实现的逻辑:首先让指针指向第一个元素的节点,如果i等于0的话,就返回头节点,如果i小于1的话,就返回一个空指针,但是要注意还有一种情况就是当i的值超过了表的限制长度时,那么还是会返回一个空指针,这是不冲突的(虽然在上面的代码中,并没有对这种情况进行考虑,但是还是实现了这样的一个功能),后面的循环就是找到那个节点就可以了,这与之前实现的逻辑是一样的!
按值查找
-
//按值查找,找到数据域==e的节点 LNode* LocateElem(LinkList L,ElemType e) { LNode* p=L->next; //从第一个节点开始查找数据域为e的节点 while(p!=NULL&&p->data!=e) { p=p->next; } return p;//找到后返回该节点的指针,否则返回NULL }
注意:这里只能够从第一个节点开始查找,因为头节点是不存储数据元素的,然后后面的循环操作还是和以前一样,但是注意如果传进来的那个要寻找的数据元素是更复杂的类型,比如结构体的话,那么就不能够使用简单的等号来进行判断了!
-
求表的长度
//求表的长度 int Length(LinkList L) { int len=0;//统计表长 LNode* p=L; while(p->next!=NULL) { p=p->next; len++; } return len; }//这里没有提供不带头结点的情况,自己去了解!
-
注意这里是带了头节点的,那如果是不带头节点的话,就要在返回len+1,为什么呢?是因为没有头节点的话,所以在定义的时候,p就只能指向第一个元素的节点了,那么就会少一个长度未被统计到,所以要在返回值上加上一个1,或者初始化len的值为1
思维导图
单链表的建立
尾插法
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList& L,int i,ElemType e)
{
if(i<1)
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;
LNode* s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;//将节点s连到p之后
return true;//插入成功
}
//尾插法建立单链表:
//初始化单链表
//设置变量length记录链表长度
//while循环{
//每次取一个数据元素e;
//ListInsert(L,length+1,e)插到尾部;
//length++;
//}
//尾插法建立单链表
LinkList List_TailInsert(LinkList& L)//正向建立单链表
{
int x;//设ElemType为整型
L=(LinkList)malloc(sizeof(LNode));//建立头节点(初始化空表)
LNode* s,*r=L;//r为表尾指针
scanf_s("%d",&x);//输入结点的值
while(x!=9999)//输入9999表示结束
{
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;//在r节点之后插入元素x
r=s;//r指向新的表尾节点(永远保持r指向最后一个节点)
scanf_s("%d,",&x);
}
r->next=NULL;//尾节点指针置空
return L;
}
头插法
//头插法建立单链表
LinkList List_HeadInsert(LinkList& L)//逆向建立单链表
{
LNode* s;
int x;
L=(LinkList)malloc(sizeof(LNode));//创建头节点
L->next=NULL;//初始为空链表
scanf_s("%d",&x);//输入节点的值
while(x!=9999)//输入9999表示结束
{
s=(LNode*)malloc(sizeof(LNode));//创建新节点
s->data=x;
s->next=L->next;
L->next=s;//将新结点插入表中,L为头指针
scanf_s("%d",&x);
}
return L;
}
//后插操作:在p节点之后插入元素e
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保存数据元素e
s->next=p->next;
p->next=s;//将节点s连到p之后
return true;
}
重要知识回顾
双链表
知识总览
-
双链表的定义以及它与单链表的比较
初始化
-
代码实现(带头节点)
//初始化双链表(带头节点) typedef struct DNode { ElemType data; struct DNode* prior,*next; }DNode,*DLinkList; //DLinkList1等价于DNode* bool InitDLinkList(DLinkList& L) { L=(DNode*)malloc(sizeof(DNode));//分配一个头节点 if(L==NULL)//内存不足,分配失败 return false; L->prior=NULL;//头节点的prior永远指向NULL L->next=NULL;//头节点之后暂时还没有节点 return true; } void testDLinkList() { //初始化双链表 DLinkList L; InitDLinkList(L); //后续代码。。。 } //判断双链表是否为空(带头节点) bool Empty(DLinkList L) { if(L->next==NULL) return true; else return false; }
插入
-
代码实现(简单版本)
-
代码实现(升级版本)
//在p节点之后插入s节点 bool InsertNextDNode(DNode* p,DNode* s) { if(p==NULL&&s==NULL)//非法参数 return false; s->next=p->next; if(p->next!=NUll)//如果p节点有后继节点 p->next->prior=s; s->prior=p;//修改指针时要注意顺序 p->next=s; return true; }//按照位序插入的时前插操作
-
注意:自己在修改指针的时候,要明确指针的指向是否正确,要自己理清逻辑(与示意图进行结合,会更加地显眼)
删除
- 删除节点以及销毁整个双链表的代码实现:
//双链表的删除
void DestoryList(DLinkList& L)
//循环释放各个数据节点
while(L->next!=NULL)
DeleteNextDNode(L);
free(L);//释放头节点
L=NULL;//头指针指向NULL
//删除p的后继节点q
p->next=q->next;
q->next->prior=p;
free(q);
//删除p节点的后继节点
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)//q节点不是最后一个节点
q->next->prior=p;
free(q);//释放节点空间
return true;
}
遍历
- 要想实现按位查找,你需要一个计数器来知道你现在在第几个节点,然后才能进行操作,按值查找直接遍历,然后与所给定的值进行一个对比就可以了!
思维导图
循环链表
循环单链表
- 单链表:表尾节点的next指针指向NULL;
- 循环单链表:表尾节点的next指针指向头节点;
循环单链表,判断循环单链表是否为空以及判断p节点是否为表尾节点代码:
//循环单链表
typedef struct LNode//定义单链表节点类型
{
ElemType data;//每个节点存放一个数据元素
struct LNode* next;//指针指向下一个节点
}LNode,*LinkList;
//初始化一个循环单链表
bool InitList(LinkList& L)
{
L=(LNode*)malloc(sizeof(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;
}
- 单链表:从一个节点出发,只能找到后续的几个节点;
- 循环单链表:从一个节点出发,可以找到其他任何一个节点;
时间复杂度分析
- 注意:上面的红色NULL,是在单链表的情况下进行分析的,不要被误解了。
循环双链表
初始化
//初始化空的循环双链表
//循环单链表
typedef struct DNode//定义单链表节点类型
{
ElemType data;//每个节点存放一个数据元素
struct DNode* prior,*next;//指针指向下一个节点
}DNode,*DLinkList;
bool InitDLinkList(DLinkList& L)
{
L=(LNode*)malloc(sizeof(LNode));//分配一个头节点
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(DLinkList L,DNode*p)
{
if(p->next==L)
return true;
else
return false;
}
void testDLinkList()
{
//初始化循环双链表
DLinkList L;
InitDLinkList L;
//....后续代码....
}
插入
//在p节点之后插入s节点
bool InsertNextDNode(DNode* p,DNode* s)
{
s->next=p->next;//将节点*s插入到节点*p之后
p->next->prior=s;
s-prior=p;
p->next=s;
}
删除
//删除p的后继节点q
p->next=q->next;
q->next->prior=p;
free(q);
思维导图
静态链表
知识总览
- 静态链表:分配一整片连续的内存空间,各个节点集中安置!
含义
- 静态链表中的头节点是不存放数据元素的,后面存放的是游标(下一个节点的数组下标),另外每个数据元素的地址都可以通过求字节大小来进行找到,公式为起始地址+每个节点的大小*游标。当游标为-1的时候,表示已经到达了表尾!
定义(代码)
#define MaxSize 10//静态链表的最大长度
typedef struct//静态链表结构类型的定义
{
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
}SLinkList[MaxSize];
#define MaxSize 10
struct Node
{
ElemType data;
int next;
};
typedef struct Node SLinkList[MaxSize];
void testSLinkList()
{
SLinkList a;//a是一个静态链表
//....后续代码....
}
void testSLinkList()
{
struct Node a[MaxSize];//a是一个Node型数组
//...后续代码...
}
- 上面这种使用typedef来定义的静态链表是很少见的,但是却可以很好地实现让用户明确地知道这是一个静态链表,而如果使用最简单的定义方式的话,那么只能够知道a是一个Node型数组,这可能会让自己混淆!
下面是验证上面的typedef定义的原理:
- 注意:上图中的代码,使用SLinkList定义的b是一个静态链表,它的大小是80,因为它是一个元素个数为10的一维数组,而且这个数组的元素类型是struct类型的,通过计算可以得知上图中的struct类型的大小是8个字节!
- 结论:SLinkList b相当于定义了一个长度为MaxSize的Node类型的数组。
基本操作
- 初始化一个静态链表:把a[0]的next设为-1。(把其它节点的next设为一个特殊值用来表示节点空闲,如-2,这也方便后续对静态链表进行操作)
- 查找:从头节点出发挨个往后遍历节点,所以时间复杂度是O(n)。(找到的是每一个位序的节点,补充一个知识点,位序:各个节点在逻辑上的顺序。而现在的静态链表中的数组下标反应的是各个节点在物理上的一个顺序。)
- 插入位序为i的节点:
- 找到一个空的节点,存放数据元素。(如何判断某个节点是否为空,可让next为某个特殊值,如-2)
- 从头节点出发找到位序为i-1的节点。
- 修改新节点的next。
- 修改i-1号节点的next。
- 删除某个节点:
- 从头节点出发找到前驱节点。
- 修改前驱节点的游标。
- 被删除节点next设为-2。
知识回顾
顺序表VS链表
知识总览
-
逻辑结构:都属于线性表,都是线性结构。
-
存储结构:
- 顺序表(顺序存储):
- 优点:支持随机存储,存储密度高,只存储数据元素,没有存放其它的东西。
- 缺点:大片连续空间分配不方便,改变容量不方便。
- 链表(链式存储):
- 优点:离散的小空间分配方便,改变容量方便。
- 缺点:不可随机存取,存储密度低(因为还要存储指针之类的东西)
- 顺序表(顺序存储):
-
基本操作(创销,增删改查):
-
创:
- 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量(容量可改变,但需要移动大量元素,时间代价高);若分配空间过大,则浪费内存资源。
- 链表:只需分配一个头节点(也可以不要头节点,只声明一个头指针),之后方便拓展。
-
销:
-
顺序表:修改Length=0,若使用静态分配(静态数组),那么系统会自动回收空间;若使用动态分配(动态数组),那么需要手动free。
代码如下:
-
链表:依次删除各个节点(free)。
-
-
增/删:
- 顺序表:插入/删除元素要将后续元素都后移/前移,时间复杂度O(n),时间开销只要来自移动元素。若数据元素很大,则移动的时间代价很高。
- 链表:插入/删除元素只需修改指针即可,时间复杂度O(n),时间开销主要来自查找目标元素。查找元素的时间代价更低。
-
查:
- 顺序表:按位查找,时间复杂度是O(1);按值查找,O(n),若表内元素有序,可在O(log2n(以2为底的n的对数))时间内找到。
- 链表:按位查找和按值查找的时间复杂度都是O(n)。
-
-
具体选择方式:
- 表长难以预估,经常要增加/删除元素 — —链表
- 表长可预估,查询(搜索)操作较多 — —顺序表