图
人与人之间的关系非常复杂。比如你认识的朋友,可能他们之间也互相认识。这就不是简单的一对一、一对多,而是多对多的关系,这便是图。
图
(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成, 通常表示为:G(V,E)
,其中,G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。
关于图的定义,需要注意的是:
- 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,则称之为顶点(Vertex)。
- 在图结构中,不允许没有顶点。
- 在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
一些图定义
-
无向边:若顶点 vi 到 vj 之间的边没有方向,则称这条边为无向边 (Edge),用无序偶对 (vi,vj) 来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图 (Undirected graphs)。
由于是无方向的,连接顶点 A 与 D 的边,可以表示成无序对
(A,D)
,也可以写成(D,A)
。 -
有向边:若从顶点 vi 到 vj 的边有方向,则称这条边为有向边,也称为弧 (Arc)。用有序偶对 <vi,vj> 来表示,vi 称为弧尾 (Tail),vj 称为弧头 (Head)。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图。
连接顶点 A 到 D 的有向边就是弧,A 是弧尾,D 是弧头,
<A,D>
表示弧,而不能写成 <D,A>。 -
在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
-
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。
含有
n
个顶点的无向完全图有n(n-1)/2
条边。 -
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
含有
n
个顶点的有向完全图有n(n-1)
条边。 -
有很少条边或弧的图称为稀疏图,反之称为稠密图。
不过稀疏和稠密是模糊的概念,都是相对而言的。
-
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权 (Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。带权的图通常称为网 (Network)。
-
假设有两个图 G=(V,{E}) 和 G’=(V’,{E’}),如果 V’⊆V 且 E’⊆E,则称 G’ 为 G 的子图。
-
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复的叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。
应用
可以用图对现实中的很多 系统建模。
比如对交通流量建模,顶点可以表示街道的十字路 口,边可以表示街道。加权的边可以表示限速或者车道的数量。建模人员可以用这个系统来判断最佳路线及最有可能堵车的街道。
任何运输系统都可以用图来建模。比如,航空公司可以用图来为其飞行系统建模。将每个机场看成顶点,将经过两个顶点的每条航线看作一条边。加权的边可以表示从一个机场到另一个机场的航班成本,或两个机场间的距离,这取决于建模的对象是什么。
包含局域网和广域网(如互联网)在内的计算机网络,同样经常用图来建模。
消费市场也可以用图来建模,顶点可以用来表示供应商和消费者。
图的存储结构:
- 邻接矩阵
- 邻接表
- 十字链表
- 邻接多重表
- 边集数组
图的实现
🍊 表示顶点
创建图类的第一步就是要创建一个 Vertex
类来保存顶点。
Vertex
类有两个数据成员:一个用于标识顶点,另一个是表明这 个顶点是否被访问过的布尔值。它们分别被命名为 label
和 wasVisited
。这个类只需要一个函数,那就是为顶点的数据成员设定值的构造函数。
Vertex
类的代码如下所示:
function Vertex(label) {
this.label = label;
}
我们将所有顶点保存到数组中,在图类里,可以通过它们在数组中的位置引用它们。
🍊 表示边
我们将表示图的边的方法称为邻接表或者邻接表数组。这种方法将边存储为由顶点的相邻顶点列表构成的数组,并以此顶点作为索引。
使用这种方法,当在程序中引用一个顶点时,可以高效地访问与这个顶点相连的所有顶点的列表。
比如,如果顶点 2 与顶点 0、 1、3、4 相连,并且它存储在数组中索引为 2 的位置,那么,访问这个元素,我们可以访问到索引为 2 的位置处由顶点 0、1、3、4 组成的数组。
另一种表示图边的方法被称为邻接矩阵。它是一个二维数组,其中的元素表示两个顶点之间是否有一条边。
构建图
🐱 Graph 类
function Graph(v) {
this.vertices = v;
this.edges = 0;
this.adj = [];
for (var i = 0; i < this.vertices; ++i) { // 通过 `for` 循环为数组中的每个元素添加一个子数组来存储所有的相邻顶点,并将所有元素初始化为空字符串。
this.adj[i] = [];
this.adj[i].push("");
}
this.addEdge = addEdge;
this.showGraph = showGraph;
}
// 👆这个类会记录一个图表示了多少条边,并使用一个长度与图的顶点数相同的数组来记录顶点的数量。
function addEdge(v, w) {
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
}
// 👆当调用这个函数并传入顶点 A 和 B 时,函数会先查找顶点 A 的邻接表,将顶点 B 添加到列表中,然后再查找顶点 B 的邻接表,将顶点 A 加入列表。最后,这个函数会将边数加 1。
function showGraph() {
for (var i = 0; i < this.vertices; ++i) {
console.log(i + " -> ");
for (var j = 0; j < this.vertices; ++j) {
if (this.adj[i][j] != undefined) {
console.log(this.adj[i][j] + ' ');
}
}
}
}
// showGraph() 函数会通过打印所有顶点及其相邻顶点列表的方式来显示图
🐱 测试 Graph 类的实现
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.showGraph();
输出结果如下:
0 ->
1
2
1 ->
0
3
2 ->
0
4
3 ->
1
4 ->
2
搜索图
在图上可以执行两种基础搜索:深度优先搜索和广度优先搜索。
🍇 深度优先搜索
深度优先搜索
包括从一条路径的起始顶点开始追溯,直到到达最后一个顶点,然后回溯, 继续追溯下一条路径,直到到达最后的顶点,如此往复,直到没有路径为止。
深度优先搜索的搜索过程如下:
深度优先搜索算法:访问一个没有访问过的顶点,将它标记为已访问,再递归地
去访问在初始顶点的邻接表中其他没有访问过的顶点。
function Graph(v) {
this.vertices = v;
this.edges = 0;
this.adj = [];
for (var i = 0; i < this.vertices; ++i) {
this.adj[i] = [];
this.adj[i].push("");
}
this.addEdge = addEdge;
this.showGraph = showGraph;
this.dfs = dfs;
this.marked = []; // 为 Graph 类添加一个数组,用来存储已访问过的顶点
for (var i = 0; i < this.vertices; ++i) { // 将它所有元素的值全部初始化为 false
this.marked[i] = false;
}
}
function addEdge(v, w) {
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
}
function showGraph() {
for (var i = 0; i < this.vertices; ++i) {
console.log(i + " -> ");
for (var j = 0; j <
this.vertices; ++j) {
if (this.adj[i][j] != undefined)
console.log(this.adj[i][j] + ' ');
}
}
}
function dfs(v) {
this.marked[v] = true;
if (this.adj[v] != undefined) {
console.log("Visited vertex: " + v);
}
if (this.adj[v] !== undefined) {
this.adj[v].forEach(element => {
if (!this.marked[element]) {
this.dfs(element);
}
});
}
}
// 测试 dfs()
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.showGraph();
g.dfs(0);
输出结果为:
0 ->
1
2
1 ->
0
3
2 ->
0
4
3 ->
1
4 ->
2
Visited vertex: 0
Visited vertex: 1
Visited vertex: 3
Visited vertex: 2
Visited vertex: 4
🍇 广度优先搜索
广度优先搜索
从第一个顶点开始,尝试访问尽可能靠近它的顶点。本质上,这种搜索在图上是逐层移动的,首先检查最靠近第一个顶点的层,再逐渐向下移动到离起始顶点最远的层。
广度优先搜索的搜索过程:
广度优先搜索算法 使用了抽象的队列来对已访问过的顶点进行排序。其算法的工作原理如下:
(1) 查找与当前顶点相邻的未访问顶点,将其添加到已访问顶点列表及队列中;
(2) 从图中取出下一个顶点 v,添加到已访问的顶点列表;
(3) 将所有与 v 相邻的未访问顶点添加到队列。
function bfs(s) {
var queue = [];
this.marked[s] = true;
queue.push(s); // 添加到队尾
while (queue.length > 0) {
var v = queue.shift(); // 从队首移除
if (this.adj[v] !== undefined) {
console.log("Visisted vertex: " + v);
}
if (this.adj[v] !== undefined) {
this.adj[v].forEach(element => {
if (!this.marked[element]) {
this.marked[element] = true;
queue.push(element);
}
});
}
}
}
// 测试
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.showGraph();
g.bfs(0);
输出结果如下:
0 ->
1
2
1 ->
0
3
2 ->
0
4
3 ->
1
4 ->
2
Visisted vertex: 0
Visisted vertex: 1
Visisted vertex: 2
Visisted vertex: 3
Visisted vertex: 4
查找最短路径
广度优先搜索对应的最短路径。
在执行广度优先搜索时,会自动查找从一个顶点到另一个相连顶点的最短路径。
例如,要查找从顶点 A 到顶点 D 的最短路径,首先会查找从 A 到 D 是否有任何一条单边路径, 接着查找两条边的路径,以此类推。这正是广度优先搜索的搜索过程,因此我们可以修改广度优先搜索算法,找出最短路径。
// Graph 类中需要添加
this.edgeTo = [];
this.pathTo = pathTo;
this.hasPathTo = hashPathTo;
// bfs 函数
function bfs(s) {
var queue = [];
this.marked[s] = true;
queue.push(s); // 添加到队尾
while (queue.length > 0) {
var v = queue.shift(); // 从队首移除
if (v == undefined) {
console.log("Visisted vertex: " + v);
}
if (this.adj[v] !== undefined) {
this.adj[v].forEach(w => {
if (!this.marked[w]) {
this.edgeTo[w] = v;
this.marked[w] = true;
queue.push(w);
}
});
}
}
}
// 函数 pathTo() 创建了一个栈,用来存储与指定顶点有共同边的所有顶点
function pathTo(v) {
var source = 0;
if (!this.hasPathTo(v)) {
return undefined;
}
var path = [];
for (var i = v; i != source; i = this.edgeTo[i]) {
path.push(i);
}
path.push(source);
return path;
}
function hasPathTo(v) {
return this.marked[v];
}
// 测试
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.bfs(0);
var vertex = 4;
var paths = g.pathTo(vertex);
var str = "";
while (paths.length > 0) {
if (paths.length > 1) {
str += paths.pop() + '-';
} else {
str += paths.pop();
}
}
console.log(str);
输出结果为:
0-2-4
拓扑排序
拓扑排序
会对有向图的所有顶点进行排序,使有向边从前面的顶点指向后面的顶点。
拓扑排序算法 与深度优先搜索类似。不同的是,拓扑排序算法不会立即输出已访问的顶点,而是访问当前顶点邻接表中的所有相邻顶点,直到这个列表穷尽时,才将当前顶点压入栈中。
🔗: