04、集合之LinkedList

本文详细介绍了LinkedList的数据底层结构,包括双向链表的特性,以及add、remove等操作的实现原理。强调了使用ListIterator进行删除操作的推荐性,并对比了LinkedList与ArrayList在性能和空间复杂度上的差异。最后,提供了选择两者使用的指导原则。
底层架构

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,如果初始化集合的时候指定集合的大小,减少扩容的次数,可以有效的提高性能;

### JavaLinkedList 集合类 #### 什么是 LinkedList 集合 `LinkedList` 是一种双向链表的数据结构,在 Java 编程中属于 `List` 接口的一种实现形式[^1]。这种数据结构允许元素之间通过指针相互链接,从而形成一个线性的序列。 #### LinkedList 的特性 - **内部实现**:基于双向链表来存储元素,这意味着每个节点不仅保存着自身的数据项还维护着前后相邻结点之间的连接关系。 - **访问速度**:由于不是连续内存分配的方式,因此随机存取效率较低;但在频繁插入删除操作时表现优异因为不需要移动大量现有元素的位置即可完成相应动作[^2]。 #### 如何创建并初始化 LinkedList 对象 为了定义一个具体的实例化对象,可以通过如下方式引入必要的包并且声明带有特定参数化的变量名: ```java import java.util.LinkedList; // 创建 LinkedList 对象,并指定泛型为 String 类型 LinkedList<String> list = new LinkedList<>(); ``` 上述代码片段展示了如何导入所需的库文件以及怎样构建一个新的列表用于后续的操作处理过程之中[^4]。 #### 常见方法概述 以下是几个常用的 API 方法供开发者调用来管理该类型的容器内的成员: - 添加元素至队列末端或者头部位置; - 移除首尾两端处的目标实体; - 获取第一个或最后一个条目而不将其移出队列之外; - 判断某个给定的对象是否存在于此集合之内等等[^5]。 #### 示例代码展示基本功能的应用场景 下面给出一段简单的例子演示了部分核心函数的具体应用情形: ```java public class Main { public static void main(String[] args) { // 初始化 LinkedList 并加入一些初始值 LinkedList<Integer> numbers = new LinkedList<>(); // 向末尾追加数值 numbers.add(1); numbers.addLast(3); // 插入到开头位置 numbers.addFirst(0); System.out.println("当前内容:" + numbers); // 删除最前面的一个数 Integer removedElement = numbers.remove(); System.out.println("被删掉的是:" + removedElement); // 查看但不取出最后面的数字 int lastNumber = numbers.peekLast(); System.out.println("最后一项是:" + lastNumber); } } ``` 这段程序先建立了一个整数类型的动态数组,接着执行了一系列增减查的动作最终打印出了预期的结果集。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值