由一条边连接在一起的顶点
称为: 相邻顶点, 比如,A 和 B 是相邻的,A 和 D 是相邻的,A 和 C 是相邻的,A 和 E 不是相邻的
一个顶点的度是其相邻顶点的数量。比如,A 和其他三个顶点相连接,因此,A 的度为 3; E 和其他两个顶点相连,因此,E 的度为 2。
声明图的骨架
function Graph() {
var vertices = []; // 定义数组来存储图中所有顶点的名字
var adjList = new Dictionary(); // 字典来存储邻接表
}
字典 将会使用顶点的名字作为键,邻接顶点列表作为值。vertices 数组和 adjList 字典两者都是我们 Graph 类的私有属性
接着,我们将实现两个方法: 一个用来向图中添加一个新的顶点(因为图实例化后是空的), 另外一个方法用来添加顶点之间的边。
// 这个方法接受顶点v作为参数。我们将该顶点添加到顶点列表中,并且在邻接表中,
// 设置顶点v作为键对应的字典值为一个空数组
this.addVertex = function(v) {
vertices.push(v);
adjList.set(v, []);
};
// 这个方法接受两个顶点作为参数。
// 首先,通过将w加入到v的邻接表中,我们添加了一条自顶点v到顶点w的边
// 例子采用无向图,所以还要加一条自定点w到v的边
this.addEdge = function(v, w) {
adjList.get(v).push(w);
adjList.get(w).push(v);
};
具体请看这个例子: createGraph.js
和树数据结构类似,我们可以访问图的所有节点。有两种算法可以对图进行遍历: 广度优先搜索(Breadth-First Search,BFS)
和 深度优先搜索(Depth-First Search,DFS)
。
图遍历可以用来干哈?
-
寻找特定的顶点
-
寻找两个顶点之间的路径
-
检查图是否连通
-
检查图是否含有环
算法 | 数据结构 | 描述 |
---|---|---|
深度优先搜索 DFS | 栈 | 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问 |
广度优先搜索 BFS | 队列 | 通过将顶点存入队列中,最先入队列的顶点先被探索 |
首先先搞明白一点,广度优先搜索(BFS)能够让我们找到两样东西之间的最短路径
,也就是说,最短路径问题,用到的算法叫做 “广度优先搜索”,用到的数据结构是 “队列”
但是最短路径问题,不是只有广度优先搜索算法
,比如狄克斯特拉算法
,也是解决最短路径问题
广度优先搜索用于在非加权图中查找最短路径
狄克斯特拉算法用于在加权图中查找最短路径
总的来说,BFS 多用于寻找最短路径的问题,DFS 多用于快速发现底部节点。
废话说的有点多了,下面继续看 DFS 和 BFS,当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。
-
白色:表示该顶点还没有被访问。
-
灰色:表示该顶点被访问过,但并未被探索过。
-
黑色:表示该顶点被访问过且被完全探索过。
这就是之前提到的务必访问每个顶点最多两次的原因
广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访 问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示:
以下是从顶点 v 开始的广度优先搜索算法所遵循的步骤。
-
(1) 创建一个队列 Q。
-
(2) 将 v 标注为被发现的(灰色),并将 v 入队列 Q。
-
(3) 如果 Q 非空,则运行以下步骤:
- (a) 将 u 从 Q 中出队列;
- (b) 将标注 u 为被发现的(灰色);
- (c) 将 u 所有未被访问过的邻点(白色)入队列;
- (d) 将 u 标注为已被探索的(黑色)。
看看该如何实现这个 简易版的 BFS 算法 ?
// 使用一个辅助数组 color。由于当算法开始执行时,所有的顶点颜色都是白色
var initializeColor = function() {
var color = [];
for (let i = 0; i < vertices.length; i++) {
color[vertices[i]] = 'white';
}
return color;
};
this.bfs = function(v, callback) {
var color = initializeColor(); // 1.初始化所有顶点
var queue = new Queue(); // 2.初始化一个队列, 它将会存储待访问和待探索的顶点
queue.enqueue(v); // 3.将起始点 v 入队列
while (!queue.isEmpty()) {
// 4. 队列非空
var u = queue.dequeue(); // 5. 从队列中移除一个顶点
neighbors = adjList.get(u); // 6. 从字典中取得它的邻接表
color[u] = 'grey'; // 7. 将该顶点置为灰色,表示被访问过,但未被探索过
for (let j = 0; j < neighbors.length; j++) {
// 8. 对于出队的这个顶点,遍历它的相邻点
var w = neighbors[j]; // 9. 取得这个点
if (color[w] === 'white') {
// 10. 如果这个点没有被访问过
color[w] = 'grey'; // 11. 标注为发现了这点,但是还没有探索
queue.enqueue(w); // 12. 将这个顶点加入到队列中
}
}
color[u] = 'black'; // 13. 标注出队点这个顶点已经访问并探索完毕,下次不应该访问
if (callback) {
callback(u); // 14. 接受一个回调
}
}
};
OK,上边的只是一个 BFS 算法的工作原理,但是可以用该算法做更多事情,而不只是输出被访问顶点的顺序。例如,考虑如何来解决下面这个问题。
给定一个图 G 和源顶点 v,找出对每个顶点 u,u 和 v 之间最短路径的距离(以边的数量计)。
对于给定顶点 v,广度优先算法会访问所有与其距离为 1 的顶点,接着是距离为 2 的顶点, 以此类推。所以,可以用广度优先算法来解这个问题。我们可以修改 bfs 方法以返回给我们一 些信息:
-
从 v 到 u 的距离 d[u]
-
前溯点 pred[u],用来推导出从 v 到其他每个顶点 u 的最短路径
改进版的 BFS 算法
this.updateBFS = function() {
var color = initializeColor(); // 1.初始化所有顶点
var queue = new Queue(); // 2.初始化一个队列, 它将会存储待访问和待探索的顶点
var d = []; // 3. 声明数组来表示距离
var pred = []; // 4. 声明pred数组来表示前溯点
queue.enqueue(v); // 5.将起始点 v 入队列
for (let i = 0; i < vertices.length; i++) {
d[vertices[i]] = 0; // 6.图中的每一个顶点,用0来初始化数组d
pred[vertices[i]] = null; // 7. 用null来初始化数组pred
}
while (!queue.isEmpty()) {
// 8. 队列非空
var u = queue.dequeue(); // 9. 从队列中移除一个顶点
neighbors = adjList.get(u); // 10. 从字典中取得它的邻接表
color[u] = 'grey'; // 11. 将该顶点置为灰色,表示被访问过,但未被探索过
for (let j = 0; j < neighbors.length; j++) {
// 12. 对于出队的这个顶点,遍历它的相邻点
var w = neighbors[j]; // 13. 取得这个点
if (color[w] === 'white') {
// 14. 如果这个点没有被访问过
color[w] = 'grey'; // 15. 标注为发现了这点,但是还没有探索
d[w] = d[u] + 1; // 16. 通过给d[u]加1来设置两点之间的距离
pred[w] = u; // 17. 发现顶点u的邻点w时,则设置w的前溯点值为u
queue.enqueue(w); // 18. 将这个顶点加入到队列中
}
}
color[u] = 'black'; // 19. 标注出队点这个顶点已经访问并探索完毕,下次不应该访问
// 20. 方法最后返回了一个包含d和pred的对象
return {
distances: d,
predecessors: pred
};
}
};
深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶 点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点,如下图所示:
深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点 v 未访问,则访问该顶点 v。
要访问顶点 v,照如下步骤做。
-
(1) 标注 v 为被发现的(灰色)。
-
(2) 对于 v 的所有未访问的邻点 w:
- (a) 访问顶点 w。
-
(3) 标注 v 为已被探索的(黑色)。
如你所见,深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用栈来存储函数调用(由递归调用所创建的栈)。
看看该如何实现这个 简易版的 DFS 算法 ?
this.dfs = function(callback) {
var color = initializeColor(); // 1.初始化所有顶点
for (let i = 0; i < vertices.length; i++) {
// 2. 对每一个未被访问过的顶点
if (color[vertices[i]] === 'white') {
dfsVisit(vertices[i], color, callback); // 3. 调用私有递归函数 dfsVisit
}
}
};
this.dfsVisit = function(u, color, callback) {
color[u] = 'grey'; // 4. 置为被发现,但是还未探索
if (callback) {
callback(u);
}
var neighbors = adjList.get(u); // 5. 取得顶点 u 所有邻点的列表
for (let j = 0; j < neighbors.length; j++) {
// 6. 遍历所有邻点
var w = neighbors[j]; // 7. 顶点u的每一个未被访问过的邻点w
if (color[w] === 'white') {
dfsVisit(w, color, callback); // 8. 调用dfsVisit函数,添加顶点w入栈
}
}
color[u] = 'blank';
};
如以下图所示 :
我强烈推荐,先去看这篇 搜索思想-DFS & BFS 文章,然后接着去 youbute 看这个视频 BFS、DFS 算法原理及 JS 实现!!!