在计算机中,图结构也是一种非常常见的数据结构。图论也是一个非常大的话题
图结构是一种与树结构有些相似的数据结构。
图论是数学的一个分支,并且,在数学的概念上,树是图的一种。
图主要研究的目的是事物之间的关系,顶点代表事物,边代表两个事物间的关系。
人与人之间的关系(比如六度空间理论),地点之间的联系图(地图App,就是通过图来计算最短路径或最优路径)
边就是顶点之间的连线
边可以是有向的也可以是无向的
比如A -> B:表示有向; A–B:表示无向
怎么在程序中表示图?
一个图包含很多顶点,另外包含顶点和顶点之间的连线(边)这两个都是非常重要的图信息,因此都需要在程序中体现出来
顶点的表示,可以抽象成ABCD,或者一些别的字符。然, A, B,C,D有可能还表示其他含义的数据(比如村庄的名字)
那么边怎么表示呢?
因为边是两个顶点之间的关系,所以表示起来会稍微麻烦一些.下面,我们具体讨论一下常见的表示方式
常见的表示图的方式:
另外一种常用的表示图的方式:邻接表
邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。这个列表有很多中方式来存储:数组/链表/字典(哈希表)都可以
图片比较容易理解:比如我们要表示和A顶点有关联的顶点(边),A和B/C/D有边,
那么我们可以通过A找到对应的数组/链表/字典,再取出其中的内容就可以。
邻接表的问题:
邻接表计算"出度"是比较简单的(出度:指向别人的数量,入度:指向自己的数量),邻接表如果需要计算有向图的"入度",那么是一件非常麻烦的事情,它必须构造一个“逆邻接表”,才能有效的计算“入度”,但开发中“入度”相对用的比较少。
// 封装图结构(用邻接表的方式封装)
class Graph {
constructor() {
// 属性:顶点(数组)/边(字典)
this.vertexs = [] // 顶点
this.edges = new Map() // 边
}
// 添加顶点的方法
addVertex(v) {
// 首先添加顶点
this.vertexs.push(v)
// 添加顶点的时候,初始化一个数组,存储相邻的顶点
this.edges.set(v, [])
}
// 添加边的方法,传入两个顶点(无向图的添加)
addEdge(v1, v2) {
this.edges.get(v1).push(v2)
this.edges.get(v2).push(v1)
}
// 实现toString方法
toString() {
// 1.定义字符串,保存最终的结果
let res = ''
let val
// 2.遍历所有的顶点,以及顶点对应的边
this.vertexs.forEach(item => {
res += item + '->'
this.edges.get(item).forEach(item => {
res += item + ','
// console.log(key, value);
})
res += '\n'
})
return res
}
}
// 测试代码
// 1.实例化图结构
let graph = new Graph()
const myVerTexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
// 2.添加顶点
myVerTexes.forEach(item => {
graph.addVertex(item)
})
// 3.添加边
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')
alert(graph)
图的遍历思想:
有两种算法可以对图进行遍历
用三种颜色表示顶点的状态
广度优先搜索会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,一次访问一层,也就是先宽度后深度的访问顶点
如图,访问顺序为:A-B-C-D-E-F-G-H-I
代码实现思路:
基于队列(先进先出)实现:
- 将V从Q中出队
- 将V标注为被发现的灰色
- 将V所有的未被访问过的邻接点标注为白色,加入到队列中
- 将V标注为黑色
代码实现:
// 初始化状态颜色
initializeColor() {
let colors = []
this.vertexs.forEach(item => {
// 初始化为白色
colors[item] = 'white'
})
return colors
}
// 广度优先搜索,传入两个参数:初始化的顶点和处理函数
bfs(initV, handler) {
// debugger
// 1.所有顶点初始化颜色
let colors = this.initializeColor()
// 2.创建队列
let queue = new Queue()
// 3.顶点入队
queue.enqueue(initV)
// 4.循环从队列中取出元素
while (!queue.isEmpty()) {
// 4.1 从队列中取出一个顶点
let v = queue.dequeue()
// 4.2 获取和顶点相邻的其他顶点
let vList = this.edges.get(v)
// 4.3 将v的颜色设置为灰色
colors[v] = 'gray'
// 4.4 遍历所有相邻的其他顶点,并且加入到队列中
vList.forEach((item) => {
// 此步判断是否访问过,只入队没访问过的,避免重复入队
if (colors[item] == 'white') {
// 访问过后改变颜色
colors[item] = 'gray'
// 入队
queue.enqueue(item)
}
})
// 4.5 处理节点
handler(v)
// 4.6 访问完的点为黑色
colors[v] = 'black'
}
}
测试代码:
// 测试bfs 这里传入第一个节点
let res = ''
graph.bfs(graph.vertexs[0], function (v) {
res += v + ' '
})
alert(res)
深度优先搜索的思路:
深度优先搜索算法的实现思路:
代码实现:
// 深度优先搜索
dfs(initV, handler) {
// 1.所有顶点初始化颜色
let colors = this.initializeColor()
// 2.从某个顶点开始依次递归访问
this.dfsVisit(initV, colors, handler)
}
// dfs的递归函数
dfsVisit(v, colors, handler) {
// 1.将颜色设置为灰色
colors[v] = 'gray'
// 2.处理v顶点
handler(v)
// 3.访问v相连的顶点
let vList = this.edges.get(v)
vList.forEach(item => {
if (colors[item] == 'white') {
this.dfsVisit(item, colors, handler)
}
})
// 4.将v设置为黑色
colors[v] = 'black'
}
测试代码:
// 测试dfs
res = ''
graph.dfs(graph.vertexs[0], function (v) {
res += v + ' '
})
alert(res)