1. 程式人生 > 實用技巧 >集合系列 List之LinkedList分析

集合系列 List之LinkedList分析

集合系列 List之LinkedList分析

LinkedList是連結串列的經典實現,其底層採用連結串列節點的方式實現。

從類繼承結構圖可以看到,LinkedList不僅實現了List介面,還實現了Deque雙向佇列介面。

原理

為了深入理解LinkedList的原理,我們將從類成員變數,構造方法,核心方法逐一介紹。

類成員變數

  //連結串列大小
transient int size = 0;
//首節點
transient Node<E> first;
//尾結點
transient Node<E> last;
//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;
}
}

其採用了連結串列節點的方式實現,並且每個節點都有前驅和後繼節點。

構造方法

LinkedList總共有2個構造方法:

  
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

核心方法

在linkedList中最為核心的是查詢,插入,刪除,擴容這個幾個方法。

查詢

linkedList底層基於連結串列結構,無法向ArrayList那樣隨機訪問指定位置的元素。linkedList查詢過程要稍微麻煩一些,需要從連結串列頭節點(或尾節點)向後查詢,時間複雜度為O(N)。

  
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;
//迴圈向後查詢,直至i == index
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;
}
}

上面的程式碼比較簡單,主要通過遍歷的方式定位目標位置的節點。獲取到節點後,取出節點儲存的值返回即可。這裡有個小優化,即通過比較index與節點數量size/2的大小,決定頭結點還是尾節點進行查詢。

插入

LinkedList除了實現了List介面相關方法·,還實現·了Deque介面的很多方法,例如:addFirst,addLast,offerFirst,offerLast等。但這些方法的實現思路大致都是一樣的,所以這裡來看看add方法的實現。

add有兩個方法,一個是直接插入隊尾,一個是插入指定位置。

直接插入隊尾

  
public boolean add(E e) {
linkLast(e);
return true;
}

可以看到其直接呼叫了 linkLast 方法,其實它就是 Deque 介面的一個方法。

  
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++;
}

上述程式碼進行了節點的建立以及引用的變化,最後增加連結串列的大小。

插入指定位置

  
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
//直接插入隊尾
linkLast(element);
else
//插入指定位置
linkBefore(element, node(index));
}

如果我們插入的位置還是連結串列尾部,那麼還是會呼叫 linkLast 方法。否則呼叫 node 方法取出插入位置的節點,否則呼叫 linkBefore 方法插入。

  
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++;
}

上述程式碼進行了節點的建立以及引用的變化,最後增加連結串列的大小。

刪除

刪除節點有兩個方法,第一個是移除特定的元素,第二個是移除某個位置的元素。

先看第一個刪除方法:移除特定的元素。

  
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;
}

大致思路:遍歷找到刪除的節點,之後呼叫unlink()方法解除引用。

  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;
}

unlink()程式碼裡就是做了一系列的引用修改操作。

總結

經過上面的分析,我們可以知道LinkedList有如下特點:

  • 底層基於連結串列實現,增刪速度快,讀取速度慢

  • 非執行緒安全。

  • 與 ArrayList 不同,LinkedList 沒有容量限制,所以也沒有擴容機制。

更多資料文章,公號《Java路》