线性表文档之静态链表

Posted 二木成林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线性表文档之静态链表相关的知识,希望对你有一定的参考价值。

静态链表

注:静态链表了解即可。

定义

概念

静态链表是在还没有指针功能时提出的概念,用数组来代替指针描述单链表,并实现单链表的相关功能,比如快速插入或删除一个元素。这种用数组实现的链表就叫做静态链表。而实现静态链表采用的是游标法。我们知道静态链表实际上是一个数组,数组中的每一个元素都是一个链表结点,链表结点由数据域和游标域组成。

如图所示:

  • 我们规定静态链表:下标为 0 的结点的数据域不存放任何信息;下标为 MAXSIZE-1 的结点的数据域也不存放任何信息。
  • 静态链表的最后一个结点(下标为 MAXSIZE-1),在图中即是下标为 999 的结点的游标指向静态链表中下标为 1 的结点。即静态链表的最后一个结点中的游标存储着静态链表中第一个数据域不为空的结点的下标
  • 静态链表的第一个结点(下标为 0),在图中即是下标为 0 的结点的游标指向静态链表中下标为 5 的结点。即静态链表的第一个结点中的游标存储着静态链表中第一个没有存放数据的结点的下标
  • 其他元素的游标都存储着直接指向它下一个元素的下标。比如下标为 1 的结点的游标为 2,因为它的下一个结点在数组中的下标就是 2;比如下标为 5 的结点的游标为6,因为它的下一个结点在数组中的下标就是6。但注意,是当前结点的后继结点,并不是单纯的在数组中后面一个结点的下标。

其实上图中,可以发现静态链表中的元素分为两类:存储了数据的结点和未使用的结点。我们可以把存储了数据的结点组成的链表称为数据链表,如 999 -> 1 -> 2 -> 3;把未使用的结点组成的链表称为备用链表,如 0 -> 5 -> 6 -> 7 -> ... -> 998


注:

  • 以上静态链表的规定都是人为规定的。有些静态链表将第二个结点作为数据链表的头结点。
  • 根据以上规定,静态链表中的数据链表从 MAXSIZE-1 开始,在图中用绿色表示;备用链表从 0 开始,在图中用红色表示。

结构体

静态链表结点由一个数据域和一个指针域(即游标)组成,其中数据域存储当前结点的数据值,而指针域存储当前结点的后继结点的游标。静态链表的结构体如下:

/**
 * 静态链表节点
 */
typedef struct SLNode 
    /**
     * 静态链表节点的数据域
     */
    int data;
    /**
     * 静态链表节点的游标,存储下一个节点的下标
     */
    int cur;
 SLNode;

特点

静态链表的特点如下:

  • 静态链表本质上就是一个数组,结构体元素组成的数组。
  • 静态链表在分配内存空间时会申请一整片连续空间。
  • 静态链表实际上是没有指针的,它只是使用游标(下一个结点的数组下标)来充当指针。
  • 在插入和删除操作时不需要移动元素,只需要修改指针,具有链式存储结构的主要优点。
  • 静态链表能容纳的元素个数在表定义时就确定了,跟顺序表一样。

参考资料:

基本操作

注,完整代码请参考:

概述

