js数据结构与算法 图的BFS和DFS

本文为技术学习的笔记-《Learning JavaScript Data Structures and Algorithms, Third Edition》

1.图的相关术语

图是网络结构的抽象模型。图是由一组连接的节点(或顶点)。

一个图G=(V, E)由以下元素组成:

  • V:一组顶点

  • E:一组边,连接V中的顶点

下图表示一个图:

在这里插入图片描述

由一条边连接在一起的顶点称为相邻顶点

一个顶点的是其相邻顶点的数量;

路径是顶点v1,v2, …, vk的一个连续序列,其中vi和vi+1是相邻的;

简单路径要求不包含重复的顶点。比如ADG是一条简单的路径。

也是一个简单路径,比如ADCA;

如果图中不存在环,则称该图是无环的;

如果图中每两个顶点间都存在路径,则该图是联通的

有向图和无向图

图可以是无向的或是有向的。

在这里插入图片描述

如果图中两个顶点在双向上都存在路径,则改图是强联通的。例如C和D是强联通的,而A和B不是强联通的。

图可以是加权的或者是未加权的。如下图所示,每个边都赋予了权值:

在这里插入图片描述

2.图的表示

2.1 邻接矩阵

图中最常见的实现是邻接矩阵。每个节点都和一个整数关联,将该整数作为数组的索引。我们使用一个二维数组来表示顶点之间的连接。如下图所示:

在这里插入图片描述

不是强连通图(稀疏图)如果用邻接矩阵表示,矩阵中就会出现很多0,这意味着我们浪费了计算机存储空间来存储不存在的边。

2.2 邻接表

邻接表由图中每个顶点的相邻顶点列表组成。存在好几种方式来表示这种数据结构,我们可以使用列表,链表,甚至三联表或是字典来表示相邻顶点列表。

在这里插入图片描述

2.3 关联矩阵

在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所示:

在这里插入图片描述

关联矩阵通常适合用于边的数量比顶点多的情况,以节省空间内存和内存。

3.创建Graph类


    class Graph {
        constructor(isDirected = false) {
            this.isDirected = isDirected;
            this.vertices = [];
            this.adjList = new Dictionary();
        }
    }

构造函数的第一个参数isDirected表示是否为有向;

vertices来存储图中所有顶点的名字;

adjList来存储邻接表,字典将会使用顶点的名字作为键,邻接列表作为值。

接着实现两个方法,一个用来向图中添加新顶点,另一个用来添加顶点之间的边;

添加新顶点:


    addVertex(v) {
        if(!this.vertices.includes(v)) {
            this.vertices.push(v);
            this.adjList.set(v, []); // 设置key->和链表
        }
    }

下面添加新的边:


    addEdge(v, w) {
        if(!this.adjList.get(v)) {
            this.addVertex(v);
        }

        if(!this.adjList.get(w)) {
            this.addVertex(w);
        }

        this.adjList.get(v).push(w);
        if(!this.isDirected) {
            this.adjList.get(w).push(v);
        }

    }

这个方法,会首先添加v和w,然后判断是否为有向图,如果是则两个顶点都要添加。

我们还需要声明两个取值的方法:一个返回顶点列表,一个返回邻接表。

    getVertices() {
        return this.vertices;
    }



    getAdjList() {
        return this.adjList;

    }

为了更加方便打印出图的结构,设计了toString()方法:


    toString() {
        let s = '';
        // 遍历顶点
        for(let i = 0; i< this.vertices.length; i++) {
            // 获取边key
            s += `${this.vertices[i]} -> `;
            // 获取领接表
            cosnt neighbors = this.adjList.get(this.vertices[i]);

            // 遍历相邻节点
            for(let j = 0; j < neighbors.length; j++) {
                s += `${neighbors[j]}`;
            }
            s += `\n`;
        }
        return s;
    }

4.图的遍历

和树的结构类似,我们可以访问树的所有节点。有两种算法可以对图进行遍历:广度优先遍历(breadth-first search, BFS)和深度优先遍历 (depth-first search, DFS)。

图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种遍历算法,都需要明确指明第一个被访问的节点。

算法数据结构描述
深度优先搜索将顶点存入栈,顶点是沿着路径被探索的,存在新的相邻顶点就访问
广度优先搜索队列将顶点存入队列,最先入队列的顶点先被搜索

当要标注已经访问过的节点的时候,我们需要用到三种颜色来反映他们的状态:

  • 白色:表示顶点还没有被访问过

  • 灰色:表示顶点已经被访问过,但是没有被探索过

  • 黑色:表示顶点已经被访问过且被探索过

