集合:LinkedList实现

LinkedList类关系介绍

    LinkedList是一类链表结构的集合,插入、删除等操作效率高,查询效率低。LinkedList除了实现List接口,还实现了Deque接口。Deque是Queue的子接口,是扩展Queue接口,即可用于FIFO(First in First out)也可用于LIFO(Last in First out)的结构队列,可在头部和尾部检索、插入、删除。因而LinkedList也具有双向特性和更加灵活丰富的双向操作方法,LinkedList继承实现关系图参考下方。

LinkedList继承实现关系图

LinkedList数据结构

    LinkedList中有一个内部私有类Node,这个类就是LinkedList数据结构的基础,它代表着链表的每一个节点,并且除了自身节点的值item外,还持有前一个节点和后一个节点,所以说LinkedList是双向链表,即可向前检索,也可以向后检索。

Node结构源码:

/**
* linkedList的底层链表节点数据结构
* @param <E> 集合内存放的对象类型
*/
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中构造函数Node(Node<E> prev, E element, Node<E> next) 用于构建新节点,并关联前一个节点及后一个节点。在插入节点的方法中会用到,会在后面的源码中多次出现。下图是LinkedList的结构图,这里只表示关系结构,不代表内存分配也是该结构。实际上,每一个节点Node的内存分配并不是连续的,而是随机离散的。

transient int size = 0;

/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

    LinkedList有first和last两个成员变量,分别为头部节点和尾部节点,并且按照注释理解,first、last具有以下关系:

  • 当first与last有一方为null时,另一方必定也为null,链表必为空链表。
  • first与last不为null前提下,first节点的prev与last节点的next一定为null,两者的item一定不为null。(这里的item一定不为null,代码中并未做强约束,实际上LinkedList是支持添加null。这里额外补充,idea中debug不显示null,只会显示not showing null elements,这里需要在Intellij idea > Preferences > Build,Execution,Deployment > Debugger > Data Views > java 这里取消勾选Hide null elements in arrays and collections。这里我的idea是2018.1 mac版本,其他版本可能会有出入,直接打开Preferences后搜索Debugger,再找类似的位置)

    下方图为LinkedList中的方法,展示每个方法实现了哪个接口。

这里我们主要看以下几个方法的源码:

  • 实现Deque的addFirst(E)、addLast(E)、removeFirstOccurrence(Object)方法
  • 实现AbstractSequentialList的addAll(Collection<? extends E>>)、get(int)、remove(int)方法
  • 实现AbstractList的add(E)方法

addFirst(E)
    addFirst(E)不是List接口下的方法,所以一般使用List的情况下(List<String> users = new LinkedList<String>()),不常用这个方法,要使用的话还需要做一次类型转换,实现Deque的其他方法同理。

public void addFirst(E e) {
    linkFirst(e);
}

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

addFirst方法实际上调用的是linkFirst(e)方法。这里就是将传入的Element构建成新的节点插入链表的头部,后续判断first是否为null(空链表),若是,将新节点也设置为last,即LinkedList只有一个节点时,first和last均为该节点;若不是,将原先的头部节点的前一个节点赋值为新的头部节点。对于链表的操作,关注两个点,一个就是新节点插入与前后节点的关联,另外就是新节点插入位置的前后节点与新节点关联。


addLast(E)add(E)
两个方法都是调用了linkLast(e),所以主要看这个方法。

public void addLast(E e) {
    linkLast(e);
}

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

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++;//   集合长度加1
    modCount++;//   修改次数加1 用于fast fail(快速失败策略)
}

代码很简单,比较好理解。这里我们可以看到调用add(E),就是在链表的尾部添加新节点。


removeFirstOccurrence(Object)
这里实际调用的是实现AbstractCollection的remove(Object o)方法,而remove(o)实际调用的是unlink(x)方法,所以主要看unlink(x)做了哪些事。

public boolean removeFirstOccurrence(Object o) {
    return remove(o);// 调用的是remove方法
}

public boolean remove(Object o) {
    if (o == null) {//  判断null用于选择判断节点的方式
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);//    主要还是看unlink(x)这个方法
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);//    主要还是看unlink(x)这个方法
                return true;
            }
        }
    }
    return false;
}

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;//    帮助 GC
    }

    if (next == null) {//   删除的节点为尾部节点
        last = prev;//  将删除的节点的前一个节点作为新的尾部节点
    } else {
        next.prev = prev;// 建立后节点与前节点的链表关系
        x.next = null;//    帮助 GC
    }
    
    x.item = null;//    帮助GC
    size--;//   集合长度减1
    modCount++;//  修改次数加1 用于fast fail(快速失败策略)
    return element;//   返回被删除的节点值
}

