组合(77+ 39 + 40 + 17) + 排列(46 + 47) + 子集(78) + 分割(131 + 93)

一、组合问题

组合问题里集合是无序的,取过的元素不会重复取,所以for循环遍历的时候要从startIndex开始。排列问题可以重复,从0开始。

77. 组合

集合中没有重复的元素,元素只能使用一次。解集不能包含重复的组合。
在这里插入图片描述

var combine = function(n, k) {
    // result存放符合条件的所有结果,path存放单一结果
    let result = [], path = [];

    // 递归函数,遍历决策二叉树。从集合n里取k个数,n决定了树的宽度,k决定了树的深度
    function backtrack(n,k,startIndex){
        // 终止条件,即到达所谓的叶子节点(path里的长度为k),收集结果。
        if(path.length == k){
            result.push([...path])
            return
        }

        // 单层搜索逻辑,for循环用于横向遍历,递归用于纵向遍历
        for(let i=startIndex;i<=n;i++){
            path.push(i) // 做出选择
            backtrack(n,k,i+1) // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
            path.pop() // 回溯,撤销选择
        }
    }
    backtrack(n,k,1)
    return result
};

39. 组合总和

集合中没有重复的元素,但是元素可以使用多次。解集不能包含重复的组合。
在这里插入图片描述

var combinationSum = function(candidates, target) {
    let result = [], path = [];

    // 1、确定递归函数的参数:题目中的参数+遍历的起始位置+path中的和
    function backtrace(candidates,target,startIndex,sum){
        // 2、终止条件
        if(sum>target){
            return
        }
        if(sum == target){
            result.push([...path])
            return
        }

        // 3、单层搜索
        for(let i=startIndex; i<candidates.length; i++){
            sum += candidates[i]  // 计算做出选择后的总和
            path.push(candidates[i]) // 做出选择
            backtrace(candidates,target,i,sum)  // 纵向可以重复选择,不需要i+1
            sum -= candidates[i] // 回溯
            path.pop()
        }
    }
    backtrace(candidates,target,0,0)
    return result
};

40. 组合总和 II (去重操作)

集合中有重复的元素,每个元素只能使用一次,但是解集不能包含重复的组合。所以要去重的是“同一树层上的使用过”。
在这里插入图片描述
在这里插入图片描述

var combinationSum2 = function(candidates, target) {
    let result = [], path = [];
    // used布尔型数组,记录元素使用情况,从而判断是在同一树枝还是树层
    const used = new Array(candidates.length).fill(false)

    // 1、确定递归函数的参数:题目中的参数+遍历的起始位置+path中的和+
    function backtrace(candidates,target,startIndex,sum,used){
        // 2、终止条件
        if(sum>target){
            return
        }
        if(sum == target){
            result.push([...path])
            return
        }

        // 3、单层搜索
        for(let i=startIndex; i<candidates.length; i++){
            // 要对同一树层使用过的元素进行跳过
            if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false){
                continue;
            }
            sum += candidates[i]  // 计算做出选择后的总和
            path.push(candidates[i]) // 做出选择
            used[i] = true
            backtrace(candidates,target,i+1,sum,used)  // 纵向可以重复选择,不需要i+1
            used[i] = false
            sum -= candidates[i] // 回溯
            path.pop()
        }
    }
    // 首先把给candidates排序,让其相同的元素都挨在一起。
    candidates.sort((a,b) => a-b)
    backtrace(candidates,target,0,0,used)
    return result
};

17. 电话号码的字母组合

和上面的组合问题不同,这里对应了不同数量的组合。横向是遍历其中一个组合,所以i从0开始。纵向的回溯是用于进入下一个组合里,所以此时需要的是控制不同组合的index。
在这里插入图片描述

var letterCombinations = function(digits) {
    // 定义字符数组,每个下标代表从0~9号码对应的字符
    const letterMap = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
    let result = [], path = [];

    if(!digits.length) return []  // 空字符情况

    // 1、参数:digits + 记录遍历到digits里第几个数字 index
    function backtrack(digits,index){
        // 2、终止条件:比如23就是遍历两层
        if(index == digits.length){
            result.push(path.join("")) // 在这里把数组转换成字符串
            return
        }

        // 3、单层遍历逻辑
        let num = digits[index] - '0' // 将index指向的数字字符'2'转为int
        let letters = letterMap[num]  // 拿到数字对应的字符集'abc'

        for(let i=0;i<letters.length;i++){  // 遍历拿到的字符集
            path.push(letters[i])
            backtrack(digits,index+1)  // 处理下一次数字'3'
            path.pop()
        }
    }
    backtrack(digits,0)
    return result
};

