資料結構與演算法解析 鍊錶篇

2021-10-07 03:51:43 字數 3553 閱讀 4620

鍊錶是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過鍊錶中的指標鏈結次序實現的。鍊錶由一系列結點(鍊錶中每乙個元素稱為結點)組成,結點可以在執行時動態生成。每個結點包括兩個部分:乙個是儲存資料元素的資料域,另乙個是儲存下乙個結點位址的指標域(對於雙向鍊錶也會儲存上乙個節點的位址域)。

常見的三種鍊錶結構:

2.1、單鏈表

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

記錄下個結點位址的指標叫作後繼指標 next。

有兩個結點是比較特殊的,它們分別是第乙個結點和最後乙個結點。我們習慣性地把第乙個結點叫作頭結點,把最後乙個結點叫作尾結點。其中,頭結點用來記錄鍊錶的基位址。有了它,我們就可以遍歷得到整條鍊錶。而尾結點特殊的地方是:指標不是指向下乙個結點,而是指向乙個空位址 null,表示這是鍊錶上最後乙個結點。

2.1.1、鍊錶的優點 ---- 高效的插入和刪除操作

在進行陣列的插入、刪除操作時,為了保持記憶體資料的連續性,需要做大量的資料搬移,所以時間複雜度是 o(n)。而在鍊錶中插入或者刪除乙個資料,我們並不需要為了保持記憶體的連續性而搬移結點,因為鍊錶的儲存空間本身就不是連續的。所以,在鍊錶中插入和刪除乙個資料是非常快速的。

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

2.1.2、鍊錶的缺點 ---- 不支援隨機訪問,訪問某個節點比較低效

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

鍊錶隨機訪問的效能沒有陣列好,需要 o(n) 的時間複雜度。

2.2、迴圈鍊錶 ---- 一種特殊的單鏈表

它跟單鏈表唯一的區別就在尾結點。我們知道,單鏈表的尾結點指標指向空位址,表示這就是最後的結點了。而迴圈鍊錶的尾結點指標是指向鍊錶的頭結點。從我畫的迴圈鍊錶圖中,你應該可以看出來,它像乙個環一樣首尾相連,所以叫作「迴圈」鍊錶。

和單鏈表相比,迴圈鍊錶的優點是從鏈尾到鏈頭比較方便。當要處理的資料具有環型結構特點時,就特別適合採用迴圈鍊錶。比如著名的約瑟夫問題。儘管用單鏈表也可以實現,但是用迴圈鍊錶實現的話,**就會簡潔很多。

2.3、雙向鍊錶

單向鍊錶只有乙個方向,結點只有乙個後繼指標 next 指向後面的結點。而雙向鍊錶,顧名思義,它支援兩個方向,每個結點不止有乙個後繼指標 next 指向後面的結點,還有乙個前驅指標 prev 指向前面的結點。

雙向鍊錶需要額外的兩個空間來儲存後繼結點和前驅結點的位址。所以,如果儲存同樣多的資料,雙向鍊錶要比單鏈表占用更多的記憶體空間。雖然兩個指標比較浪費儲存空間,但可以支援雙向遍歷,這樣也帶來了雙向鍊錶操作的靈活性。

從結構上來看,雙向鍊錶可以支援 o(1) 時間複雜度的情況下找到前驅結點,正是這樣的特點,也使雙向鍊錶在某些情況下的插入、刪除等操作都要比單鏈表簡單、高效。

雙向鍊錶相比於單鏈表的優勢:

對於第一種情況,不管是單鏈表還是雙向鍊錶,為了查詢到值等於給定值的結點,都需要從頭結點開始乙個乙個依次遍歷對比,直到找到值等於給定值的結點,然後再通過我前面講的指標操作將其刪除。

儘管單純的刪除操作時間複雜度是 o(1),但遍歷查詢的時間是主要的耗時點,對應的時間複雜度為 o(n)。根據時間複雜度分析中的加法法則,刪除值等於給定值的結點對應的鍊錶操作的總時間複雜度為 o(n)。

對於第二種情況,我們已經找到了要刪除的結點,但是刪除某個結點 q 需要知道其前驅結點,而單鏈表並不支援直接獲取前驅結點,所以,為了找到前驅結點,我們還是要從頭結點開始遍歷鍊錶,直到 p->next=q,說明 p 是 q 的前驅結點。