于是,我们定义了下面的Colors常量用于标记这些不同的状态:

    const Colors = {
        WHITE: 0,
        GREY: 1,
        BLACK: 2
    }

两个算法还需要一个辅助对象来帮助存储顶点是否已经被访问过。在每个算法的开头,所有的顶点都会被标记为白色。

    // 辅助对象用于管理顶点的访问状态,开始默认都是白色
    const initializeColor = vertices =>  {
        const color = {};
        for(let i = 0; i< vertices.length; i++) {
            color[vertices[i]] = Colors.WHITE;
        }
        return color;
    }

4.1 广度优先搜索

广度优先搜索算法会从指定的每一个顶点位置开始遍历图,先访问其所有邻点,就像一层一层访问图的一层。如下图所示:

在这里插入图片描述

以下是从顶点v开始的广度优先搜索算法所遵循的步骤:

(1)创建一个队列Q

(2)标注v为被发现的(灰色),并将v入队列Q

(3)如果Q非空,则运行以下步骤:

  (a)将u从Q中出队列

  (b)标注u为被发现的(灰色)

  ©将u所有未被访问过的邻点(白色)入队列

  (d)标注u为为已经被探索的(黑色)

具体代码如下:

    const gbreadthFirstSearch = (graph, startVertex, callback) => {
        // 获取所有的顶点
        const vertices = graph.getVertices();
        // 获取邻接表
        const adjList = graph.getAdjList();
        // 初始化颜色
        const color = initializaColor(vertices);
        // 新建一个队列
        const queue = new Queue();
        // 将起始节点放入队列
        queue.enqueue(startVertex);
        // 如果队列不为空
        while(!queue.isEmpty()) {
            //  取出元素
            const u = queue.dequeue();
            // 获取邻接点
            const neighbors = adjList.get(u);
            // 标记颜色被访问过
            color[u] = Colors.GREY;
            // 遍历子节点
            for(let i = 0; i< neighbors.length; i++) {
                // 获取当前一个字节点
                const w = neighbors[i];
                // 如果未曾被访问过,就标记一下然后放入队列中
                if(color[w] === Colors.WHITE) {
                    color[w] = Colors.GREY;
                    queue.enqueue(w);
                }
            }
            // 标记颜色为黑色,已经访问过并且已经被探索过
            color[u] = Colors.BLACK;
            // 如果callback函数存在
            if(callback) {
                // 调用callback进行打印之类对的处理
                callback(u);
            }
        }
    }

使用BFS寻找最短路径

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

对于给定的节点v,广度优先算法会访问所有距离为1的顶点,接着是距离为2的顶点,以此类推。所以可以使用广度优先算法来解决该问题。

我们可以修改breadthFirstSearch方法来返回给我们一些信息:

  • 从v到u的距离distances[u]

  • 前溯节点predecessors[u],用来推导v到每个顶点u的最短路径

    const BFS = (graph, startVertex) => {
        // 获取所有节点
        const vertices = graph.getVertices();
        // 获取邻接表
        const adjList = graph.getAdjList();
        // 初始化所有节点为白色,表示未曾访问
        const color = initializeColor(vertices);
        // 定义一个队列
        const queue = new Queue();
        // 定义距离累加器
        const distances = {};
        // 推导从v到其他每个顶点的最短路径
        const predecessors = {};
        // 把第一个节点放入队列
        queue.enqueue(startVertex);
        // 遍历所有节点
        for(let i = 0; i< vertices.length; i++) {
            // 标记距离累加器为0
            distances[vertices[i]] = 0;
            // 标记所有节点的前溯节点
            precidessors[vertices[i]] = null;
        }
        while(!queue.isEmpty()) {
            // 出队列
            const u = queue.dequeue();
            // 获取邻接点数组
            const neighbors = adjList.get(u);
            // 标记已经被访问
            color[u] = Color.GREY;
            // 遍历邻接节点
            for(let i = 0; i< neighbors.lenght; i++) {
                // 获取当前一个邻节点
                const w = neighbors[i];
                // 如果当前节点未曾被访问
                if(color[w] === Colors.WHITE) {
                    // 标记当前节点被访问
                    color[w] = Colors.GREY;
                    // 累加节点计数
                    distances[w] = distances[u] + 1;
                    // 设置w的前溯节点为u
                    predecessors[w] = u;
                    // 将此邻节点放入队列中
                    queue.enqueue(w);
                }

            }

            // 标记当前节点为黑色,代表已经被访问且被探索
            color[u] = Colors.BLACK;

        }

        // 返回最终的最小距离和所有的前溯点
        return  {
            distances,
            predecessors
        }

    }

