无向图
1.定义
在数学上,一个图(Graph)是表示物件与物件之间的关系的方法,是图论的基本研究对象。一个图看起来是由一些小圆点(称为顶点或结点)和连结这些圆点的直线或曲线(称为边)组成的。
如果给图的每条边规定一个方向,那么得到的图称为有向图,其边也称为有向边。在有向图中,与一个节点相关联的边有出边和入边之分,而与一个有向边关联的两个点也有始点和终点之分。相反,边没有方向的图称为无向图。
某个顶点的度数即为连接它的边的总数。路径或者环的长度为其中所包含的边数。
2.代码
我们可以抽象出以下的表示图的API:
Graph的API的实现可以由多种不同的数据结构来表示,最基本的是维护一系列边的集合,如下:
还可以使用邻接矩阵来表示:
也可以使用邻接列表来表示:
由于采用如上方式具有比较好的灵活性,采用邻接列表来表示的话,可以定义如下数据结构来表示一个Graph对象。
import java.util.ArrayList;
import java.util.List;
public class Graph {
private int verticals;//顶点个数
private int edges;//边的个数
private List<Integer>[] adjacency;//顶点联接列表
@SuppressWarnings("unchecked")
public Graph(int vertical) {
this.verticals = vertical;
this.edges = 0;
adjacency = (List<Integer>[])new List[vertical];
for (int v = 0; v < vertical; v++) {
adjacency[v]=new ArrayList<Integer>();
}
}
public int GetVerticals () {
return verticals;
}
public int GetEdges() {
return edges;
}
public void AddEdge(int verticalStart, int verticalEnd) {
adjacency[verticalStart].add(verticalEnd);
adjacency[verticalEnd].add(verticalStart);
edges++;
}
public List<Integer> GetAdjacency(int vetical) {
return adjacency[vetical];
}
public static void main(String[] args) {
Graph G = new Graph(13);
G.AddEdge(0, 5);
G.AddEdge(4, 3);
G.AddEdge(0, 1);
G.AddEdge(9, 12);
G.AddEdge(6, 4);
G.AddEdge(5, 4);
G.AddEdge(0, 2);
G.AddEdge(11, 12);
G.AddEdge(9, 10);
G.AddEdge(0, 6);
G.AddEdge(7, 8);
G.AddEdge(9, 11);
G.AddEdge(5, 3);
G.AddEdge(4, 9);
List<Integer> list = G.GetAdjacency(4);
for(int w : list) {
System.out.print(w + " ");
}
}
}
输出:
3 6 5 9
采用以上三种表示方式的效率如下:
深度优先搜索
1.原理
在谈论深度优先算法之前,我们可以先看看迷宫探索问题。下面是一个迷宫和图之间的对应关系: 迷宫中的每一个交会点代表图中的一个顶点,每一条通道对应一个边。
迷宫探索可以采用Trémaux绳索探索法。即:
- 选择一条没有标记过的通道,在你走过的路上铺一条绳子;
- 标记所有你第一次路过的路口和通道;
- 当来到一个标记过的路口时(用绳子)回退到上个路口;
- 当会退到的路口已经没有可走的通道时继续回退。
图示如下:
下面是迷宫探索的一个小动画:
接下来我们看图的搜索方法。
2.代码
定义一个edgesTo变量来后向记录所有到s的顶点的记录,和仅记录从当前节点到起始节点不同,我们记录图中的每一个节点到开始节点的路径。为了完成这一日任务,通过设置edgesTo[w]=v,我们记录从v到w的边,换句话说,v-w是最后一条从s到达w的边。 edgesTo[]其实是一个指向其父节点的树。
public class DepthFirstSearch {
private boolean[] marked;//记录是否被dfs访问过
private int[] edgesTo;//记录最后一个到当前节点的顶点
private int s;//搜索的起始点
public DepthFirstSearch(Graph g, int s) {
marked = new boolean[g.GetVerticals()];
edgesTo = new int[g.GetVerticals()];
this.s = s;
dfs(g, s);
}
private void dfs(Graph g, int v) {
marked[v] = true;
for(Integer w : g.GetAdjacency(v)) {
if (!marked[w]) {
edgesTo[w] = v;
dfs(g,w);
}
}
}
public boolean HasPathTo(int v) {
return marked[v];
}
public Stack<Integer> PathTo(int v) {
if (!HasPathTo(v)) {
return null;
}
Stack<Integer> path = new Stack<Integer>();
for (int x = v; x!=s; x=edgesTo[x]) {
path.push(x);
}
path.push(s);
return path;
}
public static void main(String[] args) {
Graph G = new Graph(13);
G.AddEdge(0, 5);
G.AddEdge(4, 3);
G.AddEdge(0, 1);
G.AddEdge(9, 12);
G.AddEdge(6, 4);
G.AddEdge(5, 4);
G.AddEdge(0, 2);
G.AddEdge(11, 12);
G.AddEdge(9, 10);
G.AddEdge(0, 6);
G.AddEdge(7, 8);
G.AddEdge(9, 11);
G.AddEdge(5, 3);
G.AddEdge(4, 9);
DepthFirstSearch dfs = new DepthFirstSearch(G, 0);
Stack<Integer> path = dfs.PathTo(11);
while(path.size() != 0) {
System.out.print(path.pop() + " ");
}
}
}
输出:
0 5 4 9 12 11
广度优先搜索
1.原理
我们很自然地还经常对下面这些问题敢兴趣。
单点最短路径。给定一幅图和一个起点s,回答“从s到给定目的顶点v是否存在一条路径?如果有,找出其中最短的那条等类似问题。”
解决这个问题的经典方法叫做广度优先搜索。
其主要原理是:
- 将 s放到FIFO中,并且将s标记为已访问
- 重复直到队列为空
- 移除最近最近添加的顶点v
- 将v未被访问的节点添加到队列中
- 标记他们为已经访问
广度优先是以距离递增的方式来搜索路径的。
public class BreadthFirstSearch {
private boolean[] marked;
private int[] edgeTo;
private int s;//搜索的起始点
public BreadthFirstSearch(Graph g, int s) {
marked=new boolean[g.GetVerticals()];
edgeTo=new int[g.GetVerticals()];
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.size()!=0) {
int v = queue.dequeue();
for(int w : g.GetAdjacency(v)) {
if (!marked[w]) {
edgeTo[w] = v;
marked[w] = true;
queue.enqueue(w);
}
}
}
}
public boolean HasPathTo(int v) {
return marked[v];
}
public Stack<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;
}
public static void main(String[] args) {
Graph G = new Graph(13);
G.AddEdge(0, 5);
G.AddEdge(4, 3);
G.AddEdge(0, 1);
G.AddEdge(9, 12);
G.AddEdge(6, 4);
G.AddEdge(5, 4);
G.AddEdge(0, 2);
G.AddEdge(11, 12);
G.AddEdge(9, 10);
G.AddEdge(0, 6);
G.AddEdge(7, 8);
G.AddEdge(9, 11);
G.AddEdge(5, 3);
G.AddEdge(4, 9);
BreadthFirstSearch bfs = new BreadthFirstSearch(G, 0);
Stack<Integer> path = bfs.PathTo(3);
while(path.size() != 0) {
System.out.print(path.pop() + " ");
}
}
}
输出:
0 5 4 9 11 (最短路径)
总结
本文简要介绍了无向图中的深度优先和广度优先算法,这两种算法时图处理算法中的最基础算法,也是后续更复杂算法的基础。它们搜索路径如下: