zoukankan      html  css  js  c++  java
  • LinkedList 的 API 与数据结构

    LinkedList 是 List 接口和 Deque 接口的双向链表实现,它所有的 API 调用都是基于对双向链表的操作。本文将介绍 LinkedList 的数据结构和分析 API 中的算法。

    数据结构

    LinkedList 的数据结构是一个双向链表,它有两个成员变量:first 和 last,分别指向双向队列的头和尾。

    prevnext'A'prevnext'B'prevnext'C'firstlast
    Node<E> first;
    Node<E> last;
    

    这里“双向”的含义是相对单链表而言的,双向链表的节点不仅有后继,还有前驱。LinkedList 中双向链表的节点是一个个的 Node,它是 LinkedList 的一个静态内部类。其定义如下。Node 是一个泛型类,泛型参数是存放在 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;
            }
        }
    

    一个包含若干元素的 LinkedList 如下图所示。

    LinkedList 的 API 都是基于双端队列的操作来实现的,这些操作被封装成了一系列的 private 方法。下面对这些私有方法进行分析。

    插入操作:linkFirst(e) 与 linkLast(e)

    LinkedList 通过 linkFirst(e) 与 linkLast(e) 分别往双向链表的头部和尾部插入元素。插入元素时需要考虑两种情况:1)双向链表中不包含元素;2)双向链表中已经包含了元素。插入元素属于修改操作,因此操作数 modCount 需要进行自增。更多关于 modCount 的说明可以参考这篇:(ArrayList 源码分析)[https://www.cnblogs.com/robothy/p/13969448.html]。linkFirst(e) 源码如下,linkLast(e) 源码与前者类似。

        private void linkFirst(E e) {
            final Node<E> f = first;
            final Node<E> newNode = new Node<>(null, e, f); // 创建一个 Node 对象 e,在构造方法中,e 的后继已经指向了旧的 first。
            first = newNode;
            if (f == null) // 处理边界情况:双向链表中没有元素
                last = newNode;
            else
                f.prev = newNode;
            size++; // size 用于统计 LinkedList 中的元素个数
            modCount++; // 统计操作数,用于支持迭代时 fail-fast 机制
        }
    

    在指定元素前面插入:linkBefore(e, succ)

    linkBefore(e, succ) 在元素 succ 前面插入元素 e,它需要考虑两种情况:1)succ 的前驱为空;2)succ 的前驱不为空。在指定元素前面插入操作时间复杂度为 O(1),相对 ArrayList 时间复杂度为 O(n) 插入来说,效率极高。(n 为 List 中已有元素的个数)

        void linkBefore(E e, Node<E> succ) {
            // 这里没有对 succ 为空进行检查,因为是 private 方法,在外层确保输入不为空即可
            final Node<E> pred = succ.prev; // 获取 succ 的前驱
            
            final Node<E> newNode = new Node<>(pred, e, succ); // 构造一个新的节点,此时新节点的前驱指向 succ 的前驱,新节点的后继指向 succ
            succ.prev = newNode; // 更新 succ 的前驱指向
            if (pred == null) // succ 前驱原来所指元素为空的情况
                first = newNode; // 更新 first 指针
            else
                pred.next = newNode; // 否则更新 succ 原来前驱所指即可
            size++;
            modCount++;
        }
    

    移除头部和尾部元素:unlinkFirst(f) 和 unlinkLast(l)

    unlinkFirst(f) 操作将移除头部节点,它需要考虑两种情况:1)链表中只有 1 个元素;2)链表中有超过 1 个元素。

        private E unlinkFirst(Node<E> f) {
            // assert f == first && f != null;
            final E element = f.item;
            final Node<E> next = f.next;
            f.item = null;  // 这两个 null 赋值操作斩断了引用链,让 GC 能够回收对象。
            f.next = null; // help GC
            first = next;
            if (next == null) // 只有 1 个元素的情况,last, first 指向同一个元素,因此移除了 first 所指向的元素之后,last 也要更新
                last = null;
            else // 含有多个元素的情况
                next.prev = null;
            size--;
            modCount++;
            return element;
        }
    

    移除指定元素:unlink(x)

    unlink(x) 在移除指定元素时也是小心翼翼。这个方法在功能上可以替代 unlinkFirst(f) 和 unlinkLast(f),不过因为 LinkedList 对头和尾的操作及其频繁,因此用单独的更高效的函数进行处理可以在一定程度上提升性能。

        E unlink(Node<E> x) {
            // assert x != null;
            final E element = x.item; // 取出要返回的值
            // 拿到 x 的前驱和后继
            final Node<E> next = x.next;
            final Node<E> prev = x.prev;
            
            // 处理前驱指针
            if (prev == null) { // 前驱所指为空,表示 x 为头部元素
                first = next;
            } else { // 前驱不为空
                prev.next = next;
                x.prev = null; // 帮助 GC
            }
    
            // 处理后继指针
            if (next == null) {
                last = prev;
            } else {
                next.prev = prev;
                x.next = null; // 帮助 GC
            }
    
            x.item = null; // 帮助 GC
            size--;
            modCount++;
            return element;
        }
    

    根据索引获取指定节点: node(index)

    因为是链表结构,要根据位置获取节点只能以迭代的方式进行,时间复杂度为 O(n),这里的 node(index) 方法做了一点优化:若索引号 index 在前半部分,则从头节点开始遍历;若索引好 index 在后半部分,则从尾节点开始遍历。

        Node<E> node(int index) {
            // assert isElementIndex(index);
    
            if (index < (size >> 1)) { // 如果 index 小于 size 的一半,则从头部开始遍历
                Node<E> x = first;
                for (int i = 0; i < index; i++)
                    x = x.next;
                return x;
            } else { // 如果 index 大于等于 size 的一半,则从尾部开始遍历
                Node<E> x = last;
                for (int i = size - 1; i > index; i--)
                    x = x.prev;
                return x;
            }
        }
    

    以上部分就是 LinkedList 中双端队列的操作了,不过这些方法都被 private 修饰,因此开发人员无法直接调用它们,不过 LinkedList 所暴露出来的 API 几乎都是调用这些 private 方法来完成操作的。下面介绍 LinkedList 的相关 API。

    构造方法

    LinkedList 的构造方法有两个,一个是无参构造方法,另一个构造方法可以传入一个集合。

    • LinkedList() :构造一个空的列表;
    • LinkedList(Collection<? extends E> c) :构造一个列表,并将集合中的元素插入到列表中,插入顺序与集合的迭代器返回元素的顺序一致。

    LinkedList 作为 List 接口的实现类

    size()

    LinkedList 内部维护了一个成员变量 size,每次插入或者删除元素时都会更新该变量的值。size() 方法仅仅是返回了该变量的值。

    isEmpty()

    通过 size 的值来判断,size 为 0 即表示 LinkedList 为空。

    indexOf(o)

    indexOf(o) 将返回指定元素 o 在 LinkedList 中首次出现的位置(头节点到尾节点方向),它需要从头节点开始遍历双向链表。如果元素不存在,则返回 -1。传入的 o 可以为 null,源码中专门分了两个分支来处理传入的 o 为 null 和非 null 的问题。时间复杂度为 O(n),其中 n 为 LinkedList 中元素的数量。

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

    lastIndexOf(o)

    lastIndexOf(o) 与 indexOf(o) 相反,它从双向链表的尾部开始遍历,返回元素 o 在 LinkedList 中最后出现的位置。返回 -1 表示不包含元素 o。

    contains(o)

    contains(o) 方法调用了 indexOf(o),通过检查返回值是否为 -1 来判断 LinkedList 中是否包含了 o。

    add(e)

    add(e) 将元素 e 添加到双向链表的末尾,此方法直接调用了 linkLast(e) 方法完成了操作。

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

    add(index, element)

    此方法将元素添加到指定的索引位置,它分了两种情况:一种是 index == size,直接添加到双向链表尾部即可;另一种是非添加到尾部,需要先迭代找到索引位置为 index 的元素,然后将新元素插入到它前面。时间复杂度为 O(n)。

        public void add(int index, E element) {
            checkPositionIndex(index); // 暴露给用户的 API,需要对用户的输入进行检查
    
            if (index == size)
                linkLast(element);
            else
                linkBefore(element, node(index));
        }
    

    set(index, element)

    此方法调用了 node(index) 获取 element 所在的 Node,然后将 element 挂到了 Node 上。时间复杂度为 O(n)。

        public E set(int index, E element) {
            checkElementIndex(index);
            Node<E> x = node(index);
            E oldVal = x.item;
            x.item = element;
            return oldVal;
        }
    

    remove(index)

    remove(index) 移除索引为 index 的元素,先根据索引获取节点,然后调用 unlink(e) 移除节点。

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

    remove(o)

    remove(o) 将查找元素 o 在 LinkedList 中第一次所在的节点,然后移除该节点。这一操作需要遍历双向链表。需要注意的是 remove(o) 并不会移除所有的 o ,只会移除第 1 个。

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

    listIterator()

    LinkedList 没有单独的内部类实现 Iterator 接口,调用 iterator() 方法返回的本质是一个 ListItr,和 listIterator() 返回的是一样的迭代器。

    ListItr 允许从指定下标位置开始迭代,下标位置通过构造方法的参数传入。

            ListItr(int index) {
                // assert isPositionIndex(index);
                next = (index == size) ? null : node(index);
                nextIndex = index;
            }
    

    LinkedList 有两个获取 ListItr 的 API,分别是 listIterator() 与 listIterator(index),二者本质一样。

        public ListIterator<E> listIterator() {
            return listIterator(0);
        }
    
        public ListIterator<E> listIterator(int index) {
            checkPositionIndex(index);
            return new ListItr(index);
        }
    

    ListItr 在迭代的过程中 LinkedList 不能够被其它的线程改变,否则可能抛出 ConcurrentModificationException。这是一种 fail-fast 策略,通过修改数 modCount 来实现,前面可以看到,凡是会改变链表结构的操作都会更新 modCount 的值。在迭代的过程中不断检查 modCount 是否和期望的值一致,如果不一致,则说明有其它的线程修改了双向链表的结构。此时 LinkedList 中的数据可能出现错误,但如果没有 fail-fast 机制,这种错误可能不会立即暴露出来,系统可能需要运行很长时间才暴露,到那时可能已经产生严重后果了,后面再来排查错误原因也及其困难。

    通过 modCount 机制来探测这类难以错误,一旦探测到,立即报告,这就是 fail-fast 机制。不过由于多线程操作本身存在着不确定性,modCount 也并非一定能够探测到这种错误。为了避免这种错误,在多线程访问同一个 LinkedList 对象时应该进行线程同步,最好就时不让多线程访问同一个 LinkedList。

    不过 ListItr 允许迭代器自身修改 LinkedList,它在修改之后会更新 modCount,支持的修改操作包括:

    • remove() 移除刚刚返回的元素
    • set(e) 将刚刚返回的元素所在 Node 节点的值修改为 e
    • add(e) 在刚刚返回的元素后面插入 e

    LinkedList 作为 Deque 接口的实现类

    双端队列接口 Deque 提供了一组在线性集合头部和尾部进行操作的 API,LinkedList 在通过操作双端队列的头部和尾部实现这些抽象方法。

    新增头(尾)部元素:addFirst(e), addLast(e), offer(e), offerFirst(e), offerLast(e), push(e)

        public void addFirst(E e) {
            linkFirst(e); // LinkedList 支持存放 null
        }
    

    获取头(尾)部元素:getFirst(), getLast(), peek(), peekFirst(), peekLast()

        public E getFirst() {
            final Node<E> f = first;
            if (f == null) // getXXX() 抛出遗产,peekXXX() 使用特殊值 null 来表示没有元素
                throw new NoSuchElementException();
            return f.item;
        }
    

    删除头(尾)部元素:removeFirst(), removeLast(), poll(), pollFirst(), pollLast(), pop()

        public E removeFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
    

    小结

    LinkedList 内部是双向链表结构,新增,删除元素很方便,支持存放 null 元素。

    LinkedList 实现了 List 和 Deque 接口。作为 List,LinkedList 适用于数量未知且需要大量增删操作情形,若需要随机访问或者大量查询,应该使用 ArrayList;作为 Deque,LinkedList 适用于容量未知的情形,如果容量已知,则使用 ArrayDeque 效率会更高一些。

    LinkedList 是非线程安全的,多个线程同时访问一个 LinkedList 可能破坏其内部结构。

  • 相关阅读:
    无重复字符的最长子串
    有效的括号
    最长公共前缀
    罗马数字转整数
    Android解析JSON数据异步加载新闻图片
    回文数
    Java从Json获得数据的四种方式
    JavaMD5加密工具类
    div模仿select效果二:带搜索框
    BG雪碧图制作要求
  • 原文地址:https://www.cnblogs.com/robothy/p/14098886.html
Copyright © 2011-2022 走看看