一、图的基础知识
二、邻接矩阵存图
图的文件
邻接矩阵存图的代码:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author zzyuan
* @date 2021/8/10 - 23:12
*/
// 空间复杂度:O(V^2),浪费空间
public class AdjMatrix implements Graph {
private int V; // 顶点的个数
private int E; // 边的个数
private int[][] adj; // 邻接矩阵
// 建图时间复杂度:O(E)
public AdjMatrix(String fileName) {
try {
BufferedReader reader
= new BufferedReader(new FileReader(fileName));
String line = reader.readLine();
String[] arr = line.split(" ");
this.V = Integer.valueOf(arr[0]);
this.E = Integer.valueOf(arr[1]);
this.adj = new int[V][V];
while ((line = reader.readLine()) != null) {
arr = line.split(" ");
int a = Integer.valueOf(arr[0]);
validateVertex(a);
int b = Integer.valueOf(arr[1]);
validateVertex(b);
// 检测自环边
if (a == b) {
throw new RuntimeException("出现了自环边,错误");
}
// 检测平行边
if (adj[a][b] == 1) {
throw new RuntimeException("出现了平行边,错误");
}
adj[a][b] = 1;
adj[b][a] = 1;
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void validateVertex(int v) {
if (v < 0 || v >= V) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
@Override
public int getV() {
return V;
}
@Override
public int getE() {
return E;
}
// 判断两个指定的顶点之间是否有边
// 时间复杂度:O(1)
@Override
public boolean hasEdge(int v, int w) {
validateVertex(v);
validateVertex(w);
return adj[v][w] == 1;
}
// 获取指定顶点所有相邻的顶点
// 时间复杂度:O(V)
@Override
public Collection<Integer> adj(int v) {
validateVertex(v);
List<Integer> res = new ArrayList<>();
for (int i = 0; i < V; i++) {
if (adj[v][i] == 1) {
res.add(i);
}
}
return res;
}
// 获取指定顶点的度数
@Override
public int degree(int v) {
return adj(v).size();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("顶点数 = %d,边数 = %d \n", V, E));
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
sb.append(adj[i][j] + " ");
}
sb.append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
AdjMatrix adjMatrix = new AdjMatrix("data/graph.txt");
System.out.println(adjMatrix);
}
}
二、邻接表存图
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* @author zzyuan
* @date 2021/8/10 - 23:12
*/
// 空间复杂度:O(V + E)
public class AdjList implements Graph {
private int V; // 顶点的个数
private int E; // 边的个数
private LinkedList<Integer>[] adj; // 邻接表
// 建图时间复杂度:O(E*V)
public AdjList(String fileName) {
try {
BufferedReader reader
= new BufferedReader(new FileReader(fileName));
String line = reader.readLine();
String[] arr = line.split(" ");
this.V = Integer.valueOf(arr[0]);
this.E = Integer.valueOf(arr[1]);
this.adj = new LinkedList[V];
for (int i = 0; i < V; i++) {
adj[i] = new LinkedList<>();
}
while ((line = reader.readLine()) != null) { // O(E)
arr = line.split(" ");
int a = Integer.valueOf(arr[0]);
validateVertex(a);
int b = Integer.valueOf(arr[1]);
validateVertex(b);
// 检测自环边
if (a == b) {
throw new RuntimeException("出现了自环边,错误");
}
// 检测平行边
if (adj[a].contains(b)) { // O(V)
throw new RuntimeException("出现了平行边,错误");
}
adj[a].add(b);
adj[b].add(a);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void validateVertex(int v) {
if (v < 0 || v >= V) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
@Override
public int getV() {
return V;
}
@Override
public int getE() {
return E;
}
// 判断两个指定的顶点之间是否有边
// 时间复杂度:O(V)
@Override
public boolean hasEdge(int v, int w) {
validateVertex(v);
validateVertex(w);
return adj[v].contains(w);
}
// 获取指定顶点所有相邻的顶点
// 时间复杂度:O(1)
@Override
public List<Integer> adj(int v) {
validateVertex(v);
return adj[v];
}
// 获取指定顶点的度数
@Override
public int degree(int v) {
return adj(v).size();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("顶点数 = %d,边数 = %d \n", V, E));
for (int v = 0; v < V; v++) {
sb.append(v + ": ");
for (int w : adj[v]) {
sb.append(w + " ");
}
sb.append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
AdjList adjList = new AdjList("data/graph.txt");
System.out.println(adjList);
}
}
基于TreeSet 的邻接表实现
package com.douma.graph;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
// 空间复杂度:O(V + E)
public class AdjSet implements Graph {
private int V; // 顶点的个数
private int E; // 边的个数
private TreeSet<Integer>[] adj; // 邻接表
// 建图时间复杂度:O(E*logV)
public AdjSet(String fileName) {
try {
BufferedReader reader
= new BufferedReader(new FileReader(fileName));
String line = reader.readLine();
String[] arr = line.split(" ");
this.V = Integer.valueOf(arr[0]);
this.E = Integer.valueOf(arr[1]);
this.adj = new TreeSet[V];
for (int i = 0; i < V; i++) {
adj[i] = new TreeSet<>();
}
while ((line = reader.readLine()) != null) { // O(E)
arr = line.split(" ");
int a = Integer.valueOf(arr[0]);
validateVertex(a);
int b = Integer.valueOf(arr[1]);
validateVertex(b);
// 检测自环边
if (a == b) {
throw new RuntimeException("出现了自环边,错误");
}
// 检测平行边
if (adj[a].contains(b)) { // O(logV)
throw new RuntimeException("出现了平行边,错误");
}
adj[a].add(b);
adj[b].add(a);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void validateVertex(int v) {
if (v < 0 || v >= V) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
@Override
public int getV() {
return V;
}
@Override
public int getE() {
return E;
}
// 判断两个指定的顶点之间是否有边
// 时间复杂度:O(logV)
@Override
public boolean hasEdge(int v, int w) {
validateVertex(v);
validateVertex(w);
return adj[v].contains(w);
}
// 获取指定顶点所有相邻的顶点
// 时间复杂度:O(1)
@Override
public Collection<Integer> adj(int v) {
validateVertex(v);
return adj[v];
}
// 获取指定顶点的度数
@Override
public int degree(int v) {
return adj(v).size();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("顶点数 = %d,边数 = %d \n", V, E));
for (int v = 0; v < V; v++) {
sb.append(v + ": ");
for (int w : adj[v]) {
sb.append(w + " ");
}
sb.append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
AdjSet adjList = new AdjSet("data/graph.txt");
System.out.println(adjList);
}
}
四、邻接矩阵和邻接表的对比
五、图的接口
public interface Graph {
/**
* 获取图的边数
* @return
*/
int getE();
/**
* 获取图的顶点数
* @return
*/
int getV();
/**
* 判断两个指定的顶点之间是否有边
* @param v
* @param w
* @return
*/
boolean hasEdge(int v, int w);
/**
* 获取指定顶点所有相邻的顶点
* @param v
* @return
*/
Collection<Integer> adj(int v);
/**
* 获取指定顶点的度数
* @param v
* @return
*/
int degree(int v);
}
六、图的DFS深度优先遍历
public class GraphDFS {
private Graph g;
private List<Integer> res;
// 用于防止一个节点被重复访问
private boolean[] visited;
public GraphDFS(Graph g) {
this.g = g;
if (g == null) return;
this.res = new ArrayList<>();
this.visited = new boolean[g.getV()];
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (!visited[v]) {
dfs(v);
}
}
}
// 时间复杂度:O(V)
// 空间复杂度:O(V)
private void dfs(int v) {
Stack<Integer> stack = new Stack<>();
stack.push(v);
visited[v] = true;
while (!stack.isEmpty()) {
int curr = stack.pop();
res.add(curr);
for (int w : g.adj(curr)) { // 升序排列
// 如果已经访问过了,就不压入栈,就不会再次访问了
if (!visited[w]) {
stack.push(w);
visited[w] = true;
}
}
}
}
public List<Integer> getRes() {
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
GraphDFS graphDFS = new GraphDFS(g);
System.out.println(graphDFS.getRes());
}
}
public class GraphDFSR {
private Graph g;
private List<Integer> res;
// 用于防止一个节点被重复访问
private boolean[] visited;
public GraphDFSR(Graph g) {
this.g = g;
if (g == null) return;
this.res = new ArrayList<>();
this.visited = new boolean[g.getV()];
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (!visited[v]) {
dfs(v);
}
}
}
private void dfs(int v) {
res.add(v);
visited[v] = true;
for (int w : g.adj(v)) {
if (!visited[w]) {
dfs(w);
}
}
}
public List<Integer> getRes() {
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
GraphDFSR graphDFS = new GraphDFSR(g);
System.out.println(graphDFS.getRes());
}
}
七、DFS实现图的联通分量
求联通分量的个数
public class CC {
private Graph g;
// 用于防止一个节点被重复访问
private boolean[] visited;
private int ccCount = 0;
public CC(Graph g) {
this.g = g;
if (g == null) return;
this.visited = new boolean[g.getV()];
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (!visited[v]) {
ccCount++;
dfs(v);
}
}
}
private void dfs(int v) {
visited[v] = true;
for (int w : g.adj(v)) {
if (!visited[w]) {
dfs(w);
}
}
}
public int getCcCount() {
return ccCount;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
CC graphDFS = new CC(g);
System.out.println(graphDFS.getCcCount());
}
}
求每个联通分量包含的顶点个数
public class CC1 {
private Graph g;
// 用于防止一个节点被重复访问
private int[] visited;
private int ccCount = 0;
public CC1(Graph g) {
this.g = g;
if (g == null) return;
this.visited = new int[g.getV()];
Arrays.fill(visited, -1);
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (visited[v] == -1) {
ccCount++;
dfs(v, ccCount);
}
}
}
private void dfs(int v, int ccId) {
visited[v] = ccId;
for (int w : g.adj(v)) {
if (visited[w] == -1) {
dfs(w, ccId);
}
}
}
public int getCcCount() {
return ccCount;
}
public List<Integer>[] components() {
List<Integer>[] res = new ArrayList[ccCount];
// Arrays.fill(res, new ArrayList<>());
for (int i = 0; i < ccCount; i++) {
res[i] = new ArrayList<>();
}
for (int v = 0; v < g.getV(); v++) {
int cc = visited[v];
res[cc - 1].add(v);
}
return res;
}
public boolean isConnected(int v, int w) {
validateVertex(v);
validateVertex(w);
return visited[v] == visited[w];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
CC1 graphDFS = new CC1(g);
System.out.println(Arrays.toString(graphDFS.components()));
System.out.println(graphDFS.isConnected(0, 6));
}
}
检测两个顶点是否联通
public boolean isConnected(int v, int w) {
validateVertex(v);
validateVertex(w);
return visited[v] == visited[w];
}
八、DFS求单源路径
public class SingleSourcePath {
private Graph g;
private int source;
// 用于防止一个节点被重复访问
private boolean[] visited;
private int[] prevs;
public SingleSourcePath(Graph g, int source) {
this.g = g;
this.source = source;
this.visited = new boolean[g.getV()];
prevs = new int[g.getV()];
// 将每个顶点的前一个顶点初始化为 -1
for (int i = 0; i < g.getV(); i++) {
prevs[i] = -1;
}
// 深度优先遍历,这里只需要从指定的源顶点遍历就可以
// 源顶点的前一个顶点设置为源顶点本身
dfs(source, source);
}
// 递归遍历顶点 v,并且维护顶点 v 的前一个顶点的信息
private void dfs(int v, int prev) {
visited[v] = true;
// 维护顶点 v 的前一个顶点
prevs[v] = prev;
for (int w : g.adj(v)) {
if (!visited[w]) {
// v 是 w 的前一个顶点
dfs(w, v);
}
}
}
public boolean isConnected(int target) {
validateVertex(target);
return visited[target];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
public List<Integer> path(int target) {
List<Integer> res = new ArrayList<>();
// 1. 如果源顶点到不了目标顶点,直接返回
if (!isConnected(target)) {
return res;
}
// 2. 根据 prevs 信息找到路径
while (target != source) {
res.add(target);
target = prevs[target];
}
res.add(source);
// 3. 翻转
Collections.reverse(res);
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
SingleSourcePath graphDFS = new SingleSourcePath(g, 0);
System.out.println(graphDFS.path(6));
}
}
两个顶点之间的路径
public class TwoVertexPath {
private Graph g;
private int source;
private int target;
// 用于防止一个节点被重复访问
private boolean[] visited;
private int[] prevs;
private List<Integer> res;
public TwoVertexPath(Graph g, int source, int target) {
this.g = g;
this.source = source;
this.target = target;
this.res = new ArrayList<>();
this.visited = new boolean[g.getV()];
prevs = new int[g.getV()];
// 将每个顶点的前一个顶点初始化为 -1
for (int i = 0; i < g.getV(); i++) {
prevs[i] = -1;
}
// 深度优先遍历,这里只需要从指定的源顶点遍历就可以
// 源顶点的前一个顶点设置为源顶点本身
dfs(source, source);
// 计算一次
path();
}
// 递归遍历顶点 v,并且维护顶点 v 的前一个顶点的信息
private boolean dfs(int v, int prev) {
visited[v] = true;
// 维护顶点 v 的前一个顶点
prevs[v] = prev;
if (v == target) return true;
for (int w : g.adj(v)) {
if (!visited[w]) {
// v 是 w 的前一个顶点
// 如果已经找到了,不用再去遍历 v 的下一个相邻节点了
if (dfs(w, v)) return true;
}
}
return false;
}
public boolean isConnected() {
validateVertex(target);
return visited[target];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
private List<Integer> path() {
// 1. 如果源顶点到不了目标顶点,直接返回
if (!isConnected()) {
return res;
}
// 2. 根据 prevs 信息找到路径
int tmp = target;
while (tmp != source) {
res.add(tmp);
tmp = prevs[tmp];
}
res.add(source);
// 3. 翻转
Collections.reverse(res);
return res;
}
public List<Integer> getRes() {
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
TwoVertexPath graphDFS = new TwoVertexPath(g, 0, 6);
System.out.println(graphDFS.getRes());
}
}
九、DFS实现环检测
public class CycleDetection {
private Graph g;
// 用于防止一个节点被重复访问
private boolean[] visited;
private boolean hasCycle = false;
public CycleDetection(Graph g) {
this.g = g;
this.visited = new boolean[g.getV()];
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (!visited[v]) {
dfs(v, v);
}
}
}
private void dfs(int v, int prev) {
visited[v] = true;
for (int w : g.adj(v)) {
if (!visited[w]) {
dfs(w, v);
} else { // 否则,w 顶点已经被访问
// 如果 w 不是 v 的前一个节点的话,那么就存在环
if (w != prev) {
hasCycle = true;
}
}
}
}
public boolean hasCycle() {
return hasCycle;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
CycleDetection graphDFS = new CycleDetection(g);
System.out.println(graphDFS.hasCycle());
}
}
优化代码:检测到存在环我们就退出
public class CycleDetection1 {
private Graph g;
// 用于防止一个节点被重复访问
private boolean[] visited;
private boolean hasCycle = false;
public CycleDetection1(Graph g) {
this.g = g;
this.visited = new boolean[g.getV()];
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (!visited[v]) {
if (dfs(v, v)) {
hasCycle = true;
break;
}
}
}
}
private boolean dfs(int v, int prev) {
visited[v] = true;
for (int w : g.adj(v)) {
if (!visited[w]) {
// 后面的循环就可以不执行了
if (dfs(w, v)) return true;
} else { // 否则,w 顶点已经被访问
// 如果 w 不是 v 的前一个节点的话,那么就存在环
if (w != prev) {
return true;
}
}
}
return false;
}
public boolean hasCycle() {
return hasCycle;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
CycleDetection1 graphDFS = new CycleDetection1(g);
System.out.println(graphDFS.hasCycle());
}
}
十、DFS实现二分图检测
public class BipartitionDetection {
private Graph g;
// 用于防止一个节点被重复访问
private boolean[] visited;
// -1 表示没有染颜色
// 0 红色 1 蓝色
private int[] colors;
private boolean isBipartition = true;
public BipartitionDetection(Graph g) {
this.g = g;
this.visited = new boolean[g.getV()];
this.colors = new int[g.getV()];
Arrays.fill(colors, -1);
// 遍历图中每个顶点
for (int v = 0; v < g.getV(); v++) {
// 先判断,没有遍历的顶点才能进行深度优先遍历
if (!visited[v]) {
if (!dfs(v, 0)) {
isBipartition = false;
break;
}
}
}
}
private boolean dfs(int v, int color) {
visited[v] = true;
colors[v] = color;
for (int w : g.adj(v)) {
if (!visited[w]) {
// 如果 v 的颜色是 1,那么 w 的颜色就是 0
// 如果 v 的颜色是 0,那么 w 的颜色就是 1
if (!dfs(w, 1 - color)) return false;
} else if (colors[w] == colors[v]) {
// 如果相邻顶点的颜色一样的话,则不是二分图
return false;
}
}
return true;
}
public boolean isBipartition() {
return isBipartition;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-dfs.txt");
BipartitionDetection graphDFS = new BipartitionDetection(g);
System.out.println(graphDFS.isBipartition());
}
}
十一、图的BFS广度优先遍历
public class GraphBFS {
private Graph g;
private boolean[] visited;
private List<Integer> res;
public GraphBFS(Graph g) {
this.g = g;
this.visited = new boolean[g.getV()];
this.res = new ArrayList<>();
for (int v = 0; v < g.getV(); v++) {
if (!visited[v]) bfs(v);
}
}
private void bfs(int v) {
if (g == null) return;
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = true;
while (!queue.isEmpty()) {
int curr = queue.poll();
res.add(curr);
for (int w : g.adj(curr)) {
if (!visited[w]) {
queue.add(w);
visited[w] = true;
}
}
}
}
public List<Integer> getRes() {
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
GraphBFS graphBFS = new GraphBFS(g);
System.out.println(graphBFS.getRes());
}
}
十二、BFS实现图的联通分量问题
public class CC {
private Graph g;
private int[] visited;
private int ccCount;
public CC(Graph g) {
this.g = g;
this.visited = new int[g.getV()];
Arrays.fill(visited,-1);
for (int v = 0 ; v < g.getV() ; v++){
if(visited[v] == -1){
bfs(v,ccCount);
ccCount++;
}
}
}
public void bfs(int v,int ccId){
if(g == null){
return;
}
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = ccId;
while(!queue.isEmpty()){
int curr = queue.poll();
for(int w : g.adj(curr)){
if(visited[w] == -1){
queue.add(w);
visited[w] = ccId;
}
}
}
}
public List<Integer>[] components(){
List<Integer>[] res = new ArrayList[ccCount];
for(int i = 0 ; i < ccCount ; i++){
res[i] = new ArrayList<>();
}
for(int v = 0 ; v < g.getV() ; v++){
int cc = visited[v];
res[cc-1].add(v);
}
return res;
}
public boolean isConnected(int v, int w) {
validateVertex(v);
validateVertex(w);
return visited[v] == visited[w];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
public int getCcCount() {
return ccCount;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
CC graphBfs = new CC(g);
System.out.println(graphBfs.getCcCount());
}
}
十三、BFS求单源路径
public class SingleSourcePath {
private Graph g;
private boolean[] visited;
private int[] prevs;
private int source;
public SingleSourcePath(Graph g, int source) {
this.g = g;
this.source = source;
this.visited = new boolean[g.getV()];
this.prevs = new int[g.getV()];
Arrays.fill(this.prevs, -1);
bfs(source);
}
private void bfs(int v) {
if (g == null) return;
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = true;
// 维护顶点的前一个顶点
prevs[v] = v;
while (!queue.isEmpty()) {
int curr = queue.poll();
for (int w : g.adj(curr)) {
if (!visited[w]) {
queue.add(w);
visited[w] = true;
// 维护顶点的前一个顶点
prevs[w] = curr;
}
}
}
}
public boolean isConnected(int target) {
validateVertex(target);
return visited[target];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
public List<Integer> path(int target) {
List<Integer> res = new ArrayList<>();
// 1. 如果源顶点到不了目标顶点,直接返回
if (!isConnected(target)) {
return res;
}
// 2. 根据 prevs 信息找到路径
while (target != source) {
res.add(target);
target = prevs[target];
}
res.add(source);
// 3. 翻转
Collections.reverse(res);
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
SingleSourcePath graphBFS = new SingleSourcePath(g, 0);
System.out.println(graphBFS.path(6));
}
}
public class SingleSourcePath1 {
private Graph g;
private boolean[] visited;
private int[] prevs;
private int source;
private int target;
public SingleSourcePath1(Graph g, int source, int target) {
this.g = g;
this.source = source;
this.target = target;
this.visited = new boolean[g.getV()];
this.prevs = new int[g.getV()];
Arrays.fill(this.prevs, -1);
bfs(source);
}
private void bfs(int v) {
if (g == null) return;
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = true;
// 维护顶点的前一个顶点
prevs[v] = v;
while (!queue.isEmpty()) {
int curr = queue.poll();
if (curr == target) {
return;
}
for (int w : g.adj(curr)) {
if (!visited[w]) {
queue.add(w);
visited[w] = true;
// 维护顶点的前一个顶点
prevs[w] = curr;
}
}
}
}
public boolean isConnected() {
validateVertex(target);
return visited[target];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
public List<Integer> path() {
List<Integer> res = new ArrayList<>();
// 1. 如果源顶点到不了目标顶点,直接返回
if (!isConnected()) {
return res;
}
// 2. 根据 prevs 信息找到路径
int tmp = target;
while (tmp != source) {
res.add(tmp);
tmp = prevs[tmp];
}
res.add(source);
// 3. 翻转
Collections.reverse(res);
return res;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
SingleSourcePath1 graphBFS = new SingleSourcePath1(g, 0, 6);
System.out.println(graphBFS.path());
}
}
十四、BFS实现环检测
public class CycleDetection {
private Graph g;
private boolean[] visited;
private int[] prevs;
private boolean hasCycle = false;
public CycleDetection(Graph g) {
this.g = g;
this.visited = new boolean[g.getV()];
this.prevs = new int[g.getV()];
Arrays.fill(this.prevs, -1);
for (int v = 0; v < g.getV(); v++) {
if (!visited[v]) {
if (bfs(v)) {
hasCycle = true;
break;
}
}
}
}
private boolean bfs(int v) {
if (g == null) return false;
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = true;
// 维护顶点的前一个顶点
prevs[v] = v;
while (!queue.isEmpty()) {
int curr = queue.poll();
for (int w : g.adj(curr)) {
if (!visited[w]) {
queue.add(w);
visited[w] = true;
// 维护顶点的前一个顶点
prevs[w] = curr;
} else {
// 到了这里,说明:当前的节点 curr 的相邻顶点 w 已经被访问了
// bug 修复:如果 curr 的相邻顶点 w 又不是当前顶点的前一个顶点的话,说明有环
/**
a----b
\ /
c
假设先访问 a 节点(visited[a] = true),
然后 b 和 c 都是 a 的相邻节点,依次将 b 和 c 放入到队列中
并且维护 prevs[b] = a 和 prevs[c] = a,且 visited[b] = true 和 visited[c] = true
然后从队列中把 b 节点拉出来,进行访问,b 的相邻节点分别是 a 和 c,我们分别来考虑
先考虑相邻的节点 a,这个时候:
curr = b, w = a,且 visited[w] = true,w = prevs[curr]
即相邻的节点已经被访问,并且这个相邻节点是当前节点的前一个节点,不能判断为有环
再来考虑相邻的节点 c,这个时候:
curr = b, w = c,且 visited[w] = true,w != prevs[curr]
即相邻的节点已经被访问,并且这个相邻的节点不是当前节点的前一个节点,此时说明有环
*/
if (w != prevs[curr]) {
return true;
}
}
}
}
return false;
}
public boolean hasCycle() {
return hasCycle;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
CycleDetection graphBFS = new CycleDetection(g);
System.out.println(graphBFS.hasCycle());
}
}
十五、BFS实现二分图检测
public class BipartitionDetection {
private Graph g;
private boolean[] visited;
// -1 表示没有染颜色
// 0 红色 1 蓝色
private int[] colors;
private boolean isBipartition = true;
public BipartitionDetection(Graph g) {
this.g = g;
this.visited = new boolean[g.getV()];
this.colors = new int[g.getV()];
Arrays.fill(this.colors, -1);
for (int v = 0; v < g.getV(); v++) {
if (!visited[v]) {
if (!bfs(v)) {
isBipartition = false;
break;
}
}
}
}
private boolean bfs(int v) {
if (g == null) return true;
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = true;
colors[v] = 0;
while (!queue.isEmpty()) {
int curr = queue.poll();
for (int w : g.adj(curr)) {
// 如果 w 没有遍历过,则需要染色
if (!visited[w]) {
queue.add(w);
visited[w] = true;
// 给顶点 w 染色,和 curr 的颜色不一样
colors[w] = 1 - colors[curr];
} else if (colors[w] == colors[curr]) {
// 如果 w 被访问过,并且它的颜色和相邻点一样
// 那么可以判定不是二分图
return false;
}
}
}
return true;
}
public boolean isBipartition() {
return isBipartition;
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
BipartitionDetection graphBFS = new BipartitionDetection(g);
System.out.println(graphBFS.isBipartition());
}
}
十六、BFS单源最短路问题
public class SingleSourceShortestPath {
private Graph g;
private boolean[] visited;
private int[] prevs;
private int[] distance;//每一个顶点到源顶点的距离
private int source;
public SingleSourceShortestPath(Graph g, int source) {
this.g = g;
this.source = source;
this.visited = new boolean[g.getV()];
this.prevs = new int[g.getV()];
this.distance = new int[g.getV()];
Arrays.fill(this.prevs, -1);
Arrays.fill(this.distance, -1);
bfs(source);
}
private void bfs(int v) {
if (g == null) return;
Queue<Integer> queue = new LinkedList<>();
queue.add(v);
visited[v] = true;
// 维护顶点的前一个顶点
prevs[v] = v;
distance[v] = 0;
while (!queue.isEmpty()) {
int curr = queue.poll();
for (int w : g.adj(curr)) {
if (!visited[w]) {
queue.add(w);
visited[w] = true;
// 维护顶点的前一个顶点
prevs[w] = curr;
// 维护顶点 w 的距离为前一个顶点的距离 +1
distance[w] = distance[curr] + 1;
}
}
}
}
public boolean isConnected(int target) {
validateVertex(target);
return visited[target];
}
private void validateVertex(int v) {
if (v < 0 && v >= g.getV()) {
throw new IllegalArgumentException("顶点不合法,超出范围");
}
}
// O(n)
public List<Integer> path(int target) {
List<Integer> res = new ArrayList<>();
// 1. 如果源顶点到不了目标顶点,直接返回
if (!isConnected(target)) {
return res;
}
// 2. 根据 prevs 信息找到路径
while (target != source) {
res.add(target);
target = prevs[target];
}
res.add(source);
// 3. 翻转
Collections.reverse(res);
return res;
}
// 返回从 source 到 target 两点之间的距离
// O(n) -> O(1)
public int distance(int target) {
validateVertex(target);
return distance[target];
}
public static void main(String[] args) {
Graph g = new AdjSet("data/graph-bfs.txt");
SingleSourceShortestPath graphBFS = new SingleSourceShortestPath(g, 0);
System.out.println(graphBFS.path(6));
System.out.println(graphBFS.distance(6));
}
}
十七、无向有权图
public class WeightedAdjSet implements Graph {
private int V; // 顶点的个数
private int E; // 边的个数
private TreeMap<Integer, Integer>[] adj; // 邻接表
// 建图时间复杂度:O(E*logV)
public WeightedAdjSet(String fileName) {
try {
BufferedReader reader
= new BufferedReader(new FileReader(fileName));
String line = reader.readLine();
String[] arr = line.split(" ");
this.V = Integer.valueOf(arr[0]);
this.E = Integer.valueOf(arr[1]);
this.adj = new TreeMap[V];
for (int i = 0; i < V; i++) {
adj[i] = new TreeMap<>();
}
while ((line = reader.readLine()) != null) { // O(E)
arr = line.split(" ");
int a = Integer.valueOf(arr[0]);
validateVertex(a);
int b = Integer.valueOf(arr[1]);
validateVertex(b);
// 检测自环边
if (a == b) {
throw new RuntimeException("出现了自环边,错误");
}
// 检测平行边
if (adj[a].containsKey(b)) { // O(logV)
throw new RuntimeException("出现了平行边,错误");
}
int weight = Integer.valueOf(arr[2]);
adj[a].put(b, weight);
adj[b].put(a, weight);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void validateVertex(int v) {
if (v < 0 || v >= V) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
@Override
public int getV() {
return V;
}
@Override
public int getE() {
return E;
}
// 判断两个指定的顶点之间是否有边
// 时间复杂度:O(logV)
@Override
public boolean hasEdge(int v, int w) {
validateVertex(v);
validateVertex(w);
return adj[v].containsKey(w);
}
// 获取指定边的权重值
public int getWeight(int v, int w) {
if (hasEdge(v, w)) {
return adj[v].get(w);
}
return -1;
}
// 获取指定顶点所有相邻的顶点
// 时间复杂度:O(1)
@Override
public Collection<Integer> adj(int v) {
validateVertex(v);
return adj[v].keySet();
}
// 获取指定顶点的度数
@Override
public int degree(int v) {
return adj(v).size();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("顶点数 = %d,边数 = %d \n", V, E));
for (int v = 0; v < V; v++) {
sb.append(v + ": ");
Map<Integer, Integer> adjMap = adj[v];
for (Map.Entry<Integer, Integer> entry : adjMap.entrySet()) {
sb.append("(" + entry.getKey() + ", " + entry.getValue() + ")");
}
sb.append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
WeightedAdjSet adjList = new WeightedAdjSet("data/weighted-graph.txt");
System.out.println(adjList);
}
}
十八、有权图:最小生成树
十九、prim算法
WeightedEdge
public class WeightedEdge {
private int v;
private int w;
private int weight;
public WeightedEdge(int v, int w, int weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public int getV() {
return v;
}
public int getW() {
return w;
}
public int getWeight() {
return weight;
}
@Override
public String toString() {
return "WeightedEdge{" +
"v=" + v +
", w=" + w +
", weight=" + weight +
'}';
}
}
public class Prim {
private WeightedAdjSet g;
private List<WeightedEdge> result;
public Prim(WeightedAdjSet g) {
this.g = g;
this.result = new ArrayList<>();
// g 是连通图
CC1 cc = new CC1(g);
if (cc.getCcCount() > 1) return;
// Prim
boolean[] visited = new boolean[g.getV()];
// 选择顶点 0 作为切分的一部分
visited[0] = true;
// 进行 v - 1 次切分,每次切分得到最短的横切边
for (int i = 0; i < g.getV() - 1; i++) { // O(V)
WeightedEdge minEdge = new WeightedEdge(-1, -1, Integer.MAX_VALUE);
for (int v = 0; v < g.getV(); v++) { // O(V)
if (visited[v]) {
for (int w : g.adj(v)) { // O(E)
// v-w 横切边 // 找到最小的横切边
if (!visited[w] && g.getWeight(v, w) < minEdge.getWeight()) {
minEdge = new WeightedEdge(v, w, g.getWeight(v, w));
}
}
}
}
result.add(minEdge);
// 扩展切分
int v = minEdge.getV();
int w = minEdge.getW();
int newV = visited[v] ? w : v;
visited[newV] = true;
}
}
public List<WeightedEdge> getResult() {
return result;
}
public static void main(String[] args) {
WeightedAdjSet adjSet = new WeightedAdjSet("data/prim.txt");
Prim prim = new Prim(adjSet);
List<WeightedEdge> res = prim.getResult();
for (WeightedEdge edge : res) {
System.out.println(edge);
}
}
}
使用最小堆优化
public class WeightedEdge implements Comparable<WeightedEdge>{
private int v;
private int w;
private int weight;
public WeightedEdge(int v, int w, int weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public int getV() {
return v;
}
public int getW() {
return w;
}
public int getWeight() {
return weight;
}
@Override
public String toString() {
return "WeightedEdge{" +
"v=" + v +
", w=" + w +
", weight=" + weight +
'}';
}
@Override
public int compareTo(WeightedEdge o) {//权值从小到大排序
return weight - o.getWeight();
}
}
public class Prim1 {
private WeightedAdjSet g;
private List<WeightedEdge> result;
public Prim1(WeightedAdjSet g) {
this.g = g;
this.result = new ArrayList<>();
// g 是连通图
CC1 cc = new CC1(g);
if (cc.getCcCount() > 1) return;
// Prim
boolean[] visited = new boolean[g.getV()];
// 选择顶点 0 作为切分的一部分
visited[0] = true;
PriorityQueue<WeightedEdge> pq = new PriorityQueue<>();
for (int w : g.adj(0)) {
pq.add(new WeightedEdge(0, w, g.getWeight(0, w)));
}
while (!pq.isEmpty()) { // O(E)
// 1. 拿到最小横切边
WeightedEdge minEdge = pq.poll(); // O(logE)
if (visited[minEdge.getV()] && visited[minEdge.getW()]) {
// 不是横切边
continue;
}
// 2. 加入到最小生成树中
result.add(minEdge);
// 3. 扩展切分
int newV = visited[minEdge.getV()] ? minEdge.getW() : minEdge.getV();
visited[newV] = true;
// 将新的横切边放入到优先队列
for (int w : g.adj(newV)) {
if (!visited[w]) {
pq.add(new WeightedEdge(newV, w, g.getWeight(newV, w)));
}
}
}
}
public List<WeightedEdge> getResult() {
return result;
}
public static void main(String[] args) {
WeightedAdjSet adjSet = new WeightedAdjSet("data/prim.txt");
Prim1 prim = new Prim1(adjSet);
List<WeightedEdge> res = prim.getResult();
for (WeightedEdge edge : res) {
System.out.println(edge);
}
}
}
二十、有权图:最短路径 (Di jkstra算法)
总结:
public class Dijkstra {
private WeightedAdjSet g;
private int source;
private int[] distance;
private boolean[] visited;
public Dijkstra(WeightedAdjSet g, int source) {
this.g = g;
this.source = source;
distance = new int[g.getV()];
Arrays.fill(distance, Integer.MAX_VALUE);
distance[source] = 0;
visited = new boolean[g.getV()];
while (true) { // O(V)
// 1. 找到当前没有访问的最短路径节点
int curDis = Integer.MAX_VALUE;
int curr = -1;
for (int v = 0; v < g.getV(); v++) { // O(V)
if (!visited[v] && distance[v] < curDis) {
curDis = distance[v];
curr = v;
}
}
if (curr == -1) break;
// 2. 确认这个节点的最短路径就是当前大小
visited[curr] = true;
// 3. 根据这个节点的最短路径大小,更新其他节点的路径长度
for (int w : g.adj(curr)) { // O(E)
if (!visited[w]) {
if (distance[curr] + g.getWeight(curr, w) < distance[w]) {
distance[w] = distance[curr] + g.getWeight(curr, w);
}
}
}
}
}
public int minDistanceTo(int v) {
validateVertex(v);
return distance[v];
}
public void validateVertex(int v) {
if (v < 0 || v >= g.getV()) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
public static void main(String[] args) {
WeightedAdjSet g = new WeightedAdjSet("data/Dijkstra.txt");
Dijkstra dijkstra = new Dijkstra(g, 0);
System.out.println(dijkstra.minDistanceTo(1));
}
}
优化
public class Dijkstra1 {
private WeightedAdjSet g;
private int source;
private int[] distance;
private boolean[] visited;
private class Pair implements Comparable<Pair> {
int v;
int dis;
public Pair(int v, int dis) {
this.v = v;
this.dis = dis;
}
@Override
public int compareTo(Pair o) {
return dis - o.dis;
}
}
public Dijkstra1(WeightedAdjSet g, int source) {
this.g = g;
this.source = source;
distance = new int[g.getV()];
Arrays.fill(distance, Integer.MAX_VALUE);
distance[source] = 0;
visited = new boolean[g.getV()];
PriorityQueue<Pair> pq = new PriorityQueue<>();
pq.add(new Pair(source, 0));
while (!pq.isEmpty()) { // O(V)
// 1. 找到当前没有访问的最短路径节点
int curr = pq.poll().v; // O(logV)
if (visited[curr]) continue;
// 2. 确认这个节点的最短路径就是当前大小
visited[curr] = true;
// 3. 根据这个节点的最短路径大小,更新其他节点的路径长度
for (int w : g.adj(curr)) { // O(E)
if (!visited[w]) {
if (distance[curr] + g.getWeight(curr, w) < distance[w]) {
distance[w] = distance[curr] + g.getWeight(curr, w);
pq.add(new Pair(w, distance[w]));
}
}
}
}
}
public int minDistanceTo(int v) {
validateVertex(v);
return distance[v];
}
public void validateVertex(int v) {
if (v < 0 || v >= g.getV()) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
public static void main(String[] args) {
WeightedAdjSet g = new WeightedAdjSet("data/Dijkstra.txt");
Dijkstra1 dijkstra = new Dijkstra1(g, 0);
System.out.println(dijkstra.minDistanceTo(1));
}
}
拿到路径
public class Dijkstra2 {
private WeightedAdjSet g;
private int source;
private int[] distance;
private boolean[] visited;
private int[] prevs;
private class Pair implements Comparable<Pair> {
int v;
int dis;
public Pair(int v, int dis) {
this.v = v;
this.dis = dis;
}
@Override
public int compareTo(Pair o) {
return dis - o.dis;
}
}
public Dijkstra2(WeightedAdjSet g, int source) {
this.g = g;
this.source = source;
distance = new int[g.getV()];
Arrays.fill(distance, Integer.MAX_VALUE);
distance[source] = 0;
visited = new boolean[g.getV()];
prevs = new int[g.getV()];
Arrays.fill(prevs, -1);
PriorityQueue<Pair> pq = new PriorityQueue<>();
pq.add(new Pair(source, 0));
while (!pq.isEmpty()) { // O(V)
// 1. 找到当前没有访问的最短路径节点
int curr = pq.poll().v; // O(logV)
if (visited[curr]) continue;
// 2. 确认这个节点的最短路径就是当前大小
visited[curr] = true;
// 3. 根据这个节点的最短路径大小,更新其他节点的路径长度
for (int w : g.adj(curr)) { // O(E)
if (!visited[w]) {
if (distance[curr] + g.getWeight(curr, w) < distance[w]) {
distance[w] = distance[curr] + g.getWeight(curr, w);
pq.add(new Pair(w, distance[w]));
prevs[w] = curr;
}
}
}
}
}
public int minDistanceTo(int v) {
validateVertex(v);
return distance[v];
}
public void validateVertex(int v) {
if (v < 0 || v >= g.getV()) {
throw new IllegalArgumentException(String.format("顶点 %d 不合格", v));
}
}
public boolean isConnected(int v) {
validateVertex(v);
return visited[v];
}
// 返回最短路径中所有的顶点
public Collection<Integer> path(int target) {
List<Integer> res = new ArrayList<>();
// 1. 如果源顶点到不了目标顶点,直接返回
if (!isConnected(target)) {
return res;
}
// 2. 根据 prevs 信息找到路径
while (target != source) {
res.add(target);
target = prevs[target];
}
res.add(source);
// 3. 翻转
Collections.reverse(res);
return res;
}
public static void main(String[] args) {
WeightedAdjSet g = new WeightedAdjSet("data/Dijkstra.txt");
Dijkstra2 dijkstra = new Dijkstra2(g, 0);
System.out.println(dijkstra.minDistanceTo(1));
System.out.println(dijkstra.path(1));
}
}