陣列(Array)- 極客時間(資料結構與演算法之美)

zexinChen發表於2019-11-12

定義: 陣列(Array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料

特性: 可實現隨機訪問,但是插入刪除操作比較慢

注意: 陣列訪問越界、容器與陣列如何抉擇?

一、陣列如何實現隨機訪問?

計算機會給每個記憶體單元分配一個地址,計算機通過地址來訪問記憶體中的資料。當計算機需要隨機訪問陣列中的某個元素時,它會首先通過下面的定址公式,計算出該元素儲存的記憶體地址:

a[i]_address = base_address + i * data_type_size
複製程式碼

data_type_size 表示陣列中每個元素的大小

二、為何陣列的插入刪除操作比較慢?如何優化呢?

2.1 插入

  1. 陣列末尾插入元素:無需移動資料,時間複雜度:O(1)
  2. 陣列開頭插入元素:所有資料都要移動一遍,時間複雜度:O(n)

平均時間複雜度:(1 + 2 + …n) / n == O(n)

特殊場景下,可以用如下圖方式來插入元素,可將時間複雜度降為 O(1)

時間複雜度: O(1)

2.2 刪除

刪了某條資料,為了記憶體的連續性,也要搬移資料。

  1. 陣列刪除末尾元素:時間複雜度:O(1)
  2. 陣列刪除開頭元素:時間複雜度:O(n)

平均時間複雜度:O(n)

特殊場景下 不一定非得追求陣列中資料的連續性,可進行如下處理

每次的刪除操作並不是真正地搬移資料,只是記錄資料已經被刪除。當陣列沒有更多空間儲存資料時,我們再觸發執行一次真正的刪除操作。

先記錄 a,b,c 已被刪除,等到陣列儲存空間不夠的時候一塊移除,後續資料只需遷移一次

如此可以大大減少了刪除操作導致的資料搬移。(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 就多了一次減法指令。

*六、一些思考

  1. 本篇關於陣列的原理,引出了 JVM 的標記清除垃圾回收演算法的核心原理,回顧理解下 JVM 的標記清除垃圾回收演算法。
  2. 思考下二維陣列的定址公式是怎樣的呢?

相關文章