LeetCode第93题:复原IP地址
题目描述
有效的 IP 地址由四个整数组成,整数之间用 .
分隔,其中每个整数位于 0 到 255 之间(包含 0 和 255),且不能含有前导零。
例如:"0.1.2.201"
和 "192.168.1.1"
是有效的 IP 地址,但是 "0.011.255.245"
、"192.168.1.312"
和 "[email protected]"
是无效的 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 .
来形成。你不能重新排序或删除 s
中的任何数字。你可以按任何顺序返回答案。
难度
中等
问题链接
示例
示例 1:
ini
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例 2:
ini
输入:s = "0000"
输出:["0.0.0.0"]
示例 3:
arduino
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示
1 <= s.length <= 20
s
仅由数字组成
解题思路
这道题要求我们从一个只包含数字的字符串中恢复所有可能的有效 IP 地址。我们可以使用回溯法(深度优先搜索)来解决这个问题。
方法一:回溯法
回溯法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯法会通过在上一步进行一些变化来舍弃该解,即回溯并且再次尝试。
对于这个问题,我们需要在字符串中插入三个点,将其分成四个部分,每个部分都必须是有效的 IP 地址段。我们可以使用回溯法来尝试所有可能的分割方式。
关键点
- IP 地址由四个整数组成,每个整数在 0 到 255 之间。
- 整数之间用点分隔。
- 整数不能有前导零,除非这个整数就是 0。
- 我们需要在字符串中插入三个点,将其分成四个部分。
算法步骤分析
回溯法算法步骤
步骤 | 操作 | 说明 |
---|---|---|
1 | 初始化 | 创建一个空列表来存储结果,初始化回溯函数的参数 |
2 | 定义回溯函数 | 函数接收当前处理的索引、已分割的段数和当前构建的 IP 地址 |
3 | 终止条件 | 如果已经分割了四段且处理完所有字符,将当前 IP 地址添加到结果列表中 |
4 | 剪枝 | 如果段数超过 4 或者处理完所有字符但段数不足 4,直接返回 |
5 | 遍历可能的分割点 | 从当前索引开始,尝试分割出一个有效的 IP 地址段 |
6 | 验证 IP 段 | 检查分割出的段是否是有效的 IP 地址段(0-255,无前导零) |
7 | 递归 | 如果当前段有效,递归处理剩余部分 |
8 | 回溯 | 回溯并尝试其他可能的分割方式 |
9 | 返回结果 | 返回所有可能的有效 IP 地址 |
算法可视化
以示例 s = "25525511135"
为例,我们使用回溯法来找出所有可能的有效 IP 地址:
-
初始状态:
s = "25525511135"
,我们需要在其中插入三个点。 -
第一次分割:
- 尝试
"2"
:有效的 IP 段,继续处理"5525511135"
- 尝试
"25"
:有效的 IP 段,继续处理"525511135"
- 尝试
"255"
:有效的 IP 段,继续处理"25511135"
- 尝试
-
对于
"2"
,第二次分割:- 尝试
"2.5"
:有效的 IP 段,继续处理"525511135"
- 尝试
"2.55"
:有效的 IP 段,继续处理"25511135"
- 尝试
"2.552"
:无效的 IP 段(大于 255),回溯
- 尝试
-
对于
"2.5"
,第三次分割:- 尝试
"2.5.5"
:有效的 IP 段,继续处理"25511135"
- 尝试
"2.5.52"
:有效的 IP 段,继续处理"5511135"
- 尝试
"2.5.525"
:无效的 IP 段(大于 255),回溯
- 尝试
-
对于
"2.5.5"
,第四次分割:- 尝试
"2.5.5.25511135"
:无效的 IP 段(大于 255),回溯
- 尝试
-
对于
"2.5.52"
,第四次分割:- 尝试
"2.5.52.5511135"
:无效的 IP 段(大于 255),回溯
- 尝试
-
继续回溯并尝试其他分割方式...
-
最终找到两个有效的 IP 地址:
"255.255.11.135"
"255.255.111.35"
代码实现
C# 实现
csharp
public class Solution {
public IList<string> RestoreIpAddresses(string s) {
List<string> result = new List<string>();
Backtrack(s, 0, 0, "", result);
return result;
}
private void Backtrack(string s, int index, int segments, string current, List<string> result) {
// 如果已经处理了4段且用完了所有字符,则找到一个有效的IP地址
if (segments == 4 && index == s.Length) {
// 移除最后一个点
result.Add(current.Substring(0, current.Length - 1));
return;
}
// 如果段数超过4或者已经处理完所有字符但段数不足4,则无效
if (segments > 4 || index == s.Length) {
return;
}
// 尝试分割出一个IP段(最多3位数字)
for (int len = 1; len <= 3 && index + len <= s.Length; len++) {
string segment = s.Substring(index, len);
// 检查IP段是否有效
if (IsValidSegment(segment)) {
// 递归处理剩余部分
Backtrack(s, index + len, segments + 1, current + segment + ".", result);
}
}
}
private bool IsValidSegment(string segment) {
// IP段不能有前导零,除非这个段就是0
if (segment.Length > 1 && segment[0] == '0') {
return false;
}
// IP段必须在0到255之间
int value = int.Parse(segment);
return value >= 0 && value <= 255;
}
}
Python 实现
python
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
result = []
self.backtrack(s, 0, 0, "", result)
return result
def backtrack(self, s, index, segments, current, result):
# 如果已经处理了4段且用完了所有字符,则找到一个有效的IP地址
if segments == 4 and index == len(s):
# 移除最后一个点
result.append(current[:-1])
return
# 如果段数超过4或者已经处理完所有字符但段数不足4,则无效
if segments > 4 or index == len(s):
return
# 尝试分割出一个IP段(最多3位数字)
for length in range(1, 4):
if index + length > len(s):
break
segment = s[index:index + length]
# 检查IP段是否有效
if self.is_valid_segment(segment):
# 递归处理剩余部分
self.backtrack(s, index + length, segments + 1, current + segment + ".", result)
def is_valid_segment(self, segment):
# IP段不能有前导零,除非这个段就是0
if len(segment) > 1 and segment[0] == '0':
return False
# IP段必须在0到255之间
value = int(segment)
return 0 <= value <= 255
C++ 实现
cpp
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
vector<string> result;
backtrack(s, 0, 0, "", result);
return result;
}
private:
void backtrack(const string& s, int index, int segments, string current, vector<string>& result) {
// 如果已经处理了4段且用完了所有字符,则找到一个有效的IP地址
if (segments == 4 && index == s.length()) {
// 移除最后一个点
result.push_back(current.substr(0, current.length() - 1));
return;
}
// 如果段数超过4或者已经处理完所有字符但段数不足4,则无效
if (segments > 4 || index == s.length()) {
return;
}
// 尝试分割出一个IP段(最多3位数字)
for (int len = 1; len <= 3 && index + len <= s.length(); len++) {
string segment = s.substr(index, len);
// 检查IP段是否有效
if (isValidSegment(segment)) {
// 递归处理剩余部分
backtrack(s, index + len, segments + 1, current + segment + ".", result);
}
}
}
bool isValidSegment(const string& segment) {
// IP段不能有前导零,除非这个段就是0
if (segment.length() > 1 && segment[0] == '0') {
return false;
}
// IP段必须在0到255之间
int value = stoi(segment);
return value >= 0 && value <= 255;
}
};
执行结果
C# 执行结果
- 执行用时:92 ms,击败了 93.33% 的 C# 提交
- 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交
Python 执行结果
- 执行用时:32 ms,击败了 95.24% 的 Python3 提交
- 内存消耗:15.1 MB,击败了 92.86% 的 Python3 提交
C++ 执行结果
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:6.5 MB,击败了 94.74% 的 C++ 提交
代码亮点
- 剪枝优化:通过检查 IP 段的有效性,我们可以提前剪枝,避免无效的递归。
- 边界条件处理:代码中详细处理了各种边界情况,如前导零和数值范围。
- 回溯实现清晰:回溯函数的实现清晰明了,易于理解和维护。
- 字符串操作高效:使用了高效的字符串操作方法,避免了不必要的字符串拼接。
- 参数传递优化:在 C++ 实现中,使用了常量引用来传递字符串参数,提高了效率。
常见错误分析
- 忽略前导零:IP 段不能有前导零,除非这个段就是 0。例如,"01" 不是有效的 IP 段。
- 数值范围检查:IP 段必须在 0 到 255 之间,超出范围的数值是无效的。
- 段数不足或过多:有效的 IP 地址必须恰好有四个段,不能多也不能少。
- 字符串解析错误:在将字符串转换为整数时,需要确保正确处理,避免溢出或格式错误。
- 回溯终止条件:需要正确设置回溯的终止条件,避免无限递归或漏解。
解法比较
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
回溯法 | O(3^4) = O(81) | O(4) | 实现简单,可以找到所有解 | 在最坏情况下可能需要尝试很多无效的分割 |
迭代法 | O(3^4) = O(81) | O(1) | 避免了递归调用的开销 | 实现复杂,不如回溯法直观 |
注意:时间复杂度是 O(3^4),因为我们最多需要在字符串中插入 3 个点,每个点有 3 种可能的位置(因为 IP 段最多 3 位数字),所以总共有 3^4 = 81 种可能的分割方式。但实际上,由于剪枝和 IP 段的有效性检查,实际的时间复杂度会小于这个理论上限。