文章目录
- 图
- 1) 概念
- 有向 vs 无向
- 度
- 权
- 路径
- 环
- 图的连通性
- 2) 图的表示
- 3) Java 表示
- 4) DFS
- 5) BFS
- 6) 拓扑排序
- 7) 最短路径
- Dijkstra
- Bellman-Ford
- Floyd-Warshall
- 8) 最小生成树
- Prim
- Kruskal
图
1) 概念
图是由顶点(vertex)和边(edge)组成的数据结构,例如
该图有四个顶点:A、B、C、D 以及四条有向边,有向图中,边是单向的
有向 vs 无向
如果是无向图,那么边是双向的,下面是一个无向图的例子
度
度是指与该顶点相邻的边的数量
例如上图中
- A、B、C、E、F 这几个顶点度数为 2
- D 顶点度数为 4
有向图中,细分为入度和出度,参见下图
- A (2 out / 0 in)
- B、C、E (1 out / 1 in)
- D (2 out / 2 in)
- F (0 out / 2 in)
权
边可以有权重,代表从源顶点到目标顶点的距离、费用、时间或其他度量。
路径
路径被定义为从一个顶点到另一个顶点的一系列连续边,例如上图中【北京】到【上海】有多条路径
- 北京 - 上海
- 北京 - 武汉 - 上海
路径长度
- 不考虑权重,长度就是边的数量
- 考虑权重,一般就是权重累加
环
在有向图中,从一个顶点开始,可以通过若干条有向边返回到该顶点,那么就形成了一个环
图的连通性
如果两个顶点之间存在路径,则这两个顶点是连通的,所有顶点都连通,则该图被称之为连通图,若子图连通,则称为连通分量
2) 图的表示
比如说,下面的图
用邻接矩阵可以表示为:
A B C D
A 0 1 1 0
B 1 0 0 1
C 1 0 0 1
D 0 1 1 0
用邻接表可以表示为:
A -> B -> C
B -> A -> D
C -> A -> D
D -> B -> C
有向图的例子
A B C D
A 0 1 1 0
B 0 0 0 1
C 0 0 0 1
D 0 0 0 0
A - B - C
B - D
C - D
D - empty
3) Java 表示
顶点
public class Vertex {
String name;
List<Edge> edges;
// 拓扑排序相关
int inDegree;
int status; // 状态 0-未访问 1-访问中 2-访问过,用在拓扑排序
// dfs, bfs 相关
boolean visited;
// 求解最短距离相关
private static final int INF = Integer.MAX_VALUE;
int dist = INF;
Vertex prev = null;
@Override
public String toString() {
return this.name;
}
}
边
public class Edge {
Vertex linked;
int weight;
public Edge(Vertex linked) {
this(linked, 1);
}
public Edge(Vertex linked, int weight) {
this.linked = linked;
this.weight = weight;
}
}
4) DFS
public class Dfs {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));
v2.edges = List.of(new Edge(v4));
v3.edges = List.of(new Edge(v4), new Edge(v6));
v4.edges = List.of(new Edge(v5));
v5.edges = List.of();
v6.edges = List.of(new Edge(v5));
dfs1(v1);
}
private static void dfs2(Vertex v) {
LinkedList<Vertex> stack = new LinkedList<>();
stack.push(v);
while (!stack.isEmpty()) {
Vertex pop = stack.pop();
pop.visited = true;
System.out.println(pop.name);
for (Edge edge : pop.edges) {
if (!edge.linked.visited) {
stack.push(edge.linked);
}
}
}
}
private static void dfs1(Vertex v) {
v.visited = true;
System.out.println(v.name);
for (Edge edge : v.edges) {
if (!edge.linked.visited) {
dfs(edge.linked);
}
}
}
}
5) BFS
public class Bfs {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));
v2.edges = List.of(new Edge(v4));
v3.edges = List.of(new Edge(v4), new Edge(v6));
v4.edges = List.of(new Edge(v5));
v5.edges = List.of();
v6.edges = List.of(new Edge(v5));
bfs(v1);
}
private static void bfs(Vertex v) {
LinkedList<Vertex> queue = new LinkedList<>();
v.visited = true;
queue.offer(v);
while (!queue.isEmpty()) {
Vertex poll = queue.poll();
System.out.println(poll.name);
for (Edge edge : poll.edges) {
if (!edge.linked.visited) {
edge.linked.visited = true;
queue.offer(edge.linked);
}
}
}
}
}
6) 拓扑排序
public class TopologicalSort {
public static void main(String[] args) {
Vertex v1 = new Vertex("网页基础");
Vertex v2 = new Vertex("Java基础");
Vertex v3 = new Vertex("JavaWeb");
Vertex v4 = new Vertex("Spring框架");
Vertex v5 = new Vertex("微服务框架");
Vertex v6 = new Vertex("数据库");
Vertex v7 = new Vertex("实战项目");
v1.edges = java.util.List.of(new Edge(v3)); // +1
v2.edges = java.util.List.of(new Edge(v3)); // +1
v3.edges = java.util.List.of(new Edge(v4));
v6.edges = java.util.List.of(new Edge(v4));
v4.edges = java.util.List.of(new Edge(v5));
v5.edges = java.util.List.of(new Edge(v7));
v7.edges = java.util.List.of();
List<Vertex> graph = java.util.List.of(v1,v2,v3,v4,v5,v6,v7);
for (Vertex vertex : graph) {
for (Edge edge : vertex.edges) {
edge.linked.inDegree += 1;
}
}
List<Vertex> result = new ArrayList<>();
Stack<Vertex> stack = new Stack<>();
for (Vertex vertex : graph) {
if(vertex.inDegree == 0 ){
stack.push(vertex);
}
}
while(!stack.isEmpty()){
Vertex pop = stack.pop();
result.add(pop);
for (Edge edge : pop.edges) {
edge.linked.inDegree--;
if(edge.linked.inDegree == 0 && !edge.linked.visited){
stack.add(edge.linked);
edge.linked.visited = true;
}
}
}
if(result.size() != graph.size()){
System.out.println("发现环");
}
for (Vertex vertex : result) {
System.out.println(vertex);
}
}
7) 最短路径
Dijkstra
算法描述:
- 将所有顶点标记为未访问。创建一个未访问顶点的集合。
- 为每个顶点分配一个临时距离值
- 对于我们的初始顶点,将其设置为零
- 对于所有其他顶点,将其设置为无穷大。
- 每次选择最小临时距离的未访问顶点,作为新的当前顶点
- 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小
- 例如,1->6 的距离是 14,而1->3->6 的距离是11。这时将距离更新为 11
- 否则,将保留上次距离值
- 当前顶点的邻居处理完成后,把它从未访问集合中删除
public class Dijkstra {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
v2.edges = List.of(new Edge(v4, 15));
v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
v4.edges = List.of(new Edge(v5, 6));
v5.edges = List.of();
v6.edges = List.of(new Edge(v5, 9));
List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);
dijkstra(graph, v1);
}
private static void dijkstra(List<Vertex> graph, Vertex source) {
ArrayList<Vertex> list = new ArrayList<>(graph);
source.dist = 0;
while (!list.isEmpty()) {
// 3. 选取当前顶点
Vertex curr = chooseMinDistVertex(list);
// 4. 更新当前顶点邻居距离
updateNeighboursDist(curr, list);
// 5. 移除当前顶点
list.remove(curr);
}
for (Vertex v : graph) {
System.out.println(v.name + " " + v.dist);
}
}
private static void updateNeighboursDist(Vertex curr, ArrayList<Vertex> list) {
for (Edge edge : curr.edges) {
Vertex n = edge.linked;
if (list.contains(n)) {
int dist = curr.dist + edge.weight;
if (dist < n.dist) {
n.dist = dist;
}
}
}
}
private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {
Vertex min = list.get(0);
for (int i = 1; i < list.size(); i++) {
if (list.get(i).dist < min.dist) {
min = list.get(i);
}
}
return min;
}
}
改进 - 优先级队列
- 创建一个优先级队列,放入所有顶点(队列大小会达到边的数量)
- 为每个顶点分配一个临时距离值
- 对于我们的初始顶点,将其设置为零
- 对于所有其他顶点,将其设置为无穷大。
- 每次选择最小临时距离的未访问顶点,作为新的当前顶点
- 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小,若距离更新需加入队列
- 例如,1->6 的距离是 14,而1->3->6 的距离是11。这时将距离更新为 11
- 否则,将保留上次距离值
- 当前顶点的邻居处理完成后,把它从队列中删除
public class DijkstraPriorityQueue {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
v2.edges = List.of(new Edge(v4, 15));
v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
v4.edges = List.of(new Edge(v5, 6));
v5.edges = List.of();
v6.edges = List.of(new Edge(v5, 9));
List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);
dijkstra(graph, v1);
}
private static void dijkstra(List<Vertex> graph, Vertex source) {
PriorityQueue<Vertex> queue = new PriorityQueue<>(Comparator.comparingInt(v -> v.dist));
source.dist = 0;
for (Vertex v : graph) {
queue.offer(v);
}
while (!queue.isEmpty()) {
System.out.println(queue);
// 3. 选取当前顶点
Vertex curr = queue.poll();
// 4. 更新当前顶点邻居距离
if(!curr.visited) {
updateNeighboursDist(curr, queue);
curr.visited = true;
}
// 5. 移除当前顶点
}
for (Vertex v : graph) {
System.out.println(v.name + " " + v.dist + " " + (v.prev != null ? v.prev.name : "null"));
}
}
private static void updateNeighboursDist(Vertex curr, PriorityQueue<Vertex> queue) {
for (Edge edge : curr.edges) {
Vertex n = edge.linked;
if (!n.visited) {
int dist = curr.dist + edge.weight;
if (dist < n.dist) {
n.dist = dist;
n.prev = curr;
queue.remove(n); // 先删除再添加才能改变优先级
queue.offer(n);
}
}
}
}
}
问题
按照 Dijkstra 算法,得出
- v1 -> v2 最短距离2
- v1 -> v3 最短距离1
- v1 -> v4 最短距离2
事实应当是
- v1 -> v2 最短距离2
- v1 -> v3 最短距离0
- v1 -> v4 最短距离1
Bellman-Ford
public class BellmanFord {
public static void main(String[] args) {
// 正常情况
/*Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
v2.edges = List.of(new Edge(v4, 15));
v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
v4.edges = List.of(new Edge(v5, 6));
v5.edges = List.of();
v6.edges = List.of(new Edge(v5, 9));
List<Vertex> graph = List.of(v4, v5, v6, v1, v2, v3);*/
// 负边情况
/*Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 1));
v2.edges = List.of(new Edge(v3, -2));
v3.edges = List.of(new Edge(v4, 1));
v4.edges = List.of();
List<Vertex> graph = List.of(v1, v2, v3, v4);*/
// 负环情况
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
v1.edges = List.of(new Edge(v2, 2));
v2.edges = List.of(new Edge(v3, -4));
v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));
v4.edges = List.of();
List<Vertex> graph = List.of(v1, v2, v3, v4);
bellmanFord(graph, v1);
}
private static void bellmanFord(List<Vertex> graph, Vertex source) {
source.dist = 0;
int size = graph.size();
// 1. 进行 顶点个数 - 1 轮处理
for (int i = 0; i < size - 1; i++) {
// 2. 遍历所有的边
for (Vertex s : graph) {
for (Edge edge : s.edges) {
// 3. 处理每一条边
Vertex e = edge.linked;
if (s.dist != Integer.MAX_VALUE && s.dist + edge.weight < e.dist) {
e.dist = s.dist + edge.weight;
e.prev = s;
}
}
}
}
for (Vertex v : graph) {
System.out.println(v + " " + (v.prev != null ? v.prev.name : "null"));
}
}
}
负环
如果在【顶点-1】轮处理完成后,还能继续找到更短距离,表示发现了负环
Floyd-Warshall
public class FloydWarshall {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
v1.edges = List.of(new Edge(v3, -2));
v2.edges = List.of(new Edge(v1, 4), new Edge(v3, 3));
v3.edges = List.of(new Edge(v4, 2));
v4.edges = List.of(new Edge(v2, -1));
List<Vertex> graph = List.of(v1, v2, v3, v4);
/*
直接连通
v1 v2 v3 v4
v1 0 ∞ -2 ∞
v2 4 0 3 ∞
v3 ∞ ∞ 0 2
v4 ∞ -1 ∞ 0
k=0 借助v1到达其它顶点
v1 v2 v3 v4
v1 0 ∞ -2 ∞
v2 4 0 2 ∞
v3 ∞ ∞ 0 2
v4 ∞ -1 ∞ 0
k=1 借助v2到达其它顶点
v1 v2 v3 v4
v1 0 ∞ -2 ∞
v2 4 0 2 ∞
v3 ∞ ∞ 0 2
v4 3 -1 1 0
k=2 借助v3到达其它顶点
v1 v2 v3 v4
v1 0 ∞ -2 0
v2 4 0 2 4
v3 ∞ ∞ 0 2
v4 3 -1 1 0
k=3 借助v4到达其它顶点
v1 v2 v3 v4
v1 0 -1 -2 0
v2 4 0 2 4
v3 5 1 0 2
v4 3 -1 1 0
*/
floydWarshall(graph);
}
private static void floydWarshall(List<Vertex> graph){
int size = graph.size();
int[][] dist = new int[size][size];
for (int i = 0; i < size; i++) { // 初始化
Vertex vertex = graph.get(i);
Map<Vertex, Integer> collect = vertex.edges.stream().collect(Collectors.toMap(v -> v.linked, v -> v.weight));
for (int i1 = 0; i1 < size; i1++) {
if(i == i1){
dist[i][i1] = 0;
continue;
}
dist[i][i1] = collect.getOrDefault(graph.get(i1),Integer.MAX_VALUE);
}
}
for (int k = 0; k < size; k++) {
for (int j = 0; j < size; j++) {
int dist1;
if((dist1 = dist[j][k]) < Integer.MAX_VALUE){
for (int i = 0; i < size; i++) {
int dist2;
if((dist2 = dist[k][i]) != Integer.MAX_VALUE)
dist[j][i] = Integer.min(dist1 + dist2,dist[j][i]);
}
}
}
}
for (int[] ints : dist) {
for (int anInt : ints) {
System.out.print(anInt + " ");
}
System.out.println();
}
}
}
负环
如果在 3 层循环结束后,在 dist 数组的对角线处(i==j 处)发现了负数,表示出现了负环
8) 最小生成树
图的最小生成树是一个子图,它是连通的,包含图中所有的顶点,并且所有边的权重之和最小。在最小生成树中,没有任何一条边可以被其他边替换而使得总权重变小。也就是说,最小生成树是图的所有生成树中,边的权值总和最小的生成树。
解决最小生成树问题的常用算法有Prim算法和Kruskal算法。Prim算法从一个顶点开始,每次都添加一条与当前子图连接的权重最小的边,直到所有顶点都被包含在子图中。Kruskal算法则是从所有的边开始,每次都添加一条当前所有边中权重最小的边,但需要保证添加的边不会形成环,直到所有顶点都被连接。
Prim
public class Prim {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
Vertex v7 = new Vertex("v7");
v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 4), new Edge(v4, 1));
v2.edges = List.of(new Edge(v1, 2), new Edge(v4, 3), new Edge(v5, 10));
v3.edges = List.of(new Edge(v1, 4), new Edge(v4, 2), new Edge(v6, 5));
v4.edges = List.of(new Edge(v1, 1), new Edge(v2, 3), new Edge(v3, 2),
new Edge(v5, 7), new Edge(v6, 8), new Edge(v7, 4));
v5.edges = List.of(new Edge(v2, 10), new Edge(v4, 7), new Edge(v7, 6));
v6.edges = List.of(new Edge(v3, 5), new Edge(v4, 8), new Edge(v7, 1));
v7.edges = List.of(new Edge(v4, 4), new Edge(v5, 6), new Edge(v6, 1));
List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);
prim(graph, v1);
}
static void prim(List<Vertex> graph, Vertex source) {
ArrayList<Vertex> list = new ArrayList<>(graph);
source.dist = 0;
while (!list.isEmpty()) {
Vertex min = chooseMinDistVertex(list);
updateNeighboursDist(min);
list.remove(min);
min.visited = true;
System.out.println("---------------");
for (Vertex v : graph) {
System.out.println(v);
}
}
}
private static void updateNeighboursDist(Vertex curr) {
for (Edge edge : curr.edges) {
Vertex n = edge.linked;
if (!n.visited) {
int dist = edge.weight;
if (dist < n.dist) {
n.dist = dist;
n.prev = curr;
}
}
}
}
private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {
Vertex min = list.get(0);
for (int i = 1; i < list.size(); i++) {
if (list.get(i).dist < min.dist) {
min = list.get(i);
}
}
return min;
}
}
Kruskal
private static void kruskal(List<Vertex> graph,Vertex v1){
List<Edge> edges = new ArrayList<>();
List<Vertex> pre = new ArrayList<>();
for (Vertex vertex : graph) {
for (Edge edge : vertex.edges) {
edges.add(edge);
pre.add(vertex);
}
}
for (int i = 0; i < edges.size(); i++) {
Edge minEdge = edges.get(i);
int min = minEdge.weight;
for (int j = i + 1 ; j < edges.size(); j++) {
Edge e = edges.get(j);
if(minEdge.weight > e.weight){
edges.set(j,minEdge);
minEdge = e;
edges.set(i,e);
Vertex v = pre.get(i);
pre.set(i,pre.get(j));
pre.set(j,v);
}
}
}
List<Vertex> used = new ArrayList<>();
for (int i = 0; i < edges.size(); i++) {
Vertex v = pre.get(i);
Vertex e = edges.get(i).linked;
if(!used.contains(v) || !used.contains(e)){
System.out.println(v.name + " -> " + e.name);
used.add(v);
used.add(e);
}
}
}