1. 程式人生 > >淺談演算法和資料結構----無向圖相關演算法基礎

淺談演算法和資料結構----無向圖相關演算法基礎

最近幾個專案用到了求所有最小哈密爾頓迴路,貪婪遍歷查詢等演算法,都是自己想或者查論文,雖然都是資料結構的基礎內容,但感覺比較零散,很糾結。

前幾天突然聽到“圖計算”這個名詞,覺得應該是找到組織了,因此轉載如下,後續會不斷轉載其他有用的文章。

以下內容轉載自:http://www.cnblogs.com/yangecnu/p/Introduce-Undirected-Graphs.html

從這篇文章開始介紹圖相關的演算法,這也是Algorithms線上課程第二部分的第一次課程筆記。

圖的應用很廣泛,也有很多非常有用的演算法,當然也有很多待解決的問題,根據性質,圖可以分為無向圖和有向圖。本文先介紹無向圖,後文再介紹有向圖。

之所以要研究圖,是因為圖在生活中應用比較廣泛:

graph application

無向圖

圖是若干個頂點(Vertices)和邊(Edges)相互連線組成的。邊僅由兩個頂點連線,並且沒有方向的圖稱為無向圖。 在研究圖之前,有一些定義需要明確,下圖中表示了圖的一些基本屬性的含義,這裡就不多說明。

graph terminology 

圖的API 表示

在研究圖之前,我們需要選用適當的資料結構來表示圖,有時候,我們常被我們的直覺欺騙,如下圖,這兩個其實是一樣的,這其實也是一個研究問題,就是如何判斷圖的形態。

two drawings of the same graph

要用計算機處理圖,我們可以抽象出以下的表示圖的API:

Graph API

Graph的API的實現可以由多種不同的資料結構來表示,最基本的是維護一系列邊的集合,如下:

Setofedges graph representation

還可以使用鄰接矩陣來表示:

adjacency-matrix graph

也可以使用鄰接列表來表示:

adjacency-list graph

由於採用如上方式具有比較好的靈活性,採用鄰接列表來表示的話,可以定義如下資料結構來表示一個Graph物件。

public class Graph
{
    private readonly int verticals;//頂點個數
    private int edges;//邊的個數
    private List<int>[] adjacency;//頂點聯接列表

    public Graph(int vertical)
    {
        this.verticals = vertical;
        this
.edges = 0; adjacency=new List<int>[vertical]; for (int v = 0; v < vertical; v++) { adjacency[v]=new List<int>(); } } public int GetVerticals () { return verticals; } public int GetEdges() { return edges; } public void AddEdge(int verticalStart, int verticalEnd) { adjacency[verticalStart].Add(verticalEnd); adjacency[verticalEnd].Add(verticalStart); edges++; } public List<int> GetAdjacency(int vetical) { return adjacency[vetical]; } }

圖也分為稀疏圖和稠密圖兩種,如下圖:

在這兩個圖中,頂點個數均為50,但是稀疏圖中只有200個邊,稠密圖中有1000個邊。在現實生活中,大部分都是稀疏圖,即頂點很多,但是頂點的平均度比較小。

sparse and dense graph 

採用以上三種表示方式的效率如下:

Graph representation efficiency 

在討論完圖的表示之後,我們來看下在圖中比較重要的一種演算法,即深度優先演算法:

深度優先演算法

在談論深度優先演算法之前,我們可以先看看迷宮探索問題。下面是一個迷宮和圖之間的對應關係:

迷宮中的每一個交會點代表圖中的一個頂點,每一條通道對應一個邊。

maze and graph

迷宮探索可以採用Trémaux繩索探索法。即:

  • 在身後放一個繩子
  • 訪問到的每一個地方放一個繩索標記訪問到的交會點和通道
  • 當遇到已經訪問過的地方,沿著繩索回退到之前沒有訪問過的地方:

圖示如下:

Tremaux maze exploration

下面是迷宮探索的一個小動畫:

maze exploration

深度優先搜尋演算法模擬迷宮探索。在實際的圖處理演算法中,我們通常將圖的表示和圖的處理邏輯分開來。所以演算法的整體設計模式如下:

  • 建立一個Graph物件
  • 將Graph物件傳給圖演算法處理物件,如一個Paths物件
  • 然後查詢處理後的結果來獲取資訊

下面是深度優先的基本程式碼,我們可以看到,遞迴呼叫dfs方法,在呼叫之前判斷該節點是否已經被訪問過。

public class DepthFirstSearch
{
    private bool[] marked;//記錄頂點是否被標記
    private int count;//記錄查詢次數

    private DepthFirstSearch(Graph g, int v)
    {
        marked = new bool[g.GetVerticals()];
        dfs(g, v);
    }

    private void dfs(Graph g, int v)
    {
        marked[v] = true;
        count++;
        foreach (int vertical in g.GetAdjacency(v))
        {
            if (!marked[vertical])
                dfs(g,vertical);
        }
    }

    public bool IsMarked(int vertical)
    {
        return marked[vertical];
    }

    public int Count()
    {
        return count;
    }
}

試驗一個演算法最簡單的辦法是找一個簡單的例子來實現。

trace of depth-first search

深度優先路徑查詢

有了這個基礎,我們可以實現基於深度優先的路徑查詢,要實現路徑查詢,我們必須定義一個變數來記錄所探索到的路徑。

所以在上面的基礎上定義一個edgesTo變數來後向記錄所有到s的頂點的記錄,和僅記錄從當前節點到起始節點不同,我們記錄圖中的每一個節點到開始節點的路徑。為了完成這一日任務,通過設定edgesTo[w]=v,我們記錄從v到w的邊,換句話說,v-w是做後一條從s到達w的邊。 edgesTo[]其實是一個指向其父節點的樹。

public class DepthFirstPaths
{
    private bool[] marked;//記錄是否被dfs訪問過
    private int[] edgesTo;//記錄最後一個到當前節點的頂點
    private int s;//搜尋的起始點

    public DepthFirstPaths(Graph g, int s)
    {
        marked = new bool[g.GetVerticals()];
        edgesTo = new int[g.GetVerticals()];
        this.s = s;
        dfs(g, s);
    }

    private void dfs(Graph g, int v)
    {
        marked[v] = true;
        foreach (int w in g.GetAdjacency(v))
        {
            if (!marked[w])
            {
                edgesTo[w] = v;
                dfs(g,w);
            }
        }
    }

    public bool HasPathTo(int v)
    {
        return marked[v];
    }

    public Stack<int> PathTo(int v)
    {

        if (!HasPathTo(v)) return null;
        Stack<int> path = new Stack<int>();

        for (int x = v; x!=s; x=edgesTo[x])
        {
            path.Push(x);
        }
        path.Push(s);
        return path;
    }
}

 Trace depth-first search of computer 5

上圖中是黑色線條表示 深度優先搜尋中,所有定點到原點0的路徑, 他是通過edgeTo[]這個變數記錄的,可以從右邊可以看出,他其實是一顆樹,樹根即是原點,每個子節點到樹根的路徑即是從原點到該子節點的路徑。

下圖是深度優先搜尋演算法的一個簡單例子的追蹤。

 trace depth-first search

廣度優先演算法

通常我們更關注的是一類單源最短路徑的問題,那就是給定一個圖和一個源S,是否存在一條從s到給定定點v的路徑,如果存在,找出最短的那條(這裡最短定義為邊的條數最小)

深度優先演算法是將未被訪問的節點放到一個堆中(stack),雖然在上面的程式碼中沒有明確在程式碼中寫stack,但是 遞迴 間接的利用遞迴堆實現了這一原理。

和深度優先演算法不同, 廣度優先是將所有未被訪問的節點放到了佇列中。其主要原理是:

  • 將 s放到FIFO中,並且將s標記為已訪問
  • 重複直到佇列為空
  1. 移除最近最近新增的頂點v
  2. 將v未被訪問的節點新增到佇列中
  3. 標記他們為已經訪問

廣度優先是以距離遞增的方式來搜尋路徑的。

class BreadthFirstSearch
{
    private bool[] marked;
    private int[] edgeTo;
    private int sourceVetical;//Source vertical

    public BreadthFirstSearch(Graph g, int s)
    {
        marked=new bool[g.GetVerticals()];
        edgeTo=new int[g.GetVerticals()];
        this.sourceVetical = s;
        bfs(g, s);
    }

    private void bfs(Graph g, int s)
    {
        Queue<int> queue = new Queue<int>();
        marked[s] = true;
        queue.Enqueue(s);
        while (queue.Count()!=0)
        {
            int v = queue.Dequeue();
            foreach (int w in g.GetAdjacency(v))
            {
                if (!marked[w])
                {
                    edgeTo[w] = v;
                    marked[w] = true;
                    queue.Enqueue(w);
                }
            }
        }
    }

    public bool HasPathTo(int v)
    {
        return marked[v];
    }

    public Stack<int> PathTo(int v)
    {
        if (!HasPathTo(v)) return null;

        Stack<int> path = new Stack<int>();
        for (int x = v; x!=sourceVetical; x=edgeTo[x])
        {
            path.Push(x);
        }
        path.Push(sourceVetical);
        return path;
    }

}

廣度優先演算法的搜尋步驟如下:

trace of bread first search

廣度優先搜尋首先是在距離起始點為1的範圍內的所有鄰接點中查詢有沒有到達目標結點的物件,如果沒有,繼續前進在距離起始點為2的範圍內查詢,依次向前推進。

breadth first search

總結

本文簡要介紹了無向圖中的深度優先和廣度優先演算法,這兩種演算法時圖處理演算法中的最基礎演算法,也是後續更復雜演算法的基礎。其中圖的表示,圖演算法與表示的分離這種思想在後續的演算法介紹中會一直沿用,下文將講解無向圖中深度優先和廣度優先的應用,以及利用這兩種基本演算法解決實際問題的應用。


相關推薦

演算法資料結構----相關演算法基礎

最近幾個專案用到了求所有最小哈密爾頓迴路,貪婪遍歷查詢等演算法,都是自己想或者查論文,雖然都是資料結構的基礎內容,但感覺比較零散,很糾結。 前幾天突然聽到“圖計算”這個名詞,覺得應該是找到組織了,因此轉載如下,後續會不斷轉載其他有用的文章。 以下內容轉載自:http:/

查詢演算法 演算法資料結構: 七 二叉查詢樹 演算法資料結構: 十一 雜湊表

閱讀目錄 1. 順序查詢 2. 二分查詢 3. 插值查詢 4. 斐波那契查詢 5. 樹表查詢 6. 分塊查詢 7. 雜湊查詢   查詢是在大量的資訊中尋找一個特定的資訊元素,在計算機應用中,查詢是常用的基本運算,例如編譯程式中符號表的查詢。本文

演算法資料結構: 五 優先順序佇列與堆排序

在很多應用中,我們通常需要按照優先順序情況對待處理物件進行處理,比如首先處理優先順序最高的物件,然後處理次高的物件。最簡單的一個例子就是,在手機上玩遊戲的時候,如果有來電,那麼系統應該優先處理打進來的電話。 在這種情況下,我們的資料結構應該提供兩個最基本的操作,一個是返回最高優先

演算法資料結構: 八 平衡查詢樹之2-3樹

前面介紹了二叉查詢樹(Binary Search Tree),他對於大多數情況下的查詢和插入在效率上來說是沒有問題的,但是他在最差的情況下效率比較低。本文及後面文章介紹的平衡查詢樹的資料結構能夠保證在最差的情況下也能達到lgN的效率,要實現這一目標我們需要保證樹在插入完成之後

演算法資料結構: 九 平衡查詢樹之紅黑樹

前面一篇文章介紹了2-3查詢樹,可以看到,2-3查詢樹能保證在插入元素之後能保持樹的平衡狀態,最壞情況下即所有的子節點都是2-node,樹的高度為lgN,從而保證了最壞情況下的時間複雜度。但是2-3樹實現起來比較複雜,本文介紹一種簡單實現2-3樹的資料結構,即紅黑樹(

演算法資料結構: 十 平衡查詢樹之B樹

前面講解了平衡查詢樹中的2-3樹以及其實現紅黑樹。2-3樹種,一個節點最多有2個key,而紅黑樹則使用染色的方式來標識這兩個key。 維基百科對B樹的定義為“在電腦科學中,B樹(B-tree)是一種樹狀資料結構,它能夠儲存資料、對其進行排序並允許以O(log n)的時間複雜度執行進行查詢、順序讀取、插入和刪

演算法資料結構: 十一 雜湊表

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是他們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼有沒有查詢效率更高的資料結構呢,答案就是本文接下來要介紹了

演算法資料結構: 四 快速排序

上篇文章介紹了時間複雜度為O(nlgn)的合併排序,本篇文章介紹時間複雜度同樣為O(nlgn)但是排序速度比合並排序更快的快速排序(Quick Sort)。 快速排序也是一種採用分治法解決問題的一個典型應用。在很多程式語言中,對陣列,列表進行的非穩定排序在內部實現中都使用的是快速排序。而且快速排序在

演算法資料結構: 六 符號表及其基本實現

前面幾篇文章介紹了基本的排序演算法,排序通常是查詢的前奏操作。從本文開始介紹基本的查詢演算法。 在介紹查詢演算法,首先需要了解符號表這一抽象資料結構,本文首先介紹了什麼是符號表,以及這一抽象資料結構的的API,然後介紹了兩種簡單的符號表的實現方式。 一符號表 在開始介紹查詢演算法之前,我們需要定義一個名

演算法資料結構: 三 合併排序

合併排序,顧名思義,就是通過將兩個有序的序列合併為一個大的有序的序列的方式來實現排序。合併排序是一種典型的分治演算法:首先將序列分為兩部分,然後對每一部分進行迴圈遞迴的排序,然後逐個將結果進行合併。   合併排序最大的優點是它的時間複雜度為O(nlgn),這個是我們之前的選擇排序和插入排序所達不到的。他還

演算法資料結構: 一 棧佇列

最近晚上在家裡看Algorithems,4th Edition,我買的英文版,覺得這本書寫的比較淺顯易懂,而且“圖碼並茂”,趁著這次機會打算好好學習做做筆記,這樣也會印象深刻,這也是寫這一系列文章的原因。另外普林斯頓大學在Coursera 上也有這本書同步的公開課,還有另外一門演算法分析課,這門課程的作者也是

演算法資料結構: 二 基本排序演算法

本篇開始學習排序演算法。排序與我們日常生活中息息相關,比如,我們要從電話簿中找到某個聯絡人首先會按照姓氏排序、買火車票會按照出發時間或者時長排序、買東西會按照銷量或者好評度排序、查詢檔案會按照修改時間排序等等。在計算機程式設計中,排序和查詢也是最基本的演算法,很多其他的演算法都是以排序演算法為基礎,在一般的資

演算法資料結構(11):雜湊表

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是它們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼

演算法資料結構(7):二叉查詢樹

前文介紹了符號表的兩種實現,無序連結串列和有序陣列,無序連結串列在插入的時候具有較高的靈活性,而有序陣列在查詢時具有較高的效率,本文介紹的二叉查詢樹(Binary Search Tree,BST)這一資料結構綜合了以上兩種資料結構的優點。 二叉查詢樹具有很高的靈活性

演算法資料結構:雜湊表

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是它們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼有沒

[從今天開始修煉資料結構]的應用 —— 拓撲排序關鍵路徑演算法

上一篇文章我們學習了最短路徑的兩個演算法。它們是有環圖的應用。下面我們來談談無環圖的應用。   一、拓撲排序     博主大學學的是土木工程,在老本行,施工時很關鍵的節約人力時間成本的一項就是流水施工,鋼筋沒綁完,澆築水泥的那幫兄弟就得在那等著,所以安排好流水施工,讓工作週期能很好地銜接就很關鍵。這樣的工程活

拒絕調包俠,不需要高階演算法資料結構技巧

前言 大多數工科學生或者剛剛入門近年來比較火的“人工智慧”相關演算法的同學,在選擇語言的時候,都會選擇MATLAB、Python、R等等這些高階語言,對自己所學的演算法進行實現和除錯。這些高階語言中,包含了實現複雜演算法的基礎數學演算法、基本統計演算法、基礎資料結構的實現,比如均值(mean)、方差(std

java中各種演算法資料結構的使用場景

一。通用資料結構:陣列,連結串列,樹,雜湊表 通用資料結構通過關鍵字的值來儲存並查詢資料,如報表,合同,記錄,業績等資料。通用資料結構可以用速度的快慢來分類,陣列和連結串列是最慢的,樹相對較快,雜湊表是最快的。請注意,並不是最快的就一定是最好的,因為最快的結構的

演算法資料結構

資料結構 堆 長度為n的陣列構建成最小堆的時間複雜度 B、B+樹、紅黑樹 說一下B+樹和二叉搜尋樹的區別? 說一下二叉搜尋樹和AVL樹、紅黑樹之間的差別 說下紅黑樹原理,紅黑樹你看虛擬碼的時候他有兩坨比較一樣的有沒有注意過 哪些情況下用棧 知道雜湊嗎?二叉樹比

演算法資料結構單鏈表的逆轉

public void reverse(SinglyList<T> list) { Node<T> p = list.head.next, succ = null, front = null; while (p != null) {