【leetcode刷题记录】(java)回溯

刷题参考代码随想录:https://programmercarl.com/
在这里插入图片描述

文章目录

回溯

回溯算法的本质

https://leetcode.cn/problems/palindrome-partitioning/solutions/3066497/hui-su-suan-fa-de-ben-zhi-mo-ni-you-xian-fhcb/

  • 模拟有限for循环的过程

  • 可以这么理解:在回溯算法中,我们写的那个 for 循环嵌套着递归调用,相当于模拟了多个嵌套的 for 循环。让我举个例子来说明:假设我们的字符串是 “abc”。如果不使用递归来解决划分问题,我们可能需要写出多重嵌套的 for 循环,每一层迭代一个可能的分割点,比如:

  • for (int i = 0; i < s.length(); i++){ 
    // 第一层:选择第一个分割 // ... 
        for (int j = i+1; j < s.length(); j++){ 
        // 第二层:选择第二个分割 // ... 
            for (int k = j+1; k < s.length(); k++) {
             // 第三层:选择第三个分割(如果需要) // ... 
             } 
        } 
    }
    

    当然,当字符串长度不固定时,这种写法就不现实了。而递归的巧妙之处在于:

    每一次递归调用其实都在模拟“一层新的 for 循环” ,它从当前的 startIndex 开始,一直到字符串末尾。

    递归调用结束的条件(例如 startIndex == s.length())确保了递归不无限进行,而是找到一个划分方式后回溯,继续寻找其他可能。

    换句话说,每一次 for 循环内部的递归调用,都会为剩余部分开启一个新的迭代过程,就像在无限制数量的 for 循环中不断展开选择。所以,我们通过递归和 for 循环的结合,可以实现一种灵活而高效的“无限嵌套”的遍历,枚举出所有可能的解。

回溯算法模板

  • Carl给出的回溯算法模板:
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

77. 组合

https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html#%E6%80%9D%E8%B7%AF

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

回溯

https://leetcode.cn/problems/combinations/solutions/13436/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/

77.组合3

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res =new LinkedList<>();
        Deque<Integer> path=new ArrayDeque<>();
        dfs(res,path,n,k,1);//startindex代表的是起始的数字 组合的数字 以数字1开始
        return res;
    }
    private void dfs(List<List<Integer>> res,Deque<Integer> path,int n,int k,int startIndex){//startIndex 就是防止出现重复的组合。
        if(path.size()==k){//path是存储深度优先遍历时候  从根部到叶子结点路径的所有节点的值,如果达到树高(k) 就代表可以添加到最后的res数组中              (path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了)
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex;i<=n;i++){
            path.add(i);//添加向下延伸的路径中的第一个节点的值
            dfs(res,path,n,k,i+1);//深度优先  向下遍历     ,startindex变为i+1
            path.removeLast();//回溯 删除已经加入path组的元素,之后添加新的元素 组成新的组(旧的组已经加入到res中)
             // removeLast  是 回溯,撤销处理的节点
        }
    }
}

回溯法三部曲

  • 递归函数的返回值以及参数

在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。

代码如下:

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果

剪枝优化

当n=4 k=4的时候,startindex从2开始就是没有意义的遍历,1之后的startindex是i+1赋予的,所以可以通过限制i的范围来剪枝优化

(下图中是取4个数)

77.组合4

举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。

从1开始搜索都是合理的,可以是组合[1, 2, 3]。

从2开始搜索都是合理的,可以是组合[2, 3, 4]。

从3开始搜索不合理

i <= n - (k - path.size()) + 1 限制

216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/)

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

提示:

  • 2 <= k <= 9
  • 1 <= n <= 60

代码

//77  返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。
//216  返回范围 `[1, 9]` 中所有可能的 `k` 个数的组合,而且和为n。
class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> res=new ArrayList<>();
        if(n<k){//4个数之和为3不成立
            return res;
        }
        Deque<Integer> path=new ArrayDeque<>();
        dfs(k,n,res,path,1);
        return res;
    }
	//77  返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。
    //216  返回范围 `[1, 9]` 中所有可能的 `k` 个数的组合,而且和为n。
    private void dfs(int k, int n,List<List<Integer>> res,Deque<Integer> path,int startindex) {
        if(path.size()==k){
            int sum=0;
            for(int num:path){
                sum+=num;
            }
            if(sum==n){//相比77题就是加了筛选的条件 只有 长度为k 而且 和为n 的组合才可以加入res
                res.add(new ArrayList<>(path));
            }
        }
        for(int i=startindex;i<=9;i++){
            path.add(i);
            dfs(k,n,res,path,i+1);
            path.removeLast();
        }
    }
}

