一: 说明
图作为一种重要的数据结构,在生活中是许多的应用的对应模型,图常常分为有向图和无向图两种,基本概念这里不介绍了,今天主要介绍图本身数据结构的实现以及常用算法的实现。
首先,我们先介绍一下图的应用场景,图在生活中对应的有如下几种应用:
1. 地图: 出游的人们常常想知道 “从 A 地到B 地的最短路线”, 经历过交通堵塞的人可能更想问 “哪条路径到达的时间最快”,这两个问题其实就是无向图的最短路径和加权最短路径问题
2. 网页信息:当我们浏览网页时,页面上通常都会包含其他网页的链接,这样整个互联网就是一张图,节点是网页,边就是超链接。
3. 任务调度:商品生产过程中包含许多工序及限制条件,这些条件决定了某些任务的先后次序,“怎么安排才能满足在限定条件下用最少时间完成这些工序呢?”
其他的还有商业交易、相亲配对、计算机网络,社交网络等经典应用场景等等。
二:实现
接下来我们介绍一下无向图的实现方式,这里我们参考(抄)了《算法》第四版 中的实现,将图本身的实现和其应用的算法实现分开,相关源码包可见https://download.csdn.net/download/u012871914/11562775。
首先 Graph.java 是图本身的实现api, 底层数据结构使用的是邻接表,具体代码如下:
package algorithms.graph;
import edu.princeton.cs.algs4.Bag; //可以认为是一个实现了 Iterable接口的 单链表
import edu.princeton.cs.algs4.In;
public class Graph {
private final int V; // 顶点数目
private int E; //边的数目
private Bag<Integer>[] adj; //邻接表
public Graph(int V){
this.V = V;
this.E = 0;
adj = (Bag<Integer>[]) new Bag[V]; //数组 + 链表组成的图结构
for(int v = 0; v < V; v++){
adj[v] = new Bag<Integer>();
}
}
public Graph(In in){
this(in.readInt()); //读取V并将图初始化
int E = in.readInt(); //读取E
for( int i =0; i < E; i++){
//添加一条边
int v = in.readInt(); //读取一个顶点
int w = in.readInt(); //读取另一个顶点
addEdge(v, w); // 添加一条连接他们的边
}
}
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];
}
//图的邻接表的字符串表示
public String toString(){
StringBuffer s = new StringBuffer();
s.append(V + "vertices, " + E + " edges\n");
for( int v = 0; v < V; v++){
s.append(v + ": ");
for( int w : this.adj(v) ){
s.append(w + " ");
}
s.append("\n");
}
return s.toString();
}
}
然后是图的查询api Search(找到和起点s连通的所有顶点),接口如下
package algorithms.graph;
public interface Search {
// 找到和起点s 连通的所有顶点
// v 和 s 是连通的吗
public boolean marked(int v);
// 与s 连通的顶点总数
public int count();
}
使用深度优先实现具体Search算法如下
package algorithms.graph;
public class DepthFristSearch implements Search{
private boolean[] marked;
private int count;
public DepthFristSearch(Graph G, int s){
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);
}
}
// v和 s 是连通的吗
public boolean marked(int w){
return marked[w];
}
//计算与s联通的顶点个数
public int count(){
return count;
}
}
图的寻找路径API 函数 如下Path 接口所示
package algorithms.graph;
public interface Paths {
//在图G中找出所有起点为 s 的路径
// 是否存在从s到v的路径
public boolean hasPathTo(int v);
// s 到 v 的路径,如果不存在则返回null
Iterable<Integer> pathTo(int v);
}
使用深度优先算法实现的接口具体代码如下
package algorithms.graph;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.Stack;
import edu.princeton.cs.algs4.StdOut;
public class DepthFirstPaths implements Paths{
private boolean[] marked; //这个顶点上调用过dfs()了吗
private int[] edgeTo; //从起点到一个顶点的已知路径上的最后一个顶点
private final int s; //起点
public DepthFirstPaths(Graph G, int s){
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
dfs(G, s);
}
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);
}
}
}
public boolean hasPathTo(int v){
return marked[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;
}
public static void main(String[] args) {
Graph G = new Graph(new In("tinyCG.txt"));
int s = Integer.parseInt("0");
Paths paths = new DepthFirstPaths(G, s);
for(int v = 0; v < G.V(); v++){
StdOut.print(s + "to" + v + ": ");
if(paths.hasPathTo(v)){
for(int x: paths.pathTo(v)){
if(x == s) StdOut.print(x);
else StdOut.print("-" + x);
}
}
StdOut.println();
}
}
}
以及使用广度优先算法获取的实现(广度和深度算法的不同仅仅在于从数据结构中获取下一个顶点的规则不一样,这造成了两者应用的不同,深度优先的直接应用就是找出一幅图的所有连通分量, 而广度常常解决的是另一些问题如: 单点最短路径------“从s到给定目的顶点v之间是否存在一条路径,如果有,找出其中最短的那条” 如果用比喻来说明的话, 深度优先就好比是一个人在走迷宫,而广度优先则好像是一组人在一起朝各方向走这座迷宫,每个人都有自己的绳子,当出现新的岔路时,可以假设一个探险者会分裂为更多的人来搜索它们,当两个探险者相遇时,会合二为一,并继续使用先到达者的绳子)
package algorithms.graph;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.Stack;
import edu.princeton.cs.algs4.StdOut;
public class BreadthFirstPaths implements Paths{
private boolean[] marked; //到达该顶点的最短路径已知吗
private int[] edgeTo; //到达该顶点的已知路径上最后一个顶点
private final 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];
}
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;
}
public static void main(String[] args) {
Graph G = new Graph(new In("tinyCG.txt"));
int s = Integer.parseInt("0");
BreadthFirstPaths breadthFirstPaths = new BreadthFirstPaths(G, s);
for(int v = 0; v < G.V(); v++){
StdOut.print(s + "to" + v + ": ");
if(breadthFirstPaths.hasPathTo(v)){
for(int x: breadthFirstPaths.pathTo(v)){
if(x == s) StdOut.print(x);
else StdOut.print("-" + x);
}
}
StdOut.println();
}
}
}
下一个算法是找到图的连通分量(深度优先)
package algorithms.graph;
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;
/**
* 连通分量api
*/
public class CC {
private boolean[] marked;
private int[] id; // v所在的连通分量的标识符 (0 ~ count-1)
private int count; // 连通分量数
/**
* 预处理构造函数
* @param G
*/
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;
for(int w: G.adj(v)){
if(!marked[w])
dfs(G, w);
}
}
//v 和 w 连通吗
public boolean connected(int v, int w){
return id[v] == id[w];
}
public int id(int v){
return id[v];
}
public int count(){
return count;
}
public static void main(String[] args) {
Graph G = new Graph(new In("tinyCG.txt"));
CC cc = new CC(G);
int M = cc.count;
StdOut.println(M + " components");
Bag<Integer>[] components = (Bag<Integer>[]) new Bag[M];
for(int i = 0; i < M; i++ ){
components[i] = new Bag<Integer>();
}
for (int v = 0; v < G.V(); v++ ){
components[cc.id[v]].add(v);
}
for(int i = 0; i < M; i++){
for(int v: components[i]){
StdOut.print(v + " ");
}
StdOut.println();
}
}
}
其中示例tinyCG.txt 格式如下
6
8
0 5
2 4
2 3
1 2
0 1
3 4
3 5
0 2