力扣Hot100系列23(Java)——[回溯]总结(上)(全排列,子集,电话号码的字母组合,组合总和)

文章目录


前言

本文记录力扣Hot100里面关于回溯的四道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解


一、全排列

1.题目

给定一个不含重复数字 的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]

输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]

输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]

输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

2.代码

java 复制代码
class Solution {

    public List<List<Integer>> permute(int[] nums) {
        // 存储最终所有排列结果
        List<List<Integer>> res = new ArrayList<>();
        // 临时列表,用于存放当前正在构建的排列
        List<Integer> output = new ArrayList<>();
        // 将数组元素加入临时列表
        for (int num : nums) {
            output.add(num);
        }
        // 启动回溯,从第 0 个位置开始填充
        backtrack(nums.length, output, res, 0);
        return res;
    }

    // 回溯函数:递归生成排列
    // n:数组长度,output:当前排列,res:结果集,first:当前要固定的位置索引
    private void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
        // 递归终止条件:所有位置都已填充完成
        if (first == n) {
            // 将当前完整排列加入结果集(必须新建列表保存)
            res.add(new ArrayList<>(output));
            return;
        }
        // 遍历:将 [first, n-1] 的每个元素放到 first 位置
        for (int i = first; i < n; i++) {
            // 交换:固定 first 位置的元素
            Collections.swap(output, first, i);
            // 递归:处理下一个位置
            backtrack(n, output, res, first + 1);
            // 回溯:撤销交换,恢复原状态,继续尝试下一种可能
            Collections.swap(output, first, i);
        }
    }
}

3.例子

以nums = [1,2,3]为例

java 复制代码
// 初始
output = [1,2,3]
backtrack(n=3, output, res, first=0)

第一层:first = 0

i 从 0 开始循环:i=0,1,2

① i = 0

swap(0,0) → output 不变:[1,2,3]

进入
backtrack(3, [1,2,3], res, first=1)


第二层:first = 1

i 从 1 开始:i=1,2

① i = 1

swap(1,1) → [1,2,3]

进入
backtrack(3, [1,2,3], res, first=2)


第三层:first = 2

i 从 2 开始:i=2

i = 2

swap(2,2) → [1,2,3]

进入
backtrack(3, [1,2,3], res, first=3)

终止条件:first == n(3==3)

添加结果:
res = [[1,2,3]]

回溯:swap(2,2) 还原 → 回到上一层


回到第三层循环结束

回到第二层

② i = 2

swap(1,2) → output 变成 [1,3,2]

进入
backtrack(3, [1,3,2], res, first=2)

终止条件:first=3

添加结果:
res = [[1,2,3], [1,3,2]]

回溯:swap(1,2) 还原 → [1,2,3]

第二层循环结束

回到第一层


① i=0 结束,回溯 swap(0,0)


② i = 1

swap(0,1) → output 变成 [2,1,3]

进入
backtrack(3, [2,1,3], res, first=1)

第二层:first=1

i=1,2

i=1 → swap → [2,1,3]

进入 first=2 → 得到 [2,1,3]

加入结果:
res.add([2,1,3])

i=2 → swap(1,2) → [2,3,1]

进入 first=2 → 得到 [2,3,1]

加入结果:
res.add([2,3,1])

回溯还原 → [2,1,3] → 再回溯 → [1,2,3]


③ i = 2

swap(0,2) → output 变成 [3,2,1]

进入
backtrack(3, [3,2,1], res, first=1)

第二层:first=1

i=1,2

i=1 → [3,2,1] → 加入结果

i=2 → swap → [3,1,2] → 加入结果


最终结果

复制代码
[[1,2,3],
 [1,3,2],
 [2,1,3],
 [2,3,1],
 [3,2,1],
 [3,1,2]]

非常清晰了吧!

二、子集

1.题目

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集 。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]

输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]

输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

2.代码

java 复制代码
class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>(); // 存储所有子集的结果集
        List<Integer> temp = new ArrayList<>();      // 存储当前正在构建的子集
        dfs(nums, 0, temp, res);                     // 从第0个元素开始递归
        return res;
    }
    // pathLen 是当前递归处理到数组nums的第几个元素(即元素下标),也可以理解为递归的深度
    private void dfs(int[] nums, int pathLen, List<Integer> temp, List<List<Integer>> res) {
        // 终止条件:处理完所有元素(pathLen等于数组长度)
        if (pathLen == nums.length) {
            res.add(new ArrayList<>(temp)); // 将当前子集的副本加入结果集
            return;
        }

        // 分支1:选择当前元素(将nums[pathLen]加入当前子集)
        temp.add(nums[pathLen]);
        dfs(nums, pathLen + 1, temp, res); // 递归处理下一个元素
        temp.remove(temp.size() - 1);      // 回溯:撤销选择

        // 分支2:不选择当前元素(直接处理下一个元素)
        dfs(nums, pathLen + 1, temp, res);
    }
}

关于dfs函数的执行过程
1. 先判断是否到底了,到底就保存子集
2. 先走"选"的分支

加进去 → 递归 → 撤销(删掉)
3. 再走"不选"的分支

直接递归

3.例子

以nums = [1,2,3]为例

初始状态

temp = []

pathLen = 0(处理数字 1)


开始递归!

1️⃣ 处理 1(pathLen=0)

分支1:选 1

temp 变成 [1]

进入下一层 → pathLen=1(处理 2)


2️⃣ 处理 2(pathLen=1)

分支1:选 2

temp 变成 [1,2]

进入下一层 → pathLen=2(处理 3)


3️⃣ 处理 3(pathLen=2)

分支1:选 3

temp 变成 [1,2,3]

进入下一层 → pathLen=3


4️⃣ pathLen=3(等于数组长度,终止)

把 [1,2,3] 加入结果!

返回!


回到 3️⃣,执行回溯:撤销选择

temp 删除最后一个元素

1,2,3\] → \[1,2

分支2:不选 3

直接递归 → pathLen=3

把 [1,2] 加入结果!

返回!


回到 2️⃣,执行回溯:撤销选择

temp 删除最后一个元素

1,2\] → \[1

分支2:不选 2

直接递归 → pathLen=2(处理 3)


5️⃣ 处理 3(pathLen=2)

分支1:选 3

temp → [1,3]

加入结果!

回溯撤销 → [1]

分支2:不选 3

temp → [1]

加入结果!


回到 1️⃣,执行回溯:撤销选择

temp 删除最后一个元素

1\] → \[

分支2:不选 1

直接递归 → pathLen=1(处理 2)


6️⃣ 处理 2(pathLen=1)

分支1:选 2

temp → [2]

处理 3:

选 3 → [2,3](加入结果)

不选 3 → [2](加入结果)

回溯撤销 → []

分支2:不选 2

直接处理 3:

选 3 → [3](加入结果)

不选 3 → [](加入结果)


最终所有子集

1,2,3

1,2

1,3

1

2,3

2

3

三、电话号码的字母组合

1.题目

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"

输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = "2"

输出:["a","b","c"]

提示:

  • 1 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

2.代码

java 复制代码
class Solution {
    // 存储最终的字母组合结果集
    List<String> res = new ArrayList<>();
    // 临时存储当前构建的字母组合字符串
    StringBuilder sb = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        // 边界条件:若输入数字字符串为空,直接返回空结果集
        if (digits.isEmpty()) {
            return res;
        }
        // 构建数字到字母的映射表,对应电话键盘的布局
        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");
        // 从数字字符串的第0个位置开始递归遍历构建组合
        traverseTree(digits, 0, map);
        return res;
    }

    public void traverseTree(String digits, int index, Map<Character, String> map) {
        // 终止条件:当前构建的字符串长度等于数字字符串长度,说明已完成一个组合
        if (digits.length() == sb.length()) {
            res.add(sb.toString()); // 将当前组合加入结果集
            return; // 结束当前递归分支,返回上一层
        }
        // 获取当前索引对应的数字字符,并得到其映射的字母字符串
        String str = map.get(digits.charAt(index));
        // 遍历当前数字映射的每个字母,尝试加入组合
        for (int i = 0; i < str.length(); i++) {
            sb.append(str.charAt(i)); // 选择当前字母,加入临时字符串
            traverseTree(digits, index + 1, map); // 递归处理下一个数字字符
            sb.deleteCharAt(sb.length() - 1); // 回溯:撤销选择,删除最后一个字母
        }
    }
}

1. 为什么不能直接用 String?

因为 Java 中 String 是不可变的!每次 String s = s + "a" 都会创建一个新的字符串对象,非常浪费内存、速度慢。

回溯算法需要大量拼接、删除字符,用 String 会:

  • 产生大量垃圾对象
  • 效率极低
  • 代码麻烦(每次都要新建)

2. 为什么要用 StringBuilder?

它是可变字符串,专门用来:

  • 高效拼接
  • 高效删除
  • 原地修改,不产生新对象
    回溯里这两步必须靠它:
java 复制代码
sb.append(字符);   // 加字符(选择)
sb.deleteCharAt(); // 删最后一个(回溯撤销)

3. StringBuilder vs StringBuffer 区别

  • StringBuilder:非线程安全,速度更快 → 刷题首选
  • StringBuffer:线程安全,速度慢一点 → 多线程

3.例子

digits = "23" 为例子

输入:23 → 数字 2 → abc,数字 3 → def


初始状态

res = []

sb = 空

开始执行 traverseTree("23", 0, map)


第1层递归:index = 0(处理数字 '2' → "abc")

sb 长度 0 ≠ 2 → 继续

str = "abc"

进入循环 i = 0, 1, 2

i = 0,字符 'a'

  1. sb.append('a') → sb = "a"
  2. 递归进入 traverseTree("23", 1, map)

第2层递归:index = 1(处理数字 '3' → "def")

sb 长度 1 ≠ 2 → 继续

str = "def"

进入循环 i = 0,1,2

i=0,字符 'd'

  1. sb.append('d') → sb = "ad"
  2. 递归进入 traverseTree("23", 2, map)

第3层递归:index=2

sb 长度 2 == 2

res.add("ad") → res = ["ad"]

返回

  1. sb.deleteCharAt → sb = "a"

i=1,字符 'e'

  1. sb.append('e') → sb = "ae"
  2. 递归 → index=2 → 加入结果
    res = ["ad","ae"]
  3. 删除 → sb = "a"

i=2,字符 'f'

  1. sb.append('f') → sb = "af"
  2. 递归 → 加入结果
    res = ["ad","ae","af"]
  3. 删除 → sb = "a"

回到第2层循环结束

回到第1层

第1层 i=0 结束:

sb.deleteCharAt → sb = ""


第1层 i=1,字符 'b'

  1. sb.append('b') → sb = "b"
  2. 递归 index=1(处理3)
    循环 d/e/f
    得到:
    "bd", "be", "bf"
    加入 res
  3. 删除 → sb = ""

第1层 i=2,字符 'c'

  1. sb.append('c') → sb = "c"
  2. 递归 index=1(处理3)
    循环 d/e/f
    得到:
    "cd","ce","cf"
    加入 res
  3. 删除 → sb = ""

最终结果

复制代码
["ad","ae","af","bd","be","bf","cd","ce","cf"]

四、组合总和

1.题目

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7

输出:[[2,2,3],[7]]

解释:

2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。

7 也是一个候选, 7 = 7 。

仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8

输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1

输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

2.代码

java 复制代码
class Solution {
    // 主函数
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> combine = new ArrayList<>();
        dfs(candidates, target, ans, combine, 0);
        return ans;
    }

    // 回溯DFS
    public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {
        // 遍历完数组,返回
        if (idx == candidates.length) return;
        
        // 找到有效组合,加入结果
        if (target == 0) {
            ans.add(new ArrayList<>(combine));
            return;
        }

        // 不选当前数
        dfs(candidates, target, ans, combine, idx + 1);

        // 选当前数
        if (target >= candidates[idx]) {
            combine.add(candidates[idx]);
            dfs(candidates, target - candidates[idx], ans, combine, idx);
            combine.remove(combine.size() - 1); // 回溯
        }
    }
}

3.例子

candidates = [2,3,5]target = 8 为例

输入:[2,3,5], target=8

代码执行顺序:先不选 → 再选


初始状态

ans = []

combine = []

调用:dfs(8, [], 0)


第1层:idx=0,数字=2,target=8

  1. 没到头,target≠0
  2. 先走:不选 2
    → 进入 dfs(8, [], 1)