17. 电话号码的字母组合

https://programmercarl.com/0017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88.html#%E6%80%9D%E8%B7%AF

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

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

img

示例 1:

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

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

代码

17. 电话号码的字母组合

理解本题后,要解决如下三个问题:

  1. 数字和字母如何映射
  2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
  3. 输入1 * #按键等等异常情况
class Solution {
     //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
    String[] MAPPING = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };

    public List<String> letterCombinations(String digits) {
        List<String> res = new ArrayList<>();
        if (digits == null || digits.length() == 0) {
            return res;
        }
        //每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
        StringBuffer temp = new StringBuffer();
        //迭代处理
        dfs(res, temp, digits, 0);
        return res;
    }

    void dfs(List<String> res, StringBuffer temp, String digits, int num) {
        if (temp.length() == digits.length()) {//确定终止条件
            res.add(temp.toString());
            return;
        }
        //比如digits如果为"23",num 为0,则str表示2对应的 abc
        String str = MAPPING[digits.charAt(num) - '0'];//str 表示当前num对应的字符串
        //digits[0]='2' ,之后转化为数字2,str取MAPPING[2]='adc'
        for (int i = 0; i < str.length(); i++) {
            temp.append(str.charAt(i));
            //'adc'中取一个,之后num+1 ,在‘bcd’中再取一个
            dfs(res, temp, digits, num + 1);//递归,处理下一层
            temp.deleteCharAt(temp.length() - 1);//剔除末尾的,继续尝试
        }

    }
}

39. 组合总和

给你一个 无重复元素 的整数数组 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

代码:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        Deque<Integer> path =new ArrayDeque<>();
        // Arrays.sort(candidates); // 先进行排序
        dfs(res,path,candidates,target,0,0);
        return res;
    }

    public void dfs(List<List<Integer>> res,Deque<Integer> path,int[] candidates,int target,int index,int sum){
        if(sum==target){// 找到了数字和为 target 的组合
            res.add(new ArrayList<>(path));
            return;
        }
        if(sum>target){return;}//剪枝优化
        for (int i=index;i<candidates.length;i++){
            path.add(candidates[i]);
            //一定一定要注意 下面这里dfs传入的参数是i而非index
            //之所以传入的不是i+1是因为数字可以重复,
            dfs(res,path,candidates,target,i,sum+candidates[i]);//注意这里的sum,后面没有sum-什么什么
            path.removeLast();// 回溯,移除路径 path 最后一个元素
        }
    }
}

注意回溯的过程 ,允许数字的重复怎么办:

图中

  • 上图中绿线的效果是由代码段:for (int i=index;i<candidates.length;i++){ 中的i++引起的
  • 上图中蓝线的效果,之所以上面253 下面还是253,是因为 dfs(res,path,candidates,target,i,sum+candidates[i]); 里的i没有像之前一样加1
  • 这里的代码段:for (int i=index;i<candidates.length;i++){ 中的i++ 控制了横向的和 竖向的,图中的绿色和蓝色的标识,影响树枝 和 树层的去重

注意sum的处理

注意这里面的处理:

        for (int i=index;i<candidates.length;i++){
            path.add(candidates[i]);
            sum+=candidates[i];
            dfs(res,path,candidates,i,sum,target);	
            sum-=candidates[i];
            path.removeLast();
        }

可以换成:

        for (int i=index;i<candidates.length;i++){
            path.add(candidates[i]);
            dfs(res,path,candidates,i,sum+candidates[i],target);//注意这里的sum,后面没有sum-什么什么
            path.removeLast();
        }

注意下面两种剪枝优化的效果对比:

image-20250209171543443

image-20250209171604329

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

**注意:**解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

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

提示:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

代码:

错误代码 (同时把树层和树枝都去重了):

image-20250209181716603

漏掉了类似1,1,6这种情况

正确代码:
class Solution {
   public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        Deque<Integer> path =new ArrayDeque<>();
        Arrays.sort(candidates); // 先进行排序  这个是必要的步骤,为下面的>target  剪枝做基础
        boolean []used=new boolean[candidates.length];
        // Arrays.fill(used, false);
        dfs(res,path,candidates,target,used,0,0);
        return res;
    }
    public void dfs(List<List<Integer>> res, Deque<Integer> path, int[] candidates, int target,boolean []used, int index, int sum){
        if(sum==target){// 找到了数字和为 target 的组合
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i=index;i<candidates.length;i++){//注意 
            if(i!=0 && candidates[i]==candidates[i-1] && used[i-1]==false){
                continue;
            }
            if(sum+candidates[i]>target){break;}//剪枝优化
            path.add(candidates[i]);
            used[i]=true;
            //一定一定要注意 下面这里dfs传入的参数是i而非index
            //之所以传入的不是i+1是因为数字可以重复,
            dfs(res,path,candidates,target,used,i+1,sum+candidates[i]);//注意这里的sum,后面没有sum-什么什么
            used[i]=false;
            path.removeLast();// 回溯,移除路径 path 最后一个元素
        }
    }
}

booleanBoolean 的区别!

image-20250209182417942

  • Boolean[] used = new Boolean[candidates.length];  //数组中的每个元素都会被初始化为 `null`。
    boolean[] used = new boolean[candidates.length];  //数组中的每个元素都会被初始化为 `false`。
    

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是

回文串

。返回 s 所有可能的分割方案。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

代码

class Solution {
    List<List<String>> res = new ArrayList<>();

    public List<List<String>> partition(String s) {
        if (s == null || s.length() == 0)
            return null;
        List<String> path = new ArrayList<>();
        dfs(path, s, 0);
        return res;
    }

    public void dfs(List<String> path, String s, int startindex) {
        if (startindex == s.length()) {// 已经划分到最后一个字母
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = startindex; i < s.length(); i++) {
            String sub = s.substring(startindex, i + 1);//前闭后开
            if (isHuiwen(sub)) {//注意这里先判断回文 回文是后续的基础
                path.add(sub);
                dfs(path, s, i + 1);
                path.remove(path.size() - 1);
            }
        }
    }

    public boolean isHuiwen(String sub) {
        int len = sub.length();
        for (int i = 0; i < len / 2; i++) {
            if (sub.charAt(i) != sub.charAt(len - 1 - i)) {
                return false;
            }
        }
        return true;
    }
}

93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" "192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:

  • 1 <= s.length <= 20
  • s 仅由数字组成

代码

class Solution {
    List<String> res = new ArrayList<>();
    public List<String> restoreIpAddresses(String s) {
        StringBuilder sb =new StringBuilder(s);
        backTracking(sb,0,0);
        return res;
    }
    private void backTracking(StringBuilder s, int startIndex, int dotCount){
        if (dotCount == 3) {
            if(isValid(s,startIndex,s.length()-1)){
                res.add(s.toString());
                return;
            }
        }
        for (int i=startIndex;i<s.length();i++){
            if(isValid(s,startIndex,i)){
                s.insert(i+1,".");
                backTracking(s,i+2,dotCount+1);//注意这里 因为加了逗号  所以需要在原来加1的基础上再加1
                s.deleteCharAt(i+1);
            }
        }
    }
    //[start, end]  都是闭区间
    private boolean isValid(StringBuilder s, int start, int end){
        if(start>end)return false;
        if(s.charAt(start) == '0' && start != end){
            return false;
        }
        int num=0;
        for(int i=start;i<=end;i++){
            num=num*10+(s.charAt(i)-'0');
            if(num>255)return false;
        }
        return true;
    }
}

78. 子集

给你一个整数数组 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 中的所有元素 互不相同

代码

如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!

https://programmercarl.com/0078.%E5%AD%90%E9%9B%86.html#%E6%80%9D%E8%B7%AF

78.子集

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        List<Integer> path=new ArrayList<>();
        backTracking(nums,path,0);
        return res;
    }

    public void backTracking(int[] nums,List<Integer> path,int startindex){
        res.add(new ArrayList<>(path));//注意这里不能直接写path
        for(int i=startindex;i< nums.length;i++){
            path.add(nums[i]);
            backTracking(nums,path,i+1);
            path.remove(path.size()-1);
        }
    }
}

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的

子集(幂集)。

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

示例 1:

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

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

代码1:使用used去重

