数据结构之图的广度优先搜索(寻找最短路径)

715 阅读4分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

附相关文章的地址:

上一篇文章中,我们实现了基本的广度优先搜索,也就是BFS的工作原理。而广度优先搜索可以做很多事情,接下来,我们来了解如何使用BFS寻找最短路径。

使用BFS寻找最短路径

给定一个图G和源顶点v,找出每个顶点 u 和 v 之间最短路径的距离。

  • 我们以顶点之间边的数量来统计其距离,也就是说两个顶点之间的边的数量越小,代表这两个顶点之间的路径就最短。那么求最短路径也就转换成了求两个顶点之间的边的数量的最小值
  • 从上一篇的广度优先搜索中,我们知道了,广度优先搜索是访问到某个顶点时,会探索它的邻接顶点列表。也就是说,对于给定顶点v,广度优先搜索会先访问其所有的邻接顶点(也就是所有与其距离为1的顶点),接着去访问距离为2的顶点,以此类推。

邻接表的广度优先搜索示意图.png

实现

  • 基于上一篇文章中已经实现广度优先搜索的bfs方法:
this.bfs = function (v) {
    let color = initializeColor();
    let queue = new Queue();
    
    // 创建 d对象,用来存储从顶点v到顶点u的距离d[u]
    let d = {};
    // 创建pred对象,用来存储搜索过程中 顶点v的前一个顶点。即:前溯点pred[u],用来推导出从v到其他每个顶点u的最短路径
    let pred = {};
       
    queue.enqueue(v);
    
    // 同样的,我们遍历图的所有顶点的列表,将每个顶点都其他顶点的距离初始化为0
    for (let i = 0; i < vertices.length; i++) {
        // 记录每个顶点到其他顶点的距离
        d[vertices[i]] = 0;
        pred[vertices[i]] = null;
    }
    
    // 循环队列
    while(!.queue.isEmpty()) {
        // 如果队列非空的话,继续执行以下的操作
        // 移除队列的第一项 u
        let u = queue.dequeue();
        // adjList字典中存储的是顶点和它的邻接顶点列表,所以我们拿到顶点u的邻接列表
        let neighbors = adjList.get(u);
        // 将访问过的顶点u的状态变更为灰色
        color[u] = 'gray'
        // 接着探索 顶点u的邻接列表
        for (let i = 0; i < neighbors.length; i++) {
            // 取出每个邻接顶点
            let w = neighbors[i]
            // 如果该顶点是白色的,则说明未被访问过,那么此时将其状态颜色变为gray,并将其推入队列中,同时改变 w 顶点和 u顶点之间的距离
            if (color[w] === 'white') {
                color[w] = 'gray'
                
                d[w] = d[u] + 1;
                pred[w] = u;
                
                queue.enqueue(w)
            }
        }
        // 当我们将顶点u的所以邻接顶点完全访问完之后,将顶点u的状态变为黑色
        color[u] = 'black';
    }
    // 访问完顶点之后,返回一个对象
    return {
         distances: d,
         predecessors: pred
    }
        
}
  • 改好方法之后,我们调用之后得到以下结果:
// 起始顶点为A顶点
const shortestPathA = graph.bfs(myVertices[0]); 
console.log(shortestPathA);


{
       distances: {A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2 , I: 3}, 
       predecessors: {A: null, B: "A", C: "A", D: "A", E: "B", F: "B", G: "C", H: "D", I: "E"}
}

可以看到我们拿到了 起始顶点为A 到其他顶点的距离的集合,和 每个顶点的前溯顶点的集合。那么我们就可以根据结果得到起始顶点A到其他顶点的最短路径。看下面代码:

let fromVertex = myVertices[0]; // 即顶点A
// 遍历图的顶点列表
for (let i = 1; i < myVertices.length; i++) {
    // 记录下一个顶点
    let toVertex = myVertices[i];
    // 声明path 栈 用来记录路径
    let path = new Stack();
    // 追溯 toVertex 到 fromVertex的路径;变量v赋值为其前溯顶点的值,从而进行反向追溯路径
    for (let v = toVertex; v !== fromVertex; v = shortestPathA.predecessors[v]) {
        // 将顶点v 推入栈中
        path.push(v)
    }
    // 最终将源顶点也推入栈中,得到完整的路径
    path.push(fromVertex);
    // 栈的规则:先进后出,后进先出,所有我们创建变量s,将栈末尾的源顶点赋值给它。
    let s = path.pop();
    // 循环将栈中的顶点取出,拼接到s后面
    while(!path.isEmpty()) {
        s += ` - ${path.pop()}`
    }
    console.log(s)
}

最终,我们通过上述代码得到了源顶点(也就是起始地顶点A)到图中其他顶点的最短路径。

写在最后

不太好理解,需要仔细琢磨。。。