图的结构和算法很常用,基于《算法4》中的描述,总结手撕了一波;
包含以下图的基础算法:
无向图:深度优先遍历、广度优先遍历、连通分量、最短路径;
有向图:深度优先遍历、广度优先遍历、可达性分析、最短路径;
一些概念
图:顶点和边构成;
自环:一条连接一个顶点和其自身的边;
平行边:连接同义对顶点的两条边;
连通分量(无向图):互相连通的顶点;
可达性(有向图):一个顶点可以到达另外一个顶点;
最短路径:一个顶点连通(到达)另外一个顶点最少需要走过的顶点集合;
二分图:能够将所有节点分为两部分的图,其中每条边所连接的两个顶点都分别属于不同的部分;
邻接矩阵和邻接表
图的结构,可使用邻接矩阵和邻接表,两者区别:
邻接矩阵:二维数组标识顶点之间的连接,数组的长宽都为顶点数,所以存储n个节点,需要n*n的内存;
邻接表:数组+集合,类似HashMap的数据+链表结构;
邻接表节省空间,适合存储顶点数量大的图,比如100w级别+的顶点,支持自环和平行边;这也是《算法4》中推荐使用邻接表数据结构的原因。
demo中用到的无向图结构:
demo中用到的有向图结构:
上代码:
抽象图类 Graph.java
/**
* @Author: ltx
* @Description:
*/
public abstract class Graph {
public int v; //顶点数量
public int e; //边数量
public List<Integer>[] adj; //邻接表
/**
* 构造方法-初始化图
* @param v
*/
public Graph(int v) {
//初始化顶点数
this.v = v;
//初始化边数
this.e = 0;
//初始化邻接表
this.adj = new List[v];
for (int i = 0; i < v; i++) {
adj[i] = new ArrayList<>();
}
}
/**
* 添加节点
* @param v
* @param w
*/
public abstract void addEdge(int v, int w);
/**
* 顶点v的度数
* 度数:顶点的边
*
* @param v
* @return
*/
public int degree(int v) {
int count = adj[v].size();
System.out.printf("顶点 %d 的出度数: %d\n", v, count);
return count;
}
/**
* 最大度数
*
* @return
*/
public int maxDegree() {
int v = 0;//顶点
int max = 0;//度数
for (int i = 0; i < adj.length; i++) {
if (adj[i].size() > max) {
v = i;
max = adj[i].size();
}
}
System.out.printf("度数最多顶点: %d, 度数: %d\n", v, max);
return max;
}
/**
* 打印邻接表结构
*/
public void show() {
System.out.printf("%d 个顶点, %d 条边\n", v, e);
for (int i = 0; i < v; i++) {
System.out.println(i + ": " + adj[i]);
}
}
/**
* 平均度数
* 无向图
* 有向图-出度数
*
* @return
*/
public abstract String avgDegree();
/**
* 自环个数
*
* @return
*/
public abstract int numberOfSelfLoops();
}
无向图类 UndigraphGraph.java
/**
* 无向图
*/
public class UndigraphGraph extends Graph {
public UndigraphGraph(int v) {
super(v);
}
/**
* 添加边
*
* @param v 起点
* @param w 终点
*/
@Override
public void addEdge(int v, int w) {
//注意-当v==w时,形成自环
adj[v].add(w);
adj[w].add(v);
e++;
}
/**
* 所有顶点平均度数
*
* @return
*/
@Override
public String avgDegree() {
//顶点数/边数 * 2
BigDecimal bdV = new BigDecimal(v);
BigDecimal bdE = new BigDecimal(e);
BigDecimal avg = bdV.divide(bdE, 2, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(2));
System.out.printf("所有顶点平均度数: %s\n", avg);
return avg.toString();
}
/**
* 自环个数(自环: 顶点自己连接自己)
*
* @return
*/
@Override
public int numberOfSelfLoops() {
int count = 0;
for (int i = 0; i < v; i++) {
for (Integer w : adj[i]) {
if (w.equals(i)) {
count++;
}
}
}
System.out.printf("自环个数: %d\n", count / 2);
//每条边会被记录2次
return count / 2;
}
/**
* 初始化无向图
* @return
*/
public static UndigraphGraph init() {
UndigraphGraph graph = new UndigraphGraph(13);
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(0, 5);
graph.addEdge(0, 6);
graph.addEdge(3, 4);
graph.addEdge(3, 5);
graph.addEdge(4, 5);
graph.addEdge(4, 6);
graph.addEdge(7, 8);
graph.addEdge(9, 10);
graph.addEdge(9, 11);
graph.addEdge(9, 12);
graph.addEdge(11, 12);
//平行边(多条边连接两个顶点)
// graph.addEdge(11, 12);
//自环(自己连接自己)
// graph.addEdge(5, 5);
return graph;
}
public static void main(String[] args) {
System.out.println("################无向图################");
//初始化一个图(来自《算法4》的无向图demo)
Graph graph = UndigraphGraph.init();
System.out.println("-------------------邻接表结构-------------------");
graph.show();
//图自环个数
graph.numberOfSelfLoops();
//平均度数
graph.avgDegree();
//顶点度数
graph.degree(3);
//最大度数顶点
graph.maxDegree();
//深度优先遍历
System.out.println("-------------------深度优先遍历-------------------");
DepthFirstSearch dfs = new DepthFirstSearch(graph);
dfs.startDfs(0);
System.out.println("遍历顺序: " + dfs.searchLs);
//连通分量
dfs.weight();
System.out.printf("连通分量: %d 个\n", dfs.connectedWeightCount);
dfs.printWeight();
System.out.printf("%d和%d是否连通: %s\n", 0, 1, dfs.contented(0, 1));
System.out.printf("%d和%d是否连通: %s\n", 0, 7, dfs.contented(0, 7));
//广度优先遍历
System.out.println("-------------------广度优先遍历-------------------");
BreadthFirstSearch bfs = new BreadthFirstSearch(graph);
bfs.startBfs(0);
System.out.println("遍历顺序: " + bfs.searchLs);
//利用广度优先遍历得到最短路径
System.out.println("0 -> 4, 最短路径之一:");
System.out.println(bfs.pathTo(4));
//计算连通分量
bfs.weight();
System.out.printf("连通分量: %d 个\n", bfs.connectedWeightCount);
bfs.printWeight();
System.out.printf("%d和%d是否连通: %s\n", 0, 1, dfs.contented(0, 1));
System.out.printf("%d和%d是否连通: %s\n", 0, 7, dfs.contented(0, 7));
}
}
有向图类 DirectedGraph.java
/**
* 有向图
*/
public class DirectedGraph extends Graph {
public DirectedGraph(int v) {
super(v);
}
/**
* 添加边
*
* @param v 起点
* @param w 终点
*/
@Override
public void addEdge(int v, int w) {
//注意-当v==w时,形成自环
adj[v].add(w);
//有向图和无向图的区别
// adj[w].add(v);
e++;
}
/**
* 所有顶点平均度数
*
* @return
*/
@Override
public String avgDegree() {
BigDecimal bdV = new BigDecimal(v);
BigDecimal bdE = new BigDecimal(e);
//顶点数/边数
BigDecimal avg = bdV.divide(bdE, 2, BigDecimal.ROUND_HALF_UP);
System.out.printf("所有顶点平均度数: %s\n", avg);
return avg.toString();
}
/**
* 自环个数(自环: 顶点自己连接自己)
*
* @return
*/
@Override
public int numberOfSelfLoops() {
int count = 0;
for (int i = 0; i < v; i++) {
for (Integer w : adj[i]) {
if (w.equals(i)) {
count++;
}
}
}
System.out.printf("自环个数: %d\n", count);
return count;
}
/**
* 反转有向图,可以方便的得到指向顶点的边
* 在图的强连通分量里面有用
*
* @return
*/
public DirectedGraph reverse() {
DirectedGraph R = new DirectedGraph(v);
for (int i = 0; i < v; i++) {
for (Integer w : adj[i]) {
R.addEdge(w, i);
}
}
return R;
}
/**
* 初始化有向图
*
* @return
*/
public static DirectedGraph init() {
DirectedGraph graph = new DirectedGraph(13);
graph.addEdge(0, 1);
graph.addEdge(0, 5);
graph.addEdge(2, 0);
graph.addEdge(2, 3);
graph.addEdge(3, 2);
graph.addEdge(3, 5);
graph.addEdge(4, 2);
graph.addEdge(4, 3);
graph.addEdge(5, 4);
graph.addEdge(6, 0);
graph.addEdge(6, 4);
graph.addEdge(6, 9);
graph.addEdge(7, 6);
graph.addEdge(7, 8);
graph.addEdge(8, 7);
graph.addEdge(8, 9);
graph.addEdge(9, 10);
graph.addEdge(9, 11);
graph.addEdge(10, 12);
graph.addEdge(11, 4);
graph.addEdge(11, 12);
graph.addEdge(12, 9);
//平行边(多条边连接两个顶点)
// graph.addEdge(12, 9);
//自环(自己连接自己)
// graph.addEdge(5, 5);
return graph;
}
public static void main(String[] args) {
System.out.println("################有向图################");
//初始化一个图(来自《算法4》的有向图demo)
DirectedGraph graph = DirectedGraph.init();
System.out.println("-------------------邻接表结构-------------------");
graph.show();
//反向图
// Graph r = graph.reverse();
// System.out.println("反向图:");
// r.show();
//图自环个数
graph.numberOfSelfLoops();
//平均度数
graph.avgDegree();
//顶点度数
graph.degree(3);
//最大度数顶点
graph.maxDegree();
//深度优先遍历
System.out.println("-------------------深度优先遍历-------------------");
DepthFirstSearch dfs = new DepthFirstSearch(graph);
dfs.startDfs(0);
System.out.println("遍历顺序: " + dfs.searchLs);
System.out.println("---可达性分析---");
//可达性分析
dfs.accessibility();
// //广度优先遍历
System.out.println("-------------------广度优先遍历-------------------");
BreadthFirstSearch bfs = new BreadthFirstSearch(graph);
bfs.startBfs(0);
System.out.println("遍历顺序: " + bfs.searchLs);
//利用广度优先遍历得到最短路径
System.out.println("0 -> 2, 最短路径之一:");
System.out.println(bfs.pathTo(2));
System.out.println("---可达性分析---");
//可达性分析
bfs.accessibility();
}
}
深度优先搜索类 DepthFirstSearch.java
/**
* 深度优先搜索
*/
public class DepthFirstSearch {
private int s;//起点
private Graph graph;
public boolean marked[];//标记已经遍历过的顶点
public int count;//和v连通的顶点总数
public List<Integer> searchLs; //遍历顶点顺序, 和v顶点连通的顶点集合
public int connectedWeightCount; //连通分量总数
public int[] id;//连通分量
/**
* 初始化深度优先遍历
*
* @param graph 无向图
*/
public DepthFirstSearch(Graph graph) {
this.graph = graph;
//初始化遍历顶点list
searchLs = new ArrayList<>();
//初始化marked数组
this.marked = new boolean[graph.v];
id = new int[graph.v];
// System.out.printf("起点: %d, ", s);
//深度优先遍历
// dfs(graph, s);
}
/**
* 顶点v是否和s连通
*
* @param v
* @return
*/
public boolean marked(int v) {
return marked[v];
}
/**
* 发起-深度优先遍历
*
* @param s
*/
public void startDfs(int s) {
System.out.printf("起点: %d, ", s);
//设置起点
this.s = s;
dfs(s);
}
/**
* 深度优先遍历
*
* @param v 起点
*/
public void dfs(int v) {
//记录遍历顺序
searchLs.add(v);
//标记已走
marked[v] = true;
//和v连通的顶点总数
count++;
//标记连通分量
id[v] = connectedWeightCount;
for (int w : graph.adj[v]) {
if (!marked[w]) {
dfs(w);
}
}
}
/**
* 连通分量
*/
public void weight() {
//初始化一下marked
for (int i = 0; i < marked.length; i++) {
marked[i] = false;
}
for (int i = 0; i < id.length; i++) {
if (!marked[i]) {
//深度优先遍历
dfs(i);
connectedWeightCount++;
}
}
}
/**
* 可达性分析-有向图
*/
public void accessibility() {
for (int i = 0; i < id.length; i++) {
//初始化一下marked
for (int j = 0; j < marked.length; j++) {
marked[j] = false;
id[j] = -1;
}
//深度优先遍历-可达性分析
dfsAccessibility(i, i);
List<Integer> ls = new ArrayList<>();
for (int j = 0; j < id.length; j++) {
if (id[j] == i) {
ls.add(j);
}
}
System.out.printf("%d 可达: %s\n", i, ls);
}
}
/**
* 深度优先遍历-可达性分析-有向图
*
* @param v 递归点
* @param s 起点
*/
public void dfsAccessibility(int v, int s) {
marked[v] = true;
//标记连通分量
id[v] = s;
for (int w : graph.adj[v]) {
if (!marked[w]) {
dfsAccessibility(w, s);
}
}
}
/**
* 打印连通分量
*/
public void printWeight() {
for (int temp = 0; temp < connectedWeightCount; temp++) {
System.out.printf("第 %d 个: ", temp + 1);
for (int i = 0; i < id.length; i++) {
if (this.id[i] == temp) {
System.out.printf("%d, ", i);
}
}
System.out.println();
}
}
/**
* 两个顶点是否连通
*
* @param v 顶点
* @param w 顶点
* @return
*/
public boolean contented(int v, int w) {
return id[v] == id[w];
}
}
广度优先搜索类 BreadthFirstSearch.java
/**
* 广度优先搜索
*/
public class BreadthFirstSearch {
private int s;//起点
private Graph graph;
public boolean marked[];//标记已经遍历过的顶点
public int count;//和起点s连通的顶点总数
public List<Integer> searchLs; //遍历顶点顺序, 和v顶点连通的顶点集合
public Integer edgTo[];//路径
public int[] id;//连通分量
public int connectedWeightCount; //连通分量总数
/**
* 连通分量
*/
public void weight() {
//初始化一下marked
for (int i = 0; i < marked.length; i++) {
marked[i] = false;
}
for (int i = 0; i < id.length; i++) {
if (!marked[i]) {
//广度优先遍历
bfs(i);
connectedWeightCount++;
}
}
}
/**
* 打印连通分量
*/
public void printWeight() {
int temp = --connectedWeightCount;
while (temp >= 0) {
for (int i = 0; i < id.length; i++) {
if (this.id[i] == temp) {
System.out.printf("%d, ", i);
}
}
System.out.println();
temp--;
}
}
/**
* 两个顶点是否连通
*
* @param v 顶点
* @param w 顶点
* @return
*/
public boolean contented(int v, int w) {
return id[v] == id[w];
}
/**
* 广度优先遍历-初始化
*
* @param graph 无向图
*/
public BreadthFirstSearch(Graph graph) {
this.graph = graph;
//初始化遍历顶点list
searchLs = new ArrayList<>();
//初始化marked数组
marked = new boolean[graph.v];
edgTo = new Integer[graph.v];
id = new int[graph.v];
}
/**
* 发起-广度优先遍历
*
* @param s
*/
public void startBfs(int s) {
System.out.printf("起点: %d, ", s);
//设置起点
this.s = s;
bfs(s);
}
/**
* 广度优先遍历
*
* @param v 起点
*/
public void bfs(int v) {
//利用队列FIFO特性处理
Queue<Integer> queue = new LinkedList<>();
searchLs.add(v);//起点加入路径
marked[v] = true;//标记起点
count++;
//标记连通分量
id[v] = connectedWeightCount;
queue.add(v);//起点加入队列
while (!queue.isEmpty()) {
Integer temp = queue.remove();
for (int w : graph.adj[temp]) {
if (!marked[w]) {
edgTo[w] = temp;
searchLs.add(w);
marked[w] = true;
count++;
//标记连通分量
id[w] = connectedWeightCount;
queue.add(w);
}
}
}
}
/**
* 可达性分析-有向图
*/
public void accessibility() {
for (int i = 0; i < id.length; i++) {
//初始化一下marked
for (int j = 0; j < marked.length; j++) {
marked[j] = false;
id[j] = -1;
}
//广度优先遍历-可达性分析
bfsAccessibility(i, i);
List<Integer> ls = new ArrayList<>();
for (int j = 0; j < id.length; j++) {
if (id[j] == i) {
ls.add(j);
}
}
System.out.printf("%d 可达: %s\n", i, ls);
}
}
/**
* 广度优先遍历-可达性分析-有向图
*
* @param v 递归点
* @param s 起点
*/
public void bfsAccessibility(int v, int s) {
//利用队列FIFO特性处理
Queue<Integer> queue = new LinkedList<>();
marked[v] = true;//标记起点
id[v] = s;
queue.add(v);//起点加入队列
while (!queue.isEmpty()) {
Integer temp = queue.remove();
for (int w : graph.adj[temp]) {
if (!marked[w]) {
marked[w] = true;
//标记连通分量
id[w] = s;
queue.add(w);
}
}
}
}
/**
* 顶点v是否和s连通
*
* @param v
* @return
*/
public boolean marked(int v) {
return marked[v];
}
/**
* 起点s->顶点v的最短路径
*
* @param v
* @return
*/
public List<Integer> pathTo(int v) {
System.out.printf("%d -> %d, ", s, v);
//先判断是否连通
if (!marked(v)) {
return null;
}
List<Integer> path = new ArrayList<>();
Stack<Integer> st = new Stack<>();
Integer temp = edgTo[v];
//放终点
st.push(v);
while (temp != s) {
//放中间的路径
st.push(temp);
temp = edgTo[temp];
}
//放起点
st.push(s);
//栈逆序到list,构成路径
while (!st.isEmpty()) {
path.add(st.pop());
}
return path;
}
}
无向图main执行结果:
有向图main执行结果: