IP 地址的复原问题是字符串处理与回溯算法结合的经典例题,这道题要求我们从纯数字字符串中插入 '.' 形成有效的 IP 地址,且不能改变数字的原有顺序。本文将详细拆解解题思路,分析代码实现的核心细节,帮助大家彻底掌握这道题的解法。
一、题目回顾
题目描述
有效 IP 地址正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。给定一个只包含数字的字符串 s,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成,不能重新排序或删除 s 中的任何数字。
示例说明
- 输入:
s = "25525511135",输出:["255.255.11.135","255.255.111.35"] - 输入:
s = "0000",输出:["0.0.0.0"] - 输入:
s = "101023",输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
核心约束
- 每个分段整数范围:
0 <= x <= 255 - 分段不能有前导 0(单独一个 0 除外,如 "01" 无效)
- IP 地址必须恰好分为 4 个分段
- 字符串长度
1 <= s.length <= 20
二、解题思路:回溯算法(深度优先搜索 DFS)
1. 算法选择依据
复原 IP 地址的过程,本质上是将字符串分割为 4 个有效分段的所有可能尝试。这种需要枚举所有可能情况、并在尝试过程中验证有效性、不符合条件则回退的场景,非常适合用回溯算法解决。
回溯算法的核心思想是:试探 - 验证 - 回退,即沿着一条路径往下试探,若遇到不符合条件的情况则立即回退,尝试其他路径,直到找到所有符合条件的解。
2. 回溯算法的核心要素
针对本题,我们需要明确回溯的 3 个核心要素:
- 路径 :当前已经构建好的 IP 分段(用
StringBuilder存储,避免频繁字符串拼接带来的性能损耗) - 选择列表:当前位置开始,可选择的分段结束位置(最多往后选 3 个字符,因为单个分段最大为 255,占 3 位)
- 终止条件:遍历完整个字符串,且恰好分成 4 个有效分段(找到一个解);或遍历完字符串 / 分段数达到 4(无法形成有效 IP,终止当前路径)
3. 整体解题流程
- 初始化结果列表
result和路径存储stringBuilder - 调用回溯辅助方法,从字符串起始位置(
start=0)、分段数 0(number=0)开始试探 - 在回溯方法中,先判断终止条件,符合则存入结果或直接终止
- 遍历当前可选的分段结束位置(最多 3 位),验证分段有效性
- 若分段有效,将其加入路径,并添加分隔符
'.'(前 3 个分段需要) - 递归进入下一层,分段数加 1,起始位置更新为当前分段的下一个字符
- 递归返回后,执行回退操作 (恢复分段数、删除
stringBuilder中当前添加的内容),尝试下一个可能的分段
三、代码逐行详解
java
运行
import java.util.ArrayList;
import java.util.List;
class Solution {
// 存储最终所有有效的 IP 地址结果
List<String> result = new ArrayList<String>();
// 存储当前正在构建的 IP 路径,高效进行增删操作
StringBuilder stringBuilder = new StringBuilder();
/**
* 主方法:对外暴露的接口,返回所有有效 IP 地址
* @param s 输入的纯数字字符串
* @return 有效 IP 地址列表
*/
public List<String> restoreIpAddresses(String s) {
// 调用回溯辅助方法,初始参数:起始位置0,已分段数0
restoreIpAddressesHandler(s, 0, 0);
return result;
}
/**
* 回溯辅助方法:核心逻辑实现
* @param s 输入的纯数字字符串
* @param start 当前分段的起始索引
* @param number 已经完成的分段数(0-4)
*/
public void restoreIpAddressesHandler(String s, int start, int number) {
// 终止条件1:遍历完整个字符串,且恰好分成4个分段(找到有效 IP)
if (start == s.length() && number == 4) {
result.add(stringBuilder.toString());
return;
}
// 终止条件2:遍历完字符串 或 分段数达到4(无法形成有效 IP,终止当前路径)
if (start == s.length() || number == 4) {
return;
}
// 遍历选择列表:从start开始,最多选3个字符(i-start < 3)
for (int i = start; i < s.length() && i - start < 3; i++) {
// 步骤1:提取当前分段字符串,并转换为整数验证范围(0-255)
String currentSegment = s.substring(start, i + 1);
int segmentNum = Integer.parseInt(currentSegment);
if (segmentNum < 0 || segmentNum > 255) {
break; // 超出范围,后续更长的分段也无效,直接跳出循环
}
// 步骤2:验证是否有前导0(分段长度>1且以0开头,无效)
if (i + 1 - start > 1 && s.charAt(start) - '0' == 0) {
break; // 前导0无效,后续分段也无效,跳出循环
}
// 步骤3:做出选择:将当前分段加入路径
stringBuilder.append(currentSegment);
// 前3个分段后面需要添加分隔符'.',第4个分段不需要
if (number < 3) {
stringBuilder.append(".");
}
// 步骤4:递归深入:分段数+1,起始位置更新为i+1
number++;
restoreIpAddressesHandler(s, i + 1, number);
// 步骤5:回退操作(关键):撤销当前选择,恢复状态,尝试下一个可能
number--; // 分段数回退
// 计算需要删除的字符长度,删除当前添加的分段和可能的分隔符
// 长度 = 当前分段长度 + (前3个分段添加的1个'.',否则0)
int deleteLength = currentSegment.length() + (number < 3 ? 1 : 0);
stringBuilder.delete(stringBuilder.length() - deleteLength, stringBuilder.length());
}
}
}
代码核心细节解析
- 成员变量的选择 :
result和stringBuilder定义为成员变量,避免在递归过程中频繁传递参数,简化代码结构,同时StringBuilder相比String具有更高的增删效率。 - 循环条件的限制 :
i < s.length() && i - start < 3,确保单个分段最多只有 3 个字符,符合 IP 分段的长度要求。 - 前导 0 的验证 :
i + 1 - start > 1 && s.charAt(start) - '0' == 0,单独一个 0 是有效的,只有长度大于 1 且以 0 开头的分段才无效,直接跳出循环(后续更长的分段也必然无效)。 - 分隔符的处理 :只有前 3 个分段后面需要添加
'.',第 4 个分段不需要,避免最终结果出现多余的分隔符。 - 关键的回退操作 :
- 分段数
number减 1,恢复到当前层的初始状态 stringBuilder删除当前添加的内容,计算删除长度时要包含可能添加的分隔符,确保回退后的状态与试探前一致,这是回溯算法的核心,否则会导致路径混乱。
- 分段数
- 提前终止循环 :当发现当前分段超出 255 或存在前导 0 时,直接
break而不是continue,因为后续的分段更长,必然也无效,这样可以减少不必要的试探,提升算法效率。
四、算法优化与注意事项
1. 可优化点
- 提前过滤字符串长度 :IP 地址最少 4 位(每个分段 1 位),最多 12 位(每个分段 3 位),若输入字符串
s.length() < 4或s.length() > 12,可直接返回空列表,无需进入回溯,提升效率。 - 避免重复转换整数 :在循环中,
Integer.parseInt(s.substring(start, i+1))被调用了两次,可提取为变量,减少一次转换操作。
2. 常见错误
- 回退操作不完整 :忘记删除
stringBuilder中的分隔符,或删除长度计算错误,导致路径混乱,出现无效结果。 - 前导 0 验证错误:将单独的 "0" 判定为无效,或允许 "01"、"00" 等有前导 0 的分段。
- 终止条件遗漏:只判断了找到有效解的终止条件,忽略了遍历完字符串或分段数超标的终止条件,导致递归无法正常返回。
五、总结
- 复原 IP 地址是回溯算法的经典应用,核心是通过试探 - 验证 - 回退枚举所有有效分段组合。
- 解题的关键是明确 IP 分段的有效性约束(范围 0-255、无前导 0)、回溯的终止条件和回退操作。
StringBuilder的高效增删和合理的剪枝操作(提前跳出无效循环)是提升算法性能的重要手段。- 回溯算法的核心是状态恢复,任何一次试探后的状态变更,在递归返回后都必须回退,否则会影响后续路径的尝试。
掌握本题的回溯思想后,还可以将其迁移到其他字符串分割类问题(如分割回文串),触类旁通,提升算法解题能力。