这里可以看到,源码中,将删除的节点的item、next、prev都设为null,源码的注释为help GC,java虚拟机HotSpot采用的是根搜索算法,即便不做设置为null的操作,Node也是没有任何GC Root可达,依旧会被回收。这里可能考虑到其他垃圾回收算法(如引用计数算法),使删除的节点不与其他对象存在引用关系,便于快速回收。


addAll(Collection<? extends E>>)
实际上调用的是addAll(int index, Collection<? extends E> c)方法

 public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
    //  判断是否越界 仅对addAll(int index, Collection<? extends E> c)
    //  因为 size >=0 and size <= size 恒成立
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    //  succ表示index位置的节点对象
    //  pred表示succ的前一个节点对象
    Node<E> pred, succ;
    //  这里是对 addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c) 处理
    if (index == size) {//  这里已经将 size为0的情况处理了
        //  如果index == size 说明 直接往双向链表的末尾插入新的集合
        succ = null;//  last  对象的index为 size -1 所以size为null ,直接赋值
        pred = last;//  pred 即为last节点(当size为0,pred = last = 0)
    } else {
        succ = node(index);//   将succ设置为index位置的节点
        pred = succ.prev;// pred设置为index的前一个节点
        //  P.S. pred = succ.prev = node(index - 1) 链表通过index查找效率低,找到对应节点后,通过节点持有的上下对象获取会更高效
    }

    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //  确定好集合插入的位置后,通过Node的带参构造函数,构建节点并关联进链表中
        //  pred-> 上一个 e->当前持有对象 next-> 下一个
        Node<E> newNode = new Node<>(pred, e, null);// next为null 这里可以看出 是往双向链表的末尾插入新的集合节点
        if (pred == null)
            //  若集合一开始没有节点 将设置进去的第一个节点设为链表的第一个节点
            first = newNode;
        else
            pred.next = newNode;//  有了第一个节点后,此后就将下一个节点设置为当前模拟指针节点的下一个节点
        pred = newNode;//   再将模拟指针节点设置为新建的节点,以便循环再往下添加节点
    }

    if (succ == null) {// size 0 或者 size不为0 在链表末尾插入新节点的场景
        last = pred;//  将最后一个添加进入的节点设为last
    } else {
        // 在初始的pred后添加完节点后,将新pred与原先的succ节点进行关联
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;//   修改size
    modCount++;//   用于集合的快速失败检测机制
    return true;//  返回添加成功
}

remove(int)get(int)
remove(int)核心方法还是unlink(E),我们着重看node(index)方法。

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

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

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {//    判断index在链表的前半部还是在后半部
        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;
    }
}

node(int)会先判断index在链表的前半部还是在后半部,size>>1等于size/2

size >> 1 = size / 2 ^ 1

位运算做运算效率高,但是可读性差。如果是一些封装方法并且注重效率,感觉位运算就是个很好的选择。
判断节点位置后,若在前半部则从头部开始检索,反之则从尾部开始。

LinkedList使用注意项

    不要使用普通for循环get(int)去遍历LinkedList,由上面的get(int)方法也可以看出,LinkedList需要一个一个节点去查找,若是链表有10个节点,index为5,链表需要从头部节点开始获取next节点,再根据next节点的next节点继续获取下去,直至第5个节点。若是遍历一个长度为10的链表,需要查找1+2+3+4+5+5+4+3+2+1次节点,后半部会从尾端节点开始查找,所以越靠近首尾,需要查找节点的数量会减少。这里我们可以计算遍历LinkedList的时间复杂度:

((1+N/2)*N/2)/2*2 = N/2+N^2/4

可以得出遍历LinkedList的时间复杂度为o(N^2),所以还是建议用Iterator来遍历,无论用for还是while循环迭代器,效率都会远远高于for循环get(int)的方式。LinkedList内部私有类ListItr实现了ListIterator,迭代器是用一个next成员变量,维护了一个类似模拟指针的变量,让集合能快速地获取到下一个节点。
    

posted @ 2018-10-10 17:18  eatcrow  阅读(225)  评论(0)    收藏  举报