先看第一个问题
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 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();
}
}