在计算机程序设计中,图也是一种非常常见的数据结构。图论是一个非常大的话题,在数学上起源于哥尼斯堡七桥问题。
图是一种与树有些相似的数据结构。
图长什么样子呢?或者什么样的数据使用图来模拟更合适呢?
人与人之间的关系网
互联网中的网络关系
广州地铁图
什么是图呢?
图通常有什么特点呢?
我们在学习树的时候,树有很多的其他术语,了解这些术语有助于我们更深层次的理解图。
图的术语也非常多,如果你找一本专门讲图的各个方面的书籍,会发现只是术语就可以占据一个章节。
这里,这里介绍几个比较常见的术语,某些术语后面用到的时候,再了解,没有用到的,不做赘述。
通过下图来了解图的术语:
顶点
边
相邻顶点
0 - 1
是相邻的,0 - 3
是相邻的。0 - 2
是不相邻的。度
路径
v1
,v2
...,vn
的一个连续序列,比如上图中 0 1 5 9
就是一条路径。0 1 5 9
是一条简单路径。0 1 5 6 3 0
。无向图
0 - 1
之间有边,那么说明这条边可以保证 0 -> 1
,也可以保证 1 -> 0
。有向图
0 -> 1
,不能保证一定可以 1 -> 0
,要根据方向来定。无权图
0 - 1
的边,比 4 - 9
的边更远或者用的时间更长。带权图
对交通流量建模
对飞机航线建模
一个图包含很多顶点,另外包含顶点和顶点之间的连线(边),这两个都是非常重要的图信息,因此都需要在程序中体现出来。
顶点的表示相对简单
边的表示略微复杂,因为边是两个顶点之间的关系,所以表示起来会稍微麻烦一些。
概述
图片解析
邻接矩阵的问题
如果是一个无向图,邻接矩阵展示出来的二维数组,其实是一个对称图。 也就是 A -> D 是 1 的时候,对称的位置 D -> 1 一定也是 1。 那么这种情况下会造成空间的浪费,解决办法需自己去研究下。
如果图是一个稀疏图 那么矩阵中将存在大量的 0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。 而且即使只有一个边,我们也必须遍历一行来找出这个边,也浪费很多时间。
概述
图片解析
比如我们要表示和 A 顶点有关联的顶点(边),A 和 B/C/D 有边,那么我们可以通过 A 找到 对应的数组/链表/字典,再取出其中的内容。
邻接表的问题
先来创建 Graph 类,定义了两个属性:
vertexes
用于存储所有的顶点,使用一个数组来保存。adjList
adj 是 adjoin 的缩写,邻接的意思。adjList 用于存储所有的边,这里采用邻接表的形式。class Graph {constructor() {this.vertexes = []; // 存储顶点this.adjList = new Dictionay(); //存储边信息}}
向图中添加一些顶点。
[]
,该数组用于存储顶点连接的所有的边。(回顾邻接表的实现方式)// 添加顶点addVertex(val) {// 添加点this.vertexes.push(val)// 添加点的关系 采用邻接矩阵法 结构用 Mapthis.adjList.set(val, [])}
可以指定顶点和顶点之间的边。
// 添加边addEdge(val1, val2) {// 添加边需要传入两个顶点,因为边是两个顶点之间的边,边不可能单独存在。// 这里实现的是无向图,所以这里不考虑方向问题this.adjList.get(val1).push(val2)this.adjList.get(val2).push(val1)}
为了能够正确的显示图的结果,就是拿出二维数组的每一项。
// 输出图结构toString() {let res = ''for (let i = 0; i < this.vertexes.length; i++) {res += this.vertexes[i] + "->"let adj = this.adjList.get(this.vertexes[i])for (let j = 0; j < adj.length; j++) {res += adj[j] + ""}res += "\n"}return res}
// 测试代码let graph = new Graph();// 添加顶点let myVertexes = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];for (let i = 0; i < myVertexes.length; i++) {graph.addVertex(myVertexes[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");
和其他数据结构一样,需要通过某种算法来遍历图结构中每一个数据。这样可以保证,在我们需要时,通过这种算法来访问某个顶点的数据以及它对应的边。
图的遍历算法的思想在于必须访问每个第一次访问的节点,并且追踪有哪些顶点还没有被访问到。
有两种算法可以对图进行遍历
这两种遍历算法,都需要明确指定第一个被访问的顶点。
遍历的注意点
两种算法的思想
为了记录顶点是否被访问过,我们使用三种颜色来反应它们的状态。(或者两种颜色也可以)
// 初始化顶点的颜色initializeColor() {// 白色:表示该顶点还没有被访问。// 灰色:表示该顶点被访问过,但并未被探索过。// 黑色:表示该顶点被访问过且被完全探索过。let colors = []for (let i = 0; i < this.vertexes.length; i++) {colors[this.vertexes[i]] = "white"}return colors}
广度优先搜索算法的思路 广度优先算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深的访问顶点。
图解 BFS
广度优先搜索的实现
广度优先搜索的代码
// 广度优先搜索bfs(handle) {// 1.初始化颜色let color = this._initializeColor()// 2. 创建队列let queue = new Queue// 3. 将传入的顶点放入队列queue.enqueue(this.vertexes[0])// 4.依赖队列操作数据 队列不为空时一直持续while (!queue.isEmpty()) {// 4.1 拿到队头let qVal = queue.dequeue()// 4.2 拿到队头所关联(相连)的点并设置为访问中状态(灰色)let qAdj = this.adjList.get(qVal)color[qVal] = "gray"// 4.3 将队头关联的点添加到队尾// 这一步是完成bfs的关键,依赖队列的先进先出的特点。for (let i = 0; i < qAdj.length; i++) {let a = qAdj[i]if (color[a] === "white") {color[a] = "gray"queue.enqueue(a)}}// 4.5设置访问完的点为黑色。color[qVal] = "black"if (handle) [handle(qVal)]}}
测试代码
// 调用广度优先算法let result = "";graph.bfs(graph.vertexes[0], function (v) {result += v + " ";});console.log(result); // A B C D E F G H I
深度优先搜索的思路:
深度优先搜索算法的实现:
广度优先搜索算法我们使用的是队列,这里的深度优先搜索算法可以使用栈完成,也可以使用递归。(递归本质上就是函数栈的调用)
深度优先搜索算法的代码:
// 深度优先搜索dfs(handle) {// 1.初始化颜色let color = this._initializeColor()// 2. 遍历所有顶点,开始访问for (let i = 0; i < this.vertexes.length; i++) {if (color[this.vertexes[i]] === "white") {this._dfsVisit(this.vertexes[i], color, handle)}}}// dfs 的递归方法 这里直接使用函数的调用栈_dfsVisit(val, color, handle) {// 1. 将颜色设置为访问中color[val] = "gray"// 2. 执行相应的回调if (handle) {handle(val)}// 3. 拿与该点相邻的点,对每个点操作let adj = this.adjList.get(val)for (let i = 0; i < adj.length; i++) {let w = adj[i]// 如果相邻点未未访问状态,开始访问。if (color[w] === "white") {this._dfsVisit(w, color, handle)}}// 4. 处理完后设置为访问过点。color[val] = "black"}
测试代码
// 调用深度优先算法result = "";graph.dfs(function (v) {result += v + " ";});// 输出深度优先console.log(result); //A B E I F C D G H
递归的代码较难理解一些,这副图来帮助理解过程: