代码随想录打卡|Day21(复原ip地址、子集、子集2)
回溯算法 Part03
补充小知识(字符串的常用方法)
| 操作类型 | 方法/类 | 说明 | 示例(Java) | 
|---|---|---|---|
| 截取 | substring() | 提取字符串的指定部分 | "Hello".substring(1, 3)→"el" | 
| 连接 | concat()/+ | 拼接字符串 | "A".concat("B")或"A" + "B"→"AB" | 
| 可变字符串 | StringBuilder | 高效修改字符串(非线程安全) | new StringBuilder("Hi").append("!")→"Hi!" | 
| 查找 | indexOf() | 返回字符/子串的首次出现位置 | "apple".indexOf('p')→1 | 
| 替换 | replace() | 替换字符或子串 | "abc".replace("b", "x")→"axc" | 
| 分割 | split() | 按正则表达式分割字符串 | "a,b,c".split(",")→["a", "b", "c"] | 
| 大小写转换 | toLowerCase()/toUpperCase() | 转换大小写 | "Hi".toUpperCase()→"HI" | 
| 去除空格 | trim() | 移除首尾空白字符 | " a ".trim()→"a" | 
| 格式化 | String.format() | 格式化字符串(类似 printf) | String.format("%s:%d", "ID", 10)→"ID:10" | 
| 比较 | equals()/compareTo() | 比较内容或字典序 | "a".equals("A")→false;"a".compareTo("b")→-1 | 
| 正则匹配 | matches() | 检查字符串是否匹配正则表达式 | "123".matches("\\d+")→true | 
| 字符提取 | charAt() | 获取指定位置的字符 | "cat".charAt(1)→'a' | 
| 长度 | length() | 返回字符串长度 | "hello".length()→5 | 
| 类型转换 | valueOf() | 将其他类型转为字符串 | String.valueOf(1.23)→"1.23" | 
| 反转 | StringBuilder.reverse() | 反转字符串(需配合 StringBuilder) | new StringBuilder("123").reverse()→"321" | 
| 空白检查 | isBlank()(Java 11+) | 检查字符串是否为空或仅含空白字符 | " ".isBlank()→true | 
复原ip地址
力扣题目链接
 代码随想录链接
 视频链接
题目描述: 有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 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 中的任何数字。你可以按 任何 顺序返回答案。
 
 思路:本题的结束如下所示(代码随想录):
 
