溫故而知新,在接下來的幾篇博客中,將會系統的對數據結構的相關內容進行回顧並總結。數據結構乃編程的基礎呢,仍是要不時拿出來翻一翻回顧一下。固然數據結構相關博客中咱們以Swift語言來實現。由於Swift語言是面嚮對象語言,因此在相關示例實現的時候與以前在大學學數據結構時C語言的實現有些出入,不過數據結構仍是要注重思想,至於實現語言是面向對象的仍是面向過程的影響不大。git
接觸過數據結構的小夥伴應該都知道程序 = 數據結構 + 算法。數據結構乃組織組織數據的結構,算法就是對這些結構中的數據進行操做,可見數據結構的重要性,就連算法也是依賴於數據結構的。github
在博客的開頭,咱們先簡單的聊些數據結構總體的東西。數據結構總體能夠分爲物理結構和邏輯結構,物理結構指的是數據在磁盤、內存等硬件上的存儲結構,主要包括順序結構和鏈式結構。而邏輯結構是數據自己所造成的結構,包括集合結構、線性結構、樹形結構以及圖形結構。針對不一樣的數據結構咱們能夠依據算法解決不一樣的問題,如咱們在之後博客中要介紹的最小生成樹,就是根據算法將帶權的連通圖轉換成最小生成樹,在轉換這個過程當中咱們就用到了Prim算法和克魯斯卡爾算法。固然各類排序算法,最短路徑等等也是算法與數據結構的結晶體。算法
1、線性表綜述編程
本篇博客咱們主要介紹的是邏輯結構中的線性表,也就是線性結構。線性結構的特色就比如一串珠子,其特色是第一個節點只有一個後繼,沒有前驅,最後一個節點是隻有一個前驅,沒有後繼。而其他的節點只有一個前驅和一個後繼。說吧了線性表就是一串。下方這個圖就是線性表的示例圖。中間藍色的節點前方的是就是改點對應的前驅,後邊就是改點對應的後繼。從下方能夠明確看出head沒有前驅只有後繼,而tail只有前驅沒有後繼。設計模式
上面這個是線性表的邏輯結構,接下來咱們來聊一下線性表的物理結構,也就是存儲結構。線性表的物理結構可分爲順序存儲結構和鏈式存儲結構。順序存儲結構之因此稱之爲順序存儲結構由於每一個線性表中節點的內存地址是連續的,而鏈式存儲結構中線性表的節點的內存地址能夠是不連續的。這也就是在C語言實現順序存儲線性表時先Malloc一塊連續的區域,而後用來順序的存儲線性表。而鏈表中就能夠不是連續的了,前驅與後繼間的關係由指針鏈接。數組
下方這個指示圖中,上面這個就是鏈式存儲,下方這個就是順序存儲。可見鏈式存儲是有指針域的,也就是前驅和後繼的關係有指針來連接。下方這個鏈式存儲就是單向鏈表,由於只有前驅到後繼的指針,而沒有後繼到前驅的指針。關於雙向鏈表下方會具體給出詳細的說明。而下面第二個圖就是順序存儲,前驅與後繼的關係是由緊挨的內存地址所關聯。數據結構
上面這兩種存儲方式咱們能夠換另外一種更爲形象的方式來進行說明,以下所示。順序存儲的內存區塊的內存地址是緊挨的,線性關係有相鄰的內存地址所保存。固然下方咱們是模擬的內存地址,就使用了0x1, 0x2等等來模擬。而鏈式存儲的線性表,在物理存儲結構中是不相鄰的,僅僅靠內存地址沒法去維持這種線性關係。因此就須要一個指針域來指向後繼或者前驅節點的內存地址,從而將節點之間進行關聯。在單向鏈式存儲中,一個節點不只僅須要存儲數據,並且還要存儲該節點下一個節點的內存地址,以便保持這種線性關係。具體請看下圖。函數
原理性質的東西先就聊這麼多,下面咱們會給出具體實現。主要包括線性表的順序存儲及其操做,以及線性表的單鏈以及雙鏈存儲及其操做。下方的實例依然採用Swift面嚮對象語言實現,思想理解後,用什麼語言都是能夠的呢。測試
2、線性表的順序存儲spa
關於線性表的順序存儲,咱們就使用NSMutableArray來實現,也就是使用OC中的可變數組類型來實現。咱們就假設其存儲的內存地址是連續的,固然數組中存儲對象時要複雜的多,咱們暫且假設其中的地址是連續的。下方的list就是咱們的順序線性表,count存儲的是已經存入到線性表中的元素個數,而capacity則是記錄線性表整個容量的大小。固然上述三個屬性都是private的,而下方的計算屬性length是internal類型的,供外界訪問,返回線性表元素的個數。
下方是總體結構,咱們下方會給出線性表具體的關鍵操做,並分析其時間複雜度。
1.往順序線性表中插入數據
有時候咱們會給據特定的算法往線性表中指定的位置插入數據,好比咱們常見的插入排序算法,若是你的數據是順序存儲的話,那麼就須要將數據插入到順序表中。接下來咱們就將數據插入到指定的位置。
下方該圖中是往順序表中插入一個元素的原理圖。在下圖中,咱們往AB與CD之間插入一個M。在插入M以前咱們須要作的事情就是將CD總體日後移動一個位置,爲M騰出一個位置,而後再講M這個元素進行插入。
順序表的插入仍是比較簡單的,也是很是好理解的,那麼用代碼實現起來也是用不了幾行代碼的。下方截圖就是是順序線性表的元素插入的具體實現的代碼。首先檢查index的合法性,檢查index是否在合理範圍內,而後將index後的元素依次日後移動,而後將元素插入便可。
2. 順序線性表的元素移除
上面介紹完元素的插入後,接下來要聊一下元素的移除。也就是移除指定索引中的元素。該過程剛好與上述插入的過程相反,上述在插入以前是相應的元素日後移,騰出index位置。而移除特定索引的元素時,是相應的元素左移,覆蓋掉要刪除的元素,而後將最後一個元素進行移除掉。下方的原理圖對此過程進行了說明。
該部分比較簡單,下方的代碼段就是將指定索引的元素進行移除。在線性表的順序存儲中,前驅和後繼的關係由內存地址的前後順序所關聯,因此插入和刪除元素會相對麻煩一些,而查找和修改元素就比較簡單了,直接由index能夠找到相應的元素,再次就不作過多贅述了。github上所分享的Demo中會有完整示例。
3、線性表的單鏈存儲
介紹完線性表的順序存儲,接下來咱們來介紹線性表的鏈式存儲。固然,本部分是對單向鏈表的介紹,下部分將會對雙向鏈表的介紹。下方截圖就是咱們單向鏈表相關示例的運行結果,首先咱們先正向的建立鏈表,也就是後來的元素插入到鏈表的後方。而後在給出逆向建立鏈表,與正向建立鏈表相反,後來的元素摻入到頭結點的後方。建立鏈表完畢後,咱們會給出鏈表元素的插入和移除的解決方案。
1.單向鏈表的建立
在鏈表建立以前,咱們得先建立節點的類,由於鏈表是多個節點的鏈接。下方這個OneDirectionLinkListNote類就是單向鏈表的節點類。其中的data屬性存儲的是該節點所存儲的數據,而變量next就是指向下一個節點的指針,鏈表中節點間的關係由next指針所關聯。init和deinit就是該類的構造和析構函數了,就不作過多贅述了。
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上分享的源代碼,由於篇幅有限,關於單向鏈表的更多細節就不作過多贅述了。
4、雙向鏈表
若是你對單向鏈表已經理解的話,那麼理解雙向鏈表來講並不是難事。雙向鏈表與單向鏈表相比多了一個指向前驅的節點。咱們暫且稱爲將指向前驅的節點命名我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上分享的源代碼。
5、面向接口編程的優勢
在上述咱們實現兩種鏈表時,咱們先定義了一個鏈表協議ListProtocalType。不管是雙向鏈表仍是單向鏈表都遵循這個協議,也就是說,該協議就是鏈表對外統一的接口,該協議就是操做鏈表的一個規範。下方的testLinkedList()就是咱們鏈表的測試方法,該函數的參數是遵循ListProtocalType協議的全部類的對象。也就是說只要遵循了ListProtocalType這個協議的類的對象,均可以做爲該函數的參數。至於具體操做,那麼不一樣的類會給出不一樣的操做的。
在調用該函數時,第一個傳入的是單向鏈表的類的對象,第二個是雙向鏈表的類的對象。雖然都是執行同一個方法,可是由於傳入的類的對象不一樣,因此執行的結果顯然是不一樣的。這也就是利用了面向對象的多態性,在以前設計模式系列的博客中介紹過,下方這種與策略模式相似。
本篇博客的內容也是挺多的了,固然博客中的內容是從Demo中挑出的關鍵點來說的,具體細節請看下方github上所分享的連接:
https://github.com/lizelu/DataStruct-Swift/tree/master/ListDataStruct