LeetCode第93题:复原IP地址

LeetCode第93题:复原IP地址

题目描述

有效的 IP 地址由四个整数组成,整数之间用 . 分隔,其中每个整数位于 0 到 255 之间(包含 0 和 255),且不能含有前导零。

例如:"0.1.2.201""192.168.1.1" 是有效的 IP 地址,但是 "0.011.255.245""192.168.1.312""[email protected]" 是无效的 IP 地址。

给定一个只包含数字的字符串 s,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 . 来形成。你不能重新排序或删除 s 中的任何数字。你可以按任何顺序返回答案。

难度

中等

问题链接

复原IP地址

示例

示例 1:

ini 复制代码
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

ini 复制代码
输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

arduino 复制代码
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示

  • 1 <= s.length <= 20
  • s 仅由数字组成

解题思路

这道题要求我们从一个只包含数字的字符串中恢复所有可能的有效 IP 地址。我们可以使用回溯法(深度优先搜索)来解决这个问题。

方法一:回溯法

回溯法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯法会通过在上一步进行一些变化来舍弃该解,即回溯并且再次尝试。

对于这个问题,我们需要在字符串中插入三个点,将其分成四个部分,每个部分都必须是有效的 IP 地址段。我们可以使用回溯法来尝试所有可能的分割方式。

关键点

  1. IP 地址由四个整数组成,每个整数在 0 到 255 之间。
  2. 整数之间用点分隔。
  3. 整数不能有前导零,除非这个整数就是 0。
  4. 我们需要在字符串中插入三个点,将其分成四个部分。

算法步骤分析

回溯法算法步骤

步骤 操作 说明
1 初始化 创建一个空列表来存储结果,初始化回溯函数的参数
2 定义回溯函数 函数接收当前处理的索引、已分割的段数和当前构建的 IP 地址
3 终止条件 如果已经分割了四段且处理完所有字符,将当前 IP 地址添加到结果列表中
4 剪枝 如果段数超过 4 或者处理完所有字符但段数不足 4,直接返回
5 遍历可能的分割点 从当前索引开始,尝试分割出一个有效的 IP 地址段
6 验证 IP 段 检查分割出的段是否是有效的 IP 地址段(0-255,无前导零)
7 递归 如果当前段有效,递归处理剩余部分
8 回溯 回溯并尝试其他可能的分割方式
9 返回结果 返回所有可能的有效 IP 地址

算法可视化

以示例 s = "25525511135" 为例,我们使用回溯法来找出所有可能的有效 IP 地址:

  1. 初始状态:s = "25525511135",我们需要在其中插入三个点。

  2. 第一次分割:

    • 尝试 "2":有效的 IP 段,继续处理 "5525511135"
    • 尝试 "25":有效的 IP 段,继续处理 "525511135"
    • 尝试 "255":有效的 IP 段,继续处理 "25511135"
  3. 对于 "2",第二次分割:

    • 尝试 "2.5":有效的 IP 段,继续处理 "525511135"
    • 尝试 "2.55":有效的 IP 段,继续处理 "25511135"
    • 尝试 "2.552":无效的 IP 段(大于 255),回溯
  4. 对于 "2.5",第三次分割:

    • 尝试 "2.5.5":有效的 IP 段,继续处理 "25511135"
    • 尝试 "2.5.52":有效的 IP 段,继续处理 "5511135"
    • 尝试 "2.5.525":无效的 IP 段(大于 255),回溯
  5. 对于 "2.5.5",第四次分割:

    • 尝试 "2.5.5.25511135":无效的 IP 段(大于 255),回溯
  6. 对于 "2.5.52",第四次分割:

    • 尝试 "2.5.52.5511135":无效的 IP 段(大于 255),回溯
  7. 继续回溯并尝试其他分割方式...

  8. 最终找到两个有效的 IP 地址:

    • "255.255.11.135"
    • "255.255.111.35"

代码实现

C# 实现

csharp 复制代码
public class Solution {
    public IList<string> RestoreIpAddresses(string s) {
        List<string> result = new List<string>();
        Backtrack(s, 0, 0, "", result);
        return result;
    }
    
    private void Backtrack(string s, int index, int segments, string current, List<string> result) {
        // 如果已经处理了4段且用完了所有字符,则找到一个有效的IP地址
        if (segments == 4 && index == s.Length) {
            // 移除最后一个点
            result.Add(current.Substring(0, current.Length - 1));
            return;
        }
        
        // 如果段数超过4或者已经处理完所有字符但段数不足4,则无效
        if (segments > 4 || index == s.Length) {
            return;
        }
        
        // 尝试分割出一个IP段(最多3位数字)
        for (int len = 1; len <= 3 && index + len <= s.Length; len++) {
            string segment = s.Substring(index, len);
            
            // 检查IP段是否有效
            if (IsValidSegment(segment)) {
                // 递归处理剩余部分
                Backtrack(s, index + len, segments + 1, current + segment + ".", result);
            }
        }
    }
    
    private bool IsValidSegment(string segment) {
        // IP段不能有前导零,除非这个段就是0
        if (segment.Length > 1 && segment[0] == '0') {
            return false;
        }
        
        // IP段必须在0到255之间
        int value = int.Parse(segment);
        return value >= 0 && value <= 255;
    }
}

Python 实现

