无向图
4.1.0 定义
图是由一组顶点和和一组能够将两个顶点相连的边组成的。
特殊的图,
- 自环,即一条连接一个顶点和其自身的边;
- 连接同一个对顶点的两条边称为平行边。
常常将含有平行边的图称为多重图,而将没有平行边或自环的图称为简单图。
4.1.1 术语表
- 子图
- 顶点的度数
- 路径,是由边顺序连接的一系列顶点。
- 简单路径,是一条没有重复顶点的路径。
- 两个顶点是连通的
- 连通图,如果从任意一个顶点都存在一条路径到达另一个任意顶点
- 无环图,是一种不包含环的图。
- 树,是一幅无环连通图。
- 图的密度
- 稀疏图和稠密图,我们遇到的应用使用的几乎都是稀疏图。
- 二分图
4.1.2 表示无向图的数据类型
4.1.2.1 图的几种表示方法
- 邻接矩阵,使用v*v的布尔矩阵,当顶点v和顶点w之间有相连的边时,定义v行w列的元素值为1,否则为0。缺点,浪费存储空间。
- 边的数组,存储所有的边。缺点,查询速度慢。
- 邻接表数组,使用一个以顶点为索引的列表数组,其中的每个元素都是和该顶点相邻的顶点列表。有效地解决上述两个缺点。
4.1.2.2 邻接表的数据结构
这种Graph有如下特点:
- 使用的空间和V+E成正比
- 添加一条边所需的时间为常数
- 遍历顶点v的所有相邻顶点所需的时间和v的度数成正比(处理每个相邻顶点所需的时间为常数)
import java.util.LinkedList;
import java.util.List;
/**
* Created by lah on 2017/4/8.
*/
/**
* 图的顶点编号是 0 到 vertexNum-1
*/
public class Graph {
private int vertexNum; // 顶点数目
private int edgeNum; // 边的数目
private List<Integer>[] adj; // 顶点的邻接表
public Graph(int vertexNum){
this.vertexNum = vertexNum;
this.edgeNum = 0;
this.adj = (List<Integer>[]) new LinkedList[vertexNum]; // 创建邻接表
for (int i = 0; i < vertexNum; i++){
this.adj[i] = new LinkedList<Integer>(); // 将所有邻接链表初始为空
}
}
public int getVertexNum(){
return this.vertexNum;
}
public int getEdgeNum(){
return this.edgeNum;
}
public void addEdge(int v, int w){
this.adj[v].add(w);
this.adj[w].add(v);
this.edgeNum += 1;
}
public Iterable<Integer> getAdj(int v){
return this.adj[v];
}
@Override
public String toString(){
StringBuilder string = new StringBuilder();
for (int v = 0; v < this.vertexNum; v++){
StringBuilder sBuilder = new StringBuilder();
sBuilder.append(v).append(" : ");
for (int adjVertex : this.getAdj(v))
sBuilder.append(adjVertex).append("-");
string.append(sBuilder).append("\r\n");
}
return string.toString();
}
public static void main(String[] args){
Graph graph = new Graph(4);
graph.addEdge(0,1);
graph.addEdge(0,2);
graph.addEdge(0,3);
graph.addEdge(1,3);
graph.addEdge(2,3);
System.out.println("vertexNum = " + graph.getVertexNum());
System.out.println("edgeNum = " + graph.getEdgeNum());
System.out.println("邻接表:");
System.out.println(graph.toString());
}
}
运行结果:
vertexNum = 4
edgeNum = 5
邻接表:
0 : 1-2-3-
1 : 0-3-
2 : 0-3-
3 : 0-1-2-
4.1.3 深度优先搜索(DFS, depth first search)
深度优先搜索,在访问其中一个顶点时:
- 将它标记为已访问;
- 递归地访问它的所有没有被标记过的邻居节点。
注意:
- 深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比。
- 在图中,路过每条边两次(在它的两个端点各一次),要么是第一次访问一条边,要么是沿着它从一个被标记过的顶点退回。(每条边都会被访问两次,且在第二次时总会发现这个顶点已经被标记过)
- 标记将能保证不会两次经过同一条通道(方向性)或者同一个路口。
- 递归,不停地压栈出栈,直到栈为空(栈内保存着每次访问的数据的非全局的状态)
public class DepthFirstSearch {
private boolean[] marked;
private int count;
public DepthFirstSearch(Graph graph, int startVertex){
marked = new boolean[graph.getVertexNum()];
dfs(graph, startVertex);
}
private void dfs(Graph graph, int vertex){
marked[vertex] = true; // 将该点标记为已被访问过
System.out.print(vertex + "-");
count++; // 统计已经访问过的节点个数
for (int adjVertex : graph.getAdj(vertex)) // 遍历当前节点的邻居节点
if (!marked[adjVertex]) { // 如果该节点没有被访问过,则递归访问它
dfs(graph, adjVertex);
}
}
public boolean marked(int vertex){ return marked[vertex]; }
public int getCount(){ return count; }
public static void main(String[] args){
Graph graph = new Graph(5);
graph.addEdge(0,1);
graph.addEdge(0,2);
graph.addEdge(0,3);
graph.addEdge(1,3);
// graph.addEdge(2,3);
graph.addEdge(3,4);
System.out.println("the procedure of accessing vertex: ");
DepthFirstSearch dfs = new DepthFirstSearch(graph, 0);
System.out.println();
for (int i = 0; i < graph.getVertexNum(); i++){
System.out.println("vertex " + i + " isMarked = " + dfs.marked(i));
}
System.out.println("sum of visited vertex = " + dfs.getCount());
}
}
the procedure of accessing vertex:
0-1-3-4-2-
vertex 0 isMarked = true
vertex 1 isMarked = true
vertex 2 isMarked = true
vertex 3 isMarked = true
vertex 4 isMarked = true
sum of visited vertex = 5
4.1.4 寻找路径
概念
- 连通性,“两个给定的顶点是否连通?”,等价于“两个给定的顶点之间是否存在一条路径?”,是路径检测问题。
- 单点路径,在图中找出给定起始点和终止点之间的路径。
4.1.5 广度优先搜索
问题:寻找单点最短路径。此时,DFS无能为力了,而BFS正是为了这个目标才出现的。
广度优先搜索,它使用了一个 队列 来保存所有已经被标记过但其邻接表还未被检查过的顶点。先将起点加入队列,然后重复一下步骤指导队列为空:
- 取队列中的下一个顶点 v 并标记它;
- 将与 v 相邻的所有未被标记过的顶点加入队列。
程序
import java.util.LinkedList;
import java.util.Queue;
/**
* Created by lah on 2017/4/9.
*/
public class BreadthFirstPaths {
private boolean[] marked; // 到达该顶点的最短路径已知吗
private int[] edgeTo; // 到达该顶点的已知路径上的最后一个顶点(父节点)
private final int source; // 起点
public BreadthFirstPaths(Graph graph, int source){
marked = new boolean[graph.getVertexNum()];
edgeTo = new int[graph.getVertexNum()];
this.source = source;
bfs(graph, source);
}
/**
* 广度优先搜索,记录各个点的父节点
* @param graph 图
* @param source 起点
*/
public void bfs(Graph graph, int source){
Queue<Integer> queue = new LinkedList<Integer>();
marked[source] = true;
queue.offer(source); // 将起点添加到队列末尾,初始化队列
while(!queue.isEmpty()){
int vertex = queue.poll(); // 返回并删除
for (int adjVertex : graph.getAdj(vertex)){ // 当前节点的所有邻居节点
if (!marked[adjVertex]){ // 对于没有标记的相邻节点
marked[adjVertex] = true; // 已经得到该点的最短路径
edgeTo[adjVertex] = vertex; // 保存最短路径的最后一条边(当前节点的父节点)
queue.offer(adjVertex); // 将当前点添加到队列末尾
}
}
}
}
public boolean hasPathTo(int vertex){ return marked[vertex]; }
/**
* 返回 source 到 vertex 的最短路径
* @param vertex 顶点
* @return source 到 vertex的最短路径
*/
public Iterable<Integer> pathTo(int vertex){
if (!hasPathTo(vertex)) return null;
LinkedList<Integer> stackPath = new LinkedList<Integer>();
for (int parent = vertex; parent != source; parent = edgeTo[parent])
stackPath.push(parent); // 入栈,出栈是 pop()
stackPath.push(source);
return stackPath;
}
public static void main(String[] args){
Graph graph = new Graph(6);
graph.addEdge(0,1);
graph.addEdge(0,2);
graph.addEdge(1,4);
graph.addEdge(2,3);
graph.addEdge(3,4);
graph.addEdge(4,5);
BreadthFirstPaths bfp = new BreadthFirstPaths(graph, 0);
Iterable<Integer> path = bfp.pathTo(5);
System.out.println("hasPathTo(5) = " + bfp.hasPathTo(5));
System.out.print("shortest path of 0 to 5 is: ");
for(int ele : path) System.out.print(ele + "-");
}
}
hasPathTo(5) = true
shortest path of 0 to 5 is: 0-1-4-5-