程序員面試金典——遞歸問題彙總

一、簡單動態規劃問題
1、機器人走方格I
    類似的參見《斐波那契數列》

   有一個XxY的網格,一個機器人只能走格點且只能向右或向下走,要從左上角走到右下角。請設計一個算法,計算機器人有多少種走法。

給定兩個正整數int x,int y,請返回機器人的走法數目。保證x+y小於等於12。

    class Robot {
        // public int times = 0;
        // public int times1 = 0;

        /*** 遞歸解法 **/
        public int countWays1(int x, int y) {
            if (x < 0 || y < 0)
                return 0;
            if (x == 0 || y == 0)
                return 1;
            //     times1 ++;
            return countWays1(x - 1, y) + countWays1(x, y - 1);
        }

        /**  優化的遞歸解法 **/
        public int countWays(int x, int y) {
            if (x < 0 || y < 0)
                return 0;
            int[][] counts = new int[x + 1][y + 1];
            counts[0][0] = 1;
            return countWays(x, y, counts);
        }

        private int countWays(int x, int y, int[][] counts) {
            if (x < 0 || y < 0)
                return 0;
            if (counts[x][y] <= 0) {
                counts[x][y] = countWays(x - 1, y, counts) + countWays(x, y - 1, counts);
            //         times ++;
            }
            return counts[x][y];
        }
    }
2、Minimum Path Sum (矩陣路徑最小和問題)(leetcode 65)
1)問題描述:

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

即給定一個m*n的數組矩陣,矩陣中所有值都是非負的;從左上角開始往右或者往下走,記錄最終到達右下角的路徑上所有元素之和的最小值問題;

2)問題分析:
簡單的動態規劃問題,記[i,j]位置元素的最小值爲sum[i,j];
則sum[i,j]=s[i][j] + min(sum[i+1][j], sum[i][j+1]);

3)代碼:
public class Solution {
    public int minPathSum(int[][] grid) {
        // 創建一個sum數組來記錄每個位置的sum值,這樣可以減少遞歸中的重複計算
        int[][] sum = new int[grid.length][grid[0].length];
        // 進行初始化賦值
        for (int[] sums : sum)
            Arrays.fill(sums, -1);
        return minPath(grid, 0, 0, sum);
    }

    private int minPath(int[][] grid, int x, int y, int[][] sum) {
        // 因爲下面需要用到Math.min獲取到最小值,故而這裏使用Integer.MAX_VALUE來表示超出邊界值
        if (x == grid.length || y == grid[0].length)
            return Integer.MAX_VALUE;
        if (sum[x][y] == -1) {
            int next = Math.min(minPath(grid, x + 1, y, sum),
                    minPath(grid, x, y + 1, sum));
            // 注意處理bottom位置,即右下角位置兩個方向走都超出了邊界,因而最後Math.min獲得的值都是Integer.MAX_VALUE,這裏需要將其置爲0
            next = (next == Integer.MAX_VALUE) ? 0 : next;
            sum[x][y] = grid[x][y] + next;
        }
        return sum[x][y];
    }
}

二、求子集問題

2、獲得集合中的所有子集:

問題描述:

請編寫一個方法,返回某集合的所有非空子集。

給定一個int數組A和數組的大小int n,請返回A的所有非空子集。保證A的元素個數小於等於20,且元素互異。各子集內部從大到小排序,子集之間字典逆序排序,見樣例。

測試樣例:
[123,456,789]
返回:{[789,456,123],[789,456],[789,123],[789],[456,123],[456],[123],{}}

問題分析:

注意子集問題不要忘了空集{},空集是每個集合的子集;

遞歸法:子集問題可以很簡單地轉化爲遞歸問題;假設A數組中有n個元素,前n-1個元素的排列組合的集合P(n - 1)已經獲得,則獲得n個元素的排列組合問題,即轉化爲最後一個元素A[n]是否存在的問題;若不存在,則P'(n) = P(n - 1);若存在,則P''(n) = P(n - 1) + A[n],即P(n -1)中每個集合中加上A[n];則原問題轉化爲P(n)=P(n-1) + P''(n)的問題;用遞歸很容易實現;

迭代法:遞歸的時間複雜度爲O(2^n);這裏可以採用數學中組合方法來取巧,在構造一個集合時,每個元素無非存在兩種狀態:存在(記爲1)和不存在(記爲0),則可以用一個二進制數來記錄每一個子集合;該二進制數的取值範圍顯然是0--2^n;遍歷該範圍,將二進制轉換成爲集合即可;


代碼:

遞歸法:

class Subset {
    /*@param: A--原始集合; n--數組大小*/
    public ArrayList<ArrayList<Integer>> getSubsets(int[] A, int n) {
        if (A == null || A.length != n) // 輸入不合法情況
            return null;

        return getAllSubSets(A, 0);
    }

    // 遞歸函數,index表示當前遞歸的深度
    private ArrayList<ArrayList<Integer>> getAllSubSets(int[] A, int index) {
        ArrayList<ArrayList<Integer>> subSets;
        if (A.length == index){ // 達到遞歸底層
            subSets = new ArrayList<>();
            subSets.add(new ArrayList<Integer>()); // 注意加上{}空集合
        } else {
            // 獲得P(n - 1)集合
            subSets = getAllSubSets(A, index + 1);
            // 獲得集合中當前元素
            int item = A[index];
            // 獲取P(n - 1) + a(n)的集合
            ArrayList<ArrayList<Integer>> addedSets = new ArrayList<>();
            // 遍歷所有的P(n-1)中子元素,加上a(n)
            for (ArrayList<Integer> subSet : subSets) {
                // 新創建一個ArrayList,用來保存添加item後的結果
                ArrayList<Integer> tempList = new ArrayList<>(subSet);
                tempList.add(item);
                addedSets.add(tempList);
            }
            // P(n)即是P(n - 1) + addedSets
            subSets.addAll(addedSets);
        }
        return subSets;
    }
}

迭代法:

    class Subset {
        public ArrayList<ArrayList<Integer>> getSubsets(int[] A, int n) {
            if (A == null || A.length != n)
                return null;
            ArrayList<ArrayList<Integer>> subSets = new ArrayList<>();
            // 獲得最大數值
            int max = 1 << n;
//        int max = (int)Math.pow(2, n); 
            // 遍歷所有可能的結果
            for (int i = 0; i < max; i ++) {
                subSets.add(getSetsFromNum(A, i));
            }
            return subSets;
        }

        // 將二進制數值轉化成爲集合形式
        private ArrayList<Integer> getSetsFromNum(int[] A, int num) {
            ArrayList<Integer> subSet = new ArrayList<>();
            int index = 0;
            // 遍歷A數組中每個元素對應的位置,若該位置上num二進制爲1,則添加到結果集合中;
            for (int k = num; k > 0; k >>= 1, index ++) {
                if ((k & 1) == 1)
                    subSet.add(A[index]);
            }
            return subSet;
        }
    }

三、排列組合問題:
問題描述:

編寫一個方法,確定某字符串的所有排列組合。

給定一個string A和一個int n,代表字符串和其長度,請返回所有該字符串字符的排列,保證字符串長度小於等於11且字符串中字符均爲大寫英文字符,排列中的字符串按字典序從大到小排序。(不合並重復字符串)

測試樣例:
"ABC"
返回:["CBA","CAB","BCA","BAC","ACB","ABC"]
問題分析:
採用遞歸的思想,假設字符串str,前n-1個字符已經排列組合好了,獲得的結果爲f(n-1);f(n)即是將第n個字符item插入到f(n-1)中每個字符串的任意位置所獲得結果;

代碼:

