3

数据结构05——静态链表、循环链表、双向链表

 2 years ago
source link: https://blog.51cto.com/Laccoliths/5660101
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1 静态链表

在C语言中,我们可以利用指针能力非常容易地操作内存中的地址和数据,这比其他高级语言要更加的灵活和方便。面向对象的高级语言,虽然不使用指针,但因为启用了对象引用机制,也间接实现了指针的某些作用,但对于如Basic、Fortran等早期的编程高级语言,由于没有指针,没有办法实现链表结构,此时静态链表就出现了,使得我们可以用数组来代替指针描述单链表。

1.1 静态链表的定义及初始化

静态链表:用数组代替指针来描述单链表,也可以叫做游标实现法。

数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间"一对一"的逻辑关系通过一个整形变量(称为"游标",和指针功能类似)来访问。

#define MAXSIZE 1000
typedef struct{
ElemType data;
int cur; // 游标(cursor),为0时表示无指向
}Component,StaticLinkList[MAXSIZE];

静态链表的特殊处理:对第一个和最后一个元素作为特殊元素处理,不存数据。通常把未被使用的数组元素称为备用链表。

  • 数组第一个元素,即下标为0的元素的cur存放备用链表的第一个结点的下标
  • 数组最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表的头结点作用,当整个链表为空时,则为0。
数据结构05——静态链表、循环链表、双向链表_链表

静态链表的初始化操作:

// 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,“0"表示空指针
Status InitList(StaticLinkList space){
int i;
for(i=0; i<MAXSIZE-1; i++){
space[i].cur = i + 1;
}
space[MAXSIZE-1].cur = 0; // 静态链表为空,最后一个元素的cur为0
return OK;
}

假设已经将数据存入静态链表,比如分别存放着“甲”、“乙”、“丁”等数据,则他们在静态链表中将处于如图所示的状态。

数据结构05——静态链表、循环链表、双向链表_链表

1.2 静态链表的插入操作

静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。

在动态链表中,结点的申请和释放分别借用​​malloc()​​​和​​free()​​两个函数来实现;而在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,需要我们自己实现这两个函数,才可以实现插入和删除操作。

为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第—个结点作为待插入的新结点。

int Malloc_SSL(StaticLinkList space){
int i = space[0].cur; // 当前数组第一个元素的cur存的值,即第一个备用空闲的下标
if(space[0].cur){
space[0].cur = space[i].cur; //拿出备用链表的一个分量来使用,将其作为下个分量
}
return i;
}

插入示意图如图所示:

数据结构05——静态链表、循环链表、双向链表_结点_03
Status ListInsert(StaticLinkList L,int i,ElemType e){
int j,k,l;
k = MAXSIZE-1; // k是最后一个元素下标
if(i<1 || i > ListLength(L)+1){ // 插入位置违法
return ERROR;
}
j = Malloc_SSL(L); // 获取空闲分量的下标
if(j){
L[j].data = e; // 将数据复制给此分量的data
for (l=1; l<=i-1; l++){ // 找到第i个元素之前的位置
k = L[k].cur;
}
L[j].cur = L[k].cur; // 第i个元素之前的cur赋值给新元素的cur
L[k].cur = j; // 把新元素的下标赋值给第i个元素之前元素的cur
return OK;
}
return ERROR;
}

静态链表插入算法思路:

  1. 执行插入语句时,我们的目的是要在“乙”和“丁”之间插入“丙“。调用代码时,输入i值为3。
  2. 第3行让k=MAXSIZE-1=999。
  3. 第7行,j=Malloc_SSL(L)=7。此时下标为0的cur也因为7要被占用而改变备用链表值为8。
  4. 第9行和第10行,for 循环由1到2,执行两次。代码k=L[k].cur; 使k=999,得到k=L[999].cur=1,再得到k=L[1].cur=2。
  5. 第13行,L[j].cur = L[k].cur;j=7,k=2得到L[7].cur=L[2].cur=3。这就是刚才的让丙,把他的cur改为3的意思。
  6. 第14行,L[k].cur = j;意思就是让L[2].cur=7。也就是让乙把它的cur改为指向丙的下标7.

1.3 静态链表的删除操作

删除元素时,是需要释放结点的函数​​free()​​,现在我们也需要自己实现。

/* 将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}

意思就是“甲”现在要走,这个位置就空出来了,也就是,未来如果有新人来,最优先考虑这里,所以原来的第一个空位分量,,即下标是8的分量,它降级了,把8给“甲“,所在下标为1的分量的cur,也就是space[1].cur≡space[0].cur=8,而space[0].cur=k=l,其实就是让这个删除的位置成为第—个优先空位,把它存入第一个元素的cur中,如下图所示。

数据结构05——静态链表、循环链表、双向链表_静态链表_04

ListLength():

/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}

1.4 静态链表的优缺点

优点

  • 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了再顺序存储结构中插入和删除操作需要移动大量元素的缺点。

缺点

  • 没有解决连续存储分配带来的表长难以确定的问题
  • 失去了链式存储结构随机存取的特性

2 循环链表

循环链表:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。

循环链表解决了一个很麻烦的问题:如何从当中一个结点出发,访问链表的全部结点。

循环链表带有头结点的空链表如下图所示:

数据结构05——静态链表、循环链表、双向链表_链表_05

对于非空的循环链表如下图所示:

数据结构05——静态链表、循环链表、双向链表_结点_06

要将两个循环链表合并成一个表时,有了尾指针就非常简单了。比如下面的这两个循环链表,它们的尾指针分别为​​rearA​​​和​​rearB​​,如下图所示:

数据结构05——静态链表、循环链表、双向链表_链表_07

要将它们合并,只需要如下的操作即可。

数据结构05——静态链表、循环链表、双向链表_链表_08
p = rearA->next; // 保存A表的头结点,即步骤1
rearA->next = rearB->next->next; // 1. 将本是指向B表的第一个结点(不是头结点) 2. 复制给rearA->next,即步骤2
q = rearB->next;
rearB->next = p; // 将原A表的头结点复制给rearB->next,即步骤3
free(q); // 释放q

3 双向链表

在单链表中,有了next指针,使得我们要查找下一结点的时间复杂度为,但是如果我们要查找上一结点的话,最坏的时间复杂度就是。

双向链表(double linked list):在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表的结点中都有两个指针域,一个指向直接后继,另一个指向直接前驱。

双向链表定义:

typedef struct DulNode{
ElemType data;
struct DulNode *prier; // 直接前驱指针
struct DulNode *next; // 直接后继指针
}DulNode,*DuLinkList;

双向链表的循环带头结点如图所示:

数据结构05——静态链表、循环链表、双向链表_静态链表_11

非空循环带头结点的双向链表如图所示:

数据结构05——静态链表、循环链表、双向链表_静态链表_12

双向链表是单链表中扩展出来的结构,所以它的很多操作是和单链表相同的,比如求长度的ListLength,查找元素的GetElem,获得元素位置的LocateElem等,这些操作都只要涉及一个方向的指针即可,另—指针多了也不能提供什么帮助。

插入操作很简单,但是顺序很重要。现在假设存储元素e的结点为s,要实现将结点s插入到结点p和p->next之间的步骤如图所示:

数据结构05——静态链表、循环链表、双向链表_静态链表_13
s -> prior = p; // 将p赋值给s的前驱,步骤1
s -> next = p -> next; // 把p->next赋值给s的后继,步骤2
p -> next -> prior = s; // 把s赋值给p->next的前驱,步骤3
p -> next = s; // 把s赋值给p的后继,步骤4

删除操作也很简单,若要删除结点p,只需要下面两个步骤,如图所示:

数据结构05——静态链表、循环链表、双向链表_链表_14
p->prior->next = p->next; // 把p->next赋值给p->prior的后继,步骤1
p->next->prior = p->prior; // 把p->prior赋值给p->next,步骤2
free(p); // 释放结点

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK