文章目录
一、定义Graph抽象类
/**
* Graph抽象类
*/
abstract public class Graph {
// 枚举图的种类
public enum Kind {
DG, //有向图
DN, //有向网
UDG, //无向图
UDN //无向网
}
// 用65535表示无穷大
public static int INFINITY = 65535;
protected Kind kind; // 图的种类
protected int numVertexes; // 图的顶点数
protected int numEdges; // 图的边数
}
二、图的常见算法
2.1 邻接矩阵的构建
// 邻接矩阵
private int[][] matrix;
// 构造方法:实现有向图、有向网、无向图和无向网的创建
public MGraph(Graph.Kind kind) {
this.kind = kind;
Scanner in = new Scanner(System.in);
System.out.println("请输入图的顶点数和边数:");
numVertexes = in.nextInt();
numEdges = in.nextInt();
// 初始化邻接矩阵
matrix = new int[numVertexes + 1][numVertexes + 1];
for (int i = 0; i <= numVertexes; i++)
for (int j = 0; j <= numVertexes; j++) {
matrix[i][j] = INFINITY;
}
switch (kind) {
// 创建有向图的邻接矩阵
case DG:
System.out.println("请输入有向图的边<vi,vj>");
for (int i = 0; i < numEdges; i++) {
int vi = in.nextInt();
int vj = in.nextInt();
matrix[vi][vj] = 1;
}
return;
// 创建有向网的邻接矩阵
case DN:
System.out.println("请输入有向网的边<vi,vj>和权重w:");
for (int i = 0; i < numEdges; i++) {
int vi = in.nextInt();
int vj = in.nextInt();
matrix[vi][vj] = in.nextInt();
}
return;
// 创建无向图的邻接矩阵
case UDG:
System.out.println("请输入无向图的边(vi,vj):");
for (int i = 0; i < numEdges; i++) {
int vi = in.nextInt();
int vj = in.nextInt();
matrix[vi][vj] = matrix[vj][vi] = 1;
}
return;
// 创建无向网的邻接矩阵
case UDN:
System.out.println("请输入无向网的边(vi,vj)和权重w:");
for (int i = 0; i < numEdges; i++) {
int vi = in.nextInt();
int vj = in.nextInt();
matrix[vi][vj] = matrix[vj][vi] = in.nextInt();
}
}
}
2.2 图的深度优先遍历算法
// 深度优先遍历外部API
public void DFS() {
System.out.println("深度优先遍历的结果为:");
boolean[] visited = new boolean[numVertexes + 1]; // 设置访问数组
visited[0] = true;
for (int i = 1; i <= numVertexes; i++) {
if (!visited[i])
DFSTraverse(visited, i);
}
System.out.println();
}
// 递归实现部分
private void DFSTraverse(boolean[] visited, int v) {
if (visited[v]) return; // 基线条件:该结点已访问则直接return
visited[v] = true;
System.out.print(v + " ");
for (int i = 1; i <= numVertexes; i++) {
if (!visited[i] || matrix[v][i] != INFINITY) // 递归条件
DFSTraverse(visited, i);
}
}
2.3 图的广度优先遍历算法
// 广度优先遍历
public void BFS() {
System.out.println("广度优先遍历的结果为:");
boolean[] visited = new boolean[numVertexes + 1];
visited[0] = true;
// 使用队列暂存结点的访问顺序
Queue<Integer> queue = new LinkedList<>();
for (int i = 1; i <= numVertexes; i++)
if (!visited[i]) { // 找到尚未未访问的结点
visited[i] = true;
queue.add(i); // 将该结点入队列
while (!queue.isEmpty()) {
int next = queue.remove(); // 取出队首结点
System.out.print(next + " ");
// 将该结点的所有相邻且未访问的结点入队列,并更改结点的访问属性为true
for (int j = 1; j <= numVertexes; j++) {
if (!visited[j] && matrix[next][j] != INFINITY) {
visited[j] = true;
queue.add(j);
}
}
}
}
System.out.println();
}
三、图的应用
3.1 求最小生成树(Prim算法和Kruskal算法)
// Prim最小生成树算法
public void Prim() {
// 有向图最小生成树不一定存在,因此碰到有向图或有向网就不用求最小生成树
if (kind == Kind.DG || kind == Kind.DN) return;
int[] adjvex = new int[numVertexes + 1];//保存相关顶点
int[] weight = new int[numVertexes + 1];//保存相关顶点间边的权值
System.out.println("Prim算法计算出的最小生成树为:");
for (int i = 2; i <= numVertexes; i++) {
weight[i] = matrix[1][i];
}
adjvex[2] = 1;
for (int i = 2; i <= numVertexes; i++) {
int min = INFINITY;
int j, k = j = 1;
while (j <= numVertexes) {
if (weight[j] != 0 && weight[j] < min) {
min = weight[j];
k = j;
}
j++;
}
System.out.print("(" + adjvex[k] + ", " + k + "), ");//打印当前顶点边中权值最小的边
weight[k] = 0;// 将当前顶点权值设为零,表示此顶点已完成任务
for (j = 2; j <= numVertexes; j++) {
if (weight[j] != 0 && matrix[k][j] < weight[j]) { //若下标为k顶点各边权值小于此前这些顶点未被加入生成树的权值
weight[j] = matrix[k][j]; //将较小权值存入lowcost
adjvex[j] = k;//将下标为k的顶点存入adjvex
}
}
}
System.out.println("\b\b");
}
/**
* Kruskal算法使用边集数组的数据结构来描述图
*/
public class ERGraph extends Graph {
// Edge内部类用于构建边集数组
private static class Edge {
public int begin; // 边的起点
public int end; // 边的终点
public int weight; // 边的权值
}
// 定义边集数组
private Edge[] edges = null;
public ERGraph(Kind kind) {
Scanner in = new Scanner(System.in);
System.out.println("请输入图的顶点数和边数:");
numVertexes = in.nextInt();
numEdges = in.nextInt();
int vi, vj;
switch (kind) {
// 创建有向图的边集数组
case DG:
edges = new Edge[numEdges];
System.out.println("请输入有向图的边<vi,vj>");
for (int i = 0; i < numEdges; i++) {
vi = in.nextInt();
vj = in.nextInt();
edges[i] = new Edge();
edges[i].begin = vi;
edges[i].end = vj;
edges[i].weight = 1; //图的所有边权重都为1
}
return;
// 创建有向网的边集数组
case DN:
edges = new Edge[numEdges];
System.out.println("请输入有向网的边<vi,vj>和权重w:");
for (int i = 0; i < numEdges; i++) {
vi = in.nextInt();
vj = in.nextInt();
edges[i] = new Edge();
edges[i].begin = vi;
edges[i].end = vj;
edges[i].weight = in.nextInt();
}
return;
// 创建无向图的边集数组
case UDG:
edges = new Edge[numEdges * 2];
for (int i = 0; i < numEdges * 2; i++)
edges[i] = new Edge();
System.out.println("请输入无向图的边(vi,vj):");
for (int i = 0; i < numEdges; i++) {
vi = in.nextInt();
vj = in.nextInt();
edges[i].begin = edges[i + numEdges].end = vi;
edges[i].end = edges[i + numEdges].begin = vj;
edges[i].weight = edges[i+numEdges].weight = 1;
}
return;
// 创建无向网的边集数组
case UDN:
edges = new Edge[numEdges * 2];
for (int i = 0; i < numEdges * 2; i++)
edges[i] = new Edge();
System.out.println("请输入无向网的边(vi,vj)和权重w:");
for (int i = 0; i < numEdges; i++) {
vi = in.nextInt();
vj = in.nextInt();
edges[i].begin = edges[i + numEdges].end = vi;
edges[i].end = edges[i + numEdges].begin = vj;
edges[i].weight = edges[i+numEdges].weight = in.nextInt();
}
}
}
// Kruskal最小生成树算法
public void Kruskal() {
int[] parent = new int[numVertexes + 1];//定义数组用来判断与边是否形成环路
Arrays.sort(edges, (a, b) -> a.weight - b.weight);// 对边集数组按照权重排序
System.out.println("Kruskal算法计算出的最小生成树为:");
// 遍历边集数组
for (int i = 0; i < numEdges; i++) {
int n = find(parent, edges[i].begin);
int m = find(parent, edges[i].end);
if (n != m) { // 如果n与m不等,说明此边没有与现有生成树形成环路
parent[n] = m;
System.out.print("(" + edges[i].begin + ", " + edges[i].end + "), ");
}
}
System.out.println("\b\b");
}
// 用于查找连线顶点的尾部下标
private int find(int[] parent, int f) {
while (parent[f] > 0)
f = parent[f];
return f;
}
}
3.2 求最短路径(Dijkstra算法和Floyd算法)
// Dijkstra最短路径算法
public void Dijkstra(int v) {
// 创建三个辅助数组
int[] weight = new int[numVertexes + 1]; //用来存放v到其他顶点的权值
int[] previous = new int[numVertexes + 1]; //用来存放其他顶点相对v的最短前驱顶点
boolean[] visited = new boolean[numVertexes + 1]; //访问标记数组,避免顶点的重复访问
// 初始化工作
for (int i = 1; i <= numVertexes; i++) {
weight[i] = matrix[v][i]; // 初始化v与其他顶点之间的权值
if (weight[i] == INFINITY) previous[i] = INFINITY; // i与v不相邻,前驱设为无穷大
else previous[i] = v; // 说明i与v相邻,因此设i的前驱为v
}
weight[v] = 0; // v到自身的距离为0
visited[v] = true; // weight初始化的时候访问过v
// 算法开始
for (int i = 1; i <= numVertexes; ++i) {
int minWeight = INFINITY; // 暂存相邻顶点中权值最小的顶点的距离
int minVex = v; // 暂存相邻顶点中权值最小的顶点
// 找到距离v权值最小的顶点
for (int j = 1; j <= numVertexes; j++)
if (!visited[j] && weight[j] < minWeight) {
minVex = j;
minWeight = weight[j];
}
visited[minVex] = true; // 访问完毕,已读取到该顶点的权值信息
// 调整weight数组当前顶点的权值,并修改当前顶点的前驱顶点
for (int j = 1; j <= numVertexes; j++)
if (!visited[j] && matrix[minVex][j] < INFINITY)
if (weight[j] > weight[minVex] + matrix[minVex][j]) {
weight[j] = weight[minVex] + matrix[minVex][j];
previous[j] = minVex;
}
}
// 输出算法运行结果
System.out.println("Dijkstra算法计算最短路径的结果为:");
int temp;
Stack<Integer> stack = new Stack<>();
for (int i = 1; i <= numVertexes; i++) {
temp = previous[i];
while (temp != INFINITY) {
stack.push(temp);
temp = previous[temp];
}
System.out.print("顶点 " + v + " 到顶点 " + i + " 的最短距离为: " + weight[i] + " , 最短路径为: ");
while (!stack.empty()) System.out.print(stack.pop() + "->");
System.out.println(i);
}
}
// Floyd最短路径算法
public void Floyd() {
int[][] path = new int[numVertexes + 1][numVertexes + 1]; // 存储最短路径
int[][] weights = new int[numVertexes + 1][numVertexes + 1]; // 存储权重和
// 初始化操作
for (int i = 1; i <= numVertexes; i++)
for (int j = 1; j <= numVertexes; j++) {
weights[i][j] = matrix[i][j];
path[i][j] = j;
}
// 算法开始
for (int k = 1; k <= numVertexes; k++)
for (int i = 1; i <= numVertexes; i++)
for (int j = 1; j <= numVertexes; j++) {
// 如果经过下标为k顶点路径比原来两顶点间路径更短,则将当前两点间的权值设为更小的一个
if (weights[i][j] > weights[i][k] + weights[k][j]) {
weights[i][j] = weights[i][k] + weights[k][j];
path[i][j] = path[i][k];
}
}
// 输出算法执行结果
System.out.println("Floyd算法计算最短路径的结果为:");
for (int i = 1; i <= numVertexes; i++)
for (int j = 1; j <= numVertexes; j++) {
if (i == j) {
// 在邻接矩阵初始化时没有让对角线为0,所以当i==j时往往无法显示正确结果
System.out.println("顶点 " + i + " 到顶点 " + j + " 的最短距离为: " + 0 + " , 最短路径为: " + i + "->" + j);
continue;
}
System.out.print("顶点 " + i + " 到顶点 " + j + " 的最短距离为: " + weights[i][j] + " , 最短路径为: ");
int k = path[i][j];
System.out.print(i);
while (k != j) {
System.out.print("->" + k);
k = path[k][j];
}
System.out.println("->" + j);
}
System.out.println();
}
3.3 拓扑排序
由于拓扑排序和关键路径两个算法使用邻接表实现,因此先给出邻接表创建的代码:
/**
* 图的邻接表形式,主要用于拓扑排序和关键路径的计算
*/
public class ALGraph {
private int numVertex;
private int numEdge;
// 边表结构
private static class Edge {
int adjVertex;
int weight;
Edge next;
public Edge(int vertex, int weight, Edge next) {
this.adjVertex = vertex;
this.weight = weight;
this.next = next;
}
}
// 顶点表结构
private static class Vertex {
int in; // 入度
Edge firstEdge;
}
// 枚举AOV网和AOE网
public enum Type {
AOV,
AOE
}
private Vertex[] adjList = null; // 邻接表
public ALGraph(Type type) {
Scanner input = new Scanner(System.in);
numVertex = input.nextInt();
numEdge = input.nextInt();
// 初始化邻接表
adjList = new Vertex[numVertex];
for (int i = 0; i < numVertex; i++)
adjList[i] = new Vertex();
int u, v, w;
if (type == Type.AOV) { // AOV网(有向无环图)
for (int i = 0; i < numEdge; i++) {
u = input.nextInt() - 1; // 存的是角标,要减1,防止数组越界
v = input.nextInt() - 1;
w = 1;
if (adjList[u].firstEdge == null)
adjList[u].firstEdge = new Edge(v, w, null);
else {
Edge temp = adjList[u].firstEdge;
while (temp.next != null) temp = temp.next;
temp.next = new Edge(v, w, null);
}
// 入度自增
adjList[v].in++;
}
} else { // AOE网(有向无环网)
for (int i = 0; i < numEdge; i++) {
u = input.nextInt() - 1;
v = input.nextInt() - 1;
w = input.nextInt();
if (adjList[u].firstEdge == null)
adjList[u].firstEdge = new Edge(v, w, null);
else {
Edge temp = adjList[u].firstEdge;
while (temp.next != null) temp = temp.next;
temp.next = new Edge(v, w, null);
}
// 入度自增
adjList[v].in++;
}
}
}
}
AOV网的拓扑排序算法:
/**
* 拓扑排序算法
*
* 所谓拓扑排序,就是对一个有向图构造拓扑序列的过程。构造时会有两个结果:
* 1.若此网的全部顶点都被输出,则说明它是AOV网(不存在环)
* 2.若输出顶点数少于全部顶点数,则说明存在环,不是AOV网
*
* 拓扑排序主要是为了解决一个工程能否顺序执行的问题
*
* 对AOV网进行拓扑排序算法的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,
* 并删去以此顶点为尾的弧,重复此步骤,直到输出全部顶点或AOV网中不存在入度为0的顶点为止
*/
public void topologicalSort() {
int count = 0;
Stack<Integer> stack = new Stack<>();
// 将所有入度为0的顶点入栈
for (int i = 0; i < numVertex; i++)
if (adjList[i].in == 0) stack.push(i);
while (!stack.empty()) {
int temp = stack.pop(); // 出栈
System.out.print((temp + 1) + "->");
count++;
// 遍历此顶点的边表
for (Edge edge = adjList[temp].firstEdge; edge != null; edge = edge.next) {
int k = edge.adjVertex;
adjList[k].in--;
if (adjList[k].in == 0) stack.push(k);
}
}
System.out.println("\b\b");
if (count < numVertex)
System.out.println("该AOV网存在环,拓扑排序失败!");
else
System.out.println("拓扑排序完毕");
}
3.4 关键路径
/**
* 关键路径算法解决的是工程完成需要的最短时间问题
*
* 路径长度:路径上各个活动持续时间之和
* 关键路径:从源点到汇点具有最大长度的路径
* 关键活动:关键路径上的活动
*
* 参数:
* - etv:事件的最早发生时间
* - ltv:事件的最晚发生时间
* - ete:活动的最早开始时间
* - lte:活动的最晚开始时间,即不推迟工期的最晚开工时间
* 根据ete与lte是否相等判断弧是否是关键活动
*/
public void criticalPath() {
int ete, lte;
Stack<Integer> sorted = new Stack<>();
int[] etv = new int[numVertex];
int[] ltv = new int[numVertex];
// 求etv的拓扑序列
topologicalSort(sorted, etv);
// 初始化ltv
Arrays.fill(ltv, etv[numVertex - 1]);
// 计算ltv
while (!sorted.empty()) {
int temp = sorted.pop();
for (Edge edge = adjList[temp].firstEdge; edge != null; edge = edge.next) {
int k = edge.adjVertex;
if (ltv[k] - edge.weight < ltv[temp])
ltv[temp] = ltv[k] - edge.weight;
}
}
// 计算ete、lte,并求出关键路径
int sum = 0;
System.out.println("输出关键路径为:");
for (int i = 0; i < numVertex; i++)
for (Edge edge = adjList[i].firstEdge; edge != null; edge = edge.next) {
int k = edge.adjVertex;
ete = etv[i];
lte = ltv[k] - edge.weight;
if (ete == lte) {
sum += edge.weight;
System.out.println("顶点" + i + "到顶点" + k + ", 路径长度为:" + edge.weight);
}
}
System.out.println("关键路径总长为:" + sum);
}
// 用于计算AOE网中所有事件的最早发生时间
private void topologicalSort(Stack<Integer> sorted, int[] etv) {
int count = 0;
Stack<Integer> stack = new Stack<>();
// 将所有入度为0的顶点入栈
for (int i = 0; i < numVertex; i++)
if (adjList[i].in == 0) stack.push(i);
while (!stack.empty()) {
int temp = stack.pop();
sorted.push(temp);
count++;
// 遍历此顶点的边表
for (Edge edge = adjList[temp].firstEdge; edge != null; edge = edge.next) {
int k = edge.adjVertex;
adjList[k].in--;
if (adjList[k].in == 0)
stack.push(k);
if (etv[temp] + edge.weight > etv[k])
etv[k] = etv[temp] + edge.weight;
}
}
if (count < numVertex)
System.out.println("该AOE网存在环,拓扑排序失败!");
else
System.out.println("拓扑排序完毕");
}
四、代码测试数据(测试前需要指定图的类型)
# 无向网,测试最短路径、BFS、DFS
9 16
1 2 1
1 3 5
2 3 3
2 4 7
2 5 5
3 5 1
3 6 7
4 7 3
4 5 2
5 7 6
5 8 9
5 6 3
6 8 5
7 9 7
7 8 2
8 9 4
# 有向网,测试最短路径、BFS、DFS
5 11
1 5 12
5 1 8
1 2 16
2 1 29
5 2 32
2 4 13
4 2 27
1 3 15
3 1 21
3 4 7
4 3 19
# AOV网,测试拓扑排序
14 20
1 12
1 6
1 5
2 9
2 5
2 3
3 10
3 7
3 6
4 14
4 3
5 8
6 13
6 9
7 6
9 8
10 12
10 11
11 14
13 10
# AOE网,测试关键路径
10 13
1 3 4
1 2 3
2 5 6
2 4 5
3 6 7
3 4 8
4 5 3
5 8 4
5 7 9
6 8 6
7 10 2
8 9 5
9 10 3
参考资料:清华大学《数据结构(C语言版)》、《大话数据结构》
2021/02/03更新完毕,已完结