JavaScript数据结构与算法 - 图

1. 图的介绍

  • 图是一组由边连接的节点(或顶点)
  • 任何二元关系都可以用图来表示
  • 任何社交网络,例如Facebook、Twitter和Google+,都可以用图来表示;还可以使用图来表示道路、航班以及通信等

概念:

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

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

在这里插入图片描述
相邻顶点: 由一条边连接在一起的顶点。如A和B相邻,A和E不相邻

度: 一个顶点的度是其相邻顶点的数量。如A和其他三个顶点相连接,则A的度为3

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

简单路径: 简单路径要求不包含重复的顶点。A、D、G是一条简单路径;环也是一个简单路径

有向图:

  • 有向图的边有一个方向
    -在这里插入图片描述
  • 如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。例如,C和D是强连通的,而A和B不是强连通的
  • 加权图和未加权图:
    在这里插入图片描述

2. 图的表示

2.1 邻接矩阵

图常用邻接矩阵来实现。每个节点都和一个整数相关联,该整数将作为数组的索引。

用一个二维数组来表示顶点之间的连接。如果索引为i的节点和索引为j的节点相邻,则array[i][j] === 1,否则array[i][j] === 0

在这里插入图片描述

缺点:

  1. 不是强连通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着会浪费计算机存储空间来表示根本不存在的边。例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻顶点,我们也不得不迭代一整行。
  2. 图中顶点的数量可能会改变,而二维数组不太灵活

2.2 邻接表

在这里插入图片描述


2.3 关联矩阵

在关联矩阵中,矩阵的行表示顶点,列表示边。

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

在这里插入图片描述


3. Graph类

先声明类的骨架:

class Graph {
	constructor(isDirected = false) {
	    // Graph构造函数可以接收一个参数来表示图是否有向,默认情况下是无向的
	    this.isDirected = isDirected;
	    // 使用数组来存储图中所有顶点的名字
	    this.vertices = [];
	    // 用字典来存储邻接表
	    this.adjList = new Dictionary();
	}
}

实现向图中添加一个新的顶点的方法:

addVertex(v) {
    // 接收顶点v作为参数,这个顶点不存在于图中时
    if (!this.vertices.includes(v)) {
        // 将该顶点添加到顶点列表中
        this.vertices.push(v);
        // 在邻接表中,设置顶点v作为键对应的字典值为一个空数组
        this.adjList.set(v, []);
    }
};

实现添加顶点之间的边的方法:

addEdge(v, w) {
    // 如果顶点v不存在于图中,将它加入顶点列表
    if (!this.adjList.get(v)) {
        this.addVertex(v);
    }
    // 如果顶点w不存在于图中,将它加入顶点列表
    if (!this.adjList.get(w)) {
        this.addVertex(w);
    }
    // 通过将w加入到v的邻接表中,添加一条自顶点v到顶点w的边
    this.adjList.get(v).push(w);
    if (!this.isDirected) {
        // 添加一条自w到v的边(基于无向图)
        this.adjList.get(w).push(v);
    }
}

声明返回顶点列表的方法:

getVertices() {
    return this.vertices;
}

声明返回邻接表的方法:

getAdjList() {
    return this.adjList;
}

测试代码:

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());

实现Graph类的toString方法,以便在控制台输出图:

toString() {
    let s = '';
    for (let i = 0; i < this.vertices.length; i++) {
        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;
}

结果:
在这里插入图片描述


4. 图的遍历

对图进行遍历的算法:

  1. 广度优先算法
  2. 深度优先算法

图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环等。


图遍历的思想:

  • 图遍历算法必须追踪每个第一次访问的节点,并追踪有哪些节点还没被完全探索
  • 两种算法都需要明确指出第一个被访问的顶点
  • 完全探索一个顶点要求查看该顶点的每一条边
  • 对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中
  • 为了保证算法的效率,务必访问每个顶点至多两次
  • 连通图中每条边和顶点都会被访问到

两种算法的区别:

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

要标注已经访问过的顶点时,可以用三种颜色来反映他们的状态:

  • 白色:该顶点还没被访问
  • 灰色:该顶点被访问过,但并未被探索过
  • 黑色:该顶点被访问过且被探索过

需要使用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 广度优先搜索

广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的邻点(相邻顶点),就像一次访问图的一层,也就是先宽后深地访问顶点。

在这里插入图片描述
步骤:

  1. 创建一个队列Q
  2. 标注v为被发现的(灰色),并将v入队列Q
  3. 如果Q非空,则:
    1. 将u从Q中出队列
    2. 标注u为被发现的(灰色)
    3. 将u所有未访问过的邻点(白色)入队列
    4. 标注u为已被探索的(黑色)

算法实现:

const breadthFirstSearch = (graph, startVertex, callback) => {
    const vertices = graph.getVertices();
    const adjList = graph.getAdjList();
    // 用initializeColor函数将color数组初始化为白色
    const color = initializeColor(vertices);
    // 声明和创建一个队列来存储待访问和待探索的顶点
    const enqueue = new Queue();
    
    // 将顶点入队列
    queue.enqueue(startVertex);
    
    // 如果队列非空
    while (!queue.isEmpty()) {
        // 通过出队列操作从队列中移除一个顶点
        const u = queue.dequeue();
        // 取得一个包含其所有邻点的邻接表
        const neighbors = adjList.get(u);
        // 将该顶点标注为灰色,表示发现但未探索该顶点
        const [u] = Colors.GREY;
        for (let i = 0; i < neighbors.length; i++) {
            // 对于u的每个邻点,需要取得其值
            const w = neighbors[i];
            // 如果它还没被访问过
            if (color[w] === Colors.WHITE) {
                // 标注已经发现了它,设为灰色
                color[w] = Colors.GREY;
                // 将该顶点入队列
                queue.enqueue(w);
            }
        }
        // 当完成探索该顶点和其相邻顶点后,将该顶点标注为已探索过的,即黑色
        color[u] = Colors.BLACK;
        // breadthFirstSearch方法接收一个回调。这个参数是可选的,如果传递了回调函数,就会用到它
        if (callback) {
            callback(u);
        }
    }
}

测试代码:

const printVertex = (value) => console.log('Visited vertex: ' + value);
breadthFirstSearch(graph, myVertices[0], printVertex);

结果:
在这里插入图片描述


1. 使用BFS寻找最短路径

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

对于给定顶点v,广度优先算法会先访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。

  • 从v到u的距离 distances[u]
  • 前溯点 predecessors[u],用来推导出从v到其他每个顶点u的最短路径

改进的广度优先方法:

const BFS = (graph, startVertex) => {
    const verteices = graph.getVertices();
    const adjList = graph.getAdjList();
    const color = initializeColor(verteices);
    const queue = new Queue();
    // 表示距离
    const distances = {};
    // 表示前溯点
    const predecessors = {};

    queue.enqueue(startVertex);

    // 对于图中的每一个顶点
    for(let i = 0; i < verteices.length; i++) {
        // 用0和null来初始化
        distances[verteices[i]] = 0;
        predecessors[verteices[i]] = null;
    }

    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] === CVolors.WHITE) {
                color[w] = Colors.GREY;
                // 增加v和w之间的距离
                distances[w] = distances[u] + 1;
                // 设置w的前溯点为u
                predecessors[w] = u;
                queue.enqueue(w);
            }
        }
        color[u] = Colors.BLACK;
    }
    // 返回一个包含distances和predecessors的对象
    return {
        distances,
        predecessors
    }
}

4.2 深度优先搜索

从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。

在这里插入图片描述
深度优先搜索算法不需要一个源顶点。

在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。

步骤:

  1. 标注v为被发现的(灰色)
  2. 对于v的所有未访问(白色)的邻点w,访问顶点w
  3. 标注v为已被探索的(黑色)
// 深度优先搜索
// 接收一个Graph类实例和回调函数作为参数
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) {
            // 传递的参数为 要访问的顶点u,颜色数组以及回调函数
            depthFirstSearchVisit(vertices[i], color, adjList, callback);
        }
    }
};
const depthFirstSearchVisit = (u, color, adjList, callback) => {
    // 访问顶点u时标注其为发现的
    color[u] = Colors.GREY;
    // 如果有callback函数,就执行该函数输出已访问过的顶点
    if (callback) {
        callback(u);
    }

    // 取得包含顶点u所有邻点的列表
    const neighbors = adjList.get(u);

    // 对于顶点u的每一个未访问过的邻点w,调用depthFirstSearchVisit函数,传递w和其他参数
    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;
}

在这里插入图片描述
如果希望深度优先算法遍历图G的所有节点,构建“森林”以及一组源顶点,并输出两个数组:发现时间和完成探索时间。

  • 顶点u的发现时间d[u]
  • 当顶点u被标注为黑色时,u的完成探索实践时间f[u]
  • 顶点u的前溯点p[u]
const DFS = graph => {
    const vertices = graph.getVertices();
    const adjList = graph.getAdjList();
    const color = initializeColor(vertices);
    const d = {};
    const f = {};
    const p = {};
    // 追踪发现时间和完成探索时间
    const time = { const: 0 };

    // 声明d、f、p
    for (let i = 0; i < vertices.length; i++) {
        f[vertices[i]] = 0;
        d[vertices[i]] = 0;
        p[vertices[i]] = null;
    }
    for (let i = 0; i < vertices.length; i++) {
        if (color[vertices[i]] === Colors.WHITE) {
            DFSVisit(vertices[i], color, d, f, p, time, adjList);
        }
    }
    // 为图的每一个顶点初始化数组,并在这个方法结尾处返回这些值
    return {
        discovery: d,
        finished: f,
        predecessors: p
    }
};
const DFSVisit = (u, color, d, f, p, time, adjList) => {
    color[u] = Colors.GREY;
    // 当一个顶点第一次被发现时,追踪其发现时间
    d[u] = ++time.count;
    const neighbors = adjList.get(u);

    for (let i = 0; i < verteices.length; i++) {
        if (color[w] === Colors.WHITE) {
            // 追踪前溯点
            p[w] = u;
            DFSVisit(w, color, d, f, p.time, adjList);
        }
    }
    color[u] = Colors.BLACK;
    // 顶点完全被探索后,追踪其完成时间
    f[u] = ++time.count;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值