算法-图论

87 阅读8分钟

简介

相关概念

一个图中需要包含很多的顶点和很多对应的顶点与顶点的连线,需要在后续的实现中具体的表示出来

image.png

  • 顶点
    • 图中最基本的单元,标识图中的某一个节点
  • 相邻顶点
    • 顶点之间的关联关系,由一条边连接在一起得到顶底称为相邻顶点
    • 如 0-1 相邻,0-2 不相邻
    • 顶点和顶点间的连线
    • 一个顶点的度是相邻顶点的数量
    • 如顶点0的度为2 顶点1的度为4
    • 在有向图中存在入度出度,无向图只有的概念
  • 路径
    • 路径是特定顶点间的一个连续序列,如 0-3-6-8
    • 简单路径:不包含重复顶点的路径,如 0-1-5-9
    • 回路:第一个顶点和最后一个顶点相同的路径,如 0-1-5-6-7-3-0
  • 无向图
    • 两个节点间没有特定的方向,可以来回的指向与流通;
    • 如 0 -> 1 也可以 1 -> 0
    • 特点是
      • 矩阵的length是顶点个数的平方 length²
      • 矩阵的斜边必然是无值的
  • 有向图
    • 表示图的边是有方向的,节点之间的边是有方向的
  • 无权图
    • 图中的边没有权重
  • 带权图
    • 图中每一条边都不是完全等同的,会有具体的数值表示,这些数值便是权重,对应的图被称为带权图
    • 权重表示具体的优先级
    • image.png

图的表示方式

临接矩阵

image.png

结构特点
  • 临接矩阵让每个节点和一个整数向关联,该整数作为数组的下标值
  • 用一个二维数组来表示顶点之间的连接
  • 在二维数组中,0表示没有连线,1表示有连线
    邻接矩阵使用二维数组A[i][j]来表示一条从起点i到顶点j的bian(弧),使用A[n][n]来表示你由n个顶点构成的图
  • 在无权图中,判断存不存在从指定顶点i到j的边是通过A[i][j] === 0来判断的
  • 在有权图中,可以将矩阵单元格的0/1更改为整型或浮点型,用来记录对应边的权重,对于不存在的边通常设置为null
  • 不对称的矩阵是有向图

邻接矩阵.png

代码表示
graphMatrix = [ 
    [ 0,1,1,1,0,0,0,0,0 ], 
    [ 1,0,0,0,1,1,0,0,0 ], 
    [ 1,0,0,1,0,0,1,0,0 ],
    ......
]

存在的问题

  • 如果是一个无向图,临接矩阵展现出来的二维数组其实就是一个对称图
    • 在这种情况下,会造成空间浪费
  • 当临接矩阵为一个稀疏图时,矩阵中将大量存在0,花费了大量的空间来存储根本不存在的边,而且当只有一个边时也必须遍历一行来找出这个边,浪费时间

临接表

image.png

结构特点
  • 由图中的每个顶点以及和顶点相邻的顶点列表组成
  • 每个列表有多种存储方式来存储,数组、链表、哈希表(字典)都可以
  • 当某个顶点与多个顶点有关联时,就可以通过该顶点找到对应的缓存数据取出即可
    邻接表之关心存在的边,不关心不存在的边,因此没有浪费空间,邻接表由数组和链表组成

图的封装

邻接矩阵实现

「 无向图 」

需要定义的属性
  • 缓存所有顶点的数据
  • 矩阵的初始化数据 矩阵的数据是所有顶点树的乘积 可以用Array.from({length:XXX})实现
  • 可以缓存所有顶点数量,用于后续的逻辑引用
实现思路

通过图的所有顶点数据初始化出矩阵的临时数据数组,然后再通过首位节点将矩阵中的字段值更改为1或者权重值,通过暴露的API进行对应顶点或边数的获取;

代码实现
class Adjoin {
  constructor(vertex) {
    this.vertex = vertex;
    this.quantity = vertex.length;
    this.init();
  }

  init() {
    // 初始化矩阵临时数组数据
    this.adjoinArray = Array.from({ length: this.quantity * this.quantity });
  }

  getVertexRow(id) {
    // 通过需要查找的节点找出所有节点在缓存矩阵中对应的缓存值,包括非1值
    const index = this.vertex.indexOf(id);
    const col = [];
    this.vertex.forEach((item, pIndex) => {
      col.push(this.adjoinArray[index + this.quantity * pIndex]);
    });
    return col;
  }

  getAdjoinVertexs(id) {
    // 通过指定节点反查出引用她的节点
    // 通过在矩阵中的位置找出对应的应用过的节点  需要进行过滤返回 .filter(Boolean)
    return this.getVertexRow(id).map((item, index) => (item ? this.vertex[index] : '')).filter(Boolean); // 📢📢📢
  }

  setAdjoinVertexs(id, sides) {
    // 通过下标值和长度值进行定位邻接点在缓存矩阵中的位置
    const pIndex = this.vertex.indexOf(id);
    sides.forEach((item) => {
      const index = this.vertex.indexOf(item);
      this.adjoinArray[pIndex * this.quantity + index] = 1; // 📢📢📢
    });
  }
}

// test
// 创建矩阵
const demo = new Adjoin(['v0', 'v1', 'v2', 'v3', 'v4'])

// 注册邻接点
demo.setAdjoinVertexs('v0', ['v2']);
demo.setAdjoinVertexs('v0', ['v3']);
demo.setAdjoinVertexs('v1', ['v3', 'v4']);
demo.setAdjoinVertexs('v2', ['v0']);
demo.setAdjoinVertexs('v2', ['v0', 'v3', 'v4']);
demo.setAdjoinVertexs('v3', ['v0', 'v1', 'v2']);
demo.setAdjoinVertexs('v4', ['v1', 'v2']);
console.log(demo,'demo=========')
console.log(demo.getAdjoinVertexs('v3'));
// Adjoin {
//   vertex: [ 'v0', 'v1', 'v2', 'v3', 'v4' ],
//   quantity: 5,
//   adjoinArray: [
//     undefined, undefined, 1,
//     1,         undefined, undefined,
//     undefined, undefined, 1,
//     1,         1,         undefined,
//     undefined, 1,         1,
//     1,         1,         1,
//     undefined, undefined, undefined,
//     1,         1,         undefined,
//     undefined
//   ]
// } demo=========
// [ 'v0', 'v1', 'v2' ]

邻接表实现

「 无向图 」

创建图类
需要定义的属性
  • 用于缓存所有顶点的数组属性
  • 用于存储边的属性 - 采用字典/Map来实现
代码实现
function graph() {
  this.vertexes = [] //存储所有顶点
  this.adjList = new Map() //存储边
}
添加顶点
实现逻辑

图的实现需要有维护所有顶点的数据和存储每个顶点对应边的数据

代码实现
// 添加顶点
graph.prototype.addVertex = function(v){
  if(this.adjList[v]){
    throw new Error('节点已存在')
  }else{
    // 存储当前新增的顶点
    this.vertexes.push(v)
    // 同步将该新增的顶点初始化边的数据
    this.adjList[v] = []
  }
}
添加边
实现逻辑

添加边需要有两个顶点的参数,需要将两个顶点的值存储到对应边的数据中,需要进行双向数据维护

代码实现
// 添加边
graph.prototype.addEdge = function(v,w) {
  this.adjList.get(v).push(w)
  this.adjList.get(w).push(v)
}
完整代码
function Graph() {
  this.vertexes = [] //存储所有顶点
  this.adjList = new Dictionay() //存储边
}
// 添加顶点
Graph.prototype.addVertex = function(v){
  // 存储当前新增的顶点
  this.vertexes.push(v)
  // 同步将该新增的顶点初始化边的数据
  this.adjList[v] = []
}

// 添加边
// 添加边需要有两个顶点的参数,需要将两个顶点的值存储到对应边的数据中
Graph.prototype.addEdge = function(to,from) {
  // this.adjList.[to].push(from)
  // this.adjList.[from].push(to)
  if(this.adjList[to] && this.adjList[from]){
    let temToAdj = this.adjList[to]
    let temFromAdj = this.adjList[from]
    temToAdj = [...new Set([...temToAdj,from])]
    temFromAdj = [...new Set([...temFromAdj,to])]
    this.adjList[to] = temToAdj
    this.adjList[from] = temFromAdj
  }
}

Graph.prototype.sudo = function () {
  let result = ""
  console.log(this.vertexes,'this.vertexes=========')
  for (let i = 0; i < this.vertexes.length; i++) {
    result += this.vertexes[i] + "->"
      let adjoin = this.adjList.get(this.vertexes[i])
      for (let j = 0; j < adjoin.length; j++) {
        result += adjoin[j] + " "
      }
      result += "\n"
  }
  return result
}

测试

// 测试代码
var graph = new Graph()

// 添加顶点
var vertexList = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
for (var i = 0; i < vertexList.length; i++) {
    graph.addVertex(vertexList[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.sudo(),'graph.sudo()=========')
// [
//   'A', 'B', 'C', 'D',
//   'E', 'F', 'G', 'H',
//   'I', 'J'
// ] this.vertexes=========
// A->B C D 
// B->A E F 
// C->A D G 
// D->A C G H 
// E->B I 
// F->B 
// G->C D 
// H->D 
// I->E 
// J->
//  graph.sudo()=========

图的遍历

遍历思路及常见方式

思路

从图中的某个顶点出发,沿图中路径一次访问图中的所有顶点,使得每一个顶点刚好被访问过一次,这个过程就是图的遍历;
常见的图的遍历算法是广度优先(BFS)算法和深度优先(DFS)算法

深度优先遍历(Depth-First Search - DFS)
基本思路

DFS会从某个指定的节点开始遍历,按照某个深度依次往下进行遍历,和二叉树的先序遍历有点类似;
深度优先遍历在遍历的过程中会有一个回溯到根顶点的过程,因此可以采用栈结构来存储这个访问顺序,词回溯的过程就是函数回调的过程也就睡一个栈的执行过程,因此图的深度优先遍历可以采用栈的存储结合回调递归来实现; 难点在于递归下去,回溯上来

代码实现
Graph.prototype.dfs = function (handler) {
  // 初始化颜色
  var color = this.initializeColor()

  // 遍历所有的顶点, 开始访问
  for (var i = 0; i < this.vertexes.length; i++) {
    if (color[this.vertexes[i]] === "white") {
      this.dfsVisit(this.vertexes[i], color, handler)
    }
  }
}

// dfs的递归调用方法
Graph.prototype.dfsVisit = function (u, color, handler) {
  // 将u的颜色设置为灰色
  color[u] = "gray"

  // 处理u顶点
  if (handler) {
    handler(u)
  }

  // u的所有邻接顶点的访问 递归调用
  var uAdj = this.adjList.get(u)
  for (var i = 0; i < uAdj.length; i++) {
    var w = uAdj[i]
    if (color[w] === "white") {
      this.dfsVisit(w, color, handler)
    }
  }

  // 将u设置为黑色
  color[u] = "black"
}

image.png image.png

广度优先遍历(Breadth-First Search - BFS)
基本思路

广度优先遍历一般用于解决起点到各点的最短路径等问题,是一个以为优先的遍历过程,在进行访问的过程中,可以采用队列(先进先出)来存储已经访问过的节点

基本步骤
  • 创建一个队列
  • 将传入的顶点放入队列中,并标注为灰色
  • 获取传入顶点的相邻所有节点,然后遍历找出的所有节点推入栈中,更改颜色为灰色
  • 处理完当前节点后将当前节点置为黑色 表示检测完毕
  • 最后根据是否传入回调函数来执行回调
代码实现
Graph.prototype.initializeColor = function () {
  var colors = []
  for (var i = 0; i < this.vertexes.length; i++) {
    colors[this.vertexes[i]] = "white"
  }
  return colors
}

Graph.prototype.bfs = function (v, handler) {
  // 初始化颜色
  var color = this.initializeColor()

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

  // 将传入的顶点放入队列中
  queue.enqueue(v)

  // 根据队列数据进行遍历图结构
  while (!queue.isEmpty()) {
    // 从队列中取出数据
    var qv = queue.dequeue()

    // 获取qv相邻的所有顶点
    var qAdj = this.adjList.get(qv)

    // 将qv的颜色设置成灰色
    color[qv] = "gray"

    // 遍历相邻节点 并压入栈中
    for (var i = 0; i < qAdj.length; i++) {
      var a = qAdj[i]
      if (color[a] === "white") {
        // 防止重复检测节点
        color[a] = "gray"
        queue.enqueue(a)
      }
    }

    color[qv] = "black"

    // 处理用户回调
    if (handler) {
      handler(qv)
    }
  }
}

image.png

文献推荐

dyhtps - 在JavaScript中实现图👍🏻👍🏻👍🏻
蚂蚁金服 - 图形算法👍🏻👍🏻👍🏻
coderwhy - 数据结构-图算法

codepen推荐

SKU 商品规格