复原 IP 地址(回溯算法)

IP 地址的复原问题是字符串处理与回溯算法结合的经典例题,这道题要求我们从纯数字字符串中插入 '.' 形成有效的 IP 地址,且不能改变数字的原有顺序。本文将详细拆解解题思路,分析代码实现的核心细节,帮助大家彻底掌握这道题的解法。

一、题目回顾

题目描述

有效 IP 地址正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。给定一个只包含数字的字符串 s,返回所有可能的有效 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"]

核心约束

  1. 每个分段整数范围:0 <= x <= 255
  2. 分段不能有前导 0(单独一个 0 除外,如 "01" 无效)
  3. IP 地址必须恰好分为 4 个分段
  4. 字符串长度 1 <= s.length <= 20

二、解题思路:回溯算法(深度优先搜索 DFS)

1. 算法选择依据

复原 IP 地址的过程,本质上是将字符串分割为 4 个有效分段的所有可能尝试。这种需要枚举所有可能情况、并在尝试过程中验证有效性、不符合条件则回退的场景,非常适合用回溯算法解决。

回溯算法的核心思想是:试探 - 验证 - 回退,即沿着一条路径往下试探,若遇到不符合条件的情况则立即回退,尝试其他路径,直到找到所有符合条件的解。

2. 回溯算法的核心要素

针对本题,我们需要明确回溯的 3 个核心要素:

  • 路径 :当前已经构建好的 IP 分段(用 StringBuilder 存储,避免频繁字符串拼接带来的性能损耗)
  • 选择列表:当前位置开始,可选择的分段结束位置(最多往后选 3 个字符,因为单个分段最大为 255,占 3 位)
  • 终止条件:遍历完整个字符串,且恰好分成 4 个有效分段(找到一个解);或遍历完字符串 / 分段数达到 4(无法形成有效 IP,终止当前路径)

3. 整体解题流程

  1. 初始化结果列表 result 和路径存储 stringBuilder
  2. 调用回溯辅助方法,从字符串起始位置(start=0)、分段数 0(number=0)开始试探
  3. 在回溯方法中,先判断终止条件,符合则存入结果或直接终止
  4. 遍历当前可选的分段结束位置(最多 3 位),验证分段有效性
  5. 若分段有效,将其加入路径,并添加分隔符 '.'(前 3 个分段需要)
  6. 递归进入下一层,分段数加 1,起始位置更新为当前分段的下一个字符
  7. 递归返回后,执行回退操作 (恢复分段数、删除 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());
        }
    }
}

代码核心细节解析

  1. 成员变量的选择resultstringBuilder 定义为成员变量,避免在递归过程中频繁传递参数,简化代码结构,同时 StringBuilder 相比 String 具有更高的增删效率。
  2. 循环条件的限制i < s.length() && i - start < 3,确保单个分段最多只有 3 个字符,符合 IP 分段的长度要求。
  3. 前导 0 的验证i + 1 - start > 1 && s.charAt(start) - '0' == 0,单独一个 0 是有效的,只有长度大于 1 且以 0 开头的分段才无效,直接跳出循环(后续更长的分段也必然无效)。
  4. 分隔符的处理 :只有前 3 个分段后面需要添加 '.',第 4 个分段不需要,避免最终结果出现多余的分隔符。
  5. 关键的回退操作
    • 分段数 number 减 1,恢复到当前层的初始状态
    • stringBuilder 删除当前添加的内容,计算删除长度时要包含可能添加的分隔符,确保回退后的状态与试探前一致,这是回溯算法的核心,否则会导致路径混乱。
  6. 提前终止循环 :当发现当前分段超出 255 或存在前导 0 时,直接 break 而不是 continue,因为后续的分段更长,必然也无效,这样可以减少不必要的试探,提升算法效率。

四、算法优化与注意事项

1. 可优化点

  • 提前过滤字符串长度 :IP 地址最少 4 位(每个分段 1 位),最多 12 位(每个分段 3 位),若输入字符串 s.length() < 4s.length() > 12,可直接返回空列表,无需进入回溯,提升效率。
  • 避免重复转换整数 :在循环中,Integer.parseInt(s.substring(start, i+1)) 被调用了两次,可提取为变量,减少一次转换操作。

2. 常见错误

  • 回退操作不完整 :忘记删除 stringBuilder 中的分隔符,或删除长度计算错误,导致路径混乱,出现无效结果。
  • 前导 0 验证错误:将单独的 "0" 判定为无效,或允许 "01"、"00" 等有前导 0 的分段。
  • 终止条件遗漏:只判断了找到有效解的终止条件,忽略了遍历完字符串或分段数超标的终止条件,导致递归无法正常返回。

五、总结

  1. 复原 IP 地址是回溯算法的经典应用,核心是通过试探 - 验证 - 回退枚举所有有效分段组合。
  2. 解题的关键是明确 IP 分段的有效性约束(范围 0-255、无前导 0)、回溯的终止条件和回退操作。
  3. StringBuilder 的高效增删和合理的剪枝操作(提前跳出无效循环)是提升算法性能的重要手段。
  4. 回溯算法的核心是状态恢复,任何一次试探后的状态变更,在递归返回后都必须回退,否则会影响后续路径的尝试。

掌握本题的回溯思想后,还可以将其迁移到其他字符串分割类问题(如分割回文串),触类旁通,提升算法解题能力。

相关推荐
YGGP2 小时前
【Golang】LeetCode 5. 最长回文子串
算法·leetcode
挖矿大亨2 小时前
C++中的赋值运算符重载
开发语言·c++·算法
qq_433554542 小时前
C++区间DP
c++·算法·动态规划
Halo_tjn3 小时前
Java IO流实现文件操作知识点
java·开发语言·windows·算法
历程里程碑3 小时前
滑动窗口解法:无重复字符最长子串
数据结构·c++·算法·leetcode·职场和发展·eclipse·哈希算法
Geoffwo3 小时前
归一化简单案例
算法·语言模型
Felven3 小时前
C. Maximum Median
c语言·开发语言·算法
星火开发设计3 小时前
广度优先搜索(BFS)详解及C++实现
数据结构·c++·算法··bfs·宽度优先·知识
飞天狗1113 小时前
E. Blackslex and Girls
算法