1. 程式人生 > 其它 >資料結構與演算法之美 王爭 學習筆記-2 [MD]

資料結構與演算法之美 王爭 學習筆記-2 [MD]

博文地址

我的GitHub 我的部落格 我的微信 我的郵箱
baiqiantao baiqiantao bqt20094 [email protected]

目錄

目錄

基礎篇 (38講)

05 | 陣列:為什麼很多程式語言中陣列都從0開始編號?

為什麼陣列要從 0 開始編號,而不是從 1 開始呢?

如何實現隨機訪問?

陣列(Array)是一種線性表資料結構。它用一組連續

的記憶體空間,來儲存一組具有相同型別的資料。

線性表

顧名思義,線性表就是資料排成像一條線一樣的結構。每個線性表上的資料最多隻有前和後兩個方向。除了陣列,連結串列佇列等也是線性表結構。

而與它相對立的概念是非線性表,比如二叉樹等。在非線性表中,資料之間並不是簡單的前後關係。

連續的記憶體空間和相同型別的資料

第二個是連續的記憶體空間和相同型別的資料。正是因為這兩個限制,它才有了一個堪稱殺手鐗的特性:隨機訪問

這兩個限制也讓陣列的很多操作變得非常低效,比如要想在陣列中刪除、插入一個數據,為了保證連續性,就需要做大量的資料搬移工作。

陣列是如何實現根據下標隨機訪問陣列元素的

例如陣列 int[] a = new int[10]

,計算機給陣列分配了一塊連續記憶體空間:

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

a[i]_address = base_address + i * data_type_size

這裡我要特別糾正一個錯誤。我在面試的時候,常常會問陣列和連結串列的區別,很多人都回答說,“連結串列適合插入、刪除,時間複雜度 O(1);陣列適合查詢,查詢時間複雜度為 O(1)”。實際上,這種表述是不準確的。陣列是適合查詢操作,但是查詢的時間複雜度並不為 O(1)。即便是排好序的陣列,你用二分查詢,時間複雜度也是 O(logn)

。所以,正確的表述應該是,陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為 O(1)

低效的插入和刪除

插入操作的優化

如果陣列中儲存的資料並沒有任何規律(即:無序),陣列只是被當作一個儲存資料的集合。在這種情況下,如果要將某個資料插入到第 k 個位置,為了避免大規模的資料搬移,我們還有一個簡單的辦法就是,直接將第 k 位的資料搬移到陣列元素的最後,把新的元素直接放入第 k 個位置。

利用這種處理技巧,在特定場景下,在第 k 個位置插入一個元素的時間複雜度就會降為 O(1)。這個處理思想在快排中也會用到。

正常情況,不會使用這種操作,

刪除操作的優化

在某些特殊場景下,我們並不一定非得追求陣列中資料的連續性。我們可以先記錄下已經刪除的資料,每次的刪除操作並不是真正地搬移資料,只是記錄資料已經被刪除。當陣列沒有更多空間儲存資料時,我們再觸發執行一次真正的刪除操作,這樣就大大減少了刪除操作導致的資料搬移。

這其實就是 JVM 標記清除垃圾回收演算法的核心思想。

警惕陣列的訪問越界問題

陣列越界在 C 語言中是一種未決行為,並沒有規定陣列訪問越界時編譯器應該如何處理。因為,訪問陣列的本質就是訪問一段連續記憶體,只要陣列通過偏移計算得到的記憶體地址是可用的(非受限的記憶體),那麼程式就可能不會報任何錯誤。

容器能否完全替代陣列?

ArrayList 最大的優勢就是可以將很多陣列操作的細節封裝起來。比如前面提到的陣列插入、刪除資料時需要搬移其他資料等。另外,它還有一個優勢,就是支援動態擴容

陣列本身在定義的時候需要預先指定大小,因為需要分配連續的記憶體空間

因為擴容操作涉及記憶體申請和資料搬移,是比較耗時的。所以,如果事先能確定需要儲存的資料大小,最好在建立 ArrayList 的時候事先指定容器大小

對於業務開發,直接使用容器就足夠了,省時省力。畢竟損耗一丟丟效能,完全不會影響到系統整體的效能。但如果你是做一些非常底層的開發,比如開發網路框架,效能的優化需要做到極致,這個時候陣列就會優於容器,成為首選。

解答開篇

為什麼大多數程式語言中,陣列要從 0 開始編號,而不是從 1 開始呢?

從陣列儲存的記憶體模型上來看,下標最確切的定義應該是偏移(offset)。如果用 a 來表示陣列的首地址,a[0] 就是偏移為 0 的位置,也就是首地址,a[k] 就表示偏移 k 個 type_size 的位置。

如果從 1 開始編號,每次隨機訪問陣列元素都多了一次減法運算,對於 CPU 來說,就是多了一次減法指令。

陣列作為非常基礎的資料結構,通過下標隨機訪問陣列元素又是其非常基礎的程式設計操作,效率的優化就要儘可能做到極致。所以為了減少一次減法操作,陣列選擇了從 0 開始編號,而不是從 1 開始。

不過,上面解釋得再多其實都算不上壓倒性的證明,我覺得最主要的原因可能是歷史原因。C 語言設計者用 0 開始計數陣列下標,之後的 Java、JavaScript 等高階語言都效仿了 C 語言,或者說,為了在一定程度上減少 C 語言程式設計師學習 Java 的學習成本,因此繼續沿用了從 0 開始計數的習慣。實際上,很多語言中陣列也並不是從 0 開始計數的,比如 Matlab。甚至還有一些語言支援負數下標,比如 Python。

06 | 連結串列(上):如何實現LRU快取淘汰演算法?