【数据结构与算法】学习线性表中的静态链表(Static Linked List)以及其Java实现

学习线性表中的静态链表(static linked list)以及其Java实现


  • 静态链表的概念
    • 什么是链表
    • 什么是静态链表
    • 动态链表和静态链表
    • 静态链表中结点的构成
    • 静态链表的头指针,头结点,元首结点和尾结点
    • 静态链表的代码构成
    • 静态链表的备用链表(核心)
    • 静态链表的优势
  • 静态链表的Java实现
  • 其他相关内容

静态链表的概念


什么是链表?

什么是链表?一个采取了链式存储的线性表就是一个链表


什么是静态链表?

逻辑结构上相邻的数据元素,存储在指定的一块内存空间中,数据元素只允许在这块内存空间中随机存放,这样的存储结构生成的链表称为静态链表。
(简而言之,就是一个大体上属于链式分散存储,但又被限制在某块内存区域进行分散存储的链表)


动态链表和静态链表

链表在某个角度可以以下分为两种:

  • 动态链表
    (动态链表就是通常意义上的链表,采用链式存储,在逻辑结构上相邻,在整个物理内存空间中分散存储的线性表)
  • 静态链表
    (静态链表就是在动态链表的基础上,逻辑结构相邻,但仅仅是在有限制的在某块内存空间中分散存储)

静态链表和动态链表的区别
静态链表限制了数据元素存放的位置范围;动态链表是整个内存空间。


这里写图片描述


静态链表的结点的构成

静态链表的结点,由两部分构成:

  • 数据域
  • 游标

数据域跟动态链表的数据域一样,依然是存放链表的地方。但游标则是用来代替指针域的地方,用于存放下一个结点在数组中的索引位置。


这里写图片描述


静态链表的头指针,头结点,元首结点和尾结点

头指针:
头指针就是必定指向第一个结点的指针
头结点
静态链表中,我们把数组某个索引位置作为头结点。头结点的游标存储首元结点在数组的索引位置
元首结点
在静态链表中没什么区别,即使第一个数据域有内容的结点。是链表含有内容的正式起始位置
尾结点
在动态单链表中,尾结点的指针域指向Null,在静态单链表中,尾结点的游标存放头结点在数组中的索引位置


静态链表的代码构成

说是这么说,静态链表示被限制在某块内存区域内的动态链表,那么我们要怎么实现静态链表,怎么去构建一个静态链表呢?

  • 首先要达成被限制在某块内存区域,所以静态链表使用(顺序表)数组这一数据类型来申请一个内存空间,既该链表的结点只能在申请的数组内存空间中分配。
  • 然后要在限制区域内采取链式存储,所以各数据元素在数组申请的内存空间内随机存放,为了体现逻辑上的相邻,为每一个结点配合一个游标,用于记录下一元素在数组中的位置。

所以总的来说,在数组申请的存储空间中,各结点随机存储在数组空建中,但每一个元素都记录着下一元素在数组中的位置,通过前一个元素,可以找到下一个元素,构成了一条链表,这条被局限在特定内存空间的链表就是静态链表。


静态链表的备用链表

由于静态链表提前申请了有限的内存空间,在使用的过程中,极有可能会出现申请的内存空间不足,需要使用之前被遗弃的内存空间。所以这里在静态链表里就出现了个备用链表的概念。

备用链表是什么?(要想构建静态链表,你就必须了解它的备用链表。)
备用链表就是在某个指定内存内,如数组内。指定内存内的所有的空闲内存链接成的一个链表,是空闲的链表。而含数据的链表就是我们的静态链表。当我们的静态连续需要空闲位置来插入带数据的新结点时,就会向备用结点请求分配一个空闲的结点来使用。通俗的说,备用链表就是用来帮助静态链表来管理限定区域的空闲内存的帮手。如果没有备用链表。当限定的内存区域充满了无用的废弃结点(被静态链表删除的结点)时,静态链表就无能为力了,明明还有空间,但是却无法使用。