90.子集II

class Solution {
 List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<Integer> path=new ArrayList<>();
        Arrays.sort(nums); // 先进行排序  这是必要的步骤,使相同的数字排列在一起
        boolean []used=new boolean[nums.length];
        backTracking(nums,path,used,0);
        return res;
    }
    public void backTracking(int[] nums,List<Integer> path,boolean []used,int startindex){
        res.add(new ArrayList<>(path));
        for(int i=startindex;i< nums.length;i++){
            if(i!=0 && nums[i]==nums[i-1] && used[i-1]==false){//注意这里仅仅是对树层的去重***
                continue;
            }
            path.add(nums[i]);
            used[i]=true;//注意不要遗忘了这里
            backTracking(nums,path,used,i+1);//注意这里的i+1 不要写成index,注意当初始集合里面的数字要求不能重复使用的时候,这里必须是i+1;  如果初始集合里面的数字可以重复使用,这里是i;
            used[i]=false;
            path.remove(path.size()-1);
        }
    }
}

代码2:使用hashset去重

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<Integer> path = new ArrayList<>();
        Arrays.sort(nums); // 先进行排序
        backTracking(nums, path, 0);
        return res;
    }
    public void backTracking(int[] nums, List<Integer> path, int startindex) {
        res.add(new ArrayList<>(path));
        HashSet<Integer> hs = new HashSet<>();
        for (int i = startindex; i < nums.length; i++) {
            if (hs.contains(nums[i])) {
                continue;
            }
            hs.add(nums[i]);
            path.add(nums[i]);
            backTracking(nums, path, i + 1);
            path.remove(path.size() - 1);
        }
    }
}

对于已经习惯写回溯的同学,看到递归函数上面的hs.add(nums[i]);,下面却没有对应的pop之类的操作,应该很不习惯吧

这也是需要注意的点,hs.add(nums[i]);; 是记录本层元素是否重复使用,新的一层hs都会重新定义(清空)(每次进入递归的时候hs会被重新定义),所以要知道hs只负责本层!

491. 非递减子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

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

思路

而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。

所以不能使用之前的去重逻辑! 使用used没有用

代码

image-20250210160706282

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        List<Integer> path=new ArrayList<>();
        backTracking(nums,path,0);
        return res;
    }

    private void backTracking(int[] nums,List<Integer> path,int startindex){
        if(path.size()>=2){
            res.add(new ArrayList<>(path));
        }
        HashSet<Integer> hs=new HashSet<>();
        for(int i=startindex;i<nums.length;i++){
            //nums[i]<path.get(path.size()-1) 新的元素如果比path里面最后一个元素小 (控制上图中的蓝色圈)
            //hs.contains(nums[i])            hs控制的是每层的去重(上图中的绿色标识)
            if(!path.isEmpty() && nums[i]<path.get(path.size()-1) || hs.contains(nums[i])){//前面两句先运算
                continue;
            }
            hs.add(nums[i]);
            path.add(nums[i]);
            backTracking(nums,path,i+1);
            //注意这里不需要pop之类的操作
            path.remove(path.size()-1);
        }
    }
}

对于已经习惯写回溯的同学,看到递归函数上面的hs.add(nums[i]);,下面却没有对应的pop之类的操作,应该很不习惯吧

这也是需要注意的点,hs.add(nums[i]);; 是记录本层元素是否重复使用,新的一层hs都会重新定义(清空)(每次进入递归的时候hs会被重新定义),所以要知道hs只负责本层!

46. 全排列 注意Used数组的使用

给定一个不含重复数字的数组 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 中的所有整数 互不相同

代码

全排列

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        List<Integer> path=new ArrayList<>();
        boolean []used =new boolean[nums.length];//注意这里used的使用是必要的
        backTracking(nums,path,used);
        return res;
    }
    private void backTracking(int[] nums,List<Integer> path,boolean []used){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
        }
        for(int i=0;i<nums.length;i++){
            if(used[i]==true)continue;
            path.add(nums[i]);
            used[i]=true;
            backTracking(nums,path,used);
            used[i]=false;
            path.remove(path.size()-1);
        }
    }
}

我们需要引入一个布尔类型的数组 used 来标记每个元素是否已经在当前排列中被使用。在递归调用时,只选择那些未被使用的元素。

错误代码问题分析

image-20250210165735941

