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

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

链表

虽然在不少的高级语言中,链表已经尽可能的被隐藏起来,并且其应用之处还有不少的。面试

什么是链表

链表与数据结构有些不一样。栈和队列都是申请一段连续的空间,而后按顺序存储数据;链表是一种物理上的非连续、非顺序的存储结构,数据元素之间的顺序是经过每一个元素的指针关联的。算法

链表由一系列节点组成,每一个节点通常至少会包含两部分信息;一部分是元素数据自己,另外一部分是指向下一个元素的指针。这样的存储结构让链表相比其余线性的数据结构来讲,操做会复杂一些。数组

相比数组,链表具备其余优点;链表克服了数组须要提早设置长度的缺点,在运行时能够根据须要随意添加元素;计算机的存储空间并不老是连续可用的,而链表能够灵活地使用存储空间,还能更好地对计算机的内存进行动态管理。数据结构

链表分为两种类型:单向链表和双向链表。咱们平时说的链表指单向链表。双向链表的每一个节点除存储元素数据自己外,还额外存储两个指针,分别是上一个节点和下一个节点的地址。性能

链表的存储结构

对于链表来讲,咱们只须要关心链表之间的关系,不须要关系链表实际存储位置,因此在表示一个链表关系时,通常使用箭头来关联两个联系的元素节点。测试

从链表的存储结构可知,链表的每一个节点包含两个部分,分别是数据(叫作data)和指向下个节点地址的指针(叫作next)。在存储了一个链表以后怎么找到它?这里须要一个头节点,这个头节点是一个链表的第1个节点,它的指针指向下一个节点的地址,以此类推,知道指针指向为空时,边表示没有下一个元素了。this

链表的操做

链表的操做有:建立、插入、删除、输出。操作系统

这里提出的插入、删除操做,其位置并不必定是开头或者结尾。因为链表特殊结构,在链表中间进行数据元素的插入与删除也是很容易实现的。指针

建立操做就是空间分配,把头、尾指针以及链表信息初始化。

1. 插入操做

插入操做分为三种状况,分别是头插入、尾插入、中间插入。

头插入的操做,其实是增长一个新的节点,而后把新增的节点的指针指向原来头指针指向的元素,再把头指针指向的元素指向新增的节点。

尾插入的操做,也就是增长一个指针为空的节点,而后把原尾指针指向节点的指针向新增的节点,

中间插入元素的操做会稍微复杂一些。首先新增一个节点,而后把新增的节点的指针指向插入位置的后一个位置的节点,把插入位置的前一个节点的指针指向新增的节点。

2. 删除操做
删除操做与插入操做相似,也有三种状况,分别是头删除、尾删除、中间删除。

删除头元素,先把头指针指向下一个节点,而后把原头结点的指针置空。

删除尾元素时,首先找到链表中倒数第2个元素,而后把尾指针指向的这个元素,接着把原倒数第2个元素的指针置空

删除中间元素时会相对复杂一些,首先要把删除的节点的以前一个节点的指针要指向删除节点的下一个节点,接着要把删除节点的指针置空。

public class Link<T> {

    private int size = 0;
    private Node<T> first;
    private Node<T> last;

    public Link() {
    }

    /**
     * 链表后部插入
     *
     * @param data 插入元素
     */
    public void addLast(T data) {
        if (size == 0) {
            //为空初始化先后元素
            fillStart(data);
        } else {
            Node<T> node = new Node<>();
            node.setData(data);
            last.setNext(node);
            //把最后插入的元素设置为链表尾部的元素
            last = node;
        }
        size++;
    }

    /**
     * 链表头部插入元素
     *
     * @param data 插入元素
     */
    public void addFirst(T data) {
        if (size == 0) {
            fillStart(data);
        } else {
            Node<T> node = new Node<>();
            node.setData(data);
            //把元素的下一个位置的指针指向头元素
            node.setNext(first);
            //把刚插入的元素设置为链表头元素
            first = node;
        }
        size++;
    }

    /**
     * 在链表的指定位置后面插入
     *
     * @param data  插入元素
     * @param index 下表,从0开始
     */
    public void add(T data, int index) {
        if (size > index) {
            if (size == 0) {
                //为空初始化先后元素
                fillStart(data);
                size++;
            } else if (index == 0) {
                addFirst(data);
            } else if (size == index + 1) {
                addLast(data);
            } else {
                Node<T> temp = get(index);
                Node<T> node = new Node<>();
                node.setData(data);
                node.setNext(temp.getNext());
                temp.setNext(node);
                size++;
            }
        } else {
            throw new IndexOutOfBoundsException("链表没有那么长");
        }
    }

    /**
     * 删除头元素
     */
    public void removeFirst() {
        if (size == 0) {
            throw new IndexOutOfBoundsException("链表没有元素");
        } else if (size == 1) {
            //只剩下一个时须要清除first和last
            clear();
        } else {
            Node<T> temp = first;
            first = temp.getNext();
            size--;
        }
    }

    /**
     * 删除尾元素
     */
    public void removeLast() {
        if (size == 0) {
            throw new IndexOutOfBoundsException("链表没有元素");
        } else if (size == 1) {
            clear();
        } else {
            //获取最后一个元素以前的一个元素
            Node<T> temp = get(size - 2);
            temp.setNext(null);
            size--;
        }
    }

    /**
     * 删除链表中间的元素
     * @param index 下标
     */
    public void removeMiddle(int index){
        if(size == 0){
            throw new IndexOutOfBoundsException("链表没有元素");
        }else if (size == 1){
            //只剩下一个时须要清除first和last
            clear();
        }else {
            if (index == 0){
                removeFirst();
            }else if (size == index - 1){
                removeLast();
            }else {
                Node<T> temp = get(index - 1);
                Node<T> next = temp.getNext();
                temp.setNext(next.getNext());
                size--;
            }
        }
    }

