先看第一个问题

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
回溯算法的使用场景和用法套路_for循环
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-medium/xv8ka1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

为什么使用回溯?

首先要清楚,本问题就是字符的组合问题。比如输入“23”就是对abc和def的两两组合,这很容易想到用两个for循环来完成。但是,问题在于输入的长度是不定的,可能是“23”,也可能是“345”。那么这样程序该写几个for循环呢?
考虑一下我们针对输入“23”的两层for循环逻辑:
1.外层循环遍历abc,并不断选择一个字符。
2.内层循环遍历def,并不断选择字符,拼接上外层循环的字符形成一个字符串。
3.在内层循环中,将拼接好的字符串加入到结果中。
所以,我们找一下适用于n层循环的统一规律:
1.遍历第k个指定串(1 <= k <= n)如“abc”,找到一个字符加入到待定结果串中。
2.若待定结果串的长度==n。那么将其添加到结果集中。然后移除当前选择的字符,选择下一个字符开始重复1。
3.否则继续进行下一个指定串遍历。
所以,我们只需要将上述过程抽象成一个方法,然后循环调用其n次,并且保证每次调用的指定串是按顺序遍历输入得到的。
那么,重复调用统一过程的逻辑就是:递归。而对于添加一个元素,递归,移除该元素这个过程,显然应该使用栈这样的数据结构作为待定结果串。

回溯模板

所以回溯是为了解决不定次数的循环问题。回溯将循环的逻辑抽离成函数的主体,然后递归调用,进入递归时判断有没有到达结果。所以回溯问题是存在模板的:

void backTrack(原始输入集, 层数, 临时结果, 结果){
  if(临时结果满足输入要求(一般是集合长度满足)){
    临时结果加入到结果集;  
    return;
  }
  控制层数不超过题目要求长度{
    当前元素加入到临时结果集;
    backTrack(原始输入集, 层数 + 1, 临时结果, 结果);
    当前元素移出临时结果集;
  }
}

程序实现

public List<String> letterCombinations(String digits) {
        if(digits.length() == 0){
            return new ArrayList<>();
        }
        Map<Character, String> map = new HashMap<>(); 
        map.put('2',"abc");
        map.put('3',"def");
        map.put('4',"ghi");
        map.put('5',"jkl");
        map.put('6',"mno");
        map.put('7',"pqrs");
        map.put('8',"tuv");
        map.put('9',"wxyz");
        List<String> res = new ArrayList<>();
        Deque<Character> queue = new ArrayDeque<>(); //Java doc中表示应用Deque双端队列代替Stack完成栈操作
        backTrack(res, map, queue, digits, 0);
        return res;
    }
    public void backTrack(List<String> res, Map<Character, String> map, Deque<Character> queue, String digits, int index){
        if(queue.size() == digits.length()){
            StringBuffer sb = new StringBuffer(); //使用StringBuffer完成拼接
            for(char c : queue){
                sb.append(c);
            }
            res.add(sb.toString());
            return;
        }
        for(int i = index; i < digits.length(); i++){ //控制整体递归次数,因此这个循环看似是在本层循环,实际上其深度是在本身和逐层递归中完成的
            String s = map.get(digits.charAt(i));
            for(int j = 0; j < s.length(); j++){ //每层递归要做的事
                char c = s.charAt(j);
                queue.offerLast(c);
                backTrack(res, map, queue, digits, i + 1);
                queue.pollLast();
            }
        }
    }

第二个问题

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
有效括号组合需满足:左括号必须以正确的顺序闭合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-medium/xv33m7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

问题分析

依然是不定层的循环问题,相当于这次的字符只有'('和')',且入栈的')'个数不能超过栈中'('的个数。而入栈的'('个数又不能超过n。于是可以增设两个变量分别表示栈中左括号的个数,和允许入栈的右括号个数。

    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        Deque<Character> stack = new ArrayDeque<>();
        trackBack(n, 0, 0, stack, res);
        return res;
    }
    public void trackBack(int n, int leftNums, int rightNums, Deque<Character> stack,  List<String> res){
        if(stack.size() == n * 2){
            StringBuilder sb = new StringBuilder();
            for(char c : stack){
                sb.append(c);
            }
            res.add(sb.toString());
            return;
        }
        //控制递归层数的条件。n控制左括号个数,左括号个数控制右括号个数
        if(leftNums < n){
            stack.offerLast('(');
            trackBack(n, leftNums + 1, rightNums + 1, stack, res);
            stack.pollLast();
        }
        if(rightNums > 0){
            stack.offerLast(')');
            trackBack(n, leftNums, rightNums - 1, stack, res);
            stack.pollLast();
        }

    }