一、圖的數據結構及表示法
如上圖,由一堆"點"與一堆"邊"構成的數據結構 ,就稱為圖,其中邊上可以有方向(稱為有向圖),也可以無方向(稱為無向圖)。邊上還可以有所謂的權重值。
算法書上,圖的表示方法一般有“鄰接矩陣”等,這里我們用左程雲介紹的一種相對更容易理解的表示法:
圖:
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; }