Java集合---LinkedList底层原理

java中的LinkedList类实现了List接口和Deque接口,是一种链表类型的数据结构,支持高效的插入和删除操作,同时也实现了Deque接口,使得LinkedList类也具有队列的特性。LinkedList类的底层实现的数据结构是一个双端的链表。

 

LinkedList类中有一个内部私有类Node,这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。

源码中的实现是这样的。

 

private static class Node<E> {

        E item;

       Node<E> next;

       Node<E> prev;

 

       Node(Node<E> prev, E element, Node<E> next) {

           this.item = element;

           this.next = next;

           this.prev = prev;

        }

    }

 

注意这个节点的初始化方法,给定三个参数,分别前驱节点,本节点的值,后继结点。这个方法将在LinkedList的实现中多次调用。

 

下图是LinkedList内部结构的可视化,能够帮我们更好的理解LinkedList内部的结构。

 

 

 

 

 

 

image.png

 

 

双端链表由node组成,每个节点有两个reference指向前驱节点和后继结点,第一个节点的前驱节点为null,最后一个节点的后继节点为null。

 

LinkedList类有很多方法供我们调用。我们不会一一介绍,本文会详细介绍其中几个最核心最基本的方法,LinkedList的创建添加和删除基本都和这几个操作有关。

 

 

·       linkFirst() method

·       首先我们介绍第一个方法,linkFirst(),顾名思义,这个方法是插入第一个节点,我们先直接上代码,看看它的具体实现

 

 

/**

     * Links e asfirst element.

     */

    private voidlinkFirst(E e) {

        finalNode<E> f = first;

        finalNode<E> newNode = new Node<>(null, e, f);

        first =newNode;

        if (f ==null)

            last =newNode;

        else

            f.prev= newNode;

        size++;

        modCount++;

    }

 

我们发现出现了两个变量,first和last这两个变量是LinkedList的成员变量,分别指向头结点和尾节点。他们是如下定义的:

 

/**

     * Pointer tofirst node.

     * Invariant:(first == null && last == null) ||

     *            (first.prev == null &&first.item != null)

     */

    transient Node<E>first;

 

/**

     * Pointer tolast node.

     * Invariant:(first == null && last == null) ||

     *            (last.next == null &&last.item != null)

     */

    transientNode<E> last;

 

 

我们可以看到注释中的内容。first和last需要维持一个不变量,也就是first和last始终都要维持两种状态:

首先,如果双端链表为空的时候,两个都必须为null

如果链表不为空,那么first的前驱节点一定是null,first的item一定不为null,同理,last的后继节点一定是null,last的item一定不为null。

 

知道了first和last之后,我们就可以开始分析linkFirst的代码了。

linkFirst的作用就是在first节点的前面插入一个节点,插入完之后,还要更新first节点为新插入的节点,并且同时维持last节点的不变量。

 

我们开始分析代码,首先用f来临时保存未插入前的first节点,然后调用的node的构造函数新建一个值为e的新节点,这个节点插入之后将作为first节点,所以新节点的前驱节点为null,值为e,后继节点是f,也就是未插入前的first节点。

然后就是维持不变量,首先第一种情况,如果f==null,那就说明插入之前,链表是空的,那么新插入的节点不仅是first节点还是last节点,所以我们要更新last节点的状态,也就是last现在要指向新插入的newNode。

如果f!=null那么就说明last节点不变,但是要更新f的前驱节点为newNode,维持first节点的不变量。

最后size加一就完成了操作。

 

 

·       linkLast() method

·       分析了linkFirst方法,对于 linkLast()的代码就很容易理解了,只不过是变成了插入到last节点的后面。我们直接看代码

 

 

/**

     * Links e aslast element.

     */

    void linkLast(Ee) {

        finalNode<E> l = last;

        finalNode<E> newNode = new Node<>(l, e, null);

        last =newNode;

        if (l ==null)

            first =newNode;

        else

            l.next= newNode;

        size++;

        modCount++;

    }

 