第2层:idx=1,数字=3,target=8

  1. 没到头,target≠0
  2. 先走:不选 3
    → 进入 dfs(8, [], 2)

第3层:idx=2,数字=5,target=8

  1. 没到头,target≠0

  2. 先走:不选 5

    → 进入 dfs(8, [], 3)

    → idx=3 越界,返回

  3. 选 5 :8≥5

    combine.add(5) → [5]

    进入 dfs(3, [5], 2)

    • 不选5 → 返回
    • 3<5 → 不能选
      返回
      combine.remove → []

回到第2层


回到第2层:idx=1,数字=3,target=8

  1. 选 3 :8≥3
    combine.add(3) → [3]
    进入 dfs(5, [3], 1)

第3层:idx=1,数字=3,target=5

  1. 不选 3 → dfs(5, [3], 2)

第4层:idx=2,数字=5,target=5

  1. 不选5 → 返回

  2. 选5 :5≥5

    combine.add(5) → [3,5]

    进入 dfs(0, [3,5], 2)

    ✅ target=0

    ans.add([3,5])
    ans = [[3,5]]

    回溯删除 → [3]

    返回

回到第3层

  1. 选3:5≥3 → combine=[3,3]
    进入 dfs(2, [3,3],1)
    2<3,不选,返回
    回溯删除 → [3]

返回第2层

回溯删除 → []


回到第1层:idx=0,数字=2,target=8

  1. 选 2 :8≥2
    combine.add(2) → [2]
    进入 dfs(6, [2], 0)

第2层:idx=0,数字=2,target=6

  1. 不选 2 → dfs(6, [2], 1)

第3层:idx=1,数字=3,target=6

  1. 不选3 → 无结果
  2. 选3 :6≥3
    combine.add(3) → [2,3]
    进入 dfs(3, [2,3], 1)

第4层:idx=1,数字=3,target=3

  1. 不选3 → 无结果
  2. 选3 :3≥3
    combine.add(3) → [2,3,3]
    进入 dfs(0, [2,3,3],1)
    ✅ target=0
    ans.add([2,3,3])
    ans = [[3,5], [2,3,3]]

回溯删除 → [2,3] → [2]


回到第2层:idx=0,数字=2,target=6

  1. 选 2 :6≥2
    combine.add(2) → [2,2]
    进入 dfs(4, [2,2], 0)

第3层:idx=0,数字=2,target=4

选2 → [2,2,2]

进入 dfs(2, [2,2,2], 0)


第4层:idx=0,数字=2,target=2

选2 → [2,2,2,2]

进入 dfs(0, ...)

✅ target=0

ans.add([2,2,2,2])
ans = [[3,5], [2,3,3], [2,2,2,2]]


最终结果:
[[3,5], [2,3,3], [2,2,2,2]]


如果本篇文章对您有帮助,可以点赞,收藏或评论哦!!!关注主包不迷路,让我们一起向前进步吧!!

相关推荐
tobias.b2 小时前
深度学习 超清晰通俗讲解 + 核心算法 + 使用场景
人工智能·深度学习·算法
七夜zippoe2 小时前
量子计算入门:Qiskit框架实战
python·算法·量子计算·ibm·qiskit
小此方2 小时前
Re:从零开始的 C++ STL篇(八)深度解构AVL树自平衡机制:平衡维护与旋转调整背后的严密逻辑
开发语言·数据结构·c++·算法·stl
2301_789015622 小时前
封装哈希表实现unordered_set/undered_map
c语言·数据结构·c++·算法·哈希算法
落羽的落羽2 小时前
【Linux系统】中断机制、用户态与内核态、虚拟地址与页表的本质
java·linux·服务器·c++·人工智能·算法·机器学习
拄杖忙学轻声码2 小时前
maven引入本地jar包示例(非仓库引入)
java·maven·jar
神工坊2 小时前
技术分享︱多重参考系模型在风扇通风仿真中的自动化实现:精度与效率的工程平衡
算法·hpc·并行计算·cfd·cae·流体力学·风扇仿真
独断万古他化2 小时前
【算法通关】递归:汉诺塔、合并链表、反转链表、两两交换、快速幂全解
数据结构·算法·链表·递归
lierenvip2 小时前
Spring Boot 自动配置
java·spring boot·后端