学习JavaScript数据结构与算法 — 广度优先搜索算法

duyifei0 2019-06-21

广度优先搜索(BFS)

上一次已经提到,图的遍历一般有两种算法,即广度优先和深度优先。其中广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点,如图1。

图1

学习JavaScript数据结构与算法 — 广度优先搜索算法

从顶点v开始的广度优先搜索的步骤如下:

  1. 创建一个队列Q。
  2. 将v标注为被发现的(灰色) ,并将v入队列Q。
  3. 如果Q非空,则运行以下步骤:

    • 将u从队列Q中取出
    • 将u标注为被发现的(灰色)
    • 将u所有未被访问过的邻节点(白色)加入队列
    • 将u标注为已被探索的(黑色)

我们用三种状态来反映顶点的状态:

  • 白色:表示该顶点还没有被访问。
  • 灰色:表示该顶点被访问过,但并未被探索过。
  • 黑色:表示该顶点被访问过且被完全探索过。

因此在算法开始执行时,需要将所有顶点置为白色;如下代码所示(本文的所有代码都是在图中实现的Graph类中添加的),其中vertices保存着图所有顶点的名字。

function Graph() {
        var vertices = [];
        var adjList = new Dictionary();
        // 图类的其他代码省略...
        var initializeColor = function(){
            var color = [];
            for (var i=0; i<vertices.length; i++){
                color[vertices[i]] = 'white';
            }
            return color;
        };
    }

广度优先搜索算法的核心代码如下,其中队列Queue的实现参考基于JavaScript的数据结构 — 队列的实现

this.bfs = function(v, callback){
        var color = initializeColor(), // 将所有顶点初始化为白色
            queue = new Queue(); // 实例化队列
        queue.enqueue(v); // 将起始顶点v加入队列
        while (!queue.isEmpty()){ // 一直循环处理队列,直到队列为空
            var u = queue.dequeue(), // 移除队列顶部的元素,并取得该顶点
                neighbors = adjList.get(u); // 获取该顶点的相邻顶点
            color[u] = 'grey'; // 将该顶点置为灰色,表明该顶点被访问过,但并未被探索过
            for (var i=0; i<neighbors.length; i++){ // 循环处理该顶点的相邻顶点
                var w = neighbors[i];
                if (color[w] === 'white'){ // 如果该顶点没有被访问过
                    color[w] = 'grey'; // 将该顶点置为灰
                    queue.enqueue(w); // 将该顶点加入队列
                }
            }
            color[u] = 'black'; // 该顶点访问已经被完全访问,将其置为黑
            callback && callback(u); // 用回调函数处理该节点(如果有)
        }
    };

使用BFS寻找最短路径

由于广度优先算法是一层层往下遍历的,即先访问与起始顶点距离为1的点,再访问距离为2的点,以此类推。因此,给定一个图G和源顶点v, 要找出每一个顶点u与v之间的最短距离(以边的数量计算),可以在上述代码的基础上做一定修改(修改的位置用空行隔开):

// 获取路径信息
    this.pathData = function(v){
        var color = initializeColor(),
            queue = new Queue(),
            
            d = new Array(vertices.length).fill(0), // 用于保存起始顶点v到任意顶点u的距离
            pred = new Array(vertices.length).fill(null); // 用于保存v到u的路径上u的上一级顶点(前溯点)
            
        queue.enqueue(v);
        while (!queue.isEmpty()){
            var u = queue.dequeue(),
                neighbors = adjList.get(u);
            color[u] = 'grey';
            for (i=0; i<neighbors.length; i++){
                var w = neighbors[i];
                if (color[w] === 'white'){
                    color[w] = 'grey';
                    
                    d[w] = d[u] + 1; // w到u的距离差是1
                    pred[w] = u; // w的上一级顶点是u
                    
                    queue.enqueue(w);
                }
            }
            color[u] = 'black';
        }
        
        return { // 返回保存的数据
            distances: d,
            predecessors: pred
        };
    };
    // 格式化输出路径信息
    this.printPathData = function (pathData) {
        var fromVertex = vertices[0]; // 获取起始点
        for (var i=1; i<vertices.length; i++){
            var toVertex = vertices[i], // 要到达的顶点
                path = []; // 用于保存路径
            // 从目标顶点一直回溯到起始顶点
            for (var v=toVertex; v!== fromVertex; v= pathData.predecessors[v]) {
                path.push(v); // 顶点添加进路径
            }
            path.push(fromVertex); // 将起始顶点添加进路径
            var s = path.pop();
            while (path.length > 0){
                s += ' - ' + path.pop(); // 从路径数组倒序输出顶点
            }
            console.log(s);
        }
    }

通过执行Graph.printPathData(Graph.pathData())即可输出起始顶点到每一个顶点的最短路径。

其它最短路径算法

对于加权图的最短路径,广度优先算法可能并不合适。比如,Dijkstra’s算法可以解决单源最短路径问题。Bellman–Ford算法解决了边权值为负的单源最短路径问题。A*搜索算法解决了求仅一对顶点间的最短路径问题,它用经验法则来加速搜索过程。Floyd–Warshall算法解决了求所有顶点对间的最短路径这一问题。

相关推荐