- 什么是图 ?
图是网络结构的抽象模型,图是 一组由边连接的节点(或顶点)。一个图 G=(V,E)。由 V:一组顶点;E:一组边,连接中的顶点。
- 图的相关术语 ?
- 相邻顶点:由一条边连接在一起的。
- 顶点的度:相邻顶点的数量。
- 路径:路径是顶点 v1,v2,...vk 的一个连续序列,其中 vi 和 vi+1 是相邻的。
- 简单路径:不包含重复的顶点。
- 有向 / 无向图:图可以是无向的(边没有方向)或是有向的(边有方向)。
- 连通图:如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。
- 加权图:图还可以是加权的(每条边赋予了权值)。
- 如果图中不存在环,则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是连通的。
- 图的表示 ?
- 图最常见的实现是 邻接矩阵。每个节点都和一个整数相关联,该整数将作为数组的索引。我们用一个二维数组来表示顶点之间的连接。如果索引为 i 的节点和索引为j的节点相邻,则 array[i][j] === 1,否则 array[i][j] === 0。
- 邻接表:我们也可以使用一种叫作邻接表的动态数据结构来表示图。邻接表由图中每个顶点的相邻顶点列表所组成。存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是散列表或是字典来表示相邻顶点列表。
- 我们还可以用 关联矩阵 来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所示,我们使用二维数组来表示两者之间的连通性,如果顶点 v 是边 e 的入射点,则 array[v][e] === 1;否则,array[v][e] === 0。
【注意】:要找出顶点 v 和 w 是否相邻,使用邻接矩阵会比较快。关联矩阵通常用于边的数量比顶点多的情况下,以节省空间和内存。
- 图类的封装,使用邻接表实现图 ?
邻接表:我们也可以使用一种叫作邻接表的动态数据结构来表示图。邻接表由图中每个顶点的相邻顶点列表所组成。存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是散列表或是 字典 来表示相邻顶点列表。下面是 字典类的封装。
//封装字典类
function Dictionary(){
this.items = {};
var that = this;
Dictionary.prototype.has = function(key){
return key in this.items;
}
Dictionary.prototype.set = function(key, value){
this.items[key] = value;
}
Dictionary.prototype.remove = function(key){
if(that.has(key)){
delete this.items[key];
return true;
}else{
return false;
}
}
Dictionary.prototype.get = function(key){
return that.has(key) ? this.items[key] : undefined;
}
Dictionary.prototype.values = function(){
var values = {};
for(var k in this.items){
if(that.has(k)){
values.push(this.items[k]);
}
}
return values;
}
}
邻接表,实现图。内部使用到字典。下面是 图类的具体封装。
// 创建图类
function Graph(){
// 数组,存储图中所有顶点的名字
this.vertices = [];
// 字典,存储邻接表;将顶点的名字作为键,邻接顶点列表作为值。
this.adjList = new Dictionary();
// 向图中添加一个新的顶点
Graph.prototype.addVertex = function(v){
this.vertices.push(v);
this.adjList.set(v, []);
}
// 用来添加顶点之间的边的方法
Graph.prototype.addEdge = function(v, w){
this.adjList.get(v).push(w);
this.adjList.get(w).push(v);
}
Graph.prototype.toString = function(){
var s = '';
for(var i=0; i<this.vertices.length; i++){
s += this.vertices[i] + '->';
var neighbors = this.adjList.get(this.vertices[i]);
for(var j=0; j<neighbors.length; j++){
s += neighbors[j] + ' ';
}
s += '\n';
}
return s;
}
}
图类的测试 :
var graph = new Graph();
var myVertices = ['A','B','C','D','E','F','G','H','I']; //{7}
for (var i=0; i<myVertices.length; i++){ //{8}
graph.addVertex(myVertices[i]);
}
graph.addEdge('A', 'B'); //{9}
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');
console.log(graph.toString());
- 图的遍历操作 ?
图的遍历 : 通常有长度优先遍历和深度优先遍历。图遍历可以用来 寻找特定的顶点 或 寻找两个顶点之间的路径,检查 图是否连通,检查 图是否含有环 等。
图遍历算法的思想 : 是必须追踪每个第一次访问的节点,并且追踪哪些节点还没有被完全探索。完全探索 一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现,并将其加入待访问顶点列表中。为了保证算法的效率,务必访问每个顶点至多两次。
当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。白色:表示该顶点还没有被访问。灰色:表示该顶点被访问过,但并未被探索过。黑色:表示该顶点被访问过且被完全探索过。这就是之前提到的务必 访问每个顶点最多两次 的原因。
- 广度优先搜索算法 (BFS)?
通过将顶点存入队列中,最先入队列的顶点先被探索。广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。
从顶点 v 开始的广度优先搜索算法所遵循的步骤。(1) 创建一个队列 Q。(2) 将 v 标注为被发现的(灰色),并将 v 入队列Q。(3) 如果 Q 非空,则运行以下步骤:(a) 将 u 从 Q 中出队列;(b) 将标注 u 为被发现的(灰色);(c) 将 u 所有未被访问过的邻点(白色)入队列;(d) 将 u 标注为已被探索的(黑色)。
【注意】:可以使用 BFS 寻找最短路径。
// 实现广度优先搜索算法
Dictionary.prototype.initalizeColor = function() {
var color = [];
for (var i = 0; i < this.vertices.length; i++) {
color[this.vertices[i]] = 'white';
}
return color;
}
Dictionary.prototype.bfs = function(v, callback) {
var color = that.initalizeColor();
var queue = new Queue();
queue.enqueue(v);
while (!queue.isEmpty()) {
var u = queue.dequeue();
var neighbors = adjList.get(u);
color[u] = 'grey';
for (var i = 0; i < neighbors.length; i++) {
var w = neighbors[i];
if (color[w] === 'white') {
color[w] = 'grey';
queue.enqueue(w);
}
}
color[u] = 'black';
if (callback) {
callback(u);
}
}
}
- 使用 BFS 寻找最短路径 ?
this.BFS = function(v) {
var color = initializeColor(),
queue = new Queue(),
d = [],
pred = [];
queue.enqueue(v);
for (var i = 0; i < vertices.length; i++) {
d[vertices[i]] = 0;
pred[vertices[i]] = null;
}
while (!queue.isEmpty()) {
var u = queue.dequeue(),
neighbors = adjList.get(u);
color[u] = 'grey';
for (i = 0; i < neighbors.length; i++) {
var w = neighbors[i];
if (color[w] === 'white') {
color[w] = 'grey';
d[w] = d[u] + 1;
pred[w] = u;
queue.enqueue(w);
}
}
color[u] = 'black';
}
return {
distances: d,
predecessors: pred
};
};
- 深度优先算法 (DFS)?
通过将顶点存放在栈中,顶点是沿着路径被探索的,存在新的相邻节点就去访问。深度优先搜索算法将会 从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点 v 未访问,则访问该顶点 v。
访问顶点 v:(1) 标注 v 为被发现的(灰色)(2) 对于 v 的所有未访问的邻点 w:(a) 访问顶点 w。(3)标注 v 为已被探索的(黑色)。深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用栈来存储函数调用(由递归调用所创建的栈)。
【注意】:拓扑排序——使用深度优先搜索。
// 实现广度优先算法
Dictionary.prototype.initalizeColor = function() {
var color = [];
for (var i = 0; i < this.vertices.length; i++) {
color[this.vertices[i]] = 'white';
}
return color;
}
Dictionary.prototype.dfs = function(callback) {
var color = initializeColor();
for (var i = 0; i < this.vertices.length; i++) {
if (color[vertices[i]] === 'white') {
dfsVisit(this.vertices[i], color, callback);
}
}
}
Dictionary.prototype.dfsVisit = function(u, color, callback) {
color[u] = 'grey';
if (callback) {
callback(u);
}
var neighbors = adjList.get(u);
for (var i = 0; i < neighbors.length; i++) {
var w = neighbors[i];
if (color[w] === 'white') {
dfsVisit(w, color, callback);
}
}
color[u] = 'black';
};