算法與數據結構(一) 線性表的順序存儲與鏈式存儲(Swift版)


溫故而知新,在接下來的幾篇博客中,將會系統的對數據結構的相關內容進行回顧並總結。數據結構乃編程的基礎呢,還是要不時拿出來翻一翻回顧一下。當然數據結構相關博客中我們以Swift語言來實現。因為Swift語言是面向對象語言,所以在相關示例實現的時候與之前在大學學數據結構時C語言的實現有些出入,不過數據結構還是要注重思想,至於實現語言是面向對象的還是面向過程的影響不大。

接觸過數據結構的小伙伴應該都知道程序 = 數據結構 + 算法。數據結構乃組織組織數據的結構,算法就是對這些結構中的數據進行操作,可見數據結構的重要性,就連算法也是依賴於數據結構的。

在博客的開頭,我們先簡單的聊些數據結構整體的東西。數據結構整體可以分為物理結構和邏輯結構,物理結構指的是數據在磁盤、內存等硬件上的存儲結構,主要包括順序結構和鏈式結構。而邏輯結構是數據本身所形成的結構,包括集合結構、線性結構樹形結構以及圖形結構。針對不同的數據結構我們可以依據算法解決不同的問題,如我們在以后博客中要介紹的最小生成樹,就是根據算法將帶權的連通圖轉換成最小生成樹,在轉換這個過程中我們就用到了Prim算法和克魯斯卡爾算法。當然各種排序算法,最短路徑等等也是算法與數據結構的結晶體。

 

一、線性表綜述

本篇博客我們主要介紹的是邏輯結構中的線性表,也就是線性結構。線性結構的特點就好比一串珠子,其特點是第一個節點只有一個后繼,沒有前驅,最后一個節點是只有一個前驅,沒有后繼。而其余的節點只有一個前驅和一個后繼。說吧了線性表就是一串。下方這個圖就是線性表的示例圖。中間藍色的節點前方的是就是改點對應的前驅,后邊就是改點對應的后繼。從下方可以明確看出head沒有前驅只有后繼,而tail只有前驅沒有后繼。

  

上面這個是線性表的邏輯結構,接下來我們來聊一下線性表的物理結構,也就是存儲結構。線性表的物理結構可分為順序存儲結構和鏈式存儲結構。順序存儲結構之所以稱之為順序存儲結構因為每個線性表中節點的內存地址是連續的,而鏈式存儲結構中線性表的節點的內存地址可以是不連續的。這也就是在C語言實現順序存儲線性表時先Malloc一塊連續的區域,然后用來順序的存儲線性表。而鏈表中就可以不是連續的了,前驅與后繼間的關系由指針連接。

下方這個指示圖中,上面這個就是鏈式存儲,下方這個就是順序存儲。可見鏈式存儲是有指針域的,也就是前驅和后繼的關系有指針來鏈接。下方這個鏈式存儲就是單向鏈表,因為只有前驅到后繼的指針,而沒有后繼到前驅的指針。關於雙向鏈表下方會具體給出詳細的說明。而下面第二個圖就是順序存儲,前驅與后繼的關系是由緊挨的內存地址所關聯。

  

上面這兩種存儲方式我們可以換另一種更為形象的方式來進行說明,如下所示。順序存儲的內存區塊的內存地址是緊挨的,線性關系有相鄰的內存地址所保存。當然下方我們是模擬的內存地址,就使用了0x1, 0x2等等來模擬。而鏈式存儲的線性表,在物理存儲結構中是不相鄰的,僅僅靠內存地址無法去維持這種線性關系。所以就需要一個指針域來指向后繼或者前驅節點的內存地址,從而將節點之間進行關聯。在單向鏈式存儲中,一個節點不僅僅需要存儲數據,而且還要存儲該節點下一個節點的內存地址,以便保持這種線性關系。具體請看下圖。

  

原理性質的東西先就聊這么多,下面我們會給出具體實現。主要包括線性表的順序存儲及其操作,以及線性表的單鏈以及雙鏈存儲及其操作。下方的實例依然采用Swift面向對象語言實現,思想理解后,用什么語言都是可以的呢。

 

二、線性表的順序存儲

關於線性表的順序存儲,我們就使用NSMutableArray來實現,也就是使用OC中的可變數組類型來實現。我們就假設其存儲的內存地址是連續的,當然數組中存儲對象時要復雜的多,我們暫且假設其中的地址是連續的。下方的list就是我們的順序線性表,count存儲的是已經存入到線性表中的元素個數,而capacity則是記錄線性表整個容量的大小。當然上述三個屬性都是private的,而下方的計算屬性lengthinternal類型的,供外界訪問,返回線性表元素的個數。

