現在由大惡人付有傑來從增刪改查幾個角度輕度解析LinkedList的源碼
1.整體架構
LinkedList 底層數據結構是一個雙向鏈表(),整體結構如下圖所示:
鏈表中的每個節點都可以向前或者向後追溯,我們有幾個概念如下:
- 鏈表每個節點我們叫做 Node,Node 有 prev 屬性,代表前一個節點的位置,next 屬性,代表後一個節點的位置;
- first 是雙向鏈表的頭節點,它的前一個節點是 null。
- last 是雙向鏈表的尾節點,它的後一個節點是 null;
- 當鏈表中沒有數據時,first 和 last 是同一個節點,前後指向都是 null;
- 因爲是個雙向鏈表,只要機器內存足夠強大,是沒有大小限制的。
鏈表中的元素叫做 Node,我們看下 Node 的組成部分:
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;
}
}
添加
鏈表的添加方式有很多,就說最常用的。add
public boolean add(E e) {
linkLast(e);
return true;
}
// 從尾部開始追加節點
void linkLast(E e) {
// 把尾節點數據暫存
final Node<E> l = last;
// 新建新的節點,初始化入參含義:
// l 是新節點的前一個節點,當前值是尾節點值
// e 表示當前新增節點,當前新增節點後一個節點是 null
final Node<E> newNode = new Node<>(l, e, null);
// 新建節點追加到尾部
last = newNode;
//如果鏈表爲空(l 是尾節點,尾節點爲空,鏈表即空),頭部和尾部是同一個節點,都是新建的節點
if (l == null)
first = newNode;
//否則把前尾節點的下一個節點,指向當前尾節點。
else
l.next = newNode;
//大小和版本更改
size++;
modCount++;
}
從源碼上來看,尾部追加節點比較簡單,只需要簡單地把尾巴引用指向位置修改下即可.(會畫圖的人畫圖發博客,不會畫圖的人打字發博客)
當然還有addFirst
此方法用於在頭部添加數據:
public void addFirst(E e) {
linkFirst(e);
}
// 從頭部追加
private void linkFirst(E e) {
// 頭節點賦值給臨時變量
final Node<E> f = first;
// 新建節點,前一個節點指向null,e 是新建節點,f 是新建節點的下一個節點,目前值是頭節點的值
final Node<E> newNode = new Node<>(null, e, f);
// 新建節點成爲頭節點
first = newNode;
// 頭節點爲空,就是鏈表爲空,頭尾節點是一個節點
if (f == null)
last = newNode;
//上一個頭節點的前一個節點指向當前節點
else
f.prev = newNode;
size++;
modCount++;
}
頭部追加節點和尾部追加節點非常類似,只是前者是移動頭節點的 prev 指向,後者是移動尾節點的 next 指向。
節點刪除
在看節點刪除之前,我問你,你知道了這個雙鏈表點節點,你會如何刪除它?
我就畫個圖給你看看:
假設要刪除的是第二個節點
**第一步:**修改該節點的前驅節點的next指向自己的next的指向
代碼表示
node2.pre.next = node2.next;
第二步修改自己後面的節點的pre指針指向自己的前一個結點
代碼表示
node2.next.pre = node2.pre;
第三步:將自己的pre,next全部清空
代碼表示
node2.next = null;
node2.pre = null;
節點查詢
因爲鏈表不可以像數組那麼樣按照索引訪問 元素,鏈表查詢某一個節點是比較慢的,需要挨個循環查找才行,我們看看 LinkedList 的源碼是如何尋找節點的:
// 根據鏈表索引位置查詢節點
Node<E> node(int index) {
// 如果 index 處於隊列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
if (index < (size >> 1)) {
Node<E> x = first;
// 直到 for 循環到 index 的前一個 node 停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果 index 處於隊列的後半部分,從尾開始找
Node<E> x = last;
// 直到 for 循環到 index 的後一個 node 停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
從源碼中我們可以發現,LinkedList 並沒有採用從頭循環到尾的做法,而是採取了簡單二分法,首先看看 index 是在鏈表的前半部分,還是後半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環的次數至少降低了一半,提高了查找的性能,我只能說,寫的不錯!!!。
其實鏈表就將每個節點通過前後指針連接起來,也就沒有那麼什麼擴容的麻煩啦。如果要按照索引的位置添加元素,就必須遍歷鏈表的同時index++,然後確定元素,直接斷開連接,新增就可以了,不用移動元素那麼麻煩。所以呢鏈表添加刪除的時候還是很簡便的。
我畫個圖吧:
代碼展示就是
//找到符合位置的節點 要添加的節點
add(Node node,Node e){
//獲取前面的節點
Node pre = node.pre;
//後面的節點
Node next = node.next;
e.pre = node.pre;
//我在在你的位置 上插入,自然我的下一個節點是你
e.next = node;
node.pre.next = e;
node.pre = null;
node.next = null;
}
迭代器
因爲 LinkedList 要實現雙向的迭代訪問,所以我們使用 Iterator 接口肯定不行了,因爲 Iterator 只支持從頭到尾的訪問。Java 新增了一個迭代接口,叫做:ListIterator,這個接口提供了向前和向後的迭代方法,如下所示:
// 雙向迭代器
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;//上一次執行 next() 或者 previos() 方法時的節點位置
private Node<E> next;//下一個節點
private int nextIndex;//下一個節點的位置
//expectedModCount:期望版本號;modCount:目前最新版本號
private int expectedModCount = modCount;
…………
}
我們先來看下從頭到尾方向的迭代:
// 判斷還有沒有下一個元素
public boolean hasNext() {
return nextIndex < size;// 下一個節點的索引小於鏈表的大小,就有
}
// 取下一個元素
public E next() {
//檢查期望版本號有無發生變化
checkForComodification();
if (!hasNext())//再次檢查
throw new NoSuchElementException();
// next 是當前節點,在上一次執行 next() 方法時被賦值的。
// 第一次執行時,是在初始化迭代器的時候,next 被賦值的
lastReturned = next;
// next 是下一個節點了,爲下次迭代做準備
next = next.next;
nextIndex++;
return lastReturned.item;
}
// 如果上次節點索引位置大於 0,就還有節點可以迭代
public boolean hasPrevious() {
return nextIndex > 0;
}
// 取前一個節點
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 爲空場景:1:說明是第一次迭代,取尾節點(last);2:上一次操作把尾節點刪除掉了
// next 不爲空場景:說明已經發生過迭代了,直接取前一個節點即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
// 索引位置變化
nextIndex--;
return lastReturned.item;
}
鏈表就比數組簡單多了。