到这里我们发现有这个两个方法,我们已经可以实现一个简单队列的插入操作,上面两个方法就可以理解为插入队头元素和队尾元素,这也说明了LinkedList是实现了Deque接口的。

从源码中也可以看出,addfirst和addLast这两个方法内部就是直接调用了linkFirst和LinkLast

 

/**

     * Inserts thespecified element at the beginning of this list.

     *

     * @param e theelement to add

     */

    public voidaddFirst(E e) {

        linkFirst(e);

    }

 

    /**

     * Appends thespecified element to the end of this list.

     *

     *<p>This method is equivalent to {@link #add}.

     *

     * @param e theelement to add

     */

    public voidaddLast(E e) {

       linkLast(e);

    }

 

 

·       linkBefore(E e,Node<E> succ)

·       下面我们看一个linkBefore方法,从名字可以看出这个方法是在给定的节点前插入一个节点,可以说是linkFirst和linkLast方法的通用版。

 

 

/**

     * Insertselement e before non-null Node succ.

     */

    voidlinkBefore(E e, Node<E> succ) {

        // assertsucc != null;

        finalNode<E> pred = succ.prev;

        finalNode<E> newNode = new Node<>(pred, e, succ);

        succ.prev =newNode;

        if (pred ==null)

            first =newNode;

        else

           pred.next = newNode;

        size++;

        modCount++;

    }

 

我们可以看到代码的实现原理基本和前面的两个方法一致,这里是假设插入的这个节点的位置是非空的。

 

 

·       add(int index, Eelement)

·       下面我们看add方法,这个方法就是最常用的,在指定下标插入一个节点。我们先来看下源码的实现,很简单

 

 

/**

     * Inserts thespecified element at the specified position in this list.

     * Shifts theelement currently at that position (if any) and any

     * subsequentelements to the right (adds one to their indices).

     *

     * @param indexindex at which the specified element is to be inserted

     * @paramelement element to be inserted

     * @throwsIndexOutOfBoundsException {@inheritDoc}

     */

    public voidadd(int index, E element) {

       checkPositionIndex(index);

 

        if (index== size)

           linkLast(element);

        else

           linkBefore(element, node(index));

    }

 

首先判断给定的index是不是合法的,然后如果index==size,就说明要插入成为最后一个节点,直接调用linklast方法,否则就调用linkBefore方法,我们知道linkBefore需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index的那个node。

我们看一下Node<E> node(int index)方法,这个方法就是找到给定index的node并返回,类似于数组的随机读取,但由于这里是链表,所以要进行查找

 

/**

     * Returns the(non-null) Node at the specified element index.

     */

    Node<E>node(int index) {

        // assertisElementIndex(index);

 

        if (index< (size >> 1)) {

           Node<E> x = first;

            for(int i = 0; i < index; i++)

                x =x.next;

            returnx;

        } else {

           Node<E> x = last;

            for(int i = size - 1; i > index; i--)

                x =x.prev;

            returnx;

        }

    }

 

我们看到node的实现并不是像我们想象的那样直接就线性从头查找,而是折半查找,有一个小优化,先判断index在前半段还是后半段,如果在前半段就从头开始找,如果在后半段就从后开始找,这样最坏情况也只要找一半就可以了。

 

LinkedList的源码实现并不复杂,我们只介绍这几个方法,相信你一定对于它的内部实现原理有了一定的了解,并且也学习到了优秀的代码书写风格和优化。

对于remove操作,有兴趣的读者可以自行研究代码,它类似于add操作,也是基于三个基本方法来实现的。

 

 

·       unlinkFirst(Node<E>f)

 

·       unlinkLast(Node<E>l)

 

·       unlink(Node<E>x)



作者:六尺帐篷
链接:https://www.jianshu.com/p/56c77c517e71
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/mkmkmkhh/article/details/79292104