定義: 陣列(Array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料。
特性: 可實現隨機訪問,但是插入刪除操作比較慢
注意: 陣列訪問越界、容器與陣列如何抉擇?
一、陣列如何實現隨機訪問?
計算機會給每個記憶體單元分配一個地址,計算機通過地址來訪問記憶體中的資料。當計算機需要隨機訪問陣列中的某個元素時,它會首先通過下面的定址公式,計算出該元素儲存的記憶體地址:
a[i]_address = base_address + i * data_type_size
複製程式碼
data_type_size
表示陣列中每個元素的大小
二、為何陣列的插入刪除操作比較慢?如何優化呢?
2.1 插入
- 陣列末尾插入元素:無需移動資料,時間複雜度:O(1)
- 陣列開頭插入元素:所有資料都要移動一遍,時間複雜度:O(n)
平均時間複雜度:(1 + 2 + …n) / n == O(n)
特殊場景下,可以用如下圖方式來插入元素,可將時間複雜度降為 O(1)
2.2 刪除
刪了某條資料,為了記憶體的連續性,也要搬移資料。
- 陣列刪除末尾元素:時間複雜度:O(1)
- 陣列刪除開頭元素:時間複雜度:O(n)
平均時間複雜度:O(n)
特殊場景下 不一定非得追求陣列中資料的連續性,可進行如下處理
每次的刪除操作並不是真正地搬移資料,只是記錄資料已經被刪除。當陣列沒有更多空間儲存資料時,我們再觸發執行一次真正的刪除操作。
如此可以大大減少了刪除操作導致的資料搬移。(JVM 標記清除垃圾回收演算法的核心思想)?
三、陣列訪問越界
陣列越界在 C 語言中是一種未決行為,並沒有規定陣列訪問越界時編譯器應該如何處理。因為,訪問陣列的本質就是訪問一段連續記憶體,只要陣列通過偏移計算得到的記憶體地址是可用的,那麼程式就可能不會報任何錯誤。但是會造成莫名其妙的邏輯錯誤!
在 Java 當中,編譯器會做陣列越界檢測。陣列越界會丟擲:java.lang.ArrayIndexOutOfBoundsException
四、容器能否完全替代陣列?
針對陣列型別,許多語言提供了封裝的容器類,如 Java 的 ArrayList、C++ STL 中的 vector,在實際開發中何時使用容器類,何時使用陣列呢?
容器類(以 Java 的 ArrayList 為例)
- 優勢:自動擴容,每次空間不夠都會將空間自動擴容 1.5 倍大小。
- 劣勢:擴容涉及記憶體申請和資料遷移,比較耗時。而且無法儲存基本資料型別,需要拆裝箱操作,有一定的效能損耗
陣列
- 優勢:效能好,可儲存基本資料型別。多維陣列看起來比較直觀...
- 劣勢:手動擴容,許多基本的操作方法需要自己實現
綜上所述,陣列適用於追求效能的底層框架開發,容器類適用於一般的業務開發
*五、為何陣列不從 1 開始作為下標?
下標從 0 開始的定址公式
a[i]_address = base_address + i * data_type_size
複製程式碼
下標從 1 開始的定址公式
a[i]_address = base_address + (i - 1) * data_type_size
複製程式碼
由上述定址公式可見,從 1 開始編號,每次隨機訪問陣列都會多一次減法運算,對於 CPU 就多了一次減法指令。
*六、一些思考
- 本篇關於陣列的原理,引出了 JVM 的標記清除垃圾回收演算法的核心原理,回顧理解下 JVM 的標記清除垃圾回收演算法。
- 思考下二維陣列的定址公式是怎樣的呢?