通过前溯数组我们可以使用下面的代码来构建从A到其他顶点的路径:


    // 使用广度优先遍历计算出结果放入对象shortestpathA中

    const shortestpathA = BFS(graph, myVertices[0]);
    // 起始节点
    const fromVertex = myVertices[0];
    // 遍历所有的邻接点
    for(let i = 0; i < myVertices.length; i++) {
        // 获取目标节点
        const toVertex = myVertices[i];
        // 定义一个栈(先进后出)
        const path = new Stack();
        // 从目的节点开始,利用回溯predcessors 一直往前推到开始节点,压入栈中。
        for(let v = toVertex; v!== fromVertex;
            v = shortestPathA.predcessors[v]) {
                path.push(v);
        }
        // 把起始节点也放入栈中
        path.push(fromVertex);
        // 后面依次弹出栈的内容就表示了节点的真正的路径
        let s = path.pop();
        while(!path.isEmpty()) {
            s += ' - ' + path.pop();
        }
        // 输出最终的字符串
        console.log(s);
    }

    

深入学习最短路径算法

在上述例子中不是加权图,如果要计算加权中的最短路径,广度优先搜索未必合适。

Dijkstra算法解决了单源最短路径问题。

Bellman-Ford算法解决了边权值为负的单源最短路径问题。

A*搜索算法解决了求仅一对顶点间的最短路径问题,用经验法加速搜索。

Floyd-Warshall算法解决了求所有顶点之间的最短路径这一问题。

4.2 深度优先搜索

深度优先搜索算法那将会从第一个指定的顶点开始,沿着路径到这条路径最后一个顶点被访问,接着原路回退并搜索下一条路径。如下图所示:

在这里插入图片描述

深度优先算法不需要一个源节点。在深度优先搜索算法中,如果图中顶点v未访问,则访问该顶点v。

要访问顶点v,照如下步骤做:

(1)标注v为被发现的(灰色);

(2)对于v的所有未访问的(白色)的邻点w,访问顶点w;

(3)标注v为已被探索的(黑色)。

深度优先算法实现如下:


// 深度优先遍历

const depthFirstSearch = (graph, callback) => {
    const vertices = graph.getVertices();
    const adjList = graph.getAdjList();
    const color = initializeColor(vertices);
    for(let i = 0; i < vertices.length; i++) {
        if(color[vertices[i]] === Colors.WHITE) {
            // 深度优先遍历
            depthFirstSearchVisit(vertices[i], color, adjList, callback);
        }
    }
}


// 深度优先遍历,参数:(索引,颜色标记,邻接表,回调函数)

const depthFirstSearchVisit = (u, color, adjList, callback) => {
    // 标记u已经被访问
    color[u] = Colors.GREY;
    // 如果回调函数可用
    if(callback) {
        callback(u);
    }
    // 获取邻节点
    const neighbors = adjList.get(u);
    // 遍历邻接节点
    for(let i = 0; i < neighbors.length; i++) {
        // 获取某邻接节点
        const w = neighbors[i];
        // 如果当前节点未曾被访问则递归进行深度优先遍历
        if(color[w] === Colors.WHITE) {
            depthFirstSearchVisit(w, color, adjList, callback);
        }
    }

    // 最优把该节点置为已经被访问过
    color[u] = Colors.BLACK;

}

最终汇总测试的代码如下:

class Graph {
    constructor(isDirected = false) {
        this.isDirected = isDirected;
        this.vertices = [];
        this.adjList = new Map();
    }

    addVertex(v) {
        if(!this.vertices.includes(v)) {
            this.vertices.push(v);
            this.adjList.set(v, []); // 设置key->和链表
        }
    }

    addEdge(v, w) {
        if(!this.adjList.get(v)) {
            this.addVertex(v);
        }
        if(!this.adjList.get(w)) {
            this.addVertex(w);
        }
        this.adjList.get(v).push(w);
        if(!this.isDirected) {
            this.adjList.get(w).push(v);
        }
    }

    getVertices() {
        return this.vertices;
    }

    getAdjList() {
        return this.adjList;
    }

    toString() {
        let s = '';
        // 遍历顶点
        for(let i = 0; i< this.vertices.length; i++) {
            // 获取边key
            s += `${this.vertices[i]} -> `;

            // 获取领接表
            const neighbors = this.adjList.get(this.vertices[i]);
            
            // 遍历相邻节点
            for(let j = 0; j < neighbors.length; j++) {
                s += ` ${neighbors[j]}`;
            }
            s += `\n`;
        }
        return s;
    }
   
}


