1. 程式人生 > >演算法——圖之無向圖

演算法——圖之無向圖

圖的概念

圖是演算法中是樹的拓展,樹是從上向下的資料結構,結點都有一個父結點(根結點除外),從上向下排列。而圖沒有了父子結點的概念,圖中的結點都是平等關係,結果更加複雜。

圖的分類

圖可以分為無向圖(簡單連線),有向圖(連線有方向),加權圖(連線帶權值),加權有向圖(連線既有方向又有權值)。

這篇討論無向圖。

無向圖的表示方法:

1.鄰接矩陣
2.邊的陣列
3.鄰接表陣列

1.鄰接矩陣

我們可以使用一個V*V的布林矩陣graph來表示圖。當頂點v和頂點w之間有邊相連時,則graph[v][w]和graph[w][v]為true,否則為false。

但是這種方法需要佔用的空間比較大,因為稀疏圖更常見,這就導致了很多空間的浪費。V*V的矩陣很多時候我們是不能接受的。

2.邊的陣列

我們可以使用一個數組來存放所有的邊,這樣的話陣列的大小僅有E。但是因為我們的操作總是需要訪問某個頂點的相鄰節點,對於這種資料型別,要訪問相鄰節點的話必須遍歷整個陣列,造成效率的低下,所以我們在這裡也不使用這個資料結構。

3.鄰接表陣列

我們使用一個連結串列陣列來表示,陣列中每個元素都是連結串列表頭,連結串列中存放對應下標的節點所連線的邊。

這種資料結構使用的空間為V+E。並且可以相當方便的獲取相鄰節點。

如圖:


實現如下:

import java.util.ArrayList;
import java.util.List;

public class Graph {
	private List<Integer>[] adj; // 鄰接表
	private int V;	// 頂點數目
	private int E;	// 邊的數目
	
	public Graph(int V) {
		this.V = V;
		adj = (List<Integer>[])new List[V];
		E = 0;
		for (int i = 0; i < V; i++) {
			adj[i] = new ArrayList<Integer>();
		}
	}
	
	public void addEdge(int v, int w) {
		adj[v].add(adj[v].size(), w);
		adj[w].add(adj[w].size(), v);
		E++;
	}
	
	public List<Integer> adj(int v) {
		return adj[v];
	}
	
	public int V() {
		return V;
	}
	
	public int E() {
		return E;
	}
	
	public String toString() {
		String s = V + " 個頂點, " + E + " 條邊\n";
		for (int i = 0; i < V; i++) {
			s += i + ": ";
			for (Integer node : adj(i)) {
				s += node + " ";
			}
			s += "\n";
		}
		return s;
	}

}

到此為止,我們已經完成了圖的表示。

有了表示,我們就需要使用圖完成一些簡單的應用。

例如,圖的搜尋,圖的連通分量,圖是否有環等等。

首先我們來實現圖的搜尋。

我們在這裡實現一個模板。並不實際進行搜尋。

目標:給定一個起點,在圖中進行搜尋。

方案:1.深度優先搜尋2.廣度優先搜尋。

深搜

原理:

這裡打個比喻,搜尋圖中所有的節點,就像走迷宮一樣,需要探索迷宮中所有的通道。探索迷宮所有的通道,我們需要什麼呢?

1.我們需要選擇一條沒有標記的路,並且一邊走一遍鋪上一條繩子。

2.標記走過的路。

3.當走到一個標記的地方時,我們需要回退,根據繩子回退到上一個地方。

4.當回退的地方沒有可以走的路了,就要繼續回退。

也就是說,首先我們需要一直走下去,但是我們一邊走就要一邊做標記,如果走不下去了,就回退,回退到沒有被標記的的路。迴圈往復,我們就能探索整個圖了。

實現:

import java.util.Stack;

public class DepthFirstSearch {
	private boolean[] isMarked;
	private int begin;
	private int count;
	private Integer[] edgeTo; 
	
	public DepthFirstSearch(Graph g, int begin) {
		isMarked = new boolean[g.V()];
		edgeTo = new Integer[g.V()];
		count = 0;
		this.begin = begin;
		dfs(g, begin);
	}
	
