Android程序員面試會遇到的算法(part 3 深度優先搜索-回溯backtracking)

Android程序員面試會遇到的算法系列:java

Android程序員面試會遇到的算法(part 1 關於二叉樹的那點事) 附Offer狀況git

Android程序員面試會遇到的算法(part 2 廣度優先搜索)程序員

Android程序員面試會遇到的算法(part 3 深度優先搜索-回溯backtracking)面試

Android程序員面試會遇到的算法(part 4 消息隊列的應用)算法

Android程序員會遇到的算法(part 5 字典樹)app

Android程序員會遇到的算法(part 6 優先級隊列PriorityQueue)函數

Android程序員會遇到的算法(part 7 拓撲排序)post

上一遍文章咱們過了一次廣度優先算法,算是比較好理解的,由於模式比較固定,使用隊列再進行while() 循環,既能夠知足大部分時候的需求。這一次咱們來開始學習/複習一下咱們的深度優先算法。深度優先算法其實在不少地方均可以應用到,其實在個人見解,只要搜索集合相對固定,而且使用到遞歸的算法均可以算是深度優先。並且在學習完回溯算法的不少題目以後,你們也能夠更直觀的體驗到,不少時候回溯也是暴力搜索的一種程序上的實現而已。因此,學習

1.回溯ui

2.深度優先

3.暴力搜索

這三種算法,名字雖然不一樣,可是都在某些狀況下是有很大的共同成分的。你們在看完題目以後能夠好好感覺一下。

那麼進入正題


1.理解遞歸

若是是計算機專業的同窗可能能夠忽略這一小節。

[圖片上傳失敗...(image-1a1a5f-1518421713974)]

其實遞歸的方法和通常的方法有什麼區別呢?答案是徹底沒有,遞歸的方法和通常的方法徹底沒有區別。一個標準的方法/函數,都是須要在方法/函數棧裏面進行調用和返回的。舉個栗子。

static void a(){
	b();
}

static void b(){
	c();
}


static void c(){
	System.out.println("methods")
}


public static void main(String[] args){
	a();
}


複製代碼

下面這段代碼在方法棧中的執行過程以下兩圖所示。

方法執行

方法結束

如上圖所示,全部的方法在執行結束以後都會返回,return,這個return,表明的是return到該方法的調用者,也就是執行該方法的方法內,也就是上一層中。

理解了這個,遞歸也就很好理解,一樣是使用方法/函數棧,只不過是調用的方法是相同的方法而已。

2.理解回溯

理解回溯,咱們先從一個例題來看一下。

咱們有一個集合/列表,含有若干整數(不含有重複),例如:{1,2,3}。 求該集合的全排列。 {1,2,3} {1,3,2} {2,1,3} {2,3,1} {3,1,2} {3,2,1}

從直觀的感受來講,第一眼遇到這個題目,咱們能夠這麼去抽象的構思:

咱們按照步驟/狀態來抽象的話,咱們每一刻都有一個可用集合一個答案集合。每一步都須要從可用集合裏面抽取一個元素加入到答案集合。在答案集合滿了(或者是可用集合空了)的時候,表明咱們獲取了一個答案,這時須要向後,往可用集合內部退回元素。再從新作抽取的步驟,往答案集合中放置元素。

1開頭的集合答案

能夠看出來,咱們每次獲取答案都要向上退後一步,回到以前的狀態,選取不一樣的元素放入結果集合。這個過程其實就是咱們以前所講的回溯。至於怎麼樣遍歷集合,根據題目的要求咱們能夠有不一樣的策略,通常的回溯算法都是涉及列表的遍歷,for循環足矣。

咱們來看看代碼

public List<List> getPermutation(List<Integer> list){
    List result = new ArrayList<>();
    permutateHelper(result,new ArrayList<>(), list, new HashSet<Integer>());
    return result;
}

private void permutateHelper(List result, List<Integer> temp,List<Integer> list, HashSet<Integer> visited){
    //若是temp,temp答案集合滿了,咱們加入到最終的結果集合內。
    if(temp.size() == list.size()){
        result.add(new ArrayList(temp));
    }
    else{
        //直接使用for循環進行對原集合的遍歷
        for(int i = 0; i< list.size();i++){
            //若是沒有visit過,進行遞歸
            if(!visited.contains(list.get(i))){
                int current = list.get(i);
                temp.add(current);
                visited.add(current);
                //進入下一層遞歸
                permutateHelper(result,temp,list,visited);
                visited.remove(current);
                //這裏須要直接remove掉最後一個元素,由於咱們的所有的下一層遞歸已經結束,因此能夠把該層的數字刪掉,進入for循環的下一個遍歷的開始了。這裏這個remove的動做就是咱們所謂的「回溯」
                temp.remove(temp.size()-1);      
            }
        }
    }

}

複製代碼

從以上代碼咱們能夠看出,回溯算法其實就是普通的遞歸,可是加上了對集合遍歷(for 循環)的過程,你們能夠體會一下一個小小的區別。假如在以上的代碼中,咱們的temp不刪除最後一個元素,改爲這樣:

private void permutateHelper(List result, List<Integer> temp,List<Integer> list, HashSet<Integer> visited){
    if(temp.size() == list.size()){
        result.add(new ArrayList(temp));
    }
    else{
        for(int i = 0; i< list.size();i++){
            if(!visited.contains(list.get(i))){
                int current = list.get(i);
                temp.add(current);
                visited.add(current);
                //進入下一層遞歸,不刪除最後一個元素,每次都建立一個新的temp列表
                permutateHelper(result,new ArrayList<>(temp),list,visited);
                visited.remove(current);
            }
        }
    }

}

複製代碼

簡單的一行的修改,最後的答案也是對的(先不說這個修改浪費了多少空間),但是卻徹底的改變了咱們要的回溯的本質,沒有向前退的這個動做,這個程序就變成了單純的遞歸,暴力搜索了。

關於排列組合的題目,還有更加難的,好比集合中有重複元素怎麼辦,若是不僅是求全排列,而是求子集呢?

有興趣的同窗能夠看看leetcode上的題目:

1.Permutation II 2.Subset

相信你們對所謂的回溯已經有點理解了。咱們再來一個難一點點的題目。

3. 電話鍵盤

例題來自leetcode的一道關於電話鍵盤的題目

Screen Shot 2018-02-12 at 3.14.47 PM.png

這個題目就是說,在手機上按幾個數字鍵,對應可能產生的全部可能的字母的集合。好比在手機上按23,就是從{a,b,c}和{d,e,f}中各取一個放入答案集合中。

這題和上一題的區別是,搜索集合再也不是一個固定的大集合了,而是若干的小集合,每一個數字對應一個小集合,知足搜索結果的答案的標準也不同,再也不是以集合的元素數量爲標準,而是以咱們的輸入數字的數量爲標準。

咱們直接看代碼

public List<String> letterCombinations(String digits) {
		if (digits == null || digits.length() == 0) {
			return new ArrayList<>();
		}

                ///先初始化手機按鍵的數字和字母的關係
		String[] one = { "" };
		String[] two = { "a", "b", "c" };
		String[] three = { "d", "e", "f" };
		String[] four = { "g", "h", "i" };
		String[] five = { "j", "k", "l" };
		String[] six = { "m", "n", "o" };
		String[] seven = { "p", "q", "r", "s" };
		String[] eight = { "t", "u", "v" };
		String[] nine = { "w", "x", "y", "z" };
		String[] zero = { "" };

		HashMap<Integer, String[]> map = new HashMap<>();
		map.put(0, zero);
		map.put(1, one);
		map.put(2, two);
		map.put(3, three);
		map.put(4, four);
		map.put(5, five);
		map.put(6, six);
		map.put(7, seven);
		map.put(8, eight);
		map.put(9, nine);

		ArrayList<String> result = new ArrayList<>();
		int[] allNum = new int[digits.length()];
		for (int i = 0; i < digits.length(); i++) {
			allNum[i] = Integer.parseInt(digits.substring(i, i + 1));
		}
		phoneNumberHelper(result, new StringBuilder(), 0, allNum, map);
		return result;

	}

	private void phoneNumberHelper(ArrayList<String> result, StringBuilder current, int index, int[] allNum, HashMap<Integer, String[]> com) {
                //若是找到一個排列,加入答案中
		if (index == allNum.length) {
			result.add(current.toString());
			return;
		} else {
                        //使用for循環,遍歷當前該數字的字母集合
			String[] possibilities = com.get(allNum[index]);
			for (int i = 0; i < possibilities.length; i++) {
				phoneNumberHelper(result, current.append(possibilities[i]), index + 1, allNum, com);
                                //必定要把StringBuilder的最後一位刪除掉。
				current.deleteCharAt(current.length()-1);
			}
		}
	}