回溯法
法一(使用substring方法对字符串进行操作)
//方法二:比上面的方法时间复杂度低,更好地剪枝,优化时间复杂度
class Solution {// 定义全局变量结果List<String> result = new ArrayList<>();public List<String> restoreIpAddresses(String s) {backTracking(s,0,0);return result;}// 定义回溯函数private void backTracking(String s , int startIndex , int pointNum){// 定义结果的返回条件if(pointNum == 3){if(isValid(s,startIndex,s.length() - 1)){result.add(s);}return ;}for(int i = startIndex ; i < s.length() ; i ++){if(pointNum > 3)return ;if(isValid(s,startIndex,i)){s = s.substring(0,i+1)+"."+s.substring(i+1);pointNum += 1;// 此处为i+2的原因是,加了一个"."backTracking(s,i+2,pointNum);// 回溯pointNum -= 1;s = s.substring(0,i + 1) + s.substring(i+2);}else{break;}}}private boolean isValid(String 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++){if(s.charAt(i) > '9' || s.charAt(i) < '0')return false;num = num*10 +(s.charAt(i) - '0');if(num > 255)return false; }return true;}
}
回溯法(使用StringBuilder)
StringBuilder和subtring的优劣
- StringBuilder 向字符串之中插入字符的时候无需复制整个字符串,从而减少了操作的时间复杂度,也不用开辟内存空间用于存储substring,减少了空间复杂度
- StringBuider非线程安全
StringBuilder常用方法介绍:
| 方法名 | 作用描述 | 参数说明 | 返回值类型 | 示例(输入 → 输出) | 
|---|---|---|---|---|
| ** append(x)** | 追加任意类型数据(自动转为字符串) | x: 基本类型/对象/String | StringBuilder | sb.append("Hi").append(1)→"Hi1" | 
| ** insert(idx, x)** | 在指定位置插入数据 | idx: 插入索引;x: 要插入的内容 | StringBuilder | sb.insert(1, "X")→"HXi1" | 
| ** delete(start, end)** | 删除子串(含头不含尾) | start: 起始索引;end: 结束索引 | StringBuilder | sb.delete(1, 2)→"Hi1" | 
| ** reverse()** | 反转字符串内容 | 无 | StringBuilder | sb.reverse()→"1iH" | 
| ** replace(start, end, str)** | 替换指定区间的字符为另一字符串 | start,end: 替换范围;str: 新字符串 | StringBuilder | sb.replace(0, 1, "A")→"AiH" | 
| ** charAt(idx)** | 获取指定位置的字符 | idx: 字符索引 | char | sb.charAt(0)→'A' | 
| ** setCharAt(idx, c)** | 修改指定位置的字符 | idx: 索引;c: 新字符 | void | sb.setCharAt(0, 'B')→"BiH" | 
| ** length()** | 返回当前字符序列长度 | 无 | int | sb.length()→3 | 
| ** capacity()** | 返回当前底层数组容量(≥length) | 无 | int | new StringBuilder(10).capacity()→10 | 
| ** ensureCapacity(min)** | 确保容量至少为指定值(自动扩容时通常为 2*旧容量+2) | min: 最小容量 | void | sb.ensureCapacity(20) | 
| ** toString()** | 转为不可变String对象 | 无 | String | sb.toString()→"BiH" | 
| ** substring(start[, end])** | 截取子串(返回String,不影响原内容) | start/end: 截取范围 | String | sb.substring(1)→"iH" | 
| ** indexOf(str[, fromIdx])** | 查找子串首次出现的位置 | str: 目标字符串;fromIdx: 起始索引 | int | sb.indexOf("i")→1 | 
| ** lastIndexOf(str)** | 查找子串最后一次出现的位置 | 同 indexOf() | int | sb.append("i").lastIndexOf("i")→2 | 
| ** deleteCharAt(idx)** | 删除指定位置的字符 | idx: 字符索引 | StringBuilder | sb.deleteCharAt(0)→"iH" | 
| ** setLength(len)** | 强制设置字符序列长度(截断或填充空字符 \u0000) | len: 新长度 | void | sb.setLength(2)→"Bi" | 
| ** trimToSize()** | 释放多余容量(将底层数组缩至与length相同) | 无 | void | sb.trimToSize() | 
代码如下:
//方法二:比上面的方法时间复杂度低,更好地剪枝,优化时间复杂度
class Solution {// 定义全局变量结果List<String> result = new ArrayList<>();public List<String> restoreIpAddresses(String s) {StringBuilder sb = new StringBuilder(s);backTracking(sb,0,0);return result;}// 定义回溯函数private void backTracking(StringBuilder s , int startIndex , int numOfDot){if(numOfDot == 3){if(isValid(s,startIndex,s.length() - 1)){result.add(s.toString());}return ;}for(int i = startIndex ; i < s.length() ; i++){if(isValid(s,startIndex , i)){s.insert(i+1,".");numOfDot += 1;backTracking(s,i+2,numOfDot);numOfDot -= 1;s.deleteCharAt(i+1);}}}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++){if(s.charAt(i) > '9' || s.charAt(i) < '0') return false;num = num*10 + (s.charAt(i) - '0');if(num > 255) return false;}return true;}
}
在代码之中,还可以进一步剪枝
例如将判断字符串是否合法的部分直接写道回溯递归部分,从而直接免去不必要的操作
class Solution {List<String> result = new ArrayList<String>();StringBuilder stringBuilder = new StringBuilder();public List<String> restoreIpAddresses(String s) {restoreIpAddressesHandler(s, 0, 0);return result;}// number表示stringbuilder中ip段的数量public void restoreIpAddressesHandler(String s, int start, int number) {// 如果start等于s的长度并且ip段的数量是4,则加入结果集,并返回if (start == s.length() && number == 4) {result.add(stringBuilder.toString());return;}// 如果start等于s的长度但是ip段的数量不为4,或者ip段的数量为4但是start小于s的长度,则直接返回if (start == s.length() || number == 4) {return;}// 剪枝:ip段的长度最大是3,并且ip段处于[0,255]for (int i = start; i < s.length() && i - start < 3 && Integer.parseInt(s.substring(start, i + 1)) >= 0&& Integer.parseInt(s.substring(start, i + 1)) <= 255; i++) {if (i + 1 - start > 1 && s.charAt(start) - '0' == 0) {break;}stringBuilder.append(s.substring(start, i + 1));// 当stringBuilder里的网段数量小于3时,才会加点;如果等于3,说明已经有3段了,最后一段不需要再加点if (number < 3) {stringBuilder.append(".");}number++;restoreIpAddressesHandler(s, i + 1, number);number--;// 删除当前stringBuilder最后一个网段,注意考虑点的数量的问题stringBuilder.delete(start + number, i + number + 2);}}
}
子集
力扣题目链接
 代码随想录链接
 视频链接
题目描述: 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

思路:这道题不算难题,只要将遍历过的所有情况都保存下来即可,本题的解树为:
 
本题的代码为:
class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> subsets(int[] nums) {backTracking(nums,0);return result;}private void backTracking(int[] nums , int startIndex){// 将所有的结果均记录下来result.add(new ArrayList(path));if(startIndex > nums.length - 1 ){return ;}for(int i = startIndex ; i < nums.length ; i++){path.add(nums[i]);backTracking(nums,i + 1);path.remove(path.size() - 1);}}
}
子集2
力扣题目链接
 代码随想录链接
 视频链接
题目描述: 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

思路:我们对于组合问题进行去重的时候,一般会初始化used数组用于标识数据是否被使用过。但是我们一定要分清楚什么时候使用树层去重,什么时候是树枝去重。
 
树层去重讲解链接
回溯法
代码如下:
class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean[] used;public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums);used = new boolean[nums.length];Arrays.fill(used,false);backTracking(nums,0);return result;}// 定义回溯函数private void backTracking(int[] nums , int startIndex){result.add(new ArrayList(path));if(startIndex > nums.length -1)return ;for(int i = startIndex ; i <= nums.length -1 ; i++){// 树层去重if( i > 0 && nums[i] == nums[i - 1]&&!used[i - 1] )continue;used[i] = true;path.add(nums[i]);backTracking(nums,i+1);used[i] = false;path.removeLast();}}
}