备用链表在哪?
备用链表和含数据的静态链表共有一片内存区域,如同一个数组内。备用链表是全部空闲的内存区域集合,静态链表是所有有用的内存区域集合,当静态链表向备用链表申请不到空闲内存区域时,则代表该内存空间已被耗尽。


这里写图片描述

从上图我们可以看出一些信息:

  • 这个限定内存区域是一个大小为10的数组
  • 目前这个静态链表是一个空表,长度为0,没有任何数据,只有一个头结点(尾结点和头结点重复了,位于数组索引1的位置)。
  • 其备用链表的头结点的游标记录的是下一个可提供的空闲区域索引位置。也即是说如果此时静态链表要插入一个新结点,那必然是在数组索引2的位置

静态链表的优势

静态链表综合了顺序表和动态链表的优点:使用数组存储数据元素,便于做查找遍历操作;同时,在数组中借鉴了动态链表的特点,在链表中插入或者删除结点时只需更改相关结点的游标,不需要移动大量元素。

静态链表的Java实现


以下是我实现的静态链表,本着怎么去实现的角度去写的,所以可能存在性能不咋地的问题。

package com.snailmann.datastructure.list.staticlist;



class Node { // 静态链表的结点

    Object data; // 数据域
    int cur; // 游标

    public Node(Object data, int cur) {

        this.data = data;
        this.cur = cur;
    }

    public Node() {
        this(null, 0);
    }
}

public class StaticLinkedList<T> {

    Node[] element; // 限制的内存区域,既存放数据的数组区域
    private int current; // 记录备用链表的下一个可用位置的下标
    private int head; // 静态链表头结点的下标
    private int tail; // 静态链表的尾结点的下标
    private int length; // 当前静态链表长度(有数据的结点的个数)
    int size; // 限制区域的最大空间,既静态链表的最大内存空间

    /**
     * 静态链表初始化
     *
     * @param size
     * @throws Exception
     */
    public StaticLinkedList(int size) throws Exception {

        this.element = new Node[size]; // 分配内存空间
        this.createSpareList(size);    // 备用链表初始化
        this.head = this.assignNode(); // 从备用链表中分配空闲位置给静态链表头结点,既数组的索引位置1
        this.element[this.head].cur = this.head;  //静态链表的头结点的游标更新为自己的下标。因为是空表,头结点和尾结点重合,尾结点的游标存放的是头结点的下标
        this.tail = this.head;         // 此时静态链表长度为0,头结点和尾结点重合
        this.length = 0;               // 静态链表长度为0
        this.size = size;              // 静态链表最大可用空间为size

    }

    /**
     * 创建备用链表并初始化
     *
     * @param size
     */
    public void createSpareList(int size) {
        // 备用链表的头结点时数组的第一个位置,索引位置为0
        for (int i = 0; i < size; i++) {   // 将数组的所有索引位置链接在一起
            this.element[i] = new Node();
            this.element[i].cur = i + 1;   // 每个节点的游标都存储相邻后一个位置的索引
        }
        this.element[size - 1].cur = 0;    // 备用链表的游标存储为备用链表头结点的位置
    }



    /**
     * 分配备用链表上的可用位置
     *
     * @return
     * @throws Exception
     */
    public int assignNode() throws Exception {
        // 分配备用链表的下一个可用位置给静态链表

        int i = this.element[0].cur;
        this.element[0].cur = this.element[i].cur; // 备用链表头结点存放下一个可用位置
        this.current = this.element[0].cur;        // 获得当前空闲位置
        if(this.element[0].cur == 0)
            throw new Exception("内存空间不足,length = "+this.length+" size = "+this.size ); //头结点也占空间,但不计算在length

        return i;                                  // 返回备用链表的可用位置
    }


