简介
相关概念
一个图中需要包含很多的顶点和很多对应的顶点与顶点的连线,需要在后续的实现中具体的表示出来
- 顶点
- 图中最基本的单元,标识图中的某一个节点
- 相邻顶点
- 顶点之间的关联关系,由一条边连接在一起得到顶底称为相邻顶点
- 如 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²
- 矩阵的斜边必然是无值的
- 有向图
- 表示图的边是有方向的,节点之间的边是有方向的
- 无权图
- 图中的边没有权重
- 带权图
- 图中每一条边都不是完全等同的,会有具体的数值表示,这些数值便是权重,对应的图被称为带权图
- 权重表示具体的优先级
-
图的表示方式
临接矩阵
结构特点
- 临接矩阵让每个节点和一个整数向关联,该整数作为数组的下标值
- 用一个二维数组来表示顶点之间的连接
- 在二维数组中,0表示没有连线,1表示有连线
邻接矩阵使用二维数组A[i][j]来表示一条从起点i到顶点j的bian(弧),使用A[n][n]来表示你由n个顶点构成的图 - 在无权图中,判断存不存在从指定顶点i到j的边是通过
A[i][j] === 0
来判断的 - 在有权图中,可以将矩阵单元格的0/1更改为整型或浮点型,用来记录对应边的权重,对于不存在的边通常设置为null
- 不对称的矩阵是有向图
代码表示
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,花费了大量的空间来存储根本不存在的边,而且当只有一个边时也必须遍历一行来找出这个边,浪费时间
临接表
结构特点
- 由图中的每个顶点以及和顶点相邻的顶点列表组成
- 每个列表有多种存储方式来存储,数组、链表、哈希表(字典)都可以
- 当某个顶点与多个顶点有关联时,就可以通过该顶点找到对应的缓存数据取出即可
邻接表之关心存在的边,不关心不存在的边,因此没有浪费空间,邻接表由数组和链表组成
图的封装
邻接矩阵实现
「 无向图 」
需要定义的属性
- 缓存所有顶点的数据
- 矩阵的初始化数据 矩阵的数据是所有顶点树的乘积 可以用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"
}
广度优先遍历(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)
}
}
}
文献推荐
dyhtps - 在JavaScript中实现图👍🏻👍🏻👍🏻
蚂蚁金服 - 图形算法👍🏻👍🏻👍🏻
coderwhy - 数据结构-图算法