死磕算法第二弹——栈、队列、链表(5)

本文整理来源 《轻松学算法——互联网算法面试宝典》/赵烨 编著面试

链表其实也可使用数组模拟

在C或者C++语言中有“指针”的概念。由于这个概念,链表在编程语言中可以方便地得以发挥做用,但并非全部的编程语言中都有这个指针概念,好比Java。虽然没有“指针”这个概念,可是Java有“引用”的概念,相似于指针,能够用于完成链表的实现。算法

但如有的编程怨言没有指针怎么办呢?那么就能够用数组模拟。编程

静态链表

链表有两种:静态列表和动态列表。平时所用的链表就是动态链表,空间都是须要时动态生成的;而静态链表通常是使用数组来描述的链表,多数用于一些没有指针的高级编程语言实现链表。数组

静态列表的实现

通常来讲,静态链表的实现就是使用一段固定长度的数组,其中的每一个元素须要有两个部分组成:一个是data,用于记录数据,一个是cur,用于记录指向下一个节点的位置。在C语言中通常使用结构体,在Java中通常使用对象。若是不使用这种组合方式,那么也可使用两个数组:一个数组村data,一个数组村cur,让同一个元素的data和cur的坐标保持一致。编程语言

其中静态链表也能够模拟双向链表,只须要再增长一个部分,用于记录前面的一个节点的位置便可,可是这个静态链表的维护成本更高。性能

静态链表中的结构体:学习

public class Element<E>{
        private E data;
        private int cur;

        public E getData() {
            return data;
        }

        public void setData(E data) {
            this.data = data;
        }

        public int getCur() {
            return cur;
        }

        public void setCur(int cur) {
            this.cur = cur;
        }
    }

动态链表主要包含建立、插入、删除、遍历操做;静态链表是使用数组对动态链表进行模拟,固然也能够实现这种操做。测试

建立静态链表

首先要建立。对于动态链表来讲,建立操做并不复杂;可是对于静态链表来讲,建立操做会复杂一些。this

首先须要三个标记,为了方便,咱们直接采用三个变量来记录,分别是头指针标记、尾指针标记和未使用链表的头指针标记。指针

未使用链表的头指针标记的做用是什么?因为使用数组做为链表的存储空间,因此链表的元素确定会分布在数组的一些元素上去,可是对于链表进行插入操做时,会出现链表的顺序和数组的下表顺序不一致的状况,可能会致使数组中的一些连续空间为空,即未被使用,多是这里的元素以前被删除了。

因此咱们须要把链表中未使用的空间经过一个链表串起来,这样在须要分配空间时就能够把未使用链表的头指针指向的元素给咱们真正使用的链表了。

这个未使用的链表通常叫作备用链表,用于串联那些没有被使用的数组元素,为接下来的链表中的插入的操做使用,而在链表中删除元素时,须要及时把要删除的元素加入备用链表的头部记录下来。

因此在建立一个链表时须要把这个备用链表串一下。

public StaticLink(int capacity) {
        elements = new Element[capacity];
        unUsed = 0;
        for (int i = 0; i < capacity - 1; i++) {
            elements[i] = new Element<T>();
            elements[i].setCur(i + 1);
        }
        elements[capacity - 1] = new Element<T>();
        elements[capacity - 1].setCur(-1);
    }

建立静态链表时,所须要把数组的全部元素遍历一下,用备用链表穿起来。咱们在这里对除最后一个元素外的全部元素进行循环赋值,对最后一个元素须要赋不同的值,即把它的指针赋值为-1,用于说明没有下一个元素了。

插入操纵

静态链表的头插入须要进行以下操做。

首先从备用链表头中拿出一个元素,把备用链表的投标及指向备用链表的第二个元素的数组下标,而后把这个被拿出的元素的cur设为链表头标记的位置,即当前链表中的第1个元素的数组下标,接着把头元素的标记指向这个新数组元素下标。如此完成了对链表头插入。

静态链表的尾插入相似,首先从备用链表头拿出一个元素,把备用链表的头标记指向备用链表的第2个元素的数组下标,由于要做为链表的最后一个元素,所以把这个元素的cur设为空,接着把真是链表的为指针指向这个数组元素cur设为这个被拿出来的元素的下标,接着把为指针标记的值设为这个元素的数组下标,这样就完成了链表的尾插入。

链表的中间插入须要对静态链表进行遍历,在遍历到指定位置以后进行操做。备用链表一样从头袁旭做为链表的插入元素空间。而在链表遍历到要插入元素的位置的前一个元素以后,把这个元素的cur设为新拿出来的备用元素的下标。而这个新拿出来的备用元素的cur一样须要设置为前一个cur的值(也就是本该是新插入这个元素的下一个元素的数组下标),这样就完成了链表的中间插入。

其实静态链表的插入操做和动态链表的原理同样,只是改变了一些操做步骤:一个须要处理静态链表;一个是没有指针,因此须要修改cur值为指定元素的数组下标。

删除操做

静态链表的删除操做的原理相似于动态链表,须要改变先后元素的指针方向,同时把当前元素移出(在静态链表中,就是在备用链表中进行头插入)。

头删除时,须要把头指针的值(head)设为本来链表的第2个元素的数组下标,同时须要把这个被删除元素的cur设为备用链表的头元素(unUseHead)数组下标,而后修改备用链表头标记值为这个元素额数组下标。即删除静态链表时,除了须要把链表cur的关系设定好,还须要把这个被删除的元素归还到备用链表里,以备之后使用。

尾删除时,须要把为指针(tail)前移,单因为不知道前一个元素的下标是什么(除非使用双向链表),因此尾删除和中间删除同样,都须要进行遍历。在遍历到要删除的元素的前一个元素时,把这个元素的cur设为要删除的元素的后一个元素的数组下标(若是没有,则设置为空,这时删除的这个元素确定是链表最后的一个元素)。若是要删除的元素时最后一个元素,那么须要修改微元素的标记(tail)的值。

遍历操做

插入和删除操做有时候须要遍历到链表的制定位置。遍历操做时不须要理会备用链表,只须要从头标记(head)的值开始,找到元素数组的下标,再根据每一个元素的cur去找下一个元素的数组坐标,知道cur为空为止,则说明遍历完成。当咱们须要遍历指定的位置时,须要一个计数器来记录咱们遍历了多少个元素。

静态链表不论是插入仍是删除,其操做步骤都与动态链表相似,惟一须要额外处理的就是对备用链表的操做。进行插入操做时须要对备用链表进行头删除;而进行删除操做时,则须要对备用链表进行航插入,这是须要额外维护的工做。

public class StaticLink<T> {

    private Element[] elements;

    private int unUsed;

    private int head;

    private int tail;

    private int size;

    public StaticLink(int capacity) {
        elements = new Element[capacity];
        unUsed = 0;
        for (int i = 0; i < capacity - 1; i++) {
            elements[i] = new Element<>();
            elements[i].setCur(i + 1);
        }
        elements[capacity - 1] = new Element<>();
        elements[capacity - 1].setCur(-1);
    }

    public void insert(T data, int index) {
        if (index == 0) {
            insertFirst(data);
        } else if (index == size) {
            insertLast(data);
        } else {
            checkFull();
            //获取要插入的元素的前一个元素
            Element<T> preElement = get(index);
            //获取一个未被使用的元素做为要插入的元素
            Element<T> unUsedElement = elements[unUsed];
            //记录要插入元素的数组下标
            int temp = unUsed;
            //将要备用链表中拿出来的元素的数组下标设为备用链表头
            unUsed = unUsedElement.getCur();
            //将要插入元素的指针设为本来前一个元素的指向的下标值
            unUsedElement.setCur(preElement.getCur());
            //将前一个元素的指针指向插入的元素下标
            preElement.setCur(temp);
            //赋值
            unUsedElement.setData(data);
            //链表长度+1
            size++;
        }
    }

    public void printAll() {
        Element<T> element = elements[head];
        System.out.println(element.getData());
        for (int i = 1; i < size; i++) {
            element = elements[element.getCur()];
            System.out.println(element.getData());
        }
    }

