93. 复原 IP 地址 - 力扣(LeetCode)
发布:2021年9月18日21:26:11
问题描述及示例
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 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 地址。
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000”
输出:[“0.0.0.0”]
示例 3:
输入:s = “1111”
输出:[“1.1.1.1”]
示例 4:
输入:s = “010010”
输出:[“0.10.0.10”,“0.100.1.0”]
示例 5:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/restore-ip-addresses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
提示:
0 <= s.length <= 3000
s 仅由数字组成
我的题解(回溯)
这应该是我在LeetCode碰到的第二个有关回溯的题目了,之前写过一个相关的题解博客:
其中的核心思路和本题的核心思路其实是如出一辙的。可以对照思考。
在我看来,其实回溯就是递归的一种应用,往往用在一些需要穷举所有可能的情况,而回溯和一般的递归的区别,我觉得就是所谓的“回溯”操作,也就是:当一层递归完成后,把程序中的某些变量(以我目前的经验来说,这些变量往往是定义在回溯函数之外的,可以暂且理解为全局变量吧)恢复到递归前的状态。回溯操作的典型结构如下:
let globalVar;
let globalVar2;
...
globalVar2++;
operateA(); // 在operateA之前,globalVar的状态是X;在operateA之后,globalVar的状态变为Y
backtracking(params); // backtracking() 就是一个递归函数,可以理解为是深度搜索
operateB(); // 在operateB之前,globalVar的状态是Y;在operateB之后,globalVar的状态恢复为X
globalVar2--;
...
上面这小段程序中的回溯操作其实就是 operateB()
和 globalVar2--
。
也就是说,operateA()
和 operateB()
的操作效果刚好是相互抵消的。比如数组的 push()
和 pop()
方法。这就是回溯算法最精髓的部分:把状态恢复到递归前。
这个过程其实可以抽象成对一棵多叉树的遍历过程:
注意上面【IP地址第四段】的取值其实没得选,只要前面三段选好了,
s
中剩下的字符串就全是第四段了。如果s
比较短的话,前面的几段可能都把s
中的字符抢完了,所以有些方案中甚至都没有四段。
详细解释请看下方注释:
/**
* @param {string} s
* @return {string[]}
*/
var restoreIpAddresses = function (s) {
// 从s的长度初步判断IP地址的有效性,详情请看下方【补充1】
// 如果不满足,直接返回空数组作为结果
if (s.length < 4 || s.length > 12) {
return [];
}
// results用于存储最终的结果,里面是一个个有效的IP地址字符串
let results = [];
// ip是用于存储单个有效的IP地址的,详情请看下方【补充2】
let ip = [];
// 开始对s做递归运算,注意这个递归是最外层的那个递归(也就是递归调用栈里栈底的那个)
backtracking(s, 0);
// 当上面那个最外层的递归完成后,results中就已经存储了所有有效的IP地址,将其返回
return results;
// 回溯函数(递归函数),str是要处理的完整字符串,也就是上面的s,
// start是我们本层递归开始要处理的字符的下标
function backtracking(str, start) {
// 加上下面这个if判断,也可以优化性能,因为IP地址是四段的,如果ip数组的长度大于4,
// 说明已经不符合要求了,所以完全可以停止当前层的递归,如果不加这个判断的话
// 其实也可以,因为下面的结束条件中有个ip.length===4的判断,但会多做一些不必要的工作
if(ip.length > 4) {
return;
}
// 递归的结束条件(这里非常关键!),当开始下标等于要处理的字符串的长度时,
// 说明整个要处理的字符串已经完成一次完整的深度探索了,
// 这时就应该判断ip数组中元素是否能够组成一个有效的IP地址了,
// 当然这里对整个IP地址有效性的判断逻辑被提前分解为下方对各分段有效性的判断
if (start === str.length) {
// 下面这个判断其实是对IP地址有效性判断的一部分,但是因为
// 前面已经加了对ip长度限制的判断,所以这个判断其实可以不用了。
if (ip.length === 4) {
// 如果ip数组中的元素组成的IP地址是有效的,则说明当前这次深度探索成功找到了一个
// 符合条件的答案,于是将其以 . 为分隔符拼接为一个IP地址并放入存放结果的数组中
results.push(ip.join('.'));
}
// 结束本层递归
return;
}
// 这个for循环遍历也是一个坑,总的来说,这里就是告诉当前递归层
// 该从哪个位置开始处理str详情请看下方【补充3】
for (let i = start; i < str.length; i++) {
// segment是从str中切割出来的一小段字符,也就是IP地址中的一个“候选”分段,
// 为什么说是候选分段?因为这个分段不一定是有效的,如果经过后面判断发现它是无效的,
// 则应当立马停止后面的for循环遍历,因为就算后面碰到了有效的候选分段,前面的这个
// 分段是无效的话,还是不能组成有效的IP地址
let segment = str.substring(start, i + 1);
// 用isValidSegment()函数判断当前segment分段是否是有效的IP分段,
// 如果每个分段都是有效的,那么最后组合起来的IP地址也肯定是有效的,
// 之前提到的对整个IP地址有效性的判断逻辑被提前分解,其实就体现在这儿
if (!isValidSegment(segment)) {
// 如果当前的候选分段是无效的,则应当立马结束后续的for循环遍历
break;
}
// 如果当前的候选分段是有效的IP分段,则将其压入ip数组中,作完整IP地址的一个分段
ip.push(segment);
// 开始新一层的递归,注意新一层递归的开始下标设置为了i+1,这里也是个关键
backtracking(str, i + 1);
// 当上一层递归结束后,把刚才压入ip数组的有效IP分段弹出,然后通过for循环的
// 下一层遍历(也就是i++后的那层)获取下一个候选分段,重复这一系列判断和递归。
// 这个pop()操作其实就是关键的回溯操作
ip.pop();
}
}
// isValidSegment() 是一个辅助函数,用来判断一个字符串是否是有效的IP地址分段
// 如果是,就返回true;如果不是,就返回false
function isValidSegment(str) {
if (!str || str.length > 3) {
return false;
}
if (str[0] === '0') {
return str.length > 1 ? false : true;
}
return Number(str) <= 255 ? true : false;
}
};
提交记录
执行结果:通过
147 / 147 个通过测试用例
执行用时:76 ms, 在所有 JavaScript 提交中击败了67.63%的用户
内存消耗:39.3 MB, 在所有 JavaScript 提交中击败了55.32%的用户
时间:2021/09/18 21:35
相关补充
【补充1】
-
一个IP地址最短就是形如:
8.8.8.8
这样的,所以s
的长度不能小于4
; -
一个IP地址最长就是形如:
123.123.123.123
这样的,所以s
的长度不能大于12
;一开始我没有加上第二个条件,结果提交之后就出现了下面这个超时的结果。
【补充2】
- 一个有效的IP地址分为四段,比如:
10.10.2.3
。存储在ip
数组中就是['10','10','2','3']
,也就是说ip
数组中的一个元素就是IP地址的其中一段。
【补充3】
-
对于这个
for
循环中i
的初始值以及结束条件的选择,我一开始走了歪路。我写成了:... for (let i = 1; i <= 3; i++) { ... } ...
其实主要是我由于我受到自己画的那张树形图的分支数量的影响,因为我想每个节点其实都有三个分支,而这个
for
循环其实就是对树在宽度方向上的遍历,所以就写成了上面那样。但是其实仔细想想的话,还是不对的。因为如果把for
循环的结束条件设置为i<=3
,那str
中可被遍历到的字符就限制为了3位,这显然是不合逻辑的。
对于用到递归或回溯的程序,其实我都觉得在浏览器的开发者工具中进行逐步观察是最好的理解方式,主要观察函数的调用栈、“全局”变量以及当前块作用域下的变量值的变化过程,整个流程下来,还是会有一个比较清晰的认识的,主要还是有耐心。
注意理解递归过程中传入下一层递归的参数有什么意义,当前递归返回之后又会回到上层递归的哪个地方。
官方题解
更新:2021年7月29日18:43:21
因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。
更新:2021年9月18日21:35:40
【更新结束】
有关参考
更新:2021年9月18日21:34:22
参考:【算法-LeetCode】46. 全排列(回溯算法初体验)_赖念安的博客-CSDN博客
参考:String.prototype.substring() - JavaScript | MDN