引言
在 Java 编程中,集合框架是处理数据存储和操作的强大工具。LinkedList
作为其中的重要成员,为我们提供了一种灵活的列表实现方式。与 ArrayList
基于数组的实现不同,LinkedList
采用链表结构,这使得它在某些操作上具有独特的优势。本文将深入探讨 LinkedList
的原理,包括其底层数据结构、核心属性、构造方法、常用操作的实现细节,以及性能特点和应用场景。
1. LinkedList
概述
1.1 定义与用途
LinkedList
是 Java 集合框架中 List
接口和 Deque
接口的双向链表实现。它允许存储重复元素,并且可以像列表一样按顺序访问元素,同时也支持双端队列的操作,如在头部和尾部进行高效的插入和删除。LinkedList
适用于需要频繁插入和删除元素的场景,尤其是在列表的两端进行操作。
1.2 继承关系与实现接口
LinkedList
继承自 AbstractSequentialList
,并实现了 List
、Deque
、Cloneable
和 java.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
,此时 first
和 last
都为 null
,size
为 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 需要频繁插入和删除元素的场景
当需要频繁在列表的两端或中间插入和删除元素时,LinkedList
比 ArrayList
更具优势。
8. 总结
LinkedList
作为 Java 集合框架中的重要成员,基于双向链表实现,具有灵活的插入和删除操作能力。它适用于需要频繁在列表两端进行操作以及需要实现栈和队列功能的场景。然而,由于其访问元素的时间复杂度较高,在需要频繁随机访问元素的场景下,ArrayList
可能是更好的选择。通过深入理解 LinkedList
的原理和性能特点,我们可以在实际开发中更加合理地选择和使用数据结构。