第3章 线性表(二)

第3章 线性表(二)

静态链表

​ 在早期没有指针的语言中。用数组描述单链表。

​ 让数组元素由两个数据域组成,data和cur。data相当于之前的data,cur相当于之前的next,cur存放该元素的后继在数组中的下标。cur叫做游标。

​ 我们把用数组描述的链表叫做静态链表。

线性表的静态存储结构:

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

​ 我们对数组的第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。数组第一个元素,即下标为0的元素的cur就存放着备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点的作用,当整个链表为空时,则为02。如图

image-20220227160102787

这个图就相当于初始化数组的状态,

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

image-20220227161715861

image-20220227161734333

​ 此时“甲”这里就存有下一元素“乙”的游标2,乙则存有下一元素丁的游标3。而庚是最后一个有值元素,所以他的cur设置为0。最后一个元素的cur因为第一有值元素存在它的下标1.而第一个元素则因空闲空间的第一个元素的下标是7,所以它的游标是7。

静态链表的插入操作

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

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

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

//若备用空间链表非空,则返回分配的结点下标,否则返回0。
int Malloc_SLL(StaticLinkList space) {
    int i = space[0].cur;  //当前数组第一个元素cur存在的值。就是要返回的第一个备用空闲的下标。
    if (space[0].cur) {
        space[0].cur = space[i].cur;//由于要拿出一个分量来使用,所以我们就得把它的下一个分量用来做备用。
    }
    return i;
}

我的理解:相当于是将下一个空间使用权限返回,并将再下一个空闲的地址放在静态链表的可用游标中。

​ 这段代码有意思,一方面它的作用就是返回一个下标值,这个值就是数组头元素的cur存的第一个空闲的下标。从上面的图示例子来看,其实就是返回7。
​ 那么既然下标为7的分量准备要使用了,就得有接替者,所以就把分量7的cur值赋值给头元素,也就是把8给space[0].cur,之后就可以继续分配新的空闲分量,实现类似malloc()函数的作用。
​ 现在我们如果需要在“乙”和“丁”之间,插入一个值为“丙”的元素,按照以前顺序存储结构的做法,应该要把“丁”、“戊”、“己”、“庚”这些元素都往后移一位。但目前不需要,因为我们有了新的手段。新元素“丙”,想插队,可以,你先悄悄地在队伍最后一排第7个游标位置待着,我一会就能帮你搞定。我接着找到了“乙”,告诉他,你的cur不是游标为3的了,这点小钱,意思意思,你把你下一位的游标改成7就行了。“乙”将cur改了。然后丙将cur改成3。就这样,就将整个序列发生了改变。

image-20220227184014346

实现代码:

