93. 复原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中的任何数字。你可以按 任何 顺序返回答案。
思路
题干所说可以在" s 中插入 '.' 来形成有效的IP地址",
- 由于
java语言的特性,在字符串中插入或删除一个字符的开销比较大,String是不可变的,每一次的插入和删除是都新new一个String,再将插入或删除的结果复制到新的字符串中,时间和空间开销都大。- 即便使用
String创建一个可变字符串StringBuilder或StringBuffer,这两者的底层实现中仍然使用的是数组,插入或删除的时间复杂度都是 。
- 对原字符串进行插入或删除等改动会导致下标的计算比较麻烦,
因此,我们可以不使用题目提供的方法。
- 使用列表
dotList按顺序记录插入字符串的点的下标,这样就无需对字符串进行改动。如,dotList.get(0)为1时,表明在字符串下标为1的字符的后面插入第一个.,那么该字符串的形成的IP地址的第一段就是s[0, 1]。 - 以此类推,IP地址每一段如下:
- 第一段:
s[0, dotList.chatAt(0)] - 第二段:
s[dotList.get(0) + 1, dotList.get(1)] - 第三段:
s[dotList.get(1) + 1, dotList.get(2)] - 第四段:
s[dotList.get(2) + 1, s.length() - 1] - 以上区间都是左闭右闭区间
- 第一段:
- 使用
startIndex记录待分割字符串的起始下标,即待分割字符串为s[startIndex, s.length() - 1]。 - 那么,在初始条件中,
startIndex的值为0。 - 在分割时,判断分割出来的字符是否符合标准有 2 个条件:
- 是否前前置0;
- 是否大小在
[0, 255]内。
- 因此,在一切割中,可以分为以下两个步骤:
- 判断
s[startIndex]是否为0。 - 如果是,则此次分割结果就是子串
s[startIndex, startIndex],进行下一轮分割。 - 否则,使用
i标识分割结束位置,分割的子串为s[startIndex, i]。 - 注意,由于有效IP地址的每一段的数值小于
256,即每一段长度小于等于3,因此,要满足条件i < startIndex + 3,这样一方面可以剪枝,另一方面,可以避免我们调用的方法Integer.valueOf(String)出现异常。 - 然后,判断该子串形成的数字是否在有效范围内。若在,则将
i添加到dotList中,继续进行分割字符串的操作;否则,当前分割方法不可行,不再继续做其他操作。
- 判断
- 当
dotList中有 3 个元素时,即为字符串中添加了三个.了,第四段就自然形成了,由之前的步骤验证了前三段是有效的,此时要先验证第四段是否有效,有以下步骤:- 先获得第四段的长度
len,- 如果
len等于0或者 大于3,则一定不符合要求; - 否则, 获取第四段的数字
num。
- 如果
- 再判断是否
len > 1的同时第一个字符是'0',这意味着第四段有前置 ,不符合要求。 - 然后判断
num是否在有效范围,若在有效范围内,通过s和dotList拼接字符串,再添加到结果集中。
- 先获得第四段的长度
代码
class Solution {
public List<String> restoreIpAddresses(String s) {
if(s.length() < 4){
return new ArrayList<String>();
}
List<Integer> dotList = new ArrayList<>();
List<String> res = new ArrayList<>();
backTracking(s, dotList, 0, res);
return res;
}
// dotList将 s 分成 3 段
// [0, dotList.chatAt(0)]
// [dotList.get(0) + 1, dotList.get(1)]
// [dotList.get(1) + 1, dotList.get(2)]
// [dotList.get(2) + 1, s.length() - 1]
private void backTracking(String s, List<Integer> dotList, int startIndex,List<String> res){
if(dotList.size() == 3){
// 前三段分割完成
// 判断最后一段 [dotList.get(2) + 1, s.length() - 1] 是否满足条件
int len = s.length() - startIndex;
// 无法分割出最后一段
// 最后一段明显超出255
// 加入 len > 3 ,避免转换异常
if(len == 0 || len > 3){
return;
}
int num = Integer.valueOf(s.subSequence(startIndex, s.length()).toString());
if((len > 1 && s.charAt(startIndex) == '0') || num > 255){
return;
}
// 整个字符串分割完成
// 每个字段在分割过程中完成验证
// 根据 s 和 dotList 拼接字符串
StringBuilder sb = new StringBuilder();
// subSequence : [a, b) 左闭右开区间
sb.append(s.subSequence(0, dotList.get(0) + 1));
sb.append(".");
sb.append(s.subSequence(dotList.get(0) + 1, dotList.get(1) + 1));
sb.append(".");
sb.append(s.subSequence(dotList.get(1) + 1, dotList.get(2) + 1));
sb.append(".");
sb.append(s.subSequence(dotList.get(2) + 1, s.length()));
// 加入结果集
res.add(new String(sb.toString()));
return;
}
// 待分割字符串下标为[startIndex, s.length() - 1]
// 此处将字符串分割成 s[startIndex, i]
for(int i = startIndex; i < s.length() && i < startIndex + 3; i++){
if(i == startIndex && s.charAt(i) == '0'){
// 如果首字符为0,必须直接分割
dotList.add(startIndex);
backTracking(s, dotList, startIndex + 1, res);
dotList.remove(dotList.size() - 1);
break;
}
// 判断s[startIndex, i]是否有效
Integer num = Integer.valueOf(s.subSequence(startIndex, i + 1).toString());
if(num <= 255){
// 有效
dotList.add(i);
backTracking(s, dotList, i + 1, res);
dotList.remove(dotList.size() - 1);
}
// 无效,直接剪枝
}
}
}
78.子集
给你一个整数数组
nums,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10nums中的所有元素 互不相同
思路
本题要求给定数组nums的子集,子集出现在数组枚举的过程中,即每枚举一种数组元素的选择,就产生一种子集。
为了不出现重复的子集,只要要求每次取值都在当前取值元素后面的元素中选择即可。
代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<Integer> path = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
res.add(new ArrayList<Integer>());
backTracking(nums, 0, path, res);
return res;
}
private void backTracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> res){
if(startIndex == nums.length){
return;
}
for(int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
res.add(new ArrayList<Integer>(path));
backTracking(nums, i + 1, path, res);
path.remove(path.size() - 1);
}
}
}
90.子集II
给你一个整数数组
nums,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
思路
本题题干与第40题给出的条件相同,数组元素可重复且每个元素只能使用 1 次,所求的与78.子集相同,因此,
- 去重的方法与第40题相同,
- 获取结果的方法与第78题相同。
代码
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<Integer> path = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
res.add(new ArrayList<Integer>());
Arrays.sort(nums);
backTracking(nums, 0, path, res);
return res;
}
private void backTracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> res){
if(startIndex == nums.length){
return;
}
for(int i = startIndex; i < nums.length; i++){
// 当前元素与前一元素相同
// 如果进行相同操作,产生同样的结果
// 需要去重
if(startIndex < i && nums[i] == nums[i - 1]){
continue;
}
path.add(nums[i]);
res.add(List.copyOf(path));
backTracking(nums, i + 1, path, res);
path.remove(path.size() - 1);
}
}
}