    private void clear() {
        first = null;
        last = null;
        size = 0;
    }

    public int size(){
        return size;
    }

    public Node<T> get(int index) {
        Node<T> temp = first;
        for (int i =0; i< index ; i++){
            temp = temp.getNext();
        }
        return temp;
    }

    private void fillStart(T data) {
        first = new Node<>();
        first.setData(data);
        last = first;
    }

    public void printAll(){
        Node<T> temp = first;
        System.out.println(temp.getData());
        for (int i = 0 ; i < size -1; i++){
            temp = temp.getNext();
            System.out.println(temp.getData());
        }
    }

    private class Node<V> {
        private V data;
        private Node<V> next;

        public V getData() {
            return data;
        }

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

        public Node<V> getNext() {
            return next;
        }

        public void setNext(Node<V> next) {
            this.next = next;
        }
    }

}

测试代码

public class LinkTest {

    @Test
    public void main(){
        Link<Integer> link= new Link<>();
        link.addFirst(2);
        link.addFirst(1);
        link.addFirst(4);
        link.addFirst(5);
        link.add(3,1);
        link.printAll();
        link.removeFirst();
        link.removeLast();
        link.removeMiddle(1);
        link.printAll();
        link.removeFirst();
        link.removeFirst();
        Assert.assertEquals(0,link.size());
    }
}

链表的实现逻辑有点复杂,在程序中存在抛异常的状况,在中间插入和删除也考虑到index为头和尾的状况,这样避免调用方法失误而致使程序出错。

链表的特色

链表因为自己存储结构的缘由,有如下几个特色:
1. 物理空间不连续,空间开销大。

链表的最大一个特色就是在物理空间上能够不连续。这样的有点能够利用操做系统的动态内存管理,缺点是须要更多的存储空间去存储指针信息。

2. 运行时能够动态添加

因为数组须要初始化时设定长度,因此在使用数组时每每会出现长度不够的状况,这时只能再声明一个更长的数组,而后把旧数据的数据复制进去,在前面栈的实现中已经看到这一点。使用链表,则不会出现空间不够用的状况。

3. 查找元素须要顺序查找

经过上面的代码能够看出,查找元素时,须要逐个遍历日后查找元素。其实在测试代码中采用循环队列的方法的效率并不高,尤为是当链表很长时,所须要查找的元素的位置越靠后,效率越低。在执行删除操做时,也会遇到相似问题。

4. 操做稍显复杂

在增长和删除时,不须要处理数据,还须要处理指针。从代码上看,删除最后一个元素时,获取最后一个元素很方便,可是因为操做须要实现倒数第二个元素的next指向设置为空,因此只能从头遍历并获取倒数第二个元素以后在进行删除操做。

链表的适用场景

如今计算机的空间愈来愈大,物理空间的开销已经再也不是咱们要关心的问题,运行效率才是咱们在开发中须要考虑的问题。

咱们在前面提到,链表除了单向链表,还有双向链表。通常状况下咱们会使用双向链表,由于多使用的那个指针所占的空间对于如今的计算机资源来讲并不重要。双向链表相对于单向链表的一个优点就是 ,不须要从头仍是从尾查找,操做都是同样的。所以对于尾操做进行操做时就不用逐个从头遍历了,能够直接从尾往前查找元素。

链表能够在运行时动态添加元素,这对于不肯定长度的顺序存储来讲很重要。集合(列表)采用数组实现,在空间不够时须要换更大的数组,而后进行复制操做。这时若是采用链表就很是方便了。

链表的劣势就是在查找中间元素时就须要遍历。通常而言,链表也常常配合其余结构一同使用,如散列表、栈、队里等。

通常的程序里可能会使用一个简单的队列进行消息缓冲,而队列的操做只能从头、尾进行,因此这时使用链表(双向链表)去实现就很是方便了。

链表的性能分析

通常分析性能时,将单向链表做为分析对象。链表的插入分为三种:头插、尾插、和中间插。头、尾是可以直接插入的,其时间复杂度为O(1);而中间插须要遍历链表,因此时间复杂度为O(L),L为插入下标。链表的删除也分为三种:头删、尾删、和中间删。头删是可以直接删除的,其时间复杂度为O(1);而中间删须要遍历链表,因此时间复杂度为O(L),L为删除下标。尾删的时间复杂度则达到了O(N),N为链表长度。

对于查询来说,时间复杂度为O(L),L同样是下标。

因此对于链表来讲,咱们能够发现,链表的头插和头删都是O(1)的时间复杂度,这和栈很想,因此栈能够直接使用单向链表实现。

面试举例:如何反转链表

通常在数据结构或者算法的面试题当中,尽可能不使用额外的空间去实现,尽管如今的计算机空间很充足,可是面试考察的仍是对总体性能的考虑。

方法其实有不少,咱们能够依次遍历链表,而后依次使用头插入的方法来达到目的。

其中有个简单的方法,就是把链表的每一个指针反转。

/**
     * 反转链表
     */
    public void reverse(){
        Node<T> temp = first;
        last = temp;
        Node<T> next = first.getNext();
        for (int i = 0 ; i < size - 1; i++){
            //下下个
            Node<T> nextNext = next.getNext();
            next.setNext(temp);
            temp = next;
            next = nextNext;
        }
        last.setNext(null);
        first = temp;
    }
相关文章
相关标签/搜索