什麼是陣列
陣列(Array)是一種線性表資料結構,它用一組連續的記憶體空間來儲存一組具有相同型別的資料。
下面是兩個值得注意的概念:
1. 線性表(linear list)
線性表,即資料的邏輯結構是線性的。每個線性表中的資料最多隻有向前和向後兩個方向。典型的線性表有陣列、連結串列、佇列和棧。
和線性表相對應的就是非線性表,即資料之間不是簡單的前後關係。如樹、堆、圖等。
2. 連續的記憶體空間和相同型別的資料
這使陣列具有一個堪稱“殺手鐗”的特性 ——“隨機訪問”,即可以通過下標隨機訪問陣列元素。我們知道,計算機會給每個記憶體單元分配一個地址,然後通過地址來訪問記憶體中的資料。當計算機需要訪問陣列中的某個元素時,就會通過下面的定址公式,計算出儲存該元素的記憶體地址:
a[i]_address = base_address + i * data_type_size
其中,data_type_size 是陣列中每個元素的大小,base_address 是記憶體塊的首地址。這樣,就可以用 O(1) 的時間複雜度獲取到下標為 i 的陣列元素。
當然,有利就有弊,這一特性使得陣列的很多操作變得低效,比如插入、刪除操作,為了保證連續性,需要做大量的資料搬移工作。
低效的插入和刪除
下面來看看,為什麼陣列的插入和刪除操作是低效的,又有哪些改進方法呢?
1. 插入
假設陣列長度為 n,要在第 k 位插入一個元素。那麼就需要將第 k ~ n 位元素順序後移,然後將新元素插入第 k 位。
如果 k = n - 1,則不需要再移動資料,此時時間複雜度為 O(1);如果 k = 0,則需要移動所有的資料,此時時間複雜度為 O(n)。用前一篇文章的分析方法(演算法複雜度分析(下)),可知平均時間複雜度為:
(1 + 2 + ... + n) / n = O(n)
所以,陣列的插入操作平均時間複雜度為 O(n),是一項低效的操作。
但是,如果陣列中儲存的資料沒有任何規律,插入後不需要保持原來的順序,則可以進行如下操作:
直接將第 k 位的資料搬移到陣列元素最後,然後把新的元素直接放在第 k 位。
這樣,時間複雜度就被降為了 O(1)。這個思想在快排中也會用到。
2. 刪除
和插入操作類似,如果要刪除第 k 位的元素,為了記憶體的連續性,也需要搬移資料,否則中間就會出現空洞,記憶體就不連續了。容易分析出,刪除操作的平均時間複雜度也為 O(n)。
事實上,我們可以將多次刪除操作集中在一起執行,先標記下已經刪除的資料,然後在某些條件下(比如沒有更多空間儲存資料時),統一執行真正的刪除操作,這樣就大大減少了刪除操作導致的資料搬移。
這種標記清除思想在其他地方也有所應用,比如垃圾回收中的標記清除演算法。
陣列越界問題
訪問陣列的本質是訪問一段連續記憶體,只要陣列通過偏移計算得到的記憶體地址都是可用的,那麼程式就可能不會報任何錯誤。
很多計算機病毒也正是利用了程式碼中陣列越界可以訪問非法地址的漏洞來進行攻擊,所以寫程式碼時一定要警惕陣列越界。
JavaScript 中的陣列
作為一名前端,最後重點講一講 JavaScript 中的陣列。
1. 不是在連續記憶體空間中儲存同型別資料
JavaScript 中的陣列是一種引用資料型別(也叫複雜資料型別),它其實是一種物件,繼承於 Object,儲存與堆記憶體中。
Array.prototype.__proto__ === Object.prototype // true
複製程式碼
JavaScript 中可以通過陣列的 length 屬性來獲取其長度:
arr.length
複製程式碼
而 C 中需要使用這種方法:
sizeof(arr) / sizeof(arr[0])
複製程式碼
python 中則是:
len(arr)
複製程式碼
JavaScript 中的陣列本質上是 key 為 '0', '1', '2', ... 的物件,對應的 value 可以為任意值。這樣的話,就可以很容易解釋下面這種現象:
var arr = ['a', 1]
var key = { toString: function () { return '1' }}
key in arr // true
複製程式碼
2. 插入、刪除的效能問題
很慚愧,學藝不精,這個話題暫時就不發表評論了,以免誤人子弟。感興趣的同學可以移步這裡:stackoverflow.com/questions/8…
3. 不會有陣列越界問題
JavaScript 陣列空間不足時,會自動進行擴容操作,不會出現陣列越界問題。具體分析可以參考這裡:zhuanlan.zhihu.com/p/26388217
本文是《資料結構與演算法之美》的讀書筆記,首發於微信公眾號《程式碼寫完了》