前端小白的數據結構學習總結——圖


什么是圖

圖是一種非線性的數據結構,是對網的一種抽象的理解,比如說中國鐵路網:
鐵路網
圖片中可以看到,每個城市之間的由鐵路連成了網,這個網中城市則為“點”,鐵路則為“線”,那么我們這個“網”再抽象一點,就成了這樣的一個圖:

圖1-1

通常我們用G=(V, E)來表示圖

一些概念

  • vertex:上圖中的圓表示一個城市,在圖的概念中我們稱其為“vertex(頂點)”
  • edge:與頂點相連的表示的是城市之間的鐵路,在圖的概念中我們稱其為“Edge(邊)”
  • weight(權):北京到上海鐵路1400多公里,那么這個1400就是這邊的weight(權),通常這種帶有權的圖,我們把他稱之為,比如中國鐵路網,就是一個帶權的圖
  • degree(度):一座城市鏈接的鐵路的數目,也就是與頂點相連接的邊的數目我們稱其為“degree(度),在有向圖中,因為邊存在方向,所以度還分為入度(ID)出度(OD)

無向圖和有向圖

上文中提到了Edge(邊),這個邊可以是具備方向的,那么有向圖就很好理解了,邊具備方向,像這樣:

有向圖

上圖中頂點之間的有用箭頭來表示方向,這種邊我們成為“弧(arc)”用<A, B>表示,而弧呢又分為弧頭弧尾,A -> B這樣的一個弧中,我們將頂點A成為弧尾將頂點B成為弧頭,通常用尖括號表示弧。
與之相反,無向圖就是邊沒有方向的圖,用小括號來表示一個邊(A,B)

圖的表示方式

我們可以用多種形式來標識一個圖,有哪些頂點,以及頂點之間的鏈接關系

鄰接矩陣

以矩陣的方式來描述一張圖,橫豎都是頂點,如果兩頂點連接,那么這個位置的值為1,如果不連接則值為0

鄰接矩陣

開發時,我們可以定義一個二維數組,那么這個數組應該是這樣的

let array = [
//   A  B  C  D  E  F  G  H  I
    [0, 1, 1, 1, 0, 0, 0, 0, 0],// A
    [1, 0, 0, 0, 1, 1, 0, 0, 0],// B
    [1, 0, 0, 1, 0, 0, 1, 0, 0],// C
    [1, 0, 1, 0, 0, 0, 1, 1, 0],// D
    [0, 1, 0, 0, 0, 0, 0, 0, 1],// E
    [0, 1, 0, 0, 0, 0, 0, 0, 0],// F
    [0, 0, 1, 1, 0, 0, 0, 0, 0],// G
    [0, 0, 0, 1, 0, 0, 0, 0, 0],// H
    [0, 0, 0, 0, 1, 0, 0, 0, 0] // I
]

這里我們就用二維數組描述了一個圖,但是上面我們有提到權的概念,如果是有權的圖,那么這里二維數組中的值就應該是個權的值,但是這樣一想,如果數值標識權的話,沒有聯通的頂點還弄用0表示嗎?其實加權圖的鄰接矩陣中用無窮表示未連接,應該是這樣:

加權鄰接矩陣

鄰接矩陣會存在一個問題,當這個圖為稀疏圖,即邊相對於頂點很少的時候,可能會出現矩陣中大部分都是0,只有極少數為1,但是內存中還是會分配一個這么大的內存空間,這就造成了內存的浪費

鄰接表

鄰接表則是通過數組或者鏈表或者Map來描述一個圖的鏈接關系

鄰接表

上圖中用表來描述了一個圖,以Map為例,key則為頂點,而value則為與頂點相連接的頂點集合,js中可能是這樣的

let map = {
    "A" : ["B", "C", "D"],
    "B" : ["A", "E", "F"],
    "C" : ["A", "D", "G"]
    // ... 
}

但是如果說邊是帶權的,那鄰接表應該如何來描述呢?首先value這里如果是頂點的集合,那就是不行的,不能清楚的描述,所以value應該是邊的集合

let map = {
    "A" : [
        {
            vertex : "B",
            weight : 100
        },
        {
            vertex : "C",
            weight : 1
        }
    ]
    // ...
}