二、子集问题

78. 子集

组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!所以result.push([…path])无需判断每次递归都要收集。
子集也是一种组合问题,无序,for就要从startIndex开始,而不是从0开始!
在这里插入图片描述

var subsets = function(nums) {
    let result = [], path = [];

    function backtrack(nums,startIndex){
        // 单层递归结束条件,剩余集合为空,就是for循环结束条件
        if(startIndex > nums.length) return 

        // 子集问题每一层都要收集
        result.push([...path])

        // 单层搜索逻辑
        for(let i=startIndex;i<nums.length;i++){
            path.push(nums[i])
            backtrack(nums,i+1)
            path.pop()
        }
    }
    backtrack(nums,0)
    return result
};

三、排列问题

46. 全排列

在这里插入图片描述

// 由于是排列,所以[1,2]和[2,1]是两个集合。不需要startIndex
var permute = function(nums) {
    let result = [], path = []
    const used = new Array(nums.length).fill(false)

    // 1、确定参数:nums + used
    function backtrack(nums,used){
        // 2、终止条件
        if(path.length == nums.length){
            result.push([...path])
        }

        // 3、单层搜索
        for(let i=0; i<nums.length; i++){
            // 由于没有startIndex控制横向for循环的次序,所以需要使用used数组。
            // used为true,代表已经遍历,跳过
            if(used[i] == true) continue

            used[i] = true
            path.push(nums[i])
            backtrack(nums,used)
            used[i] = false
            path.pop()
        }
    }
    backtrack(nums,used)
    return result
};

47. 全排列 II

题目 : 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
解析:和40. 组合总和 II 一样,由于序列有重复数字,所以需要去重。去重是树层的重复:if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false) continue,并且还需要排序

在这里插入图片描述

var permuteUnique = function(nums) {
    let result = [], path = []
    const used = new Array(nums.length).fill(false)

    // 1、确定参数:nums + used
    function backtrack(nums,used){
        // 2、终止条件
        if(path.length == nums.length){
            result.push([...path])
        }

        // 3、单层搜索
        for(let i=0; i<nums.length; i++){
            // 由于没有startIndex控制横向for循环的次序,所以需要使用used数组。
            // 去重操作:同一树层的需要跳过
            if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false) continue

            // 没有遍历过的才需要,对应used[i]为true时跳过
            if(used[i] == false){
                used[i] = true
                path.push(nums[i])
                backtrack(nums,used)
                path.pop()
                used[i] = false
            }
        }
    }
    // 需要排序
    nums.sort((a,b) => a-b)
    backtrack(nums,used)
    return result
};

四、分割问题

131、分割回文串

切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的
在这里插入图片描述

// 判断回文子串
var isPalindrome = function(s,start,end){
    for(let i=start,j=end; i<j; i++,j--){
        if(s[i] != s[j]) return false
    }
    return true
}

var partition = function(s) {
    // path存放分割后的子串
    let path = [],result = [];

    // 递归函数
    function backtrack(s,startIndex){
        // 单层递归(每个竖向分支)结束条件:切割线切到字符串最后
        if(startIndex >= s.length){
            result.push(Array.from(path));
        }

        // 单层搜索逻辑
        for(let i=startIndex;i<s.length;i++){
            if(isPalindrome(s,startIndex,i)){  // 是回文子串
                // 获取[startIndex,i]在s中的子串
                let str = s.slice(startIndex,i+1)
                path.push(str)
            }else{
                continue // 跳过
            }
            backtrack(s,i+1)
            path.pop()
        }
    }
    backtrack(s,0)
    return result
};

93、复原IP地址

在这里插入图片描述

var restoreIpAddresses = function(s) {
    // path为分割后的字符,res存放最后所有结果
    let res = [], path = []

    function backtrack(s,startIndex){
        // 单层递归结束条件:必须分割为4份且已经遍历完字符串
        const len = path.length
        if(len > 4) return 
        if(len == 4 && startIndex == s.length){
            res.push(path.join("."))
            return 
        }

        // 单层搜索逻辑
        for(let i=startIndex;i<s.length;i++){
            // 拿到截取的子串
            const str = s.slice(startIndex,i+1)
            // 字符长度不能超过3,不能超过255
            if(str.length > 3 || str > 255) break
            // 0x/0xx这种异常,开头不能为0 
            if(str.length > 1 && str[0] == '0') break

            path.push(str)
            backtrack(s,i+1)
            path.pop()
        }
    }
    backtrack(s,0)
    return res
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值