G=(V, E)
V: 一组顶点
E: 一组边,连接V中的顶点
相邻顶点:由一条边连接在一起的顶点
度:相邻顶点的数量
路径:顶点v1,v2…vk的一个连续序列。
简单路径:不包含重复的顶点。
环:如果该图不存在环,则该图是无环的
连通:如果途中每两个顶点间都存在路径,则该图是连通的
有向图无向图:图可以是无向的(边没有方向)或有向的(有向图)
强连通:如果图中每两个顶点间在双向上都存在路径,则该图是强连通。
加权:加权图的边被赋予了权值。
每个节点都和一个整数相关,该整数作为数组的索引。
用数组表示顶点间的链接。如果索引为i和索引为j的节点相邻,
则array[i][j] === 1,否则为0
图:每个节点为行头和列头,行列相邻得1。
非强联通的图(稀疏图),会浪费存储空间来表示不存在的边。
顶点的数量会变,数组不灵活。
动态数据结构
每个顶点的相邻顶点列表组成
可用列表,链表,哈希表,字典等数据结构来表示
图:一个顶点为行头,向右展开它的相邻顶点,没有列头
大多数问题的比较好的选择
矩阵的行表示顶点,列表示边。
使用数组表示两者间的连通性,如果顶点v是边e的入射点,
则array[v][e] === 1, 否则为0
图:顶点为行头,每个顶点的每种相邻关系为列,相邻且在该行顶点中得1。
图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环
算法 | 数据结构 | 描述 |
---|---|---|
深度优先搜索 | 栈 | 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问 |
广度优先搜索 | 队列 | 通过将顶点存入队列中,最先入队列的顶点先被探索 |
当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。
从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访
问图的一层。换句话说,就是先宽后深地访问顶点
(1) 创建一个队列Q。
(2) 将v标注为被发现的(灰色),并将v入队列Q。
(3) 如果Q非空,则运行以下步骤:
(a) 将u从Q中出队列;
(b) 将标注u为被发现的(灰色);
© 将u所有未被访问过的邻点(白色)入队列;
(d) 将u标注为已被探索的(黑色)。
求出最短路径
本章中的图不是加权图。
如果要计算加权图中的最短路径
(例如,城市A和城市B之间的最短路径——GPS和Google Maps中用到的算法),
广度优先搜索未必合适。
其他:
Dijkstra’s算法解决了单源最短路径问题。
Bellman–Ford算法解决了边权值为负的单源最短路径问题。
A*搜索算法解决了求仅一对顶点间的最短路径问题,它用经验法则来加速搜索过程。
Floyd–Warshall算法解决了求所有顶点对间的最短路径这一问题。
会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶
点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点
不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访
问该顶点v。
要访问顶点v,照如下步骤做。
(1) 标注v为被发现的(灰色)。
(2) 对于v的所有未访问的邻点w:
(a) 访问顶点w。
(3) 标注v为已被探索的(黑色)。
function Dictionary() {
let items = {}
this.set = function (key, value) {
items[key] = value
}
this.remove = function (key) {
if (this.has(key)) {
delete items[key]
return true
}
return false
}
this.has = function (key) {
return items.hasOwnProperty(key)
}
this.get = function (key) {
return this.has(key) ? items[key] : undefined
}
this.clear = function () {
items = {}
}
this.size = function () {
return Object.keys(items).length
}
this.keys = function () {
return Object.keys(items)
}
this.values = function () {
return Object.values(items)
}
this.getItems = function () {
return items
}
}
let Queue = function () {
let items = []
this.enqueue = function (v) {
items.push(v)
}
this.dequeue = function (v) {
return items.shift(v)
}
this.front = function (v) {
return items[0]
}
this.isEmpty = function (v) {
return items.length === 0
}
this.size = function (v) {
return items.length
}
this.print = function (v) {
console.log(items.toString())
}
}
function Stack() {
let items = []
this.push = function (v) {
items.push(v)
}
this.pop = function (v) {
return items.pop(v)
}
this.peek = function () {
return items[items.length - 1]
}
this.isEmpty = function () {
return items.length === 0
}
this.clear = function () {
items = []
}
this.size = function () {
return items.length
}
this.print = function () {
console.log(items.toString())
}
}
// 存储顶点
// 顶点对应的字典
// 形成顶点为行左开头,字典为行内容的图。
function Graph() {
let vertices = [] // 存储顶点
let adjList = new Dictionary() // 顶点关系字典表
this.addEdge = function (v, w) {
// 互存顶点关系
adjList.get(v).push(w)
adjList.get(w).push(v)
}
// 初始化顶点以及顶点关系字典表
this.addVertex = function (v) {
vertices.push(v) // 创建顶点记录
adjList.set(v, []) // 创建顶点行下标
}
this.print = function () {
console.log(vertices)
}
this.printAdjList = function () {
Object.entries(adjList.getItems()).map(item =>
console.log(item[0], item[1])
)
}
this.toString = function () {
var s = ''
for (var i = 0; i < vertices.length; i++) {
//{10}
s += vertices[i] + ' -> '
var neighbors = adjList.get(vertices[i]) //{11}
for (var j = 0; j < neighbors.length; j++) {
//{12}
s += neighbors[j] + ' '
}
s += '\n' //{13}
}
return s
}
// 颜色标记法
// 初始化颜色标识函数,全部顶点为白色。
let initColor = function () {
let color = {}
for (let i = 0; i < vertices.length; i++) {
color[vertices[i]] = 'white'
}
return color
}
// 广度优先遍历
/*
寻找最短路径
给定一个图G和源顶点v,找出对每个顶点u,u和v之间最短路径的距离(以边的数量计)。
对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推
*/
/*
(1) 创建一个队列Q。
(2) 将v标注为被发现的(灰色),并将v入队列Q。
(3) 如果Q非空,则运行以下步骤:
(a) 将u从Q中出队列;
(b) 将标注u为被发现的(灰色);
(c) 将u所有未被访问过的邻点(白色)入队列;
(d) 将u标注为已被探索的(黑色)。
*/
// 使用队列,依靠队列的先入先出来操作查找相关的顶点
this.bfs = function (v, cb) {
let color = initColor()
let queue = new Queue()
// 距离
let d = []
// 前溯点
let pred = []
// 初始化,顶点入栈
for (let i = 0; i < vertices.length; i++) {
d[vertices[i]] = 0
pred[vertices[i]] = null
}
// 入列开启搜索
queue.enqueue(v)
while (!queue.isEmpty()) {
// u出列
let u = queue.dequeue()
// u被发现
color[u] = 'grey'
// 取出邻近顶点数组
let neighbors = adjList.get(u)
for (let i = 0; i < neighbors.length; i++) {
// 一个邻顶点
let w = neighbors[i]
if (color[w] === 'white') {
// 搜索过的邻顶点入列
queue.enqueue(w)
// 设置已被搜索过
color[w] = 'grey'
// 设置v,w距离 A
d[w] = d[u] + i
// 设置w的前溯点为u。设置该搜索到的顶点w的上一个顶点是u。如果一个顶点有两个父顶点,以第一个为准。
// 据此可以查找当前元素的顶点一直往上找个所有联通的路径。
pred[w] = u
}
}
// 标注被访问过的顶点
color[u] = 'black'
if (cb) {
cb(u)
}
}
return {
distances: d,
predecessors: pred,
}
}
// 深度优先搜索
// 从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,
// 接着原路回退并探索下一条路径。
/*
不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。
(1) 标注v为被发现的(灰色)。
(2) 对于v的所有未访问的邻点w:
(a) 访问顶点w。
(3) 标注v为已被探索的(黑色)。
*/
/*
构建“森林”(有根树的一个集合)以及一组源顶点(根),
并输出两个数组:发现时间和完成探索时间。
*/
this.dfs = function (cb) {
let time = 0 // 次数
let color = initColor()
// 1. 顶点u的发现时间d[u]
let d = []
// 2. 当顶点u被标注为黑色时,u的完成探索时间f[u]
let f = []
// 3. 顶点u的前溯点p[u]
let p = []
// 搜索每个顶点
const dfsVist = function (u, color, cb) {
color[u] = 'grey'
d[u] = ++time
if (cb) cb(u)
let neighbors = adjList.get(u)
for (let i = 0; i < neighbors.length; i++) {
let w = neighbors[i]
if (color[w] === 'white') {
dfsVist(w, color, cb)
p[w] = u
}
}
color[u] = 'black'
f[u] = ++time
}
for (let i = 0; i < vertices.length; i++) {
let vertice = vertices[i]
d[vertice] = 0
f[vertice] = 0
p[vertice] = null
}
for (let i = 0; i < vertices.length; i++) {
if (color[vertices[i]] === 'white') {
dfsVist(vertices[i], color, cb)
}
}
return {
discovery: d,
finished: f,
predecessors: p,
}
}
/*
由:
1. 时间(time)变量值的范围只可能在图顶点数量的一倍到两倍之间;
2. 对于所有的顶点u,d[u]
}
let graph = new Graph()
let myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] // 顶点数组
for (let i = 0; i < myVertices.length; i++) {
// 顶点入表
graph.addVertex(myVertices[i])
}
graph.print()
// 依次添加字典关系表
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')
graph.printAdjList()
/*
// 由字典和数组组成的图结构
A (3) ['B', 'C', 'D']
B (3) ['A', 'E', 'F']
C (3) ['A', 'D', 'G']
D (4) ['A', 'C', 'G', 'H']
E (2) ['B', 'I']
F ['B']
G (2) ['C', 'D']
H ['D']
I ['E']
// 其实是将下面的列根据是否相邻,
// 一个个加入行开头的下标中。
A B C D E F G H I
A x x x
B x x x
C x x x
D x x x x
E x x
F x
G x x
H x
I x
*/
console.log('------------')
// console.log(`graph.toString()`, graph.toString())
// --- 广度优先遍历 ---
//
/*
* @function: 所有s到其他顶点的路径
* @params:
* graph: 图
* myVertices: 所有顶点数组
* s: 源顶点
* d: 目标顶点
*/
function searchPaths(graph, myVertices, s, d) {
let vertices = myVertices.slice()
let index = vertices.indexOf(s)
let shortestPathA = graph.bfs(vertices[index], function (v) {
console.log(`vertex`, v)
})
console.log('shortestPathA', shortestPathA)
// 取源顶点
let fromVertex = vertices[index]
// 除去查找的顶点,剩下的所有顶点
vertices.splice(index, 1)
// 最短路径数组集合
let sortestPathArr = []
let sortestPathArr2 = []
// 遍历查找顶点
for (let i = 0; i < vertices.length; i++) {
let toVertice = vertices[i] // 一个顶点
let path = new Stack() // 一条路径,先入后出,后入先出。
// 当前顶点到源顶点
// 取上一个路径
// 根据 predecessors 来查找顶点
for (
let v = toVertice;
v !== fromVertex;
v = shortestPathA.predecessors[v]
) {
path.push(v)
}
path.push(fromVertex) // 最后入栈源顶点 ba ca da eba fba gca hda ieba
let s = path.pop() // 取出源顶点
let toVerticeArr = [] // 当前路径的数组
// 取其中一个入栈的路径数组用来比较长度
let preToVerticeArr =
sortestPathArr2.length > 0 ? sortestPathArr2[0] : []
toVerticeArr.push(s)
while (!path.isEmpty()) {
let temp = path.pop()
toVerticeArr.push(temp)
s += ' - ' + temp
}
console.log(s)
// 第一次的时候加入一个数组
if (sortestPathArr2.length === 0) {
sortestPathArr2.push(toVerticeArr)
} else {
// 以后每次都和上一个数组比较长度,同样则加入
if (preToVerticeArr.length === toVerticeArr.length) {
sortestPathArr2.push(toVerticeArr)
// 如果是比里面的小,则清空之前的,从新加入。
} else if (preToVerticeArr.length > toVerticeArr.length) {
sortestPathArr2 = []
sortestPathArr2.push(toVerticeArr)
}
}
// 当前路径数组存入总路径数组
sortestPathArr.push(toVerticeArr)
}
// s最小路径数组集合
console.log('s的最小路径数组集合', sortestPathArr2)
sortestPathArr.sort((pre, cur) => {
return pre.length - cur.length
})
// s到d最短路径
let sortestPathArr3 = sortestPathArr.slice()
sortestPathArr3 = sortestPathArr3.filter(item => item.includes(d))
sortestPathArr3.sort((pre, cur) => {
return pre.length - cur.length
})
console.log('A到${d}(H)最短路径', sortestPathArr3[0])
// s从短到长排序的路径集合
return sortestPathArr
}
let pathArr = searchPaths(graph, myVertices, 'A', 'H')
console.log('从短到长排序的路径', pathArr)
/*
* @function: 求出最短路径
* @params:
* graph: 图
* from: 源顶点
* to: 目标顶点
*/
const searchSortestPath = function (graph, from, to) {
let s = graph.bfs('A')
let v = to
let path = new Stack()
while (v != from) {
path.push(v)
v = s.predecessors[v]
}
path.push(v)
let str = ''
while (!path.isEmpty()) {
str += path.pop() + '-'
}
console.log(str.slice(0, -1))
return str.slice(0, -1)
}
searchSortestPath(graph, 'A', 'H')
const currying = function (fn) {
let args = []
return function () {
if (arguments.length === 0) {
return fn.apply(this, args)
} else {
;[].push.apply(args, arguments)
}
}
}
const paths = (function () {
return function () {
return Array.from(arguments).join('-')
}
})()
const memDfsPath = currying(paths)
let dfsInfo = graph.dfs(function (v) {
memDfsPath(v)
})
console.log(`memPath()`, memDfsPath())
console.log(`dfsInfo`, dfsInfo)
// 拓扑排序
graph = new Graph()
myVertices = ['A', 'B', 'C', 'D', 'E', 'F']
for (i = 0; i < myVertices.length; i++) {
graph.addVertex(myVertices[i])
}
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('B', 'D')
graph.addEdge('B', 'E')
graph.addEdge('C', 'F')
graph.addEdge('F', 'E')
var result = graph.dfs()
let discovery = result.discovery
let finished = result.finished
let tuopu = []
Object.keys(discovery).map(key => {
tuopu.push({
key,
value: (discovery[key] / finished[key]).toFixed(3) * 1000,
})
})
const compareObjByKey = function (key) {
return function (a, b) {
return a[key] - b[key]
}
}
tuopu.sort(compareObjByKey('value'))
console.log(`tuopu`, tuopu)
const memTuopuPath = currying(paths)
tuopu.map(item => memTuopuPath(item.key))
console.log(`memTuopuPath`, memTuopuPath())
请问:distances 在这里有什么用?
它描述了s到各个顶点要搜索的次数,用来表示距离。
广度优先和深度优先有什么区别?
为什么广度优先就能找到最短距离,深度优先就能搞定拓扑排序?反过来可以吗?