Java ArrayList 與 LinkedList

信碼由韁發表於2021-10-30

【注】本文譯自: Java ArrayList vs LinkedList | Baeldung

1. 概述

對於 collections (集合),Java 標準庫提供了大量可供選擇的選項。在這些選項中,有兩個著名的 List 實現,稱為 ArrayListLinkedList,每個實現都有自己的屬性和用例。
在本教程中,我們將看到這兩者是如何實現的。然後,我們將為評估每個應用的不同。

2. ArrayList

在內部,ArrayList 使用陣列來實現 List 介面。由於陣列在 Java 中是固定大小的,因此 ArrayList 建立一個具有一些初始容量的陣列。在此過程中,如果我們需要儲存比預設容量更多的項,它將用一個新的、更大的陣列替換該陣列。
為了更好地理解它的屬性,讓我們根據它的三個主要操作來評估這個資料結構:新增項、通過索引獲取項和通過索引刪除項。

2.1. 新增

當我們建立一個空的 ArrayList 時,它會使用預設容量(當前為 10)初始化其後備陣列:

在該陣列尚未滿時新增新專案就像將該項分配給特定陣列索引一樣簡單。這個陣列索引由當前陣列大小決定,因為我們實際上是附加到列表中:

backingArray[size] = newItem;
size++;

因此,在最佳和一般情況下,加法操作的時間複雜度為 O(1),這非常快。但是,隨著後備陣列變滿,新增實現的效率會降低:

要新增新專案,我們應該首先初始化一個容量更大的全新陣列,並將所有現有專案複製到新陣列中。只有在複製當前元素後,我們才能新增新專案。因此,在最壞的情況下時間複雜度為 O(n),因為我們必須先複製 n 個元素。
從理論上講,新增新元素的執行時間為攤銷常數。也就是說,新增 n 個元素需要 O(n) 時間。但是,由於複製開銷,某些單次新增可能表現不佳。

2.2. 按索引訪問

通過索引訪問項是 ArrayList 的真正亮點。要檢索下標為 i 的項,我們只需要返回位於後備陣列中第 i 個下標的項。因此,通過索引操作訪問的時間複雜度始終為 O(1)

2.3. 通過索引刪除

假設我們要從 ArrayList 中刪除索引 6,它對應於我們的後備陣列中的元素 15:

將所需元素標記為已刪除後,我們應該將其後的所有元素向後移動一個索引。顯然,元素越靠近陣列的開頭,我們應該移動的元素就越多。因此,時間複雜度在最佳情況下為 O(1),在平均和最壞情況下為 O(n)。

2.4. 應用和限制

.通常,當需要 List 實現時,ArrayList 是許多開發人員的預設選擇。事實上,當讀取次數遠遠超過寫入次數時,這實際上是一個明智的選擇
有時我們需要同樣頻繁的讀取和寫入。如果我們確實估計了可能專案的最大數量,那麼使用 ArrayList 仍然有意義。如果是這種情況,我們可以使用初始容量初始化 ArrayList:

int possibleUpperBound = 10_000;
List<String> items = new ArrayList<>(possibleUpperBound);

這種估計可以防止大量不必要的複製和陣列分配。

此外,陣列由 Java 中的 int 值索引。因此,在 Java 陣列中儲存超過 2 的 32 次方個元素是不可能的,因此,在 ArrayList 中也是如此

3. LinkedList

LinkedList,顧名思義,使用連結節點的集合來儲存和檢索元素。例如,以下是新增四個元素後的 Java 實現:

每個節點維護兩個指標:一個指向下一個元素,另一個指向前一個元素。對此進行擴充套件,雙向連結串列有兩個指向第一項和最後一項的指標
同樣,讓我們根據相同的基本操作來評估這個實現。

3.1. 新增

為了新增新節點,首先,我們應該將當前最後一個節點連結到新節點:

然後更新最後一個指標:

由於這兩個操作都很簡單,因此加法操作的時間複雜度始終為 O(1)

3.2. 通過索引訪問

LinkedList 與 ArrayList 不同,不支援快速隨機訪問。因此,為了按索引查詢元素,我們應該手動遍歷列表的某些部分

在最好的情況下,當請求的專案接近列表的開頭或結尾時,時間複雜度將與 O(1) 一樣快。然而,在平均和最壞情況下,我們可能會以 O(n) 的訪問時間結束,因為我們必須一個接一個地檢查許多節點。

3.3. 通過索引刪除

為了刪除一項,我們應該首先找到請求的項,然後從列表中取消它的連結。因此,訪問時間決定了時間複雜度——即在最佳情況下為 O(1),在平均和最壞情況下為 O(n)。

3.4. 應用

當新增率遠高於讀取率時,LinkedLists更適合。
此外,當大多數時候我們想要第一個或最後一個元素時,它可以用於讀取密集的場景。值得一提的是,LinkedList 還實現了 Deque 介面——支援對集合兩端的高效訪問。
通常,如果我們知道它們的實現差異,那麼我們可以輕鬆地為特定用例選擇一個。
例如,假設我們將在類似列表的資料結構中儲存大量時間序列事件。我們知道我們每秒都會收到突發事件。
此外,我們需要定期檢查所有事件並提供一些統計資料。對於此用例,LinkedList 是更好的選擇,因為新增速率遠高於讀取速率。
此外,我們會讀取所有專案,因此我們無法超過 O(n) 上限。

4. 結論

在本教程中,我們首先深入研究了 ArrayListLinkLists 如何在 Java 中實現。
我們還評估了其中每一個的不同用例。

相關文章