06 Javascript資料結構與演算法 之 圖

zhaoyezi發表於2018-08-30

1. 定義

圖是網路結構的抽象模型,是一組由連結的節點(或定點)。

06 Javascript資料結構與演算法 之 圖

1.1 圖的表示

一個圖 G = (V, E)由以下元素組成。下圖表示一個

  • V: 一組定點
  • E: 一組邊,連線V中的定點

06 Javascript資料結構與演算法 之 圖

1.2 圖的術語

下面我們先熟悉一下的術語:

  • 由一條連結在一起的頂點,稱為相鄰節點。上圖中 AB是相鄰節點,AD是相鄰節點。
  • 一個定點的是其相鄰節點數量。 上圖中A3。=>【有向圖才會有入度和出度】
  • 路徑是頂點v1,v2,v3...vk的一個連續序列。其中vivi+1是相鄰的。上圖中包含有ABEI,ABF,ACGDH,ACDG,ACDH,ADG,ADH,這些都是簡單路徑
  • 簡單路徑:不包含重複的定點。例如:ADG是一條簡單路徑。除去最後一個節點,也是一個簡單路徑。例如ADCA(除去最後一個頂點只剩ADC)。

1.3 有向圖 / 無向圖

圖分為無向圖(邊沒有方向)和有向圖(邊有方向)。上面的圖是無向圖。下圖是有向圖:

06 Javascript資料結構與演算法 之 圖

不對稱矩陣是有向圖

如果圖中每兩個頂點間在雙向都存在路徑,則該圖為強聯通的。例如下圖:AC不是強聯通,而CD是強聯通的。
圖還可以使加權未加權的。加權圖的邊被賦予了權值。下圖是為加權的。

06 Javascript資料結構與演算法 之 圖

2. 圖的表示

圖的正確表示法取決於待解決的問題和圖的型別。

2.1 鄰接矩陣

圖最常見的實現是鄰接矩陣

  • 通過二維陣列來表示頂點之間的關係
  • 每個節點 和 一個整數相關,該整數作為陣列的索引
  • 索引為i的節點 和索引為j的節點相鄰,則array[i][j] === 1,不相鄰時array[i][j] === 0

06 Javascript資料結構與演算法 之 圖

特性:

  • 不是強聯通的圖 (稀疏圖)使用鄰接矩陣表示,矩陣中儲存了很多0浪費了計算機儲存空間來儲存不存在的邊。
  • 圖的定點的數量可能會改變,二維陣列不太靈活
  • 查詢具體兩個頂點是否是相鄰節點,比較
  • 查詢某個頂點的所有相鄰節點,比較

2.2 鄰接表

我們可以使用一種叫鄰接表的動態資料結構表示圖。領接表由圖的每個頂點相鄰頂點的列表所組成。相鄰頂點的列表資料結構可以通過列表(陣列)連結串列字典雜湊表來表示。

06 Javascript資料結構與演算法 之 圖

特性:

  • 查詢具體兩個頂點是否是相鄰節點,比較,需要獲取所有相鄰節點列表,再查詢具體頂點
  • 查詢某個頂點的所有相鄰節點,比較,直接獲取列表
function Graph() {
    // 使用陣列存放所有頂點
    let vertices = [];
    // 使用字典存放 相鄰頂點 的列表
    let adjList = new Map(); // 這裡使用ES6的Map,也就是之前的字典型別資料

    // 用於初始化一個頂點:該頂點需要新增到vertices中,並在adjList中建立一個儲存相鄰節點的資料
    this.addVertex = (v) => {
        vertices.push(v);
        adjList.set(v, []);
    };

    // 實現新增兩個頂點之間的路徑(互相新增,表示無向圖, 只新增一個頂點,則表示有向圖)
    this.addEdge =(v, w) => {
        adjList.get(v).push(w);
        // 有向圖:不需要這條設定
        adjList.get(w).push(v);
    };

    this.toString = function(){
        var s = '';
        for (var i=0; i<vertices.length; i++){ //{10}
            s += vertices[i] + ' -> ';
            var neighbors = adjList.get(vertices[i]); //{11}
            for (var j=0; j<neighbors.length; j++){ //{12}
                s += neighbors[j] + ' ';
            }
                s += '\n'; //{13}
            }
        return s;
    };
}

