代码随想录算法训练营第二十七天 | LeetCode 93. 复原 IP 地址、78. 子集、90. 子集 II
目录
代码随想录算法训练营第二十七天 | LeetCode 93. 复原 IP 地址、78. 子集、90. 子集 II
1. LeetCode 93. 复原 IP 地址
1.1 思路
- 解释一下题目给出的一个“0”开头的合法 IP 地址,意思是如果是“0”开头了那这部分就只能是个 0,不能是“0235”这种。
- 首先定义个全局变量 result 里面放的是合法的字符串,是全部的答案
- 回溯函数的参数和返回值:返回值 void,参数首先是字符串 s,然后是 startIndex 是控制进入下一层递归时,从剩下的字符串中切割,就是告诉我们下一层递归从哪里开始切割,控制的是起始位置,再是 pointSum,因为我们要在字符串中加入 " . ",要加入才能放入 result 中,我们需要 3 个这个点。在代码中 startIndex 代表的就是我们的切割分割线,因为在我们进入下一层递归中时,startIndex 控制的就是从剩下的字符串中开始查找
- 终止条件:如果 pointSum==3,就要终止了,因为 IP 地址要三个点就行了,这个点就决定了树的深度,其实就是 3 层。这里注意下我们加入点的时候,是对加点位置的前面的字符串进行合法性判断,因为是 3 个点,但我们有 4 个子串,因此在终止条件中我们还需要对最后一段进行合法性判断,然后才能加入到 result 中。合法性判断的函数是 isValid。由上面的 startIndex 表示切割线的位置说明,isValid(s,startIndex,s.length()-1),分别表示字符串,起始位置,终止位置,如果为 true 就加入到 result 中,然后 return 就行
- 单层搜索的逻辑: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--,因为我们要继续往右搜索,因此加入的点要删去才能去右搜索
- 合法性判断:返回值 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。
- 提一下 s.charAt(i)-'0' 这步,其实就是字符的 ASCII 码值相减,比如是字符 ‘1’-‘0’,那么得到的结果就是真正的数字 1,字符 '0' 的 ASCII 码值是 48,字符 '1' 则是 49。
1.2 代码
//
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 思路
- 这题要求的是集合里的所有子集,就是像数学集合里的一样,有 3 个元素,那就有 2^3 = 8 个子集。而且这里求的是组合,[1,2] 和 [2,1] 是一样的,所以本题依然要用 startIndex 来控制收集剩余集合时的起始位置。这题和上面的题区别在于收获结果的地方是很大差别的
- 这题比较大的区别就在于我们是要收集每个节点的结果,这个节点就是一层递归,每进入一层递归就把当前本层递归的单个结果放入 result 中
- 定义两个全局变量,result 放全部结果,path 放单个结果
- 回溯函数的参数和返回值:返回值 void,参数 nums,startIndex 来控制当前这个递归层从哪里开始往后取,也就是控制起始位置
- 终止条件:如果 startIndex>=nums.length 就是到了叶子节点的位置了,就 return
- 单层搜索的逻辑:for(int i= startIndex; i<nums.length; i++),然后 path.add(nums[i]),然后就是递归的过程,backtracking(nums, i+1),然后是回溯的过程,path.removeLast()去掉最后的元素然后继续往右搜索。
- 而我们收获结果的地方就是在这个回溯函数的第一行,就直接 result.add(path) 这样子,为什么放这里?因为我们递归进入时就要把当前的 path 放入 result 里,如果放在了终止条件下,那最后一个元素放入后进入递归就直接 return 了,就少了最后一个 path 子集
2.2 代码
//
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 思路
- 这集给的数组的元素是有重复元素的,这是和上题的区别,而且要求不能有重复子集,例如是不能出现 2 次 [1,2]。本题是结合了40. 组合总和 II和78. 子集。这题需要对回溯算法进行去重操作
- 我们需要一个 used 数组来标记哪个元素我们是否用过,true 为用过。我们用这种去重的操作方式需要先给题目的 nums 数组排序,因为我们要让相邻的元素挨在一起,因为这题同理要做的是树层去重。而且我们需要用 startIndex 来控制我们要在剩下的元素里取数,不然就会出现 [1,2],[2,1] 这种情况。而树枝上不用去重,因为我们已经用了 startIndex 控制了起始位置,用的元素其实不是同一个元素,如果相同其实只是数值相同。而我们的树形结构上的所有节点都是我们的结果,也就决定了把 path 加入 result 的位置是放在回溯函数的首行。回溯函数进去后的时候就是一个节点,取数的过程是在 for循环里
- 定义全局变量 result 全部结果、path 单个结果、used 标记是否用过
- 回溯函数的参数和返回值:返回值 void,参数是 nums 数组,startIndex 控制起始位置
- 终止条件:startIndex>=nums.length 就 return
- 单层搜索的逻辑:首行先 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跳过此次循环,继续往后取。
- 然后就是继续收集元素的过程path.add(数组[i]);used[i]=true;然后就递归的过程backtracking(nums, i+1)。然后就是回溯的过程,path.removeLast()去除最后一个元素也就是刚刚加入的元素,这样才能往树的右边搜索,used[i]=false。这里又变为false是为了往右搜索,逻辑和第一层往右搜索的原因是一样的
3.2 代码
//
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;
}
}
}