一、题目分析
题目描述
有效 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 = "25525511135"
输出:["255.255.11.135","255.255.111.35"]输入:s = "0000"
输出:["0.0.0.0"]输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
思路分析
- 首先根据题目本身可以看到这是一道涉及字符串分割的问题,是属于回溯算法的一种,不太了解此算法的小伙伴可以先去了解一下,回溯算法它是DFS(深度优先搜索)的一种,与DFS不同的是,它不会一直向下遍历,会回溯寻找其他方案。就如此题一样,至于如何分割,我==画了一张图==,大家一看便知
- 怎么样,是不是很清晰,对此题有了一些基本的了解,接下去继续分割就形成了一个树形结构,这是数据结构中的一种,不懂的同学可以去了解一下。举个例子,对于第一个IP串,已经截取了2,接下去它可以再截取5,或者是55,但是552就不行了,因为其超出了255的范围界限,然后用回溯去一步步地判断每一个字段是否合法即可,且听我慢慢道来:mag_right:
二、代码的细究与详解
回溯三部曲
①递归函数的返回值以及参数
void backtracking(string s,int startindex,int pointnum)
- 首先我们要考虑的就是这个递归函数的返回值以及参数,对于返回值,一般都是void,有些特殊情况写成int或其他类型,第一个参数是传入的待处理字符串s,而startindex是每次对于字符串要从哪里开始分割的起始位置,因为每次回溯都需要从不同的位置开始,最后的pointnum指的是IP地址所需要的逗点“.”数量。题目中有讲述到IP地址是由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0)组成的,因此最多就只能有三个逗点,这也是我们要进行的第二步,判断其递归函数的递归出口
②递归出口
if(pointnum == 3) //3个逗点表示四个区域
{
if(judge(s,startindex,s.size()-1)) //判断IP是否合法
{
result.push_back(s);
}
return;
}
- 对于此递归出口,就是我如上所说的逗点到达3的的判断,而且在内部,还需要判断你传进来的从startindex起始位置到i终止位置的字段串是否合法,因为不仅是要判断整个IP是否合法,而且对于每一个字段的判断也是很重要的,这样才能筛选出最正确又合法的IP地址,怎么样,准备开始==头脑风暴==:ocean:吧,好戏才刚刚开始,如果判断其是合法的字段,则将其放入result这个结果集中,它的定义是这样的
vector<string> result;
- 这个的话大家其实看给出主接口函数集合,看它的返回值是什么,那一定要在私有变量定义区private中定义一个与其返回值相同的容器来存储和接收合法的IP
vector<string> restoreIpAddresses(string s)
③单层搜索的逻辑
for(int i = startindex;i < s.size(); ++i)
{
if(judge(s,startindex,i)) //若是有效IP地址
{
//在其后(0-255)插入逗点
s.insert(s.begin() + i + 1,'.');
pointnum++;
backtracking(s,i + 2,pointnum); //多加i是因为逗点也需要占一个位置
//回溯
pointnum--;
s.erase(s.begin() + i + 1);
}
}
- 对于此单层搜索逻辑的判断,我们可以看到循环都是从startindx开始的,直到字符串的末尾,去逐一判断,那判断IP地址是否合法这个功能我们是不是在整道题中都需要用到呢?答案是的,因此我们要将其封装成一个函数,需要的时候将其调用即可,接下来insert()就是插入函数在其后插入逗点,前一个参数是位置迭代器,begin()是起始迭代器,+ i + 1就到了当前字段的末尾,后一个参数便是所要插入的内容“.”,因为插入了一个逗点,所以pointnum就要++,以便可以传入backtracking函数进行下一次层的递归,因为我们上面讲到了pointnum==3,所以这个pointnum是一直在递增的。
- 对于startindex这个位置==为什么传入i + 2==我解释一下,首先因为是回溯,你要遍历到下一个树枝,而下一个树枝也就是下一个字段,它的起始位置和上一个字段是不一样的,所以要i + 1,而为什么要i + 1再+ 1呢,是因为你插入了一个逗点了,这个逗点也是需要占一个位置的,所以要从i + 2个位置开始遍历
- 下面的函数是判断IP是否合法的
bool judge(const string str,int start,int end)
{
if(start > end) //截取越界不合法
return false;
if(str[start] == '0' && start != end) //开头为数字'0'不合法
return false;
else
{
int num = 0;
for(int i = start;i <= end; ++i)
{ //此尾端点必须包含,否则判断时将会出现错位混乱
if(str[i] < '0' || str[i] > '9') //非法数字
return false;
num = num*10 + (str[i] - '0');
//*10因为每次分割的种类都差10倍,去回溯时;-‘0’是因为要将字符转成数字
if(num > 255)
return false; //若计算后所得num > 255,不合法
}
}
return true; //讨论完所有不合法情况,其余的便是合法
}
- 可以知道,判断一个IP是否合法,返回值类型只有true和false,因此要设置为bool类型,对于内容,肯定是要考虑到尽可能多的情况。
- 这里的第一种情况就是start > end,也就是你传进来的字符截取得越界了,也就是不合法;
- 第二种便是要考虑到开头为数字'0'的情况,这个题目本身已经告诉我们了,而且还要考虑的一种情况就是字段串单独为0的这一种,这个题目也给出事例了,如果忘了可以拉上去看一下,因为==0.0.0.0==这种IP也是合法的,这是一种特殊地址,一般在网络编程的时候会使用。所以回归题目,这种情况就是start == end的情况,所以当start != end时,这个字段串就不止一个数字,一定是两个及两个以上的,例如01,03,005之类的,这些就可以判断出其不合法
- 对于第三种也及时其他情况,我们就需要去循环遍历判断了,从传入的start到end,如果碰见了str[i] < '0' || str[i] > '9'的这种情况,那这个数串一定是不合法的,可以判断其为非法数字
- 最后一种,我们还需要考虑到也就是题目给出的IP不可越界的情况,这就需要我们去思考了,因为每次你继续分割的IP要么是分割一个、两个最多也就是三个,不会出现四个的可能,这就对应了我们的数字中的==个位十位和百位==,这一点及其重要,大家要理解这个,因为三个位上的数字相差的10倍的,因为我们要乘上10去依次判断,因为每次回溯之后你进入下一分段的判断这个位数肯定上一次的十倍,上一次分割了一位,那这次就分割两位。但后面为什么要用str[i] - '0'呢,这其实就是一个基本操作,你想想,因为我们拿到的是一个string字符串呀,怎么能对字符串进行一个数字运算呢,所以将其转化数字才对嘛,最后转化后的数字如果> 255则也可以认定为是不合法的,返回false
- 最后当所有不合法的情况都判断完,那剩下的就是合法,返回true即可,有一点要注意的是以后大家在写判断函数的时候不要在内部判断如果合法就返回true,因为这样的话其他的就判断不了了,一定要在判断完所有最坏情况下才能安心返回true.讲到这里你有没有觉得自己又头脑风暴了呢,如果累了就休息一下,再讲整体的逻辑理一遍,对于初学回溯的人来说这个题还是很难理解的(高手请无视:dog:)。
num = num*10 + (str[i] - '0');
OK,说完了回溯三部曲后,你是不是对这道题的整体逻辑和框架结构有所了解了呢,如果还没有很清楚的话,我这里将上面的图又继续地画了一遍,大家可以对照着讲解再看一看
最后就是这个主函数的调用
public:
vector<string> restoreIpAddresses(string s) {
result.clear();
if(s.size() < 4 || s.size() > 12) return result;
backtracking(s,0,0);
return result;
}
}
- 解释一下这个s.size() < 4 || s.size() > 12是什么意思,这步的话其实就是一个==剪枝操作==,回溯的话如果你学地再好一些应该要学会灵活地剪枝,也就是我上图画的不合法的分段,试想如果将这些不合法的分段在递归函数调用之前就减去,那是不是又可以减少这运行的时间了呢,这其实就是一个对代码优化的过程,到后面慢慢地也会习惯。
- 好,我们回归正题,这里给出两种极端的IP地址,仔细想想应该也可以考虑到,有一种其实题目已经给出了,数一下两种情况各自所用的位数(不包含逗点)就很清楚了,第一种是4,第二种是12,那合法的IP是不是肯定在这两种情况之间呐,就像begin和end一样,如果是超过它们的两端,那就不对了,就直接return result结果即可
1.1.1.1 192.168.255.254
三、相似题目
这道提的话也是比较经典的回溯分割串问题,可以去练练手:snowman:
131.分割回文串
四、总结与提炼
说完了这道力扣上编号为93的题目,你对回溯算法有没有一些初步或者加深的理解了呢,但这么一道题目是完全不够的,回溯算法有五大类问题,分别是组合、割串、子集、排列和棋盘问题,有兴趣想要深入了解的可以再去做做对应LeetCode上的题目,后续会继续更新关于回溯类的题目,敬请期待。最后,感谢您对本文的阅读,如有问题请于评论区或私信指出,谢谢:cherry_blossom: