无向图概念
术语表:
- 多重图:将含有平行边的图称为多重图。
- 简单图:将没有平行边和自环的图称为简单图。
- 相邻:当两个顶点通过一条边相连时,称这两个顶点相邻,并称这条边依附于这两个顶点。
- 度数:一个顶点的度数即依附于它的边的总数。
- 简单路径:是一条没有重复顶点的路径。
- 简单环:是一条(除了起点和终点必须相同外)没有相同顶点的环。
- 路径或环的长度:其中所包含的边数。(有权无向图则为边的权重和)
- 连通图:从任一顶点能够达到另一个任意顶点。
选用数据结构:
- 邻接矩阵:占用空间太大。对于含有上百万个顶点的图,V^2的空间需求是不能满足的。
- 邻接表数组:可以实现。使用一个以顶点为索引的列表数组,其中每个元素都是和该顶点相邻的顶点列表。
典型Graph实现的性能复杂度
数据结构 | 所需空间 | 添加一条边 | 检查v、w是否相邻 | 遍历v所有相邻顶点 |
边的列表 | E | 1 | E | E |
邻接矩阵 | V^2 | 1 | 1 | V |
邻接表 | E+V | 1 | degree(V) | degree(V) |
邻接集 | E+V | logV | logV | logV+degree(V) |
使用邻接表实现Graph性能有如下特点:
- 使用的空间和V+E成正比
- 添加一条边所需要的时间为常数
- 遍历顶点v所需要的时间和v的度数成正比
邻接表实现无向图:
public class Graph {
private int V;//顶点数
private int E;//边数
private Bag<Integer>[] adj;//邻接表
public Graph(int V) {
this.V = V;
adj = (Bag<Integer>[]) new Bag[V];
for(int i=0;i<adj.length;i++) {
adj[i] = new Bag<Integer>();
}
}
public int V() {return V;}
public int E() {return E;}
public void addEdge(int v,int w) {
adj[v].add(w);
adj[w].add(v);
E++;
}
public Iterable<Integer> adj(int v){return adj[v];}
}
图处理算法的设计模式:
一般我们会将数据结构和基于数据结构的算法分离。为此,我们会为相关的任务创建相关的类,然后采用组合的方式,在算法类中组合使用数据结构类。
深度优先遍历算法
算法思想:
首先访问图的出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止。若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。
算法实现:
public class DepthFirstPaths {
private boolean[] marked; //标记已经访问过的结点
private int count;
public DepthFirstPaths(Graph G,int s) {//以s作为起始顶点深度优先遍历无向图G
marked = new boolean[G.V()];
dfs(G,s); //调用真正的深度优先遍历方法
}
//深度优先遍历
private void dfs(Graph G,int v) {
marked[v] = true;//标记当前顶点
count++;
for(int w: G.adj(v))
if(!marked[w]) dfs(G,w);//递归访问它所有没有标记过的相邻顶点
}
public boolean marked(int w) {return marked[w];}
public int count() {return count;}
}
深度优先遍历标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比。
深度优先遍历算法应用:
1 查找图中路径:
只需很简单的修改深度优先遍历算法即可实现查找路径。添加一个实例变量edgeTo[]数组用来返回从每个与s相通的顶点返回s顶点的路径。搜索结果是一棵以起点为根节点的树,edgeTo[]是一棵由父节点组成的树。
修改深度优先遍历:
private void dfs(Graph G,int v){
marked[v] = true;
for(int w : G.adj(v))
if(!marked[w]){
edgeTo[w] = v; //只是多了这一条语句,保存路径中当前节点的上一节点
dfs(G,w);
}
}
获取s到v的路径:
public Iterable<Integer> pathTo(int v){
if(!hasPathTo(v)) return null;
Stack<Integer> path = new Stack<Integer>();//用栈保存路径
for( int x = v; x!=s;x = edgeTo[x]) //从顶点向下搜索
path.push(x); //路径上的结点进栈
path.push(s);
return path;
}
使用深度优先遍历得到从给定起点到任意标记顶点的路径所需的时间与路径长度成正比。
2 找出无向图中所有的连通分量:
使用深度优先算法求解连通分量,递归第一次调用的参数是顶点0,它会标记所有与0连通的顶点。然后构造函数中的for循环会查找每个没有被标记的顶点并递归调用dfs()来标记和它相邻的所有顶点。
添加了一个id[]数组,同一个连通分量中的顶点的id[]值相同。
public CC(Graph G){
marked = new boolean[G.V()];
id = new int[G.V()];
for(int s = 0; s<G.V(); s++)
if(!marked[s]){
dfs(G,s);
count++; //连通分量个数加一
}
}
private void dfs(Graph G, int v){
marked[v] = true;
id[v] = count; //同一个连通分量下id[]值相同
for(int w:G.adj(v)
if(!marked[w])
dfs(G,w);
}
深度优先遍历的预处理使用的时间和空间与V+E成正比且可以在常数时间内处理图的连通性查询。
3 深度优先遍历和union-find算法比较:
理论上,深度优先算法比union-find快,因为它能够保证所需时间是常数而union-find算法不行。
实际上,union-find算法更快,因为它不需要完整的构造并表示一张图。更重要的是union-find算法是一种动态算法(我们在任何时候都能用接近常数的时间检查两个顶点是否连通,甚至在添加一条边的时候),但深度优先算法必须对图进行预处理。
广度优先遍历算法
广度优先搜索比深度优先搜索更容易解决最短路径问题。
代码实现:
public class BreadthFirstPaths {
private boolean[] marked;
private int[] edgeTo;
private int s;
public BreadthFirstPaths(Graph G,int s){
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s =s ;
bfs(G,s);
}
//广度优先遍历
private void bfs(Graph G,int s) {
Queue<Integer> queue = new Queue<Integer>(); //用队列保存遍历到的结点
marked[s] = true;//标记起点
queue.enqueue(s);//将起点加入队列
while(!queue.isEmpty()) {
int v = queue.dequeue();//从队列中删去下一个顶点
for(int w:G.adj(v)) //将与该点相连的结点加入队列中
if(!marked[w]) {
edgeTo[w] = v;//保存路径(这里直接就是最短路径)
marked[w] = true;//标记它
queue.enqueue(w);//添加到队列中
}
}
}
public boolean hasPathTo(int v) {return marked[v];}
}
- 对于从s可达的任意顶点v,广度优先搜索都能找到一条s到v的最短路径。
- 广度优先搜索最坏情况下所需时间和V+E成正比。