LinkedList 剖析

LinkedList

基于 JDK 1.8。
ArrayList 随机访问效率很高,但插入和删除性能比较低,而 LinkedList 相反,它的继承关系如下:

这里写图片描述

它和 ArrayList 同样实现了 List 接口,而 List 接口扩展了 Collection 接口,Collection 接口又扩展了 Iterable 接口。但又多实现了 Deque 接口,Deque 接口拓展了 Queue 接口,而 Queue 接口又拓展了 Collection 接口。

实现的接口

Queue

Queue 接口:

public interface Queue<E> extends Collection<E> {

    boolean add(E e);

    boolean offer(E e);

    E remove();

    E poll();

    E element();

    E peek();
}

Queue 接口扩展了 Collection 接口,它的主要操作有三个:

  • 在尾部添加元素 (add, offer)
  • 查看头部元素 (element, peek),返回头部元素,但不改变队列
  • 删除头部元素 (remove, poll),返回头部元素,并且从队列中删除

每种操作都有两种形式,区别在于,对于特殊情况的处理不同。特殊情况是指,队列为空或者队列为满,为空容易理解,为满是指队列有长度大小限制,而且已经占满了。LinkedList 的实现中,队列长度没有限制,但别的Queue 的实现可能有。

在队列为空时,

  • element 和 remove 会抛出异常 NoSuchElementException
  • peek 和 poll 返回特殊值 null

在队列为满时,

  • add 会抛出异常 IllegalStateException
  • offer 只是返回 false

把 LinkedList 当做 Queue 使用也很简单,比如,可以这样:

Queue<String> queue = new LinkedList<>();

queue.offer("a");
queue.offer("b");
queue.offer("c");

while(queue.peek()!=null){
    System.out.println(queue.poll());    
}

输出为:

a
b
c

Stack(没有单独的栈接口)

栈是一种常用的数据结构,与队列相反,它的特点是先进后出、后进先出。

扫描二维码关注公众号,回复: 1996714 查看本文章

Java 中有一个类 Stack,用于表示栈,但这个类已经过时了,Java 中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口 Deque 中,主要有三个方法:

void push(E e)
E pop()
E peek()

  • push 表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push 会抛出异常 IllegalStateException
  • pop 表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出异常 NoSuchElementException
  • peek 查看栈头部元素,不修改栈,如果栈为空,返回 null
Deque<String> stack = new LinkedList<>();

stack.push("a");
stack.push("b");
stack.push("c");

while(stack.peek()!=null){
    System.out.println(stack.pop());    
}

输出为:

c
b
a

Deque

Deque 接口定义如下:

public interface Deque<E> extends Queue<E> {

   void addFirst(E e);

   void addLast(E e);

   boolean offerFirst(E e);

   boolean offerLast(E e);

   E removeFirst();

   E removeLast();

   E pollFirst();

   E pollLast();

   E getFirst();

   E getLast();

   E peekFirst();

   E peekLast();

   boolean removeFirstOccurrence(Object o);

   boolean removeLastOccurrence(Object o);

   // *** Queue methods ***

   //...

   // *** Stack methods ***

   //...

   // *** Collection methods ***

   //...

}

在 Deque 接口中,有 Queue 方法与 Stack 方法以及 Collection 方法。

xxxFirst操作头部,xxxLast操作尾部。

与队列类似,每种操作有两种形式,区别也是在队列为空或满时,处理不同。

队列为空时,getXXX / removeXXX 会抛出异常,而 peekXXX / pollXXX 会返回 null。

为满时,addXXX 会抛出异常,offerXXX只是返回 false。

栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰。

Deque 接口还有一个迭代器方法,其注释为 Returns an iterator over the elements in this deque in reverse sequential order. The elements will be returned in order from last (tail) to first (head).:

Iterator descendingIterator();

例子:

Deque<String> deque = new LinkedList<>(
        Arrays.asList(new String[]{"a","b","c"}));
Iterator<String> it = deque.descendingIterator();
while(it.hasNext()){
    System.out.print(it.next()+" ");
}

输出为

c b a 

用法小结

LinkedList 的用法是比较简单的,与 ArrayList 用法类似,支持 List 接口,只是,LinkedList 增加了一个接口 Deque,可以把它看做队列、栈、双端队列,方便的在两端进行操作。

构造函数

构造函数:

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

例子:

List<String> list = new LinkedList<>();
List<String> list2 = new LinkedList<>(
        Arrays.asList(new String[]{"a","b","c"}));

基本原理

组成

ArrayList 内部是数组,元素在内存是连续存放的,而 LinkedList 的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起。

