静态(单)链表分析——游标与一维数组

前言

(本文参考严蔚敏《数据结构》C语言版,针对静态单链表提供了更为详细的分析,并加入了一些自己的风格与更详细注释,供初学者参考讨论)

在了解静态单链表之前,我们先简要回顾一下顺序表和动态单链表:

1)顺序表:

数组实现,用一组地址连续的存储单元依次存储线性表的数据元素。利用 realloc 和 LISTINCREMENT(块)实现变长数组(resizable array)

特点:逻辑上相邻的两个元素在物理位置上也相邻。

优点:允许随机访问(通过下标),“求表长”和“取第 i 个元素”时间复杂度O(1)。

缺点:执行删除或插入时要移动大量元素。

2)动态单链表:

链式实现,节点(结点)分为 数据域 和 (单个)指针域(指向直接后继元素),通过指针域的指针(链)将 n 个结点链成一个链表。

因为链表的长度可用 malloc 和 free 自由控制,故称其为“动态”。

特点:用一组任意的(连续或不连续)存储单元存储线性表的数据元素。

优点:对存储空间的利用率高,插入和删除方便。

缺点:不支持随机访问。

本人创建链表的习惯:(下例为双向链表)

typedef struct _polynomical {
    int coe;//系数 coefficient
    int expo;//指数 exponent
    struct _polynomical *next;//!!!切记 struct
    struct _polynomical *prev;//!!!切记 struct
} polynomical;
typedef struct _poly {
    unsigned size;//项数
    polynomical *head;
    polynomical *tail;
} Poly;

多项式ADT的初阶链表实现(添项、相加、相乘)https://blog.csdn.net/m0_60412535/article/details/125013261?spm=1001.2014.3001.5501

 3)静态单链表 

静态单链表就像是顺序表和动态单链表的结合版:利用了顺序表的一维数组描述方法,又有动态单链表的类似结点结构和访问特性(指针与游标)。

“静态”的含义:一般静态单链表只需 define 一个 MAXSIZE,之后链表大小不再改变。不需要像顺序表一样 resizable,因为它能像动态单链表一样重复利用空间(注意静态单链表并没有“释放”空间,而是利用了一个备用链表)

关键1:游标cursor

关键2:备用链表

意义:静态单链表的描述方法便于在不设“指针”类型的高级程序设计语言中使用链表结构。

静态单链表创建

#define MAXSIZE 100//链表的最大长度

typedef struct {
    int data;
    int cur;//!!!游标:(cursor)其值为下个元素的数组下标(等效于指针)
} component, SLinkedList[MAXSIZE];

 静态单链表的第一个关键点就是游标的使用:

在某些高级程序设计语言中并没有“指针”,此时 int 型的游标就可以发挥指针的作用,即在每个结点内存储了下一个结点在结构数组中的位置。

main()中创建:

SLinkedList ls;

结构分析

参考严蔚敏《数据结构》

 我们可以与动态单链表做类比:

分析:

若令 i = S[0].cur,则 S[i].data 为第一个数据元素

i = S[i].cur 类似于 p = p->next

例如元素的查找:

//在静态链表查找第一个 e 元素
//找到则返回位序,否则返回 0
int LocateElem_SL(SLinkedList S, int e){
    int i = S[0].cur;// S[0].cur 的作用相当于头指针
    while (i && S[i].data != e) {
        i = S[i].cur;//相当于 p = p->next;
    }
    return i;
}

静态单链表的删除和插入(前奏)

关键问题:提高空间利用率,又为了避免大量的元素移动,我们应该如何辨明数组中哪些分量未被使用或被删除?

关键点2:备用链表——将所有未使用或已删除的分量单独又链成一个链表。

插入时:从备用链表上取得“第一个”(不一定最靠前,而是 space[0].cur所指分量)结点作为代插入的新节点。

删除时:将被删除的结点转移到备用链表中。

初始化代码:

//!!!辨明数组中哪些分量未被使用:
//  创建一个备用链表(数组)表示未被使用的和被删除的分量
//!!!注意:
//  备用链表并非新建一个链表,而是将原链表中未被使用的和被删除的分量用游标链成一个链表
//!!!此函数应该当作初始化函数(整个都是备用链表)!!!
void InitSpace_SL(SLinkedList space){
    for (int i = 0; i < MAXSIZE - 1; ++i) {
        space[i].data = 0;//可省
        space[i].cur = i + 1;
    }
    space[MAXSIZE - 1].data = 0;//可省
    space[MAXSIZE - 1].cur = 0;
}

自定义一种 “malloc”:

注意每次调用都会更新 space[0].cur,即未使用或删除的分量位置(不一定最靠前)改变,便于下次使用。

//若备用空间链表非空,则返回节点下标,否则返回 NULL
//可用于生成头节点 或 分配新节点
//!!!此函数应用于循环中,每次更新 space[0].cur
int Malloc_SL(SLinkedList space){
    int i = space[0].cur;
    if (space[0].cur) {//备用空间链表非空
        space[0].cur = space[i].cur;
    }
    return i;
}

自定义一种 “free”:

//将下标为 k 的空间回收到备用链表
void Free_SL(SLinkedList space, int k){
    space[k].cur = space[0].cur;
    //分析:
    // space[0] 可视作备用链表头节点,space[0].cur 为整个链表中的一个未被利用/被删除的分量下标
    // space[k] 为要删除的节点
    // space[k].cur = space[0].cur; 让该节点的游标指向 space[0].cur 原来所指分量
    space[0].cur = k;
    //使备用链表头节点游标指向 space[k] ,让该节点融入备用链表
}

这种回收到备用链表的方法/思路有点像“从表尾到表头逆向建立单链表的算法”:

我们用动态单链表表示这种思路:

备用链表工作原理与此类似,将被删除的分量插在备用链表头节点后面。

这种方法能确保每次最先使用的是被删除的分量,而非之后未使用的分量。

从实例了解插入和删除:

现在要解决这样一个问题:

求(A - B)U (B - A)

文字描述:假设由终端输入集合元素,先建立表示集合 A 的静态链表 S ,而后在输入集合 B 的元素的同时查找 S 表,若存在和 B 相同的元素,则从 S 表中删除之,否则将元素插入 S 表。

思路:

首先初始化备用空间(备用链表),确定链表 S (A)的头节点。

因为是先创建 A 再从输入中把 B 的元素依次与 A 中每一项比较,所以我们要定好 A 的尾元素在哪里—— r 指向 S 的当前最后节点。

创建 A(尾插即可),依次读入 B 中元素,将每个元素与 A 中元素依次比较,相同从 A 中删除(此时要更改 r),不同则添加到 r 之后(注意不是扩充后的链表尾端,此时不要更改 r)

具体实现:

int difference(SLinkedList space){
    InitSpace_SL(space);//初始化,创建链表和备用链表
    int S = Malloc_SL(space);//因为刚刚初始化,此为生成头节点
    int r = S;// r 用于指向 S 的当前最后节点
    unsigned m, n;//记录 A 和 B 的个数
    
    printf("Enter the size of A & B:\n");
    scanf("%u %u", &m, &n);//获取 A 和 B 的个数
    for (int j = 1; j <= m; ++j) {//建立 A 的链表
        int i = Malloc_SL(space);//用 i 获取到被分配的空间下标
        printf("Enter the No.%d in A:\n", j);
        scanf("%d", &(space[i].data));
        space[r].cur = i;//让 S 的当前最后节点游标指向刚被分配到的空间(插入到表尾)
        r = i;//更新 r(最后节点变化)
    }
    space[r].cur = 0;//尾节点游标为 0 ,表示 A 读取结束
    //此时 r 为链表 A 的尾节点
    for (int j = 1; j <= n; ++j) {//依次读入 B 的元素
        int b;//获取 B 中每一个元素的值
        int p = S;// p 表示 k 的(逻辑上)前一个节点,便于删除操作
        int k = space[S].cur;// space[S] 为链表 A 头节点,k 指向 A 的第一个节点
        
        printf("Enter a number in B:\n");
        scanf("%d", &b);
        while (k != space[r].cur && space[k].data != b) {// k 在指向尾节点 或 找到 b 后退出 while
            p = k;
            k = space[k].cur;
        }
        if (k == space[r].cur) {//如果 k 是到了尾节点,表明 A 中没有该元素,执行插入
            int i = Malloc_SL(space);
            space[i].data = b;
            space[i].cur = space[r].cur;
            //意为:所有元素都插在 r 后面,而非扩充后的 A 链表尾端
            //省去一个指向新链表尾端的游标
            space[r].cur = i;//插入在 r 之后,注意 r 表示链表 A 的尾节点,不要更改
        }else{//如果找到了 b ,执行删除
            space[p].cur = space[k].cur;// p 为 k 的前一个节点,其游标指向 k 的下一个节点
            Free_SL(space, k);
            if (r == k) {//!!!注意:当删除的是尾节点时,更改 A 的尾节点( r )
                r = p;
            }
        }
    }
    
    return S;
}

过程图解:

书中实例:A = (c,b,e,g,f,d) ; B = (a,b,n,f) ,求(A - B)U (B - A)

1)前四行代码执行后

注意创建 S 的头节点时用了一次 “malloc”,更新了备用链表的头节点游标

 2)建立 A 的链表 S

 3)修改过程:

 

在main()中测试 

#include <stdio.h>
#include "StaticLinkedList.h"

int main(int argc, char *argv[]){
    SLinkedList ls;//创建了一个结构数组
    
    int head1 = difference(ls);
    printf("The final list:\n");
    show_LS(ls, head1);
    
    return 0;
}

测试案例:

A = (2,1,6,7,4) ; B = (7,9,1) 求(A - B)U (B - A)

预期结果:2,6,4,9

 答案正确

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值