这段代码出现 StackOverflowError 错误,主要原因在于递归调用没有合理控制元素的使用,导致无限递归。具体来说,在生成排列的过程中,没有对已经使用过的元素进行标记和排除,每次递归时都会尝试将所有元素添加到排列中,使得递归无法正常终止。

  • 缺乏元素使用标记:**在 backTracking 方法的 for 循环里,每次递归调用都会尝试将 nums 数组中的所有元素添加到 path 中,**这就造成同一个元素可能被多次使用,从而引发无限递归。
  • 未正确终止递归:虽然代码里有递归终止条件 if(path.size() == nums.length),但由于元素使用的重复问题,这个条件很难被有效触发,进而导致栈溢出错误。

47. 全排列 II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

思路分析:

47.全排列II1

这道题目和46.全排列 (opens new window)的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列

去重法1:used(一般都需要先排序)

还要强调的是使用used数组去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了

image-20250210172723323

去重法2:hashset(更通用)

image-20250210172819210

代码:

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<Integer> path=new ArrayList<>();
        boolean []used =new boolean[nums.length];//注意这里used的使用是必要的
        backTracking(nums,path,used);
        return res;
    }
    private void backTracking(int[] nums,List<Integer> path,boolean []used){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
        }
        HashSet<Integer> hs=new HashSet<>();
        for(int i=0;i<nums.length;i++){
            if(used[i]==true || hs.contains(nums[i]))continue;
            hs.add(nums[i]);
            path.add(nums[i]);
            used[i]=true;
            backTracking(nums,path,used);
            used[i]=false;
            path.remove(path.size()-1);
        }
    }
}
class Solution {
    int res = 0;
    Map<Integer, Integer> path = new HashMap<>();
    public int beautifulSubsets(int[] nums, int k) {
        backTracking(nums, 0, k);
        return res;
    }
    public void backTracking(int[] nums, int startindex, int k) {
        if (!path.isEmpty()) {// 重要
            res++;
        }
        for (int i = startindex; i < nums.length; i++) {
            // 如果(新元素值 +k 或者-k) 不在path 中,那么加入
            if (path.getOrDefault(nums[i] - k, 0) == 0 && path.getOrDefault(nums[i] + k, 0) == 0) {
                path.merge(nums[i], 1, Integer::sum);// 元素
                backTracking(nums, i + 1, k);
                path.merge(nums[i], -1, Integer::sum);
            }
        }
    }
}

其他

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]
class Solution {
    List<String> res=new ArrayList<>();

    public List<String> generateParenthesis(int n) {
        String path=""; //从根结点到叶子结点的路径字符串
        dfs(path,n,n);
        return res;
    }
    private void dfs(String path,int left,int right){//左括号还可以使用的个数
        if(left==0 && right==0){
            res.add(path);
        }else if(left>right){
            return;
        }
        if(left>0){
            dfs(path+'(',left-1,right);
        }
        if(right>0){
            dfs(path+')',left,right-1);
        }
    }
}

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

img

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

img

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true

code

https://leetcode.cn/problems/word-search/solutions/2361646/79-dan-ci-sou-suo-hui-su-qing-xi-tu-jie-5yui2/?envType=study-plan-v2&envId=top-100-liked

https://leetcode.cn/problems/word-search/solutions/2927294/liang-ge-you-hua-rang-dai-ma-ji-bai-jie-g3mmm/?envType=study-plan-v2&envId=top-100-liked


class Solution {
    int dirs[][] = new int[][]{{0, 1}, {-1, 0}, {0, -1}, {1, 0}};

    public boolean exist(char[][] board, String word) {
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (dfs(board, word, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    boolean dfs(char[][] board, String word, int i, int j, int k) {
        if (board[i][j] != word.charAt(k)) {
            return false;
        }
        if (k == word.length()-1) {
            return true;
        }
        board[i][j] = 0;//走过路设置为0  下次就不走了 因为 board[i][j]=0  != word.charAt(k)
        for (int[] d : dirs) {
            int x = i + d[0];
            int y = j + d[1]; // 相邻格子
            if (0 <= x && x < board.length && 0 <= y && y < board[x].length && dfs(board, word, x, y, k + 1)) {
                return true; // 搜到了!
            }
        }
        board[i][j] = word.charAt(k);//恢复原来的样子
        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~柠月如风~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值