考研408复习笔记—— 数据结构(四)
编写不易,希望各位看到能点个赞。
若发布的内容有什么错误,欢迎留言探讨。
前篇跳转
二、线性表(三)
2.4.2 双链表
双链表,同样是链式存储的结构,与单链表,只能一个方向进行遍历不同,它存储了两个指针域
,用于存放前后两个结点的指针,方便开发时进行双向遍历。创建代码如下:
typedef struct DNode(){ //定义双链表
ElemType data; //数据域
struct DNode *prior,*next; //定义前驱和后继
}DNode, *DLinkList;
2.4.2.1 双链表的初始化
初始化的操作与单链表类似,同样一般采用的是带头节点的链表。具体过程就不进行描述了,直接展示代码:
//初始化双链表
bool InitDLinkList(DLinkList &L){
L=(DNode *)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) return false; //内存不足,分配失败
L->prior=NULL; //创建前驱与后继,头节点的prior永远指向NULL
L->next=NULL;
return true;
}
针对该链表,如果想要判断链表是否为空,就只要判断链表头结点的next指针是否为空便可以了。
2.4.2.2 双链表的插入
对于双链表的基本插入操作,我们只需要更改要插入位置结点的后继指针和插入位置原本后继结点的前驱指针就可以了。具体代码如下:
//在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s){
s->next=p->next; //将s插入到p结点后更改相应指针
if(p->next !=NULL){ //如果p后有后继结点,不修改原后继结点的前驱指针
p->next->prior=s;
}
s->prior=p;
p->next=s;
}
由于双链表的遍历比较方便,因此,通过该插入操作便可以满足基本的插入需求。
2.4.2.3 双链表的插入
同样相比于单链表,咱们只需要断开想要删除的结点的前后指针没然后令其的前驱与后继相接便可以了。
代码如下:
//删除p结点的后继结点
bool DeleteNextDNode(DNode *p){
DNode *q=p->next; //找到要删除的后继结点
if(q==NULL) return false; //p不存在后继,停止操作
p->next=q->next;
if(q->next!=NULL){ //q不是最后一个结点
q->next->prior=p;
}
free(q); //释放q
return true;
}
删除整个双链表操作代码:
void DestoryList(DLinkList &L){
//循环释放各个数据结点
while(L->next != NULL){
DeleteNextDNode(L);
}
free(L); //释放头结点
L=NULL; //头指针清空
}
相对来说,双链表的遍历比较方便,我们只需要通过next和prior指针的跳转就可以找到所有元素。进行按位查找和按值查找的操作,只需要通过计数器来定位我们所在的结点位序便可以实现,在此就不做详细的描述,大家可以自己写一些代码尝试一下。
2.4.3 循环链表
循环链表是在单链表和双链表的基础上进行改进而成的。
1、循环单链表
循环单链表中,单链表的最后一个结点的后继指针不会指向NULL而是直接指向头结点。
这样一来在初始化单链表时,我们便要将开始头结点的后继指针指向头结点,代码如下:
bool InitLinst(LinkList &L){
L=(LNode *L)malloc(sizeof(LNode)); //分配一个头结点
if(L==NULL) return false; //内存空间满,结束操作
L->next=L; //头结点后继指向头结点
return true;
}
这样一来,在检测循环单链表
是否为空时,我们只需要看头结点的next是不是指向头结点便可以了,而在检测结点是否为最后一个结点时,我们也只需看其的next指针是不是指向头结点,是头结点则为最后一个结点。
就使用
而言,如果给的是一个单链表
,随便给一个结点p,那么我们只能够看到该链表之后的数据。而循环单链表
中,随便一个结点,我们就能够找到表中的所有结点。
2、循环双链表
与单链表一样,循环双链表同样需要让表头和表尾的两个元素相连接。这样就需要让表头的prior指向表尾,表尾的next指向表头。在该表中,next指针和prior指针各自形成了一个互为反向的闭环。
同样在初始化时,我们需要进行一些更改。令初始化时头结点的prior和next都指向头结点。代码如下:
bool InitDLinkList(DLinkList &L){
L=(DNode *)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) return false; //内存不足,分配失败
L->prior=L; //创建前驱与后继,令他们指向L
L->next=L;
return true;
}
同样的,在判断链表是否为空我们也可以得到省略, 只需看头结点的next指针是否为头结点本身。判断表尾结点则直接看该元素的next是否为头结点即可。
而在使用
中,插入和删除操作我们便不需要去考虑结点p的next结点是否存在直接进行操作即可。
2.4.4 静态链表
相对于前面两种链表离散地存储数据。静态链表是一次性在内存中申请一个连续地空间来进行存放。它在链表中一部分存放数据,另一部分(游标)存放下一个结点的数组下标。如下图,0号位置为头结点,它的下一个结点指向2,2中则存放了数据e1下一个结点则为1,以此类推。而当游标的值为-1时则表示该链表在这个结点后没有其他结点了。
创建静态链表的代码如下:
#define MaxSize 10 //静态链表最大长度
typedef struct {
ElemType data; //存储数据元素
int next; //下一个结点的数组下标
}SLinkList[MaxSize]
初始化:
在静态链表的初始化与顺序表的初始化类似,不过相应的我们需要将头结点的next值
设置为-1,空余结点的next设置为-2。
void InitList(SLinkList &L){
for(int i=0; i<MaxSize;i++){
L[i].next=-2; //将所有的空闲结点的next设置为-2
}
L[0].next=-1; //头结点的
}
查找:
从头结点出发,按照游标开始遍历,找到想要的结点的位置。(纯遍历,就不写代码了,自己写写试试)
插入位序为i的结点:
1)找到一个空的结点,存入数据元素
2)从头结点找到位序为i-1的结点
3)修改新结点的next
4)修改i-1号结点的next
bool ListInsert(SLinkList &L,int i,ElemType e){
if(i<1 || i>LinkListLength(L)) return false; //i的位置不合法停止操作
int j;
for(j=1;j<MaxSize;j++){
if(L[j].next==-2) break; //找到第一个空结点,跳出循环
}
int temp=0;
for(int k=0;k<i-1;k++){
temp=L[temp].next; //找到i-1结点位置
}
L[j].next=L[temp].next; //令j结点的next等于i-1的next
L[j].data=e; //存入数据
L[temp].next=j; //令i-1结点的next等于j
return ture;
}
删除某个结点:
1)从头结点出发找到要删除结点的前驱
2)修改前驱结点的游标
3)被删除结点next设为-2
bool LinkListDelete(SLinkList &L,int i,ElemType e){
if(i<1 || i>LinkListLength(L)) return false; //i的位置不合法停止操作
int temp=0;
for(int j=0;j<i-1;j++){
temp=L[temp].next; //找到要删除结点的前驱
}
L[temp].next=L[L[temp].next].next; //更改前驱结点的游标
e=L[L[temp].next].data;
L[L[temp].next].data=0; //清空该结点数据
L[L[temp].next].next=-2; //将删除节点还原为空结点状态
return true;
}
相对于其他链表,静态链表虽然是以数组
形式实现的链表,但是在数组中的相邻的数据不用再相邻的物理地址上。
优点:增删操作不需要大量移动元素
确定:不能随即存取,只能从头结点开始一次往后查找,`容量固定不变。
应用场景:1)不支持指针的高级语言 2)数据元素数固定不变的场景(如操作系统的文件分配表FAT)`
相对来说,顺序链表现在的使用已经很少了,在考试中也很少会考到相关的应用。
2.5 顺序表与链表的对比
相同点:
两者都属于线性表
,都是线性结构
。
不同点:
顺序表是顺序存储
,链表是链式存储
。
顺序表优点:支持随机存储、存储密度高
缺点:需要申请大片连续的空间,计算机分配不方便,改变容量不方便。
链表优点:离散的小空间,分配方便,也容易改变容量。
缺点:不可随机存储,存储密度低。
基本操作优劣对比:
1、创建:
顺序表:
创建时需要分配大片连续空间,分配过小,则不易拓展,过大则容易浪费内存资源。在静态分配时,容量不可改变,在动态分配时,更改容量需要移动大量元素,时间代价高昂。
链表:
在创建时,只需声明一个头结点,然后根据使用需求创建新的结点。相对来说,在这个操作中链表更胜一筹。
2、销毁
顺序表:
在销毁表时,如果是静态分配,我们只需将length改为0即可,但是动态分配的空间我们需要使用free函数进行释放。
链表:
在销毁时,我们需要使用free来删除每个结点。
3、插入与删除
顺序表:
在顺序表中,要求相邻的元素的物理存储位置相邻,因此插入和删除时我们需要大量的移动表内元素。所用的时间复杂度为O(n),主要来源于移动元素。
链表:
在插入和删除元素时,我们只需更改每个元素的指针即可。所用的时间复杂度为O(n),主要来源则是查找相应位置,相对来说,时间代价低于顺序表。
4、查找
顺序表:
按位查找只需要O(1),表内数据无序时,按值查找需要O(n),有序时则只需要O(log2n)。
链表:
查找操作的时间复杂度都是O(n)。