    /**
     * 释放无用静态链表结点,回收到备用链表中
     *
     * @param k
     *            数组索引位置
     */
    void freeNode(int k) {
        // 把下标为i的结点,既要删除的结点回收到备用链表
        this.element[k].cur = this.element[0].cur; // 删除结点的游标更新为备用链表头结点的游标(下一个可用空间)
        this.element[0].cur = k;  // 备用链表头结点的游标更新为要删除结点的下标i,既要删除的结点的位置作为下一个可用空间
    }


    /**
     * 静态链表是否空表
     *
     * @return
     */
    public boolean isEmpty() {
        return this.element[this.head].cur == this.head; //判断静态链表头尾结点是否重合就知道是否是空表了
    }


    /**
     * 当前静态链表的长度
     *
     * @return
     */
    public int length() {
        return this.length;
    }


    /**
     * 根据静态链表的相对索引位置取得数据域
     *
     * @param i 从元首结点开始算起,起始索引为0 (非数组的索引位置)
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public T get(int i) throws Exception {

        checkException(i);
        int count = 0;                               // 相对链表的位置0,对应的是首元结点
        int cur = this.element[this.head].cur;       // 既首元结点的下标,相对位置0
        while (this.element[cur].cur != this.head) { // 只要该结点的游标是存储着静态链表头结点的下标,那么静态链表已经遍历结束了
            // 也可以用for(int i = ; i < this.length(); i++)来遍历
            if (count == i)                          // 如果链表相对位置等于要取的位置,则打断循环
                break;
            count++;                                 // 链表相对位置
            cur = this.element[cur].cur;

        }
        return (T) this.element[cur].data;

    }


    /**
     * 查看静态链表是否含有该数据域
     *
     * @param
     * @throws Exception
     */
    public boolean contains(T t) throws Exception {
        checkException(t);
        int cur = this.element[this.head].cur; // 既首元结点的下标
        while (this.element[cur].cur != this.head) { // 只要该结点的游标是存储着静态链表头结点的下标,那么静态链表已经遍历结束了
            if (t.equals(this.element[cur].data)) {
                return true;
            }
            cur = this.element[cur].cur; // 遍历链表,获得下一个结点的下标
        }
        return false;

    }


    /**
     * 根据静态链表的相对索引位置进行更新
     *
     * @param i
     * @param t
     * @throws Exception
     */
    public void set(int i, T t) throws Exception {
        checkException(i, t);
        int count = 0; // 首元结点的在静态链表的相对位置
        int cur = this.element[this.head].cur;
        while (this.element[cur].cur != this.head) {
            if (count == i)
                break;
            count++;
            cur = this.element[cur].cur;

        }

        this.element[cur].data = (Object) t;

    }


    /**
     * 在静态链表尾顺序插入
     *
     * @param t
     * @throws Exception
     */
    public void insert(T t) throws Exception {

        int lastCur = this.tail; // 获得尾结点的下标,因为是链尾顺序插入,插入点上一个位置必定是尾结点
        this.current = assignNode(); // 从备用链表中分配获得可用空闲位置
        this.element[current] = new Node((Object) t, this.head); // 在空闲位置插入新结点,新结点的游标存入静态链表的头结点位置,既作为尾结点
        this.tail = this.current; // 更新静态链表尾结点在数组中位置,既更新尾结点的下标
        this.element[lastCur].cur = this.current; // 新插入结点的上一个结点的游标存入新插入结点的位置,链接起来
        this.length++;
    }


