目录
回溯算法理论基础
回溯三部曲:
1. 回溯函数模板返回值以及参数
在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。
回溯算法中函数返回值一般为void,伪代码如下:
void backtracking(参数)
2. 回溯函数终止条件
既然是树形结构,那么我们在讲解二叉树的递归 (opens new window)的时候,就知道遍历树形结构一定要有终止条件,所以回溯也有要终止条件。
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
所以回溯函数终止条件伪代码如下:
if (终止条件) {
存放结果;
return;
}
3. 回溯搜索的遍历过程
在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
注意图中,我特意举例集合大小和孩子的数量是相等的!
回溯函数遍历过程伪代码如下:
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
backtracking这里自己调用自己,实现递归。
大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
分析完过程,回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
第77题. 组合
先自己用递归写了一个巨慢无比的算法:
该方法由于从最后一个元素开始考虑,并且进行递归,所以造成重复计算子问题。
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
if(k == 0){
result.add(new ArrayList<>());
return result;
}
if(n == k){
List<Integer> list = new ArrayList<>();
for(int j = 1; j<=k; j++) list.add(j);
result.add(list);
return result;
}
List<List<Integer>> firRes = combine(n-1,k-1);
for (List<Integer> innerList : firRes) {
innerList.add(n); // 直接向每个内部列表添加n
result.add(innerList);
}
List<List<Integer>> secRes = combine(n-1,k);
result.addAll(secRes);
return result;
}
}
我们按照答案的回溯法完整看一遍:
回溯应该从1开始遍历每个数字,选择是否将其加入当前路径。当路径长度等于k时,添加到结果中。这样可以避免重复的列表操作,通过回溯来撤销选择,继续探索其他可能性。
注意其中的剪枝操作,从 i 开始能满足所需元素个数的 i 值:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);
return result;
}
public void backtracking(int n, int k, int startIndex){
if(cur.size() == k){
result.add(new ArrayList<>(cur));
return;
}
// 剪枝操作
for(int i = startIndex; i <= n - k + cur.size() + 1; i++){
cur.add(i);
backtracking(n, k, i + 1);
cur.remove(cur.size()-1); // 回溯操作
}
}
}
一定切记使用回溯法时要复制一个副本: result.add(new ArrayList<>(cur));
216.组合总和III
按照回溯模板芜湖起飞:(回溯函数参数,回溯函数终止条件,遍历顺序)
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k,n,1);
return result;
}
private void backtracking(int k, int n, int startInt){
if(sum == n && cur.size() == k){
result.add(new ArrayList<>(cur));
return;
}
if(cur.size() >= k) return;
for(int i = startInt; i <= 9; i++){
cur.add(i);
sum += i;
backtracking(k,n,i+1);
cur.remove(cur.size()-1);
sum -= i;
}
}
}
17.电话号码的字母组合
使用StringBuilder和回溯完成了如下方法:
(注意由于使用递归每次处理从numIndex开始的数字,所以只需要一个for循环)
(直接使用StringBuilder而不是之后再转化,减少时间复杂度)
class Solution {
List<String> result = new ArrayList<>();
StringBuilder cur = new StringBuilder();
String[] s = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
if(digits.isEmpty()) return result;
char[] ch = digits.toCharArray();
backtracking(ch,0);
return result;
}
private void backtracking(char[] ch, int numIndex){
if(cur.length() == ch.length){
String res = cur.toString();
result.add(res);
return;
}
// 注意由于使用递归每次处理从numIndex开始的数字,所以只需要一个for循环
for(int j = 0; j < s[(ch[numIndex]-'0')].length(); j++){
cur.append(s[(ch[numIndex]-'0')].charAt(j));
backtracking(ch,numIndex+1);
cur.deleteCharAt(cur.length()-1);
}
}
}
注意语法:空字符串和null的区别
在 Java 中,空字符串和 null
是完全不同的概念:
- 检查空字符串:
if (s != null && s.isEmpty()) { System.out.println("这是空字符串"); }
- 检查
null
:if (s == null) { System.out.println("这是 null"); }
注意语法:StringBuilder的常见用法
以下是 StringBuilder
的常用操作速览:
1. 初始化
StringBuilder sb = new StringBuilder(); // 空构造,默认容量16
StringBuilder sb2 = new StringBuilder("abc"); // 初始内容为 "abc"
2. 添加内容
方法 | 示例 | 结果(假设初始为空) |
---|---|---|
append(任何类型) | sb.append("Hello").append(123); | "Hello123" |
insert(int index, 任何类型) | sb.insert(5, " "); → 在索引5插入空格 | "Hello 123" |
3. 删除内容
方法 | 示例 | 结果 |
---|---|---|
delete(int start, int end) | sb.delete(5, 7); | "Helo123" → 移除5-6 |
deleteCharAt(int index) | sb.deleteCharAt(0); | "ello123" |
setLength(int newLength) | sb.setLength(3); | "Hel" (截断) |
clear() (实际是 setLength(0) ) | sb.setLength(0); | 清空内容 |
4. 长度与容量
方法 | 说明 |
---|---|
length() | 返回当前字符数(如 sb.length() →5) |
capacity() | 返回当前容量(总空间,≥length) |
ensureCapacity(int min) | 确保容量≥指定值 |
5. 其他操作
方法 | 示例 | 结果 |
---|---|---|
reverse() | sb.reverse(); | "321olleH" |
replace(int start, int end, String str) | sb.replace(0, 5, "Hi"); | "Hi 123" |
toString() | String s = sb.toString(); | 转为不可变 String |
性能提示
- 优先用
append
替代字符串+
操作(减少中间对象)。 - 线程不安全,单线程用
StringBuilder
,多线程用StringBuffer
。