文章目录
什么是图
- 图结构是一种与
树结构
有些相似的数据结构。 - 图论是数学的一个分支,并且,在数学的概念上,树是图的一种。
- 它以图为研究对象,研究顶点和边组成的图形的数学理论和方法。
- 主要研究的目的是事物之间的关系,顶点代表事物,边代表两个事物之间的关系
图的特点
- 一组顶点:通常V(Vertex)表示顶点的集合
- 一组边:通常用E(Edge)表示边的集合
- 边是顶点与顶点之间的连线
- 边可以有向的,也可以是无向的
欧拉七桥的问题
解决
奇点的数目不是0个就是2个
奇点,连接的边为奇数个
相应的偶点,就是连接的边是偶数个的
图的术语
- 顶点 :表示图中的一个节点
- 边 : 表示顶点与顶点之间的连线
- 相邻顶点 : 表示一条边连接的两个顶点
- 顶点的度: 一个顶点连接的边的个数就是顶点的度
- 路径: 一个顶点到另一个顶点之间的经过的边,点顶就是其中的一个路径
- 回路:一个点沿着一条路径到达一个顶点,然后可以沿着相同的路径回到起点,这条路径就是回路
- 无向图:图中的边没有方向
- 有向图: 图中的边有方向
- 无权图:图中的边没有携带权重
- 有权图:图中的边携带了权重
图的表示
- 邻接矩阵表示法
让每一个节点和一个整数项关联,该整数作为数组的下标值,
用一个二维数组表示顶点之间的连接
-
邻接矩阵的问题
- 如果图是一个稀疏图,那么矩阵中会有大量的0,这样属于是浪费了存储空间。 -
邻接表表示法
- 领接表由图中
每个顶点
以及和顶点相邻的顶点列表
组成 - 这个列表可以由 数组/链表/哈希表 存储
- 领接表由图中
邻接表的缺点
- 计算出度比较容易,计算入度比较麻烦(出度:指向别人的边的数量,入度:指向自己的边的数量)
- 它必须构建一个’逆邻接表“,才能有效的计算入度,但是开发中’入度’相对用的较少
图的遍历(每个顶点都要访问一遍,且不能出现重复访问)
- 广度优先搜索(Breadth - first search)(bfc)
先查找相邻顶点,查找完成后再查找相邻节点的相邻节点····
- 深度优先搜索(Depth - first search)(dfs)
先沿着一条路径查找到底部直到附近找不到已被探索标记的顶点,然后沿路径返回上一级顶点,再次查找有无未探索的相邻节点
有,则继续探索,无,则继续返回,再重复查找操作,直到返回根顶点
探索过程:1 —— > 17 (其中:9,10,12,13,14,15,16,17是返回过程)
- 这两种遍历方法,都需要指定第一个遍历顶点
顶点的表示(设置访问状态)
- 灰色:表示该顶点被访问过了,但未被探索过。
- 白色:表示该顶点未被访问过。
- 黑色:表示该顶点被访问过且被探索过
bfs:基于队列,入队列的顶点先被探索
dfs:基于栈或者使用递归,通过将顶点存入栈,顶点是沿着路径被探索的,存在新的相邻顶点就去访问
图的实现
创建实例图
// 创建实现图
class MyGraph {
// 顶点
#vertices = [];
// 边(通过map类型实现)
#edEgs = new Map();
}
创建出来的顶点以数组的方式存储,创建的边以map类型的方式存储
当要添加顶点关系时,使用map的set方法添加顶点A
和接收与它相邻顶点的信息
如添加A节点与它相邻节点的信息:xxx.set(‘A’,[]);
数组收集所有相邻节点信息,有新的相邻节点则取出对应map中对应数组,然后添加(push)顶点信息
引入队列
因为广度优先搜索需要用到队列,先进先出,后进后出
class Queue {
#arr = [];
constructor() {
}
// 向数组后端添加数据
enqueue() {
for (const number of Array.prototype.slice.call(arguments)) {
this.#arr.push(number);
}
return this.#arr;
}
// 向数组前端删除数据
dequeue() {
let lastNum = this.#arr[0];
this.#arr.shift();
return lastNum;
}
// 查看队列是否为空
isEmpty() {
return !this.#arr.length;
}
// 查看队列
toString() {
return this.#arr.join(' ');
}
// 查看队列元素个数
size() {
return this.#arr.length;
}
}
添加顶点方法
// 添加顶点方法
addVertices(v) {
// 判断添加顶点已存在???
if (this.#vertices.indexOf(v) !== -1) {
throw new Error(`${v} existing!`);
}
// 添加顶点
this.#vertices.push(v);
// 添加顶点到map
this.#edEgs.set(v, []);
}
当添加顶点时,就在对应存储顶点信息的数组中添加顶点,同时在map(边)中添加对应顶点的相邻顶点信息
console.log('-----------添加顶点---------');
myGraph.addVertices('A');
myGraph.addVertices('B');
myGraph.addVertices('C');
myGraph.addVertices('D');
myGraph.addVertices('E');
myGraph.addVertices('F');
myGraph.addVertices('G');
myGraph.addVertices('H');
myGraph.addVertices('I');
添加边
// 添加边
addEdEg(v1, v2) {
// 判断越界
this.#crossingTheLineJudge(v1, v2);
this.#edEgs.get(v1).push(v2);
this.#edEgs.get(v2).push(v1);
}
向map中对应key值的value(数组)中添加相连节点信息,注意两个顶点的相邻顶点信息都要添加
console.log('-----------建立顶点之间的边---------');
myGraph.addEdEg('A', 'B');
myGraph.addEdEg('A', 'C');
myGraph.addEdEg('A', 'D');
myGraph.addEdEg('C', 'D');
myGraph.addEdEg('C', 'G');
myGraph.addEdEg('D', 'G');
myGraph.addEdEg('D', 'H');
myGraph.addEdEg('B', 'E');
myGraph.addEdEg('B', 'F');
myGraph.addEdEg('E', 'I');
结构
toString方法
// 打印方法
toString() {
let str = '',
i = 0,
j;
//循环打印顶点信息
do {
str += this.#vertices[i] + ' = >';
j = 0;
//取出对应相邻顶点列表
let mapArray = this.#edEgs.get(this.#vertices[i]);
//循环打印相邻顶点列表
do {
str += mapArray[j];
} while (++j < mapArray.length);
str += '\n';
} while (++i < this.#vertices.length);
return str;
}
实现思路:先循环打印顶点信息数组中的顶点,然后嵌套循环打印对应顶点的相邻顶点信息
测试
console.log('-----------字符串打印输出---------');
let data = myGraph.toString();
console.log(data);
-----------字符串打印输出---------
A = >BCD
B = >AEF
C = >ADG
D = >ACGH
E = >BI
F = >B
G = >CD
H = >D
I = >E
图的遍历
广度优先搜索
- 颜色初始化方法
// 初始化颜色标记
#initColor() {
// 创建一个颜色标记数组
let colors = [], i = 0;
do {
// 添加顶点默认标记
colors[this.#vertices[i]] = 'white';
} while (++i < this.#vertices.length)
return colors;
}
- 搜索
// 广度优先搜索
breadthFirstSearch(currentVer, callback) {
// 判断越界
this.#crossingTheLineJudge(currentVer);
// 1 初始化颜色标记
let verColor = this.#initColor();
// 2 创建队列
let myQueue = new Queue();
// 3 入队列
myQueue.enqueue(currentVer);
// 4 循环出队列---当队列中无数据是终止循环
do {
// 5 出队列获得出队列元素值
let toQueue = myQueue.dequeue();
// 6 获取出队列元素的相邻顶点
let adjacentVertices = this.#edEgs.get(toQueue);
// 改变访问状态 - - 方便终止出队列循环
verColor[toQueue] = 'gray';
let i = 0;
// 这个循环的作用是---获得此时出队列的顶点的相邻顶点
do {
// 判断顶点颜色状态是不是白色---未访问状态
if (verColor[adjacentVertices[i]] === 'white') {
// 为访问状态则入队列 -- 并改变颜色状态
verColor[adjacentVertices[i]] = 'gray';
// 相邻顶点入队列
myQueue.enqueue(adjacentVertices[i]);
}
} while (++i < adjacentVertices.length)
// 使用回调函数获取所遍历得的值 -- 访问 注意访问后颜色变为黑色
callback(toQueue);
verColor[toQueue] = 'black';
} while (!myQueue.isEmpty())
}
实现思路,从指定顶点开始遍历,将此顶点入队列,再出队列遍历顶点的相邻顶点信息,若相邻顶点未被访问(颜色值为白色)则入队列,然后继续出队列(循环实现,终止条件是队列为空时),然后再遍历这个顶点的相邻顶点信息,未访问就入队列,直到所有入队列的顶点出队列完,就停止循环。在此过程中将出队列的顶点打印。注意访问后颜色的改变
测试
console.log('-----------广度优先搜索---------');
// 第一个参数表示从那个顶点开始遍历,第二个参数用于接收遍历所拿到的顶点
let breadthFirstSearchData = [];
myGraph.breadthFirstSearch('D', (item) => {
breadthFirstSearchData.push(item);
});
console.log(breadthFirstSearchData);
-----------广度优先搜索---------
[
'D', 'A', 'C',
'G', 'H', 'B',
'E', 'F', 'I'
]
深度优先搜索
// 深度优先搜素
depthFirstSearch(currentVer, callback) {
// 判断
this.#crossingTheLineJudge(currentVer);
// 初始化顶点
let verColor = this.#initColor();
// 调用递归方法
this.#recursive(currentVer, verColor, callback);
}
// 深度搜索递归
#recursive(current, colors, callback) {
// 返回当前顶点
callback(current);
// 改变访问状态
colors[current] = 'black';
// 获取当前顶点的相邻顶点
let ldin = this.#edEgs.get(current);
let i = 0;
// 循环遍历出当前顶点的相邻顶点
do {
// 相邻顶点未访问,则添加访问,并开始递归
if (colors[ldin[i]] === 'white') {
colors[ldin[i]] = 'gray';
this.#recursive(ldin[i], colors, callback);
}
} while (++i < ldin.length)
}
实现思路:递归访问
从初始顶点开始,先初始化访问状态,然后传入顶点信息,颜色信息,回调到递归方法
先打印访问到的顶点
然后改变访问状态为已打印
获得顶点的相邻顶点信息
遍历相邻顶点信息
若相邻顶点状态为未访问,则改变状态为访问未打印,再递归
测试
console.log('-----------深度优先搜索---------');
let depthFirstSearchData = [];
myGraph.depthFirstSearch('I', (item) => {
depthFirstSearchData.push(item);
});
console.log(depthFirstSearchData);
-----------深度优先搜索---------
[
'I', 'E', 'B',
'A', 'C', 'D',
'G', 'H', 'F'
]
越界判断
判断创建边
,广度搜索
,深度搜索
时初始顶点没有的方法
//特殊判断
#crossingTheLineJudge() {
[...arguments].forEach((item) => {
let index = this.#vertices.indexOf(item);
// 在遍历搜索中,起始顶点不存在则报错
if (index === -1) {
throw new Error(`Vertices don't exist is "${item}"`);
}
})
}
全部代码
// import {Queue} from './队列/demo';
class Queue {
#arr = [];
constructor() {
}
// 向数组后端添加数据
enqueue() {
for (const number of Array.prototype.slice.call(arguments)) {
this.#arr.push(number);
}
return this.#arr;
}
// 向数组前端删除数据
dequeue() {
let lastNum = this.#arr[0];
this.#arr.shift();
return lastNum;
}
// 查看队列是否为空
isEmpty() {
return !this.#arr.length;
}
// 查看队列
toString() {
return this.#arr.join(' ');
}
// 查看队列元素个数
size() {
return this.#arr.length;
}
}
// 创建实现图
class MyGraph {
// 顶点
#vertices = [];
// 边(通过map类型实现)
#edEgs = new Map();
// 添加顶点方法
addVertices(v) {
// 判断添加顶点已存在???
if (this.#vertices.indexOf(v) !== -1) {
throw new Error(`${v} existing!`);
}
// 添加顶点
this.#vertices.push(v);
// 添加顶点到map
this.#edEgs.set(v, []);
}
// 添加边
addEdEg(v1, v2) {
// 判断
this.#crossingTheLineJudge(v1, v2);
this.#edEgs.get(v1).push(v2);
this.#edEgs.get(v2).push(v1);
}
// 打印方法
toString() {
let str = '',
i = 0,
j;
do {
str += this.#vertices[i] + ' = >';
j = 0;
let mapArray = this.#edEgs.get(this.#vertices[i]);
do {
str += mapArray[j];
} while (++j < mapArray.length);
str += '\n';
} while (++i < this.#vertices.length);
return str;
}
// 初始化颜色标记
#initColor() {
// 创建一个颜色标记数组
let colors = [], i = 0;
do {
// 添加顶点默认标记
colors[this.#vertices[i]] = 'white';
} while (++i < this.#vertices.length)
return colors;
}
// 图的遍历
// 广度优先搜索
breadthFirstSearch(currentVer, callback) {
// 判断
this.#crossingTheLineJudge(currentVer);
// 1 初始化颜色标记
let verColor = this.#initColor();
// 2 创建队列
let myQueue = new Queue();
// 3 入队列
myQueue.enqueue(currentVer);
// 4 循环出队列---当队列中无数据是终止循环
do {
// 5 出队列获得出队列元素值
let toQueue = myQueue.dequeue();
// 6 获取出队列元素的相邻顶点
let adjacentVertices = this.#edEgs.get(toQueue);
// 改变访问状态 - - 方便终止出队列循环
verColor[toQueue] = 'gray';
let i = 0;
// 这个循环的作用是---获得此时出队列的顶点的相邻顶点
do {
// 判断顶点颜色状态是不是白色---未访问状态
if (verColor[adjacentVertices[i]] === 'white') {
// 为访问状态则入队列 -- 并改变颜色状态
verColor[adjacentVertices[i]] = 'gray';
// 相邻顶点入队列
myQueue.enqueue(adjacentVertices[i]);
}
} while (++i < adjacentVertices.length)
// 使用回调函数获取所遍历得的值 -- 访问 注意访问后颜色变为黑色
callback(toQueue);
verColor[toQueue] = 'black';
} while (!myQueue.isEmpty())
// console.log(verColor)
}
// 深度优先搜素
depthFirstSearch(currentVer, callback) {
// 判断
this.#crossingTheLineJudge(currentVer);
// 初始化顶点
let verColor = this.#initColor();
// 调用递归方法
this.#recursive(currentVer, verColor, callback);
}
// 深度搜索递归
#recursive(current, colors, callback) {
// 返回当前顶点
callback(current);
// 改变访问状态
colors[current] = 'black';
// 获取当前顶点的相邻顶点
let ldin = this.#edEgs.get(current);
let i = 0;
// 循环遍历出当前顶点的相邻顶点
do {
// 相邻顶点未访问,则添加访问,并开始递归
if (colors[ldin[i]] === 'white') {
colors[ldin[i]] = 'gray';
this.#recursive(ldin[i], colors, callback);
}
} while (++i < ldin.length)
}
//特殊判断
#crossingTheLineJudge() {
[...arguments].forEach((item) => {
let index = this.#vertices.indexOf(item);
// 在遍历搜索中,起始顶点不存在则报错
if (index === -1) {
throw new Error(`Vertices don't exist is "${item}"`);
}
})
}
}
测试
let myGraph = new MyGraph();
console.log('-----------添加顶点---------');
myGraph.addVertices('A');
myGraph.addVertices('B');
myGraph.addVertices('C');
myGraph.addVertices('D');
myGraph.addVertices('E');
myGraph.addVertices('F');
myGraph.addVertices('G');
myGraph.addVertices('H');
myGraph.addVertices('I');
console.log('-----------建立顶点之间的边---------');
myGraph.addEdEg('A', 'B');
myGraph.addEdEg('A', 'C');
myGraph.addEdEg('A', 'D');
myGraph.addEdEg('C', 'D');
myGraph.addEdEg('C', 'G');
myGraph.addEdEg('D', 'G');
myGraph.addEdEg('D', 'H');
myGraph.addEdEg('B', 'E');
myGraph.addEdEg('B', 'F');
myGraph.addEdEg('E', 'I');
console.log('-----------字符串打印输出---------');
let data = myGraph.toString();
console.log(data);
console.log('-----------广度优先搜索---------');
// 第一个参数表示从那个顶点开始遍历,第二个参数用于接收遍历所拿到的顶点
let breadthFirstSearchData = [];
myGraph.breadthFirstSearch('D', (item) => {
breadthFirstSearchData.push(item);
});
console.log(breadthFirstSearchData);
console.log('-----------深度优先搜索---------');
let depthFirstSearchData = [];
myGraph.depthFirstSearch('I', (item) => {
depthFirstSearchData.push(item);
});
console.log(depthFirstSearchData);
-----------添加顶点---------
-----------建立顶点之间的边---------
-----------字符串打印输出---------
A = >BCD
B = >AEF
C = >ADG
D = >ACGH
E = >BI
F = >B
G = >CD
H = >D
I = >E
-----------广度优先搜索---------
[
'D', 'A', 'C',
'G', 'H', 'B',
'E', 'F', 'I'
]
-----------深度优先搜索---------
[
'I', 'E', 'B',
'A', 'C', 'D',
'G', 'H', 'F'
]