	public void dfs(Graph g, int begin) {
		isMarked[begin] = true;
		for (Integer i : g.adj(begin)) {
			if (!isMarked[i]) {
				edgeTo[i] = begin;
				count++;
				dfs(g, i);
			}
		}
	}
	
	
	public boolean hasPath(int v) {
		return isMarked[v];
	}
	
	public int count() {
		return count;
	}
	
	public String pathTo(int v) {
		if (!hasPath(v)) return "";
		Stack<Integer> stack = new Stack<>();
		stack.push(v);
		for (int i = v; i != begin; i = edgeTo[i]) {
			stack.push(edgeTo[i]);
		}
		
		return stack.toString();
	}
	
	
}
我們需要一個數組來標記某個節點是否已經走過了,如果走過了,我們就不會再走了。
並且我們有一個數組去儲存是從哪個節點到達當前節點。這樣,我們往回追朔的時候,就可以找到一條路徑了。

這是一個模板,並沒有具體的搜尋某個節點,而是將所有節點都搜尋了一遍,在實際過程中,我們可以判斷節點是否找到,找到就停止了。

對於無向圖來說,深搜雖然可以找到一條從v到w的路徑,但是這條路徑是否是最優的並不是可靠的,往往都不是。

如果我們希望找到一條最短的路徑,我們就應該使用廣搜。

廣搜

原理:

廣搜並不是先一條路走到黑,而是慢慢的根據距離進行搜尋。例如,一開始先根據距離是1進行搜尋,先搜尋所有距離為1的地方。如果沒找到,再搜尋距離為2的地方。以此類推。

如果說深搜是一個人在迷宮中搜索,那麼廣搜就是一組人在朝著各個方向進行搜尋。當然不是效率比較高的意思,只是比喻而已。

實現:

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

public class BreadthFirstSearch {
	
	private boolean[] isMarked;
	private Integer[] edgeTo;
	private int begin;
	private int count; // 多少個點連通
	
	public BreadthFirstSearch(Graph g, int begin) {
		isMarked = new boolean[g.V()];
		edgeTo = new Integer[g.V()];
		this.begin = begin;
		count = 0;
		bfs(g, begin);
	}
	
	private void bfs(Graph g, int begin) {
		Queue<Integer> queue = new LinkedList<>();
		isMarked[begin] = true;
		queue.offer(begin);
		while (!queue.isEmpty()) {
			Integer temp = queue.poll();
			for (Integer i : g.adj(temp)) {
				if (!isMarked[i]) {
					isMarked[i] = true;
					count++;
					edgeTo[i] = temp;
					queue.offer(i);
				}
			}
		}
	}
	
	public boolean hasPath(int v) {
		return isMarked[v];
	}
	
	public int count() {
		return count;
	}
	
	public String pathTo(int v) {
		if (!hasPath(v)) return "";
		Stack<Integer> stack = new Stack<>();
		stack.push(v);
		for (int i = v; i != begin; i = edgeTo[i]) {
			stack.push(edgeTo[i]);
		}
		
		return stack.toString();
	}

}

其實廣搜和深搜的不同就在於搜尋規則的不同,深搜使用的是stack的LIFO(後進先出)的思想,總是搜尋最新的節點。而廣搜則是使用queue的FIFO(先進先出)的規則。

就如同上面的一樣,節點進入佇列的順序是根據距離的,所以我們就可以實現慢慢範圍的擴大搜索。

同樣的,我們也標記了進入節點的前一個節點,用來追蹤路徑。因為我們是根據範圍搜尋的,所以得到的就是最短路徑。


我們可以使用廣搜和深搜來實現很多應用,例如是否有環,是否是二部圖等等。這裡我們就不展開了。

我們上面的圖的節點都是以數字作為標記的,而對於實際應用來講,圖的節點一般都不會是數字,而是String型別的字串等。

要實現這種符號圖,我們只需要將我們的程式碼進行一些擴充套件,使用符號表的方法,將字串對映到某個整數上就可以了。

例如:


我們只需要在將字串對映得到一個數字,也就是使用散列表的方式,儲存成鍵值對,就可以繼續使用上面的程式碼了。