算法筆記(II) 數據結構

衆所周知,線性與非線性是數據結構分類的一個標準。對於線性數據結構來說,其數據的關係簡單一致,因此也呈顯一種表的表現形式,按照其數據的存儲形式分爲順序表和鏈表;順序表一般是就是我們常用的數組,因此考察數組操作是每一個計算機專業學生的基本功,數組是一種最爲簡單的線性結構,利於索引訪問,但是不利於查找等操作。對於存儲順序並非有序的鏈表來說,其無法採用索引讓計算機尋址操作,但是其操作靈活多變。同時,與數學一樣這些線性的表可以通過多個組合使用來表現非線性的結構:樹與圖;當然,在最近翻看的《計算機程序的構造與解釋》中,LISP語言將數據結構抽象成表結構,本質上其實現的是一種廣義表形式,既可以表現線性的結構也可以是複雜的非線性結構。

同時,我們甚至數據結構與數學中的代數結構等等結構都有一定的相通之處,他們都反映了數據以及數據的關係,這種關係反應的往往是一種序關係,隊和棧就是其中的典型。先入先出和先入後出,其操作表現的就是一種特殊的序關係。很多人說嚴蔚敏的《數據結構》一書編寫的不易讀,不過關於一些基本問題上卻是有很多閃光的地方,例如關於各書不明確的數據結構一詞有很好的說明,我覺得值得細讀一下:

“ 

 數據結構(Data Structure)是相互之間存在一種或者多種特定關係的數據元素的集合。這是本書對數據結構的一種簡單解釋。在任何問題中,數據元素都不是孤立存在的,而是在它們之間存在着某種關係,這種數據元素相互之間的關係稱之爲結構(Structure)。根據數據元素之間的關係的不同特性,通常有以下四類基本結構(如篇頭圖):(1)集合 結構中的數據元素之間除了“同屬於一個集合”的關係外,別無其他關係;(2)線性結構 結構中的數據元素之間存在着一個一對一的關係;(3)樹形結構 結構中的數據元素之間存在着一個對多個的關係;(4)圖狀結構或網狀結構 結構中的數據元素之間存在多個對多個的關係。數據結構的形式定義爲:數據結構是一個二元組 Data_Strucure = (D ,S),其中:D是數據元素的有限集,S是D上關係的有限集。                                                  

 ”

數據結構的定義顯然借鑑了數學上關於空間、數學結構等定義,例如空間(數學)的定義:空間是指一種具有特殊性質及一些額外結構的集合;這也讓我們看到了很多科學分支往往是數學名詞在新的context下的轉述。下面我們將按照上述分類講解,當然我們的大分類仍然基於線性關係以及非線性關係展開。

 

線性數據結構:隊列與棧

對於隊列和棧集中考察基本的實現與刪除、查詢、添加基本的數據結構操作上,這種線性的數據結構可以用數組(或者多個數組)實現,或者通過鏈表實現,一般簡單的應用可以方便的利用數組實現。同時,正如在以前日誌所提到的,隊列與棧是兩個非常有實際意義的線性數據結構,例如隊列可以用於時間上(或優先級)的模擬,這一點適合於操作系統的進程管理,消息傳遞等等,同時將隊列用於樹的層次遍歷,也就是寬度優先遍歷;對於棧,它的意義更加的深遠,從下推自動機,到遞歸的實現等等,都需要棧實現。同時也要明白隊列的特性適合表現事件的先後次序,而棧的特性適合保存現場和回覆現場,在操作系統中的中斷機制中有關鍵的應用。

非線性數據結構:樹與圖

對於非線性的數據結構主要集中在 樹 和 圖上,本質上樹是一種無環圖,其存儲和實現方式主要可以通過數組 和鏈表實現,對於數組的話可以通過兩個數組完成對樹的實現,一個數組記錄值,另一個數組記錄其父節點的索引(因爲對於每一個節點其顯然有不超過一個父節點);靈活的實現可以通過鏈表實現。

同時,考慮比樹更復雜的圖結構可以同樣通過數組實現,如前向星或後向星就是一種樹的數組存儲的推廣;數組實現的樹結構和圖結構利於編程和設計操作。

樹是一種具有良好性質的數據結構,分類和用途也很廣泛,其設計考慮到不同的應用:

 堆(這裏考慮二叉堆),特殊的樹,尤其特有的序關係;其方便提取最值,因此用於排序算法以及貪心算法;

 二叉樹,其設計考慮到數的序關係是一個全序關係且具有傳遞性,即a<b & b<c: a<c;因此其用於查找;

  考慮二叉樹這種高效的數據結構仍存在不足,如喪失平衡性的情況下,應該調整其結構使其平衡,提升效率。因此設計了很多高級的樹結構:平衡二叉樹,這些結構是在教科書中能看到的最複雜的數據結構,但是一般應用不會應用到,很少筆試題考到。

另外,我們考察一種獨特的數據結構:串,本質上串仍然是數組,他不過是一種編碼;串相關的內容仍然是考察的重點,有很多的題目關於字符串匹配以及基本處理,其中最爲著名的就是字符串的匹配和查找:

如KMP算法就包含了自動機理論。如何合理的提高字符串的處理效率是一項重要內容。

圖是相對最爲複雜的結構,廣義上圖是一種對象關係模型,並注意這是一種離散結構,區別於數學分析中的連續結構。之所以提到這一點是因爲,在圖中定義一些性質抑或結構將區別於連續空間,例如圖的距離就不可以簡單的應用連續空間常用的歐式距離等等。圖的算法是一般數據結構書中涉及到算是最複雜的算法,例如最小生成樹、常用的最短路徑算法、甚至網絡流的算法。

這裏提到一種特殊的圖,我們稱之爲DAG(Directed acyclic graph),也就是有向無環圖。這種圖有其特殊意義,我們知道很多計算機問題往往是線性的求解模式,如線性迭代抑或線性遞歸。對於一些複雜的問題,往往基於分而治之的思想,拆分問題,這樣一類問題往往呈現樹形遞歸的方式。同時注意樹是一種無環結構,否則將會無限的遞歸下去了。而因爲常常會求解相同的子問題(一個節點會有若干個前驅),所以按照樹的定義,這種樹形結構顯然並不是真正意義上的樹,本質上它是有向無環圖。按照我們上述的講解,大家很容易將DAG和動態規劃聯繫起來,事實確實是這樣。

並且在編譯原理中的,屬性值計算中就要利用各個節點的依賴關係,它們也是用DAG表示的。

 

另類的數據結構:哈希表

同時,一個很另類的數據結構就是哈希表了,哈希表的性能依賴於哈希函數的設計,它出色的將存儲內容與存儲索引建立了單射的映射關係(除非發生衝突,則是多對一),這是其高效的保證。哈希技術常常用於那些需要高速的查詢、修改以及插入的數據環境,如果要進行排序等功能則無法在線性時間下完成。哈希技術在計算機應用非常廣泛,如文件哈希用於電驢等下載工具定位資源文件,在編譯器設計中,一般利用哈希技術維護符號表,要比平衡二叉樹等要方便。同時,我們的解壓文件工具往往有SHA1以及CRC碼校驗、MD5等,事實上它們也是一種哈希函數計算得到的鍵值,而且他們相比發生衝突的可能性極低。

其實這裏就涉及了一個問題,是不是加密算法就是一種hash呢?似乎他們都在尋求一種一對一的映射關係(其實hash只需要單射就可以了,加密必須是一一映射)。這裏已經有位同學注意到了,這是他的Blog

2010-10-20

2010-11-13 補充部分內容

2011-3-8 補充Hash與加密算法的異同說明

再續

2011-3-11 修正一一映射錯誤,改作單射

不相交集合:並查集

並查集也是一種常用的數據結構,在實際中也有廣泛的實用。其實現也相當的簡單,通過一個索引表就可以完成。索引記錄的是節點的父節點,如下所示:

對於兩個不相交集合:

{1,2,3}   {4,5,6 } ; 我們分別以 1和4 作爲這兩個集合的代表元,則我們得到索引爲:

index:   1,1,1,4,4,4       

在檢索時,如果我們知道一個元素i與元素2同類,我們將遞歸的檢索2的父節點直到1,並將i的索引設置爲1;每種數據結構均支持不同的操作,並查集的實現結構是一種樹結構,但是顯然其省略了樹的序關係,因此支持的操作只有三個:初始化新元素、查找元素的所屬集合、合併集合;

 2010-11-24

再續

 

最近從新參看一些acm的課件,覺得其中關於數據結構的分類值得思考,正因爲對於數據結構這個定義來說,仍然並沒有任何確定的定義,而本文開頭所述的線性與非線性的劃分方式並沒能涵蓋繁複的數據結構,例如哈希表並不容易歸類到線性或非線性的結構中去,而對於並查集這種數據結構其本質是實現包括並操作以及查找操作的集合,而樹結構實現只是它的實現方式,因此單純的將其歸類到非線性結構是不可取的。而對比哈希表實現的是存儲內容與鍵值的映射關係,所以說我們的分類方式並沒有突出這個結構所蘊含的特徵。

下面參見NOCOW的wiki頁面,對數據結構的定義以及闡述都十分的透徹:例如在程序設計的時候,一旦確定數據結構了,往往算法也隨之確定,因爲算法往往是數據結構的一系列變換。而對數據結構的分類又有了相對於線性與非線性的傳統分類方法的補充,即按功能分類:

按功能分類的數據結構

對於集合的實現,往往體現在如何完成查詢、查找、修改、添加的功能,因此對於並查集實現集合常用的並運算以及查找元素所屬類的功能;對於字典來說(字典的高效實現主要爲trie以及hash兩種),實現了高效的查找以及添加等等操作。對於有序表實現體現了元素間的序關係,如前驅、後驅等等,而對於樹結構是父節點—子節點,兄弟節點等關係;對於hash來說,其是一種獨特的集合實現,通過hash function來完成存儲與內容的映射。

對於棧和隊列,在按存儲結構分類來說,是一種線性結構。但是這兩個獨特而功能強大的結構可以獨立成爲一種功能實現:棧用於保存現場,實現遞歸;隊列表示優先關係,方便事件模擬;

對於優先隊列(或稱爲堆),我覺得不應歸併於傳統的隊列中,事實上大多數數據結構書是將優先隊列獨立成章的(因爲傳統的數據結構書是以組織結構即線性與非線性來講解的,堆的實現往往是非線性實現方式,如二叉堆、左式堆)。但是對於按功能分類的話,則是一種廣義的隊列:實現優先關係。

對於雙端隊列來說,我認爲並不方便將其孤立的列出來。雙端隊列(deque,全名double-ended queue)在STL中有較佳的實現,並是STL中stack、queue的基本結構(priority_queue是基於vector實現的),這三種機構均是通過適配器實現的。

後面的矩陣、串、後綴樹以及剖析樹等,功能過於專一:矩陣這種結構以及相關的算法在MATLAB裏面就有很強的應用,串以及後綴樹在字符串檢索匹配等領域常用;剖析樹(parse tree)主要用於編譯器中的語法分析以及文檔解析上。相對上面的所述結構,則用途顯得窄些,在這裏就不敘述了。如果需要則再補充...

用STL來理解數據結構是一個很好的方式:因爲STL實現導向是應用以及功能驅動的,例如set的實現就是一個例子,儘管實現的方式是非常的複雜:紅黑樹,但是在用戶看來不論線性與非線性也好,體現是功能的實現。

下面參見網頁上的說明:

有序表的實現

優先隊列的實現

的表示

2010.12.2

 

重談stack, queue, heap(priority queue) 即棧、隊列、堆(優先隊列)

重新閱讀了相關書籍以及stl後,我對這三種最爲常用的數據結構又有了新的認識,原本打算將這段論述補充在上述關於棧與隊列部分的,但是我覺得爲了突出理解的過程性,我特意重點在此講述這三種結構與搜索方法上的關聯[wiki]。

我們知道分而治之是一種重要的算法設計思路,廣泛應用於多種算法中,如分治法,動態規劃等等,不同的搜索方式將決定使用何種數據結構進行輔助,反過來說,使用何種數據結構也就決定了我們的算法。例如使用stack將伴隨着DFS以及回溯,queue將對應於寬度優先搜索,而對於heap或者優先隊列來說,其伴隨着分治定界算法(B&B)[hudong baike]。不同的搜索方法維護不同的數據結構,也就造成了不同的搜索模式。

分支定界算法與回溯不同:

回溯使用DFS方式遍歷解答樹,是一種窮舉法的巧妙實現(因爲一定程度上避免擴展根本不可能的分支);分支定界的實現方式主要是寬度優先或者帶有啓發性的優先搜索方式,因此可以維護一個隊列結構或者一個優先隊列結構。

回溯法搜索解答樹時,不免要搜索本質上其實相同的情況(即解答樹上的節點)多次,但是在分支定界算法中,每一個節點只被擴展一次,對於子節點來說,我們會通過限界法捨棄那些不好的情況甚至不可行的情況。設想我們每次採用一個堆結構獲得當前最有可能會是最優解的節點優先擴展它,那我們似乎更有可能早一點搜索最優解。

2010.12.05

附記 2011-5-19:

在 回溯算法 和 DFS  的異同上存在爭議。回溯搜索似乎本身就是一種DFS,但是正如《數據結構與算法分析》中所言,回溯是一種有技巧的窮舉。在《人工智能——一種現代方法》中,作者精闢地提到: 回溯是DFS的一種變形,其使用的內存空間比DFS更少。原因有兩點:

  •  只記錄當前擴展的節點和下一個待擴展的節點,而不是記錄當前層所有待擴展的節點,這樣內存就下降了一個 分支數倍數b;
  •  通過修改狀態描述,恢復狀態描述來輔助搜索,將狀態描述壓縮爲只有一個,行動數目仍然是最大深度m。(很自然是一個壓棧和出棧,但是dancing link 給了我們新的啓發) 

自己的解釋能力有限,參見原文:

A variant of depth-first search called backtracking search uses still less memory. In backtracking, only one successor is generated at a time rather than all successors; each partially expanded node remembers which successor to generate next. In this way, only O(m) memory is needed rather than O(bm). Backtracking search facilitates yet another memory-saving (and time-saving) trick: the idea of generating a suc-cessor by modifying the current state description directly rather than copying it first. This reduces the memory requirements to just one state description and O(m) actions.

For this to work, we must be able to undo each modification when we go back to generate the next successor. For problems with large state descriptions, such as robotic assembly, these techniques are critical to success.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章