Android程式設計師面試會遇到的演算法系列:
上次我們在結束二叉樹的題目分析之前,做了一個簡單的二叉樹層序遍歷(廣度優先搜尋)的模板程式碼的學習,我們應該還能記得,廣度優先要使用佇列
,AKA -> Queue這個資料結構來做。用Java的虛擬碼我們再複習一遍
public void levelTraverse(TreeNode root){
if(root == null){
return;
}
//初始化佇列
Queue queue = new LinkedList();
//把根節點加入佇列
queue.add(root);
//開始遍歷佇列
while(!queue.isEmpty()){
TreeNode current = queue.poll();
System.out.println(current.toString());
//只要當前節點的左右節點不為空,那麼我們就可以把其加入到佇列的尾部,等待下一次遍歷,我們continue這個while迴圈
if(current.left != null){
queue.add(current.left);
}
if(current.right != null){
queue.add(current.right);
}
}
}
複製程式碼
從以上模板程式碼我們可以看出,對於廣度優先搜尋,其核心就在於使用佇列Queue來做一個while迴圈,在迴圈內除了對當前節點的處理之外,還需要將當前節點的孩子節點放入佇列的尾部。這樣我們就實現了簡單的廣度優先搜尋。
就是這麼的簡單!
那麼,問題來了,核心的部分既然這麼簡單,廣度優先搜尋的應用又有哪些呢?
今天文章的重點就是,哪些普遍的問題可以用廣度優先搜尋來解決。
1.幾度好友問題?
熟練玩耍各種社交網站的朋友都會發現網站經常都會給你推薦可能認識的好友,還會"友情"提示該推薦好友到底通過什麼途徑推薦。這裡我們重點介紹幾度好友這種推薦模式。
顧名思義,幾度好友的意思代表的就是該位使用者和你中間相隔了有多少度,也就是多少個人。曾經哈佛大學的心理學教授Stanley Milgram 提出了一個叫六度分隔理論每個人和另外隨機的一個陌生人的距離只隔著6個人。也就是說你和川普之間可能也就是隔著6個人哦。
好了,交代了這麼多背景,我們可以開始思索一個問題了,假如說我們已經有了好友相關資訊,推薦系統怎麼找到對應度數的好友呢?
舉個例子。人人網現在要推薦給一個使用者他的三度以內的好友放在推薦欄裡面,我們怎麼獲取?
先把使用者的資料結構貼出來
public class User{
//這個friends是該使用者的直接好友,也就是一度好友、
private List<User> friends;
private String name;
//獲取好友列表
public List<User> getFriends(){
return Collections.unmodifiableList(friends);
}
public String getUserName(){
return name;
}
}
複製程式碼
假設我們已經有了這樣的一個好友結構在我們的記憶體裡面(當然在實際的場景裡面,我們不可能把一個社交網路的所有使用者資訊存在Ram裡面,這不現實,不過找幾度好友的原理肯定一樣,只不過在分散式場景裡面獲取使用者資訊的過程要複雜很多),每個User都有一個叫friends的List,儲存他的直接好友。
根據以上的條件我們可以這麼思考,我們需要去求的幾度好友的這個幾度,是不是其實就是層序遍歷的那個_層_呢?x度難度不就是x層麼?有了這個訊息,我們就知道其實根據上面的廣度優先的模板程式碼稍微修改一下,我們就可以得到第x層(x度)以內的好友了。
public List<User> getXDegreeFriends(User user, int degree){
List<User> results = new ArrayList<User>();
Queue<User> queue = new LinkedList<User>();
queue.add(user);
//用於記錄已經遍歷過的User,因為A是B的好友,那麼B也一定是A的好友,他們互相存在於對方的friends列表中。
HashSet<User> visited = new HashSet<User>();
//用一個counter記錄當前的層數。
int count = degree;
//這裡結束while迴圈的兩個條件,一個是層數,一個是queue是否為空,因為萬一該當前使用者壓根就沒有那麼多層的社交網路,比如他壓根就沒有朋友。
while(count>=1 && !queue.isEmpty()){
int queueSize = queue.size();
for(int i = 0 ; i < queueSize; i++){
User currentUser = queue.poll();
//假如該使用者已經遍歷過,那麼不做任何處理。
if(!visited.contains(currentUser)){
results.add(currentUser);
queue.addAll(currentUser.getFriends();
visited.add(currentUser);
}
}
count--;
}
return results;
}
複製程式碼
就是這麼簡單!通過佇列Queue,我們成功的把一個User的x度好友全部包在一個佇列裡面然後返回,這樣我們就完成了一個簡單的推薦好友方法!!。
同時一個小細節是我們使用HashSet這個資料結構來去重,為什麼我們要多做這麼一步呢?無論是廣度還是深度優先搜尋,這一步都可以說是重中之重,,因為我們在遍歷節點的時候,會遇到重複已經遍歷過的節點。比如:
A使用者和B使用者互為好友,所以他們的getFriends()
方法會返回對方。
假設我們在程式碼中沒有使用去重的資料結構的話,第一步放入A的時候,我們返回B加入佇列,第二步我們呼叫B的getFriends()
的時候又會返回A。。。。所以程式就會無限制的走下去了,同時層數也不正確了。所以我們遍歷過的節點,一定要通過某種方式儲存其相關資訊防止重複遍歷。
2.走迷宮問題(最短距離問題)。
說到最短距離,我們第一反應肯定都是想到迪杰特斯拉演算法。
在一個有向圖或者無向圖中,每一個節點與節點之間都有不同的權值(可以理解為距離),最後算出每個點與點之間的最短距離與其相應的路徑。
這次我們我們要學習的是這種演算法的一個特例。也就是如果節點與節點之間權值相等,但是可能存在障礙物的情況。
最經典的就是走迷宮問題了。
假設一個二維整型矩陣代表迷宮,0代表路,1代表牆壁(不能走不能通過),左上角是入口,右下角是出口(保證都為0)。求最少需要多少步可以走到出口。
對於這種問題,我們同樣需要用廣度優先來處理。為什麼呢?
因為對於每一個節點來說,往下走一層都是需要一步(大家距離權值相等),那麼我們在走迷宮的過程其實就是像一個決策樹一樣,每一層都只需要一步來走完,那麼終點的步數,其實就是取決於終點這個節點在這個樹結構中的第幾層。終點在第x層,就代表至少需要x步才能走到。
比如上圖,從A出發,到其他節點的分層為 1層: B,C,D 2層: F, E 3層: H,G 4層: I 其相應與A的距離也就是他們的層數。
所以在求迷宮問題的距離時,我們可以從起點開始做廣度優先的遍歷,不停的記錄當前的層數,當遍歷到終點的時候,檢視當前已經遍歷的層數,該層數也就是步數了。
public int getMinSteps(int[][] matrix) {
int row = matrix.length;
int col = matrix[0].length;
//迷宮可以走四個方向,這個二維陣列代表四個方向的x與y的偏移量
int[][] direction = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
HashSet<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
//把起點加入到佇列
queue.add(0);
int level = 0;
while (!queue.isEmpty()) {
//把該層的節點全部遍歷
int size = queue.size();
for (int i = 0; i < size; i++) {
int current = queue.poll();
if (!visited.contains(current)) {
visited.add(current);
//確定該節點的x與y座標
int currentX = current / col;
int currentY = current % col;
//假如該點是重點,那麼直接返回level
if (currentX == matrix.length - 1 && currentY == matrix[0].length - 1) {
return level;
}
//如果不是,那麼我們分別把它的四個方向的節點都嘗試加入到佇列尾端,也就是下一層中
for (int j = 0; j < direction.length; j++) {
int tempX = currentX + direction[j][0];
int tempY = currentY + direction[j][1];
//因為1代表牆壁,我們不能走,只能加數值為0的點
if (tempX > -1 && tempY > -1 && tempX <row&& tempY < col&& matrix[tempX][tempY] != 1) {
int code = tempX * col + tempY;
queue.add(code);
}
}
}
}
level++;
}
return -1;
}
複製程式碼
以上程式碼就是簡單的解決了迷宮問題中的最短路徑,同時還可以幫助判斷該迷宮到底有沒有可行的路徑到達出口(以上方法假如沒有路徑的時候會返回-1),因為方法在進行while迴圈的時候,從起點開始所有能遍歷的點都遍歷過之後,我們還沒有找到右下角的點,while迴圈會結束。
3.Multi-End 廣度優先搜尋(多重點廣度優先)。
Multi-End 廣度優先搜尋和 之前的迷宮問題有點類似,我們只需要把上述條件改一改。
假如在這個迷宮裡面有不止一個出口,那麼我們從出口(座標為0,0的點)開始,到達任何一個出口的最短路徑該怎麼求呢?
有朋友可能會說,假設有K個出口,那麼我執行之前走迷宮的方法K次,比較最短路徑不就行了。假設我們的節點有MN個,這樣的方式時間複雜度就是O(km*n)了。有沒有更快一點的方法呢?
我們可以嘗試著反向去思考這個問題,我們之前都是以起點為開始點,做廣度優先搜尋。對於多重點的問題,我們難道不可以用重點們作為起點,加入佇列中,再不停更新道路的權值,直到我們找到起點不就行了麼。
這個演算法我就不具體展開了,有興趣的朋友可以看看leetcode的Gates and Wall這題
具體的解答在這裡
今天的文章就暫時到這了,下一期會重點介紹一個深度優先演算法的回朔演算法的講解。