一、概述
數據結構,即數據存放的方式。算法,解決問題的方法。討論數據結構與算法時,常常不會僅僅滿足於能解決一個特定的問題,而是在追求如何優雅而高效的解決一類問題。
本文針對學堂在線的數據結構課程的小結,用以鞏固知識點。課程主要介紹的是向量、鏈表、BST、堆等數據結構的特點以及在這些數據中存儲、訪問數據的具體的、不同的實現算法的比較,其中有大量的實例和具體的數據變換時數據結構的狀態,便於理解。
二、算法好壞的評判
算法的定義
特定計算模型下,旨在解決特定問題的指令序列,需具備的特性:a.正確性:的確可以解決指定的問題。 //得到正確的結果 b.確定性:任一算法都可以描述爲一個由基本操作組成的序列 //可以被轉化爲具體的操作步驟,且無二義性 c.可行性:每一基本操作都可實現,且在常數時間內完成。 d.有窮性:對於任何輸入,經有窮次基本操作,都可以得到輸出
具備以上特性,只能說是一個算法,而好算法,還需要滿足這些要求:
A.正確:能正確處理簡單的、大規模的、一般的、退化的(平凡的,即特例)、任意合法的輸入 B.健壯:能辨別不合法的輸入並做適當處理 //任意非法輸入 C.可讀:命名、註釋、統一風格 //便於理解和維護
算法優劣的評判
漸進分析:大O記號,忽略常數,更關心輸入足夠大後的成本,注重考察成本的增長趨勢。
注意,有些特殊的算法在漸進意義上可以讓人滿意,但實際在使用時,由於被忽略的常數項太大、訪問內存地址非連續而導致無法利用CPU的高速緩存特性等原因,表現的效果與漸進分析結果相差很大,所以需在此特別指出,漸進分析只是一種主要的分析工具,但它不是評判算法好壞的硬性標準,需以具體算法在應用中的實際表現爲準。
算法正確性的證明:
類似遞歸,由算法的不變性(處理方式固定) 和單調性(規模不斷縮減),最終得到算法的正確性。遞歸到迭代的轉換:
a. 記憶:將已計算過的實例的結果製表備查
b. 動態規劃:顛倒計算方向,有自頂而下的遞歸,改爲自底而上的迭代。
三、向量
- 向量,是數組的抽象與泛化,由一組元素按線性次序封裝而成。
相關概念:
輸入敏感:對於不同的輸入,算法所需時間相差很大,比如,對於無序向量的順序查找操作,最好需要O(1)常數,最差需要O(n)線性。 有序性和無序性:是否存在相鄰的逆序對。而相鄰逆序對的數量可用於度量向量的逆序程度。 判等器:比較元素之間是否相等。 比較器:比較不同元素在邏輯上的大小關係。 無序向量可以只有判等器,而沒有比較器;有序向量必須兩者都有。所謂有序的序,就是由比較器比較得到的順序。 排序算法的穩定性:針對含有重複元素的向量而言的。所謂的穩定,就是相同的元素在排序前後的,它們之間的次序保持不變。
得到的啓發:
在有序向量的去重(唯一化)算法中,其最終算法簡述如下:維持兩個指針,一個指針j用於遍歷向量,另一個記錄應該保留的元素,當找到每一塊重複區域中的第一個元素,即一個新的不重複元素時,arr[i++] = arr[j],若元素存在arr[j]==arr[i],則i不變j++,最後將向量長度調整爲i即可。這種覆蓋無效元素處理方式,在時間和空間上都是高效的,值得借鑑。
四、列表
根據是否修改的數據結構,所有的操作大致分爲兩類方式
靜態:僅讀取,數據結構的內容及組成一般不變。類似於http的get操作,重複多次操作得到的結果相同。
動態:需寫入,數據結構的局部或整體將改變。類似於http協議中的post、remove等非冪等的操作。
對應的數據元素的存儲與組織方式也分爲兩種
靜態:通過物理內存的位置來體現數據的層級關係。如數組,左式堆等。
動態:物理位置任意,數據的層級關係由與數據綁定存儲的額外信息提供,如鏈表,二叉樹等。每個元素都會維護相應的引用,指向與其有關聯的元素所在存儲位置。
列表的相關概念
頭、首、末、尾節點,其秩可理解爲:-1、0、n-1、n。
頭尾哨兵的引用,使列表首節點前、末節點後插入元素以及查找元素等處的處理變得簡單。
前驅爲元素指向前一個節點的引用,後繼爲元素指向後一個節點的引用。進行插入、刪除元素等動態操作時,需同步維護相鄰元素的前驅、後繼。
補充
在不支持指針的語言中實現列表的方法:
兩個數組,一個爲elem[]保存數據,另一個在秩相同的位置保存後繼元素的秩(實現對後繼的引用),需對外提供首節點的秩。
五、棧
操作
壓棧 push
彈出 pop
查頂 top
棧深度 size
特點
先進後出(FILO)
實現
以向量來實現,並以向量末端爲棧頂。如此實現比以向量首端爲棧頂或列表的實現,效率更高。
應用
逆序輸出
短除法,結果存入棧中,最後逆序輸出void convert(Stack<char> &s, _int64 n, int base){ //base 待轉進制 static char digit[] = {'0','1','2','3','4','5','6','7'}; while(n > 0){ s.push(digit[n % base]); n /= base; } }
遞歸嵌套
以括號匹配爲例,採用減而治之或分而治之。需要分解之前的情況爲分解之後情況的必要條件,而非充分條件。即,可以由分解後的情況並不是唯一可以推出分解前情況的解。
重要條件:減去緊鄰的括號
使用棧來實現:bool paren(const char exp[],int lo,int hi){ //exp中只存有左右括號。 Stack<char> s; for(int i = lo;i < hi;i++){ if('(' == exp[i]){s.push(exp[i]);} else if(!s.empty()){s.pop();} else{return false;} } return s.empty(); } 計數器也可實現上述功能。+/- 但存在多個括號,如“[(])”時,因爲不能嵌套使用括號,所以使用棧,而計數器將無效。 可由此推廣到html標籤的嵌套檢查。
棧混洗:stack permutation
[棧底,棧頂>
長度爲n的序列,可能的棧混洗總數:(sp(n)<=n!全排列);
推導步驟:1號元素作爲第k個元素推入棧中時的總數:
sp(k-1)*sp(n-k)
對上式從1到n(k可能的取值)求和,得總數爲catalan(n) = (2*n)!/(n+1)!/n!。棧混洗的甄別:
任意三個元素能否按某相對次序出現於混洗中,與其他元素無關。
充要條件:對於任何1<=i < j < k <= n, k,i,j必然非棧混洗 (禁形,“3,1,2”必定無法洗出)甄別算法一: 任意i<j,不含j+1,i,j模式,即爲合適的棧混洗(n^2) 甄別算法二: 直接藉助棧A,B,S模擬棧混洗過程 pop前檢測S是否爲空;或需彈出的元素在S中,卻非頂元素 n的元素對應的棧混洗有多少種,n對括號對應的合法表達式就有多少種。
延遲緩衝
線性掃描,預讀。liunx: $echo $(表達式) dos: set /a 表達式 表達式:(!0 ^<^< (1-2+3*4)) -5 *(6^|7)/(8^^9)
棧式計算
中綴表達式的計算:
核心是將數值壓入棧,將操作符壓入另一個棧,根據棧頂的操作符與當前待壓入棧中的操作符的關係(如下圖),決定處理當前操作符,還是壓入、彈出棧頂操作符並運算,知道當前操作符可以入棧。
逆波蘭表達式(RPN,後綴表達式)的計算:
特點:不使用括號,即可表示優先級的運算關係。(但需引入另一個原字符,用於分隔相鄰的數值)
只使用一個棧來求值,從左向右掃描表達式,當遇到數值時壓入棧中,當遇到操作符時,取出相應的數值,運算後再次壓入棧中。
中綴表達式到後綴表達式的轉換:
1.用括號顯式地表示優先級
2.將運算符移到對應的括號後,
3.抹去所有的括號
六、樹
1. 相關概念
圖論中得到的結論:
樹:無環連通圖、極小連通圖、極大無環圖
任意節點通往根的路徑是唯一的。節點v ---- 路徑v到根的路徑 ---- 以節點爲根的子樹 (唯一的)
每個節點的直接孩子個數即爲該節點的出度數之和,所有節點的出度數之和爲n-1,即總邊數。n爲節點總數
半線性:祖先若存在,則必然唯一;後代若存在,則未必唯一。
根節點:所有節點的公共祖先,深度爲0。
葉子:沒有後代的節點稱爲葉子。
樹的高度:所有葉子深度中的最大者爲樹的高度。空樹高度爲-1.
樹的參考實現,使用鏈表數據結構(考慮查找、增加、刪除的效率),
每個節點中維護的引用關係由如下方式表示:
父親孩子法:
序號 | 節點數據 | 父節點序號 | 子節點列表集
長子兄弟法(主要):
序號 | 節點數據 | 父節點 | 第一個子節點 | 下一個兄弟節點
整個樹結構需維護的信息:
樹的高度:增刪時需更新樹高。
二叉樹:
非葉子節點度數不大於2
真二叉樹:
每個節點的出度爲偶數
二叉樹可以表示所有有根有序的樹:
長子--左節點
兄弟--右節點
完全二叉樹:
葉節點:僅限於最低兩層,底層葉子均居於次底層葉子左側;除末節點的父親,內部節點均有雙子。葉節點不少於內部節點的個數,但最多多出一個。
2. 樹的遍歷
先序、中序、後序遍歷:以樹根節點的位置區分,遞歸遍歷。三種遍歷都有後代先於祖先被訪問,即,存在逆序的,所以需藉助棧結構來實現遍歷操作
先序遍歷的遞歸實現:(根節點|左子樹|右子樹)
方法一,棧結構,根元素先入棧,後進行循環,每次循環彈出棧頂並訪問,同時若有右孩子則將其,再判斷若有左孩子,有也將其壓棧並進入下一個循環,直到棧空。(要先左後右,在壓棧時就要相反)
方法二,棧結構,不斷沿左側鏈訪問,並將右子樹的根節點入棧;當訪問元素爲空時(一條左側鏈訪問完畢),從棧中彈出一個節點,同樣沿着左側鏈訪問並將右子樹壓入同一個棧,循環至棧空。
中序遍歷的遞歸實現:(左子樹|根節點|右子樹)
棧結構,【沿根節點將所有左側鏈的元素壓入棧中,彈出棧頂並訪問,轉向其右孩子】,重複【】內的操作直至棧空
後序遍歷的遞歸實現:(左子樹|右子樹|根節點)
棧結構,根節點入棧,【檢查棧頂元素,若有左孩子,1.當左孩子(lc)還有右孩子(lc-rc)時,右孩子(lc-rc)入棧,然後纔是左孩子(lc),2.當左孩子(lc)沒有右孩子(lc-rc)時,直接入棧;若棧頂元素沒有左孩子時,棧頂元素的右孩子(rc)入棧,若右孩子(rc)也沒有,表明到達葉節點,彈出棧頂元素並訪問】,檢查棧頂元素,若爲上一個訪問元素的父親,則可直接訪問,否則必爲其右兄,重複【】。
層次遍歷:按樹的深度訪問
祖先節點必定先於後代訪問,即,順序訪問(自上而下,先左後右),藉助隊列結構實現
根節點入隊,循環取出首節點,訪問並將其左右節點順序放入隊列尾部,直至隊空。
七、圖
1. 基本概念
鄰接:同一條邊的兩個頂點,彼此鄰接。
自環:同一頂點自我鄰接,爲自環。
簡單圖:不含自環的圖,主要考察的對象。
關聯:頂點與其所屬的邊彼此關聯。
度:與同一頂點關聯的邊數。出入度,表示由頂點出、或指向頂點。
無向邊:邊的兩個頂點無次序。根據組成圖的邊有無向可將圖分爲無向圖、有向圖、混合圖。
有向圖可以表示無向圖和混合圖:一條無向邊可看做正反兩條有向邊。
DAG:有向無環圖
歐拉環路:各邊各出現一次。
哈密爾頓環路:各頂點各出現一次。
平面圖:可嵌於平面的圖。各邊互不相交。
平面圖的本質:任意平面圖滿足歐拉公式
v - e + f - c = 1
v:0維的元素,頂點
e:1維的元素,邊
f:2維的元素,區域面片
c:連通域的總數
對於平面圖:e<=3*n-6 = o(n) << n^2
邊的總數不可能超過頂點的總數
支撐樹:也稱生成樹(spanning tree),以無向圖爲研究對象。以某一節點爲根,通過裁剪部分邊,得到一棵樹,通過該樹,可以遍歷所有的節點。特點:每對節點之間的只有唯一的路徑,支撐並不唯一。當無向圖的每條邊都附帶權重時,若得到的支撐樹的所有路徑權重之和最小(與該無向圖的其他支撐樹相比),則稱其爲最小支撐樹。實際應用:網絡中的路由計算。
連通圖:無向圖中,任意兩個頂點之間都有路徑,則稱爲連通圖。
連通域:從一個節點可以通過遍歷到達的所有區域。一個圖可能不只一個連通域
連通分量:無向圖的極大連通子圖稱爲G的連通分量。
連通圖的連通分量爲其自身。非連通圖的連通分量有多個,每個極大的連通子圖即爲一個連通分量。
可達分量:與連通分量類似,可達分量用於描述有向圖。自一個頂點通過有向圖可以達到的極大子圖即爲一個可達分量。
遍歷:
將圖變爲樹。
將數變爲線性表。
2. 圖的表示
鄰接矩陣:
頂點個數*頂點個數的二維矩陣,如若(u, v)有向邊存在,則將第u行第v列置1,否則置0。
適用:經常檢測邊的存在、做邊的插入刪除操作、稠密圖、圖規模固定(頂點樹固定)
關聯矩陣:
頂點個數*邊的個數的二維矩陣,若頂點與邊關聯,則置1,否則置0.
鄰接表:使用數組存儲頂點,每個頂點維持一個列表,只存儲存在的邊(存儲邊的另一個關聯頂點即可)
適用:經常計算頂點的度數、遍歷、稀疏圖、頂點數目不固定。
3. 圖的遍歷:
廣度優先搜索BFS:
-> 從一個節點開始,一次訪問所有尚未訪問的鄰接頂點,再循環訪問直至完成。
-> 使用隊列實現,等同於樹的廣度優先遍歷。最後會得到一顆支撐樹。
-> 遍歷所有的頂點,若未被訪問,則從其開始做BFS搜索。直到所有頂點被訪問。最後可能有多顆樹。
-> 邊的分類,一個頂點訪問後,找到一條未發現的邊,若邊的另一個鄰接頂點標記爲未訪問,則將該邊置爲樹邊(在最後支撐樹種存在的邊),否則若爲已訪問,則將該邊標記爲跨邊(不會再最後的支撐樹中出現)。
->一個連通/可達分量會得到一個支撐樹。
深度優先搜索DFS:
自頂點起,若有尚未訪問的頂點,則任取其一,遞歸執行DFS,否則返回。
最初頂點狀態初始化爲undiscovered,從某一頂點剛進入DFS時,設置dTime以及狀態爲discovered,表示開始訪問時間,當找不到未訪問頂點時,在退出DFS之前設置fTime以及裝填visited,表示訪問結束時間。
根據邊關聯的頂點狀態判斷並標識邊:當前頂點狀態一定爲discovered)
當前頂點 ---> undiscovered 樹邊
當前頂點 ---> discovered 後向邊(迴路)
當前頂點 ---> visited (當前頂點更早被發現) 前向邊(只在有向圖中出現)
當前頂點 ---> visited (當前頂點較晚發現) 跨邊
等效於樹的先序遍歷,最後也會得到一顆支撐樹。
括號引理:
頂點活躍期:fTime - dTime
一個頂點u爲另一個頂點v的祖先,當且僅當u的活躍期包含v的活躍期。
u與v無關當且僅當兩者活躍期無交集。
原因:使用遞歸,dTime、fTime的設置分別在遞歸的開始和結尾。