一、圖的數據結構及表示法

如上圖,由一堆"點"與一堆"邊"構成的數據結構 ,就稱為圖,其中邊上可以有方向(稱為有向圖),也可以無方向(稱為無向圖)。邊上還可以有所謂的權重值。
算法書上,圖的表示方法一般有“鄰接矩陣”等,這里我們用左程雲介紹的一種相對更容易理解的表示法:
圖:
import java.util.List;
public class Graph {
//點集
public List<Node> nodes;
//邊集
public List<Edge> edges;
public Graph(List<Node> nodes, List<Edge> edges) {
this.nodes = nodes;
this.edges = edges;
}
}
節點:
import java.util.ArrayList;
import java.util.List;
public class Node {
public int value;
//入度(即:有幾個節點連到自己)
public int in;
//出度(即:對外連到幾個其它節點)
public int out;
//對外連了哪些相鄰節點
public List<Node> nexts;
//有幾條從自已出發的邊
public List<Edge> edges;
public Node(int val) {
this.value = val;
this.in = 0;
this.out = 0;
this.nexts = new ArrayList<>();
this.edges = new ArrayList<>();
}
}
注:如果為了調試方便輸出,可以加上toString()
// @Override
// public String toString() {
// return "Node{" +
// "value=" + value +
// ", in=" + in +
// ", out=" + out +
// ", nexts=" + nexts +
// ", edges=" + edges +
// '}';
// }
@Override
public String toString() {
return value + "";
}
邊:
public class Edge {
public Node from;
public Node to;
public int weight = 0;
public Edge(int weight, Node from, Node to) {
this.weight = weight;
this.from = from;
this.to = to;
}
}
最開始那張圖,就可以類似下面的代碼來構建:
public Graph initGraph() {
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
Node n4 = new Node(4);
Node n5 = new Node(5);
Node n6 = new Node(6);
Node n7 = new Node(7);
n1.in = 1;
n1.out = 1;
n1.nexts.add(n4);
n1.edges.add(new Edge(0, n1, n4));
n6.in = 1;
n2.in = 1;
n2.out = 2;
n2.nexts.add(n1);
n2.nexts.add(n6);
n2.edges.add(new Edge(0, n2, n1));
n2.edges.add(new Edge(0, n2, n6));
n3.in = 1;
n3.out = 1;
n3.nexts.add(n2);
n3.edges.add(new Edge(0, n3, n2));
n5.in = 1;
n5.out = 1;
n5.nexts.add(n7);
n5.edges.add(new Edge(0, n5, n7));
n4.in = 1;
n4.out = 2;
n4.nexts.add(n3);
n4.nexts.add(n5);
n4.edges.add(new Edge(0, n4, n3));
n4.edges.add(new Edge(0, n4, n5));
List<Node> nodes = new ArrayList<>();
nodes.add(n1);
nodes.add(n2);
nodes.add(n3);
nodes.add(n4);
nodes.add(n5);
nodes.add(n6);
nodes.add(n7);
List<Edge> edges = new ArrayList<>();
edges.addAll(n1.edges);
edges.addAll(n2.edges);
edges.addAll(n3.edges);
edges.addAll(n4.edges);
edges.addAll(n5.edges);
edges.addAll(n6.edges);
edges.addAll(n7.edges);
Graph g = new Graph(nodes, edges);
return g;
}
二、廣度優先遍歷
思路:從源節點開始(注:假設源節點不是有進無出的終止節點),依次遍歷自己相鄰的節點,這里要注意下,如果圖中有環,不要形成死循環。
/**
* breadth-first search
* @param g
*/
void bfs(Graph g) {
if (g == null || g.nodes == null || g.nodes.size() == 0) {
return;
}
Node n = g.nodes.get(0);
Queue<Node> queue = new LinkedList<>();
//用於輔助判斷,節點是否已經遍歷過,防止有環情況下,形成死循環
Set<Node> set = new HashSet<>();
queue.add(n);
set.add(n);
while (!queue.isEmpty()) {
Node curr = queue.poll();
System.out.printf(curr.value + " ");
for (Node next : curr.nexts) {
if (!set.contains(next)) {
queue.add(next);
set.add(next);
}
}
}
}
以本文最開始的圖為例,輸出為:1 4 3 5 2 7 6
三、深度優先遍歷
思路:與廣度優先不同,深度優先要沿着某個節點,盡可能向縱深走,而非優先看自身相鄰節點,這里要換成Stack,而非Queue,詳見下面的代碼
/**
* depth-first search 深度優先遍歷
*
* @param g
*/
void dfs(Graph g) {
if (g == null || g.nodes == null || g.nodes.size() == 0) {
return;
}
Node n = g.nodes.get(0);
Stack<Node> stack = new Stack<>();
//用於輔助判斷,節點是否已經遍歷過,防止有環情況下,形成死循環
Set<Node> set = new HashSet<>();
stack.add(n);
set.add(n);
System.out.printf(n.value + " ");
while (!stack.isEmpty()) {
//先把自己彈出來
Node curr = stack.pop();
for (Node next : curr.nexts) {
if (!set.contains(next)) {
//再把自己及下1個節點壓進去
//由於stack是先進后出,
//所以彈出的順序就變成了 下一個節點(即:更深層的)先彈出
//從而達到了深度優先的效果
stack.add(curr);
stack.add(next);
set.add(next);
System.out.printf(next.value + " ");
break;
}
}
}
}
輸出結果:1 4 3 2 6 5 7
四、帶權重的遍歷

比如上圖,如果邊上有權重值,假設權重值越大,優先級越高,那么只要把上述的代碼略做調整,在入隊/入棧時,按權重排下序即可
帶權重的廣度優先遍歷:
/**
* 帶權重的breadth-first search
*
* @param g
*/
void bfs2(Graph g) {
if (g == null || g.nodes == null || g.nodes.size() == 0) {
return;
}
Node n = g.nodes.get(0);
Queue<Node> queue = new LinkedList<>();
//用於輔助判斷,節點是否已經遍歷過,防止有環情況下,形成死循環
Set<Node> set = new HashSet<>();
queue.add(n);
set.add(n);
while (!queue.isEmpty()) {
Node curr = queue.poll();
System.out.printf(curr.value + " ");
//根據邊上的權重值排序
curr.edges.sort((o1, o2) -> o1.weight < o2.weight ? 1 : -1);
for (Edge next : curr.edges) {
if (!set.contains(next.to)) {
queue.add(next.to);
set.add(next.to);
}
}
}
}
輸出:1 4 5 3 7 2 6
帶權重的深度優先遍歷:
/**
* 帶權重的深度優先遍歷(菩提樹下的楊過 yjmyzz.cnblogs.com)
* @param g
*/
void dfs2(Graph g) {
if (g == null || g.nodes == null || g.nodes.size() == 0) {
return;
}
Node n = g.nodes.get(0);
Stack<Node> stack = new Stack<>();
Set<Node> set = new HashSet<>();
stack.add(n);
set.add(n);
System.out.printf(n.value + " ");
while (!stack.isEmpty()) {
Node curr = stack.pop();
//根據邊上的權重值排序
curr.edges.sort((o1, o2) -> o1.weight < o2.weight ? 1 : -1);
for (Edge next : curr.edges) {
if (!set.contains(next.to)) {
stack.add(curr);
stack.add(next.to);
set.add(next.to);
System.out.printf(next.to.value + " ");
break;
}
}
}
}
輸出:1 4 5 7 3 2 6
五、無向圖的處理
對於無向圖而言,可以看成是有向圖的特例:

如上圖,節點1與節點2構成了1張最簡單的圖,從1可以走到2,從2也可以走到1,可以理解為1與2之間,各有一條指向對方的邊,用代碼表示的話,類似下面這樣:
public Graph buildGraph() {
Node n1 = new Node(1);
Node n2 = new Node(2);
n1.out = 2;
n1.in = 2;
n1.nexts.add(n2);
n1.edges.add(new Edge(0, n1, n2));
n2.out = 2;
n2.in = 2;
n2.nexts.add(n1);
n2.edges.add(new Edge(0, n2, n1));
List<Node> nodes = new ArrayList<>();
nodes.add(n1);
nodes.add(n2);
List<Edge> edges = new ArrayList<>();
edges.addAll(n1.edges);
edges.addAll(n2.edges);
Graph g = new Graph(nodes, edges);
return g;
}