// 測試
var graph = new Graph();
var myVertices = ['A','B','C','D','E','F','G','H','I']; //{7}
for (var i=0; i<myVertices.length; i++){ //{8}
graph.addVertex(myVertices[i]);
}
graph.addEdge('A', 'B'); //{9}
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');

console.log(graph.toString);
複製程式碼

結果:

06 Javascript資料結構與演算法 之 圖

2.3 關聯矩陣

關聯矩陣: 矩陣的表示頂點,矩陣的表示。我們使用二維陣列來表示頂點的連通性。
如果v 頂點邊 e 的入射點(任意一個連線的定點),則array[v][e] === 1,否則array[v][e] === 0

06 Javascript資料結構與演算法 之 圖

關聯矩陣通常用於邊的數量頂點多的情況下,以節省空間和記憶體

3. 圖的遍歷

圖的遍歷方式有兩種:深度優先(Breadth-First Search,BFS)和廣度優先(Depth-First Search,DFS)。
圖的用途:尋找特定的頂點、尋找兩個頂點之間的路徑、檢查圖是否聯通、檢查圖是否含有環等等。
圖遍歷演算法思想:

  • 必須追蹤每個第一次訪問的節點,並且追蹤哪些節點沒有被完全探索
  • 兩種演算法需要明確指出第一個被訪問的頂點

完全探索一個頂點:要求我們檢視該頂點的每一條邊,對於每一條邊連線而未被訪問的頂點,標註為發現並新增進待訪問頂點列表
為保準效率:每個頂點務必訪問至多兩次。連通圖中每條邊和頂點都會被訪問到

06 Javascript資料結構與演算法 之 圖

當標註已訪問的頂點時,使用三種顏色反應它們的狀態,這也是為什麼上面說一個頂點務必最多訪問2次的原因:

  • 白色: 表示該頂點 未被訪問
  • 灰色: 表示該頂點 被訪問但未被探索過
  • 黑色: 表示該頂點 被訪問且被探索過

3.1 廣度優先(Breadth-First-Search, BFS)

從上面可以知道:從頂點v開始廣度優先的步驟如下:

  1. 建立一個佇列Q: 用於存放被訪問,未被探索【灰色】的頂點
  2. 從頂點v開始,將v設定為灰色,存放如Q
  3. 如果Q為非空【處理佇列資料】:
    • 將w從Q佇列中中取出
    • 將w的所有相鄰節點(白色)標記為灰色,放入Q佇列
    • 將w標記為黑色

06 Javascript資料結構與演算法 之 圖

// --- 該部分程式碼 直接複製到Graph中

// 將所有的頂點顏色初始化為白色
let initializeColor = () => {
    // 儲存所有帶顏色的頂點
    let colors = [];
    vertices.map(vertice => {
        colors[vertice] = 'white';
    })
    return colors;
};

// BFS
this.bfs = (v, callback) => {
    // 存放所有訪問但未被探索過的節點
    let queue = new Queue();
    let colors = initializeColor();
    queue.enqueue(v);

    // 迴圈處理所有頂點
    while(!queue.isEmpty()) {
        let u = queue.dequeue();
        colors[u] = 'grey';
        let neighbors = adjList.get(u);
        neighbors.map(w => {
            if (colors[w] === 'white') {
                colors[w] = 'grey';
                queue.enqueue(w);
            }
        });
        colors[u] = 'black';
        if (callback) {
            callback(u);
        }
    }
}


// --- 測試遍歷
function printNode(value){ //{16}
console.log('Visited vertex: ' + value); //{1
}
graph.bfs(myVertices[0], printNode); //{18}
複製程式碼

3.2 通過 BFS 計算最短路徑

通過上面的遍歷,我們可以通過小的改造,新增兩個屬性來記錄 傳入節點v[被計算的開始節點] 和 任意其他節點w[任意其他節點]之間的距離。

  • distance[w]: 存放v節點到w節點的距離。 初始化distance[w] = 0
  • predecessors[w]: 存放w節點的前輩。 初始化predecessor[w] = null