//在L中第i个元素之前插入新的数据元素e
Status SSLInsert(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_SLL(L);//获得空间分配的下标
    if (j) 
    {
        L[i].data = e;  //将数据赋值给分配好的空间。
        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. 第四行代码让k = MAXSIZE-1=999。//减一是因为从下标为1的地方开始存。
  3. 第7行,j = Malloc_SSL(L) = 7。此时下标为0的cur也因为7要被占用而更改链表的值为8。
  4. 第11~12行for循环1由1到2,执行两次。代码k = L[1].cur = 2。
  5. 第13行,L[j].cur = L[k].cur;因j=7,而k=2得到L[7].cur = L[2].cur=3。//类似于单链表找到前一个元素更改他的next。
  6. 第14行,L[k].cur = j;意思就是L[2].cur = 7。//插入新元素的next。

静态链表的删除操作

先实现Free_SLL:

//将下标为k的空间结点回收到备用链表
void Free_SSL(StaticLinkList space,int k){
    space[k].cur = space[0].cur;//把第一个元素cur值赋给要删除的分量cur
    space[0].cur = k;// 把要删除的分量下标赋值给第一个元素的cur
}

​ 意思就是甲要走,这个位置要空出来了,也就是,未来如果有新人来先使用这里的空间。并且将空出来位置的cur换成原来的有值元素的最后一个cur。

image-20220227191818164

实现删除:

//删除在L中第i个数据元素e
Status SSLDelete(StaticLinkList L, int i) {
    int j, k;
    if (i < 1 || i > ListLength(L))
        return ERROR;
    k = MAXSIZE - 1;  //最后一个元素的cur存放的是第一个元素的地址。
    for (j = 1;j<=1;j++)
        k = L[k].cur;
    j = L[k].cur;
    L[k].cur = L[j].cur;
    Free_SSL(L,j);
    return OK;
}

前面的代码中有需要实现Listlength():

//计算SSL的长度
int ListLength(StaticLinkList L) {
    int j = 0;
    int i = L[MAXSIZE - 1].cur;
    while (i) {
        i = L[i].cur;
        j++;
    }
    return j;
}

​ 具体其他的功能实现都类似。

静态链表优缺点

优点:

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

缺点:

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

代码汇总:

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


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

//若备用空间链表非空,则返回分配的结点下标,否则返回0。
int Malloc_SLL(StaticLinkList space) {
    int i = space[0].cur;  //当前数组第一个元素cur存在的值。就是要返回的第一个备用空闲的下标。
    if (space[0].cur) {
        space[0].cur = space[i].cur;//由于要拿出一个分量来使用,所以我们就得把它的下一个分量用来做备用。
    }
    return i;
}

//计算SSL的长度
int ListLength(StaticLinkList L) {
    int j = 0;
    int i = L[MAXSIZE - 1].cur;
    while (i) {
        i = L[i].cur;
        j++;
    }
    return j;
}

//在L中第i个元素之前插入新的数据元素e
Status SSLInsert(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_SLL(L);//获得空间分配的下标
    if (j) {
        L[i].data = e;  //将数据赋值给分配好的空间。
        //找到第i个元素之前的位置。
        for (l = 1; l < i - 1; ++l) {
            k = L[k].cur;
        }
        L[j].cur = L[k].cur;//把第i个元素之前的cur赋值给新的cur
        L[k].cur = j;//把新的元素的下标赋值给第i个元素之前元素的cur
        return OK;
    }
    return ERROR;
}

//将下标为k的空间结点回收到备用链表
void Free_SSL(StaticLinkList space,int k){
    space[k].cur = space[0].cur;//把第一个元素cur值赋给要删除的分量cur
    space[0].cur = k;// 把要删除的分量下标赋值给第一个元素的cur
}


//删除在L中第i个数据元素e
Status SSLDelete(StaticLinkList L, int i) {
    int j, k;
    if (i < 1 || i > ListLength(L))
        return ERROR;
    k = MAXSIZE - 1;  //最后一个元素的cur存放的是第一个元素的地址。
    for (j = 1;j<=1;j++)
        k = L[k].cur;
    j = L[k].cur;
    L[k].cur = L[j].cur;
    Free_SSL(L,j);
    return OK;
}

循环链表

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

为了使空链表与非空链表处理一致,我们通常设一个头结点,当然,这并不是说循环链表一定要有头结点。

image-20220227193820298

​ 其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在是p->next不等于头结点,则循环未结束。

​ 在单链表中,我们有头结点时候,可以用O(1)的时间访问第一个结点,但是对于要访问到最后一个结点,却需要O(n)时间,因为我们需要将单链表全部扫描一遍。

​ 我们可以终端结点的尾指针来表示循环链表,这样查找开始和结尾链表就都很方便了。都只要O(1)的时间复杂度。

image-20220227194247020

​ 从上面的图可以看到,终端结点用尾指针rear表示,则查找终端结点是O(1),而开始结点,就是rear->next->next,其时间复杂度也为O(1)。

​ 举个例子,将两个循环合并成一个表时,有了尾指针就非常简单了。如图视:

合并前:image-20220227194528405

合并后:

image-20220227194554646

p = rear->next;  //保存A表的头结点就是①
rearA->next = rearB->next->next;//将本来指向B表的第一个结点(不是头结点)赋值给rearA->next,②
q = rear->next;
rearB->next = p;  //将原来A表头的头结点赋值给rearB->next,即③
free(q);// 释放空间。

双向链表

​ 双向链表是在单链表的每个节点中,在设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接前驱,另一个指向直接后继。

//线性表的双向链表存储结构
typedef struct DulNode{
    ElemType data;
    struct DulNode *prior;  //直接前驱指针
    struct DulNode *next;   //直接后继指针
}DulNode,*DulLinkList;

双向链表可以是一个循环表。

image-20220227201508083

​ 由于这是双向链表,那么对于链表中的某个结点p他的后继的前驱是谁?他的前驱的后继又是谁?答案都是自己。

​ 双向链表的很多操作都和单链表相同。如求长度,查找元素,获得元素位置等。这些只需要使用一个方向的指针就可以了。

​ 在插入和删除的时候需要付出小小的代价,就是需要多更改两个指针。而且更改指针时的顺序必须正确,否则就无法将链表正确连接。

image-20220227201934536

s->prior = p;			//把p赋值给s的前驱如图中①
s->next = p->next;		//如图中②
p->next->prior = s;		//如图中③
p->next = s;			//如图中④

​ 这里关键是他们的执行顺序,由于第二和三部都用到了p->next。如果第四步先执行,则会使用p->next就提前变成了s,会使得插入的工作完不成。所以我们需要理解上面的图。顺序就是先搞定s的前驱和后继,然后搞定后结点的前驱,最后搞定前结点的后继。

​ 插入理解了删除就很简单了。

image-20220227203830137

核心代码实现:

p->prior->next = p->next;  //如图中1
p->next->prior = p->prior;//如图中2
free(p);//释放空间。

代码汇总:

总结

image-20220227205447228

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黎丶辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值