1. LinkedList 和 ArrayList 的区别是什么?
解答:
LinkedList
和 ArrayList
都实现了 List
接口,但它们的底层实现不同,因此它们的性能特性也有所不同。
-
实现方式:
-
ArrayList
基于动态数组实现,元素是连续存储的,支持通过索引随机访问。 -
LinkedList
基于双向链表实现,每个节点包含一个元素和指向前后节点的指针。
-
-
随机访问性能:
-
ArrayList
提供 O(1) 的随机访问性能,通过索引可以直接访问任何元素。 -
LinkedList
提供 O(n) 的访问性能,需要通过遍历节点来访问指定索引的元素。
-
-
插入和删除性能:
-
ArrayList
在中间插入或删除元素时,需要移动数组中的其他元素,时间复杂度是 O(n)。 -
LinkedList
在插入或删除操作时,只需要修改节点的前后指针,时间复杂度是 O(1),前提是你已经定位到节点。
-
-
内存消耗:
-
ArrayList
的元素是存储在一个连续的内存块中,内存使用较为紧凑。 -
LinkedList
每个元素都包含额外的前后指针(通常是 2 个引用),内存开销较大。
-
ArrayList
和 LinkedList
的源码分析:
java
// LinkedList 的节点类
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;
}
}
private transient Node<E> first; // 链表的头节点
private transient Node<E> last; // 链表的尾节点
LinkedList
使用Node
类来表示链表中的每个节点,节点存储数据以及前后指针,保证了双向链表的操作。
2. LinkedList 中如何进行元素插入和删除?
解答:
LinkedList
的插入和删除操作都基于链表结构,通过调整节点的前后指针来完成。其时间复杂度是 O(1),但前提是你已经定位到操作的节点。
插入操作:
-
在链表的头部插入元素:
addFirst()
或offerFirst()
-
在链表的尾部插入元素:
addLast()
或offerLast()
-
在指定位置插入元素:
add(int index, E element)
删除操作:
-
删除链表头部元素:
removeFirst()
或pollFirst()
-
删除链表尾部元素:
removeLast()
或pollLast()
-
删除指定元素:
remove(Object o)
或remove(int index)
插入和删除的源码分析:
java
// 插入头部
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++;
}
// 删除头部
public E removeFirst() {
final Node<E> f = first;
if (f == null) throw new NoSuchElementException();
final E element = f.item;
final Node<E> next = f.next;
first = next;
if (next == null)
last = null;
else
next.prev = null;
f.item = null;
size--;
modCount++;
return element;
}
-
addFirst()
方法插入新节点到链表头部,removeFirst()
方法从链表头部删除元素。 -
每次插入和删除时,通过调整相邻节点的
next
和prev
指针,确保链表结构的正确性。
3. LinkedList 是否支持快速随机访问?
解答:
LinkedList
不支持快速随机访问。它的元素是通过链表节点连接的,每次访问元素都需要从头节点或尾节点开始遍历链表,直到找到目标节点。
get(int index)
方法源码分析:
java
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private Node<E> node(int 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()
方法首先检查索引是否有效,然后通过node()
方法遍历链表来获取指定位置的节点。 -
由于需要遍历链表,访问的时间复杂度是 O(n),这与
ArrayList
的 O(1) 随机访问不同。
4. LinkedList 的线程安全吗?
解答:
LinkedList
不是线程安全的。在多个线程同时访问和修改同一个 LinkedList
时,可能会导致数据不一致的问题。LinkedList
需要显式的同步机制来保证线程安全。
线程安全实现:
- 如果需要线程安全的链表,可以使用
Collections.synchronizedList()
来包装LinkedList
,或者使用CopyOnWriteArrayList
(适用于读多写少的场景)。
java
List<Integer> synchronizedList = Collections.synchronizedList(new LinkedList<>());
5. LinkedList 中如何查找元素的索引?
解答:
LinkedList
提供了 indexOf()
和 lastIndexOf()
方法来查找元素的索引。
indexOf()
方法源码分析:
java
public int indexOf(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return indexOfNull(x);
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return indexOfNonNull(o, x);
}
}
return -1;
}
indexOf()
方法遍历链表,从头节点开始,依次检查每个节点的值,直到找到匹配的元素。时间复杂度是 O(n),因为需要遍历链表。
6. LinkedList 中的 offerFirst()
和 offerLast()
方法是什么?
解答:
offerFirst()
和 offerLast()
方法是 Deque
接口中的方法,用于在链表的头部和尾部插入元素,分别是 addFirst()
和 addLast()
方法的替代。
-
offerFirst(E e)
:在链表头部插入元素,如果成功返回true
。 -
offerLast(E e)
:在链表尾部插入元素,如果成功返回true
。
offerFirst()
方法源码分析:
java
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
offerFirst()
方法其实是调用addFirst()
来在链表头部插入元素。
7. LinkedList 中的 remove(Object o)
方法如何工作?
解答: remove(Object o)
方法删除链表中第一次出现的指定元素。如果找到了元素,删除它,并调整相邻节点的指针。
remove(Object o)
方法源码分析:
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;
}
private void unlink(Node<E> x) {
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;
if (next == null)
last = prev;
else
next.prev = prev;
x.item = null;
size--;
modCount++;
}
remove(Object o)
方法遍历链表,找到元素后通过unlink()
方法断开该节点与前后节点的链接,从而删除该节点。
8. 如何反转一个 LinkedList?
解答: 可以通过遍历链表并调整每个节点的 next
和 prev
指针来实现链表的反转。
java
public void reverse() {
Node<E> current = first;
Node<E> temp = null;
while (current != null) {
temp = current.prev;
current.prev = current.next;
current.next = temp;
current = current.prev;
}
if (temp != null) {
first = temp.prev;
}
}
- 通过反转每个节点的前后指针,最终完成链表的反转。
总结
-
LinkedList
的性能特点是插入和删除操作(特别是头尾操作)效率较高,但访问元素的效率较低。 -
它使用双向链表实现,每个节点包含数据和指向前后节点的指针。
-
插入和删除操作时间复杂度是 O(1),访问操作(通过索引)时间复杂度是 O(n)。
-
线程不安全,使用时需要注意同步问题。
不积跬步,无以至千里 --- xiaokai