静态链表的常见操作如下:

  • void init(SLNode *list):初始化静态链表。其中 list 是待初始化的静态链表。
  • int createList(SLNode *list, int nums[], int n):通过数组创建静态链表。其中 list 是未初始化的静态链表;nums 是待插入到静态链表中的数据数组;n 是数组长度。如果创建成功则返回 1,否则返回 0 表示创建失败。
  • int insert(SLNode *list, int i, int ele):在静态链表第 i 个位置插入值为 ele 的新结点。其中 list 是静态链表;i 是结点序号,从 1 开始;ele 是新结点的数据值。如果插入成功则返回 1,否则返回 0 表示插入失败。
  • void append(SLNode *list, int ele):在静态链表的尾部插入新结点。其中 list 是静态链表;ele 是新结点的值。
  • int removeByNum(SLNode *list, int i, int *ele):移除静态链表中第 i 个结点,并用 ele 保存被删结点的数据值。其中 list 是静态链表;i 是结点序号,从 1 开始;ele 保存被删结点的数据值。如果删除成功则返回 1,否则返回 0 表示删除失败。
  • SLNode findByNum(SLNode *list, int i):检索静态链表中第 i 个结点。其中 list 是静态链表;i 是结点序号,从 1 开始。如果找到则返回该节点,否则返回 NULL。
  • SLNode findByEle(SLNode *list, int ele):检索静态链表中第一个值为 ele 的结点。其中 list 是静态链表;ele 是指定值。如果找到则返回该结点,否则返回 NULL。
  • int mallocNode(SLNode *list):分配空结点,即从静态链表中的备用链表中取出一个空结点。其中 list 是静态链表。返回查找到的空结点的下标,如果返回 0 表示备用链表为空表了。
  • void freeNode(SLNode *list, int i):释放结点,即将被删结点链接到备用链表中。其中 list 是静态链表;i 是待被释放结点的下标,从 0 开始。
  • int size(SLNode *list):静态链表的实际长度,即数据链表中的结点个数。其中 list 是静态链表。返回静态链表实际长度。
  • int isEmpty(SLNode *list):判断静态链表是否为空,如果静态链表为空则返回 1,否则返回 0。其中 list 是静态链表。
  • void print(SLNode *list):打印静态链表中的数据链表的所有结点数据值。其中 list 是静态链表。

init

初始化静态链表。

实现步骤:

  • 循环遍历静态链表数组,将每个结点的游标指向它下一个结点的数组下标,初始所有结点都是备用链表的结点。
  • 最后需要将倒数第二个结点的游标置为 0,表示这是备用链表的最后一个结点。
  • 将最后一个结点的游标置为 0,表示这是这个空的静态链表。初始状态下,就应该是空表。

实现代码如下:

/**
 * 初始化静态链表
 * @param list 待初始化的静态链表
 */
void init(SLNode *list) 
    // 初始情况下,静态链表是空链表,除了最后一个节点之外(最后一个节点要用作数据链表的头结点),每一个节点的游标cur都指向后面一个节点的下标
    for (int i = 0; i < MAXSIZE; i++) // 注意,这里赋值了数组最后一个元素的游标cur,但在循环结束之后更改了它的游标值
        // 初始时每个节点的游标 cur 的值为下一个节点的下标
        list[i].cur = i + 1;
    
    list[MAXSIZE - 2].cur = 0;
    // 将最后一个节点的游标置为0,表示这是一个空静态链表
    list[MAXSIZE - 1].cur = 0;

createList

根据数组批量插入元素创建静态链表。以 nums=[11, 22, 33, 44, 55]; n=5 为例:

实现步骤:

  • 参数校验,数组长度参数 n 必须在 [1, MAXSIZE-2] 范围内,否则多余元素无法插入。
  • 遍历数组所有元素,从第二个元素开始,依次赋予每个结点的数据域值为当前正在迭代的数组元素值。因为第一个元素是备用链表的头结点,所以不能赋值。依次赋值是因为第一次赋值,空链表,每个结点都可以是挨着的。
  • 修改数据链表的尾结点的游标。将数组中第 n 个结点的游标置为 0,即指向备用链表的头结点,表示该结点是数据链表的尾结点了,也以此 cur==0 作为数据链表结束的标志。
  • 修改数据链表的头结点的游标。将静态链表中下标为 MAXSIZE-1 的结点(下标为 MAXSIZE-1 的结点是数据链表的头结点)的游标置为 1,因为下标为 1 的结点就是数据链表的开始结点的下标。
  • 修改备用链表的头结点的游标。因为有一些结点已经被赋予了数据值,所以不该在备用链表中存在了,而下标为 0 的结点的游标存储着备用链表的开始结点的下标,所以重置为 n+1,就是下标为 n=1 的结点才是备用链表的开始结点了。

实现代码如下:

/**
 * 根据数组创建静态链表
 * @param list 静态链表
 * @param nums 数组
 * @param n 数组长度
 * @return 如果创建成功则返回 1,否则返回 0
 */
