資料結構與演算法之美 陣列 vs 鍊錶

2021-10-14 07:15:23 字數 3311 閱讀 5137

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

非線性表

連續的記憶體控制項和相同型別的資料

陣列和鍊錶區別

鍊錶適合插入、刪除,時間複雜度o(1);陣列適合查詢,查詢時間複雜度為 o(1)」。

但是,這種表述不準確。陣列是適合查詢操作,但是查詢的時間複雜度並不為 o(1)。即便是排好序的陣列,你用二分查詢,時間複雜度也是o(logn)。所以,正確的表述應該是,陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為 o(1)。

/**

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

*/a[i]_address = base_address + i * data_type_size

陣列低效得『插入』和「刪除」

插入如果在陣列的末尾插入元素,那就不需要移動資料了,這時的時間複雜度為 o(1)。但如果在陣列的開頭插入元素,那所有的資料都需要依次往後移動一位,所以最壞時間複雜度是 o(n)。 因為我們在每個位置插入元素的概率是一樣的,所以平均情況時間複雜度為 (1+2+…n)/n=o(n)。

**簡單辦法:**直接將第 k 位的資料搬移到陣列元素的最後,把新的元素直接放入第 k 個位置。

刪除操作

和插入類似,如果刪除陣列末尾的資料,則最好情況時間複雜度為 o(1);如果刪除開頭的資料,則最壞情況時間複雜度為 o(n);平均情況時間複雜度也為 o(n)。

陣列 a[10] 中儲存了 8 個元素:a,b,c,d,e,f,g,h。現在,我們要依次刪除 a,b,c 三個元素。

為了避免 d,e,f,g,h 這幾個資料會被搬移三次,我們可以先記錄下已經刪除的資料。每次的刪除操作並不是真正地搬移資料,只是記錄資料已經被刪除。當陣列沒有更多空間儲存資料時,我們再觸發執行一次真正的刪除操作,這樣就大大減少了刪除操作導致的資料搬移。

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

從陣列儲存的記憶體模型上來看,「下標」最確切的定義應該是「偏移(offset)」。前面也講到,如果用 a 來表示陣列的首位址,a[0] 就是偏移為 0 的位置,也就是首位址,a[k] 就表示偏移 k 個 type_size 的位置,所以計算 a[k] 的記憶體位址只需要用這個公式:

a[k]_address = base_address + k * type_size
a[k]_address = base_address +

(k-1

)*type_size

對比兩個公式,我們不難發現,從 1 開始編號,每次隨機訪問陣列元素都多了一次減法運算,對於 cpu 來說,就是多了一次減法指令。

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

鍊錶

陣列 : 需要一塊連續得記憶體空間

鍊錶:通過 「指標」 將一組零散得記憶體塊串聯起來使用

鍊錶通過指標將一組零散得記憶體快串聯在一起,其中我們把記憶體塊稱為鍊錶得「結點」。為了將所有的節點串起來,每個鍊錶的結點除了儲存資料之外,還需要記錄鏈上的下乙個結點的位址。我們把這個記錄下個結點位址的指標叫做後續指標next

鍊錶:第乙個結點為頭節點,最後乙個結點為尾結點

鍊錶 資料查詢、插入、刪除

針對鍊錶的插入和刪除操作,我們只需要考慮相鄰結點的指標改變,所以對應的時間複雜度是 o(1)。

**但是,**有利就有弊。鍊錶要想隨機訪問第 k 個元素,就沒有陣列那麼高效了。因為鍊錶中的資料並非連續儲存的,所以無法像陣列那樣,根據首位址和下標,通過定址公式就能直接計算出對應的記憶體位址,而是需要根據指標乙個結點乙個結點地依次遍歷,直到找到相應的結點。

你可以把鍊錶想象成乙個隊伍,隊伍中的每個人都只知道自己後面的人是誰,所以當我們希望知道排在第 k 位的人是誰的時候,我們就需要從第乙個人開始,乙個乙個地往下數。所以,鍊錶隨機訪問的效能沒有陣列好,需要 o(n) 的時間複雜度。

迴圈鍊錶

雙向鍊錶

空間換時間設計思想

記憶體空間充足的時候,如果我們更加追求**的執行速度,我們就可以選擇空間複雜度相對較高、但時間複雜度相對很低的演算法或者資料結構。相反,如果記憶體比較緊缺,比如**跑在手機或者微控制器上,這個時候,就要反過來用時間換空間的設計思路。

對於執行較慢的程式,可以通過消耗更多的記憶體(空間換時間)來進行優化;而消耗過多記憶體的程式,可以通過消耗更多的時間(時間換空間)來降低記憶體的消耗。

鍊錶 vs 陣列效能

時間複雜度

陣列鍊錶

插入刪除

o(n)

o(1)

隨機訪問

o(1)

o(n)

鍊錶**技巧

指標 、 引用它們得意思都是一樣得,都是儲存所指物件的記憶體位址

將某個變數賦值給指標,實際上就是將這個變數的位址賦值給指標,或者反過來說,指標中儲存了這個變數的記憶體位址,指向了這個變數,通過指標就能找到這個變數。

警惕指標丟失和記憶體洩漏
插入結點時,一定要注意操作的順序刪除鍊錶結點時,也一定要記得手動釋放記憶體空間,否則,也會出現記憶體洩漏的問題。

利用哨兵簡化實現難度
針對鍊錶的插入、刪除操作,需要對插入第乙個結點和刪除最後乙個結點的情況進行特殊處理

重點留意邊界條件處理
我經常用來檢查鍊錶**是否正確的邊界條件有這樣幾個:

舉例畫圖,輔助思考
舉例法畫圖法

多寫多練,沒有捷徑

資料結構與演算法之美 鍊錶

如何優雅的寫出鍊錶 6大學習技巧 一 理解指標或引用的含義1.含義 將某個變數 物件 賦值給指標 引用 實際上就是就是將這個變數 物件 的位址賦值給指標 引用 2.示例 p next q 表示p節點的後繼指標儲存了q節點的記憶體位址。p next p next next 表示p節點的後繼指標儲存了p...

資料結構與演算法之美 鍊錶

1.如何實現lru快取淘汰演算法 答 回答這個問題之前,我們首先了解一下什麼是快取 2.鍊錶的三種形式 單鏈表 雙向鍊錶 迴圈鍊錶 簡介一下單鏈表 頭節點用於記錄基位址,有了它我們就可以遍歷整條鍊錶,而尾節點特殊地方不是指向下乙個節點,而是指向乙個空位址。鍊錶因為不是乙個連續的位址,所以不需要考慮連...

《資料結構與演算法之美》筆記 鍊錶

typedef struct node node 陣列和鍊錶都是線性表。陣列必須是連續空間,而鍊錶無所謂。鍊錶 單鏈表 迴圈鍊錶 雙向鍊錶 陣列 插入 刪除的時間複雜度是o n 隨機訪問的時間複雜度是o 1 鍊錶 插入 刪除的時間複雜度是o 1 隨機訪問的時間複雜端是o n 快取淘汰策略 先進先出策...