實現一個Graph類

首先構造函數中初始化一個map,用來存放頂點以及邊的數據,我們以一個有向的無權圖為例

有向圖

class Graph{
    constructor(){
         this.vertexMap = new Map();   
    }
}

這個這個圖可以添加頂點,將頂點傳入,先判斷是否重復傳入,如果圖中已經存在則不再添加

class Graph{
    constructor(){
         this.vertexMap = new Map();   
    }
    
    addVertex(vertex){
        // 先判斷頂點是否已經添加,如果已經添加則不再添加
        if(!this.vertexMap.has(vertex)){
            this.vertexMap.set(vertex, []);
        }
    }
    
    // 同時,定義一個獲取所有頂點的方法
    getVertexes(){
        return this.vertexMap.keys();
    }
}

添加完了頂點我們還能添加邊,要描述邊,那么自然需要兩個頂點來描述,所以這個方法傳入兩個參數,第一個參數為弧尾,第二個參數為弧頭

class Graph{
    constructor(){
         this.vertexMap = new Map();   
    }
    
    addEdge(vertex1, vertex2){
        // 先判斷弧頭是否經存在,不存在的話先保存弧頭
        if(!this.vertexMap.has(vertex1)){
            this.vertexMap.set(vertex1, [vertex2]);
        }else{
            this.vertexMap.get(vertex1).push(vertex2);
        }
    }
}

基本的方法實現完了,實現一個打印方法看一看

class Graph{
    constructor(){
         this.vertexMap = new Map();   
    }
    
    print(){
        for(let item of this.vertexMap){
            console.log(item[0] + "-->", item[1]);
        }
    }
}

let g = new Graph();
let vertex = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];

g.addEdge("A", "B");
g.addEdge("A", "C");
g.addEdge("A", "D");
g.addEdge("B", "E");
g.addEdge("B", "F");
g.addEdge("C", "D");
g.addEdge("C", "G");
g.addEdge("D", "G");
g.addEdge("D", "H");

g.print();

最終執行以下能看到結果

A--> [ 'B', 'C', 'D' ]
B--> [ 'E', 'F' ]
C--> [ 'D', 'G' ]
D--> [ 'C', 'G', 'H' ]
E--> [ 'I' ]

圖的遍歷

我們圖中任意一個頂點都可以作為遍歷的起點,遍歷的就意味着要確保每一個頂點都被訪問到,其實遍歷的思路非常簡單,首先獲取到起始點,然后根據起始點的邊找到其連接的頂點,然后這樣一直循環下去,但是這里就會出現兩種情況深度優先遍歷廣度優先遍歷

廣度優先搜索

BFS(Breadth Frist Search),顧名思義先大范圍搜索,比如說對於這個圖
有向圖

假如我們以頂點A為起點,那么廣度優先搜索的順序就應該是A-B-C-D-E-.....

BFS

那么我們的遍歷邏輯應該是

  1. 選取一點為起始點
  2. 找到該點的鄰接點
  3. 遍歷鄰接點找到各自的鄰接點
  4. 一直循環知道所有的點都訪問到

深度優先搜索

DFS(Depth First Search),很好理解,先順着一條路徑一直訪問頂點,還是一這個圖為例
有向圖

那么頂點的訪問順序應該是A-B-E-I-F-C-G-D-H

DFS

那么我們的邏輯應該是(有點混亂,后面會詳細說)

  1. 選取起始點
  2. 獲得起始點的第一個鄰接點
  3. 循環上個步驟直到這條路徑走完訪問到最后一個頂點
  4. 向上返回到分支的地方訪問另一個鄰接點
  5. 繼續走完這個路徑

兩種算法的相同點和不同點

其實對於頂點來說,無非就是三種狀態

  1. 沒找到
  2. 找到了
  3. 找到了並且獲取了所有的鄰接點

那么我們在遍歷的過程中,需要針對頂點不同的狀態做不同的邏輯處理,比如

  1. 遍歷的時候發現這個頂點之前已經獲取了所有的鄰接點,那么就不會再訪問了
  2. 上面介紹深度優先搜索的時候中,遍歷邏輯步驟4,當我們一條路徑走完后,需要往回走找到分支點,那么這個分支點是什么呢?這便是已經找到的但是沒有獲取鄰接點的頂點