为了表示链接关系,需要一个节点的概念,节点包括实际的元素,但同时有两个链接,分别指向前一个节点(前驱)和后一个节点(后继),节点是一个内部类,定义如下:

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;
    }
}

Node 类表示节点,item 指向实际的元素,next 指向下一个节点,prev 指向前一个节点。

LinkedList 内部是由三个变量组成:

transient int size = 0;

transient Node first;

transient Node last;

size 表示链表长度,默认为 0,first 指向头节点,last 指向尾节点,初始值都为null。

Add 方法

Add(E e) 方法

方法代码如下:

public boolean add(E e) {
    linkLast(e);
    return true;
}

其中调用了 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++;
}

该方法先创建一个新的节点 newNode。prev 指向原来的尾节点,后继指向 null。代码为:

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

修改尾节点 last,指向新的最后节点 newNode。代码为:

last = newNode;

修改前节点的后向链接,如果原来链表为空,则让头节点指向新节点,否则让原尾节点的 next 指向新节点。代码为:

if (l == null)
    first = newNode;
else
    l.next = newNode;

增加链表大小。代码为:

size++

modCount++ 的目的与 ArrayList 是一样的,记录修改次数,便于迭代中间检测结构性变化。

Add(int index, E element) 方法

代码如下:

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

先是调用 checkPositionIndex(int index) 方法,该方法检测下标的有效性,如果下标无效,则抛出异常 IndexOutOfBoundsException。

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

如果 index 为 size,添加到最后面;而当 index 小于 size 时,插入到 index 对应的结点之前,调用 linkBefore(E e, Node succ) 方法,代码如下:

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;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

新建结点 newNode ,然后将前驱设为 pred,后继设为 succ,代码如下:

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

然后将 succ 的前驱设为 newNode,代码如下:

 succ.prev = newNode;

判断 succ 是否为头结点,如果是,则将头结点设为 newNode;如果不是,则将 succ 前驱的后继设为 newNode,代码如下:

if (pred == null)
    first = newNode;
else
    pred.next = newNode;

长度增加:

size++;

可以看出,在中间插入元素,LinkedList 只需按需分配内存,修改前驱和后继节点的链接,而 ArrayList 则可能需要分配很多额外空间,且移动所有后续元素

Get 方法

LinkedList 类的 get(int index) 方法如下:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

其中 checkElementIndex(int index) 方法是也是检测下标的有效性,如果下标无效,则抛出异常 IndexOutOfBoundsException,与 checkPositionIndex(int index) 有细微的差距。

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

若下标有效,则调用 node(int index) 方法,来寻找对应的结点,其item属性就指向实际元素内容:

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 < (size>>1) ,则从头节点开始查找,否则,从尾节点开始查找。

ArrayList中数组元素连续存放,可以直接随机访问,而在LinkedList中,则必须从头或尾,顺着链接查找,效率比较低。

IndexOf 方法

indexOf 方法,它是从头节点顺着链接往后找,如果要找的是 null,则找第一个 item 为 null 的节点,否则使用equals 方法进行比较,找到后返回下标,如果未找到,即返回 -1。代码如下:

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

Remove 方法

Remove(Object o) 方法

代码如下:

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

代码很简单,找到待删除的 object 后,调用 unlink(Node x) 方法,unlink(Node x) 方法如下:

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 {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

首先先判断是否为头结点,如果是,则将头结点设为

Remove(int index) 方法

代码如下:

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

先是调用 checkElementIndex(int index) 方法判断 index 是否越界,然后再调用 node(int index) 方法找到节点,接着调用 unlink(Node x) 方法,代码如下:

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 {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

删除 x 节点,基本思路就是让 x 的前驱和后继直接链接起来,next 是 x 的后继,prev 是 x 的前驱,具体分为两步:

  1. 第一步是让 x 的前驱的后继指向 x 的后继。如果 x 没有前驱,说明删除的是头节点,则修改头节点指向 x 的后继。
  2. 第二步是让 x 的后继的前驱指向 x 的前驱。如果 x 没有后继,说明删除的是尾节点,则修改尾节点指向 x 的前驱。

对于队列、栈和双端队列接口,长度可能有限制,LinkedList 实现了这些接口,不过 LinkedList 对长度并没有限制。

特点分析

LinkedList 内部是用双向链表实现的,它是一个 List ,但也实现了 Deque 接口,可以作为队列、栈和双端队列使用,维护了长度、头节点和尾节点,这决定了它有如下特点:

  • 按需分配空间,不需要预先分配很多空间
  • 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
  • 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
  • 在两端添加、删除元素的效率很高,为O(1)。
  • 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。

猜你喜欢

转载自blog.csdn.net/qq_37138933/article/details/80953911