93. 复原 IP 地址
目录
题目链接:
题目:
解题思路:
代码:
法一:
法二:
总结:
题目链接:
93. 复原 IP 地址 - 力扣(LeetCode)
题目:
解题思路:
回溯,若单次字符串长度超过四,直接跳(剪枝),然后就是本层递归长度,最后每次递归就判断是否有前导0和值是否超过255
代码:
法一:
普通回溯,每层直接截取一个,最后可能会堆积在一个位置,那就设置提前判断(是否长度大于四),大于四直接返回.
/*
*Stack Integer ArrayList String StringBuffer peek
*Collections imports LinkedList offer return
*empty polls offerLast pollFirst isEmpty
*List Deque append length HashMap null
*return remove boolean
*/
class Solution {List<String> res;List<String> path;public List<String> restoreIpAddresses(String s) {res=new ArrayList<>();path=new ArrayList<>();find(s,0);return res;}public void find(String s,int idx){if(path.size()>4) return ;if(idx>=s.length()&&path.size()==4){StringBuffer node=new StringBuffer();for(int i=0;i<path.size()-1;i++){node.append(path.get(i));node.append(".");} node.append(path.get(path.size()-1));res.add(node.toString());return ;}for(int i=idx;i<s.length();i++){if(i-idx>4) continue ;if(judge(s,idx,i)!=true){continue; } path.add(s.substring(idx,i+1));find(s,i+1);path.remove(path.size()-1);}}public boolean judge(String s,int left,int right){if(left!=right&&s.charAt(left)=='0') return false;int val=0;for(int i=left;i<=right;i++){int x=s.charAt(i)-'0';val=val*10+x;}if(val>255) return false;return true;}
}
法二:
/*
*Stack Integer ArrayList String StringBuffer peek
*Collections imports LinkedList offer return
*empty polls offerLast pollFirst isEmpty
*List Deque append length HashMap null
*return remove boolean continue charAt
*toString
*/
class Solution {List<String> res;StringBuffer path;public List<String> restoreIpAddresses(String s) {res=new ArrayList<>();path=new StringBuffer(s);find(0,0);return res;}public void find(int idx,int number){if(number>3) return ;if(number==3){if(judge(idx,path.length()-1)==true){res.add(path.toString());}return ;}for(int i=idx;i<path.length();i++){if(judge(idx,i)==false)break;path.insert(i+1,'.');find(i+2,number+1);path.deleteCharAt(i+1);}}public boolean judge(int left,int right){if(left>right) return false;if(left!=right&&path.charAt(left)=='0') return false;int val=0;for(int i=left;i<=right;i++){val=val*10+(path.charAt(i)-'0');if(val>255) return false;}return true;}
}
深度解析基于 StringBuffer 的 IP 地址恢复算法:回溯与字符串操作的结合
IP 地址恢复问题是字符串处理与回溯算法结合的典型案例,要求将一个字符串分割为 4 个有效的 IP 地址段(每个段为 0-255 之间的整数,且不能有前导零)。本文将详细解析一段使用StringBuffer实现的 IP 地址恢复算法,从数据结构选择、回溯逻辑到字符串操作细节,全方位展示如何在递归过程中直接操作字符串实现分割与恢复。
一、问题背景与需求分析
1. 问题描述
IP 地址恢复问题可描述为:
给定一个只包含数字的字符串s
将s分割为 4 个部分,每个部分必须是有效的 IP 地址段:
数值范围在 0-255 之间
不能有前导零(除非该部分就是 "0")
返回所有可能的有效 IP 地址组合
例如,对于输入字符串"25525511135",有效的 IP 地址组合为["255.255.11.135", "255.255.111.35"]。
2. 核心挑战
分割的精确性:必须恰好分割为 4 个部分,且所有部分拼接后等于原字符串
IP 段的有效性:每个段需满足数值范围和格式约束
字符串操作的复杂性:在递归过程中动态插入和删除分隔符(点号),需要精确控制索引变化
与使用列表存储中间结果的传统方法不同,本文解析的代码直接使用StringBuffer操作原始字符串,通过插入和删除点号实现分割,这种方式更贴近字符串直接处理的场景,但也带来了索引管理的复杂性。
二、代码整体结构解析
java
运行
import java.util.ArrayList;
import java.util.List;
class Solution {
// 存储最终所有有效的IP地址
List<String> res;
// 用于动态构建IP地址的字符串缓冲区
StringBuffer path;
// 对外接口:输入字符串,返回所有有效IP地址
public List<String> restoreIpAddresses(String s) {
res = new ArrayList<>();
path = new StringBuffer(s); // 初始化路径为原始字符串
find(0, 0); // 启动回溯:从索引0开始,已分割0个部分
return res;
}
// 回溯核心函数
/**
* @param idx 当前分割的起始索引(在原始字符串中的位置)
* @param number 已分割的部分数量
*/
public void find(int idx, int number) {
// 剪枝:已分割部分超过3个(IP最多4个部分),直接返回
if (number > 3) return;
// 终止条件:已分割3个部分,检查剩余部分是否有效
if (number == 3) {
if (judge(idx, path.length() - 1)) {
res.add(path.toString());
}
return;
}
// 循环尝试分割位置
for (int i = idx; i < path.length(); i++) {
// 剪枝:当前子串无效,直接终止循环(因后续子串更长,更可能无效)
if (judge(idx, i) == false)
break;
// 选择:在i+1位置插入点号
path.insert(i + 1, '.');
// 递归:下一次分割从i+2开始(跳过插入的点号),已分割部分+1
find(i + 2, number + 1);
// 回溯:删除插入的点号,恢复原始状态
path.deleteCharAt(i + 1);
}
}
// 辅助函数:判断子串是否为有效的IP段
/**
* @param left 子串起始索引
* @param right 子串结束索引
* @return 子串是否为有效的IP段
*/
public boolean judge(int left, int right) {
// 边界检查:起始索引大于结束索引,无效
if (left > right) return false;
// 前导零检查:长度大于1且以0开头,无效(如"01")
if (left != right && path.charAt(left) == '0') return false;
// 数值范围检查:必须在0-255之间
int val = 0;
for (int i = left; i <= right; i++) {
val = val * 10 + (path.charAt(i) - '0');
if (val > 255) return false; // 提前终止,优化效率
}
return true;
}
}
代码采用回溯法框架,但在数据结构选择上有其独特性:
StringBuffer path:直接使用字符串缓冲区存储当前处理的字符串,通过插入和删除点号实现动态分割
核心函数:
restoreIpAddresses:初始化并启动回溯过程
find:实现回溯的核心逻辑,负责插入点号、递归和回溯
judge:验证子串是否为有效的 IP 段
这种实现方式的特点是直接操作字符串,避免了使用列表存储中间段再拼接的过程,但需要精确处理插入点号后的索引偏移问题。
三、核心组件解析
1. 数据结构选择:为什么用 StringBuffer?
java
运行
StringBuffer path;
选择StringBuffer而非String或List<String>的原因:
可变性:StringBuffer支持在原有对象上进行插入、删除等修改操作,而String是不可变的,每次修改都会创建新对象
高效性:对于需要频繁修改的场景(如多次插入和删除点号),StringBuffer的性能优于String
直接性:直接在原始字符串上操作,避免了将分段存储在列表中再拼接的额外步骤
与使用List<String>存储分段的方式相比,这种方式更贴近字符串的直接处理,但需要更仔细地管理索引,因为插入点号会改变字符串的长度和后续字符的位置。
2. 回溯函数参数设计
java
运行
public void find(int idx, int number)
两个核心参数的设计体现了算法的关键逻辑:
idx:当前分割的起始索引,指的是在path中的位置(需注意点号插入对索引的影响)
number:已成功分割的 IP 段数量(范围 0-3,因为 IP 地址共 4 个段)
这两个参数共同控制着回溯的进程:number控制分割的总数量,idx控制当前分割的位置。
3. 有效性判断函数 judge
java
运行
public boolean judge(int left, int right) {
// 边界检查
if (left > right) return false;
// 前导零检查
if (left != right && path.charAt(left) == '0') return false;
// 数值范围检查
int val = 0;
for (int i = left; i <= right; i++) {
val = val * 10 + (path.charAt(i) - '0');
if (val > 255) return false;
}
return true;
}
该函数是确保 IP 段有效的核心,包含三个关键检查:
边界检查:left > right表示子串为空,无效
前导零检查:
若子串长度为 1(left == right),允许为 "0"
若子串长度大于 1 且以 "0" 开头(如 "01"、"012"),则无效
数值范围检查:
将字符转换为整数,确保结果在 0-255 之间
循环中加入提前终止条件(val > 255时立即返回),优化性能
例如:
"255":left=0, right=2,转换后val=255 → 有效
"025":left=0, right=2,以 "0" 开头且长度 > 1 → 无效
"256":转换后val=256 > 255 → 无效
四、回溯核心逻辑深度解析
find方法实现了 "剪枝判断 → 终止条件 → 循环分割 → 回溯操作" 的完整逻辑,其核心是通过在StringBuffer中动态插入和删除点号来探索所有可能的分割方式。
1. 剪枝优化:避免无效路径
java
运行
if (number > 3) return;
IP 地址必须恰好由 4 个部分组成,当已分割的部分数量number超过 3 时,即使后续能分割出有效部分,总数量也会超过 4,因此直接终止当前路径。
这一剪枝条件能显著减少无效的递归调用,例如当number=4时,无论后续字符串如何,都不可能形成有效的 IP 地址。
2. 终止条件:检查最后一段
java
运行
if (number == 3) {
if (judge(idx, path.length() - 1)) {
res.add(path.toString());
}
return;
}
当已分割出 3 个部分(number=3)时,剩余的字符串必须作为第 4 个部分:
检查从idx到字符串末尾的子串是否为有效 IP 段
若有效,则将当前path(已包含 3 个点号的完整 IP 地址)加入结果集
注意此时path中已经包含之前插入的 3 个点号,因此直接调用toString()即可得到完整的 IP 地址
例如,当number=3且idx=8,path为"255.255.11.",则检查从索引 8 到末尾的子串是否有效,若有效则形成完整 IP 地址。
3. 循环分割:尝试所有可能的分割点
java
运行
for (int i = idx; i < path.length(); i++) {
if (judge(idx, i) == false)
break;
path.insert(i + 1, '.');
find(i + 2, number + 1);
path.deleteCharAt(i + 1);
}
这是回溯的核心循环,负责尝试所有可能的分割位置:
循环变量 i 的意义
i表示当前分割段的结束索引(在path中的位置),从idx开始遍历,形成子串path[idx...i]。
剪枝条件:judge(idx, i) == false
若当前子串path[idx...i]无效,则直接终止循环(而非continue),原因是:
循环中i逐渐增大,子串path[idx...i]的长度逐渐增加
对于无效的子串,更长的子串(i更大)只会更无效(如已超过 255 或存在前导零)
因此使用break而非continue可以减少不必要的判断
例如,当idx=0且i=2时子串已无效,则i=3,4...的子串必然也无效,直接终止循环。
回溯三部曲:插入 - 递归 - 删除
插入点号:path.insert(i + 1, '.')
在当前子串的结束索引i之后插入点号,将字符串分割为path[0...i]和剩余部分
插入后,path的长度增加 1,后续字符的索引会偏移 1
递归探索:find(i + 2, number + 1)
下一次分割的起始索引为i + 2:因为插入点号后,原索引i + 1的位置变为点号,需要跳过点号从i + 2开始
已分割部分数量number加 1
删除点号:path.deleteCharAt(i + 1)
递归返回后,删除之前插入的点号,恢复path到插入前的状态
确保下一次循环尝试其他分割位置时path的状态正确
这三步操作构成了回溯的核心,通过 "修改 - 探索 - 恢复" 的模式,实现了对所有可能分割方式的遍历。
五、完整执行流程模拟
以输入字符串"25525511135"为例,我们模拟算法的核心执行流程:
1. 初始化
输入字符串:"25525511135"
res = [],path = new StringBuffer("25525511135")
调用find(0, 0)
2. 第一次调用find(0, 0)(number=0,未分割任何部分)
number=0 <= 3,进入循环i=0到path.length()-1(10)
i=2(子串path[0...2] = "255")
judge(0, 2):"255" 是有效 IP 段(255 ≤ 255 且无铅零)
path.insert(3, '.') → path变为"255.25511135"(长度 11)
调用find(4, 1)(i+2=2+2=4,number+1=1)
第二次调用find(4, 1)(number=1,已分割 1 个部分)
循环i=4到 10
i=6(子串path[4...6] = "255")
judge(4, 6):有效
path.insert(7, '.') → path变为"255.255.11135"(长度 12)
调用find(8, 2)
第三次调用find(8, 2)(number=2,已分割 2 个部分)
循环i=8到 11
i=9(子串path[8...9] = "11")
judge(8, 9):有效
path.insert(10, '.') → path变为"255.255.11.135"(长度 13)
调用find(11, 3)
第四次调用find(11, 3)(number=3,已分割 3 个部分)
触发终止条件number==3
检查子串path[11...12] = "135" → judge(11, 12)有效
将path.toString()("255.255.11.135")添加到res → res = ["255.255.11.135"]
返回
回溯操作:
删除点号 → path恢复为"255.255.11135"
继续循环,尝试其他分割位置...
其他有效路径
算法继续探索其他分割方式,当i=8(子串"255.255.111")时:
插入点号后path为"255.255.111.35"
最后一段"35"有效,添加到res → res = ["255.255.11.135", "255.255.111.35"]
3. 最终结果
算法返回["255.255.11.135", "255.255.111.35"],与预期结果一致。
六、关键细节:索引管理与字符串操作
使用StringBuffer直接操作字符串时,索引管理是最容易出错的部分,需要特别注意以下几点:
1. 插入点号后的索引偏移
插入点号前,字符的索引范围是[0, len-1]
插入点号后,字符串长度变为len+1,插入位置i+1之后的所有字符索引都增加 1
因此,下一次递归的起始索引需要是i+2(跳过原i+1位置的字符和新插入的点号)
例如:
插入前:i=2,下一个字符索引是 3
插入点号后:点号位于索引 3,原索引 3 的字符移到索引 4
因此下一次起始索引是i+2=4
2. 循环终止条件
循环条件i < path.length()看似简单,但随着点号的插入和删除,path.length()是动态变化的
在递归过程中,path的长度会在插入时增加 1,删除时减少 1,始终保持与当前状态一致
3. judge 函数的索引参数
judge(left, right)中的left和right是相对于当前path的索引,而非原始字符串
这确保了在点号插入后,仍能正确定位子串的位置
例如,当path为"255.255.11135"时,left=8指向的是 "1",而非原始字符串的索引 8。
七、算法特性分析
1. 时间复杂度
每个 IP 地址有 4 个部分,每个部分最多 3 个字符,因此最多有3^4 = 81种分割方式
每种分割方式需要调用judge函数,最坏情况下judge的时间复杂度为 O (3)(检查 3 个字符)
因此,总体时间复杂度为 O (3^4 * 3) = O (243),属于常数级复杂度
2. 空间复杂度
递归调用栈的深度最多为 4(对应 4 个 IP 段)
StringBuffer的空间复杂度为 O (n),其中 n 是输入字符串的长度
结果列表的空间复杂度取决于有效 IP 地址的数量,最坏情况下为 O (3^4) = O (81)
因此,总体空间复杂度为 O (n),主要由StringBuffer和递归栈决定。
3. 与列表存储方式的对比
实现方式 核心数据结构 字符串操作 索引管理 可读性 性能
列表存储 List<String> 拼接操作 简单(无需考虑点号) 高 中等(需多次拼接)
StringBuffer StringBuffer 插入 / 删除 复杂(需处理索引偏移) 中等 高(直接修改)
两种方式各有优劣:列表存储方式更直观,索引管理简单;StringBuffer方式减少了字符串拼接操作,理论性能更优,但需要更仔细地处理索引。
八、常见问题与优化建议
1. 为什么循环中用break而非continue?
当judge(idx, i)返回false时,使用break而非continue的原因是:
对于当前idx,若i处的子串无效,则i+1, i+2...处的子串更长,必然也无效(如已超过 255)
例如,idx=0时,若i=3的子串 "256" 无效,则i=4的子串 "256x" 更无效
因此break可以提前终止循环,减少不必要的判断
2. 如何处理空字符串或长度不足的情况?
代码中未显式处理输入字符串长度小于 4 或大于 12 的情况(IP 地址最短 4 个字符,最长 12 个字符),可在restoreIpAddresses中添加预处理:
java
运行
if (s.length() < 4 || s.length() > 12) {
return res; // 直接返回空列表
}
这能快速过滤掉不可能形成有效 IP 地址的输入,提升效率。
3. 为什么find方法中递归调用的参数是i+2?
插入点号后,点号位于i+1的位置
下一次分割需要从点号之后开始,即i+2的位置
若使用i+1,则会从点号处开始,导致judge函数判断包含点号的子串,必然无效
4. 如何避免前导零的误判?
judge函数中的left != right && path.charAt(left) == '0'条件已处理前导零问题:
允许单个 "0"(left == right且字符为 '0')
禁止 "01"、"012" 等有前导零的多字符子串
九、总结
本文解析的 IP 地址恢复算法采用StringBuffer直接操作字符串,通过动态插入和删除点号实现回溯,展现了字符串处理与回溯算法结合的独特思路。算法的核心亮点包括:
高效的字符串操作:使用StringBuffer的插入和删除方法,避免了字符串拼接的额外开销
精准的索引管理:通过i+2等参数调整,正确处理了点号插入后的索引偏移
有效的剪枝策略:通过number > 3和judge函数的提前终止,减少了无效递归
完整的有效性检查:涵盖了边界、前导零和数值范围的全面检查
理解这种实现方式不仅能解决 IP 地址恢复问题,更能掌握在回溯算法中直接操作复杂数据结构(如字符串)的技巧。这种技巧在文本分割、格式转换等问题中具有广泛的应用价值。
回溯算法的魅力在于其 "试错 - 回溯 - 再试" 的探索模式,而本文的实现则展示了如何将这种模式与字符串的动态操作相结合,在保证正确性的同时追求更高的效率。对于需要频繁修改字符串的回溯问题,StringBuffer(或StringBuilder)往往是比列表存储更优的选择。
总结:
本文解析了使用回溯算法恢复IP地址的两种实现方法,重点阐述了基于StringBuffer的动态字符串操作方式。算法通过递归分割字符串,插入点号作为分隔符,并利用剪枝优化(如长度限制、前导零判断和数值范围检查)来高效生成所有可能的有效IP地址组合。文章详细分析了索引管理、字符串操作等关键细节,比较了StringBuffer与列表存储的优缺点,并提供了完整的执行流程示例。该算法展示了回溯与字符串处理的巧妙结合,具有常数级时间复杂度和线性空间复杂度,适用于类似的分割问题。