其實最開始學習算法,聽到拓撲排序這幾個字我也是懵逼的,後來學着學着才慢慢知道是怎麼一回事。關於拓撲這個詞,在網上找到這麼一段解釋:java
所謂「拓撲」就是把實體抽象成與其大小、形狀無關的「點」,而把鏈接實體的線路抽象成「線」,進而以圖的形式來表示這些點與線之間關係的方法,其目的在於研究這些點、線之間的相連關係。表示點和線之間關係的圖被稱爲拓撲結構圖。拓撲結構與幾何結構屬於兩個不一樣的數學概念。在幾何結構中,咱們要考察的是點、線之間的位置關係,或者說幾何結構強調的是點與線所構成的形狀及大小。如梯形、正方形、平行四邊形及圓都屬於不一樣的幾何結構,但從拓撲結構的角度去看,因爲點、線間的鏈接關係相同,從而具備相同的拓撲結構即環型結構。也就是說,不一樣的幾何結構可能具備相同的拓撲結構。算法
拓撲在計算機領域研究的就是圖,既然是圖,那就會有節點和邊,節點表示的是現實生活中抽象的東西,邊表示的是這些東西之間的關係。數組
仔細想一想,其實現實生活中的不少東西都可以抽象成計算機世界當中的圖,好比說對於實際的地圖,咱們能夠用節點表示岔路口,邊表示岔路口之間的連線;人的朋友圈,咱們用節點表示人,用邊表示人與人之間的關係;再好比歷史事件,咱們用節點表示事件,用邊表示事件之間的聯繫;不論是人或者事,只要找到了其對應的聯繫,每每均可以抽象成圖。app
拓撲排序解決的是依賴圖問題,依賴圖表示的是節點的關係是有依賴性的,好比你要作事件 A,前提是你已經作了事件 B。除了 「先有雞仍是先有蛋」 這類問題,通常來講事件的依賴關係是單向的,所以咱們都用有向無環圖來表示依賴關係。拓撲排序就是根據這些依賴來給出一個作事情,或者是事件的一個順序,就舉個例子,朋友來家裏吃飯,你準備作飯,你要作飯,首先得買菜,買菜得去超市,去超市要出門搭車,所以這個順序就是 出門搭車 -> 到超市 -> 買菜 -> 回家作飯。固然我舉的這個例子你不須要計算機的幫助也能很清楚地知道這個順序,可是有些事情並很差直接看出來,好比常見的 「一個很是大的項目中,如何肯定源代碼文件的依賴關係,並給出相應的編譯順序?」,咱們要學的是一個解決一類問題的普適的方法,而不是學習怎麼快速獲得一個具體問題的結果,換句話說,在學習之中,思考過程每每比結果和答案更重要。函數
和圖相關的問題常見的算法就是搜索,深度和廣度,拓撲排序也不例外,咱們首先來看看稍微簡單,好理解的廣度優先搜索,先放上代碼模版:oop
public List<...> bfsTopologicalSort() {
// 邊界條件檢測
if (...) {
return true;
}
Map<..., List<...>> graph = new HashMap<>();
// 構建圖
...
// 這裏表示的是對於每一個節點,其有多少前置條件(前置節點)
int[] inDegree = new int[totalNodeNumber];
// 根據圖中節點的依賴關係去改變 inDegree 數組
for (Entry.Map<..., List<...>> entry : graph.entrySet()) {
...
}
Queue<...> queue = new LinkedList<>();
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
queue.offer(...);
}
}
List<...> result = new ArrayList<...>();
while (!queue.isEmpty()) {
int cur = queue.poll();
// 記錄當前結果
result.add(...);
// 對於當前節點,解除對其下一層節點的限制
for (... i : map.getOrDefault(cur, new ArrayList<...>())) {
inDegree[i]--;
if (inDegree[i] == 0) {
queue.offer(...);
}
}
}
return result;
}
複製代碼
對於拓撲排序問題,咱們以前講過,它是基於圖的算法,那麼首先咱們要作的就是將問題抽象爲圖,這裏我用了一個 HashMap 來表示圖,其中 Key 表示的是具體的一個節點,Value 表示的是這個節點其下層節點,也就是 List 裏面的節點依賴於當前節點,之因此這樣表示依賴關係是爲了咱們後面實現的方便。接下來咱們會用一個 inDegree 數組來表示每一個節點有多少個依賴的節點,選出那些不依賴任何節點的節點,這些節點應該被最早輸出,按照通常的廣度優先搜索思惟,咱們開一個隊列,將這些沒有依賴的節點放進去。最後就是廣度優先搜索的步驟,咱們保證從隊列裏面出來的節點是當前沒有依賴或者依賴已經被解除的節點,咱們將這些節點輸出,這個節點輸出,其下一層節點的依賴就要相應的減小,咱們改變 inDegree 數組中對應的值便可,若是改變後,對應節點沒有任何依賴了,代表這個節點能夠被輸出了,就把它加進隊列,等待被輸出。最後的最後就是輸出咱們獲得的答案,可是這裏要提醒的是,咱們要考慮出現環的狀況,就相似 「雞生蛋,蛋生雞」 的問題,好比 A 依賴於 B,B 也依賴於 A,這時咱們是得不到答案的。學習
咱們再來看看深度優先搜索,其思想和前面講的廣度優先搜索略微不一樣,咱們再也不須要 inDegree 數組了,並且咱們須要去用另外一種方式去判斷圖中是否存在環,先放上代碼模版:ui
public ... dfsTopologicalSort() {
// 邊界條件檢測
if (...) {
}
Map<..., List<...>> graph = new HashMap<>();
// 構建圖
...
boolean[] visited = new boolean[numCourses];
boolean[] isLooped = new boolean[numCourses];
List<...> result = new ArrayList<...>();
for (int i = 0; i < totalNodeNumber; ++i) {
if (!visited[i] && !dfs(result, graph, visited, isLooped, i)) {
return new ArrayList<...>();
}
}
return result;
}
private ... dfs(List<...> result,
Map<..., List<...>>[] graph,
boolean[] visited,
boolean[] isLoop,
... curNode) {
// 判斷是否有環
if (isLoop[curNode]) {
return false;
}
isLoop[curNode] = true;
// 遍歷當前節點的前置節點,也就是依賴節點
for (int i : graph.get(curNode) {
if (visited[i]) {
continue;
}
if (!dfs(graph, visited, isLoop, i)) {
return false;
}
}
isLoop[curNode] = false;
// record answer
result.add(curNode)
visited[curNode] = true;
return true;
}
複製代碼
有一點須要注意的是,構建圖的時候,咱們須要和以前的廣度優先搜索反轉一下,也就是這裏的 Key 表示的是一個節點,Value 中存的是其所依賴的節點,這也是和咱們的實現方式有關,咱們須要用到遞歸,遞歸用到函數棧,先處理的函數(節點)後輸出結果。理解了上面這一點,下面就是用遞歸去解決這個深度優先搜索問題,可是有一點是咱們須要用到兩個 boolean 數組,一個(visited 數組)是記錄咱們訪問過的節點,避免重複訪問,另一個是防止環的出現,怎麼避免,深度優先搜索是沿着一條路徑一直搜索下去,咱們須要保證這一條路徑不會通過某個節點兩次,注意我這裏說的是一條路徑。編碼
兩種算法算是兩種實現的方式,可是目的都是同樣的,思想是相似的。spa
LeetCode 210. Course Schedule II
題目分析:
有一些課程,每一個課程都有前置課程,必須把前置課程修完了才能修這門課程。這道題就是單純使用了拓撲排序,這裏我把兩種實現方式都列下來:
代碼實現:
基於深度優先搜索版本:
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer> result = new ArrayList<>();
List<Integer>[] graph = new ArrayList[numCourses];
for (int i = 0; i < numCourses; ++i) {
graph[i] = new ArrayList<Integer>();
}
for (int i = 0; i < prerequisites.length; ++i) {
graph[prerequisites[i][0]].add(prerequisites[i][1]);
}
boolean[] visited = new boolean[numCourses];
boolean[] isLooped = new boolean[numCourses];
for (int i = 0; i < numCourses; ++i) {
if (!visited[i] && !dfs(graph, visited, isLooped, i, result)) {
return new int[0];
}
}
int[] output = new int[numCourses];
int index = 0;
for (int i : result) {
output[index++] = i;
}
return output;
}
private boolean dfs(List<Integer>[] graph, boolean[] visited, boolean[] isLooped, int curCourse, List<Integer> result) {
if (isLooped[curCourse]) {
return false;
}
isLooped[curCourse] = true;
for (int i : graph[curCourse]) {
if (!visited[i] && !dfs(graph, visited, isLooped, i, result)) {
return false;
}
}
isLooped[curCourse] = false;
visited[curCourse] = true;
result.add(curCourse);
return true;
}
複製代碼
基於廣度優先搜索版本:
public int[] findOrder(int numCourses, int[][] prerequisites) {
if (prerequisites == null) {
return new int[0];
}
int[] inDegree = new int[numCourses];
Map<Integer, List<Integer>> map = new HashMap<>();
for (int i = 0; i < numCourses; ++i) {
map.put(i, new ArrayList<Integer>());
}
for (int i = 0; i < prerequisites.length; ++i) {
map.get(prerequisites[i][1]).add(prerequisites[i][0]);
inDegree[prerequisites[i][0]]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
List<Integer> result = new ArrayList<>();
while (!queue.isEmpty()) {
int cur = queue.poll();
result.add(cur);
for (int i : map.getOrDefault(cur, new ArrayList<Integer>())) {
inDegree[i]--;
if (inDegree[i] == 0) {
queue.offer(i);
}
}
}
int[] output = new int[result.size()];
int index = 0;
for (int i : result) {
output[index++] = i;
}
return output.length == numCourses ? output : new int[0];
}
複製代碼
LeetCode 269. Alien Dictionary
題目分析:
看完了簡單的題,咱們如今看看稍微有點難度的題。這道題的題意是,如今有一種新的語言,輸入參數是一個按字符大小順序排好的單詞數組,注意這裏的排序是依據這個新語言來講的,而不是英語,題目要咱們輸出一個字符大小的可能順序,這裏我用了 「一個」 和 「可能」 這兩個詞,拓撲排序事後可能會有多種解,好比說 A 依賴於 C,B 也依賴於 C,那麼輸出可能就是 CAB 或者 CBA,咱們只須要輸出其中的一個解便可。
這道題的實現能夠分爲兩個部分,構建圖,還有就是拓撲排序,構建圖我使用的就是最最直接的辦法,讓單詞先後兩兩比較,找出兩個單詞第一個不相同的字符,排在前面的單詞中的字符就要比排在後面單詞的字符小。當咱們構建完圖,其餘的就是老樣子。
代碼實現:
public String alienOrder(String[] words) {
if (words == null || words.length == 0) {
return "";
}
Map<Character, Set<Character>> graph = new HashMap<>();
for (int i = 0; i < words.length; ++i) {
for (char c : words[i].toCharArray()) {
if (!graph.containsKey(c)) {
graph.put(c, new HashSet<Character>());
}
}
}
buildGraph(words, graph);
Set<Character> visited = new HashSet<>();
Set<Character> isLooped = new HashSet<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < words.length; ++i) {
for (char c : words[i].toCharArray()) {
if (!visited.contains(c) && !dfs(graph, visited, isLooped, sb, c)) {
return "";
}
}
}
return sb.toString();
}
private boolean dfs(Map<Character, Set<Character>> graph, Set<Character> visited, Set<Character> isLooped, StringBuilder sb, char cur) {
if (isLooped.contains(cur)) {
return false;
}
isLooped.add(cur);
for (char c : graph.get(cur)) {
if (!visited.contains(c) && !dfs(graph, visited, isLooped, sb, c)) {
return false;
}
}
isLooped.remove(cur);
visited.add(cur);
sb.append(cur);
return true;
}
private void buildGraph(String[] words, Map<Character, Set<Character>> graph) {
int maxLen = 0;
for (int i = 0; i < words.length; ++i) {
maxLen = Math.max(words[i].length(), maxLen);
}
for (int i = 0; i < maxLen; ++i) {
for (int j = 1; j < words.length; ++j) {
if (words[j].length() <= i || words[j - 1].length() <= i) {
continue;
}
if (i == 0) {
if (words[j].charAt(0) != words[j - 1].charAt(0)) {
graph.get(words[j].charAt(0)).add(words[j - 1].charAt(0));
}
} else {
if (words[j].substring(0, i).equals(words[j - 1].substring(0, i))
&& words[j].charAt(i) != words[j - 1].charAt(i)) {
graph.get(words[j].charAt(i)).add(words[j - 1].charAt(i));
}
}
}
}
}
複製代碼
拓撲排序其實就是圖類問題當中的一個簡單應用,它實際上是有固定的實現方式的,咱們只須要掌握這些實現方式中的算法思想,相信它再也不是一個難題。仍是想說說本身對作算法題的認識,咱們作題不是爲了訓練咱們的作題速度,編碼能力,更重要的是學習算法裏面的一種思考問題的方式和方法,這種先進的思想或者說是思惟模式能夠引領着咱們朝計算機領域更廣闊、更深層次的地方去。