递归的套路

分治和回溯

本质上就是一种特殊的递归(较为复杂的递归)。碰到算法问题先找重复性,最优的重复性就是动态规划,最近的重复性根据重复性怎么构造怎么分解就有什么分治或者最后要回溯或者实在其他的各种办法,但本质上其实就是一种递归,就是要去找它的重复性。一般都需要分解问题和最后组合每个子问题的结果。

 

代码模板

分治代码模板:

def divide_conquer(problem, param1, param2, ...): 
  # 1. recursion terminator (子问题没有了/问题解决了,本质上是递归的层级到了最下面这个层级,也就是到了叶子结点)
  if problem is None: 
	print_result 
	return 

  # 2. process(split your big problem) / prepare data (处理当前逻辑,就是把这个大问题如何分成子问题)
  data = prepare_data(problem) 
  subproblems = split_problem(problem, data) 

  # 3. conquer subproblems (调用这个函数下探一层,解决更细节的子问题)
  subresult1 = self.divide_conquer(subproblems[0], p1, ...) 
  subresult2 = self.divide_conquer(subproblems[1], p1, ...) 
  subresult3 = self.divide_conquer(subproblems[2], p1, ...) 
  …

  # 4. merge / process and generate the final result (把这些结果组装成一个大的结果,最后返回)
  result = process_result(subresult1, subresult2, subresult3, …)
	
  # 5. revert the current level states

最一般的泛型递归的代码模板(java版):

public void recur(int level, int param) { 
  // 1. recursion terminator (递归的终止条件)
  if (level > MAX_LEVEL) { 
    // process result 
    return; 
  }

  // 2. process current logic (处理当前层逻辑)
  process(level, param); 

  // 3. drill down (下探到下一层)
  recur( level: level + 1, newParam); 

  // 4. revert the current level status if needed (清扫当前层)
 
}

回溯

简单来说就是不断地在每一层去试,看这个方法行不行。经典使用场景就是八皇后和数独的问题。

总结:

1、如果非要说他和泛型递归有一点不同的话就是,在drill down 和revert status之间,你必须把每一个子问题它的中间结果全部组合起来,得到最终的结果再返回回去。

2、递归的思想和自顶向下的编程思想一致,当前层只要考虑当前层的 问题,一般来说不要下探(至少不要下探太多),一方面人脑不太擅长用人肉递归,很难模拟容易出错也很累。

3、 学习递归就像理解这种去来兮的感觉。如果有这种感觉了,那你就得道了。

 

实战经验分享

1、先(和面试官)确定题目的意思。

例如 :能否调用某些库函数,数据的边界范围,某些特殊情况是否需要考虑。

2、写出模板,套用

3、考虑边界情况、 是否合并、是否恢复变量(尤其是对象,要注意有时候对象要拷贝出一份来处理,不然有可能会被递归给不停地改变里面的数据)

 