this.bfs = (v) => {
    console.log(this.toString())
    let colors = {},  // 初始化顏色,後續變化中:未被訪問 白色,訪問未被探索 grey, 被探索 black
        distance = {}, // 初始化每個頂點離v的距離
        predecessors = {}, // 初始化每個頂點的前輩頂點
        queue = new Queue(); // 存放待探索的節點
    
    
    let init = () => {
        vertices.map(w => {
            colors[w] = 'white';
            distance[w] = 0;
            predecessors[w] = null;
        });
    };

    // 需要被探索的v
    queue.enqueue(v);
    init();

    while(!queue.isEmpty()) {
        let u = queue.dequeue();
        colors[u] = 'grey';
        let neighbors = adjList.get(u); 
        neighbors.map(w => {
            if (colors[w] === 'white') {
                queue.enqueue(w);
                distance[w] = distance[u] + 1; // v => w 的具體,通過前輩節點+1
                predecessors[w] = u; // 新增元件節點
            }
            colors[w] = 'grey';
        });
        colors[u] = 'black';
    }

    return {
        distance,
        predecessors
    };
}
}

// 獲取所有的定點的最短路徑的所經過的點
this.getRoutes = (v) => {
    // 根據bfs獲取 圖中各個祖先節點的關係
    let { predecessors } = this.bfs(v);
    // 存放所有最短路徑 
    let allRoute = {};

    vertices.map((w) => {
        // 除去頂點自己,不需要計算 自己到自己的軌跡
        if (w != v) {
            // 存放當前 v -> w 的路徑
            let route = [w], item = w;

            // 從 predecessors 中獲取祖先節點,直到最頂層祖先節點為v,表示v -> w的路徑統計完畢
            while (item != v) {
                // 獲取自己的祖先頂點
                let preW = predecessors[item];
                route.push(preW);
                item = preW;
            }
            allRoute[w] = route.reverse().join('-');
        }
    });
    return allRoute;
}
複製程式碼

按照這種演算法:例如查詢A -> D 的距離:看起來有兩種, AD, ACD。 但是在遍歷A的鄰接點時,D已被探索過,因此當探索C頂點時,C的D鄰接點為黑色,表明已經先到達了D頂點,因此不再探索。最終結果為AD路徑。

06 Javascript資料結構與演算法 之 圖

3.3 DFS 深度優先

深度優先搜尋演算法:將會從第一個指定的頂點開始遍歷圖,沿著直到這條路徑的最後一個頂點訪問,按著原路返回並探索下一條路徑。

06 Javascript資料結構與演算法 之 圖

訪問途中的v頂點的步驟如下:

  • 所有頂點,初始化顏色為white
  • 開始訪問v頂點,標記v頂點為grey。此時記錄頂點v發現時間
  • 訪問v所有未被訪問的鄰接點w
    1. 訪問w,此時記錄w前溯點
  • v被探索完畢,標記v為黑色。 此時記錄頂點v完成探索時間
// --- 該部分程式碼 直接複製到Graph中

this.dfs =() => {
    let colors = {}, // 初始化所有節點顏色: key:頂點, value: grey, white, black
    d = [], // 記錄 發現時間 集合
    f = [], // 記錄 完成探索時間 集合
    p = {}, // 記錄 前溯點 
    time = 0; // 計時開始時間
    // 初始化顏色
    let init = () => {
        vertices.map(w => {
            colors[w] = 'white';
        });
    };

    // 訪問處理
    let dfsVisit = (u) => {

        // 當被訪問,修改為grey,表示開始訪問,但鄰接點未訪問完畢
        colors[u] = 'grey';

        // 記錄u的開始時間
        d[u] = ++time;
         console.log('discovered ' + u);

        // 當鄰接點訪問完畢,自己才算完成
        let neighbors = adjList.get(u);
        neighbors.map(w => {
            if (colors[w] === 'white') {
                dfsVisit(w);
                // 記錄w的前溯點
                p[w] = u;
            }
        });
        // 自己訪問完畢,修改為black
        colors[u] = 'black';
        // 記錄u的完成時間
        f[u] = ++time;
        console.log('explored ' + u);
    };

    init();
    vertices.map(w => {
        if (colors[w] === 'white') {
            dfsVisit(w);
        }
    })
    return {
        discovery: d,
        finished: f,
        predecessors: p
    };
}
複製程式碼

訪問路徑:

06 Javascript資料結構與演算法 之 圖

深度搜尋結果:

06 Javascript資料結構與演算法 之 圖

相關文章