代码随想录 | 回溯法(1)

目录

回溯算法理论基础

第77题. 组合

216.组合总和III

17.电话号码的字母组合

注意语法:空字符串和null的区别

         注意语法:StringBuilder的常见用法

回溯算法理论基础

回溯三部曲:

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题. 组合

77. 组合 - 力扣(LeetCode)

带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili

先自己用递归写了一个巨慢无比的算法:

该方法由于从最后一个元素开始考虑,并且进行递归,所以造成重复计算子问题。

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 值:

i + k - cur.size() - 1 <= n

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

216. 组合总和 III - 力扣(LeetCode)

和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III_哔哩哔哩_bilibili

 按照回溯模板芜湖起飞:(回溯函数参数,回溯函数终止条件,遍历顺序)

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.电话号码的字母组合

力扣题目链接

还得用回溯算法!| LeetCode:17.电话号码的字母组合_哔哩哔哩_bilibili

 使用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

 

### 关于代码随想录中的回溯算法学习资源 代码随想录是一个专注于帮助初学者掌握数据结构算法的学习平台,其内容覆盖广泛,尤其适合那些希望系统化提升编程能力的人群[^1]。对于回溯算法这一重要主题,代码随想录提供了详尽的讲解和实践案例。 #### 回溯算法简介 回溯法是一种经典的递归求解方法,在解决组合问题、排列问题以及子集问题等方面具有广泛应用。它的核心思想是通过逐步构建解决方案并尝试每一步可能的选择来探索所有可能性。如果发现当前路径无法满足条件,则立即退回上一状态重新选择其他分支继续试探。这种方法能够有效避免穷举所有的候选方案从而提高效率。 #### 代码随想录中的回溯算法教学特点 在代码随想录里,有关回溯算法的内容通常被安排在一个较为系统的框架下进行讲授: - **理论基础**:从基本概念出发介绍什么是回溯法及其适用场景; - **经典题目解析**:选取LeetCode上的典型例题作为练习素材,比如全排列问题、N皇后问题等; - **实战演练**:鼓励读者跟随视频或者文档一步步完成编码过程,并提供详细的错误分析指导; 以下是基于Python语言实现的一个简单版本的全排列生成器示例: ```python def permute(nums): result = [] # 辅助函数用于执行实际的回溯逻辑 def backtrack(path, options): if not options: result.append(path[:]) # 当选项为空时记录当前路径到最终结果列表中 return for i in range(len(options)): chosen = options[i] path.append(chosen) # 做出选择 remaining_options = options[:i]+options[i+1:] backtrack(path,remaining_options) # 继续深入下一个决策节点 path.pop() # 撤销刚才所做的决定以便测试新的方向 backtrack([],nums) return result ``` 此段程序展示了如何运用栈的思想去模拟整个搜索树的过程,其中`backtrack()`负责管理每一次的状态转移直至找到符合条件的结果为止。 #### 推荐参考资料链接 虽然上述提到的具体章节尚未更新完毕[^3],但是可以参考官方博客或者其他社区贡献者分享的相关文章进一步加深理解。同时也可以关注作者Carl发布的最新动态获取更多优质课程信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值