下方是整體結構,我們下方會給出線性表具體的關鍵操作,並分析其時間復雜度。

  

 

1.往順序線性表中插入數據

有時候我們會給據特定的算法往線性表中指定的位置插入數據,比如我們常見的插入排序算法,如果你的數據是順序存儲的話,那么就需要將數據插入到順序表中。接下來我們就將數據插入到指定的位置。

下方該圖中是往順序表中插入一個元素的原理圖。在下圖中,我們往AB與CD之間插入一個M。在插入M之前我們需要做的事情就是將CD整體往后移動一個位置,為M騰出一個位置,然后再講M這個元素進行插入。

  

順序表的插入還是比較簡單的,也是非常好理解的,那么用代碼實現起來也是用不了幾行代碼的。下方截圖就是是順序線性表的元素插入的具體實現的代碼。首先檢查index的合法性,檢查index是否在合理范圍內,然后將index后的元素依次往后移動,然后將元素插入即可。

  

 

2. 順序線性表的元素移除

上面介紹完元素的插入后,接下來要聊一下元素的移除。也就是移除指定索引中的元素。該過程恰好與上述插入的過程相反,上述在插入之前是相應的元素往后移,騰出index位置。而移除特定索引的元素時,是相應的元素左移,覆蓋掉要刪除的元素,然后將最后一個元素進行移除掉。下方的原理圖對此過程進行了說明。

  

該部分比較簡單,下方的代碼段就是將指定索引的元素進行移除。在線性表的順序存儲中,前驅和后繼的關系由內存地址的先后順序所關聯,所以插入和刪除元素會相對麻煩一些,而查找和修改元素就比較簡單了,直接由index可以找到相應的元素,再次就不做過多贅述了。github上所分享的Demo中會有完整示例。

  

 

三、線性表的單鏈存儲

介紹完線性表的順序存儲,接下來我們來介紹線性表的鏈式存儲。當然,本部分是對單向鏈表的介紹,下部分將會對雙向鏈表的介紹。下方截圖就是我們單向鏈表相關示例的運行結果,首先我們先正向的創建鏈表,也就是后來的元素插入到鏈表的后方。然后在給出逆向創建鏈表,與正向創建鏈表相反,后來的元素摻入到頭結點的后方。創建鏈表完畢后,我們會給出鏈表元素的插入和移除的解決方案。

  

 

1.單向鏈表的創建

在鏈表創建之前,我們得先創建節點的類,因為鏈表是多個節點的連接。下方這個OneDirectionLinkListNote類就是單向鏈表的節點類。其中的data屬性存儲的是該節點所存儲的數據,而變量next就是指向下一個節點的指針,鏈表中節點間的關系由next指針所關聯。initdeinit就是該類的構造和析構函數了,就不做過多贅述了。

  

 

2.鏈表協議的創建

在創建鏈表之前,我們可以先創建一個鏈表協議ListProtocalType。在ListProtocalType協議中定義了鏈表所必須的方法,無論是單向鏈表還是雙向鏈表都要遵循該協議。其實這就是“面向接口”的提現。單向鏈表與雙向鏈表都遵循了這協議,那么說明這兩種鏈表對外的接口是一致的,這樣便於程序的維護,這也是面向接口編程的好處。下方就是我們事先定義好的ListProtocalType協議的部分內容。

下方協議中只給出了方法的定義,未給出具體實現。所有鏈表都要遵循該協議,ListProtocalType中定義了鏈表結構所必須的方法。可以說下方這個協議就是鏈表的大綱。

  

 

3.單向鏈表的構建

下方就是我們單向鏈表類的屬性和構造函數。headNote就是我們鏈表的頭結點,而tailNote是我們鏈表的尾結點。length就是我們鏈表的長度,也就是我們鏈表中元素的個數。一個空鏈表中tailNote = headNote

  

下方我們將會介紹鏈表的正向創建和逆向創建。 下方這個截圖中就是正向創建鏈表,其實就是將新創建的數據往鏈表的尾部插入,然后更新一下tail的位置即可。關鍵步驟就兩步,第一步是tail->next = newNode,第二步是tail = newNode。插入步驟如下所示:

  

