图
图的表示方式:邻接矩阵、邻接链表
1. 邻接矩阵表示图
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
// 图的邻接矩阵存储结构
public class Graph {
int maxVertexNum; // 最大顶点数
List<String> vertex; // 顶点表
int[][] edge; // 邻接矩阵,边表
int edgeNum; // 图中当前的边数
boolean[] visited; // 顶点是否被访问
public Graph(int maxVertexNum) {
this.maxVertexNum = maxVertexNum;
vertex = new ArrayList<>(maxVertexNum);
edge = new int[maxVertexNum][maxVertexNum];
visited = new boolean[maxVertexNum];
}
// 显示邻接矩阵
public void display() {
for (int i = 0; i < edge.length; i++) {
System.out.println(Arrays.toString(edge[i]));
}
}
// 在图中插入一个顶点
public void insertVertex(String vertex) {
this.vertex.add(vertex);
}
// 向图中添加一条无向边<vs, ve>,其权值为weight
public void addEdge(int vs, int ve, int weight) {
edge[vs][ve] = weight;
edge[ve][vs] = weight;
edgeNum++;
}
// 获取边<vs, ve>的权值
public int getEdgeValue(int vs, int ve) {
return edge[vs][ve];
}
// 获取顶点vertex的第一个邻接点*
public int firstNeighbor(int vertex) {
for (int c = 0; c < this.vertex.size(); c++) {
if (edge[vertex][c] > 0)
return c;
}
return -1;
}
// 获取vertex的除了exclude顶点的下一个邻接点*
public int nextNeighbor(int vertex, int exclude) {
for (int c = exclude + 1; c < this.vertex.size(); c++) {
if (edge[vertex][c] > 0)
return c;
}
return -1;
}
// 对图进行深度优先遍历
public void dfsTraverse() {
int vertexNum = vertex.size();
// 初始化visited
for (int v = 0; v < vertexNum; v++)
visited[v] = false;
// 从0开始逐个访问
for (int v = 0; v < vertexNum; v++)
if (!visited[v])
dfs(v);
}
// Depth First Search,深度优先搜索,递归
private void dfs(int v) {
System.out.print(vertex.get(v) + " "); // 访问顶点v
visited[v] = true; // 标记为已被访问
// adjacent为v的尚未被访问的邻接顶点
for (int adjacent = firstNeighbor(v); adjacent >= 0; adjacent = nextNeighbor(v, adjacent)) {
// v的所有邻接结点是从左到右按顺序访问
if (!visited[adjacent])
dfs(adjacent);
}
}
// 借助栈,非递归,从顶点v开始深度优先*
public void dfsTraverse(int v) {
LinkedList<Integer> stack = new LinkedList<>();
// 初始化visited
for (int i = 0; i < vertex.size(); i++)
visited[i] = false;
// 将v入栈,入栈一个就将其置为已被访问
stack.push(v);
visited[v] = true;
while (!stack.isEmpty()) {
// 出栈一个顶点并访问
v = stack.pop();
System.out.print(vertex.get(v) + " ");
// 将v的所有邻接结点adjacent入栈
for (int adjacent = firstNeighbor(v); adjacent >= 0; adjacent = nextNeighbor(v, adjacent))
// 由于是栈,因此v的所有邻接结点是从右到左逆序访问
if (!visited[adjacent]) { // 未进过栈的顶点入栈
stack.push(adjacent);
visited[adjacent] = true;
}
}
}
// 对图进行广度优先遍历
public void bfsTraverse() {
// 初始化visited
for (int i = 0; i < vertex.size(); i++)
visited[i] = false;
// 从0开始逐个访问
for (int i = 0; i < vertex.size(); i++)
if (!visited[i])
bfs(i);
}
// Breadth First Search,广度优先搜索,借助队列,非递归*
private void bfs(int v) {
LinkedList<Integer> queue = new LinkedList<>();
// 访问v,并将其入队
System.out.print(vertex.get(v) + " ");
visited[v] = true;
queue.addLast(v);
while (!queue.isEmpty()) {
// 顶点v出队
v = queue.removeFirst();
// 访问v的所有邻接点
for (int adjacent = firstNeighbor(v); adjacent >= 0; adjacent = nextNeighbor(v, adjacent))
if (!visited[adjacent]) {
// 访问adjacent,并将其入队
System.out.print(vertex.get(adjacent) + " ");
visited[adjacent] = true;
queue.addLast(adjacent);
}
}
}
}
// 测试
class GraphClient {
public static void main(String[] args) {
String[] vertex = {"1", "2", "3", "4", "5", "6", "7", "8"};
Graph graph = new Graph(vertex.length);
for (String s : vertex)
graph.insertVertex(s);
graph.addEdge(0, 1, 1); // 1-2
graph.addEdge(0, 2, 1); // 1-3
graph.addEdge(1, 3, 1); // 2-4
graph.addEdge(1, 4, 1); // 2-5
graph.addEdge(2, 5, 1); // 3-6
graph.addEdge(2, 6, 1); // 3-7
graph.addEdge(3, 7, 1); // 4-8
graph.addEdge(4, 7, 1); // 5-8
graph.addEdge(5, 6, 1); // 6-7
graph.display();
System.out.println("递归深度优先:");
// 1 2 4 8 5 3 6 7
graph.dfsTraverse();
System.out.println("\n非递归深度优先:");
// 1 3 7 6 2 5 8 4
graph.dfsTraverse(0);
System.out.println("\n非递归广度优先:");
// 1 2 3 4 5 6 7 8
graph.bfsTraverse();
}
}
2. 最小生成树
最小生成树可以用Prim(普里姆)算法或Kruskal(克鲁斯卡尔)算法求出。
Prim算法简述:
- 输入:一个加权连通图,其中顶点集合为V,边集合为E
- 初始化: V n e w = { x } V_{new}=\{x\} Vnew={x},其中x为集合V中的任一节点, E n e w = { } E_{new}=\{\} Enew={},为空
- 重复下列操作,直到
V
n
e
w
=
V
V_{new}=V
Vnew=V:
- 在集合E中选取权值最小的边 < u , v > <u,v> <u,v>,其中u为集合 V n e w V_{new} Vnew中的元素,而v不在 V n e w V_{new} Vnew集合当中,并且 v ∈ V v \in V v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一)
- 将v加入集合 V n e w V_{new} Vnew中,将 < u , v > <u,v> <u,v>边加入集合 E n e w E_{new} Enew中
- 输出:使用集合 V n e w V_{new} Vnew和 E n e w E_{new} Enew来描述所得到的最小生成树
Kruskal算法简述:
- 构造一个只含n个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有n棵树的一个森林。
- 从边集E中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。直至森林中只有一棵树,也即子图中含有n-1条边为止。
import java.util.ArrayList;
import java.util.List;
class Edge {
String start;
String end;
int weight;
public Edge(String start, String end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
}
public class MinimumSpanningTree {
// prim求解最小生成树,v表示从第v个顶点开始生成
public static void prim(Graph graph, int v) {
// visited标记结点是否被访问
boolean[] visited = new boolean[graph.maxVertexNum];
// 初始化visited
for (int i = 0; i < visited.length; i++)
visited[i] = false;
// 顶点v已被访问
visited[v] = true;
// vs和ve记录两个顶点的下标
int vs = -1;
int ve = -1;
int minWeight = Integer.MAX_VALUE;
for (int k = 1; k < graph.maxVertexNum; k++) {
// 确定每一次生成的子图和哪个结点的距离最近
for (int i = 0; i < graph.maxVertexNum; i++) { // 遍历已经访问过的结点
for (int j = 0; j < graph.maxVertexNum; j++) { // 遍历所有没有访问过的结点
// 如果是一个访问过的结点和一个没有访问过的结点,并且当前边的权值小于最小权值
if (visited[i] == true && visited[j] == false && graph.edge[i][j] < minWeight) {
minWeight = graph.edge[i][j];
vs = i;
ve = j;
}
}
}
System.out.println(graph.vertex.get(vs) + "->" + graph.vertex.get(ve) + ": " + minWeight);
visited[ve] = true;
minWeight = Integer.MAX_VALUE;
}
}
// kruskal求解最小生成树
public static void kruskal(Graph graph, List<Edge> edges) {
// 保存已有最小生成树中,每个顶点所在树的根的索引
int[] ends = new int[edges.size()];
// 对边进行排序,按照权值从小到大
edges.sort(((o1, o2) -> o1.weight - o2.weight));
// 遍历边集合,将边添加到最小生成树中
// 判断准备加入的边是否形成回路,即是同一棵树的两个顶点
for (int i = 0; i < edges.size(); i++) {
// 获取第i条边的起点索引
int start = graph.vertex.indexOf(edges.get(i).start);
// 获取第i条边的终点索引
int end = graph.vertex.indexOf(edges.get(i).end);
// 获取起点所在树的根索引
int startRoot = getRoot(ends, start);
// 获取终点所在树的根索引
int endRoot = getRoot(ends, end);
if (startRoot != endRoot) { // 不在同一棵树
// 设置startRoot这棵树在已有最小生成树中的终点
ends[startRoot] = endRoot;
// 输出这一条边
System.out.println(edges.get(i).start + "--" + edges.get(i).end + ": " + edges.get(i).weight);
}
}
}
// (kruskal的难点)返回顶点i所在树的根的索引,ends数组记录各个顶点所在树的根的索引
private static int getRoot(int[] ends, int i) {
while (ends[i] != 0)
i = ends[i];
return i;
}
// 测试
public static void main(String[] args) {
String[] vertex = {"V1", "V2", "V3", "V4", "V5", "V6"};
// 将顶点加入到集合
List<String> list = new ArrayList<>();
for (String v : vertex)
list.add(v);
// max表示不连通
int max = Integer.MAX_VALUE;
// 初始化邻接矩阵
int[][] edge = new int[][]{
{max, 6, 1, 5, max, max},
{6, max, 5, max, 3, max},
{1, 5, max, 5, 6, 4},
{5, max, 5, max, max, 2},
{max, 3, 6, max, max, 6},
{max, max, 4, 2, 6, max}
};
// 将边加入到集合
List<Edge> edges = new ArrayList<>();
for (int r = 0; r < edge.length; r++)
for (int c = r + 1; c < edge[r].length; c++)
if (edge[r][c] != max)
edges.add(new Edge(vertex[r], vertex[c], edge[r][c]));
// 创建一个图,图是自己定义的
Graph graph = new Graph(vertex.length);
graph.vertex = list;
graph.edge = edge;
graph.edgeNum = edges.size();
prim(graph, 0);
System.out.println("---------");
kruskal(graph, edges);
}
}
3. 最短路径
最短路径可以用Dijkstra(迪杰斯特拉)算法或Floyd(弗洛伊德)算法求出。
- Dijkstra算法可以求出指定顶点到其他各个顶点的最短路径
- Floyd算法可以求出每一个顶点到其他各个顶点的最短路径
Dijkstra算法过程:Dijkstra算法不适用于含有负权边的图
- 初始化:集合S初始为{0},dist[]的初始值 d i s t [ i ] = a r c s [ 0 ] [ i ] , i = 1 , 2 , . . . , n − 1 dist[i]=arcs[0][i],i=1,2,...,n-1 dist[i]=arcs[0][i],i=1,2,...,n−1。
- 从顶点集合 V − S V-S V−S中选出 v j v_j vj,满足 d i s t [ j ] = M i n { d i s t [ i ] ∣ v i ∈ V − S } dist[j]=Min\{dist[i]|v_i\in V-S\} dist[j]=Min{dist[i]∣vi∈V−S}, v j v_j vj就是当前求得的一条从 v 0 v_0 v0出发的最短路径的终点,令 S = S ∪ { j } S=S \cup \{j\} S=S∪{j}。
- 修改从 v 0 v_0 v0出发到集合 V − S V-S V−S上任一顶点 v k v_k vk可达的最短路径长度:若 d i s t [ j ] + a r c s [ j ] [ k ] < d i s t [ k ] dist[j]+arcs[j][k]<dist[k] dist[j]+arcs[j][k]<dist[k],则令 d i s t [ k ] = d i s t [ j ] + a r c s [ j ] [ k ] dist[k]=dist[j]+arcs[j][k] dist[k]=dist[j]+arcs[j][k]。
- 重复2~3操作共n-1次,直到所有的顶点都包含在S中。
- dist[]:记录从源点 v 0 v_0 v0到其他各顶点当前的最短路径长度, d i s t [ i ] dist[i] dist[i]的初值为 a r c s [ v 0 ] [ i ] arcs[v_0][i] arcs[v0][i]。
- path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点,在算法结束时,可根据其值追溯得到源点 v 0 v_0 v0到顶点 v i v_i vi的最短路径。
Floyd算法过程:Floyd算法又称为插点法,利用了动态规划的思想
- 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
- 对于每一对顶点u和v,看看是否存在一个顶点w使得从u到w再到v比已知的路径更短,如果存在则更新它。
- 把图用邻接矩阵G表示出来,如果从 v i v_i vi到 v j v_j vj有路可达,则 G [ i ] [ j ] = d G[i][j]=d G[i][j]=d,d表示该路的长度;否则 G [ i ] [ j ] = + ∞ G[i][j]=+\infty G[i][j]=+∞。定义一个矩阵D用来记录所插入点的信息, D [ i ] [ j ] D[i][j] D[i][j]表示从 v i v_i vi到 v j v_j vj需要经过的点,初始化 D [ i ] [ j ] = j D[i][j]=j D[i][j]=j。把各个顶点插入图中,比较插点后的距离与原来的距离, G [ i ] [ j ] = m i n ( G [ i ] [ j ] , G [ i ] [ k ] + G [ k ] [ j ] ) G[i][j]=min(G[i][j],G[i][k]+G[k][j]) G[i][j]=min(G[i][j],G[i][k]+G[k][j]),如果 G [ i ] [ j ] G[i][j] G[i][j]的值变小,则 D [ i ] [ j ] = k D[i][j]=k D[i][j]=k。在G中包含有两点之间最短道路的信息,而在D中则包含了最短通路径的信息。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Dijkstra算法的辅助数组类
class AuxiliaryArray {
// 记录各个顶点是否访问过:1访问过,0未访问
int[] visited;
// 记录从源点到其他各顶点当前的最短路径长度
int[] dist;
// path[i]表示从源点到顶点i之间的最短路径的前驱结点
int[] path;
// 顶点数
int vertexNum;
// 源点的索引
int source;
public AuxiliaryArray(int vertexNum, int source) {
this.vertexNum = vertexNum;
this.source = source;
visited = new int[vertexNum];
dist = new int[vertexNum];
path = new int[vertexNum];
Arrays.fill(dist, Integer.MAX_VALUE / 2); // 初始化dist数组
visited[source] = 1; // 标记源点已被访问
dist[source] = 0; // 设置源点到源点的距离为0
}
// 判断顶点v是否被访问过
public boolean isVisited(int v) {
return visited[v] == 1;
}
// 返回从源点到顶点v当前的最短路径长度
public int getDistance(int v) {
return dist[v];
}
// 更新源点到顶点v的距离为distance
public void updateDist(int v, int distance) {
dist[v] = distance;
}
// 更新顶点v的前驱结点为顶点prev
public void updatePath(int v, int prev) {
path[v] = prev;
}
// 选出vj,dist[j]为剩余结点i中dist[i]最小的
public int next() {
int minValue = Integer.MAX_VALUE / 2;
int minIndex = 0;
for (int i = 0; i < visited.length; i++) {
if (visited[i] == 0 && dist[i] < minValue) {
minValue = dist[i];
minIndex = i;
}
}
// 更新minIndex顶点被访问过
visited[minIndex] = 1;
return minIndex;
}
// 便于展示
public void display(List<String> vertex) {
System.out.println("visited: ");
for (int i = 0; i < visited.length; i++)
System.out.print(visited[i] + " ");
System.out.println("\npath: ");
for (int i = 0; i < path.length; i++)
System.out.print(path[i] + " ");
System.out.println("\ndist: ");
for (int i = 0; i < dist.length; i++)
System.out.print(dist[i] + " ");
System.out.println("\n源点" + vertex.get(source) + "到各顶点的最短路径:");
for (int i = 0; i < dist.length; i++) {
if (dist[i] != Integer.MAX_VALUE / 2)
System.out.print(vertex.get(source) + "-" + dist[i] + "->" + vertex.get(i) + "\t");
else
System.out.print("MAX ");
}
}
}
public class ShortestPath {
// dijkstra更新辅助数组Dist和Path
private static void updateDistAndPath(Graph graph, AuxiliaryArray auxiliary, int j) {
// 遍历邻接矩阵的第j行
for (int k = 0; k < graph.edge[j].length; k++) {
// distance表示源点到顶点v的距离+源点到顶点j的距离
int distance = auxiliary.getDistance(j) + graph.edge[j][k];
// 如果顶点j没有被访问过,并且distance小于源点到顶点j的距离,就更新
if (!auxiliary.isVisited(k) && distance < auxiliary.getDistance(k)) {
// 更新顶点j的前驱结点为顶点v
auxiliary.updatePath(k, j);
// 更新源点到顶点j的距离为distance
auxiliary.updateDist(k, distance);
}
}
}
// Dijkstra算法求最短路径
public static void dijkstra(Graph graph, AuxiliaryArray auxiliary, int source) {
updateDistAndPath(graph, auxiliary, source);
for (int j = 1; j < graph.vertex.size(); j++) {
// 获取下一个访问顶点next
int next = auxiliary.next();
// 设置到next顶点的最短路径和前驱
updateDistAndPath(graph, auxiliary, next);
}
}
// Floyd算法求最短路径
public static void floyd(Graph graph, int[][] dist, int[][] path) {
// 初始化dist为邻接矩阵
for (int r = 0; r < graph.maxVertexNum; r++)
for (int c = 0; c < graph.maxVertexNum; c++)
dist[r][c] = graph.edge[r][c];
// 初始化path
for (int i = 0; i < graph.maxVertexNum; i++)
// 初始时,各个顶点到源点i的前驱默认为i
Arrays.fill(path[i], i);
// Floyd求最短路径过程
for (int k = 0; k < dist.length; k++) { // 中间顶点k
for (int i = 0; i < dist.length; i++) { // 从顶点i出发
for (int j = 0; j < dist.length; j++) { // 到达顶点j
// distance为路径i-->k-->j的距离
int distance = dist[i][k] + dist[k][j];
// 如果i-->k-->j的距离小于i-->j的距离
if (distance < dist[i][j]) {
// 更新最短路径
dist[i][j] = distance;
// 更新前驱
path[i][j] = path[k][j];
}
}
}
}
}
// 测试
public static void main(String[] args) {
String[] vertex = {"A", "B", "C", "D", "E", "F", "G"};
// 将顶点加入到集合
List<String> list = new ArrayList<>();
for (String v : vertex)
list.add(v);
// max表示不连通
int max = Integer.MAX_VALUE / 2;
// 初始化邻接矩阵
int[][] edge = new int[][]{
{0, 5, 7, max, max, max, 2},
{5, 0, max, 9, max, max, 3},
{7, max, 0, max, 8, max, max},
{max, 9, max, 0, max, 4, max},
{max, max, 8, max, 0, 5, 4},
{max, max, max, 4, 5, 0, 6},
{2, 3, max, max, 4, 6, 0}
};
// 创建一个图,图是自己定义的
Graph graph = new Graph(vertex.length);
graph.vertex = list;
graph.edge = edge;
System.out.println("-----Dijkstra-----");
AuxiliaryArray auxiliary = new AuxiliaryArray(graph.maxVertexNum, 2);
dijkstra(graph, auxiliary, auxiliary.source);
auxiliary.display(graph.vertex);
System.out.println("\n-----Floyd-----");
int[][] dist = new int[graph.maxVertexNum][graph.maxVertexNum];
int[][] path = new int[graph.maxVertexNum][graph.maxVertexNum];
floyd(graph, dist, path);
// 遍历dist和path
for (int r = 0; r < dist.length; r++) {
System.out.print("各个顶点到源点" + graph.vertex.get(r) + "的前驱:");
for (int c = 0; c < dist.length; c++)
System.out.print(vertex[path[r][c]] + " ");
System.out.println();
for (int c = 0; c < dist.length; c++)
System.out.print(vertex[r] + "-" + dist[r][c] + "->" + vertex[c] + "\t");
System.out.println();
}
}
}