图的基本概念与实现……
一 基本概念
图(Graph)是由顶点和连接顶点的边构成的离散结构。在计算机科学中,图是最灵活的数据结构之一,很多问题都可以使用图模型进行建模求解。
图可用公式表示:
// V:一组顶点;
// E: 一组边,用来连接 V 中的顶点
G=(V,E)
1. 普通概念
- 相邻顶点:由一条边连接在一起的顶点。
- 路径:依次遍历顶点序列之间的边所形成的轨迹。注意,依次就意味着有序,先 A 后 B 和先 B 后 A 不一样。
2. 无向图
图的边没有方向。
3. 有向图
图的边有方向。
4. 加权图
5. 环
通过某条路径可以返回到起始顶点。
二 图结构的表示方式
1. 邻接矩阵
如下表,纵向为顶点,横向为相邻顶点,使用二维数组存储,如果两个顶点之间存在连线,则值为 1, 否则为 0。
会有比较多的内存浪费。
| A | B | C | D | E | F | G | H | I | |
|---|---|---|---|---|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| B | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
| C | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| D | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| E | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| F | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
| G | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
| H | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| I | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2. 邻接表
邻接表是对邻接矩阵的改进,可以使用数组、链表、字典、散列表等来表示。
| 顶点 | 相邻顶点 |
|---|---|
| A | B-C |
| B | A-D-E |
| C | A-F-G |
| D | B-I |
| E | B |
| F | C-H |
| G | C-H |
| H | F-G |
| I | D |
3. 关联矩阵
纵向为顶点,横向为边。适用于顶点远大于边的情况。
| e1 | e2 | e3 | e4 | e5 | e6 | e7 | e8 | e9 | |
|---|---|---|---|---|---|---|---|---|---|
| A | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| B | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
| C | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| D | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
| E | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| F | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
| G | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
| H | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| I | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
三 图的创建
class Graph {
constructor() {
this.vertices = []; // 存放顶点
// Dictionary类见 https://uninspire.gitee.io/blog/2021/06/28/0023-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E6%95%A3%E5%88%97/
this.edgeList = new Dictionary(); // 存放边
}
/*
* 添加顶点
*/
addVertex(v) {
if (!this.vertices.includes(v)) {
this.vertices.push(v);
this.edgeList.push(v, []);
}
}
/*
* 添加边
*/
addEdge(a, b) {
// 如果顶点a不存在,则在图中添加a
if (!this.vertices.includes(a)) {
this.vertices.push(a);
}
// 如果顶点b不存在,则在图中添加b
if (!this.vertices.includes(b)) {
this.vertices.push(b);
}
// 在顶点a中添加指向顶点b的边
this.edgeList.get(a).push(b);
// 在顶点b中添加指向顶点a的边
this.edgeList.get(b).push(a);
}
/*
* 获取顶点列表
*/
getVertices() {
return this.vertices;
}
/*
* 获取边列表
*/
getEdgeList() {
return this.edgeList;
}
}
四 图的遍历
遍历的辅助功能:在遍历顶点的过程中,给每个节点定义一个状态,用来表示访问次数。每个顶点最多被访问两次。
- 白:未被访问
- 灰:被访问但是未被处理
- 黑:已被处理
1. 广度优先搜索
从第一个顶点开始遍历,访问所有相邻顶点,再访问相邻顶点的相邻顶点。直到所有顶点访问结束。
{% asset_img 广度优先搜索.jpg 广度优先搜索图解 %}
// 定义三种状态 黑白灰
let Colors = {
WHITE: 0,
GRAY: 1,
BLACK: 2
}
// 初始化顶点状态
let initVertices = vertices => {
let color = {}
vertices.forEach(v => color[v] = Colors.WHITE)
}
function deepSearch (graph, start, cb) {
// 获取顶点列表
let vertices = graph.getVertices()
// 获取边裂变
let edgeList = graph.getEdgeList()
// 将所有顶点初始化状态
let color = initVertices(vertices)
// 定义一个空队列
let queue = new Queue()
// 将开始顶点推入队列中
queue.push(start)
// 循环遍历,直到队列为空
while(!queue.isEmpty()) {
// 获取队列第一个元素 队列从头部出队列
let u = queue.pop()
// 获取到顶点u所对应的所有相邻顶点,
edgeList.get(u).forEach(item => {
// 如果相邻顶点的状态为未访问,则推入到队列中
if (color[item] = Colors.WHITE) {
// 将已经访问过的顶点状态置为灰色
color[item] = Colors.GRAY;
// 将未访问的顶点推入到队列中
queue.push(item)
}
})
// 将已经处理过的顶点置为黑色
color[u] = Color.BLACK
// 打印输出顶点u
console.log(u)
}
}
无权图的最短路径问题
// 计算最短路径,只需要在 deepSearch 方法中添加计数即可
function deepSearch (graph, start, cb) {
let vertices = graph.getVertices()
let edgeList = graph.getEdgeList()
let color = initVertices(vertices)
let queue = new Queue()
queue.push(start)
// -------------------新添加-----------------------
let distanceData = {} // 统计顶点之间距离
let preVertices = {} // 统计顶点的前置顶点
// 初始化顶点之间距离和前置顶点
vertices.forEach(child => {
distanceData[child] = 0
preVertices[child] = null
})
// ------------------- 结束 -----------------------
while(!queue.isEmpty()) {
let u = queue.pop()
edgeList.get(u).forEach(item => {
if (color[item] = Colors.WHITE) {
color[item] = Colors.GRAY;
// 每经过一个新顶点,距离加1
distanceData[item] = distanceData[item] + 1
// 前置节点修改为 item
preVertices[item] = item
queue.push(item)
}
})
color[u] = Color.BLACK
}
// 打印所有节点之间的距离和前直节点
console.log(distanceData, preVertices)
}
加权图的最短路径(狄克斯特拉算法)
// 取当前数值的最小值
const INF = Number.MAX_SAFE_INTEGER;
// 狄克斯特拉算法主体
const dijkstra = (graph, src) => {
// 当前图的顶点列表
const dist = [];
// 已经遍历过的顶点列表
const visited = [];
// 图中的顶点数量
const {length} = graph;
// 将所有的权重设置为INF 所有的节点设置为为遍历状态
for (let i = 0; i < length; i++) {
dist[i] = INF;
visited[i] = false;
}
// 当前节点的权重设置为0
dist[src] = 0;
// 遍历所有的顶点
for (let i = 0; i < length - 1; i++) {
// 获取到距离当前顶点权重最低的节点
const index = minDistance(dist, visited);
// 将此节点设置为访问状态
visited[index] = true;
for (let v = 0; v < length; v++) {
if (!visited[v] && graph[index][v] !== 0 && dist[index] !== INF && dist[index] + graph[index][v] < dist[v]) {
dist[v] = dist[index] + graph[index][v];
}
}
}
return dist;
};
// 获取距离当前顶点最短距离的顶点索引
const minDistance = (dist, visited) => {
let min = INF;
let minIndex = -1;
for (let v = 0; v < dist.length; v++) {
// 如果当前顶点未被访问过,且两者之间的距离小于最小距离,则将最小距离设置为此顶点,并记录此节点的索引
if (visited[v] === false && dist[v] <= min) {
min = dist[v];
minIndex = v;
}
}
return minIndex;
};
2. 深度优先搜索
沿着第一个顶点的一条路径递归查找到最后一个顶点,然后返回查找未被探索的路径,直到所有路径都被查到。
{% asset_img 深度优先搜索.jpg 深度优先搜索图解 %}
let Colors = {
WHITE: 0,
GRAY: 1,
BLACK: 2
}
// 深度遍历顶点的方法
let depthFirstSearchVisit = (u, color, adjList, callback) => {
color[u] = Colors.GREY;
if (callback) callback(u);
// 遍历顶点
adjList.get(u).forEach(n => {
// 如果顶点的颜色为白色,则将此顶点推入栈中
if (color[n] === Colors.WHITE) {
depthFirstSearchVisit(n, color, adjList, callback);
}
});
// 处理过的顶点置为黑色
color[u] = Colors.BLACK;
};
let depthFirstSearch = (graph, callback) => {
// 获取当前的顶点列表
let vertices = graph.getVertices();
// 获取当前的边列表
let adjList = graph.getAdjList();
// 初始化顶点状态
let color = initializeColor(vertices);
// 遍历为被访问过的顶点
vertices.forEach(v => {
if (color[v] === Colors.WHITE) {
depthFirstSearchVisit(v, color, adjList, callback);
}
});
};
参考:yancy__ 、 Jerzy 、raywenderlich