外观
复原 IP 地址
⭐ 题目日期:
字节 - 2024/12/2,字节 - 2024/11/27,小米 - 2024/10/24
🌳 题目描述:
有效 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
中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000"
输出:["0.0.0.0"]
示例 3:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
🕵🏽 面试评估:
本题属于典型的回溯问题,考察候选人对递归、剪枝优化以及字符串处理的理解与应用。题目在输入规模上虽然有限,但需要结合多种约束条件和逻辑处理,适合用于评估候选人的代码能力、算法设计能力以及问题分析能力。难度中等偏上。
🧗难度系数:
⭐️ ⭐️ ⭐️ ⭐️
📝思路分析:
IP 地址由四部分组成,每部分的取值范围是 0 到 255。需要注意除了数字 0 本身,其它部分如果以 0 开头则视为不合法。我们可以用深度优先尝试每一种可能的分段方式,找出所有合法的IP地址。
这道题看似复杂,要处理的情况多。其实仔细拆分,可以分成几个简单的小模块。
- 如何判断每段 IP 地址的合法性?如果每段IP合法,那么整个IP地址就合法。IP的地址的合法性只需满足两个要求:
- 没有先导 0。判断方法:如果字符串的长度 > 1, 第一个字符是否为0,s.length() > 1 && s.charAt(0) == '0'
- 每段字符表示的数值在[0, 255] 之间,0 <= Integer.parseInt(s) <= 255
- 深度优先遍历
- 每段 IP 选 1 - 3 个字符,标记当前遍历 S 的位置
- 剪枝:剩下的字符个数不能少于还需遍历的段数,也不能超过其 3 倍,因为每段只能用 1 - 3 个字符
- 每段后面加 '.', 最后一段去掉。
- 递归终止条件:刚好将字符分成了 4 段,没有多余的字符剩余,两个条件可以判断;1. 段数 2. 索引
递归树图示如下:
💻代码:
Java
public List<String> restoreIpAddresses(String s) {
List<String> result = new ArrayList<>();
if (s == null || s.length() < 4 || s.length() > 12) {
return result;
}
backtrack(s, 0, 0, "", result);
return result;
}
private void backtrack(String s, int start, int segment, String path, List<String> result) {
if (segment == 4) {
if (start == s.length()) {
// 去掉末尾的 '.'
result.add(path.substring(0, path.length() - 1));
}
return;
}
// 剪枝 - 剩下的每段最少需要字符总数 [segments, 3 * segments]
int remaining = s.length() - start;
if (remaining < (4 - segment) || remaining > (4 - segment) * 3) {
return;
}
// 尝试截取 1 到 3 个字符
for (int i = 1; i <= 3 && start + i <= s.length(); i++) {
String str = s.substring(start, start + i);
if (isValid(str)) {
// 递归下一层
backtrack(s, start + i, segment + 1, path + str + ".", result);
}
}
}
private boolean isValid(String s) {
// 前导零
if (s.length() > 1 && s.charAt(0) == '0') {
return false;
}
int num = Integer.parseInt(s);
return num >= 0 && num <= 255;
}
Python
def restoreIpAddresses(self, s: str) -> List[str]:
result = []
if not s or len(s) < 4 or len(s) > 12:
return result
def backtrack(s: str, start: int, segment: int, path: str, result: List[str]):
if segment == 4:
if start == len(s):
# 去掉末尾的 '.'
result.append(path[:-1])
return
# 剪枝 - 剩下的每段最少需要字符总数 [segments, 3 * segments]
remaining = len(s) - start
if remaining < (4 - segment) or remaining > (4 - segment) * 3:
return
# 尝试截取 1 到 3 个字符
for i in range(1, 4):
if start + i > len(s):
break
str_segment = s[start:start + i]
if isValid(str_segment):
# 递归下一层
backtrack(s, start + i, segment + 1, path + str_segment + ".", result)
def isValid(s: str) -> bool:
# 前导零
if len(s) > 1 and s[0] == '0':
return False
num = int(s)
return 0 <= num <= 255
backtrack(s, 0, 0, "", result)
return result
C++
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
vector<string> result;
if (s.length() < 4 || s.length() > 12) {
return result;
}
backtrack(s, 0, 0, "", result);
return result;
}
private:
void backtrack(const string& s, int start, int segment, string path,
vector<string>& result) {
if (segment == 4) {
if (start == s.length()) {
// 去掉末尾的 '.'
path.pop_back();
result.push_back(path);
}
return;
}
// 剪枝 - 剩下的每段最少需要字符总数 [segments, 3 * segments]
int remaining = s.length() - start;
if (remaining < (4 - segment) || remaining > (4 - segment) * 3) {
return;
}
// 尝试截取 1 到 3 个字符
for (int i = 1; i <= 3 && start + i <= s.length(); i++) {
string str = s.substr(start, i);
if (isValid(str)) {
// 递归下一层
backtrack(s, start + i, segment + 1, path + str + ".", result);
}
}
}
bool isValid(const string& s) {
// 前导零
if (s.length() > 1 && s[0] == '0') {
return false;
}
int num = stoi(s);
return num >= 0 && num <= 255;
}
};
🚵🏻复杂度分析:
时间复杂度: O(1); O(3^4) = O(81),由于 IP 地址的每一段最多有 3 种选择,共 4 段,所以总的组合数为 3^4 = 81,属于常数级别。
空间复杂度: O(1),递归调用的最大深度为 4。