其實無論是深度優先還是廣度優先,上述的思路都是一致的,但是不同點在哪呢,主要是就在於上文中提到的容器,我們這里放入容器的順序是

  1. 找到頂點
  2. 按順序將頂點放入容器
  3. 按順序取出頂點
  4. 獲取取出的頂點的鄰接點
  5. 再循環到步驟2

上文提到了按順序,放進容器按順序,從容器中拿出按順序,而這兩個算法的不同點就在於順序不同。

  • 深度優先搜索:將數據儲存在(先入后出)中
  • 廣度優先搜索:將數據存儲在隊列(先入先出)中

還是以這個圖為例,我們用數組模擬棧和隊列:

有向圖

深度優先搜索,以頂點A為起始點舉例

// 定義一個棧,入棧從數組尾部添加,出棧從數組尾部取值
let stack = []

// 首先找到起始點A的鄰接點, 並且入棧,A標記為不再訪問
stack = [D, C, B]
// 從尾部出棧,把B取出來,獲取B的鄰接點, B被標記為不再訪問
stack = [D, C]  
B - E ,  B - F
// E, F入棧
stack = [D, C, F, E]
// E出棧獲取鄰接點
stack = [D, C, F]
E - I
// I 入棧
stack = [D, C, F, I]
// I從出棧獲取鄰接點
stack = [D, C, F]
I - E
// F出棧
// .......

廣度優先搜索,同樣以頂點A為起始點舉例

// 定義一個隊列, 添加從數組尾部添加,取出從數組首部取值
let queue = []

// 首先找到起始點A的鄰接點,並且加入隊列,A標記為不再訪問
queue = [B, C, D]
// 從隊列首部取出頂點B,獲取頂點B的鄰接點,B被標記為不再訪問
queue = [C, D]
B - E, B - F
// E, F入隊列
queue = [C, D, E, F]
// 從首部取出頂點C,獲取領接點,C被標記為不再訪問
queue = [D, E, F]
C - G, C - D
// D已經存在於隊列中,所以只有G入隊列
queue = [D, E, F, G]
// 從隊列中取出頂點D.....

深度優先搜索代碼實現

前面的例子中,我們提到了用代碼實現了一個Graph類,並且添加了一些頂點和邊,最終打印了結果,接下來,我們接着這個例子,用代碼實現一個深度優先搜索,這里就直接貼了,代碼注釋中有講解

// 首先定義一個DFS函數,因為是要對圖進行深度優先搜索,所以參數接收一個Graph的實例
let DFS = function(graph){
    // 接收到實例后,首先獲取圖中的頂點
    let vertexes = graph.getVertexes();
    // 定義一個set保存不用再訪問的頂點
    let notVisitAgainVertexes = new Set();

     // 定義一個訪問節點的方法
    let visit = function(vertex){
        // 先判斷當前頂點是不是不用再次訪問,是的話這次執行直接跳出
        if(notVisitAgainVertexes.has(vertex)){
            return;
        }
        
        // 將該頂點添加到“不再訪問”的容器中
        notVisitAgainVertexes.add(vertex);
        // 獲取該頂點的鄰接點
        let neighborVertexes = graph.vertexMap.get(vertex);
        
        // 如果存在鄰接點,則遞歸調用visit
        if(neighborVertexes instanceof Array){
            for(let i = 0; i < neighborVertexes.length; i++){
                visit(neighborVertexes[i]);
            }
        }
    }
    
    // 遍歷所有頂點,並且執行visit
        for(let vertex of vertexes){
        visit(vertex);
    }
}

上面代碼中,我們通過遞歸調用了visit函數實現了深度優先遍歷,我們在visit函數中加一個console.log可以看到結果

按順序訪問頂點 A
按順序訪問頂點 B
按順序訪問頂點 E
按順序訪問頂點 I
按順序訪問頂點 F
按順序訪問頂點 C
按順序訪問頂點 D
按順序訪問頂點 G
按順序訪問頂點 H

是不是和這個圖一致

DFS

廣度優先搜索代碼實現

未完待續


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM