JavaScript 数据结构(十一):图

JavaScript 数据结构系列目录

JavaScript 数据结构(一): 数组

JavaScript 数据结构(二): 栈

JavaScript 数据结构(三):队列

JavaScript 数据结构(四):双端队列

JavaScript 数据结构(五):链表

JavaScript 数据结构(六):集合

JavaScript 数据结构(七):字典

JavaScript 数据结构(八):散列表

JavaScript 数据结构(九): 树


文章目录


一、图的概述

首先让我们来看看图长啥样。

JavaScript 数据结构(十一):图
(图片来源于网络,侵权必删)

在计算机科学里,图是由一些节点(或顶点) 组成的 集合。而这些顶点通过一系列的来链接。

任何社交网络,例如一个人所拥有的人脉都可以用图来表示。

二、图的相关术语

相邻顶点 :由一条边来连接在一起的顶点称为相邻顶点。
如上图里,a 与 b是相邻的,a 与 c 是相邻的,但 a 与 d 不是相邻的。
:一个顶点度是其相邻顶点的数量。
如上图里 a 与两个顶点相邻,那么其度为2。b 与三个顶点相邻,那么其度为3。
路径 :路径是顶点 v1,v2,…,vk的一个连续序列,其中 vi 和 vi + 1 是相邻的。
如上图所示,a,b,d,f 是一条简单的路径。
:如果路径的最后一个顶点能重新回到该路径的第一个顶点,那么该路径称之为环。
如果图中不存在环,那么该图是无环的。如果图中每两个顶点都存在路径,则该图是联通的

1、有向图与无向图

图可以是无向的(边没有方向,即无向图),也可以是有向的(即有向图,有向图的边至少有一个方向)。

我们可以观察一下下图。

JavaScript 数据结构(十一):图

在这个图里我们可以看见,A 与 E点是双向链接的,则证明此图是强连通的。

例如,A 与 E 是强连通的,A 与 B不是强连通的。

2、加权图与未加权图

图片也还可以是未加权的(上面的两个图都是未加权的)或是加权的(加权图的边被赋予了值)。

JavaScript 数据结构(十一):图

三、图的表示

从数据结构的角度来说,我们有很多方式来表示,在所有的表示法中,不存在绝对正确的方法。

图的正确表示法取决于待解决的问题和其类型。

1、邻接矩阵

图最常见的表示方法就是邻接矩阵

这里我们可以参考下图。

JavaScript 数据结构(十一):图
不是强连通的图如果用邻接矩阵来表示,则矩阵中会有很多0,这意味着我们浪费了计算机的存储空间来表示根本不存在的边。

同样的,如果图中的顶点数里改变了,那么我们也不太好修改矩阵(二维数组不太灵活)。

2、邻接表

我们也可以使用 邻接表 来表示图。

邻接表由图中每个顶点的相邻点列表组成。

而这种数据结构存在好几种的实现方法。

我们可以用数组,链表,字典或是散列表来实现。

下图展示了邻接表的数据结构。

JavaScript 数据结构(十一):图

四、创建图类

照例声明一个类。

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

在 Graph 类的构造函数里,我们声明了三个变量。

isDirected 变量来表示图是否存在向,默认情况下图是无向的。

vertices 变量用来存储图中所欲顶点的名字。

adjList 字典(该字典我们在本系列第七篇中以实现)用来存储邻接表。

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

声明完后,我们来实现这么几个方法。

1、addVertex 方法

addVertex(v) {
    if ( this.vertices.includes(v) ) return;

    this.vertices.push(v);
    this.adjList.set(v,[]);
}

该方法用于创建一个顶点。

如果该值存在于图中,那么则不进行任何操作。‘

如果不存在,那么我们将这个值放入图中,并且为其对应的字典值设为一个空数组。

2、addEdge 方法

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

该方法接收两个顶点作为参数,也就是说我们要建立链接的两个顶点。

在连接前,我们先要判定这两个顶点是否全部存在于图中,如若不存在,则去创建。

如若存在,则为第一个顶点对应的字典值放入第二个顶点。

并且如果图是无向的,那么第二个顶点对应的字典纸同样放入第一个顶点。

3、getVertices、getAdjList 与 toString 方法

如果要完成创建 Graph 类,那么我们还需要声明两个取值的方法:一个返回顶点列表,一个返回邻接表。

getVertices() { return this.vertices; }

getAdjList() { return this.adjList; }

toString() {
    let string = "";

    for ( let i = 0; i < this.vertices.length; i++ ) {
        string += `${this.vertices[i]} -> `;
        let neighbors = this.adjList.get(this.vertices[i]);
        for ( let j = 0; j < neighbors.length; j++ ) s += `${neighbors[j]}`;
        string += '\n';
    }

    return string;
}

当然了,还有一个方便我们查看的方法。

完成了这三个方法后让我们来测试一下以下的代码吧。

const graph = new Graph();
const vertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];

for (let i = 0; i < vertices.length; i++) graph.addVertex(vertices[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());

其输出结果如下。

JavaScript 数据结构(十一):图

五、图的遍历

同树数据结构类似,我们可以访问图的所有节点。

其中,有两种算法可以对图进行遍历:广度优先搜索(Breadth-first Search,BFS)深度优先搜索(Depth-first Search,DFS)

图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。

完全探索一个顶点要求我们查看该顶点的每一条边,对于每一条没有被访问过的,需将对其进行标记住待访问,并将其加入待访问的列表中。

为了保证算法的效率,每个顶点的访问次数尽量最多两次。连通图中每条边和顶点都会被访问到。

接下来,我们来创建一个常量,作为标记顶点状态的常量。

const Status = {
	toBeVisited : 0,// 未访问
	incompleteAccess : 1,// 未完全访问
	fullAccess : 2// 完全访问
}

其次还需要一个函数来初始化每个顶点的状态。

const initializeStatus = vertices => {
    const statu = {};
    for ( let i = 0; i < vertices.length; i++ ) statu[vertices[i]] = Status.toBeVisited;
    return statu;
}

1、广度优先搜索

广度优先搜索算法会先宽后深的访问顶点。

理解了这个概念后,我们来实现广度优先搜索算法。

const breadthFirstSearch = (graph,starVertex,callback) => {
    let vertices = graph.getVertices(),
        adjList = graph.getAdjList(),
        statu = initializeStatus(vertices),
        queue = new Queue();

    queue.enqueue(starVertex);

    while ( !queue.isEmpty() ) {
        let u = queue.dequeue(),
            neighbors = adjList.get(u);
            
        statu[u] = Status.incompleteAccess;

        for ( let i = 0; i < neighbors.length; i++ ) {
            let w = neighbors[i];
            if ( statu[w] === Status.toBeVisited ) {
                statu[w] = Status.incompleteAccess;
                queue.enqueue(w);
            }
        }
        statu[u] = Status.fullAccess;
        if ( callback ) callback(u);
    }
}

breadthFirstSearch 方法接收一个图实例和顶点作为算法的起始点。其起始点是必要的,我们需要将他加入队列,其次就是遍历。

不过在遍历前,我们需要用 initializeStatusv 函数来将 statu 数组初始化为 未访问状态。

与此同时我们还需要声明一个 Queue 实例来存储待访问和待探索的顶点。

声明完这两个变量后,我们就可以开始进行遍历了。

如果队列非空,那么我们将通过队列操作从队列中移除一个顶点,并取得一个包含其所有邻接点的表。再将该顶点标注为未完全访问,表示我们已经访问了,但未完全探索完。

当对该店全部探索完后,我们将该点标注为完全访问。

之后为了查看结果,我们为 breadthFirstSearch 方法还填充了一个可以接收回调函数的功能,方便我们来进行测试。

(1)、使用 BFS 寻找最短路径

上例中,我们展示了 BSF 算法的工作原理,那么接下来我们尝试用它做更多的事情。

例如,我们给定一个图 G 和源顶点 v,找出每个顶点 u 和 v 之间的最短路径的距离(以边的数里计算)。

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

我们可以修改 breadthFirstSearch 方法返回给我们的信息:

  1. 从 v 到 u 的距离
  2. 前溯点 predecessors[u]。

让我们来看看改进后的广度优先方法的实现。

const BFS = (graph,starVertex) => {
    let vertices = graph.getVertices(),
        adjList = graph.getAdjList(),
        statu = initializeStatus(vertices),
        queue = new Queue(),
        distances = {},
        predecessors = {};

    queue.enqueue(starVertex);

    for ( let i = 0; i < vertices.length; i++ ) {
        distances[vertices[i]] = 0;
        predecessors[vertices[i]] = null;
    }

    while ( !queue.isEmpty() ) {
        let u = queue.dequeue(),
            neighbors = adjList.get(u);

        statu[u] = Status.incompleteAccess;

        for ( let i = 0; i < neighbors.length; i++ ) {
            let w = neighbors[i];
            if ( statu[w] === Status.toBeVisited ) {
                statu[w] = Status.incompleteAccess;
                distances[w] = distances[u] + 1;
                predecessors[w] = u;
                queue.enqueue(w);
            }
        }
        statu[u] = Status.fullAccess;
    }

    return {
        distances,
        predecessors
    }
}

改进后的 BFS 方法有什么改变呢?

首先是我们还需要声明数组 distances 来表示距离,以及 predecessors 数组来表示前溯点。

下一步则是对于图中的每个顶点的循环初始化。

用 0 来初始化数组 destances ,用 null 来初始化数组 predecessor。

之后当我们发现顶点 u 的邻点 w是,则设置 w 的前溯点为 u。并且我们通过给 distances[u] 加 1来增加 v 和 w 之间的距离。

最后返回一个包含 distances 和 predecessor 的对象。

2、深度优先搜索

深度优先搜索算法会先深后广的访问顶点。

理解了这个概念后,我们来实现深度优先搜索算法。

const depthFirstSearch = (graph,callback) => {
    let vertices = graph.getVertices(),
        adjList = graph.getAdjList(),
        statu = initializeStatus(vertices);

    for ( let i = 0; i < vertices.length; i++ ) {
        if ( statu[vertices[i]] === Status.toBeVisited ) {
            depthFirstSearchVisit(vertices[i],statu,adjList,callback);
        }
    }
}

const depthFirstSearchVisit = (u,statu,adjList,callback) => {
    statu[u] = Status.incompleteAccess;
    if ( callback ) callback(u);
    let neighbors = adjList.get(u);
    for ( let i = 0; i < Status.length; i++ ) {
        let w = neighbors[i];
        if ( statu[w] === Status.toBeVisited ) {
            depthFirstSearchVisit(w,color,adjList,callback);
        }
    }
    statu[u] = Status.fullAccess;
}

depthFirstSearch 方法接收一个图实例和回调函数作为参数。

在初始化每个顶点的颜色后,对于图实例中每一个违背访问过的顶点,我们调用私有的递归函数 depthFirstSearchVisit ,传递的参数为要访问的顶点 u、状态数组、邻接表与回调函数。

当访问顶点 u 时,我们标注其为未完全访问后,提取包含顶点 u 所有邻点的列表。

对于顶点 u 的每一个未被访问过的邻点 w。

我们将再次调用这个函数。

最后在该顶点和邻点按深度访问后,我们回退,并将该顶点标注为完全访问。


鉴于图的知识实在是太多了,这篇文章还需要好好整理一下再完善

上一篇:php解析url并得到url中的参数及获取url参数的四种方式


下一篇:深度优先搜索遍历(无向图)