0
点赞
收藏
分享

微信扫一扫

数据结构与算法 — 认识图结构、图的表示、图结构的实现、图的遍历

目录

一、初识图结构

1.什么是图

2.图的特点

  3.图的术语

        (1).顶点

        (2).边

        (3).相邻顶点

        (4).度

        (5).路径

        (6).无向图

        (7).有向图

        (8).无权图和带权图

二. 图的表示

  1.顶点表示

  2.邻接矩阵

  3.邻接表

三. 图结构的实现

  1.封装图结构

  2.添加顶点的方法

  3.添加边的方法

  4.toString()方法

四、图的遍历 

  1.遍历的方式

        (1).图的遍历思想

        (2).遍历的注意点

        (3).两种算法的思想

  2.广度优先搜索

        (1).广度优先搜索算法的思路

        (2).广度优先搜索的实现

  3.深度优先搜索

        (1).深度优先搜索的思路

        (2).深度优先搜索算法的实现


一、初识图结构

1.什么是图

        图是一种与树有些相似的数据结构,实际上,在数学的概念上,树是图的一种。

树可以用来模拟很多现实的数据结构,比如: 家谱、公司组织架构等等。同时,可以使用地铁地图来模拟图:

        上面的结点(其实图中叫顶点Vertex)之间的关系,是不能使用树来表示(几叉树都不可以),这个时候,就可以使用图来模拟它们。

2.图的特点

一组顶点:通常用 V (Vertex) 表示顶点的集合

一组边:通常用 E (Edge) 表示边的集合

边是顶点和顶点之间的连线

边可以是有向的,也可以是无向的。(比如A --- B,通常表示无向。 A --> B,通常表示有向)

  3.图的术语

下图是一个抽象出来的图:

        (1).顶点

        顶点表示图中的一个结点。

        比如地铁站中某个站、多个村庄中的某个村庄、互联网中的某台主机、人际关系中的人。

        (2).边

        边表示顶点和顶点之间的连线。

        比如地铁站中两个站点之间的直接连线,就是一个边。

        注意: 这里的边不要叫做路径,路径有其他的概念

        在上面的图中: 0 - 1有一条边, 1 - 2有一条边, 0 - 2没有边.

        (3).相邻顶点

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

        比如0 - 1是相邻的, 0 - 3是相邻的. 0 - 2是不相邻的

        (4).度

        一个顶点的度是相邻顶点的数量。

比如0顶点和其他两个顶点相连, 0顶点的度是2

比如1顶点和其他四个顶点相连, 1顶点的度是4

        (5).路径

        路径是顶点v1, v2..., vn的一个连续序列,比如上图中0 1 5 9就是一条路径.

   1.简单路径: 简单路径要求不包含重复的顶点. 比如 0 1 5 9是一条简单路径.

   2.回路: 第一个顶点和最后一个顶点相同的路径称为回路. 比如 0 1 5 6 3 0

        (6).无向图

        上面的图就是一张无向图,因为所有的边都没有方向。

比如 0 - 1之间有变, 那么说明这条边可以保证 0 -> 1, 也可以保证 1 -> 0.

        (7).有向图

        有向图表示的图中的边是有方向的。

比如 0 -> 1, 不能保证一定可以 1 -> 0, 要根据方向来定.

        (8).无权图和带权图

        1.无权图

        上面的图就是一张无权图(边没有携带权重)

我们上面的图中的边是没有任何意义的, 不能收 0 - 1的边, 比4 - 9的边更远或者用的时间更长.

        2.带权图

        带权图表示边有一定的权重。

这里的权重可以是任意希望表示的数据,比如距离或者花费的时间或者票价.

下图是一张有向和带权的图:

二. 图的表示

        一个图包含很多顶点,另外包含顶点和顶点之间的连线(边),这两个都是非常重要的图信息。

  1.顶点表示

        顶点的表示相对简单,上面的顶点抽象成了1 2 3 4, 也可以抽象成A B C D。这些A B C D可以使用一个数组来存储起来(存储所有的顶点)。当然, A, B, C, D有可能还表示其他含义的数据(比如地铁站的名字),这个时候,可以另外创建一个数组,用于存储对应的其他数据.

  2.邻接矩阵

        邻接矩阵让每个节点和一个整数相关联,该整数作为数组的下标值。用一个二维数组来表示顶点之间的连接

邻接矩阵的问题:

        如果是一个无向图, 邻接矩阵展示出来的二维数组, 其实是一个对称图。也就是A -> D是1的时候, 对称的位置 D -> 1一定也是1。

        邻接矩阵还有一个比较严重的问题就是如果图是一个稀疏图,那么矩阵中将存在大量的0, 这意味着浪费了计算机存储空间来表示根本不存在的边,而且即使只有一个边, 也必须遍历一行来找出这个边, 也浪费很多时间.

  3.邻接表

        邻接表由图中每个顶点以及和顶点相邻的顶点列表组成,这个列表有很多中方式来存储: 数组/链表/字典(哈希表)等等

        要表示和A顶点有关联的顶点(边), A和B/C/D有边, 那么可以通过A找到对应的数组/链表/字典, 再取出其中的内容就可以。

邻接表的问题:

        邻接表计算"出度"是比较简单的(出度: 指向别人的数量, 入度: 指向自己的数量)。邻接表如果需要计算有向图的"入度",是比较麻烦的。必须构造一个"“逆邻接表", 才能有效的计算"入度"。

三. 图结构的实现

  1.封装图结构

    //封装图结构
    function Graph(){
        //属性:顶点(数组)/边(字典)
        this.vertexes = []  //顶点
        this.edges = new Dictionay()    //边

    }

  2.添加顶点的方法

        向图中添加一些顶点

    //1.添加顶点的方法
    Graph.prototype.addVertex = function(v){
        this.vertexes.push(v)
        this.edges.set(v,[])
    }

  3.添加边的方法

    //2.添加边的方法
    Graph.prototype.addEdge = function(v1,v2){
        this.edges.get(v1).push(v2)
        this.edges.get(v2).push(v1)
    }

测试代码:

    //测试代码
    //1.创建图结构
    var g = new Graph()
    //2.添加顶点
    var myVertexes = ['A','B','C','D','E','F','G','H','I']
    for(var i=0; i<myVertexes.length; i++){
        g.addVertex(myVertexes[i])
    }
    //3.添加边
    g.addEdge('A','B')
    g.addEdge('A','C')
    g.addEdge('A','D')
    g.addEdge('C','D')
    g.addEdge('C','G')
    g.addEdge('D','G')
    g.addEdge('D','H')
    g.addEdge('B','E')
    g.addEdge('B','F')
    g.addEdge('E','I')
    //测试
    alert(g)

  4.toString()方法

    //实现toString方法
    Graph.prototype.toString = function(){
        //1.定义字符串,保存最终的结果
        var resultString = ''

        //2.遍历所有的节点,以及顶点对应的边
        for(var i =0; i<this.vertexes.length; i++){
            resultString += this.vertexes[i] + "->"
            var vEdges = this.edges.get(this.vertexes[i])
            for(var j=0; j<vEdges.length; j++){
                resultString += vEdges[j] + ' '
            }
            resultString += '\n'
        }
        return resultString
    }

四、图的遍历 

  1.遍历的方式

        (1).图的遍历思想

        图的遍历算法的思想在于必须访问每个第一次访问的节点, 并且追踪有哪些顶点还没有被访问到。有两种算法可以对图进行遍历:广度优先搜索(Breadth-First Search, 简称BFS)、深度优先搜索(Depth-First Search, 简称DFS)

        两种遍历算法, 都需要明确指定第一个被访问的顶点。

        (2).遍历的注意点

        对于每一条所连接的没有被访问过的顶点, 将其标注为被发现的, 并将其加进待访问顶点列表中。为了保证算法的效率: 每个顶点至多访问两次。

        (3).两种算法的思想

BFS: 基于队列, 入队列的顶点先被探索.

DFS: 基于栈, 通过将顶点存入栈中, 顶点是沿着路径被探索的, 存在新的相邻顶点就去访问.

为了记录顶点是否被访问过, 我们使用三种颜色来反应它们的状态:(或者两种颜色也可以)

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

灰色: 表示该顶点被访问过, 但并未被探索过

黑色: 表示该顶点被访问过且被完全探索过

初始化颜色代码:

    //初始化状态颜色
    Graph.prototype.initializeColor = function(){
        var colors = []
        for(var i=0; i<this.vertexes.length; i++){
            colors[this.vertexes[i]] = 'white'
        }
        return colors
    }

  2.广度优先搜索

        (1).广度优先搜索算法的思路

        广度优先算法会从指定的第一个顶点开始遍历图, 先访问其所有的相邻点, 就像一次访问图的一层。换句话说, 就是先宽后深的访问顶点。

        (2).广度优先搜索的实现

    //实现广度优先搜索
    Graph.prototype.bfs = function (initV, handler) {
        //1.初始化颜色
        var colors = this.initializeColor()

        //2.创建队列
        var queue = new Queue()

        //3.将顶点加入到队列中
        queue.enqueue(initV)

        //4.循环从队列中取出元素
        while (!queue.isEmpty()) {
            //4.1.从队列中取出一个顶点
            var v = queue.dequeue()

            //4.2.获取和顶点相连的另外顶点
            var vList = this.edges.get(v)

            //4.3.将v的颜色设置为灰色
            colors[v] = 'gray'

            //4.4遍历所有的顶点,并且加入到队列
            for (var i = 0; i < vList.length; i++) {
                var e = vList[i]
                if (colors[e] == 'white') {
                    colors[e] = 'gray'
                    queue.enqueue(e)
                }
            }

            //4.5访问顶点
            handler(v)

            //4.6.将顶点设置为黑色
            colors[v] = 'black'
        }
    }

测试代码:

// 调用广度优先算法
var result = ""
graph.bfs(graph.vertexes[0], function (v) {
    result += v + " "
})
alert(result) // A B C D E F G H I 

  3.深度优先搜索

        (1).深度优先搜索的思路

        深度优先搜索算法将会从第一个指定的顶点开始遍历图, 沿着路径知道这条路径最后被访问了.

接着原路回退并探索吓一条路径。

 

        (2).深度优先搜索算法的实现

        广度优先搜索算法使用的是队列,在这里可以使用栈完成,也可以使用递归。为了方便代码书写,使用递归完成(递归本质上就是函数栈的调用)

    //深度优先搜索(DFS)
    Graph.prototype.dfs = function(initV,handler){
        //1.初始化颜色
        var colors = this.initializeColor()

        //2.从某个顶点开始依次递归访问
        this.dfsVisit(initV,colors,handler)
    }
    Graph.prototype.dfsVisit = function(v,colors,handler){
        //1.将颜色设置为灰色
        colors[v] = 'gray'

        //2.处理v节点
        handler(v)

        //3.访问v相连的顶点
        var vlist = this.edges.get(v)
        for(var i=0; i < vlist.length; i++){
            var e = vlist[i]
            if(colors[e] == 'white'){
                this.dfsVisit(e,colors,handler)
            }
        }

        //4.将v设置为黑色
        colors[v]='black'
    }

测试代码:


    //调用深度优先算法
    result = ''
    g.dfs(g.vertexes[0],function(v){
        result += v + ' '
    })
    alert(result)

递归过程: 

 

举报

相关推荐

0 条评论