先看再點贊,給自己一點思考的時間,思考過後請毫不猶豫微信搜尋【沉默王二】,關注這個長髮飄飄卻靠才華苟且的程式設計師。
本文 GitHub github.com/itwanger 已收錄,裡面還有技術大佬整理的面試題,以及二哥的系列文章。
上一篇入坑了 ArrayList,小夥伴們反響不錯,那這篇就繼續入坑 LinkedList,它倆算是親密無間的兄弟,相愛相殺的那種,不離不棄的那種,介紹了這個就必須介紹那個的那種。
明目張膽地告訴大家一個好訊息,我寫了一份 4 萬多字的 Java 小白手冊,小夥伴們可以在「沉默王二」公眾號後臺回覆「小白」獲取免費下載連結。覺得不錯的話,請隨手轉發給身邊需要的小夥伴,贈人玫瑰,手有餘香哈。
最開始學習 Java 的時候,我還挺納悶的,有了 ArrayList,幹嘛還要 LinkedList 啊,都是 List,不是很多餘嗎?當時真的很傻很天真,不知道有沒有同款小夥伴。搞不懂兩者之間的區別,什麼場景下該用 ArrayList,什麼場景下該用 LinkedList,傻傻分不清楚。那麼這篇文章,可以一腳把這種天真踹走了。
和陣列一樣,LinkedList 也是一種線性資料結構,但它不像陣列一樣在連續的位置上儲存元素,而是通過引用相互連結。
LinkedList 中的每一個元素都可以稱之為節點(Node),每一個節點都包含三個專案:其一是元素本身,其二是指向下一個元素的引用地址,其三是指向上一個元素的引用地址。
Node 是 LinkedList 類的一個私有的靜態內部類,其原始碼如下所示:
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList 看起來就像下面這個樣子:
第一個節點由於沒有前一個節點,所以 prev 為 null;
最後一個節點由於沒有後一個節點,所以 next 為 null;
這是一個雙向連結串列,每一個節點都由三部分組成,前後節點和值。
那可能有些小夥伴就會和當初的我一樣,好奇地問,“為什麼要設計 LinkedList 呢?”如果能給 LinkedList 類的作者打個電話就好了,可惜沒有他的聯絡方式。很遺憾,只能靠我來給大家解釋一下了。
第一,陣列的大小是固定的,即便是 ArrayList 可以自動擴容,但依然會有一定的限制:如果宣告的大小不足,則需要擴容;如果宣告的大小遠遠超出了實際的元素個數,又會造成記憶體的浪費。儘管擴容的演算法已經非常優雅,儘管記憶體已經綽綽有餘。
第二,陣列的元素需要連續的記憶體位置來儲存其值。這就是 ArrayList 進行刪除或者插入元素的時候成本很高的真正原因,因為我們必須移動某些元素為新的元素留出空間,比如說:
現在有一個陣列,10、12、15、20、4、5、100,如果需要在 12 的位置上插入一個值為 99 的元素,就必須得把 12 以後的元素往後移動,為 99 這個元素騰出位置。
刪除是同樣的道理,刪除之後的所有元素都必須往前移動一次。
LinkedList 就擺脫了這種限制:
第一,LinkedList 允許記憶體進行動態分配,這就意味著記憶體分配是由編譯器在執行時完成的,我們無需在 LinkedList 宣告的時候指定大小。
第二,LinkedList 不需要在連續的位置上儲存元素,因為節點可以通過引用指定下一個節點或者前一個節點。也就是說,LinkedList 在插入和刪除元素的時候代價很低,因為不需要移動其他元素,只需要更新前一個節點和後一個節點的引用地址即可。
LinkedList 類的層次結構如下圖所示:
LinkedList 是一個繼承自 AbstractSequentialList 的雙向連結串列,因此它也可以被當作堆疊、佇列或雙端佇列進行操作。
LinkedList 實現了 List 介面,所以能對它進行佇列操作。
LinkedList 實現了 Deque 介面,所以能將 LinkedList 當作雙端佇列使用。
明白了 LinkedList 的一些理論知識後,我們來看一下如何使用 LinkedList。
01、如何建立一個 LinkedList
LinkedList<String> list = new LinkedList<>();
和建立 ArrayList 一樣,可以通過上面的語句來建立一個字串型別的 LinkedList(通過尖括號來限定 LinkedList 中元素的型別,如果嘗試新增其他型別的元素,將會產生編譯錯誤)。
不過,LinkedList 無法在建立的時候像 ArrayList 那樣指定大小。
02、向 LinkedList 中新增一個元素
可以通過 add()
方法向 LinkedList 中新增一個元素:
LinkedList<String> list = new LinkedList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");
感興趣的小夥伴可以研究一下 add()
方法的原始碼,它在新增元素的時候會呼叫 linkLast()
方法。
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
新增第一個元素的時候,last 為 null,建立新的節點(next 和 prev 都為 null),然後再把新的節點賦值給 last 和 first;當新增第二個元素的時候,last 為第一個節點,建立新的節點(next 為 null,prev 為第一個節點),然後把 last 更新為新的節點,first 保持不變,第一個節點的 next 更新為第二個節點;以此類推。
還可以通過 addFirst()
方法將元素新增到第一位;addLast()
方法將元素新增到末尾;add(int index, E element)
方法將元素新增到指定的位置。
03、更新 LinkedList 中的元素
可以使用 set()
方法來更改 LinkedList 中的元素,需要提供下標和新元素。
list.set(0, "沉默王五");
來看一下 set()
方法的原始碼:
public E set(int index, E element) {
checkElementIndex(index);
LinkedList.Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
該方法會先對指定的下標進行檢查,看是否越界,然後根據下標查詢節點:
LinkedList.Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
LinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
LinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
node()
方法會對下標進行一個初步的判斷,如果靠近末端,則從最後開始遍歷,這樣能夠節省不少遍歷的時間,小夥伴們眼睛要睜大點了,這點要學。
找到節點後,再替換新值並返回舊值。
04、刪除 LinkedList 中的元素
可以通過 remove()
方法刪除指定位置上的元素:
list.remove(1);
該方法會呼叫 unlink()
方法對前後節點進行更新。
E unlink(LinkedList.Node<E> x) {
// assert x != null;
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.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;
}
還可以使用 removeFirst()
和 removeLast()
方法刪除第一個節點和最後一個節點。
05、查詢 LinkedList 中的元素
如果要正序查詢一個元素,可以使用 indexOf()
方法;如果要倒序查詢一個元素,可以使用 lastIndexOf()
方法。
來看一下 indexOf()
方法的原始碼:
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (LinkedList.Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (LinkedList.Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
基本上和 ArrayList 的大差不差,都需要遍歷,如果要查詢的元素為 null,則使用“==”操作符,可以避免丟擲空指標異常;否則使用 equals()
方法進行比較。
另外,getFirst()
方法用於獲取第一個元素;getLast()
方法用於獲取最後一個元素;poll()
和 pollFirst()
方法用於刪除並返回第一個元素(兩個方法儘管名字不同,但方法體是完全相同的);pollLast()
方法用於刪除並返回最後一個元素;peekFirst()
方法用於返回但不刪除第一個元素。
06、最後
如果要我們自己實現一個連結串列的話,上面這些增刪改查的輪子方法是一定要白嫖啊,不對,一定要借鑑啊。
上一篇 ArrayList 中提到過,隨機訪問一個元素的時間複雜度為 O(1),但 LinkedList 要複雜一些,因為資料增大多少倍,耗時就增大多少倍,因為要迴圈遍歷,所以時間複雜度為 O(n)。
至於 LinkedList 在插入、新增、刪除元素的時候有沒有比 ArrayList 更快,這要取決於資料量的大小,以及元素所在的位置。不過,從理論上來說,由於不需要移動陣列,應該會更快一些。但到底快不快,下一篇帶來答案,小夥伴們敬請期待。
我是沉默王二,一枚有顏值卻靠才華苟且的程式設計師。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,奧利給。
注:如果文章有任何問題,歡迎毫不留情地指正。
如果你覺得文章對你有些幫助歡迎微信搜尋「沉默王二」第一時間閱讀,回覆「小白」更有我肝了 4 萬+字的 Java 小白手冊 2.0 版,本文 GitHub github.com/itwanger 已收錄,歡迎 star。