程序员面试金典——递归问题汇总

一、简单动态规划问题
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;
    }
}

你可能感兴趣的:(程序员面试金典——递归问题汇总)