int createList(SLNode *list, int nums[], int n) 
    // 参数校验
    if (n < 1 || n > MAXSIZE - 2) 
        return 0;
    

    // 注意,下标为 0 的元素要存储备用链表的头结点,它的数据域不存放如何东西
    for (int i = 0; i < n; i++) 
        // 这里之所以是list[i+1],是因为list[0]要存储备用链表的头结点,所以要从数组的第二个元素开始
        list[i + 1].data = nums[i];
    
    // 数据链表最后一个元素的游标cur指向备用链表的头结点(即数组下标为0的元素)
    list[n].cur = 0;
    // 数组最后一个元素存储的是数据链表的头结点(即第一个有数据元素位置的)
    list[MAXSIZE - 1].cur = 1;// 即数组下标为1的元素
    // 数组下标为0的元素(即备用链表的头结点)的游标cur存储着备用链表第一个可用元素的位置,即数组长度加1后面的节点是空的
    list[0].cur = n + 1;

    return 1;

insert

在静态链表的第 i 个位置插入新结点。

实现步骤:

  • 参数校验,i 必须在 [1, length] 范围内。否则参数不合法,返回 0 表示插入失败。
  • 调用 mallocNode 方法从静态链表中的备用链表中申请一个空结点,返回新结点在链表中的下标。如果下标不为 0,则赋予新值。
  • 声明变量 indexpreIndex 分别用来记录静态链表中第 i 个和第 i-1 个结点的下标。从头到尾扫描静态链表中的数据链表找到第 i 和第 i-1 个结点,并且记录它们的下标。
  • 将新结点与第 i 个结点相连接。

实现代码如下:

/**
 * 在静态链表中的第 i 个节点处插入新节点
 * @param list 静态链表
 * @param i 序号,从 1 开始
 * @param ele 新节点的数据域值
 * @return 如果插入成功则返回 1,否则返回 0
 */
int insert(SLNode *list, int i, int ele) 
    // 参数校验
    if (i < 1 || i > size(list)) 
        return 0;
    
    // 去从静态链表中获得一个空节点
    int newIndex = mallocNode(list);
    // 如果该下标有效则插入
    if (newIndex != 0) 
        // 将新节点添加到链表中并为数据域赋值
        list[newIndex].data = ele;

        // 第一个数据节点的下标
        int index = list[MAXSIZE - 1].cur;
        // 计数器,记录数据节点个数
        int count = 0;
        // 记录第 i 个节点的前驱节点的下标
        int preIndex = MAXSIZE - 1;// 初始值为数据链表的头结点在数组中的下标
        // 找到第 i 个节点的前驱节点(即第 i-1 个节点)
        while (index != 0) 
            if (count == i - 1) 
                break;
            
            count++;
            preIndex = index;
            // 继续下一个节点
            index = list[index].cur;
        

        // 插入第 i 个节点
        list[newIndex].cur = list[preIndex].cur;// 将新节点的游标指向原第 i 个节点的前驱节点的游标
        list[preIndex].cur = newIndex;// 将第 i 个节点的前驱节点的游标指向新节点
        return 1;
    

    return 0;

append

在静态链表的尾部插入一个新结点。

实现步骤:

  • 调用 mallocNode 方法从静态链表中申请一个空结点,返回的就是新结点在静态链表中的下标。为该下标所指结点的数据域赋予值。
  • 声明变量 lastNodeIndex 来保存静态链表中数据链表最后一个结点的下标。从头到尾扫描静态链表中的数据链表,找到最后一个结点并记录它在静态链表中的下标。
  • 连接静态链表最后一个结点与新结点。

实现代码如下:

/**
 * 向静态链表的尾部追加元素
 * @param list 静态链表
 * @param ele 待追加的元素
 */
void append(SLNode *list, int ele) 
    // 为新节点分配空间,即在数组中找到一个空节点出来
    int newIndex = mallocNode(list);
    // 将新节点填充到分配出来的空间中并为数据域赋值
    list[newIndex].data = ele;

    // 将新节点连接到链表中
    int lastNodeIndex = MAXSIZE - 1;// 保存数据链表最后一个节点的下标
    int index = list[MAXSIZE - 1].cur;// 数据链表第一个节点的下标
    while (index != 0) 
        lastNodeIndex = index;
        // 继续下一个数据节点
        index = list[index].cur;
    
    list[newIndex].cur = list[lastNodeIndex].cur;// 其实数据链表最后一个节点的游标cur一直都是0,都指向备用链表的头结点(即数组的第一个元素)
    list[lastNodeIndex].cur = newIndex;

removeByNum

删除静态链表中第 i 个结点。

实现步骤:

  • 参数校验,序号 i 的范围在 [1, length] 之内。
  • 声明变量 index 记录第 i 个结点的下标,初始为数据链表开始结点的下标;变量 preIndex 记录第 i-1 个结点的下标,即第 i 个结点的前驱结点的下标,初始为数据链表的头结点的下标;变量 count 记录已经扫描过的结点个数。
  • 从头到尾扫描静态链表中的数据链表,记录第 i 个结点和第 i-1 个结点的下标。
  • 删除第 i 个结点。即将第 i-1 个结点的游标指向第 i 个结点的游标。
  • ele 保存被删结点的数据值。
  • 调用 freeNode 结点释放结点空间。

实现代码如下:

/**
 * 除静态链表中序号为 i 的节点
 * @param list 静态链表
 * @param i 序号,从 1 开始
 * @param ele 保存被删除节点的数据域值
 * @return 被移除节点的数据域值
 */
int removeByNum(SLNode *list, int i, int *ele) 
    // 参数校验
    if (i < 1 || i > size(list)) 
        return 0;
    

    // 第一个数据节点的下标
    int index = list[MAXSIZE - 1].cur;
    // 计数器,记录数据节点个数
    int count = 0;
    // 记录第 i 个节点的前驱节点的下标
    int preIndex = MAXSIZE - 1;// 初始值为数据链表的头结点在数组中的下标
    // 找到第 i 个节点的前驱节点(即第 i-1 个节点)
    while (index != 0) 
        if (count == i - 1) 
            break;
        
        count++;
        preIndex = index;
        // 继续下一个节点
        index = list[index].cur;
    

    list[preIndex].cur = list[index].cur;// 删除节点
    *ele = list[index].data;// 保存待删除节点的数据域值
    freeNode(list, index);// 释放节点空间,必须释放,否则即使删除节点空间仍然不会被利用起来
    return 1;

findByNum

寻找静态链表中第 i 个节点。

实现步骤:

  • 参数校验,序号 i 必须在 [1, length] 范围内。
  • 声明一个变量 count 用来记录已经遍历到第几个结点了。
  • 从头到尾扫描静态链表中的数据链表,每扫描一个,计数器 count 加一,然后将其与 i 进行比较,如果相等则返回该结点,否则继续链表的下一个结点。
  • 直至循环完成。

实现代码如下:

/**
 * 根据序号查找第 i 个节点
 * @param list 静态链表
 * @param i 序号,从 1 开始
 * @return 如果找到则返回该节点
 */
SLNode findByNum(SLNode *list, int i) 
    SLNode node;
    // 参数校验
    if (i < 1 || i > size(list)) 
        return node;
    

    // 数据链表的第一个节点
    int index = list[MAXSIZE - 1].cur;
    // 计数器,记录当前是第几个数据节点
    int count = 0;
    // 循环遍历数据链表
    while (index != 0) 
        // 计数器加1,表示已经遍历一个节点
        count++;
        // 判断第 i 个节点
        if (count == i) 
            node = list[index];
        
        // 继续下一个节点
        index = list[index].cur;
    
    return node;

findByEle

寻找静态链表中第一个值等于 ele 的结点。以 list=[11, 22, 33, 44, 55]; ele=33 为例如图所示:

实现步骤:

  • 找到数据链表的头结点(即静态链表下标为 MAXSIZE-1 的结点),获取它的游标 cur。它的游标就是数据链表中第一个结点的下标。
  • 以游标不为 0 作为循环结束的条件,遍历数据链表,比较结点的数据域与指定值是否相等,如果相等,则返回该结点,否则继续判断数据链表的下一个结点。

实现代码如下:

/**
 * 根据指定值查找节点
 * @param list 静态链表
 * @param ele 指定值
 * @return 如果找到则返回该节点
 */
SLNode findByEle(SLNode *list, int ele) 
    SLNode node;
    // 数据链表的第一个节点
    int index = list[MAXSIZE - 1].cur;
    // 循环遍历数据链表
    while (index != 0) 
        // 当前节点的数据域值是否与输出的参数相等,如果相等则表示找到然后返回该节点
        if (list[index].data == ele) 
            node = list[index];
        
        // 继续下一个节点
        index = list[index].cur;
    
    return node;

mallocNode

从静态链表中的备用链表中摘取一个结点。

