數組和鏈表是程序中經常使用的兩種數據結構,也是面試中常考的面試題之一。然而對於不少人來講,只是模糊的記得兩者的區別,可能還記得不必定對,而且每次到了面試的時候,都得把這些的概念拿出來背一遍才行,未免有些麻煩。而本文則會從執行過程圖以及性能評測等方面入手,讓你更加深刻的理解和記憶兩者的區別,有了此次深刻的學習以後,相信會讓你記憶深入。java
在開始(性能評測)以前咱們先來回顧一下,什麼是數組?面試
數組的定義以下:算法
數組(Array)是由相同類型的元素(element)的集合所組成的數據結構,分配一塊連續的內存來存儲。利用元素的索引(index)能夠計算出該元素對應的存儲地址。最簡單的數據結構類型是一維數組。例如,索引爲 0 到 9 的 32 位整數數組,可做爲在存儲器地址 2000,2004,2008,...2036 中,存儲 10個 變量,所以索引爲 i 的元素即在存儲器中的 2000+4×i 地址。數組第一個元素的存儲器地址稱爲第一地址或基礎地址。數組
簡單來講,數組就是由一塊連續的內存組成的數據結構。這個概念中有一個關鍵詞「連續」,它反映了數組的一大特色,就是它必須是由一個連續的內存組成的。數據結構
數組的數據結構,以下圖所示:框架
數組添加的過程,以下圖所示:性能
數組的「連續」特徵決定了它的訪問速度很快,由於它是連續存儲的,因此這就決定了它的存儲位置就是固定的,所以它的訪問速度就很快。好比如今有 10 個房間是按照年齡順序入住的,當咱們知道第一房子住的是 20 歲的人以後,那麼咱們就知道了第二個房子是 21 歲的人,第五個房子是 24 歲的人......等等。學習
禍兮福所倚,福兮禍所伏。數組的連續性既有優勢又有缺點,優勢上面已經說了,而缺點它對內存的要求比較高,必需要找到一塊連續的內存才行。測試
數組的另外一個缺點就是插入和刪除的效率比較慢,假如咱們在數組的非尾部插入或刪除一個數據,那麼就要移動以後的全部數據,這就會帶來必定的性能開銷,刪除的過程以下圖所示:
數組還有一個缺點,它的大小固定,不能動態拓展。ui
鏈表是和數組互補的一種數據結構,它的定義以下:
鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,可是並不會按線性的順序存儲數據,而是在每個節點裏存到下一個節點的指針(Pointer)。因爲沒必要須按順序存儲,鏈表在插入的時候能夠達到 O(1) 的複雜度,比另外一種線性表順序錶快得多,可是查找一個節點或者訪問特定編號的節點則須要 O(n) 的時間,而順序表相應的時間複雜度分別是 O(logn) 和 O(1)。
也就說鏈表是一個無需連續內存存儲的數據結構,鏈表的元素有兩個屬性,一個是元素的值,另外一個是指針,此指針標記了下一個元素的地址。
鏈表的數據結構,以下圖所示:
鏈表添加的過程,以下圖所示:
鏈表刪除的過程,以下圖所示:
鏈表主要分爲如下幾類:
單向鏈表中包含兩個域,一個信息域和一個指針域。這個連接指向列表中的下一個節點,而最後一個節點則指向一個空值,咱們上面所展現的鏈表就是單向鏈表。
雙向鏈表也叫雙鏈表,雙向鏈表中不只有指向後一個節點的指針,還有指向前一個節點的指針,這樣能夠從任何一個節點訪問前一個節點,固然也能夠訪問後一個節點,以致整個鏈表。
雙向鏈表的結構以下圖所示:
循環鏈表中第一個節點以前就是最後一個節點,反之亦然。循環鏈表的無邊界使得在這樣的鏈表上設計算法會比普通鏈表更加容易。
循環鏈表的結構以下圖所示:
有人可能會問,既然已經有單向鏈表了,那爲何還要雙向鏈表呢?雙向鏈表有什麼優點呢?
這個就要從鏈表的刪除提及了,若是單向鏈表要刪除元素的話,不但要找到刪除的節點,還要找到刪除節點的上一個節點(一般稱之爲前驅),由於須要變動上一個節點中 next 的指針,但又由於它是單向鏈表,因此在刪除的節點中並無存儲上一個節點的相關信息,那麼咱們就須要再查詢一遍鏈表以找到上一個節點,這樣就帶來了必定的性能問題,因此就有了雙向鏈表。
鏈表的優勢大體可分爲如下三個:
鏈表的主要缺點是不能隨機查找,必須從第一個開始遍歷,查找效率比較低,鏈表查詢的時間複雜度是 O(n)。
瞭解了數組和鏈表的基礎知識以後,接下來咱們正式進入性能評測環節。
在正式開始以前,咱們先來明確一下測試目標,咱們須要測試的點其實只有 6 個:
由於添加操做和刪除操做在執行時間層面基本是一致的,好比數組添加須要移動後面的元素,刪除也一樣是移動後面的元素;而鏈表也是如此,添加和刪除都是改變自身和相連節點的信息,所以咱們就把添加和刪除的測試合二爲一,用添加操做來進行測試。
測試說明:
ArrayList
,而鏈表的表明爲 LinkedList
,所以咱們就用這兩個對象來進行測試;import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static final int operationSize = 100; // 操做次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動執行事件 arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void addArrayByFirst(Blackhole blackhole) { for (int i = 0; i < +operationSize; i++) { arrayList.add(i, i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(arrayList); } @Benchmark public void addLinkedByFirst(Blackhole blackhole) { for (int i = 0; i < +operationSize; i++) { linkedList.add(i, i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(linkedList); } }
從以上代碼能夠看出,在測試以前,咱們先將 ArrayList
和 LinkedList
進行數據初始化,再從頭部開始添加 100 個元素,執行結果以下:
從以上結果能夠看出,LinkedList
的平均執行(完成)時間比 ArrayList
平均執行時間快了約 216 倍。
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static final int operationSize = 100; // 操做次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動執行事件 arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void addArrayByMiddle(Blackhole blackhole) { int startCount = maxSize / 2; // 計算中間位置 // 中間部分進行插入 for (int i = startCount; i < (startCount + operationSize); i++) { arrayList.add(i, i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(arrayList); } @Benchmark public void addLinkedByMiddle(Blackhole blackhole) { int startCount = maxSize / 2; // 計算中間位置 // 中間部分進行插入 for (int i = startCount; i < (startCount + operationSize); i++) { linkedList.add(i, i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(linkedList); } }
從以上代碼能夠看出,在測試以前,咱們先將 ArrayList
和 LinkedList
進行數據初始化,再從中間開始添加 100 個元素,執行結果以下:
從上述結果能夠看出,LinkedList
的平均執行時間比 ArrayList
平均執行時間快了約 54 倍。
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static final int operationSize = 100; // 操做次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動執行事件 arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void addArrayByEnd(Blackhole blackhole) { int startCount = maxSize - 1 - operationSize; for (int i = startCount; i < (maxSize - 1); i++) { arrayList.add(i, i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(arrayList); } @Benchmark public void addLinkedByEnd(Blackhole blackhole) { int startCount = maxSize - 1 - operationSize; for (int i = startCount; i < (maxSize - 1); i++) { linkedList.add(i, i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(linkedList); } }
以上程序的執行結果爲:
從上述結果能夠看出,LinkedList
的平均執行時間比 ArrayList
平均執行時間快了約 32 倍。
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static final int operationSize = 100; // 操做次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動執行事件 arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void findArrayByFirst() { for (int i = 0; i < operationSize; i++) { arrayList.get(i); } } @Benchmark public void findLinkedyByFirst() { for (int i = 0; i < operationSize; i++) { linkedList.get(i); } } }
以上程序的執行結果爲:
從上述結果能夠看出,從頭部查詢 100 個元素時 ArrayList
的平均執行時間比 LinkedList
平均執行時間快了約 1990 倍。
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static final int operationSize = 100; // 操做次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動執行事件 arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void findArrayByMiddle() { int startCount = maxSize / 2; int endCount = startCount + operationSize; for (int i = startCount; i < endCount; i++) { arrayList.get(i); } } @Benchmark public void findLinkedyByMiddle() { int startCount = maxSize / 2; int endCount = startCount + operationSize; for (int i = startCount; i < endCount; i++) { linkedList.get(i); } } }
以上程序的執行結果爲:
從上述結果能夠看出,從中間查詢 100 個元素時 ArrayList
的平均執行時間比 LinkedList
平均執行時間快了約 28089 倍,真是恐怖。
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static final int operationSize = 100; // 操做次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Setup public void init() { // 啓動執行事件 arrayList = new ArrayList<Integer>(); linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); linkedList.add(i); } } @Benchmark public void findArrayByEnd() { for (int i = (maxSize - operationSize); i < maxSize; i++) { arrayList.get(i); } } @Benchmark public void findLinkedyByEnd() { for (int i = (maxSize - operationSize); i < maxSize; i++) { linkedList.get(i); } } }
以上程序的執行結果爲:
從上述結果能夠看出,從尾部查詢 100 個元素時 ArrayList
的平均執行時間比 LinkedList
平均執行成時間快了約 1839 倍。
接下來咱們再來測試一下,正常狀況下咱們從頭開始添加數組和鏈表的性能對比,測試代碼以下:
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) // 測試完成時間 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱次數和時間 @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試次數和時間 @Fork(1) // fork 1 個線程 @State(Scope.Thread) public class ArrayOptimizeTest { private static final int maxSize = 1000; // 測試循環次數 private static ArrayList<Integer> arrayList; private static LinkedList<Integer> linkedList; public static void main(String[] args) throws RunnerException { // 啓動基準測試 Options opt = new OptionsBuilder() .include(ArrayOptimizeTest.class.getSimpleName()) // 要導入的測試類 .build(); new Runner(opt).run(); // 執行測試 } @Benchmark public void addArray(Blackhole blackhole) { // 中間刪數組表 arrayList = new ArrayList<Integer>(); for (int i = 0; i < maxSize; i++) { arrayList.add(i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(arrayList); } @Benchmark public void addLinked(Blackhole blackhole) { // 中間刪除鏈表 linkedList = new LinkedList<Integer>(); for (int i = 0; i < maxSize; i++) { linkedList.add(i); } // 爲了不 JIT 忽略未被使用的結果計算 blackhole.consume(linkedList); } }
以上程序的執行結果爲:
接下來,咱們將添加的次數調至 1w,測試結果以下:
最後,咱們再將添加次數調至 10w,測試結果以下:
從以上結果能夠看出在正常狀況下,從頭部依次開始添加元素時,他們性能差異不大。
本文咱們介紹了數組的概念以及它的優缺點,同時還介紹了單向鏈表、雙向鏈表及循環鏈表的概念以及鏈表的優缺點。咱們在最後的評測中能夠看出,當咱們正常從頭部依次添加元素時,鏈表和數組的性能差不不大。但當數據初始化完成以後,咱們再進行插入操做時,尤爲是從頭部插入時,由於數組要移動以後的全部元素,所以性能要比鏈表低不少;但在查詢時性能恰好相反,由於鏈表要遍歷查詢,而且 LinkedList
是雙向鏈表,因此在中間查詢時性能要比數組查詢慢了上萬倍(查詢 100 個元素),而兩頭查詢(首部和尾部)時,鏈表也比數組慢了將近 1000 多倍(查詢 100 個元素),所以在查詢比較多的場景中,咱們要儘可能使用數組,而在添加和刪除操做比較多時,咱們應該使用鏈表結構。
數組和鏈表的操做時間複雜度,以下表所示:
數組 | 鏈表 | |
---|---|---|
查詢 | O(1) | O(n) |
插入 | O(n) | O(1) |
刪除 | O(n) | O(1) |