public class Permutation {
    public ArrayList<String> getPermutation(String A) {
        // 判斷字符串A是否合法
        if (A == null)
            return null;
        ArrayList<String> resultList = getThePermutation(A);
        // 進行字典序排序(從大到小)
        Collections.sort(resultList, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                if (o1.compareTo(o2) > 0) return -1;
                if (o1.compareTo(o2) < 0) return 1;
                return 0;
            }
        });
        return resultList;
    }

    // 遞歸解法
    private ArrayList<String> getThePermutation(String A) {
        ArrayList<String> resultList = new ArrayList<>();
        // 返回結果值
        if (A.length() == 0) {
            resultList.add("");
            return resultList;
        }

        char item = A.charAt(0); // 獲得字符串的首字符
        A = A.substring(1);      // 移除字符串的首字符
        ArrayList<String> preList = getThePermutation(A); // 獲得p(n - 1)
        // 往P(n-1)中每個字符串中插入item字符,形成新的排列組合
        for (String str : preList) {
            for (int i = 0; i <= str.length(); i++) { // 注意是<=號
                String newStr = insertIntoStr(str, item, i);
                resultList.add(newStr);
            }
        }
        return resultList;
    }

    // 往字符串的指定位置插入字符
    private String insertIntoStr(String str, char item, int index) {
        String start = str.substring(0, index);
        String end = str.substring(index, str.length());
        return start + item + end;
    }
}<span style="widows: auto; font-family: 微軟雅黑; background-color: inherit;"> </span>


四、魔術索引問題
1、無重複值的魔術索引問題
問題描述:

在數組A[0..n-1]中,有所謂的魔術索引,滿足條件A[i]=i。給定一個升序數組,元素值各不相同,編寫一個方法,判斷在數組A中是否存在魔術索引。請思考一種複雜度優於o(n)的方法。

給定一個int數組A和int n代表數組大小,請返回一個bool,代表是否存在魔術索引。

測試樣例:
[1,2,3,4,5]
返回:false
問題分析:
簡單的二分查找問題,如果A[mid] < mid,則mid(即索引位置)往左遞減,A[mid]也是每步至少遞減1,則A[mid]會始終小於mid值;但往右遞增,mid每步遞增1,A[mid]可以遞增一個較大值,趕上mid,可能會發生滿足A[mid]=mid情況,故下一次遞歸右子序列即可;同理A[mid] > mid,查找左子序列;

代碼:
public class MagicIndex {
   public boolean findMagicIndex(int[] A, int n) {
      // 直接二分查找
      if (A == null || A.length == 0)
         return false;
      int start = 0;
      int end = A.length -1;

      while (start <= end) {
         int mid = (end - start) / 2 + start;
         // 找到魔術索引
         if (A[mid] == mid)
            return true;
         if (A[mid] < mid) // 在右子序列
            start = mid + 1;
         else
            end = mid - 1;
      }
      return false;
   }
}


2、有重複值的魔術索引:
問題分析:

在數組A[0..n-1]中,有所謂的魔術索引,滿足條件A[i]=i。給定一個不下降序列,元素值可能相同,編寫一個方法,判斷在數組A中是否存在魔術索引。請思考一種複雜度優於o(n)的方法。

給定一個int數組A和int n代表數組大小,請返回一個bool,代表是否存在魔術索引。

測試樣例:
[1,1,3,4,5]
返回:true
問題描述:
有重複值的情況下,就不能簡單的使用前面的直接二分查找了;但是在二分查找基礎上,可以所以查找範圍;
比如若A[mid] < mid,則索引在A[mid] + 1--mid範圍內一定沒有魔術索引,因爲這一段範圍內最大值也只能爲A[mid];則可以將數組分割爲start--A[mid]和mid+1 -- end兩個子數組進行繼續查找;
同理若A[mid] > mid,則索引在mid--A[mid]-1範圍內一定沒有魔術索引,因爲該範圍內最小值爲A[mid],同樣可以分割成爲兩個子數組,進而進行遞歸操作;

代碼:

public class MagicIndex {
    public boolean findMagicIndex(int[] A, int n) {
        if (A == null || A.length == 0)
            return false;
        return findMagicIndex(A, 0 , A.length - 1);
    }

    private boolean findMagicIndex(int[] A, int start, int end) {
        if (start > end)
            return false;
        boolean result = false;
        int mid = (end - start) / 2 + start;

        if (A[mid] == mid)
            return true;
        if (A[mid] < mid) {
            result = findMagicIndex(A, start, A[mid]) ||
                    findMagicIndex(A, mid + 1, end);
        } else {
            result = findMagicIndex(A, start, mid - 1) ||
                    findMagicIndex(A, A[mid], end);
        }
        return result;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章