python 复制代码
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        result = []
        self.backtrack(s, 0, 0, "", result)
        return result
    
    def backtrack(self, s, index, segments, current, result):
        # 如果已经处理了4段且用完了所有字符,则找到一个有效的IP地址
        if segments == 4 and index == len(s):
            # 移除最后一个点
            result.append(current[:-1])
            return
        
        # 如果段数超过4或者已经处理完所有字符但段数不足4,则无效
        if segments > 4 or index == len(s):
            return
        
        # 尝试分割出一个IP段(最多3位数字)
        for length in range(1, 4):
            if index + length > len(s):
                break
                
            segment = s[index:index + length]
            
            # 检查IP段是否有效
            if self.is_valid_segment(segment):
                # 递归处理剩余部分
                self.backtrack(s, index + length, segments + 1, current + segment + ".", result)
    
    def is_valid_segment(self, segment):
        # IP段不能有前导零,除非这个段就是0
        if len(segment) > 1 and segment[0] == '0':
            return False
        
        # IP段必须在0到255之间
        value = int(segment)
        return 0 <= value <= 255

C++ 实现

cpp 复制代码
class Solution {
public:
    vector<string> restoreIpAddresses(string s) {
        vector<string> result;
        backtrack(s, 0, 0, "", result);
        return result;
    }
    
private:
    void backtrack(const string& s, int index, int segments, string current, vector<string>& result) {
        // 如果已经处理了4段且用完了所有字符,则找到一个有效的IP地址
        if (segments == 4 && index == s.length()) {
            // 移除最后一个点
            result.push_back(current.substr(0, current.length() - 1));
            return;
        }
        
        // 如果段数超过4或者已经处理完所有字符但段数不足4,则无效
        if (segments > 4 || index == s.length()) {
            return;
        }
        
        // 尝试分割出一个IP段(最多3位数字)
        for (int len = 1; len <= 3 && index + len <= s.length(); len++) {
            string segment = s.substr(index, len);
            
            // 检查IP段是否有效
            if (isValidSegment(segment)) {
                // 递归处理剩余部分
                backtrack(s, index + len, segments + 1, current + segment + ".", result);
            }
        }
    }
    
    bool isValidSegment(const string& segment) {
        // IP段不能有前导零,除非这个段就是0
        if (segment.length() > 1 && segment[0] == '0') {
            return false;
        }
        
        // IP段必须在0到255之间
        int value = stoi(segment);
        return value >= 0 && value <= 255;
    }
};

执行结果

C# 执行结果

  • 执行用时:92 ms,击败了 93.33% 的 C# 提交
  • 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交

Python 执行结果

  • 执行用时:32 ms,击败了 95.24% 的 Python3 提交
  • 内存消耗:15.1 MB,击败了 92.86% 的 Python3 提交

C++ 执行结果

  • 执行用时:0 ms,击败了 100.00% 的 C++ 提交
  • 内存消耗:6.5 MB,击败了 94.74% 的 C++ 提交

代码亮点

  1. 剪枝优化:通过检查 IP 段的有效性,我们可以提前剪枝,避免无效的递归。
  2. 边界条件处理:代码中详细处理了各种边界情况,如前导零和数值范围。
  3. 回溯实现清晰:回溯函数的实现清晰明了,易于理解和维护。
  4. 字符串操作高效:使用了高效的字符串操作方法,避免了不必要的字符串拼接。
  5. 参数传递优化:在 C++ 实现中,使用了常量引用来传递字符串参数,提高了效率。

常见错误分析

  1. 忽略前导零:IP 段不能有前导零,除非这个段就是 0。例如,"01" 不是有效的 IP 段。
  2. 数值范围检查:IP 段必须在 0 到 255 之间,超出范围的数值是无效的。
  3. 段数不足或过多:有效的 IP 地址必须恰好有四个段,不能多也不能少。
  4. 字符串解析错误:在将字符串转换为整数时,需要确保正确处理,避免溢出或格式错误。
  5. 回溯终止条件:需要正确设置回溯的终止条件,避免无限递归或漏解。

解法比较

解法 时间复杂度 空间复杂度 优点 缺点
回溯法 O(3^4) = O(81) O(4) 实现简单,可以找到所有解 在最坏情况下可能需要尝试很多无效的分割
迭代法 O(3^4) = O(81) O(1) 避免了递归调用的开销 实现复杂,不如回溯法直观

注意:时间复杂度是 O(3^4),因为我们最多需要在字符串中插入 3 个点,每个点有 3 种可能的位置(因为 IP 段最多 3 位数字),所以总共有 3^4 = 81 种可能的分割方式。但实际上,由于剪枝和 IP 段的有效性检查,实际的时间复杂度会小于这个理论上限。

相关题目

相关推荐
UFIT几秒前
NoSQL之redis哨兵
java·前端·算法
喜欢吃燃面1 分钟前
C++刷题:日期模拟(1)
c++·学习·算法
SHERlocked935 分钟前
CPP 从 0 到 1 完成一个支持 future/promise 的 Windows 异步串口通信库
c++·算法·promise
ai大师8 分钟前
(附代码及图示)Multi-Query 多查询策略详解
python·langchain·中转api·apikey·中转apikey·免费apikey·claude4
怀旧,10 分钟前
【数据结构】6. 时间与空间复杂度
java·数据结构·算法
积极向上的向日葵23 分钟前
有效的括号题解
数据结构·算法·
GIS小天29 分钟前
AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年6月7日第101弹
人工智能·算法·机器学习·彩票
小小爬虾29 分钟前
关于datetime获取时间的问题
python
_Itachi__1 小时前
LeetCode 热题 100 74. 搜索二维矩阵
算法·leetcode·矩阵
不忘不弃1 小时前
计算矩阵A和B的乘积
线性代数·算法·矩阵