LC-全排列、子集、电话号码的字母组合、组合总和、括号生成

全排列

回溯算法的基本思想是:

  1. 从数组的第一个元素开始,递归地交换每个位置上的元素,并从剩下的元素中选择下一个元素。
  2. 每当递归到最后一个元素时,表示找到一个排列,将其加入结果集。
  3. 在递归返回时,撤销操作(即回溯),恢复之前的状态,继续探索其他排列。
class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums,new ArrayList<>(),result);
        return result;
    }

    //回溯函数
    private void backtrack(int[] nums,List<Integer> tempList,List<List<Integer>> result){
        //如果tempList的大小和nums一样,说明已经构造出一个完整的排列
        if(tempList.size() == nums.length){
            result.add(new ArrayList<>(tempList));//将当前排列加入结果
            return;
        }

        for(int i = 0;i < nums.length;i++){
            //跳过已选择的元素
            if(tempList.contains(nums[i])){
                continue;
            }
            //选择当前元素
            tempList.add(nums[i]);
            //递归生成下一个元素的排列
            backtrack(nums,tempList,result);
            //撤销选择,回到上一个状态
            tempList.remove(tempList.size() - 1);
        }
    }
}

子集

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

    //回溯函数
    private void backtrack(int[] nums,int start,List<Integer> tempList,List<List<Integer>> result){
        result.add(new ArrayList<>(tempList));//将当前子集加入结果

        //从当前元素开始,逐个尝试每个元素
        for(int i = start;i < nums.length;i++){
            //将当前元素添加到子集中
            tempList.add(nums[i]);
            //递归进入下一个元素
            backtrack(nums,i + 1,tempList,result);
            //撤销选择,回到之前的状态
            tempList.remove(tempList.size() - 1);
        }
    }
}

电话号码的字母组合

可以使用回溯算法来穷举所有可能的字母组合。具体做法是:

  1. 遍历字符串中的每个数字,每个数字对应多个字母,逐个尝试选择每个字母,并将它们组合起来。
  2. 当选完所有数字的字母时,就得到了一个有效的字母组合。
  3. 使用回溯递归地生成每个字母组合,并最终返回所有组合。
class Solution {
    public List<String> letterCombinations(String digits) {
        List<String> result = new ArrayList<>();

        //边界条件:如果输入为空字符串,返回空列表
        if(digits == null || digits.length() == 0){
            return result;
        }

        // 定义数字与字母的映射关系
        String[] mapping = {
            "",     // '0' 没有对应字母
            "",     // '1' 没有对应字母
            "abc",   // '2' 对应 "abc"
            "def",   // '3' 对应 "def"
            "ghi",   // '4' 对应 "ghi"
            "jkl",   // '5' 对应 "jkl"
            "mno",   // '6' 对应 "mno"
            "pqrs",  // '7' 对应 "pqrs"
            "tuv",   // '8' 对应 "tuv"
            "wxyz"   // '9' 对应 "wxyz"
        };

        backtrack(digits,0,new StringBuilder(),result,mapping);

        return result;
    }


    private void backtrack(String digits,int index,StringBuilder current,List<String> result,String[] mapping){
        //如果current的长度等于digits的长度,说明已经找到一个组合,加入结果
        if(index == digits.length()){
            result.add(current.toString());
            return;
        }

        //获取当前数字对应的字母
        char digit = digits.charAt(index);
        String letters = mapping[digit - '0'];

        //遍历当前数字的所有字母
        for(int i = 0;i < letters.length();i++){
            //将当前字母添加到路径中
            current.append(letters.charAt(i));
            //递归处理下一个数字
            backtrack(digits,index + 1,current,result,mapping);
            //回溯,移除最后添加的字母
            current.deleteCharAt(current.length() - 1);
        }
    }
}

组合总和

  1. 递归回溯:尝试不同的数,构造出所有可能的组合。
  2. 剪枝优化
    1. 每个数可以被重复使用,所以递归时仍然从当前索引开始,而不是从 0 开始。
    2. 当当前组合的和大于 target 时,剪枝返回。
    3. 由于 candidates 中元素无重复,我们不需要额外去重。
class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> combination = new ArrayList<>();
        backtrack(candidates,target,0,combination,result);
        return result;
    }

    private void backtrack(int[] candidates,int target,int start,List<Integer> combination,List<List<Integer>> result){
        //终止条件:当前组合的和正好等于target
        if(target == 0){
            result.add(new ArrayList<>(combination));//记录当前组合
            return;
        }

        for(int i = start;i < candidates.length;i++){
            if(target - candidates[i] < 0){
                //剪枝:如果剩余的target小于0,则停止搜索
                continue;
            }
            //选择当前数字
            combination.add(candidates[i]);

            //递归尝试:因为可以重复,所以start仍然是i
            backtrack(candidates,target - candidates[i],i,combination,result);

            //回溯:撤销选择,尝试其他可能性
            combination.remove(combination.size() - 1);
        }
    }
}

括号生成

思路:

  1. 有效括号的定义:一个有效的括号组合需要满足:

    • 括号的数量是平衡的,即每个左括号 ( 都有一个对应的右括号 )
    • 在任何位置,右括号 ) 的数量不能超过左括号 ( 的数量。
  2. 回溯法:这个问题最常用的方法是回溯法(Backtracking)。我们可以构建一个递归过程,通过不断增加左括号 ( 或右括号 ),并在每一步判断是否满足条件来生成有效的括号组合。

解决步骤:

  1. 需要两个参数:

    • left:当前已经放入组合中的左括号的数量。
    • right:当前已经放入组合中的右括号的数量。
  2. 递归终止条件:

    • 如果 left == nright == n,说明当前生成的组合是一个有效的括号组合,加入结果列表。
  3. 递归选择:

    • 可以在当前组合中添加一个左括号 (,前提是 left < n
    • 可以在当前组合中添加一个右括号 ),前提是 right < left(即不能让右括号的数量超过左括号的数量)。
class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        backtrack(result,"",0,0,n);
        return result;
    }

    private void backtrack(List<String> result,String current,int left,int right,int n){
        //递归终止条件:当左右括号数量都达到n,说明生成了一个有效组合
        if(current.length() == 2 * n){
            result.add(current);
            return;
        }

        //如果左括号还可以继续添加
        if(left < n){
            backtrack(result,current + "(",left + 1,right,n);
        }

        //如果右括号的数量小于左括号的数量,可以添加右括号
        if(right < left){
            backtrack(result,current + ")",left,right + 1,n);
        }

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值