04、集合之LinkedList

底层架构

LinkedList底层数据架构是双向链表,整体架构如下图所示:

在这里插入图片描述

从架构图可以得知:

  1. 链表中的每个元素节点叫Node,每个Node由prev、item、next三部分组成,item中存放元素的值,prev指向前一个节点,next指向后一个节点;
  2. LinkedList中有两个成员变量first和last,first指向头节点,last指向尾节点,如果LinkedList为空,first和last都指向null;
  3. 头节点的prev指向null,尾节点的next也指向null;
  4. 因为是双向链表,理论上只要机器内存足够大,链表的长度是没有限制的;

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;
    }
}
新增元素

新增元素时,可以选择新增到链表头部还是链表尾部,add()方法默认是新增到链表尾部,addFirst()方法是新增到链表头部,两种新增方式的源码如下:

从尾部追加(add方法)
// 从尾部开始追加节点
void linkLast(E e) {
    // 把尾节点数据暂存
    final Node<E> l = last;
    // 新建新的节点,初始化入参含义:
    // l 是新节点的前一个节点,当前值是尾节点值
    // e 表示当前新增节点,当前新增节点后一个节点是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 新建节点追加到尾部
    last = newNode;
    //如果链表为空(l 是尾节点,尾节点为空,链表即空),头部和尾部是同一个节点,都是新建的节点
    if (l == null)
        first = newNode;
    //否则把前尾节点的下一个节点,指向当前尾节点。
    else
        l.next = newNode;
    //大小和版本更改
    size++;
    modCount++;
}

从源码上来看,尾部追加节点比较简单,只需要简单地把指向位置修改下即可。

从头部追加(addFirst方法)
// 从头部追加
private void linkFirst(E e) {
    // 头节点赋值给临时变量
    final Node<E> f = first;
    // 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值
    final Node<E> newNode = new Node<>(null, e, f);
    // 新建节点成为头节点
    first = newNode;
    // 头节点为空,就是链表为空,头尾节点是一个节点
    if (f == null)
        last = newNode;
    //上一个头节点的前一个节点指向当前节点
    else
        f.prev = newNode;
    size++;
    modCount++;
}

头部追加节点和尾部追加节点非常类似,只是前者是移动头节点的 prev 指向,后者是移动尾节点的 next 指向。

删除元素

LinkedList删除元素的方式比较多,主要有以下方法:

  1. remove()方法和removeFirst()方法,从链表头部删除元素,remove()方法底层是直接调用的removeFirst()方法;

  2. remove(Object o)方法和remove(int index)方法,分别是根据对象和下标删除集合中的元素;

  3. removeLast()方法,从链表尾部删除元素;

  4. removeFirstOccurrence(Object c)方法和removeLastOccurrence()方法,分别是从链表头部和尾部删除第一个匹配到的元素;

  5. clear()方法,清空集合中的所有元素;

  6. removeAll(Collection c)方法,删除参数集合中的所有元素;

  7. removeIf(Predicate<? super E> filter)方法,根据条件删除集合中的元素,该方法是JDK 1.8新加的,参数支持lambda表达式,使用方式如下:

    public static void main(String[] args) {
      LinkedList<String> list = new LinkedList<>();
      list.add("a");
      list.add("b");
      list.add("c");
      list.add("0");
      list.add("1");
      list.add("2");
    
      // 删除集合中所有的为数字的字符串
      list.removeIf(e -> e.matches("^[\\d]*$"));
      System.out.println(list);
    }
    

其中removeFirst、removeLast、removeFirstOccurrence、removeLastOccurrence这四个方法是LinkedList继承Deque接口的,是ArrayList所没有的;其他的几个方法都是从List继承的,ArrayList也有。

迭代器

虽然LinkedList有很多删除元素的方法,但是仍然推荐使用Iterator迭代器进行删除操作,如下代码跟ArrayList一样,依然会有问题:

public static void main(String[] args) {
  LinkedList<String> list = new LinkedList<>();
  list.add("a");
  list.add("b");
  list.add("c");
  list.add("d");
  list.add("e");
  list.add("f");
  list.add("0");
  list.add("1");
  list.add("2");

  for(int i = 0; i < list.size(); ++i) {
    list.remove(i);
    System.out.println("list size: " + list.size());
  }
  System.out.println("list size: " + list);
}

/**
* 运行结果:
* list size: 8
* list size: 7
* list size: 6
* list size: 5
* list size: 4
* list: [b, d, f, 1]
*/

因为在remove()方法中会执行size–,改变集合的长度。

因为 LinkedList 要实现双向的迭代访问,所以使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做:ListIterator,这个接口提供了向前和向后的迭代方法,如下所示:

迭代顺序方法
从尾到头迭代方法hasPrevious、previous、previousIndex
从头到尾迭代方法hasNext、next、nextIndex

LinkedList 实现了 ListIterator 接口,如下图所示:

// 双向迭代器
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;//上一次执行 next() 或者 previos() 方法时的节点位置
    private Node<E> next;//下一个节点
    private int nextIndex;//下一个节点的位置
    //expectedModCount:期望版本号;modCount:目前最新版本号
    private int expectedModCount = modCount;
}

使用迭代器删除的代码如下:

public static void main(String[] args) {
  LinkedList<String> list = new LinkedList<>();
  list.add("a");
  list.add("b");
  list.add("c");
  
  // 从前往后遍历
  ListIterator<String> iterator = list.listIterator();
  while (iterator.hasNext()) {
    System.out.println(iterator.nextIndex() + " : " + iterator.next());
  }

  // 从后往前遍历
  iterator = list.listIterator(list.size());
  while(iterator.hasPrevious()) {
    System.out.println(iterator.previousIndex() + " : " + iterator.previous());
    iterator.remove();
  }

  System.out.println("list: " + list);
}
查找元素

链表查询某一个节点是比较慢的,需要挨个循环查找才行,源码如下:

// 根据链表索引位置查询节点
Node<E> node(int index) {
    // 如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 直到 for 循环到 index 的前一个 node 停止
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {// 如果 index 处于队列的后半部分,从尾开始找
        Node<E> x = last;
        // 直到 for 循环到 index 的后一个 node 停止
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

从源码中可以发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能,这种思想值得我们借鉴。

ArrayList与LinkedList对比
时间复杂度
ArrayListLinkedList
get(index)直接读取第几个下标,复杂度 O(1)获取第几个元素,依次遍历,复杂度O(n)
add(E)添加元素,直接在后面添加,复杂度O(1)添加到末尾,直接改变last的指向,复杂度O(1)
add(index, E)添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)添加第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n)
remove(index)删除元素,后面的元素需要逐个移动,复杂度O(n)删除第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n)
空间复杂度

LinkedList底层数据结构是双向链表,链表中的每个节点除了存放数据外,还要额外的存储前后节点的引用,当数据量比较大时,会耗费大量内存;而ArrayList的底层数据结构是数组,耗费的内存空间较小,ArrayList主要的问题是扩容时会耗费大量内存空间,并且在数组中间新增或删除元素时性能低

如何选择使用ArrayList还是LinkedList

如果业务中涉及频繁的向集合头部或中间新增、删除数据,建议使用LinkedList;

如果业务中只是向集合尾部新增或删除数据,并且频繁的通过下标获取集合中的元素,建议使用ArrayList,如果初始化集合的时候指定集合的大小,减少扩容的次数,可以有效的提高性能;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值