超级简单的图算法,图文并茂,学不会,你来打我
认识图
图是由节点
集合和边(路径)
集合组成的图形
如果图是有方向的,那就称为有序图,否则称为无序图
如果每条路径
有成本
或者权重
,那么图就是有权图
无权图
可以认为是权重相同的有权图
最小生成树
在描述图
时,我们通常根据边的权重
将图转为最小生成树
,因为最小生成树
可以包含所有节点信息和最少的边,可以使计算量缩减到最小
例如上图的最小生成树如下
有两种方法将图转为最小生成树
kruskal(克鲁斯卡尔)算法
思路:根据权重
,将边排序,每次从边中选择权重最小的边,如果使图连通(形成环)
了,那就放弃这条边
上图中加入边的顺序以此为:(2->5, 1)、(5->6, 2)、(6->3, 3)、(4->1, 3)、(5->4, 5)
prim(普里姆)
思路:从一个节点出发,在所有连接的可选值只保留代价
最小的边,例如上图,从节点1
开始经过该算法后最小生成树是这样的
节点1:可选边为(1->2, 6)、(1->4, 3),只能选:(1->4, 3)
节点1、4:可选边为(1->2, 6)、(4->5, 5)只能选:(4->5, 5)
节点1、4、5:可选边为(5->2, 1)、(1->2, 6)、(5->6, 2)只能选(5->2, 1)
节点1、4、5、2:可选边为(5->6, 2)、(1->2, 6)、(2->3, 4)只能选(5->6, 2)
节点1、4、5、2、6:可选边为(1->2, 6)、(2->3, 4)、(6->3, 3)只能选(6->3, 3)
好了现在可以按照树的形式表示图了
描述节点
描述每个节点需要唯一标识,这样方便后续对每个节点的操作,所以我们先定义下面的类来描述节点
class Vertex { constructor (uuid) { this.uuid = uuid // ...你可以在这里添加others props } }
定义边
描述边成熟的做法是使用邻接表
或者邻接数组
邻接表
是一个描述每个节点相关边的对象,它以每个节点的ID为key
,与之相连的边数组集合作为value
,例如上图中的每个节点的邻接表如下所示:
邻接数组
是用二维数组的方式描述
你可能习惯性的想在节点上标记每个节点相关边的信息,这样的是可以的, 但是对于后续的查询和变更,会消耗很大的性能,
同样,你如果想单独定义一个边的类描述边信息,也是一样的损耗性能。
实现图
现在就让我们开始实现基本的属性和功能吧
属性
首先是定义Graph
类,需要vertexs
数组存放所有的节点,需要edges
对象存放邻接表
class Graph { constructor(vertexNumber) { this.vertexs = [] // 存放节点 this.edges = {} // 存放邻接表 this.marked = {} // 记录标记 } }
addNodes
方法
增加节点,除了初始化节点之外,需要初始化新节点的邻接表和标记状态
class Graph { // 增加节点 addNodes (uuid) { this.vertexs.push(new Vertex(uuid)) this.edges[uuid] = [] this.marked[uuid] = false } }
addEdges
方法
增加边的本质就是增加邻接表信息
class Graph { // 增加边 addEdges (source, target) { // 分别给对方的邻接表添加边 this.edges[source].push(target) this.edges[target].push(source) } }
showGraoh
方法
展示图时,我们是通过展示邻接表来展示图的,所以邻接表就是图的精髓所在,后面的方法主要是操作邻接表
class Graph { // 展示图 showGraoh () { this.vertexs.forEach(vertex => console.log( vertex.uuid, '->', this.edges[vertex.uuid].toString() )) } }
添加测试数据
const graph = new Graph() [1, 2, 3, 4, 5, 6].forEach((n) => graph.addNodes(n)) graph.addEdges(1, 2) graph.addEdges(1, 3) graph.addEdges(1, 4) graph.addEdges(3, 4) graph.addEdges(2, 5) graph.addEdges(5, 6) graph.showGraoh() // 1 '->' '2,3,4' // 2 '->' '1,5' // 3 '->' '1,4' // 4 '->' '1,3' // 5 '->' '2,6' // 6 '->' '5'
此时的树为下图所示
深度优先和广度优先
遍历图中每个节点,根据不同的策略,节点的遍历顺序也不相同,最常见的是深度优先(dfs)
、广度优先(bfs)
深度优先
深度优先(dfs)
是指每次优先遍历子节点,没有子节点时再回到兄弟节点,以此类推
这里为了避免标记混乱,使用了单独的变量visited标记深度优先,它和marked一样
// 深度优先搜索 dfs (uuid) { this.visited[uuid] = true // 深度优先单独标记,以免影响广度优先算法和最短路径算法 console.log('dfs', uuid) // 循环邻接表中子节点 this.edges[uuid].forEach(edge => { // 如果没有标记,就继续下钻 if (!this.visited[edge]) this.dfs(edge) // 递归 }) }
广度优先
广度优先(bfs)
是指每次兄弟节点优先遍历,没有兄弟节点时,在遍历子节点的兄弟节点,以此类推
它是通过队列实现的:
- 先初始化一个空队列
- 将起始节点放入队列
- 弹出队列第一个节点,并且访问它子节点
- 如果没有被标记,那就标记它,并放入队列
- 开始循环第三步,直到队列为空
// 广度优先搜索 bfs (uuid) { // 1.先初始化一个空队列 const queue = [] this.marked[uuid] = true // 2.将起始节点放入队列 queue.push(uuid) console.log('bfs', uuid) while (queue.length) { // 3.弹出队列第一个节点,并且访问它子节点 const uuid_ = queue.shift() this.edges[uuid_].forEach(edge => { // 4.如果没有被标记,那就标记它,并放入队列 if (!this.marked[edge]) { this.marked[edge] = true console.log('bfs', edge) queue.push(edge) } }) } }
测试
graph.dfs(1) console.log(`<=========>`) graph.bfs(1) // dfs 1 // dfs 2 // dfs 5 // dfs 6 // dfs 3 // dfs 4 // <=========> // bfs 1 // bfs 2 // bfs 3 // bfs 4 // bfs 5 // bfs 6
最短路径
图经常被用到的地方其实查询从某个节点到另一个节点的最短距离,比如,从你的住处到公司,在四通八达的北京,道路可能不止一条,但是总有一条是最短的
求最短路径的算法有多种,今天介绍bfs最短距离
,顾名思义,就是借助广度优先算法实现的
在广度优先算法中,我们遍历节点的每个子节点时,总会遇到一个没有被标记的节点,此时,我们需要记录这个没有被标记的节点的父节点,并将这些信息记录在edgeTo
属性中。
完成广度优先算法后,我们就可以知道每个子节点对应的父节点了,接着我们只需要从目标节点,往上逆推,找到它的父节点,然后在往上推,知道源节点或者根节点,下面是以跟节点为例的实现
// 记得添加这个属性 this.edgeTo = {} // 对广度优先搜索改造 bfs (uuid) { const queue = [] this.marked[uuid] = true queue.push(uuid) console.log('bfs', uuid) while (queue.length) { const uuid_ = queue.shift() this.edges[uuid_].forEach(edge => { if (!this.marked[edge]) { this.edgeTo[edge] = uuid_ // 记录每个节点的父节点 this.marked[edge] = true console.log('bfs', edge) queue.push(edge) } }) } console.log(this.edgeTo) // 打印每个节点对应父节点的信息 } // 找出目标节点到根节点的路径 pathTo (uuid) { const source = this.vertexs[0].uuid const path = [] if (this.marked[uuid]) { for(let i = uuid; i !== source; i = this.edgeTo[i]) { path.push(i) } } path.push(source) return path } // 格式化展示 printMinPathTo (uuid) { const path = this.pathTo(uuid).join('->') console.log(path) }
测试
graph.printMinPathTo(6) // 依次打印 // { // 2: 1, // 3: 1, // 4: 1, // 5: 2, // 6: 5 // } // 6->5->2->1
好了,分享就到这了,欢迎指正出现的问题