目标是给数字字符串插入 3 个点,将它切成恰好 4 个合法字段。每个字段必须满足:
"0",不能以 "0" 开头。回溯时维护「当前读取位置」和「已经选择的字段」,每层枚举下一段的 1~3 位。遇到非法字段就 break,当前分支不再往下展开。
给定一个只包含数字的字符串 s,在不重排、不删除任何数字的前提下插入 3 个 .,返回所有可能的有效 IPv4 地址。答案可以按任意顺序返回。
示例 1:
示例 2:
示例 3:
两道题的长度约束不同:
1 <= s.length <= 20。0 <= s.length <= 3000。由于有效 IPv4 地址去掉点后只能包含 4~12 位数字,长度不在这个范围内时可以直接返回空数组。
一个字段合法,当且仅当同时满足:
"0"。例如:
| 字段 | 是否合法 | 原因 |
|---|---|---|
"0" | 是 | 单个零合法 |
"01" | 否 | 含有前导零 |
"10" | 是 | 数值在范围内 |
"255" | 是 | 等于上界 |
"256" | 否 | 大于 255 |
递归函数 dfs(start, path) 中:
start 表示下一段从 s[start] 开始。path 保存已经确定的字段数组(如 ["255", "255"])。result 保存所有完整且合法的 IP 地址。每次递归入口都满足以下不变量:
path 中的每个字段都合法。path.join('') 恰好等于 s.slice(0, start)。path.length <= 4。因此,当 path 恰好有 4 段并且 start === s.length 时,才能加入答案。
由于 path 最多只有 4 个短字符串,拷贝代价可以忽略。用 [...path, seg] 代替 push/pop 的好处是:
path,逻辑更纯粹;pop() 导致的分支污染。如果面试官追问,也可以改回 push/pop 以展示对回溯状态恢复的理解。
dfs 的 for 循环只枚举字段长度 len = 1, 2, 3。一旦 start + len > s.length,说明剩余字符不足以支撑当前长度,直接 break。
如果当前字段的第一个字符是 "0",只有单字符 "0" 合法。更长的候选都会保留前导零,因此可以直接 break。
同一个起点下,字段从 1 位扩展到 3 位时数值只会增大。一旦数值超过 255,继续增加数字也不可能重新合法,可以 break。
IPv4 固定 4 段、每段最多 3 位,搜索树深度最多 4、分支因子最多 3。即便输入长度达到 3000,有效的递归调用也只有几十次,因此不必像通用回溯那样显式计算 remainingChars 来做范围剪枝。
以 s = "010010" 为例:
| 已选字段 | 剩余字符串 | 下一段的有效选择 |
|---|---|---|
[] | "010010" | 只能选 "0" |
["0"] | "10010" | "1"、"10"、"100" |
["0", "10"] | "010" | 只能选 "0" |
["0", "10", "0"] | "10" | 选 "10",得到 0.10.0.10 |
["0", "100"] | "10" | 选 "1",最后选 "0",得到 0.100.1.0 |
其他分支会因为越界、前导零或字段数值超过 255 被剪掉。
最终结果:
| 阶段 | 对应代码 | 作用 |
|---|---|---|
| 初始化 | result | 保存答案 |
| 结束条件 | path.length === 4 | 只有恰好用完字符串时才记录答案 |
| 枚举字段 | len = 1; len <= 3 | 下一段只可能包含 1~3 位 |
| 越界剪枝 | start + len > s.length | 字符不够时停止 |
| 前导零剪枝 | str[0] === '0' | 禁止 "00"、"01" 等字段 |
| 数值剪枝 | +str <= 255 | 禁止超过 IPv4 字段上界 |
| 递归与回溯 | [...path, seg] | 用扩展运算符传入新路径,无需 pop |
可以从「生成的答案都合法」和「不会漏掉合法答案」两方面证明。
加入 path 的字段都通过了 valid() 检查(长度天然在 1~3 内、无前导零、数值不超过 255)。算法只有在 path 恰好包含 4 段且所有字符都被使用时才记录结果,因此生成的每个字符串都是合法 IPv4 地址。
任意合法 IPv4 地址都由 4 个长度为 1~3 的合法字段组成。DFS 会从每个字段起点依次尝试长度 1、2、3,因此一定会枚举到该地址的四个字段。
被剪掉的分支只可能是:
start + len > s.length)。这些情况都不可能属于合法答案,所以剪枝不会漏解。
"0000" 只能得到 "0.0.0.0"。"010010" 中可以选择 "0",但不能选择 "01" 或 "010"。"255" 合法,"256" 非法。IPv4 固定为 4 段,每段最多尝试 3 种长度,因此搜索树的状态数存在与输入长度无关的常数上界:
每次只处理至多 3 个字符。由于 4 和 3 都是 IPv4 的固定常数,也可以记为:
O(1)。O(1),不计返回结果。长度超过 12 的输入仍会以常数时间结束,因为递归深度被字段数限制在 4 层。
三个点的位置存在多种选择,而每次选择都会影响后续剩余字符。回溯可以枚举每一段的长度,并在发现当前前缀不可能形成合法 IP 时立即停止。
只需要当前读取位置 start 和已经选择的字段 path。原字符串和答案数组在闭包中共享,不需要为每层复制剩余字符串。
push/pop 和用[...path, seg] 有什么区别?push/pop 复用同一个数组,需要手动恢复现场,空间更省;[...path, seg] 每层创建新数组,逻辑更纯粹,不用担心遗忘 pop()。本题中 path 极短,两种写法都可以接受。
break?当字段以 "0" 开头时,只有单字符 "0" 合法。更长的候选都会保留这个前导零,因此都不合法,可以停止当前起点的后续枚举。
break?当前字段只包含数字,继续在右侧添加数字不会让它重新落回 0~255,所以更长候选同样非法。
只有「恰好 4 段」和「恰好用完所有字符」同时满足才是完整地址。缺少任一条件,都可能接受三段地址或仍有字符残留的错误结果。
O(1)?IPv4 的字段数固定为 4,每段长度固定不超过 3,搜索状态数量存在与输入长度无关的常数上界。
可以枚举三个点的位置 i < j < k,分别验证 s[0..i)、s[i..j)、s[j..k) 和 s[k..n)。本质上仍是在枚举所有切分位置,代码可能更直接,但字段合法性判断不能省略。
"0" 和含前导零的 "00"、"01" 混为一谈。path 配合 push/pop 后忘记 pop(),污染其他递归分支。break,不必只跳过当前候选。先只回答第 1 题,再展开后续问题:
valid() 里不需要显式判断 str.length > 3?"0000" 时,为什么每一层都只能选择一位?path.length === 4,却不检查 start === s.length,会接受什么错误结果?[...path, seg] 改回 push/pop 写法,并说明两者的优劣。