具体的代码实战

  • pow(x, n)问题(https://leetcode-cn.com/problems/powx-n/):
class Solution {
    private double fastPow(double x, long n) {
        // 1. recursion terminator(子问题没有了/问题解决了,本质上是递归的层级到了最下面这个层级,也就是到了叶子结点)
        if (n == 0) {
            return 1.0;
        }

        // 2. process(split your big problem) / prepare data (处理当前逻辑,就是把这个大问题如何分成子问题)
        // 3. conquer subproblems (调用这个函数下探一层,解决更细节的子问题)
        double half = fastPow(x, n / 2);

        // 4. merge / process and generate the final result (把这些结果组装成一个大的结果,最后返回)
        return n % 2 == 0 ? half * half : half * half * x;

        // 5. revert the current level status if needed(此处不需要)
    }
    public double myPow(double x, int n) {
        long N = n;
        if (N < 0) {
            x = 1 / x;
            N = -N;
        }

        return fastPow(x, N);
    }
};
  • 子集 问题 (https://leetcode-cn.com/problems/subsets/):
public List> subsets(int[] nums) {
    List> ans = new ArrayList();
    if (nums == null) {
        return ans;
    }
    dfs(ans, nums, new ArrayList(), 0);
    return ans;
}

private void dfs(List> ans, int[] nums, ArrayList list, int index) {
    // 1. recursion terminator
    if (index == nums.length) {
        ans.add(new ArrayList<>(list)); // 拷贝出来的一份list,不要影响原来的list
        return;
    }

    // 2. process(split your big problem) / prepare data (处理当前逻辑,就是把这个大问题如何分成子问题)
    // 3. conquer subproblems (调用这个函数下探一层,解决更细节的子问题)
    dfs(ans, nums, list, index + 1);    //index位置的元素选
    list.add(nums[index]);
    dfs(ans, nums, list, index + 1);    //index位置的元素不选

    // 4. revert the current level status if needed(此处需要,因为不能影响不同层)
    list.remove(list.size() - 1);
}
  • 电话号码字母组合 问题 (https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/)
public class TestDemo {

    public static void main(String[] args) {
        TestDemo test = new TestDemo();
        List res = test.letterCombinations("23");
        System.out.println(res);
    }

    Map map = new HashMap() {{
        put('2', "abc");
        put('3', "def");
        put('4', "ghi");
        put('5', "jkl");
        put('6', "mno");
        put('7', "pqrs");
        put('8', "tuv");
        put('9', "wxyz");
    }};

    public List letterCombinations(String digits) {
        if (digits.length() == 0) {
            return new ArrayList<>();
        }
        List res = new ArrayList();
        search("", digits, 0, res);
        return res;
    }

    /**
     * @param s      每一轮生成的结果
     * @param digits 输入的参数
     * @param i      level
     * @param res    最终全部递归完成后生成的结果
     */
    private void search(String s, String digits, int i, List res) {
        // 1. recursion terminator(子问题没有了/问题解决了,本质上是递归的层级到了最下面这个层级,也就是到了叶子结点)
        if (i == digits.length()) {
            res.add(s);
            return;
        }

        // 2. process(split your big problem) / prepare data (处理当前逻辑,就是把这个大问题如何分成子问题)
        String letters = map.get(digits.charAt(i));
        for (int j = 0; j < letters.length(); j++) {
            // 3. drill down(调用这个函数下探一层,解决更细节的子问题)
            search(s + letters.charAt(j), digits, i + 1, res);    // 注意这儿i+1千万不能写成i++或者++i,原因自己体会
        }

        // 4. revert the current level status if needed(此处不需要)
    }
}
  • N皇后 问题(https://leetcode-cn.com/problems/eight-queens-lcci/)
// 之前的皇后所能攻击的位置
Set cols = new HashSet<>();
Set pies = new HashSet<>();
Set nas = new HashSet<>();

// 主函数入口
public List> solveNQueens(int n) {
    if (n < 1) {
        return new ArrayList<>();
    }

    List> result = new LinkedList<>();

    // init
    List currentStatus = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        currentStatus.add(init(n));
    }

    process(0, result, n, currentStatus);
    return result;
}

/**
 *
 * @param row        当前层
 * @param result    最终的结果
 * @param n         总层数
 * @param currentStatus
 */
private void process(int row, List> result, int n, List currentStatus) {
    // 1. recursion terminator
    if (row == n) {
        // 千万别直接使用该对象,需要拷贝出一份来,防止result里的数据又被接下来的递归栈改变了。鄙人在此排查了很久才发现该隐晦的BUG
        // result.add(currentStatus);
        List newCurrentStatus = copy(currentStatus, n);
        result.add(newCurrentStatus);
        return;
    }

    // 2. process logic in current level
    for (int col = 0; col < n; col++) {
        if (cols.contains(col) || pies.contains(col + row) || nas.contains(col - row)) { // go die
            continue;
        }
        // update the flag
        cols.add(col);
        pies.add(col + row);
        nas.add(col - row);

        String newLine = updateLine(n, col, currentStatus.get(row));
        currentStatus.set(row, newLine);

        // 3. drill down (调用函数去做子问题)
        process(row + 1, result, n, currentStatus);

        // 4. revert the current level status if needed
        cols.remove(col);
        pies.remove(col + row);
        nas.remove(col - row);

    }
}

private List copy(List currentStatus, int n) {
    List ans = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        ans.add(currentStatus.get(i));
    }
    return ans;
}

public String init(int n) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < n; i++) {
        sb.append(".");
    }
    return sb.toString();
}

public String updateLine(int n, int col, String line) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < line.length(); i++) {
        if (i == col) {
            sb.append("Q");
        } else {
            sb.append(".");
        }
    }
    return sb.toString();
}

总结

  • 递归的思维要点
  1. 抵制人肉递归的诱惑(最大误区)
  2. 找到最近最简的方法,将其拆解成可重复解决的问题(重复子问题)
  3. 数学归纳法的思维(最开始最简单的条件是成立的,且你能够证明当n成立的时候可以推导出n+1也是成立的。类似放鞭炮,自己领悟)

Tip:

本章分享的这一类的递归套路其实并不是最优的,因为我将result的变量也作为参数参与递归了,经历了递归栈的耗时,略微性能有些折损,但是便于理解,且这点折损微乎其微,一般来说都可以接受。如果非要性能更好的方式的话,可以将递归函数变成带返回值的递归函数。但是理解难度会稍微大一些。

需要多练习多找找感觉。外加一丢丢的悟性。一起加油吧!

 

 

 

 

你可能感兴趣的:(算法)