JavaScript数据结构与算法 - 图

Posted 友人A ㅤ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;

以上是关于JavaScript数据结构与算法 - 图的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript数据结构与算法 - 图

JavaScript 图

图的算法

JavaScript数据结构与算法博客目录

数据结构与算法:终于可以用三种语言(C,C#,JavaScript)把图的广度优先遍历讲清楚了(推荐收藏)

数据结构与算法:终于可以用三种语言(C,C#,JavaScript)把图的广度优先遍历讲清楚了(推荐收藏)