Android程序員面試會遇到的算法系列:java
這一期是我打算作的安卓算法面試系列的最後一期了,一來是自歷來了美國以後,天天的工做實在太忙了,除了週末以外不多時間能完完整整的總結一些東西。不過第二個緣由,也是最重要的緣由,就是在這以後我打算好好沉澱積累一下,等有更多的心得體會再分享出來。網絡
這期我打算聊一聊拓撲排序這個算法。在Java裏面具體的實現和一些細節。這裏我儘可能不用太多的專業術語,用比較通俗的講法來解釋一些概念。(實際上是個人狗嘴也吐不出啥象牙。。。之前學的算法知識早就還給老師了)數據結構
其實拓撲排序和廣度優先搜索算法在代碼上真的很像,說穿了其實就是圖的遍歷,只不過遍歷的順序和規則有些少量不一樣。
相信各位學習計算機科學專業的同窗應該都對高等數學或者大學物理有深入的陰影。。。我還記得我當時考完大學物理2已經以爲本身要掛了,沒忍住給老師打了一個電話求情,雖然最後老師說我離掛科還遠,可是69分的大學物理2也讓我與那個學期的獎學金無緣了。
可能有人問爲何計算機專業不直接學Java,C++或者web開發?必定要先上大學物理或者高等數學?說了這麼多廢話,我想說的重點是,每一個學科都有一個本身的課程安排,學習一門專業課以前必需要有一些基礎課程的支撐才行。咱們不能不學高等數學和線性代數直接跳去學機器學習,咱們也不能不學Java或者python直接上手web項目。這也引伸出了這一期的內容,拓撲排序, 怎麼樣在已知某些節點的前序(prerequisites)
節點的狀況下,把這些節點的順序排列出來。就比如,我知道必定課程的先後順序的狀況下,把我這四年大學的課程時間安排排列出來,最後打印成課程表。
好比上面這幅圖,咱們怎麼能夠將其課程的依賴關係,按照前後順利排列起來,這就是拓撲排序能夠解決的其中一種,也是最經典的問題。
首先對於圖來講,咱們要知道每一個節點有多少子節點,也就是後繼節點,在課程安排例子裏面能夠理解爲,學了A課程以後能夠學的課程B。那麼A就是B的前驅節點,B就是A的後繼節點。 在Java中咱們可使用HashMap
來實現,根據題目的不一樣,有時候也可使用別的數據結構好比二維數組。不過我我的比較喜歡HashMap。
那麼節點的關係能夠用一個HashMap來表達,課程使用String 來表示
//節點的後繼節點
HashMap<String, HashSet<String>> courses = new HashMap();
複製代碼
同時,在拓撲排序中,咱們還須要記錄某個節點的前驅節點的數量,由於只有當某個節點的前驅節點爲0的時候,咱們才能處理該節點。對應到課程學習中,就是隻有當咱們學習完畢了某個課程的全部前驅課程,咱們才能學習該課程。好比圖中的計算機網絡課程,須要先學習組成原理和通訊原理同樣。
//記錄每一個點的前驅節點數量
HashMap<String,Integer> preCount = new HashMap<String,Integer>
複製代碼
假設咱們已經有了這兩個數據結構而且數據已經填充好了。咱們就能夠開始進行拓撲排序了。算法很簡單,把前驅節點數量爲0的節點先放入隊列,每次從隊列彈出的時候把本身的後繼節點的preCount數量減小1,假如此時後繼節點的preCount數量減小到0了,就把節點加入到隊列中。在這個例子裏面,彈出一個節點的意義就是學習一門課程。
這個很好理解,好比咱們學習完組成原理
,距離學習計算機網絡還差一門課。
當咱們把通訊原理學習完畢以後,計算機網絡的前驅節點數量從1減小爲0,咱們才能夠學習計算機網絡。
用代碼來表示的話,以下
//課程調度隊列
Queue<String> queue = new LinkedList<>();
//最後課程的順序
List<String> sequence = new ArrayList<>();
while (!queue.isEmpty()) {
//獲取當前隊列中的第一個課程,將其加入到最後的課程順序列表中
String currentCourse = queue.poll();
sequence.add(currentCourse);
//每當一個課程結束學習以後,找到它的後繼課程
for (String course : courses.get(currentCourse)) {
//加入後繼課程的前驅節點數量仍是大於0 的,說明該課程還沒被學習
if (preCount.get(course) > 0) {
//減小該後繼課程的前驅節點數量
preCount.put(course, preCount.get(course) - 1);
//若是前去梳理減到0,說明咱們已經能夠開始學習該課程了,
//加到隊列裏面
if (preCount.get(course) == 0) {
queue.add(course);
}
}
}
}
return sequence;
複製代碼
其實看代碼你們也能夠知道,拓撲排序其實就是廣度優先搜索的一種,只不過拓撲排序在插入子節點到隊列的時候,有一些限制。就是在這裏:
if (preCount.get(course) == 0) {
queue.add(course);
}
複製代碼
通常的廣度優先只要遍歷了當前節點,就要把當前節點的全部本身點都一股腦的插入到隊列中。在拓撲排序裏面,由於每一個節點的前驅節點數量可能會大於1,因此,不能簡單的插入子節點(或者說後繼節點),而是須要額外的數據結構,preCount這個HashMap來決定是否能夠把後繼節點插入。
圖搜索的一個經典問題是,若是有環怎麼辦?一樣的,在拓撲排序裏面,也可能出現存在環的狀況。好比
在下圖這種狀況,學生就沒辦法學了。。。。
可是在拓撲排序下面,判斷是否有環的方法還不太同樣,好比寬度優先搜索的狀況下,咱們能夠用一個叫visited的HashSet來記錄已經訪問過的節點。可是拓撲排序不行。
好比下圖這種狀況
當咱們學習完A以後,其實咱們是不能遍歷徹底全部節點的,由於B和C的前驅節點數量都爲1,程序在跑完第一個循環
while (!queue.isEmpty()) {
//獲取到A
String currentCourse = queue.poll();
sequence.add(currentCourse);
複製代碼
以後,就會直接結束了。 因此其實咱們判斷環的方法要換成->判斷咱們是否能學習完全部課程。
HashMap<String, HashSet<String>> courses = new HashMap();
//假如最後咱們能學習完全部課程
if(result.size() == course.keySet().size()){
return true;
}else{
return false;
}
複製代碼
拓撲排序的題目能夠出現不少種,可是都是萬變不離其宗,掌握好咱們須要的數據結構,熟練的寫出廣度優先算法的模板代碼, 其實就萬事大吉了。之後好比還有相似的問題,像安裝軟件,好比要安裝A,要先安裝依賴C,等等之類的問題,相信你們均可以迎刃而解了。總結的來說,一旦咱們發現須要進行對依賴之間進行排序的,用拓撲排序都沒毛病。
Leetcode 裏面的Course Schedule, 你們能夠本身練習一下。 我沒有講的部分就是數據初始化的部分,不過很簡單,你們本身摸索。 個人答案
public int[] findOrder(int numCourses, int[][] prerequisites) {
// record dependecy counts
HashMap<Integer, Integer> dependeciesCount = new HashMap<>();
HashMap<Integer, HashSet<Integer>> dependeciesRelation = new HashMap<>();
for (int i = 0; i < numCourses; i++) {
dependeciesCount.put(i, 0);
dependeciesRelation.put(i, new HashSet<>());
}
for (int i = 0; i < prerequisites.length; i++) {
int pre = prerequisites[i][1];
int suf = prerequisites[i][0];
dependeciesCount.put(suf, dependeciesCount.get(suf) + 1);
dependeciesRelation.get(pre).add(suf);
}
Queue<Integer> queue = new LinkedList<>();
for (Map.Entry<Integer, Integer> entry : dependeciesCount.entrySet()) {
if (entry.getValue() == 0) {
queue.add(entry.getKey());
}
}
int[] index = new int[numCourses];
int currentIndex = 0;
while (!queue.isEmpty()) {
Integer currentCourse = queue.poll();
index[currentIndex] = currentCourse;
currentIndex++;
for (Integer nei : dependeciesRelation.get(currentCourse)) {
if (dependeciesCount.get(nei) > 0) {
dependeciesCount.put(nei, dependeciesCount.get(nei) - 1);
if (dependeciesCount.get(nei) == 0) {
queue.add(nei);
}
}
}
}
int[] empty = {};
return currentIndex == numCourses ? index : empty;
}
複製代碼
最後一期算法教程寫完了,其實感受若是你們能把這7個大塊給充分理解,面對大部分的公司的算法面試其實也沒多大問題了。這也是我2017年-2018年初面試各個公司的算法題的一些心得體會。 雖然個人標題一直都是以面試
開頭,可是我以爲最重要的仍是學習,或者說是複習算法的這個過程。去理解去學習的這個過程纔是精髓。固然,這些內容也是上學就應該學好的,如今從新複習,也算是還債(technical debt)。。。。 回頭看這個系列的初衷,也是但願你們在面對面試的同時,能回顧一些之前上學時候的知識,作到溫故而知新。只要讀者看了個人文章,能發出一種「挖槽這個之前好像學過啊」的感嘆,我也就知足了~
2019年對我來講是一個新的起點,我也要不停的督促本身好好工做,多反思多學習,之後爭取能分享更多高質量的文章和知識。但願本身永遠不要忘掉當初雄心壯志面試硅谷公司的那顆赤子之心。