問題
(1)LinkedList只是一個List嗎?
(2)LinkedList還有其它什麼特性嗎?
(3)LinkedList爲啥經常拿出來跟ArrayList比較?
(4)我爲什麼把LinkedList放在最後一章來講?
簡介
LinkedList是一個以雙向鏈表實現的List,它除了作爲List使用,還可以作爲隊列或者棧來使用,它是怎麼實現的呢?讓我們一起來學習吧。
繼承體系
通過繼承體系,我們可以看到LinkedList不僅實現了List接口,還實現了Queue和Deque接口,所以它既能作爲List使用,也能作爲雙端隊列使用,當然也可以作爲棧使用。
源碼分析
主要屬性
// 元素個數
transient int size = 0;
// 鏈表首節點
transient Node<E> first;
// 鏈表尾節點
transient Node<E> last;
屬性很簡單,定義了元素個數size和鏈表的首尾節點。
主要內部類
典型的雙鏈表結構。
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;
}
}
主要構造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
兩個構造方法也很簡單,可以看出是一個無界的隊列。
添加元素
作爲一個雙端隊列,添加元素主要有兩種,一種是在隊列尾部添加元素,一種是在隊列首部添加元素,這兩種形式在LinkedList中主要是通過下面兩個方法來實現的。
// 從隊列首添加元素
private void linkFirst(E e) {
// 首節點
final Node<E> f = first;
// 創建新節點,新節點的next是首節點
final Node<E> newNode = new Node<>(null, e, f);
// 讓新節點作爲新的首節點
first = newNode;
// 判斷是不是第一個添加的元素
// 如果是就把last也置爲新節點
// 否則把原首節點的prev指針置爲新節點
if (f == null)
last = newNode;
else
f.prev = newNode;
// 元素個數加1
size++;
// 修改次數加1,說明這是一個支持fail-fast的集合
modCount++;
}
// 從隊列尾添加元素
void linkLast(E e) {
// 隊列尾節點
final Node<E> l = last;
// 創建新節點,新節點的prev是尾節點
final Node<E> newNode = new Node<>(l, e, null);
// 讓新節點成爲新的尾節點
last = newNode;
// 判斷是不是第一個添加的元素
// 如果是就把first也置爲新節點
// 否則把原尾節點的next指針置爲新節點
if (l == null)
first = newNode;
else
l.next = newNode;
// 元素個數加1
size++;
// 修改次數加1
modCount++;
}
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
// 作爲無界隊列,添加元素總是會成功的
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
典型的雙鏈表在首尾添加元素的方法,代碼比較簡單,這裏不作詳細描述了。
上面是作爲雙端隊列來看,它的添加元素分爲首尾添加元素,那麼,作爲List呢?
作爲List,是要支持在中間添加元素的,主要是通過下面這個方法實現的。
// 在節點succ之前添加元素
void linkBefore(E e, Node<E> succ) {
// succ是待添加節點的後繼節點
// 找到待添加節點的前置節點
final Node<E> pred = succ.prev;
// 在其前置節點和後繼節點之間創建一個新節點
final Node<E> newNode = new Node<>(pred, e, succ);
// 修改後繼節點的前置指針指向新節點
succ.prev = newNode;
// 判斷前置節點是否爲空
// 如果爲空,說明是第一個添加的元素,修改first指針
// 否則修改前置節點的next爲新節點
if (pred == null)
first = newNode;
else
pred.next = newNode;
// 修改元素個數
size++;
// 修改次數加1
modCount++;
}
// 尋找index位置的節點
Node<E> node(int index) {
// 因爲是雙鏈表
// 所以根據index是在前半段還是後半段決定從前遍歷還是從後遍歷
// 這樣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;
}
}
// 在指定index位置處添加元素
public void add(int index, E element) {
// 判斷是否越界
checkPositionIndex(index);
// 如果index是在隊列尾節點之後的一個位置
// 把新節點直接添加到尾節點之後
// 否則調用linkBefore()方法在中間添加節點
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
在中間添加元素的方法也很簡單,典型的雙鏈表在中間添加元素的方法。
添加元素的三種方式大致如下圖所示:
在隊列首尾添加元素很高效,時間複雜度爲O(1)。
在中間添加元素比較低效,首先要先找到插入位置的節點,再修改前後節點的指針,時間複雜度爲O(n)。
刪除元素
作爲雙端隊列,刪除元素也有兩種方式,一種是隊列首刪除元素,一種是隊列尾刪除元素。
作爲List,又要支持中間刪除元素,所以刪除元素一個有三個方法,分別如下。
// 刪除首節點
private E unlinkFirst(Node<E> f) {
// 首節點的元素值
final E element = f.item;
// 首節點的next指針
final Node<E> next = f.next;
// 添加首節點的內容,協助GC
f.item = null;
f.next = null; // help GC
// 把首節點的next作爲新的首節點
first = next;
// 如果只有一個元素,刪除了,把last也置爲空
// 否則把next的前置指針置爲空
if (next == null)
last = null;
else
next.prev = null;
// 元素個數減1
size--;
// 修改次數加1
modCount++;
// 返回刪除的元素
return element;
}
// 刪除尾節點
private E unlinkLast(Node<E> l) {
// 尾節點的元素值
final E element = l.item;
// 尾節點的前置指針
final Node<E> prev = l.prev;
// 清空尾節點的內容,協助GC
l.item = null;
l.prev = null; // help GC
// 讓前置節點成爲新的尾節點
last = prev;
// 如果只有一個元素,刪除了把first置爲空
// 否則把前置節點的next置爲空
if (prev == null)
first = null;
else
prev.next = null;
// 元素個數減1
size--;
// 修改次數加1
modCount++;
// 返回刪除的元素
return element;
}
// 刪除指定節點x
E unlink(Node<E> x) {
// x的元素值
final E element = x.item;
// x的前置節點
final Node<E> next = x.next;
// x的後置節點
final Node<E> prev = x.prev;
// 如果前置節點爲空
// 說明是首節點,讓first指向x的後置節點
// 否則修改前置節點的next爲x的後置節點
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 如果後置節點爲空
// 說明是尾節點,讓last指向x的前置節點
// 否則修改後置節點的prev爲x的前置節點
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 清空x的元素值,協助GC
x.item = null;
// 元素個數減1
size--;
// 修改次數加1
modCount++;
// 返回刪除的元素
return element;
}
// remove的時候如果沒有元素拋出異常
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// remove的時候如果沒有元素拋出異常
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
// poll的時候如果沒有元素返回null
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// poll的時候如果沒有元素返回null
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
// 刪除中間節點
public E remove(int index) {
// 檢查是否越界
checkElementIndex(index);
// 刪除指定index位置的節點
return unlink(node(index));
}
刪除元素的三種方法都是典型的雙鏈表刪除元素的方法,大致流程如下圖所示。
在隊列首尾刪除元素很高效,時間複雜度爲O(1)。
在中間刪除元素比較低效,首先要找到刪除位置的節點,再修改前後指針,時間複雜度爲O(n)。
棧
前面我們說了,LinkedList是雙端隊列,還記得雙端隊列可以作爲棧使用嗎?
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
棧的特性是LIFO(Last In First Out),所以作爲棧使用也很簡單,添加刪除元素都只操作隊列首節點即可。
總結
(1)LinkedList是一個以雙鏈表實現的List;
(2)LinkedList還是一個雙端隊列,具有隊列、雙端隊列、棧的特性;
(3)LinkedList在隊列首尾添加、刪除元素非常高效,時間複雜度爲O(1);
(4)LinkedList在中間添加、刪除元素比較低效,時間複雜度爲O(n);
(5)LinkedList不支持隨機訪問,所以訪問非隊列首尾的元素比較低效;
(6)LinkedList在功能上等於ArrayList + ArrayDeque;
彩蛋
java集合部分的源碼分析全部完結,整個專題以ArrayList開頭,以LinkedList結尾,我覺得非常合適,因爲ArrayList代表了List的典型實現,LInkedList代表了Deque的典型實現,同時LinkedList也實現了List,通過這兩個類一首一尾正好可以把整個集合貫穿起來。