实现步骤:

  • 找到备用链表的头结点(即静态链表中下标为 0 的结点)的游标 i,该游标就指向备用链表的开始结点在静态链表中的下标。
  • 如果该下标不为 0,则将备用链表的头结点的游标指向该下标所表示结点的游标,即相当于在备用链表中删除了该结点。
  • 如果该下标为 0,表示静态链表中的备用链表为空表了,不能再分配空间了,也就不能再往静态链表插入新结点了。

实现代码如下:

/**
 * 获取到空闲节点的下标
 * @param list 静态链表
 * @return 可用节点的下标
 */
int mallocNode(SLNode *list) 
    // 获取备用链表头结点的游标cur,该游标指向了备用链表中第一个可用节点的下标
    int i = list[0].cur;
    // 因为这个节点被使用后,就不应该留在备用链表中,所以要将该节点的后继节点作为新的备用链表中的第一个可用节点,所以将数组中第一个元素的游标cur指向备用链表中下一个可用节点的游标
    if (i != 0) 
        list[0].cur = list[i].cur;
    
    // 返回这个可用节点的下标
    return i;

freeNode

释放静态链表中下标为 i 的结点。要删除掉一个结点之后才需要释放结点,否则没必要。

实现代码如下:

/**
 * 释放下标为 i 的节点
 * @param list 静态链表
 * @param i 下标,从 0 开始
 */
void freeNode(SLNode *list, int i) 
    // 将被释放的节点插入到备用链表中
    list[i].cur = list[0].cur;
    list[i].data = 0;// 将数据域恢复为 0
    list[0].cur = i;

size

计算静态链表的长度,即数据链表的结点个数。

实现步骤:

  • 声明一个变量 count 来记录静态链表中的结点个数。
  • 找到数据链表的头结点(即静态链表下标为 MAXSIZE-1 的结点),获取它的游标 cur。它的游标就是数据链表中第一个结点的下标。
  • 以游标不为 0 作为循环结束的条件,遍历数据链表,计数器 count 加一。
  • 结束循环,返回 count 即为链表长度。

实现代码如下:

/**
 *  获取静态链表的长度
 * @param list 静态链表
 * @return 静态链表的长度
 */
int size(SLNode *list) 
    // 计数器,记录有效数据节点个数
    int count = 0;
    // 第一个数据节点的下标
    int i = list[MAXSIZE - 1].cur;
    // 遍历数据链表
    while (i != 0) 
        // 计数器加1
        count++;
        // 继续下一个节点
        i = list[i].cur;
    
    // 返回长度
    return count;

isEmpty

判断静态链表是否为空,即数据链表的结点个数是否为零。

实现步骤:

  • 由于静态链表规定如果最后一个结点的游标为零,则它是一个空表。

实现代码如下:

/**
 * 判断静态链表是否为空
 * @param list 静态链表
 * @return 如果为空则返回 1,否则返回 0
 */
int isEmpty(SLNode *list) 
    // 即判断最后一个节点的游标cur是否等于0,如果等于0则表示这是一个空静态链表
    return list[MAXSIZE - 1].cur == 0;

print

打印静态链表中数据链表的所有结点值。

实现步骤:

  • 找到数据链表的头结点(即静态链表下标为 MAXSIZE-1 的结点),获取它的游标 cur。它的游标就是数据链表中第一个结点的下标。
  • 以游标不为 0 作为循环结束的条件,遍历数据链表,打印链表节点的值。
  • 每个结点之间用逗号加空格进行分隔。然后以当前结点的游标作为下一个结点的下标进行遍历。

实现代码如下:

/**
 * 打印静态链表所有数据节点
 * @param list 静态链表
 */
void print(SLNode *list) 
    int i = list[MAXSIZE - 1].cur;
    // 循环静态链表
    printf("[");
    while (i != 0) 
        printf("%d", list[i].data);
        if (list[list[i].cur].cur != 0) 
            printf(", ");
        
        i = list[i].cur;
    
    printf("]\\n");

以上是关于线性表文档之静态链表的主要内容,如果未能解决你的问题,请参考以下文章

线性表之双向链表

数据结构第四篇——线性表的链式存储之双向链表

数据结构之线性表

《数据结构》复习之线性表(顺序表和链表)

数据结构与算法-线性表之双向链表

线性表——顺序存储结构之静态链表