class Queue {
    constructor () {
        this.count = 0;
        this.lowestCount = 0;
        this.items = {};
    }

    enqueue(element) {
        this.items[this.count] = element;
        this.count++;
    }

    dequeue() {
        if(this.isEmpty()) {
            return undefined;
        }
        const result = this.items[this.lowestCount];
        delete this.items[this.lowestCount]
        this.lowestCount++;
        return result;
    }

    peek() {
        if(this.isEmpty()){
            return undefined;
        }
        return this.items[this.lowestCount];
    }

    isEmpty() {
        return this.count - this.lowestCount === 0;
    }
    
    size() {
        return this.count - this.lowestCount;
    }

    clear() {
        this.count = 0;
        this.lowestCount = 0;
        this.items = {};
    }

    toString() {
        if(this.isEmpty()) {
            return '';
        }
        // 先获取第一个元素
        let objString = `${this.items[this.lowestCount]}`;
        // 然后依次遍历,利用模板技术可以代替传统的字符串拼接的操作
        for(let i = this.lowestCount+1;i < this.count; i++) {
            objString = `${objString}, ${this.items[i]}`;
        }
        return objString();
    }
}

const Colors = {
    WHITE: 0,
    GREY: 1,
    BLACK: 2
}

// 辅助对象用于管理顶点的访问状态,开始默认都是白色
const initializeColor = vertices =>  {
    const color = {};
    for(let i = 0; i< vertices.length; i++) {
        color[vertices[i]] = Colors.WHITE;
    }
    return color;
}

const breadthFirstSearch = (graph, startVertex, callback) => {
    // 获取所有的顶点
    const vertices = graph.getVertices();
    // 获取邻接表
    const adjList = graph.getAdjList();
    // 初始化颜色
    const color = initializeColor(vertices);

    // 新建一个队列
    const queue = new Queue();
    // 将起始节点放入队列
    queue.enqueue(startVertex);

    // 如果队列不为空
    while(!queue.isEmpty()) {
        //  取出元素
        const u = queue.dequeue();
        // 获取邻接点
        const neighbors = adjList.get(u);
        // 标记颜色被访问过
        color[u] = Colors.GREY;
        // 遍历子节点
        for(let i = 0; i< neighbors.length;  i++) {
            // 获取当前一个字节点
            const w = neighbors[i];
            // 如果未曾被访问过,就标记一下然后放入队列中
            if(color[w] === Colors.WHITE) {
                color[w] = Colors.GREY;
                queue.enqueue(w);
            }
        }
        // 标记颜色为黑色,已经访问过并且已经被探索过
        color[u] = Colors.BLACK;
        // 如果callback函数存在
        if(callback) {
            // 调用callback进行打印之类对的处理
            callback(u);
        }
    }
}


测试广度优先和深度优先遍历算法/

const graph = new Graph();

const myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];

for(let i = 0; i < myVertices.length; i++) {
    graph.addVertex(myVertices[i]);
}

graph.addEdge('A', 'B');
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());

// 广度优先遍历
const printVertex = value => console.log('Visited vertex:' +value);
breadthFirstSearch(graph, myVertices[0], printVertex)

// 深度优先遍历
const depthFirstSearch = (graph, callback) => {
    const vertices = graph.getVertices();
    const adjList = graph.getAdjList();
    const color = initializeColor(vertices);

    for(let i = 0; i < vertices.length; i++) {
        if(color[vertices[i]] === Colors.WHITE) {
            // 深度优先遍历
            depthFirstSearchVisit(vertices[i], color, adjList, callback);
        }
    }
}

// 深度优先遍历,参数:(索引,颜色标记,邻接表,回调函数)
const depthFirstSearchVisit = (u, color, adjList, callback) => {
    // 标记u已经被访问
    color[u] = Colors.GREY;
    // 如果回调函数可用
    if(callback) {
        callback(u);
    }
    // 获取邻节点
    const neighbors = adjList.get(u);
    // 遍历邻接节点
    for(let i = 0; i < neighbors.length; i++) {
        // 获取某邻接节点
        const w = neighbors[i];
        // 如果当前节点未曾被访问则递归进行深度优先遍历
        if(color[w] === Colors.WHITE) {
            depthFirstSearchVisit(w, color, adjList, callback);
        }
    }
    // 最优把该节点置为已经被访问过
    color[u] = Colors.BLACK;
}

console.log("\n ");
depthFirstSearch(graph, printVertex);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值