LinkedList原理解析


title: LinkedList原理解析
date: 2019-03-04 15:13:49
tags: Java基础


LinkedList原理解析

在前面我们对ArrayList(See my ArrayList原理解析 page for details.)已做了解析,ArrayList操作维护的是内部数组,元素在内存中是连续存放的,可以通过索引直接访问,访问效率高,但是对于删除和移动来说,性能就较低了。而LinkedList呢,顾名思义,它是一个链表,更确切的说,它是一个双向链表,因为LinkedList的元素都是单独存放的,元素之间在逻辑上通过链接连在一起,下面,我们就解析下LinkedList的原理。

LinkedList中元素以节点(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中有个内部类Node,它来表示LinkedList中的元素。而在其内部也保存着当前节点的前驱和后继两个节点。

LinkedList中维护着三个变量:

 transient int size = 0;

transient Node<E> first;

transient Node<E> last;

LinkedList的长度,头节点以及尾节点。LinkedList里的操作大都是围绕着这三个变量来进行的。我们先从add方法看起。

  public boolean add(E e) {

    linkLast(e);

    return true;

}

Ladd(E e)方法,将调用linkLast(E e)方法,插入新节点。

   void linkLast(E e) {

    final Node<E> l = last;

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

    last = newNode;

    if (l == null)

        first = newNode;

    else

        l.next = newNode;

    size++;

    modCount++;

}

第一步:创建新节点,前驱节点指向尾节点,后继节点为空。

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

第二步:令尾节点为这个新建的节点;若原来链表为空,则头节点指向新节点;非空,则尾节点的后继节点指向新节点;链表长度加1;modCount加1,用于迭代时判断链表的结构变化。

这是在链表末尾进行插入,若要指定位置插入呢?再看看另外一个add(int index,E element)方法

 public void add(int index, E element) {

    //检查索引值是否满足index >= 0 && index <= size

    checkPositionIndex(index);

    //若index值与链表长度相同,则插入到链表末尾。

    if (index == size)

        linkLast(element);

    else

        linkBefore(element, node(index));

}

检查完毕,并且想要插入的位置与链表长度包不同,这调用linkBefore方法进行插入。

因为链表无法向数组那样可以直接查找索引,进行插入,所以要根据索引值查找到对应的节点,在这里调用了node方法来查找节点值

   Node<E> node(int index) {

    // assert isElementIndex(index);

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

        Node<E> x = first;

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

            x = x.next;

        return x;

    } else {

        Node<E> x = last;

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

            x = x.prev;

        return x;

    }

}

与链表长度的一半来进行比较,若index小于链表长度的一半,则从头开始;反之,则从尾节点开始。最后返回找到对应索引值的节点。

找到index对应的节点后,就可以插入了。

    void linkBefore(E e, Node<E> succ) {

    // assert succ != null;

    final Node<E> pred = succ.prev;

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

    succ.prev = newNode;

    //如果index节点为头节点,则新节点为头节点;不然则插入到index节点之前

    if (pred == null)

        first = newNode;

    else

        pred.next = newNode;

    size++;

    modCount++;

} 

在中间插入,LinkedList只需要分配内存就行,而ArrayList则需要其他空间,还要移动后续的元素。

我们再来看看remove(int index)方法。

 public E remove(int index) {

    checkElementIndex(index);

    return unlink(node(index));

}

remove(int index)方法同样也需要检查index值,然后找到index值对应的节点,最后调用unlink删除节点后,返回删除的节点。

    E unlink(Node<E> x) {

    // assert x != null;

    final E element = x.item;

    final Node<E> next = x.next;

    final Node<E> prev = x.prev;
    if (prev == null) {

        first = next;

    } else {

        prev.next = next;

        x.prev = null;

    }
    if (next == null) {

        last = prev;

    } else {j

        next.prev = prev;

        x.next = null;

    }
    x.item = null;

    size--;

    modCount++;

    return element;

}

remove(int index)方法的基本思路也很简单:将删除节点x的前驱节点指向x的后继节点,将x的候后继节点的前驱节点指向x的前驱节点。

首先,若x的前驱节点为空,即x为头节点,则x的后继自然就是头节点了。

然后,若x的后继节点为空,即x为尾节点,则x的前驱自然就是头节点了。

结论

以上,我们介绍了LinkedList的几个方法,其余方法也都类似,就是链表的一些基本操作。LinkedList内部是以node节点的方式来进行维护的,每个节点内部又有前驱和后继节点,这就相当于一个双向链表,并且在内部还维护着头节点、尾节点以及长度。通过这些,可以得出一些关于LinkedList的一些特点:

(1)LinkedList不需要预先分配空间,按需进行分配;

(2)进行头、尾的插入很方便;

(3)按索引插入,时间复杂度较低,为O(N/2),但插入效率较高,为O(1);

(4)查找的话,效率也较低,时间复杂度为O(N),不管是否已排序;

(5)在两端进行查找、删除,时间复杂度为O(1);

(6)在中间进行查找、删除,需要逐个比对,时间复杂度为O(N),但修改效率就只有O(1)

综上,若进行的操作涉及大量的插入、删除,尤其是在两端的插入、删除,并且查找中间元素的操作较少的话,使用LinkedList是比较好的选择。

猜你喜欢

转载自blog.csdn.net/weixin_38950807/article/details/88218168