但是對於雙向鍊錶來說,這種情況就比較有優勢了。因為雙向鍊錶中的結點已經儲存了前驅結點的指標,不需要像單鏈表那樣遍歷。所以,針對第二種情況,單鏈表刪除操作需要 o(n) 的時間複雜度,而雙向鍊錶只需要在 o(1) 的時間複雜度內就搞定了!

某個指定結點前面插入乙個結點

同刪除類似,在某個指定結點前面插入乙個結點,雙向鍊錶比單鏈表有很大的優勢。雙向鍊錶可以在 o(1) 時間複雜度搞定,而單向鍊錶需要 o(n) 的時間複雜度。

在有序鍊錶中,按值查詢的效率更高

對於乙個有序鍊錶,雙向鍊錶的按值查詢的效率也要比單鏈表高一些。因為,我們可以記錄上次查詢的位置 p,每次查詢時,根據要查詢的值與 p 的大小關係,決定是往前還是往後查詢,所以平均只需要查詢一半的資料。

所以,基於雙向鍊錶的優勢,在實際的軟體開發中,雙向鍊錶儘管比較費記憶體,但還是比單鏈表的應用更加廣泛的原因。

2.4、雙向迴圈鍊錶

維護乙個有序單鏈表,越靠近鍊錶尾部的結點是越早之前訪問的。當有乙個新的資料被訪問時,從煉表頭開始順序遍歷鍊錶。

如果此資料之前已經被快取在鍊錶中了,我們遍歷得到這個資料對應的結點,並將其從原來的位置刪除,然後再插入到鍊錶的頭部。

如果此資料沒有在快取鍊錶中,又可以分為兩種情況:

如果此時快取未滿,則將此結點直接插入到鍊錶的頭部;

如果此時快取已滿,則鍊錶尾結點刪除,將新的資料結點插入鍊錶的頭部。

這樣就用鍊錶實現了乙個 lru 快取。

如何輕鬆寫出正確的鍊錶**?下面是幾個有用的技巧。

當你寫完鍊錶**之後,除了看下你寫的**在正常的情況下能否工作,還要看下在上面我列舉的幾個邊界條件下,**仍然能否正確工作。如果這些邊界條件下都沒有問題,那基本上可以認為沒有問題了。

當然,邊界條件不止那些。針對不同的場景,可能還有特定的邊界條件,這個需要你自己去思考,不過套路都是一樣的。

實際上,不光光是寫鍊錶**,你在寫任何**時,也千萬不要只是實現業務正常情況下的功能就好了,一定要多想想,你的**在執行的時候,可能會遇到哪些邊界情況或者異常情況。遇到了應該如何應對,這樣寫出來的**才夠健壯!

技巧五:舉例畫圖,輔助思考

使用舉例法和畫圖法。

你可以找乙個具體的例子,把它畫在紙上,釋放一些腦容量,留更多的給邏輯思考,這樣就會感覺到思路清晰很多。

當我們寫完**之後,也可以舉幾個例子,畫在紙上,照著**走一遍,很容易就能發現**中的 bug。

技巧六:多寫多練,沒有捷徑

資料結構與演算法 鍊錶篇

鍊錶屬於線性結構之一,主要功能是提供可動態擴充套件的線性結構,可使用不連續的的記憶體空間,為程式的動態特性提供支援。邏輯結構如下 引用自csdn部落格 一般的定義如下 the data structure of link list typedef int datatype typedef struc...

資料結構與演算法 鍊錶篇

反轉鍊錶這道演算法題應該算是所有鍊錶題的底層了,所以一定要理解掌握這道題 下面展示一些內聯 片。判斷鍊錶head或head.next是否為空,為空返回鍊錶head if head null head.next null 當前節點的前乙個節點 listnode pre null 當前節點 listno...

資料結構 鍊錶篇

鍊錶的優點 插入和刪除速度快 記憶體利用率高 可以隨時擴充套件,不必擔心儲存滿 鍊錶的缺點 不能隨機查詢,只能通過從頭乙個乙個找,查詢效率低 實現如下 include include typedef struct node node 函式宣告 node head create list 頭插法建立鍊...