    public int size() {
        return size;
    }

    public Element<T> get(int index) {
        checkEmpty();
        Element<T> element = elements[head];
        for (int i = 0; i < index; i++) {
            element = elements[element.getCur()];
        }
        return element;
    }

    public void checkFull() {
        if (size == elements.length) {
            throw new IndexOutOfBoundsException("数组不够长了");
        }
    }

    public void deleteFirst() {
        checkEmpty();
        Element deleteElement = elements[head];
        int temp = head;
        head = deleteElement.getCur();
        deleteElement.setCur(unUsed);
        unUsed = temp;
        size--;
    }

    public void deleteLast() {
        delete(size - 1);
    }

    public void delete(int index) {
        if (index == 0) {
            deleteFirst();
        } else {
            checkEmpty();
            Element pre = get(index - 1);
            int del = pre.getCur();
            Element deleteElement = elements[del];
            pre.setCur(deleteElement.getCur());
            if (index == size - 1) {
                tail = index - 1;
            }
            deleteElement.setCur(unUsed);
            unUsed = del;
            size--;
        }
    }

    public void checkEmpty() {
        if (size == 0) {
            throw new IndexOutOfBoundsException("链表为空");
        }
    }

    public void insertLast(T data) {
        checkFull();
        Element<T> unUsedElement = elements[unUsed];
        int temp = unUsed;
        unUsed = unUsedElement.getCur();
        elements[tail].setCur(temp);
        unUsedElement.setData(data);
        tail = temp;
        size++;
    }

    public void insertFirst(T data) {
        checkFull();
        Element<T> unUsedElement = elements[unUsed];
        int temp = unUsed;
        unUsed = unUsedElement.getCur();
        unUsedElement.setCur(head);
        unUsedElement.setData(data);
        head = temp;
        size++;
    }


    public class Element<V> {
        private V data;
        private int cur;

        public V getData() {
            return data;
        }

        public void setData(V data) {
            this.data = data;
        }

        public int getCur() {
            return cur;
        }

        public void setCur(int cur) {
            this.cur = cur;
        }
    }

}

测试代码

public class StaticLinkTest {

    @Test
    public void main(){
        StaticLink<Integer> link = new StaticLink<>(10);
        link.insertFirst(2);
        link.insertFirst(1);
        link.insertLast(4);
        link.insertLast(5);
        link.insert(3,1);
        link.printAll();
        link.deleteFirst();
        link.deleteLast();
        link.delete(1);
        link.printAll();
        Assert.assertEquals(4,(int)link.get(1).getData());
        link.deleteFirst();
        link.deleteFirst();
        Assert.assertEquals(0,link.size());
    }

}

静态列表的特色

静态列表的大多数状况下和动态链表类似,可是静态链表的实现方式当值链表失去了原有的优点,并且操做变得更加复杂了,主要体现为与如下几点。

  1. 空间须要连续申请,并且空间有限的。

因为静态链表使用数组模拟的,因此空间是连续的,虽然链表在数组中能够不按照顺序排列,可是对于整个存储空间来讲,仍是须要连续的。

另外,因为用到数组模拟,因此咱们在建立数组时须要初始化长度,链表自己的一个优势就是动态添加,可是静态链表没有办法这样添加。当链表的长度须要大于数组的长度时就没法实现数组模拟了,除非复制到一个更长的新数组,那是就须要考虑更多问题,好比串联备用链表、更高性能消耗等。

  1. 查找元素须要遍历查询

这点和动态链表类似,在找一个元素时须要对链表进行遍历。

  1. 操做更复杂

因为执行操做须要额外维护一个备用链表,因此不管是插入仍是删除,都须要额外关心操做元素的动向,因此静态链表操做比动态链表更复杂。

存在以上问题,因此静态链表除了有助于咱们分析问题,在不少语言中并不经常使用,尤为是如今不支持指针或者引用高级编程语言愈来愈少的状况下。可是,咱们学习算法时,仍是须要帐户哦静态链表的。

相关文章
相关标签/搜索