上面這個示意圖是往鏈表的后方添加元素,接下來是王鏈表的頭部插入元素。該過程也是由關鍵的兩步組成,第一步就是newNode->next = head->next,第二步是head->next = newNode。經過這兩步我們就可以把元素插入到頭結點的后方了。

  

下方這段代碼是正向創建鏈表的具體代碼,就是一直往鏈表的尾部插入元素。逆向創建鏈表的代碼就不往上粘貼了,與下方代碼類似,github上會進行完整實例的分享。上面雖然是往頭結點和尾部結點的插入,但是原理是適用於往兩邊中間插入元素的,在此就不做過多贅述了。

  

 

4.單向鏈表元素的移除

上面我們聊完元素的插入,解析來我們要聊一下元素的移除。要想移除單向鏈表中的一個元素,首先我們得找到被移除結點的前驅的位置,比如是pre。當前移除的元素是remove,讓我我們讓pre->next = remove->next, 然后再執行remove->next = nil。經過上面這些步驟,remove這個結點就與鏈表脫離關系了。示意圖如下所示。

  

根據上述的示意圖,我們就可以給出具體的代碼實現了。單向鏈表的核心操作就介紹完了,其他更多細節請參考在github上分享的源代碼,因為篇幅有限,關於單向鏈表的更多細節就不做過多贅述了。

  

 

四、雙向鏈表

如果你對單向鏈表已經理解的話,那么理解雙向鏈表來說並非難事。雙向鏈表與單向鏈表相比多了一個指向前驅的節點。我們暫且稱為將指向前驅的節點命名我pre指針。下方這個示意圖就是雙向鏈表的示意圖,與單向鏈表相比,多了一個指向前驅的指針域。如下所示。接下來將會給出雙向鏈表的插入和移除。

  

1.雙向鏈表元素的插入

雙向鏈表的插入要比單向鏈表的插入要復雜一些,不過也是蠻好理解的。下方示意圖中就是往節點A后方插入一個節點D。主要分為四個步驟,第一步是將D節點的next指針指向A節點next指針指向的節點,也就是D->next = A->next。第二步是講D節點的pre指針指向A節點,也就是D->pre = A。第三步是將A的next指針指向D,也就是A->next = D。最后將D節點的下一個節點的pre指針指向D,也就是D->next->pre = D。經過這幾步,我們就可以將節點D插入到A與B的中間。當然這個順序不是一定的,只要能保證鏈的正確關聯即可。

  

下方是上述元素插入的核心代碼,如下所示。主要將newItem節點,插入到cursor節點后方。

  

 

2.雙向鏈表元素的刪除

雙向鏈表因為比單向鏈表多一個前驅指針域,所以元素的刪除要麻煩一下,不過還是比較好理解的。下方這個截圖就是刪除B節點的示意圖。首先將B節點前驅節點的next指針域指向B節點的后繼,也就是B->pre->next = B->next。 然后將B節點的后繼節點的前驅指針指向B的前驅節點,對應着B->next-pre = B->pre。最后將B的next和pre指針域置為nil。如下所示:

    

下方代碼段就是雙向鏈表移除節點的具體實現,如下所示。至於鏈表的遍歷等其他操作在此就不做過多的贅述了,具體內容請看github上分享的源代碼。

  

 

五、面向接口編程的優點

在上述我們實現兩種鏈表時,我們先定義了一個鏈表協議ListProtocalType。無論是雙向鏈表還是單向鏈表都遵循這個協議,也就是說,該協議就是鏈表對外統一的接口,該協議就是操作鏈表的一個規范。下方的testLinkedList()就是我們鏈表的測試方法,該函數的參數是遵循ListProtocalType協議的所有類的對象。也就是說只要遵循了ListProtocalType這個協議的類的對象,都可以作為該函數的參數。至於具體操作,那么不同的類會給出不同的操作的。

在調用該函數時,第一個傳入的是單向鏈表的類的對象,第二個是雙向鏈表的類的對象。雖然都是執行同一個方法,但是因為傳入的類的對象不同,所以執行的結果顯然是不同的。這也就是利用了面向對象的多態性,在之前設計模式系列的博客中介紹過,下方這種與策略模式類似。

  

 

 

本篇博客的內容也是挺多的了,當然博客中的內容是從Demo中挑出的關鍵點來講的,具體細節請看下方github上所分享的鏈接:

https://github.com/lizelu/DataStruct-Swift/tree/master/ListDataStruct


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM