• 死磕算法第二弹——栈、队列、链表(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;
            }
        }
    

    动态链表主要包含创建、插入、删除、遍历操作;静态链表是使用数组对动态链表进行模拟,当然也可以实现这种操作。

    创建静态链表

    首先要创建。对于动态链表来说,创建操作并不复杂;但是对于静态链表来说,创建操作会复杂一些。

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

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

    所以我们需要把链表中未使用的空间通过一个链表串起来,这样在需要分配空间时就可以把未使用链表的头指针指向的元素给我们真正使用的链表了。

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

    所以在创建一个链表时需要把这个备用链表串一下。

        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. 操作更复杂

    由于执行操作需要额外维护一个备用链表,所以无论是插入还是删除,都需要额外关心操作元素的动向,所以静态链表操作比动态链表更复杂。

    存在以上问题,所以静态链表除了有助于我们分析问题,在很多语言中并不常用,尤其是现在不支持指针或者引用高级编程语言越来越少的情况下。但是,我们学习算法时,还是需要账户哦静态链表的。

  • 相关阅读:
    js中级-函数封装
    js中级-11.7
    js中级-11.5
    js中级-11.2
    js中级-this
    js中级-作用域链
    10.23
    10.22
    10.19js
    10.18
  • 原文地址:https://www.cnblogs.com/mr-cc/p/8659860.html
Copyright © 2020-2023  润新知