複製代碼

能夠看出,回溯的題目並不難,在理解了排列組合題目以後,理解這個就簡單一些了。咱們對比一下和排列組合題目的不一樣。

搜索集合在每個狀態都不同,排列組合在每一個步驟都是相同的搜索集合,電話按鍵根據按下的數字不同,對應的字母不同。

對於回溯算法的精髓,你們經過對兩個題目的練習能夠發現,就在於那個remove的動做,假如上面這個算法我改爲這樣,不使用StringBuilder,而直接使用Sting.

public List<String> letterCombinations(String digits) {
		if (digits == null || digits.length() == 0) {
			return new ArrayList<>();
		}
		String[] one = { "" };
		String[] two = { "a", "b", "c" };
		String[] three = { "d", "e", "f" };
		String[] four = { "g", "h", "i" };
		String[] five = { "j", "k", "l" };
		String[] six = { "m", "n", "o" };
		String[] seven = { "p", "q", "r", "s" };
		String[] eight = { "t", "u", "v" };
		String[] nine = { "w", "x", "y", "z" };
		String[] zero = { "" };

		HashMap<Integer, String[]> map = new HashMap<>();
		map.put(0, zero);
		map.put(1, one);
		map.put(2, two);
		map.put(3, three);
		map.put(4, four);
		map.put(5, five);
		map.put(6, six);
		map.put(7, seven);
		map.put(8, eight);
		map.put(9, nine);

		ArrayList<String> result = new ArrayList<>();
		int[] allNum = new int[digits.length()];
		for (int i = 0; i < digits.length(); i++) {
			allNum[i] = Integer.parseInt(digits.substring(i, i + 1));
		}
		phoneNumberHelper(result, "", 0, allNum, map);
		return result;

	}

	private void phoneNumberHelper(ArrayList<String> result, String current, int index, int[] allNum, HashMap<Integer, String[]> com) {
		if (index == allNum.length) {
			result.add(current);
			return;
		} else {
			String[] possibilities = com.get(allNum[index]);
			for (int i = 0; i < possibilities.length; i++) {
                //不使用StringBuilder,直接使用String鏈接一個String,這個作法其實和new String()是同樣的。建立了一個新的String,
				phoneNumberHelper(result, current + possibilities[i], index + 1, allNum, com);
			}
		}
	}


複製代碼

算法大部分都是相同的,可是直接直接使用String鏈接一個String,這個作法其實和new String()是同樣的。建立了一個新的String,和咱們的排列組合裏面的new ArrayList<>(temp)這段代碼同樣,雖然最終結果沒錯,可是卻喪失了回溯算法的本質和優點,浪費了空間。一行之差。

4.N皇后問題

這一篇的最後一個問題,固然非N皇后問題莫屬,題目太經典,我就不浪費篇幅再寫一次算法了。我此次就着重分析一下這個怎麼把這個問題抽象成回溯問題。怎麼把這個問題模型化,通俗點說,怎麼把這個問題和排列組合問題找到相同的地方,方便你們理解。

咱們每一次在棋盤上放棋子,其實就是從原集合,往答案集合中加入元素的一個動做。和排列組合問題不一樣的是,往答案集合裏面加入元素這個動做不是每次都是合法的,而排列組合是不管怎麼加都對。

舉個栗子

u=4191624954,1499040036&fm=27&gp=0.jpg

在放置第五個棋子的時候,咱們在遍歷的過程當中須要判斷第五個能夠合法的放置在哪一個位置,第一行?不行,由於第一個棋子也在第一行。第二第三第四同理。都通不過咱們的檢查。因此在for循環中要對以前已經放置的棋子作比較,看看能不能放置到咱們想放置的位置,若是不行,那麼咱們須要回退到上一層。

因此N皇后問題最後的難點就在於,怎麼表示放置棋子的位置?怎麼作所謂的合法檢查?這些你們能夠本身思考一下再用java實現一下。

當我之前在複習N皇后問題的時候,我有那麼一剎那和排列組合問題作了個比較,頓時恍然大悟。原來原理是相同的。

此次的回溯算法的分析就到此位置,下一次我會作一個更全面的深度優先的算法分析。

祝你們新年快樂!!

u=3534538955,3983762870&fm=27&gp=0.jpg
相關文章
相關標籤/搜索