0x00 描述
LinkedList
是一個雙向連結串列,這是一個基礎的資料結構。開啟 LinkedList
原始碼,可以看到它繼承於 AbstractSequentialList
,這個是 AbstractList
的子類。同時也實現了 List
、Deque
、Clone
、Serializable
介面。所以簡化的類關係圖可以表示為
關鍵屬性
size
記錄當前陣列元素的個數first
連結串列頭指標last
連結串列尾部指標modCount
記錄修改次數,這個欄位是繼承於AbstractList
LinkedList
是實現了序列化介面 Serializable
,而以上屬性都被宣告為 transient
表示這些欄位不參與序列化。
節點
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);
}
複製程式碼
這是通過列表來建立連結串列的。它呼叫了 addAll
方法。這個方法後文會講到。
0x01 常用方法
addFirst(E e)
在連結串列頭部新增節點
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++;
}
複製程式碼
它實際是呼叫了內部的一個私有方法 linkFirst
。只需要改變指標指向,時間複雜度O(1)。
addLast(E e)
public void addLast(E e) {
linkLast(e);
}
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++;
}
複製程式碼
在連結串列尾部新增一個節點。它也是內部的 linkLast
方法。這方法執行效率也很高,只需要改變指標指向,時間複雜度是O(1)。
add(E e)
public boolean add(E e) {
linkLast(e);
return true;
}
複製程式碼
可以看出也是呼叫了 linkLast
方法。
add(int index, E element)
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
複製程式碼
在某個 index
前插入元素。
首先它會檢查 index
是否正確。如果在 0~size 範圍內的下標,那麼就執行插入的方法;
它會判斷如果 index
是等於 size
那麼就在尾部插入元素,否則就在 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++;
}
複製程式碼
這個方法在節點 succ
前面新增元素,時間複雜度為O(1)。
在呼叫這個方法之前需要獲取到節點
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {//size >> 1 相當於 size/2
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
是在前半部分時從連結串列頭部開始遍歷;如果 index
超過當前連結串列的一半時則從後面開始遍歷查詢,它的時間複雜度為O(n)。
addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
複製程式碼
在尾部插入一個列表,通過呼叫 add(int,Collection)
來實現。
addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) {
//先檢測 index 是否有效
checkPositionIndex(index);
//以陣列的形式獲取到列表資料
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//找到index的前向指標,後向指標
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//依次把陣列中的節點插入到列表中
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
//連結後向指標
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//更新size和modCount
size += numNew;
modCount++;
return true;
}
複製程式碼
這個方法稍微複雜一點
- 先檢測
index
是否有效 - 以陣列的形式獲取到列表資料
- 找到
index
所在節點的前向指標,後向指標 - 依次把陣列中的節點插入到列表中
- 連結後向指標的資料
- 更新
size
和modCount
get(int index)
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
複製程式碼
獲取 index
所在元素,通過 node
方法獲取。前面分析可以知道,這個方法需要遍歷,它的時間複雜度是O(n)。
contains(Object o)
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
複製程式碼
查詢某個物件是否存在於該連結串列中是通過遍歷來實現的。
peek()
檢視連結串列頭節點
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
複製程式碼
peekFirst()
檢視連結串列頭節點
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
複製程式碼
peekLast()
檢視連結串列尾部節點
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
複製程式碼
poll()
獲取頭節點,並把頭節點從連結串列中刪除
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
複製程式碼
pollFirst()
同上
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
複製程式碼
pollLast()
獲取尾部節點,並將尾部節點刪除
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
複製程式碼
remove()
刪除頭節點
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
複製程式碼
clear()
清空連結串列
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
複製程式碼
遍歷整個連結串列,將節點中的資料置為 null
。
0x02 總結
LinkedList
是一個雙向連結串列,它是執行緒不安全的。LinkedList
擅長插入、刪除操作,時間複雜度是O(1);但是如果事先不知道被插入的節點,則需要通過遍歷來查詢到該節點,而查詢操作就不是很高效了,時間複雜度是O(n)。get
方法需要遍歷獲得,containts
方法也需要遍歷- 在連結串列頭部或尾部插入節點效率要高,但是通過下標
index
插入節點則需要遍歷找到插入的位置,再執行插入操作。