    /** 
     * 在静态链表的相对索引位置插入
     *
     * @param i
     *            静态链表相对索引位置,从元首结点开始算起,起始索引为0 (非数组的索引位置)
     * @param t
     * @throws Exception
     */
    public void insert(int i, T t) throws Exception {

        checkException(i,t);
        int count = 0; // 链表的相对位置,从首元结点为0开始算起
        int cur = this.element[this.head].cur; // 初始化赋予首元结点在数组的索引位置

        if (i == 0) { // 是否从静态链表首元结点位置插入

            this.current = assignNode();// 从备份链表获得空闲位置
            this.element[current] = new Node((Object) t, this.element[this.head].cur);
            this.element[this.head].cur = this.current; // 静态链表头结点的游标改为新插入结点在数组的索引位置
            this.length++;

        } else if (i == this.length - 1) { // 是否从静态链表尾结点插入

            insert(t);

        } else { // 除首元结点和尾结点位置插入的情况

            // j是要遍历的次数,等于静态链表的长度-1次
            for (int j = 0; j < this.length - 1; j++) {
                if (count == i - 1)          // 要得到上一个结点
                    break;
                count++;                     // 链表的相对位置
                cur = this.element[cur].cur; // 上一个结点在数组中的下标
            }
            this.current = assignNode();
            // 从备用链表中分配来的空闲位置插入新结点,新结点的游标为其直接后继结点
            this.element[current] = new Node((Object) t, this.element[cur].cur);
            this.length++;
            this.element[cur].cur = current;
        }

    }


    /**
     * 顺序删除静态链表的最尾部结点
     *
     */
    public void remove() {

        int cur = this.element[this.head].cur; // 首元结点的下标

        while (this.element[cur].cur != this.tail) { // 循环整个静态链表,找到尾结点的前一个结点的下标
            cur = this.element[cur].cur; // 循环链表
        }
        this.element[cur].cur = this.head; // 要删除结点的前驱结点的游标更新为静态链表头结点的下标,既作为新的尾结点
        freeNode(this.tail); // 释放所删除的结点,回收金备用链表
        this.tail = cur; // 更新尾结点的下标
        this.length--; // 静态链表长度减1

    }


    /**
     * 根据静态链表相对索引位置删除结点
     *
     * @param i
     *            静态链表相对索引位置,从元首结点开始算起,起始索引为0 (非数组的索引位置)
     * @param t
     * @throws Exception
     */
    public void remove(int i) throws Exception {
        checkException(i);
        if (i == 0) { // 删除首元结点

            int cur = this.element[this.head].cur; // 获得要删除的首元结点的下标
            this.element[this.head].cur = this.element[this.element[this.head].cur].cur; // 静态链表头结点的游标更新为原首元结点的游标,既原首元结点的后继结点成为新的首元结点
            freeNode(cur); // 将要删除的结点坐标传入释放函数中,让备用链表回收要删除的结点
            this.length--;

        } else if (i == this.length - 1) { // 删除尾结点

            remove();

        } else { // 其余情况
            int count = 0;
            int cur = this.element[this.head].cur; // 获得首元结点的下标
            int lastCur = 0;
            int deleteCur = 0;
            while (this.element[cur].cur != this.head) { // 遍历链表,获得要删除结点的前驱结点的下标,和删除结点的后继结点的下标
                if (count == i - 1) // 此时的cur就是前驱结点的下标
                    lastCur = cur;
                if (count == i) // 此时的cur是要删除的结点的下标
                    deleteCur = cur;
                if (count == i + 1) // 此时的cur就是后继结点的下标
                    break;
                count++;
                cur = this.element[cur].cur;
            }

            this.element[lastCur].cur = cur; // 删除结点的前驱结点的游标更新为删除结点的后继结点的下标,将删除结点从中移除
            freeNode(deleteCur);
            this.length--;

        }
    }


    /**
     * 异常检查
     * @param i 结点位置
     * @param t 数据域
     * @throws Exception
     */
    public void checkException(int i, T t) throws Exception {
        if (isEmpty()) { // 链表没有数据
            throw new Exception("这是一个空表");
        }
        if (i < 0 || i >= length() || i > Integer.MAX_VALUE) {
            throw new Exception("结点位置越界越界"); // 传入的结点位置i不允许越界
        }
        if (t == null) {
            throw new NullPointerException("元素不能为null"); // 传入元素不能为null
        }
    }


