数据结构:静态链表

什么叫静态链表?

用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。

如何用数组了描述呢 ?
简单的说,就是我们会先创建一个固定的数组,然后数组中的每一个元素都使用一个结构体,该结构体包括两个元素,一个是要储存的数据,一个是游标。

什么是游标?
游标是用来记录链表的下一个结点的位置(节点所在数组中的下标)。

这都是一些比较抽象的概念,不太好理解,也很容易搞乱,下面我结合这些图简单的说一些。

我的理解

数据结构:静态链表

大家可以先建立几个概念,数组和链表可不要搞混了。
首先这个数组,其实就是我们用来储存用的。那链表呢, 我个人的理解,指得是数组已经使用的这一部分(剩余没使用的那部分暂且叫做备用链表)。我们使用这个数组下标为0的这个位置来当做链表的头结点,所以说这个位置不进行实际的储存。

我们在进行存储的时候,是按照数组下标的位置把数据逐一存储进数组的(数组的第0个位置不做实际的存储功能,下面会说到),也就是说我们要把‘甲’,‘乙’,‘丙’,‘丁’,‘戊’,‘己’,‘庚’ 这七个字储存起来,使用的是数组的下标1到6的连续位置,但是在数组中的连续位置,不代表他在链表中的位置,打个比方说,我们要继续存储一个‘辛’这个字,但是我要把它存到甲和乙中间,他的数组的位置就是在下标为7的位置,但是他的链表位置是在甲和乙中间,这是通过游标来体现出来的,下面会说到

好的,我们分析一下
在初始化完这个数组之后,所有的位置上都没有储存数据,然后每个位置的游标表示数组的下一个元素,也就是第0个元素的游标为1,第1个元素的游标为2....
进行了存储之后,如上图,我们看几个比较关键的位置,第0个元素的游标变味了7,第6个元素的游标为0,第999个元素的游标为1,这代表什么意思呢?
我们在进行插入数据操作的时候,第0个元素的游标总是指向下次储存要用的那个位置,数组的最后一个元素也就是999这个位置的元素,他的游标指向链表的第一个节点(不包括头结点的)在数组中的下标。然后链表的最后一个节点的游标要指向0,因为没有下一个了嘛,所以我们看到第6个元素的位置的游标是0。

代码实现

理论差不多说完了,下面该说具体的实现了,网上的实现代码很多,直接找了一份代码,咱们一起看一下代码吧

int main(int argc, const char * argv[]) {  
    StaticLinkList list[MAXSIZE];
    // 初始化数组中的数据
    createLinkList(list);
    
    for (int i = 0; i < 2; i ++) {
        ListInsert(list, 1, 100+i);
    }
    return 0;
}

我们在循环中执行了两次的插入操作,ListInsert方法中的参数1指的是每次都是把数据插在链表的第1个结点之前,我们先推测一下结果。

我们先存了一个100这个数字,又存了101这个数字,因为之前初始化之后并没有进行别的存储,所以完成存储之后,因为按照之前的说法,数组的第1个元素应该放的100,第2个位置才是101,但是我们在储存的时候,是把要插入的数据插入到链表第一个元素的前面,所以从链表的角度看,101应该是第一个节点,100是第二个。那要怎么表现出来呢?结合我们上面说的,999这个位置的游标应该指向链表第一个节点,也就是游标应该为2,数组下标2的元素因为他是链表的第一个节点,所以他的游标应该指向链表第二个节点,也就是数组下标1。链表就两个节点,所以最后一个节点的游标要指向0. 然后数组下标0的位置的游标指向了下一个可用的存储位置3。

数据结构:静态链表
数据结构:静态链表

结果跟我们之前分析的是一致的。

只要理清楚了这些思路之后,其他的代码看起来也就容易很多了。

比如说下面这个计算链表长度的方法

int linkLength(StaticLinkList list[]) {
    int j = 0;
    int i = list[MAXSIZE-1].cur;
    while (i) {
        i = list[i].cur;
        j ++;
    }
    return j;
}

先拿到数组的最后一个元素游标的数值,如果是0,就说明还没有储存呢,索引链表长度就是0,如果已经储存过了,所以得到i的值就不是0,通过一个while循环,一个个的遍历链表结点,直到链表最后一个结点的游标又是0,跳出循环,然后通过一个j来标识循环次数,也就是链表长度。

关于静态链表就说这么多吧,更多的内容大家可以上网搜一下。

下面我把使用的代码放上,主要是实现了插入操作,删除暂时没做,大家可以看懂了插入然后自己写一下删除,代码上我尽可能详细的做了注释,大家可以看看代码 ~

//  main.c
//  链表
//  Created by sunxiaobin on 2018/6/15.

#include <stdio.h>
#define MAXSIZE 1000
typedef int ElemType;
typedef struct {
    ElemType data;
    int cur;
}StaticLinkList;

// 声明
void createLinkList(StaticLinkList list[]);
int linkLength(StaticLinkList list[]);
int Malloc_SLL(StaticLinkList list[]);
int ListInsert(StaticLinkList L[], int i, ElemType e);

int main(int argc, const char * argv[]) {
    StaticLinkList list[MAXSIZE];
    
    // 初始化数组中的数据
    createLinkList(list);
    
    for (int i = 0; i < 2; i ++) {
        ListInsert(list, 1, 100+i);
    } 
    return 0;
}

void createLinkList(StaticLinkList list[]){
    int i;// i就代表数组下标
    for (i = 0; i < MAXSIZE-1; i ++) {
        list[i].cur = i + 1;  // cur存放的是下一个结点的下标
    }
    list[MAXSIZE-1].cur = 0; //
    
}

// 求链表长度
int linkLength(StaticLinkList list[]) {
    int j = 0;
    int i = list[MAXSIZE-1].cur;
    while (i) {
        i = list[i].cur;
        j ++;
    }   
    return j;
}

// 得到备用链表的第一个元素下标
int Malloc_SLL(StaticLinkList list[]) {
    
    int i = list[0].cur;
    
    if (list[0].cur)
        // 更新头结点的游标, 原来头结点的游标指向i这个位置,
        // 但是这个位置要进行了存储 , 被使用了,
        // 头结点应该把游标更新到新的备用链表的第一个元素, 也就是刚刚使用了这个i位置的元素的游标指向的位置.
        list[0].cur = list[i].cur;
    
    return i;
}

// 在静态链表L中第i个元素之前插入新的数据元素e
int ListInsert(StaticLinkList list[], int i, ElemType e) {
    
    int k = MAXSIZE - 1; // 数组的最后一个元素
    
    //判断要插入的位置是不是合法范围
    if (i<1 || i>linkLength(list)+1) return 0;
    
    int j = Malloc_SLL(list); // 得到备用链表的第一个元素下标
    
    if (j) {  // 如果元素存在,(不存在就说明数组已经满了)
        
        list[j].data = e;// 先把e存进来,然后通过改变某几个结点的游标
        
        // 这个for循环内, 如果是第一次存储的话根本不走
        //
        for (int l = 1; l<=i-1; l++) {
            // 得到第i-1个结点在数组中的下标
            k = list[k].cur;
        }
        
        list[j].cur = list[k].cur; // 将第i-1个元素的cur设置为新加的这个结点的下标,将新加的这个结点的下标设置为之前第i-1个元素存储的cur值
        list[k].cur = j;
    
        return 1;
    } 
    return 0;    
}