深度解析LinkedList工作原理

引言

在 Java 编程中,集合框架是处理数据存储和操作的强大工具。LinkedList 作为其中的重要成员,为我们提供了一种灵活的列表实现方式。与 ArrayList 基于数组的实现不同,LinkedList 采用链表结构,这使得它在某些操作上具有独特的优势。本文将深入探讨 LinkedList 的原理,包括其底层数据结构、核心属性、构造方法、常用操作的实现细节,以及性能特点和应用场景。

1. LinkedList 概述

1.1 定义与用途

LinkedList 是 Java 集合框架中 List 接口和 Deque 接口的双向链表实现。它允许存储重复元素,并且可以像列表一样按顺序访问元素,同时也支持双端队列的操作,如在头部和尾部进行高效的插入和删除。LinkedList 适用于需要频繁插入和删除元素的场景,尤其是在列表的两端进行操作。

1.2 继承关系与实现接口

LinkedList 继承自 AbstractSequentialList,并实现了 ListDequeCloneablejava.io.Serializable 接口。这意味着它既具有列表的特性,又具备双端队列的功能,同时还支持克隆和序列化。

java 复制代码
import java.util.LinkedList;
import java.util.List;
import java.util.Deque;

public class LinkedListOverview {
    public static void main(String[] args) {
        // 创建一个 LinkedList 对象
        LinkedList<String> linkedList = new LinkedList<>();
        // 可以将其赋值给 List 或 Deque 接口类型的变量
        List<String> list = linkedList;
        Deque<String> deque = linkedList;
    }
}

2. 底层数据结构:双向链表

2.1 双向链表的基本概念

双向链表是一种由节点组成的数据结构,每个节点包含三个部分:数据域、指向前一个节点的引用和指向后一个节点的引用。通过这些引用,节点可以相互连接形成一个链状结构。在 LinkedList 中,每个元素都存储在一个节点中,节点之间通过双向引用连接,使得可以从链表的头部或尾部进行遍历。

2.2 LinkedList 中的节点类

LinkedList 中,节点类是一个私有静态内部类,定义如下:

java 复制代码
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;
    }
}
  • item:存储节点的数据。
  • next:指向下一个节点的引用。
  • prev:指向前一个节点的引用。

3. 核心属性

LinkedList 包含几个核心属性,用于管理链表的状态:

java 复制代码
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
  • size:表示链表中元素的数量。
  • first:指向链表的第一个节点。
  • last:指向链表的最后一个节点。

4. 构造方法

4.1 无参构造方法

java 复制代码
public LinkedList() {
}

无参构造方法创建一个空的 LinkedList,此时 firstlast 都为 nullsize 为 0。

4.2 带集合参数的构造方法

java 复制代码
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

该构造方法接受一个集合作为参数,将集合中的元素依次添加到 LinkedList 中。

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class LinkedListConstructors {
    public static void main(String[] args) {
        // 无参构造方法
        LinkedList<String> list1 = new LinkedList<>();

        // 带集合参数的构造方法
        List<String> arrayList = new ArrayList<>();
        arrayList.add("apple");
        arrayList.add("banana");
        LinkedList<String> list2 = new LinkedList<>(arrayList);
        System.out.println(list2);
    }
}

5. 常用操作原理

5.1 添加元素

5.1.1 在链表尾部添加元素

java 复制代码
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++;
    modCount++;
}
  • add(E e) 方法调用 linkLast(E e) 方法将元素添加到链表的尾部。
  • linkLast(E e) 方法创建一个新节点,将其 prev 引用指向原链表的最后一个节点,然后更新 last 引用为新节点。如果原链表为空,则同时更新 first 引用。

5.1.2 在指定位置插入元素

java 复制代码
public void add(int index, E element) {
    checkPositionIndex(index);

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

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++;
}
  • add(int index, E element) 方法首先检查索引的有效性,然后根据索引位置决定是在尾部添加还是在指定节点之前插入。
  • linkBefore(E e, Node<E> succ) 方法创建一个新节点,将其插入到指定节点 succ 之前,并更新相关节点的引用。

5.2 访问元素

java 复制代码
public E get(int index) {
    checkElementIndex(index);
    return node(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;
    }
}
  • get(int index) 方法首先检查索引的有效性,然后调用 node(int index) 方法找到指定位置的节点,并返回该节点的数据。
  • node(int index) 方法根据索引的位置,决定是从链表头部还是尾部开始遍历,以提高查找效率。

5.3 修改元素

java 复制代码
public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
  • set(int index, E element) 方法首先检查索引的有效性,然后找到指定位置的节点,将其数据更新为新元素,并返回旧元素。

5.4 删除元素

5.4.1 删除指定位置的元素

java 复制代码
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

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) 方法首先检查索引的有效性,然后调用 unlink(Node<E> x) 方法删除指定位置的节点。
  • unlink(Node<E> x) 方法更新相关节点的引用,将被删除节点从链表中移除,并返回被删除节点的数据。

5.4.2 删除指定元素

java 复制代码
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;
}
  • remove(Object o) 方法遍历链表,找到第一个与指定元素相等的节点,然后调用 unlink(Node<E> x) 方法将其删除。如果找到并删除了节点,则返回 true,否则返回 false

6. 性能分析

6.1 时间复杂度

  • 插入和删除操作:在链表的头部或尾部插入和删除元素的时间复杂度为 O(1),因为只需要更新相关节点的引用。在指定位置插入和删除元素的平均时间复杂度为 O(n),因为需要遍历链表找到指定位置。
  • 访问操作:访问指定位置的元素的平均时间复杂度为 O(n),因为需要遍历链表找到指定位置。

6.2 空间复杂度

LinkedList 的空间复杂度为 O(n),主要用于存储节点对象和节点之间的引用。

7. 应用场景

7.1 实现栈和队列

由于 LinkedList 实现了 Deque 接口,它可以很方便地实现栈和队列的功能。

java 复制代码
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

public class LinkedListStackQueue {
    public static void main(String[] args) {
        // 实现栈
        Stack<String> stack = new LinkedList<>();
        stack.push("apple");
        stack.push("banana");
        System.out.println(stack.pop());

        // 实现队列
        Queue<String> queue = new LinkedList<>();
        queue.offer("cherry");
        queue.offer("date");
        System.out.println(queue.poll());
    }
}

7.2 需要频繁插入和删除元素的场景

当需要频繁在列表的两端或中间插入和删除元素时,LinkedListArrayList 更具优势。

8. 总结

LinkedList 作为 Java 集合框架中的重要成员,基于双向链表实现,具有灵活的插入和删除操作能力。它适用于需要频繁在列表两端进行操作以及需要实现栈和队列功能的场景。然而,由于其访问元素的时间复杂度较高,在需要频繁随机访问元素的场景下,ArrayList 可能是更好的选择。通过深入理解 LinkedList 的原理和性能特点,我们可以在实际开发中更加合理地选择和使用数据结构。

相关推荐
代码s贝多芬的音符1 小时前
ios android 小程序 蓝牙 CRC16_MODBUS
android·ios·小程序
四谎真好看1 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程2 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t2 小时前
ZIP工具类
java·zip
lang201509282 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan3 小时前
第10章 Maven
java·maven
百锦再3 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说3 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多4 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring
百锦再4 小时前
对前后端分离与前后端不分离(通常指服务端渲染)的架构进行全方位的对比分析
java·开发语言·python·架构·eclipse·php·maven