    /**
     * 异常检查
     * @param t 数据域
     * @throws Exception
     */
    public void checkException(T t) throws Exception {
        if (isEmpty()) {
            throw new Exception("这是一个空表");
        }
        if (t == null) {
            throw new NullPointerException("元素不能为null");
        }
    }


    /**
     * 异常检查
     * @param i 结点位置
     * @throws Exception
     */
    public void checkException(int i) throws Exception {
        if (isEmpty()) {
            throw new Exception("这是一个空表");
        }
        if (i < 0 || i >= length() || i > Integer.MAX_VALUE) {
            throw new Exception("结点位置越界越界");
        }
    }


    /**
     * test
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        StaticLinkedList<String> list = new StaticLinkedList<>(10);
        list.insert("a");
        list.insert("c");
        list.insert(0, "b");
        list.insert(list.length - 1, "d");
        list.remove(3);
        list.set(2, "m");
        for (int i = 0; i < list.size; i++) {

            System.out.println(list.element[i].data + " " + list.element[i].cur);
        }
        System.out.println("静态链表长度 :" + list.length + "  可分配空间为 " + list.size);

        System.out.println(list.get(2));

    }
}

本代码实现采用的是两个头结点,两个尾结点的方式,既备用链表有自己的头尾结点,静态链表也有自己的头尾结点。
静态链表的初始化流程是:

  • 创建一个size大小的数组,作为内存空间
  • 初始化备用链表,数组0位置作为备用链表头结点,将数组内所有可用空间串联起来,游标都初始化好,一个接着一个
  • 初始化要带数据的静态链表,首先请求备用链表的一个空闲区域作为头结点。静态链表初始化完成,头尾结点重合,游标存放自己的下标(因为尾结点存放的是头结点的下标)

这里写图片描述

插入操作流程:

  • 插入第一个数据时,静态链表向备用链表申请空闲位置,得到空闲位置2,所以在位置2插入数据。备用链表头结点游标更新为位置2的游标(既位置3)。静态链表头结点游标更新为位置2,新插入结点成为尾结点,所以其游标更新为头结点的位置1
  • 再插入新数据,再向备用链表申请空闲位置,得到空闲位置3,所以在位置3插入数据。备用链表头结点游标更新为位置3的游标(既位置4)。blablabla…

这里写图片描述

删除操作流程:

  • 删除一个静态链表的首元结点,既数组索引2的位置。所以静态链表头结点的游标从2更新为3。然后被删除的结点位置2被备用链表回收,成为备用链表的头结点游标,位置2的游标更新为原本的头结点的游标(既位置4)。
  • 此时再插入一个数据,静态链表向备用链表申请空闲位置,得到位置2,在位置插入数据。位置2的结点成为静态链表的尾结点,所以游标更新为头结点的位置1。备用链表头结点的游标也更新为位置4。

这里写图片描述

在代码实现方面,因为是链表,只要是链表,如何遍历该链表就是关键。因为这是链表的核心。有大致两种遍历的方式:
方法一:

    int cur = 首元结点的索引位置;
    while (this.element[cur].cur != this.head) { 
            cur = this.element[cur].cur; //通过判断游标是否已经头结点的位置来遍历
    }

方法二:

    int cur = 首元结点的索引位置;
    for (int j = 0; j < this.length - 1; j++) {
        cur = this.element[cur].cur; // 上一个结点在数组中的下标
    }

其他相关内容


【数据结构与算法】初入数据结构中的线性表(Linear table)及Java代码实现


参考资料

首先强烈推荐学习数据结构的朋友,直接去看该网站学习,作者是@严长生。里面的资料是基于严蔚敏的《数据结构(C语言版)》,本文的概念知识都是基于该网站和《数据结构(C语言版)》这个书来分析的。
静态链表及C语言实现 - 作者:@严长生

谈谈静态链表(JAVA实现)
静态链表 - 作者:@流云哭翠

猜你喜欢

转载自blog.csdn.net/snailmann/article/details/80523430