數據結構與算法小結(1)

一、概述

數據結構,即數據存放的方式。算法,解決問題的方法。討論數據結構與算法時,常常不會僅僅滿足於能解決一個特定的問題,而是在追求如何優雅而高效的解決一類問題。
本文針對學堂在線的數據結構課程的小結,用以鞏固知識點。課程主要介紹的是向量、鏈表、BST、堆等數據結構的特點以及在這些數據中存儲、訪問數據的具體的、不同的實現算法的比較,其中有大量的實例和具體的數據變換時數據結構的狀態,便於理解。

二、算法好壞的評判

  1. 算法的定義
    特定計算模型下,旨在解決特定問題的指令序列,需具備的特性:

    a.正確性:的確可以解決指定的問題。  //得到正確的結果
    b.確定性:任一算法都可以描述爲一個由基本操作組成的序列    //可以被轉化爲具體的操作步驟,且無二義性
    c.可行性:每一基本操作都可實現,且在常數時間內完成。
    d.有窮性:對於任何輸入,經有窮次基本操作,都可以得到輸出
    

    具備以上特性,只能說是一個算法,而好算法,還需要滿足這些要求:

    A.正確:能正確處理簡單的、大規模的、一般的、退化的(平凡的,即特例)、任意合法的輸入
    B.健壯:能辨別不合法的輸入並做適當處理    //任意非法輸入
    C.可讀:命名、註釋、統一風格   //便於理解和維護
    
  2. 算法優劣的評判
    漸進分析:大O記號,忽略常數,更關心輸入足夠大後的成本,注重考察成本的增長趨勢。

    漸進分析的量級
    常見遞歸式的解

    注意,有些特殊的算法在漸進意義上可以讓人滿意,但實際在使用時,由於被忽略的常數項太大、訪問內存地址非連續而導致無法利用CPU的高速緩存特性等原因,表現的效果與漸進分析結果相差很大,所以需在此特別指出,漸進分析只是一種主要的分析工具,但它不是評判算法好壞的硬性標準,需以具體算法在應用中的實際表現爲準。

  3. 算法正確性的證明:
    類似遞歸,由算法的不變性(處理方式固定) 和單調性(規模不斷縮減),最終得到算法的正確性。

  4. 遞歸到迭代的轉換:
    a. 記憶:將已計算過的實例的結果製表備查
    b. 動態規劃:顛倒計算方向,有自頂而下的遞歸,改爲自底而上的迭代。

三、向量

  1. 向量,是數組的抽象與泛化,由一組元素按線性次序封裝而成。
  2. 相關概念:

    輸入敏感:對於不同的輸入,算法所需時間相差很大,比如,對於無序向量的順序查找操作,最好需要O(1)常數,最差需要O(n)線性。
    
    有序性和無序性:是否存在相鄰的逆序對。而相鄰逆序對的數量可用於度量向量的逆序程度。
    
    判等器:比較元素之間是否相等。
    比較器:比較不同元素在邏輯上的大小關係。
    無序向量可以只有判等器,而沒有比較器;有序向量必須兩者都有。所謂有序的序,就是由比較器比較得到的順序。
    
    排序算法的穩定性:針對含有重複元素的向量而言的。所謂的穩定,就是相同的元素在排序前後的,它們之間的次序保持不變。
    
  3. 得到的啓發:

    在有序向量的去重(唯一化)算法中,其最終算法簡述如下:維持兩個指針,一個指針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)

實現

以向量來實現,並以向量末端爲棧頂。如此實現比以向量首端爲棧頂或列表的實現,效率更高。

應用

  1. 逆序輸出
    短除法,結果存入棧中,最後逆序輸出

    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;
        }
    }
    
  2. 遞歸嵌套
    括號匹配爲例,採用減而治之或分而治之。需要分解之前的情況爲分解之後情況的必要條件,而非充分條件。即,可以由分解後的情況並不是唯一可以推出分解前情況的解。
    重要條件:減去緊鄰的括號
    使用棧來實現:

    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對括號對應的合法表達式就有多少種。
    
  3. 延遲緩衝
    線性掃描,預讀。

    liunx:
        $echo $(表達式)
    dos:
        set /a 表達式
        表達式:(!0 ^<^< (1-2+3*4)) -5 *(6^|7)/(8^^9)
    
  4. 棧式計算
    中綴表達式的計算
    核心是將數值壓入棧,將操作符壓入另一個棧,根據棧頂的操作符與當前待壓入棧中的操作符的關係(如下圖),決定處理當前操作符,還是壓入、彈出棧頂操作符並運算,知道當前操作符可以入棧。
    這裏寫圖片描述

逆波蘭表達式(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的設置分別在遞歸的開始和結尾。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章