代码随想录算法训练营第二十七天 | LeetCode 93. 复原 IP 地址、78. 子集、90. 子集 II

代码随想录算法训练营第二十七天 | LeetCode 93. 复原 IP 地址、78. 子集、90. 子集 II

文章链接:复原IP地址 子集 子集II

视频链接:复原IP地址 子集 子集II

目录

[代码随想录算法训练营第二十七天 | LeetCode 93. 复原 IP 地址、78. 子集、90. 子集 II](#代码随想录算法训练营第二十七天 | LeetCode 93. 复原 IP 地址、78. 子集、90. 子集 II)

[1. LeetCode 93. 复原 IP 地址](#1. LeetCode 93. 复原 IP 地址)

[1.1 思路](#1.1 思路)

[1.2 代码](#1.2 代码)

[2. LeetCode 78. 子集](#2. LeetCode 78. 子集)

[2.1 思路](#2.1 思路)

[2.2 代码](#2.2 代码)

[3. LeetCode 90. 子集 II](#3. LeetCode 90. 子集 II)

[3.1 思路](#3.1 思路)

[3.2 代码](#3.2 代码)


1. LeetCode 93. 复原 IP 地址

1.1 思路

  1. 解释一下题目给出的一个"0"开头的合法 IP 地址,意思是如果是"0"开头了那这部分就只能是个 0,不能是"0235"这种。
  2. 首先定义个全局变量 result 里面放的是合法的字符串,是全部的答案
  3. 回溯函数的参数和返回值:返回值 void,参数首先是字符串 s,然后是 startIndex 是控制进入下一层递归时,从剩下的字符串中切割,就是告诉我们下一层递归从哪里开始切割,控制的是起始位置,再是 pointSum,因为我们要在字符串中加入 " . ",要加入才能放入 result 中,我们需要 3 个这个点。在代码中 startIndex 代表的就是我们的切割分割线,因为在我们进入下一层递归中时,startIndex 控制的就是从剩下的字符串中开始查找
  4. 终止条件:如果 pointSum==3,就要终止了,因为 IP 地址要三个点就行了,这个点就决定了树的深度,其实就是 3 层。这里注意下我们加入点的时候,是对加点位置的前面的字符串进行合法性判断,因为是 3 个点,但我们有 4 个子串,因此在终止条件中我们还需要对最后一段进行合法性判断,然后才能加入到 result 中。合法性判断的函数是 isValid。由上面的 startIndex 表示切割线的位置说明,isValid(s,startIndex,s.length()-1),分别表示字符串,起始位置,终止位置,如果为 true 就加入到 result 中,然后 return 就行
  5. 单层搜索的逻辑:for(int i=startIndex;i<s.length();i++),这个 for循环就是我们取数然后分割的过程,就要进行合法性判断,如果不合法就没必要往下遍历了。因此 if(isValid(s, startIndex, i)),其中 [startIndex, i] 这个就是子串的区间,本层中 startIndex 是固定的,但 i 是不断后移的,因此没毛病。如果合法,要先加入点,s=substring(start, i+1)+"."+substring(i+1) ,这里的 substring 函数是左闭右开的区间,因此要加 1,如果只传入一个参数就是这个位置往后的元素都要取出,然后 pointSum++。接着就向下一层递归 backtracking(s, i+2, pointSum),这里为什么是加 2 而不是加 1,因为我们加了个点了,所以要跳 2 个位置。然后就是回溯,我们要把之前插入的点删去,s=s.substring(0, i+1)+substring(i+2),这里就是跳过了那个点,然后 pointSum--,因为我们要继续往右搜索,因此加入的点要删去才能去右搜索
  6. 合法性判断:返回值 boolean,参数字符串 s,起始位置 start,结束位置 end,如果 start>end 就 false;如果第一个位置是 字符 '0'并且这个第一个位置不是终止位置,就 false;然后进入循环遍历,for(int i=start; i<=end; i++)这里要<=,因为 end 就是最后一个位置。然后如果字符串的 i 位置是>9 或者<0 的字符就 false,其实就是判断是否为 0 到 9 的数字;然后求整个子串是否在 0 到 255 之间,就 num=num*10+(s.charAt(i)-'0'),如果 num>255 就 false。以上均不发生就为 true。
  7. 提一下 s.charAt(i)-'0' 这步,其实就是字符的 ASCII 码值相减,比如是字符 '1'-'0',那么得到的结果就是真正的数字 1,字符 '0' 的 ASCII 码值是 48,字符 '1' 则是 49。

1.2 代码

java 复制代码
//
class Solution {
    List<String> result = new ArrayList<>();

    public List<String> restoreIpAddresses(String s) {
        if (s.length() > 12) return result; // 算是剪枝了
        backTrack(s, 0, 0);
        return result;
    }

    // startIndex: 搜索的起始位置, pointNum:添加逗点的数量
    private void backTrack(String s, int startIndex, int pointNum) {
        if (pointNum == 3) {// 逗点数量为3时,分隔结束
            // 判断第四段⼦字符串是否合法,如果合法就放进result中
            if (isValid(s,startIndex,s.length()-1)) {
                result.add(s);
            }
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            if (isValid(s, startIndex, i)) {
                s = s.substring(0, i + 1) + "." + s.substring(i + 1);    //在str的后⾯插⼊⼀个逗点
                pointNum++;
                backTrack(s, i + 2, pointNum);// 插⼊逗点之后下⼀个⼦串的起始位置为i+2
                pointNum--;// 回溯
                s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点
            } else {
                break;
            }
        }
    }

    // 判断字符串s在左闭⼜闭区间[start, end]所组成的数字是否合法
    private Boolean isValid(String s, int start, int end) {
        if (start > end) {
            return false;
        }
        if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法
            return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法
                return false;
            }
            num = num * 10 + (s.charAt(i) - '0');
            if (num > 255) { // 如果⼤于255了不合法
                return false;
            }
        }
        return true;
    }
}

2. LeetCode 78. 子集

2.1 思路

  1. 这题要求的是集合里的所有子集,就是像数学集合里的一样,有 3 个元素,那就有 2^3 = 8 个子集。而且这里求的是组合,[1,2] 和 [2,1] 是一样的,所以本题依然要用 startIndex 来控制收集剩余集合时的起始位置。这题和上面的题区别在于收获结果的地方是很大差别的
  2. 这题比较大的区别就在于我们是要收集每个节点的结果,这个节点就是一层递归,每进入一层递归就把当前本层递归的单个结果放入 result 中
  3. 定义两个全局变量,result 放全部结果,path 放单个结果
  4. 回溯函数的参数和返回值:返回值 void,参数 nums,startIndex 来控制当前这个递归层从哪里开始往后取,也就是控制起始位置
  5. 终止条件:如果 startIndex>=nums.length 就是到了叶子节点的位置了,就 return
  6. 单层搜索的逻辑:for(int i= startIndex; i<nums.length; i++),然后 path.add(nums[i]),然后就是递归的过程,backtracking(nums, i+1),然后是回溯的过程,path.removeLast()去掉最后的元素然后继续往右搜索。
  7. 而我们收获结果的地方就是在这个回溯函数的第一行,就直接 result.add(path) 这样子,为什么放这里?因为我们递归进入时就要把当前的 path 放入 result 里,如果放在了终止条件下,那最后一个元素放入后进入递归就直接 return 了,就少了最后一个 path 子集

2.2 代码

java 复制代码
//
class Solution {
    List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
    LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
    public List<List<Integer>> subsets(int[] nums) {
        subsetsHelper(nums, 0);
        return result;
    }

    private void subsetsHelper(int[] nums, int startIndex){
        result.add(new ArrayList<>(path));//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。
        if (startIndex >= nums.length){ //终止条件可不加
            return;
        }
        for (int i = startIndex; i < nums.length; i++){
            path.add(nums[i]);
            subsetsHelper(nums, i + 1);
            path.removeLast();
        }
    }
}

3. LeetCode 90. 子集 II

3.1 思路

  1. 这集给的数组的元素是有重复元素的,这是和上题的区别,而且要求不能有重复子集,例如是不能出现 2 次 [1,2]。本题是结合了40. 组合总和 II78. 子集。这题需要对回溯算法进行去重操作
  2. 我们需要一个 used 数组来标记哪个元素我们是否用过,true 为用过。我们用这种去重的操作方式需要先给题目的 nums 数组排序,因为我们要让相邻的元素挨在一起,因为这题同理要做的是树层去重。而且我们需要用 startIndex 来控制我们要在剩下的元素里取数,不然就会出现 [1,2],[2,1] 这种情况。而树枝上不用去重,因为我们已经用了 startIndex 控制了起始位置,用的元素其实不是同一个元素,如果相同其实只是数值相同。而我们的树形结构上的所有节点都是我们的结果,也就决定了把 path 加入 result 的位置是放在回溯函数的首行。回溯函数进去后的时候就是一个节点,取数的过程是在 for循环里
  3. 定义全局变量 result 全部结果、path 单个结果、used 标记是否用过
  4. 回溯函数的参数和返回值:返回值 void,参数是 nums 数组,startIndex 控制起始位置
  5. 终止条件:startIndex>=nums.length 就 return
  6. 单层搜索的逻辑:首行先 result.add(path),因为我们进入回溯函数后就证明这个节点已经是取好数的了。取数的过程就是 for(int i=startIndex; i<nums.length; i++)我们剪枝的过程是放在 for循环开始的,如果(i>0&&nums[i-1]==nums[i]&&!used[i-1])避免i-1为负数,首先要求i>0,然后是如果前后两个元素相等了就不能再取了,不然就重复了,然后下一个条件是前一个元素不能用过,这里是为了往下递归时,举例,取了一个1后,还能取多个1,这里的多个1不是同一个1,只是数值相同,这个条件不是为了树层去重的,而是为了取数找到结果。如果符合条件就是发现重复了就continue跳过此次循环,继续往后取。
  7. 然后就是继续收集元素的过程path.add(数组[i]);used[i]=true;然后就递归的过程backtracking(nums, i+1)。然后就是回溯的过程,path.removeLast()去除最后一个元素也就是刚刚加入的元素,这样才能往树的右边搜索,used[i]=false。这里又变为false是为了往右搜索,逻辑和第一层往右搜索的原因是一样的

3.2 代码

java 复制代码
//
class Solution {
   List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
   LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
   boolean[] used;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if (nums.length == 0){
            result.add(path);
            return result;
        }
        Arrays.sort(nums);
        used = new boolean[nums.length];
        subsetsWithDupHelper(nums, 0);
        return result;
    }
    
    private void subsetsWithDupHelper(int[] nums, int startIndex){
        result.add(new ArrayList<>(path));
        if (startIndex >= nums.length){
            return;
        }
        for (int i = startIndex; i < nums.length; i++){
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
                continue;
            }
            path.add(nums[i]);
            used[i] = true;
            subsetsWithDupHelper(nums, i + 1);
            path.removeLast();
            used[i] = false;
        }
    }
}
相关推荐
__lost几秒前
MATLAB直接推导函数的导函数和积分形式(具体方法和用例)
数学·算法·matlab·微积分·高等数学
thesky1234564 分钟前
活着就好20241224
学习·算法
ALISHENGYA9 分钟前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战项目二)
数据结构·c++·算法
guogaocai12312 分钟前
连续自成核退火热分级(SSA)技术表征共聚聚丙烯(PP)分子链结构
算法
DARLING Zero two♡35 分钟前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
贵州晓智信息科技1 小时前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
游是水里的游1 小时前
【算法day19】回溯:分割与子集问题
算法
不想当程序猿_1 小时前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
南城花随雪。2 小时前
单片机:实现FFT快速傅里叶变换算法(附带源码)
单片机·嵌入式硬件·算法
dundunmm2 小时前
机器学习之scikit-learn(简称 sklearn)
python·算法·机器学习·scikit-learn·sklearn·分类算法