bigywater.tech
算法与数据结构 · LeetCode 93

LeetCode 93. 复原 IP 地址

给定一个只含数字的字符串,通过插入三个点把它切成四段,使每一段都是合法的 IP 地址段:数值在 0 到 255 之间,并且不能有前导 0(0 本身合法,00、01 不合法)。返回所有可能的合法 IP 地址。这是回溯的经典应用:依次切四段,每段试 1 到 3 个字符,检查合法性后递归。这道题的特点是搜索空间非常小(每段最多三种切法,一共 3⁴ = 81 种),所以重点不在效率而在合法性检查。本文用真实数据展示漏掉前导 0 检查会多出多少非法地址,给出五种语言的实现,并把切分过程和两种剪枝做成分步演示。

30,000
组随机对拍,与暴力解的结果全部一致
45 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。结果的顺序不作要求,按集合比较。基准解是三层循环枚举三个切点,再对四段逐一检查,与递归回溯是两种独立的写法。一共 16 组用例,覆盖:官方三个示例、字符串太短或太长(无法切成四段)、超过 255、前导 0、长度恰好 12 的 255255255255:

真实实测 · 本机执行输出
输入 ["25525511135"]
输出 ["255.255.11.135","255.255.111.35"]
输入 ["0000"]
输出 ["0.0.0.0"]
输入 ["101023"]
输出 ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
输入 ["1111"]
输出 ["1.1.1.1"]
输入 ["255255255255"]
输出 ["255.255.255.255"]
输入 ["256256256256"]
输出 []

陷阱:前导 0

只检查数值不超过 255 是不够的:"010" 转成数值是 10,看起来合法,但它有前导 0,不是合法的 IP 段。合法的段只有 "0" 本身,或者以 1 到 9 开头。下面是漏掉前导 0 检查的写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 没有「不能有前导 0」检查的写法
'25525511135': 该写法 -> ['255.255.11.135', '255.255.111.35']    solve -> ['255.255.11.135', '255.255.111.35']
'0000': 该写法 -> ['0.0.0.0']    solve -> ['0.0.0.0']
'101023': 该写法 -> ['1.0.1.023', '1.0.10.23', '1.0.102.3', '1.01.0.23', '1.01.02.3', '1.010.2.3', '10.1.0.23', '10.1.02.3', '10.10.2.3', '101.0.2.3']    solve -> ['1.0.10.23', '1.0.102.3', '10.1.0.23', '10.10.2.3', '101.0.2.3']
'010010': 该写法 -> ['0.1.0.010', '0.1.00.10', '0.1.001.0', '0.10.0.10', '0.10.01.0', '0.100.1.0', '01.0.0.10', '01.0.01.0', '01.00.1.0', '010.0.1.0']    solve -> ['0.10.0.10', '0.100.1.0']
'00000': 该写法 -> ['0.0.0.00', '0.0.00.0', '0.00.0.0', '00.0.0.0']    solve -> []
随机 20000 组:该写法出错的有 8760 组(43.8%)

"101023" 应该只有 5 个合法地址,漏检的写法给出 10 个(多出 1.0.1.023、1.01.0.23 这种带前导 0 的非法地址);"010010" 应该只有 2 个(0.10.0.10 和 0.100.1.0),错误写法给出 10 个;"00000" 应该没有合法地址(五个 0 无法切成四段而每段都是单独的 0,要么某段有 00),错误写法给出 4 个。"25525511135" 和 "0000" 里前导 0 没有影响,两种写法一致。在随机的 20000 组数据里(字符取自 0、1、2、5),错误写法出错的有 8760 组,占 43.8%。

回溯:五种语言的实现

