js-数据结构和算法-图-广度优先搜索和深度优先搜索-最短距离-拓扑排序

  1. 网络结构的抽象模型
  2. 一组由边连接的节点。
  3. 任何二元关系都可以用图表示。

图的组成

G=(V, E)
V: 一组顶点
E: 一组边,连接V中的顶点

相邻顶点:由一条边连接在一起的顶点
度:相邻顶点的数量
路径:顶点v1,v2…vk的一个连续序列。
简单路径:不包含重复的顶点。
环:如果该图不存在环,则该图是无环的
连通:如果途中每两个顶点间都存在路径,则该图是连通的
有向图无向图:图可以是无向的(边没有方向)或有向的(有向图)
强连通:如果图中每两个顶点间在双向上都存在路径,则该图是强连通。
加权:加权图的边被赋予了权值。

邻接矩阵

每个节点都和一个整数相关,该整数作为数组的索引。
用数组表示顶点间的链接。如果索引为i和索引为j的节点相邻,
则array[i][j] === 1,否则为0
图:每个节点为行头和列头,行列相邻得1。
非强联通的图(稀疏图),会浪费存储空间来表示不存在的边。
顶点的数量会变,数组不灵活。

邻接表

动态数据结构
每个顶点的相邻顶点列表组成
可用列表,链表,哈希表,字典等数据结构来表示
图:一个顶点为行头,向右展开它的相邻顶点,没有列头
大多数问题的比较好的选择

关联矩阵

矩阵的行表示顶点,列表示边。
使用数组表示两者间的连通性,如果顶点v是边e的入射点,
则array[v][e] === 1, 否则为0
图:顶点为行头,每个顶点的每种相邻关系为列,相邻且在该行顶点中得1。

遍历

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

思想

  1. 图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探
    索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
  2. 完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的
    顶点,将其标注为被发现的,并将其加进待访问顶点列表中
  3. 为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。
  4. 广度优先搜索算法和深度优先搜索算法基本上是相同的,只有一点不同,那就是待访问顶点列表的数据结构
算法 数据结构 描述
深度优先搜索 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问
广度优先搜索 队列 通过将顶点存入队列中,最先入队列的顶点先被探索

当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。

  • 白色:表示该顶点还没有被访问。
  • 灰色:表示该顶点被访问过,但并未被探索过。
  • 黑色:表示该顶点被访问过且被完全探索过。
    这就是之前提到的务必访问每个顶点最多两次的原因

广度优先遍历(Breadth-First Search BFS)

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

步骤

(1) 创建一个队列Q。
(2) 将v标注为被发现的(灰色),并将v入队列Q。
(3) 如果Q非空,则运行以下步骤:
(a) 将u从Q中出队列;
(b) 将标注u为被发现的(灰色);
© 将u所有未被访问过的邻点(白色)入队列;
(d) 将u标注为已被探索的(黑色)。

应用

求出最短路径

本章中的图不是加权图。
如果要计算加权图中的最短路径
(例如,城市A和城市B之间的最短路径——GPS和Google Maps中用到的算法),
广度优先搜索未必合适。

其他:
Dijkstra’s算法解决了单源最短路径问题。
Bellman–Ford算法解决了边权值为负的单源最短路径问题。
A*搜索算法解决了求仅一对顶点间的最短路径问题,它用经验法则来加速搜索过程。
Floyd–Warshall算法解决了求所有顶点对间的最短路径这一问题。

深度优先遍历 (Depth-First Search DFS)

会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶
点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点

步骤

不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访
问该顶点v。
要访问顶点v,照如下步骤做。
(1) 标注v为被发现的(灰色)。
(2) 对于v的所有未访问的邻点w:
(a) 访问顶点w。
(3) 标注v为已被探索的(黑色)。

应用

拓扑排序
  1. 适用于有向无环图(DAG)
  2. 当我们需要编排一些任务或步骤的执行顺序时,这称为拓扑排序(topological sorting,英文 亦写作topsort或是toposort)
 function Dictionary() {
        let items = {}
        this.set = function (key, value) {
          items[key] = value
        }
        this.remove = function (key) {
          if (this.has(key)) {
            delete items[key]
            return true
          }
          return false
        }
        this.has = function (key) {
          return items.hasOwnProperty(key)
        }
        this.get = function (key) {
          return this.has(key) ? items[key] : undefined
        }
        this.clear = function () {
          items = {}
        }
        this.size = function () {
          return Object.keys(items).length
        }
        this.keys = function () {
          return Object.keys(items)
        }
        this.values = function () {
          return Object.values(items)
        }
        this.getItems = function () {
          return items
        }
      }
      let Queue = function () {
        let items = []
        this.enqueue = function (v) {
          items.push(v)
        }
        this.dequeue = function (v) {
          return items.shift(v)
        }
        this.front = function (v) {
          return items[0]
        }
        this.isEmpty = function (v) {
          return items.length === 0
        }
        this.size = function (v) {
          return items.length
        }
        this.print = function (v) {
          console.log(items.toString())
        }
      }
      function Stack() {
        let items = []
        this.push = function (v) {
          items.push(v)
        }
        this.pop = function (v) {
          return items.pop(v)
        }
        this.peek = function () {
          return items[items.length - 1]
        }
        this.isEmpty = function () {
          return items.length === 0
        }
        this.clear = function () {
          items = []
        }
        this.size = function () {
          return items.length
        }
        this.print = function () {
          console.log(items.toString())
        }
      }
      // 存储顶点
      // 顶点对应的字典
      // 形成顶点为行左开头,字典为行内容的图。
      function Graph() {
        let vertices = [] // 存储顶点
        let adjList = new Dictionary() // 顶点关系字典表
        this.addEdge = function (v, w) {
          // 互存顶点关系
          adjList.get(v).push(w)
          adjList.get(w).push(v)
        }
        // 初始化顶点以及顶点关系字典表
        this.addVertex = function (v) {
          vertices.push(v) // 创建顶点记录
          adjList.set(v, []) // 创建顶点行下标
        }
        this.print = function () {
          console.log(vertices)
        }
        this.printAdjList = function () {
          Object.entries(adjList.getItems()).map(item =>
            console.log(item[0], item[1])
          )
        }
        this.toString = function () {
          var s = ''
          for (var i = 0; i < vertices.length; i++) {
            //{10}
            s += vertices[i] + ' -> '
            var neighbors = adjList.get(vertices[i]) //{11}
            for (var j = 0; j < neighbors.length; j++) {
              //{12}
              s += neighbors[j] + ' '
            }
            s += '\n' //{13}
          }
          return s
        }
        // 颜色标记法
        // 初始化颜色标识函数,全部顶点为白色。
        let initColor = function () {
          let color = {}
          for (let i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white'
          }
          return color
        }
        // 广度优先遍历
        /*
        寻找最短路径
        给定一个图G和源顶点v,找出对每个顶点u,u和v之间最短路径的距离(以边的数量计)。
        对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推
         */
        /*
          (1) 创建一个队列Q。
          (2) 将v标注为被发现的(灰色),并将v入队列Q。
          (3) 如果Q非空,则运行以下步骤:
            (a) 将u从Q中出队列;
            (b) 将标注u为被发现的(灰色);
            (c) 将u所有未被访问过的邻点(白色)入队列;
            (d) 将u标注为已被探索的(黑色)。
        */
        // 使用队列,依靠队列的先入先出来操作查找相关的顶点
        this.bfs = function (v, cb) {
          let color = initColor()
          let queue = new Queue()
          // 距离
          let d = []
          // 前溯点
          let pred = []
          // 初始化,顶点入栈
          for (let i = 0; i < vertices.length; i++) {
            d[vertices[i]] = 0
            pred[vertices[i]] = null
          }
          // 入列开启搜索
          queue.enqueue(v)
          while (!queue.isEmpty()) {
            // u出列
            let u = queue.dequeue()
            // u被发现
            color[u] = 'grey'
            // 取出邻近顶点数组
            let neighbors = adjList.get(u)
            for (let i = 0; i < neighbors.length; i++) {
              // 一个邻顶点
              let w = neighbors[i]
              if (color[w] === 'white') {
                // 搜索过的邻顶点入列
                queue.enqueue(w)
                // 设置已被搜索过
                color[w] = 'grey'
                // 设置v,w距离 A
                d[w] = d[u] + i
                // 设置w的前溯点为u。设置该搜索到的顶点w的上一个顶点是u。如果一个顶点有两个父顶点,以第一个为准。
                // 据此可以查找当前元素的顶点一直往上找个所有联通的路径。
                pred[w] = u
              }
            }
            // 标注被访问过的顶点
            color[u] = 'black'
            if (cb) {
              cb(u)
            }
          }
          return {
            distances: d,
            predecessors: pred,
          }
        }
        // 深度优先搜索
        // 从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,
        // 接着原路回退并探索下一条路径。
        /*
          不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。
          (1) 标注v为被发现的(灰色)。
          (2) 对于v的所有未访问的邻点w:
          (a) 访问顶点w。
          (3) 标注v为已被探索的(黑色)。
         */
        /*
          构建“森林”(有根树的一个集合)以及一组源顶点(根),
          并输出两个数组:发现时间和完成探索时间。
         */
        this.dfs = function (cb) {
          let time = 0 // 次数
          let color = initColor()
          // 1. 顶点u的发现时间d[u]
          let d = []
          // 2. 当顶点u被标注为黑色时,u的完成探索时间f[u]
          let f = []
          // 3. 顶点u的前溯点p[u]
          let p = []
          // 搜索每个顶点
          const dfsVist = function (u, color, cb) {
            color[u] = 'grey'
            d[u] = ++time
            if (cb) cb(u)
            let neighbors = adjList.get(u)
            for (let i = 0; i < neighbors.length; i++) {
              let w = neighbors[i]
              if (color[w] === 'white') {
                dfsVist(w, color, cb)
                p[w] = u
              }
            }
            color[u] = 'black'
            f[u] = ++time
          }
          for (let i = 0; i < vertices.length; i++) {
            let vertice = vertices[i]
            d[vertice] = 0
            f[vertice] = 0
            p[vertice] = null
          }
          for (let i = 0; i < vertices.length; i++) {
            if (color[vertices[i]] === 'white') {
              dfsVist(vertices[i], color, cb)
            }
          }

          return {
            discovery: d,
            finished: f,
            predecessors: p,
          }
        }
        /*
          由:
            1. 时间(time)变量值的范围只可能在图顶点数量的一倍到两倍之间;
            2. 对于所有的顶点u,d[u]
      }
      let graph = new Graph()
      let myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] // 顶点数组
      for (let i = 0; i < myVertices.length; i++) {
        // 顶点入表
        graph.addVertex(myVertices[i])
      }
      graph.print()
      // 依次添加字典关系表
      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')
      graph.printAdjList()
      /*
        // 由字典和数组组成的图结构
        A (3) ['B', 'C', 'D']
        B (3) ['A', 'E', 'F']
        C (3) ['A', 'D', 'G']
        D (4) ['A', 'C', 'G', 'H']
        E (2) ['B', 'I']
        F ['B']
        G (2) ['C', 'D']
        H ['D']
        I ['E']
        // 其实是将下面的列根据是否相邻,
        // 一个个加入行开头的下标中。
          A B C D E F G H I
        A   x x x
        B x       x x
        C x     x     x
        D x   x       x x
        E   x             x
        F   x
        G     x x
        H       x
        I         x
       */
      console.log('------------')
      // console.log(`graph.toString()`, graph.toString())
      // --- 广度优先遍历 ---
      //
      /*
       * @function: 所有s到其他顶点的路径
       * @params:
       *  graph: 图
       *  myVertices: 所有顶点数组
       *  s: 源顶点
       *  d: 目标顶点
       */
      function searchPaths(graph, myVertices, s, d) {
        let vertices = myVertices.slice()
        let index = vertices.indexOf(s)
        let shortestPathA = graph.bfs(vertices[index], function (v) {
          console.log(`vertex`, v)
        })
        console.log('shortestPathA', shortestPathA)
        // 取源顶点
        let fromVertex = vertices[index]
        // 除去查找的顶点,剩下的所有顶点
        vertices.splice(index, 1)
        // 最短路径数组集合
        let sortestPathArr = []
        let sortestPathArr2 = []
        // 遍历查找顶点
        for (let i = 0; i < vertices.length; i++) {
          let toVertice = vertices[i] // 一个顶点
          let path = new Stack() // 一条路径,先入后出,后入先出。
          // 当前顶点到源顶点
          // 取上一个路径
          // 根据 predecessors 来查找顶点
          for (
            let v = toVertice;
            v !== fromVertex;
            v = shortestPathA.predecessors[v]
          ) {
            path.push(v)
          }
          path.push(fromVertex) // 最后入栈源顶点 ba ca da eba fba gca hda ieba
          let s = path.pop() // 取出源顶点
          let toVerticeArr = [] // 当前路径的数组
          // 取其中一个入栈的路径数组用来比较长度
          let preToVerticeArr =
            sortestPathArr2.length > 0 ? sortestPathArr2[0] : []
          toVerticeArr.push(s)
          while (!path.isEmpty()) {
            let temp = path.pop()
            toVerticeArr.push(temp)
            s += ' - ' + temp
          }
          console.log(s)
          // 第一次的时候加入一个数组
          if (sortestPathArr2.length === 0) {
            sortestPathArr2.push(toVerticeArr)
          } else {
            // 以后每次都和上一个数组比较长度,同样则加入
            if (preToVerticeArr.length === toVerticeArr.length) {
              sortestPathArr2.push(toVerticeArr)
              // 如果是比里面的小,则清空之前的,从新加入。
            } else if (preToVerticeArr.length > toVerticeArr.length) {
              sortestPathArr2 = []
              sortestPathArr2.push(toVerticeArr)
            }
          }
          // 当前路径数组存入总路径数组
          sortestPathArr.push(toVerticeArr)
        }
        // s最小路径数组集合
        console.log('s的最小路径数组集合', sortestPathArr2)
        sortestPathArr.sort((pre, cur) => {
          return pre.length - cur.length
        })
        // s到d最短路径
        let sortestPathArr3 = sortestPathArr.slice()
        sortestPathArr3 = sortestPathArr3.filter(item => item.includes(d))
        sortestPathArr3.sort((pre, cur) => {
          return pre.length - cur.length
        })
        console.log('A到${d}(H)最短路径', sortestPathArr3[0])
        // s从短到长排序的路径集合
        return sortestPathArr
      }
      let pathArr = searchPaths(graph, myVertices, 'A', 'H')
      console.log('从短到长排序的路径', pathArr)

      /*
       * @function: 求出最短路径
       * @params:
       *   graph: 图
       *   from: 源顶点
       *   to: 目标顶点
       */
      const searchSortestPath = function (graph, from, to) {
        let s = graph.bfs('A')
        let v = to
        let path = new Stack()
        while (v != from) {
          path.push(v)
          v = s.predecessors[v]
        }
        path.push(v)
        let str = ''
        while (!path.isEmpty()) {
          str += path.pop() + '-'
        }
        console.log(str.slice(0, -1))
        return str.slice(0, -1)
      }
      searchSortestPath(graph, 'A', 'H')
      const currying = function (fn) {
        let args = []
        return function () {
          if (arguments.length === 0) {
            return fn.apply(this, args)
          } else {
            ;[].push.apply(args, arguments)
          }
        }
      }
      const paths = (function () {
        return function () {
          return Array.from(arguments).join('-')
        }
      })()
      const memDfsPath = currying(paths)
      let dfsInfo = graph.dfs(function (v) {
        memDfsPath(v)
      })
      console.log(`memPath()`, memDfsPath())
      console.log(`dfsInfo`, dfsInfo)

      // 拓扑排序
      graph = new Graph()
      myVertices = ['A', 'B', 'C', 'D', 'E', 'F']
      for (i = 0; i < myVertices.length; i++) {
        graph.addVertex(myVertices[i])
      }
      graph.addEdge('A', 'C')
      graph.addEdge('A', 'D')
      graph.addEdge('B', 'D')
      graph.addEdge('B', 'E')
      graph.addEdge('C', 'F')
      graph.addEdge('F', 'E')
      var result = graph.dfs()
      let discovery = result.discovery
      let finished = result.finished
      let tuopu = []
      Object.keys(discovery).map(key => {
        tuopu.push({
          key,
          value: (discovery[key] / finished[key]).toFixed(3) * 1000,
        })
      })
      const compareObjByKey = function (key) {
        return function (a, b) {
          return a[key] - b[key]
        }
      }
      tuopu.sort(compareObjByKey('value'))
      console.log(`tuopu`, tuopu)
      const memTuopuPath = currying(paths)
      tuopu.map(item => memTuopuPath(item.key))
      console.log(`memTuopuPath`, memTuopuPath())

请问:distances 在这里有什么用?


它描述了s到各个顶点要搜索的次数,用来表示距离。


广度优先和深度优先有什么区别?
为什么广度优先就能找到最短距离,深度优先就能搞定拓扑排序?反过来可以吗?

你可能感兴趣的:(js数据结构和算法,算法,javascript,数据结构)