93. 复原 IP 地址(Restore IP Addresses)

难度:中等 | 主题:字符串、回溯

题目

给定一个只包含数字的字符串 s,返回所有可能的有效 IP 地址。有效 IP 地址由 4 个整数(0~255)组成,用 . 分隔,且不能有前导零(除非是 “0” 本身)。

示例

 
输入: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"]
 

思路

先讲个故事:切蛋糕的四刀

想象你有一条长蛋糕(字符串),要切成恰好 4 段(IP 地址的 4 个部分)。每段长度 13 厘米,每段的值在 0255 之间,而且不能有前导零(比如 “01” 就是非法的)。

你怎么切?从左到右,每刀试切 1、2、3 厘米,切满 4 刀且刚好用完蛋糕就成功了。切不下去就回退,换个长度再试。

这就是回溯——试错 + 回退。

引导式推导:回溯三要素

要素 1:状态(当前切到哪了)

  • start:当前从字符串的哪个位置开始切

  • path:已经切出的段,如 ["255", "255"]

  • seg_count:已切出几段(0~4)

要素 2:选择(每刀切多长)

每刀尝试切 1、2、3 个字符:

 
s = "25525511135", start=0
 
  切1位: "2" ✓ → 递归
 
  切2位: "25" ✓ → 递归
 
  切3位: "255" ✓ → 递归
 

要素 3:剪枝(提前放弃)

  • 剩余字符太多:即使每段都取 3 位也放不下 → 剪掉

  • 剩余字符太少:即使每段都取 1 位也不够分 → 剪掉


graph TD

    A["start=0, path=[]"] --> B["切 '2'"]

    A --> C["切 '25'"]

    A --> D["切 '255'"]

    B --> E["继续切..."]

    C --> E

    D --> E

    E --> F{"seg_count==4<br/>且 start==n?"}

    F -->|"是"| G["加入结果"]

    F -->|"否"| H["回退,换长度"]

为什么复杂度是 O(1)?

IP 地址固定 4 段,每段最多 3 位,总组合数不超过 3^4 = 81,常数级。


代码

 
def restoreIpAddresses(self, s: str) -> list[str]:
 
    res = []
 
    n = len(s)
 
    def backtrack(start: int, path: list[str], seg_count: int):
 
        # 终止条件:切满了 4 段
 
        if seg_count == 4:
 
            if start == n:
 
                res.append('.'.join(path))
 
            return
 
        # 剪枝:剩余字符太多,即使每段 3 位也放不下
 
        if (4 - seg_count) * 3 < n - start:
 
            return
 
        # 剪枝:剩余字符太少,即使每段 1 位也不够分
 
        if 4 - seg_count > n - start:
 
            return
 
        # 尝试切 1~3 个字符作为当前段
 
        for length in range(1, 4):
 
            if start + length > n:
 
                break
 
            segment = s[start:start + length]
 
            # 合法性检查:前导零 或 超出范围
 
            if (segment[0] == '0' and len(segment) > 1) or int(segment) > 255:
 
                continue
 
            backtrack(start + length, path + [segment], seg_count + 1)
 
    backtrack(0, [], 0)
 
    return res
 

复杂度

指标解释
时间O(1)最多 3^4 = 81 种组合,常数级
空间O(1)递归栈深度最多 4 层,结果列表也是常数大小

实战考量

频率分析

出现在:回溯经典题,约 30% 的同类题会考。关键在于你回溯框架的熟练度,以及能不能想到剪枝优化。

延伸思考

Q:为什么时间复杂度是 O(1)?

A:IP 地址固定 4 段,每段 1~3 位,组合数上限 3^4 = 81,是常数。即使字符串长度 1000 位,组合数也不变(因为大部分会被剪掉)。

Q:前导零检查怎么写?

A:标准写法 segment[0] == '0' and len(segment) > 1。不能写成 segment[0] == '0' and segment != '0'(虽然等价),也不能写成 int(segment[0]) == 0(多此一举)。

Q:剪枝条件怎么推导?

A:剩余段数 remaining = 4 - seg_count,剩余字符数 remaining_chars = n - start。如果 remaining * 3 < remaining_chars(最多装不下),或 remaining > remaining_chars(最少不够分),都可以剪枝。

Q:如果不用回溯,用三重循环?

A:可以,固定 4 段用三重循环定位 3 个切割点,但回溯更通用,代码也更简洁。

易错点

  • 前导零判断只写 segment[0] == '0' 而忘记 len(segment) > 1

  • int(segment) > 255 写成 >= 255(漏了 255)

  • 回溯时忘记 start + length 越界检查

  • path + [segment] 创建新列表,不需要手动回溯(pop)


生活类比

复原 IP → 切蛋糕的四刀

一条长蛋糕(字符串),要切成恰好 4 段。每段 13 厘米,值在 0255 之间,不能有前导零。从左到右试切,切不下去就回退换个长度。

一句话总结:试错 + 回退 = 回溯。


相关题目

题目关系
131分割回文串同样的回溯切割字符串模板
17电话号码的字母组合回溯框架的基础题
165比较版本号字符串分割为多段后处理

→ 返回题单:LeetCode学习路线图 > 二、字符串

速记卡(面试闪卡)

Q1:一句话讲清「93. 复原 IP 地址(Restore IP Addresses)」到底是什么?

A:把纯数字串切回所有合法的 4 段 IP 地址,每段 0~255 且无前导零。

Q2:思路 —— 怎么理解?

A:像把长蛋糕切四刀:每段 13 厘米、值在 0255、无前导零,切不下就回退(Backtracking,回溯)。

Q3:代码 —— 怎么理解?

A:回溯状态 start/path/seg_count,每刀试 1~3 位,剪枝剩余过多或过少(Pruning,剪枝)。

Q4:复杂度 —— 怎么理解?

A:时间 O(1) 最多 3⁴=81 种组合;空间 O(1) 栈深 4 层(Constant Time,常数时间)。

Q5:实战考量 —— 怎么理解?

A:回溯经典题约 30% 会考,考框架熟练度与剪枝(IP Segment,IP 段)。

Q6:核心速记主线有哪些?

  • 目标:4 段、每段 13 位、0255、无前导零

  • 回溯三要素:状态、选择、剪枝

  • 剪枝:剩余太多或太少都提前放弃

  • 前导零判断 segment[0]==‘0’ 且 len>1

口诀

A:复原 IP 切四刀

每段限长三、值二五五

前导零要拦得住

回溯剪枝稳稳包

相关链接