个人主页:@Sherry的成长之路
学习社区:Sherry的成长之路(个人社区)
专栏链接:练题
长路漫漫浩浩,万事皆有期待
复原IP地址这道题也是一道切割字符串的问题,也是略有难度的一道题,起初我做的时候,想不通是怎么精确的把字符串分割成四段做终止条件的,后来看了题解知道,并不是一开始就能够确定切出多大一块,而是一点点试错的,虽然我们知道,是要将该字符串序列分成四份,由三个点分割开来,但是我们代码实现时候是无法做到一开始就确定前半部分是切割前几个数字的,这是因为字符串序列可能是不一样的,题目要求数字前面不能由前导0构成,只有0是可以的,且每一块分割出来的数字大于等于0小于等于255,由于字符序列的不同,有的时候我们第一次只能切一个数字,就是0的情况,0不能做前导,有的时候我们可以多切一点,这是无法固定的,所以我们要一点点切割递归下去,好在我们有函数递归可以帮助我们完成这一难题。
其他的困难部分我们在上一期的切割回文子串已经讲的差不多了,无非也就是如何表示切割线,如何传进去切割区间做判断,不明白可以去看上一期。
区别在于,这道题的终止判断条件也是有所不同的,它的终止条件是我们已经放入了三个逗点做分割了,这时候我们进入终止判断,分割问题,通常都是由题目要求的关键信息,作为终止条件,它和这几期做的那些组合题不一样,不是固定的套路,需要自行想出。
class Solution {
private:
vector<string> result;// 记录结果
// startIndex: 搜索的起始位置,pointNum:添加逗点的数量
void backtracking(string& s, int startIndex, int pointNum)
{
if (pointNum == 3)
{ // 逗点数量为3时,分隔结束
// 判断第四段子字符串是否合法,如果合法就放进result中
if (isValid(s, startIndex, s.size() - 1))
{
result.push_back(s);
}
return;
}
for (int i = startIndex; i < s.size(); i++)
{
if (isValid(s, startIndex, i))
{ // 判断 [startIndex,i] 这个区间的子串是否合法
s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点
pointNum++;
backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2
pointNum--; // 回溯
s.erase(s.begin() + i + 1); // 回溯删掉逗点
} else break; // 不合法,直接结束本层循环
}
}
// 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法
bool isValid(const string& s, int start, int end)
{
if (start > end)
{
return false;
}
if (s[start] == '0' && start != end)
{ // 0开头的数字不合法
return false;
}
int num = 0;
for (int i = start; i <= end; i++)
{
if (s[i] > '9' || s[i] < '0')
{ // 遇到非数字字符不合法
return false;
}
num = num * 10 + (s[i] - '0');
if (num > 255)
{ // 如果大于255了不合法
return false;
}
}
return true;
}
public:
vector<string> restoreIpAddresses(string s)
{
result.clear();
if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了
backtracking(s, 0, 0);
return result;
}
};
还有三点需要注意的细节,第一点是除了在for循环里要判断该切割区间是否合法,我们在终止条件还要额外判断一次,这为什么呢?原因在于我们终止条件是三个逗点就插入字符串,而我们之前只是判断了三次切割串是否合法,因为判断一次要放一个标点所以肯定是只判断了三次,我们在这里加一次判断的目的是为了知道,这最后剩余的部分是否依然具有合法性。第二点是字符串的insert成员函数插入标点是传入下标的前一个位置,所以要进行+1传入,而且我们在进行递归下一层时,传入的下一层开始遍历起始位置是i+2,而不是i+1,因为加入了一个标点的缘故。第三点是判断部分代码的细节,下面的for循环来依次取各个数相加判断和是否大于255,其实这里的要点主要在于对每一个字符的判断,防止它是字符1-9之外的数,但是leetcode上已经明确说了传入的都是数字序列,可是题目仍然要求判断,感觉可能是题目描述没有全改过来的缘故。
子集问题和前面讲过很多的组合问题,解法如出一辙,只是在处理数据加入结果集里有所不同。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
{
result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己
if (startIndex >= nums.size()) { // 终止条件可以不加
return;
}
for (int i = startIndex; i < nums.size(); i++)
{
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums)
{
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
和组合问题一样,需要注意的是处理数据,是每一次都将节点放入结果数组,因为求的是子集要保留所有的节点,每一个节点都是它的子集,所以不存在剪枝操作。那空集是如何放入的呢?实际上就是没有节点时候直接放入了path,这一点也很好理解。单层递归是用一个for循环这一点就是组合的内容,不懂的可以翻看前面组合的章节。
子集II整体代码和子集差不多,只是加入了组合的去重逻辑。
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backtraking(vector<int>&nums,int start,vector<int>&nums2)
{
result.push_back(path);
for(int i=start;i<nums.size();i++)
{
if(i>=1&&nums[i]==nums[i-1]&&nums2[i-1]==0)continue;
nums2[i]=1;
path.push_back(nums[i]);
backtraking(nums,i+1,nums2);
path.pop_back();nums2[i]=0;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int>nums2(nums.size(),0);
sort(nums.begin(),nums.end());
backtraking(nums,0,nums2);
return result;
}
};
用一个辅助数组记录我们所要添加数的数组,各个数字的加入情况。这里再简单讲一下去重的逻辑,画一个树形图辅助理解,当我们在一个数组里加入元素,这道题说是给定数组有重复元素,当我们构成一个答案数据时候i,由于每次递归会i+1,走到下一个数据位置,作为下一次放元素的起始位置,所以我们不需要担心会一个数据取两次,但是如果有两个数据,我们第一次用到它取的一系列答案数据,再你第二次用到的时候会将这些答案数据又取一次,因为第一次用的这个数字包含了你第二次用的时候取到的全部答案了,这一点画出树形图很容易理解,所以要将它去掉,也就是去重的逻辑,需要做的是将给定数组排序,让重复数据挨在一起,然后用一个数组去辅助做判断,如果本次要放入的数据和上一个位置数据值相等,且上一个相等数据本次没有放入,则说明我们要删除它,这是数层上的去重逻辑。
今天我们完成了复原 IP 地址、子集、子集 II三道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~