dfs(start):left = 4 - len(parts) 是还要切几段。left 为 0 时,如果字符串刚好用完(start == len(s)),就把四段用 . 连接后收集;否则失败。剪枝:剩下的字符数 remain 必须在 [left, 3 * left] 之间(每段 1 到 3 个字符),否则直接返回。然后依次尝试这一段的长度 1、2、3:取出这一段 seg,如果它有前导 0(长度大于 1 且以 0 开头)或者数值大于 255,就跳过;否则加入 parts,递归,撤销。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(s): res, parts = [], [] def dfs(start): left = 4 - len(parts) # 还需要切几段 if left == 0: if start == len(s): # 4 段刚好用完整个字符串 res.append(".".join(parts)) return remain = len(s) - start if remain < left or remain > left * 3: # 剪枝:剩下的字符数必须在 [left, 3*left] 之间 return for length in (1, 2, 3): if start + length > len(s): break seg = s[start:start + length] if (len(seg) > 1 and seg[0] == "0") or int(seg) > 255: # 不能有前导 0("0" 本身可以),数值不超过 255 continue parts.append(seg) dfs(start + length) parts.pop() dfs(0) return res
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
["25525511135"]  →  ["255.255.11.135","255.255.111.35"]
["0000"]  →  ["0.0.0.0"]
["101023"]  →  ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
["1111"]  →  ["1.1.1.1"]
["255255255255"]  →  ["255.255.255.255"]
["256256256256"]  →  []
Java · 解法代码class Solution { public List<String> restoreIpAddresses(String s) { List<String> res = new ArrayList<>(); dfs(s, 0, new ArrayList<>(), res); return res; } private void dfs(String s, int start, List<String> parts, List<String> res) { int left = 4 - parts.size(); // 还需要切几段 if (left == 0) { if (start == s.length()) res.add(String.join(".", parts)); // 4 段刚好用完整个字符串 return; } int remain = s.length() - start; if (remain < left || remain > left * 3) return; // 剪枝:剩下的字符数必须在 [left, 3*left] 之间 for (int len = 1; len <= 3 && start + len <= s.length(); len++) { String seg = s.substring(start, start + len); if ((len > 1 && seg.charAt(0) == '0') || Integer.parseInt(seg) > 255) continue; // 不能有前导 0,数值不超过 255 parts.add(seg); dfs(s, start + len, parts, res); parts.remove(parts.size() - 1); } } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
["25525511135"]  →  ["255.255.11.135","255.255.111.35"]
["0000"]  →  ["0.0.0.0"]
["101023"]  →  ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
["1111"]  →  ["1.1.1.1"]
["255255255255"]  →  ["255.255.255.255"]
["256256256256"]  →  []
Go · 解法代码func restoreIpAddresses(s string) []string { res := []string{} parts := []string{} var dfs func(start int) dfs = func(start int) { left := 4 - len(parts) // 还需要切几段 if left == 0 { if start == len(s) { // 4 段刚好用完整个字符串 res = append(res, strings.Join(parts, ".")) } return } remain := len(s) - start if remain < left || remain > left*3 { // 剪枝:剩下的字符数必须在 [left, 3*left] 之间 return } for length := 1; length <= 3 && start+length <= len(s); length++ { seg := s[start : start+length] v, _ := strconv.Atoi(seg) if (length > 1 && seg[0] == '0') || v > 255 { // 不能有前导 0,数值不超过 255 continue } parts = append(parts, seg) dfs(start + length) parts = parts[:len(parts)-1] } } dfs(0) return res }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
["25525511135"]  →  ["255.255.11.135","255.255.111.35"]
["0000"]  →  ["0.0.0.0"]
["101023"]  →  ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
["1111"]  →  ["1.1.1.1"]
["255255255255"]  →  ["255.255.255.255"]
["256256256256"]  →  []
JavaScript · 解法代码function restoreIpAddresses(s) { const res = []; const parts = []; const dfs = (start) => { const left = 4 - parts.length; // 还需要切几段 if (left === 0) { if (start === s.length) res.push(parts.join(".")); // 4 段刚好用完整个字符串 return; } const remain = s.length - start; if (remain < left || remain > left * 3) return; // 剪枝:剩下的字符数必须在 [left, 3*left] 之间 for (let len = 1; len <= 3 && start + len <= s.length; len++) { const seg = s.substring(start, start + len); if ((len > 1 && seg[0] === "0") || Number(seg) > 255) continue; // 不能有前导 0,数值不超过 255 parts.push(seg); dfs(start + len); parts.pop(); } }; dfs(0); return res; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
["25525511135"]  →  ["255.255.11.135","255.255.111.35"]
["0000"]  →  ["0.0.0.0"]
["101023"]  →  ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
["1111"]  →  ["1.1.1.1"]
["255255255255"]  →  ["255.255.255.255"]
["256256256256"]  →  []
C++ · 解法代码#include <string> #include <vector> using namespace std; class Solution { void dfs(const string& s, int start, vector<string>& parts, vector<string>& res) { int left = 4 - (int)parts.size(); // 还需要切几段 if (left == 0) { if (start == (int)s.size()) { // 4 段刚好用完整个字符串 res.push_back(parts[0] + "." + parts[1] + "." + parts[2] + "." + parts[3]); } return; } int remain = (int)s.size() - start; if (remain < left || remain > left * 3) return; // 剪枝:剩下的字符数必须在 [left, 3*left] 之间 for (int len = 1; len <= 3 && start + len <= (int)s.size(); len++) { string seg = s.substr(start, len); if ((len > 1 && seg[0] == '0') || stoi(seg) > 255) continue; // 不能有前导 0,数值不超过 255 parts.push_back(seg); dfs(s, start + len, parts, res); parts.pop_back(); } } public: vector<string> restoreIpAddresses(string s) { vector<string> res, parts; dfs(s, 0, parts, res); return res; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
["25525511135"]  →  ["255.255.11.135","255.255.111.35"]
["0000"]  →  ["0.0.0.0"]
["101023"]  →  ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
["1111"]  →  ["1.1.1.1"]
["255255255255"]  →  ["255.255.255.255"]
["256256256256"]  →  []
真实实测 · 本机执行输出
共 16 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 16/16
  Java        通过 16/16
  Go          通过 16/16
  JavaScript  通过 16/16
  C++         通过 16/16

随机对拍

Python 版本另外和三层循环枚举做了随机对拍:字符串长度 1 到 13,字符取自 0、1、2、5(这样前导 0、超过 255、长度不合适这几类情形都会经常出现):

真实实测 · 本机执行输出
随机对拍 30000 组(长度 1 到 13、字符取自 0、1、2、5 的数字串(含前导 0、超过 255、长度不合适等情形);结果按集合比较),solve 与 brute 的结果全部一致。

另外四种语言只在上面 16 组固定用例上验证过,没有做大规模随机对拍。这道题的搜索空间很小(最多 81 种切法),所以没有做规模对比实验。

分步演示:101023

下面的演示用 "101023",共 5 个结果。可以看到几种情形:切满四段后还剩字符(失败)、剩下的字符数不在 [left, 3·left] 里(剪枝)、以及带前导 0 的段(如 "02"、"023")被跳过。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示最终得到的地址集合与 solve 的返回值一致。

复原 IP 地址:s = "101023"
已经切下的段正在尝试的这一段

    参考与说明

    • 官方题目页:LeetCode 93. 复原 IP 地址。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:IPv6 地址;输入含非数字字符时的处理(题目保证只含数字)。
    京ICP备2026067502号-1京公网安备11010502063487号