我是一名很普通的大三學生。我將堅持寫博客,輸出知識的同時鞏固自己的基礎,記錄自己的成長和鍛鍊自己,奧利給!!
如果你覺得內容對你有幫助的話,不如給個贊鼓勵一下更新?(σ゚∀゚)σ…:*☆哎喲不錯哦
LinkedList 適用於集合元素先入先出和先入後出的場景,在隊列中被頻繁使用。下面我們就來簡單瞭解一下它,並看看它跟常用的ArrayList的區別。
LinkedList結構分析
LinkedList 底層數據結構是一個雙向鏈表,整體結構如下圖所示:
我們看一下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;
}
}
從上圖和Node節點的源碼中我們可以看出,鏈表中的每個節點都可以向前或者向後遍歷,我們有幾個概念如下:
- 鏈表每個節點我們叫做 Node,prev 屬性,代表前一個節點的位置,next 屬性,代表後一個節點的位置;
- first 是雙向鏈表的頭節點,它的前一個節點是 null。
- last 是雙向鏈表的尾節點,它的後一個節點是 null;
- 當鏈表中沒有數據時,first 和 last 是同一個節點,前後指向都是 null;
- 鏈表理論上是沒有大小限制的。
雖然在添加元素的時候沒有“是否超過最大容量”這種判斷,但是 LinkedList 實際大小用的是 int 類型,這也說明了LinkedList 不能超過 Integer 的最大值,不然會溢出。
從底層結構上看,我們可以看出LinkedList
和ArrayList
是完全不一樣的,一個是數組實現,一個是鏈表實現。
新增、刪除、查詢操作
追加(新增)
追加節點時,我們可以選擇追加到鏈表頭部,還是追加到鏈表尾部,add
方法默認是從尾部開始追加,addFirst
方法見名知其意,就是從頭部開始追加,我們分別來看下兩種不同的追加方式:
// 從尾部開始追加節點
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++;
}
// 從頭部追加
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 指向。都只需要簡單地把指向位置修改下即可,就偷懶不畫圖了~
刪除
同理,刪除也有從頭部刪除和尾部刪除,且刪除操作會把節點的值,前後指向節點都置爲 null,幫助 GC 進行回收。
這裏就不一一例舉了,就看下從尾部刪除的吧,頭部刪除其實差不多。
private E unlinkLast(Node<E> l) {
// l在這裏就是last節點
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 // 否則把前節點的下一個節點,指向null。
prev.next = null;
size--;
modCount++;
return element;
}
到這裏,我們已經可以看出,鏈表結構的節點新增、刪除都非常簡單,僅僅把前後節點的指向修改下,並不像ArrayList
可能涉及到數組的複製,這也是爲什麼說LinkedList
適合頻繁的增刪。
查詢
我們都知道,其實鏈表查詢某一個節點是比較慢的,因爲它需要挨個循環查找纔行,我們看看 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是在鏈表的前半部分,還是後半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環的次數至少降低了一半,提高了查找的性能,這種思想值得我們借鑑。
注意:LinkedList 實現了 Queue 接口,在新增、刪除、查詢等方面增加了很多新的方法,這些方法在平時特別容易混淆,在鏈表爲空的情況下,返回值也不太一樣,我們列一個表格,方便大家記錄:
方法含義 | 返回異常 | 返回特殊值 | 底層實現 |
---|---|---|---|
新增 | add(e) | offer(e) | 底層實現相同 |
刪除 | remove() | poll(e) | 鏈表爲空時,remove 會拋出異常,poll 返回 null |
查找 | element() | peek() | 鏈表爲空時,element 會拋出異常,peek 返回 null |
迭代器
LinkedList的迭代器並不是我們常使用的Iterator 接口,因爲要實現雙向的迭代訪問,而Iterator 只支持從頭到尾的訪問。Java 新增了一個迭代接口繼承Iterator,叫做:ListIterator
,這個接口提供了向前和向後的迭代方法,如下所示:
迭代順序 | 方法 |
---|---|
從尾到頭迭代方法 | hasPrevious、previous、previousIndex |
從頭到尾迭代方法 | hasNext、next、nextIndex |
這裏同樣在循環刪除元素時,也推薦通過迭代器進行刪除
總結
LinkedList適用於要求有順序、並且會按照順序進行迭代的場景,主要是依賴於底層的鏈表結構。
看到這,思考下如下問題:
- ArrayList 和 LinkedList 有何不同?
- ArrayList 和 LinkedList 應用場景有何不同?
- 你能描述下雙向鏈表麼,以及它的新增和刪除?