IP 地址无效化 图解题解
这道题到底在问什么
- 输入
- address = "1.1.1.1"
- 输出
- "1[.]1[.]1[.]1"
- 输入
- 本节演示 address = "100.2.30.4"
- 输出
- "100[.]2[.]30[.]4"
先想最直接的笨办法
上面这一排是地址 "100.2.30.4" 拆开的 10 个字符,数字和小数点夹在一起。开扫之前先准备一个空的结果串,等会儿每看一个字符就往里添东西。从最左边第 0 个字符开始,一个一个往右看。(动画第 4 步)
最优解:为什么这么做
一句话答案:LeetCode 1108 IP 地址无效化:把地址字符串里每个小数点 "." 都替换成 "[.]" 三个字符,数字原样照抄——扫一遍逐字拼新串,或直接调内置 replace,时间 O(n)、空间 O(n)。
把 IP 里的点包起来,要返回什么串
给一个合法的 IPv4 地址,也就是形如 a.b.c.d、四段数字用三个点隔开的那种网络地址,要做的只有一件事:把每个小数点 "." 换成 "[.]" 这三个字符,数字和别的内容原封不动,返回换好的新串。题面例子 "1.1.1.1" 变成 "1[.]1[.]1[.]1";本节演示的 "100.2.30.4" 变成 "100[.]2[.]30[.]4"——三个点各鼓成一段方括号,数字一位没变。
就地把点改成方括号,后面下标为什么会乱
最容易踩的一步,是拿着原串从左往右就地替换。麻烦在于 "[.]" 有三个字符、比原来一个点长,把第一个点改完,它右边所有还没处理的字符会被整体往后顶三格,下标全部右移;接着再按老下标去找下一个点,找到的已经不是原来那个位置了。真想在原串上就地改,得反过来从右往左动:已经改过的都堆在右边,左边还没碰的部分下标纹丝不动。
扫一遍另拼一个新串,或一行 replace
更省心的办法是不在原串上动刀:另开一个空结果串,从左到右一个字符一个字符看。看到小数点,就往结果串后面接 "[.]" 三个字符;不是小数点,就把这个字符原样接上去。原串只读不改,新串从空慢慢长出来,下标错乱的问题压根不会发生。
落到实现上更直接——Python 和 Java 都有现成的字符串替换,一行 address.replace(".", "[.]") 就把所有点全换掉、返回新串,连循环都省了。逐字扫描那套,是它内部替你干的活;自己手写多半用来练手,或碰上没有现成替换函数的场合兜底。
"100.2.30.4" 逐字走一遍
结果串从空开始。开头 "1"、"0"、"0" 三个数字都不是点,依次抄进去,结果串成 "100"。第 4 位是小数点,不直接抄、写成 "[.]",结果串变 "100[.]"。第 5 位 "2" 照抄,得 "100[.]2"。第 6 位又是点,补 "[.]",成 "100[.]2[.]"。第 7、8 位 "3"、"0" 抄上,成 "100[.]2[.]30"。第 9 位是第三个点,再补 "[.]",成 "100[.]2[.]30[.]"。最后一位 "4" 照抄,拼出 "100[.]2[.]30[.]4"。原串三个点各换成一段方括号,十个数字字符一个没动,和题面输出对得上。
拿 replaceAll 当正则,点就成了万能符
有个语言层的坑得防:别的符号别跟着换,题目只认小数点 ".",数字和其它字符必须留原样。还有 Java 里 String.replace 收的是字面字符,点就是点、不走正则那套按模式匹配的规则;只有 replaceAll 才按正则解析,那时得先把 "." 转义成 "\.",否则它会当成「匹配任意一个字符」,把整串都换烂。
复杂度上,每个字符只看一遍、决定抄一个还是写三个,时间 O(n),n 是地址长度。空间要另存一个新串,每个点会膨胀成三个字符,新串最长约 3n,仍是 O(n)。地址不管几段、数字几位、末段是不是 0,规则始终一条:是点换成 "[.]",不是点照抄。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢两句话:是小数点就写成 "[.]",不是小数点就原样照抄。下面每一帧都在套这两句。
- 4上面这一排是地址 "100.2.30.4" 拆开的 10 个字符,数字和小数点夹在一起。开扫之前先准备一个空的结果串,等会儿每看一个字符就往里添东西。从最左边第 0 个字符开始,一个一个往右看。
- 5扫到第 0 位,是数字 "1"。它不是小数点,处理起来最简单。
- 6不是小数点,就把 "1" 原封不动接到结果串后面。现在结果串是 "1"。这一位也搞定,往后走。
- 7扫到第 1 位,是数字 "0"。它不是小数点,处理起来最简单。
- 8不是小数点,就把 "0" 原封不动接到结果串后面。现在结果串是 "10"。这一位也搞定,往后走。
- 9扫到第 2 位,是数字 "0"。它不是小数点,处理起来最简单。
- 10不是小数点,就把 "0" 原封不动接到结果串后面。现在结果串是 "100"。这一位也搞定,往后走。
- 11扫到第 3 位,是一个小数点 "."。按规矩,小数点不能直接抄,得换个写法。
- 12是小数点,就往结果串后面写上 "[.]" 这三个字符,代替原来的一个点。写完结果串变成 "100[.]"。这一位处理好了,继续看下一个。
- 13扫到第 4 位,是数字 "2"。它不是小数点,处理起来最简单。
- 14不是小数点,就把 "2" 原封不动接到结果串后面。现在结果串是 "100[.]2"。这一位也搞定,往后走。
- 15扫到第 5 位,是一个小数点 "."。按规矩,小数点不能直接抄,得换个写法。
- 16是小数点,就往结果串后面写上 "[.]" 这三个字符,代替原来的一个点。写完结果串变成 "100[.]2[.]"。这一位处理好了,继续看下一个。
- 17扫到第 6 位,是数字 "3"。它不是小数点,处理起来最简单。
- 18不是小数点,就把 "3" 原封不动接到结果串后面。现在结果串是 "100[.]2[.]3"。这一位也搞定,往后走。
- 19扫到第 7 位,是数字 "0"。它不是小数点,处理起来最简单。
- 20不是小数点,就把 "0" 原封不动接到结果串后面。现在结果串是 "100[.]2[.]30"。这一位也搞定,往后走。
- 21扫到第 8 位,是一个小数点 "."。按规矩,小数点不能直接抄,得换个写法。
- 22是小数点,就往结果串后面写上 "[.]" 这三个字符,代替原来的一个点。写完结果串变成 "100[.]2[.]30[.]"。这一位处理好了,继续看下一个。
- 23扫到第 9 位,是数字 "4"。它不是小数点,处理起来最简单。
- 24不是小数点,就把 "4" 原封不动接到结果串后面。现在结果串是 "100[.]2[.]30[.]4"。这一位也搞定,往后走。
- 2510 个字符全扫完了。原串里的 3 个小数点都换成了 "[.]",数字一个没动,拼出来就是 "100[.]2[.]30[.]4"。可以对一下:原来的 "100.2.30.4" 里三个点的位置,现在各是一段 "[.]",答案 "100[.]2[.]30[.]4" 成立。
⚠️ 容易写错的地方
✗ 错:把别的符号(像冒号)也跟着换了
✓ 对:只替换小数点 "." 这一种字符
题目只要求把 "." 换成 "[.]",数字和其它字符必须原样保留
✗ 错:Java 里担心 replace 把 "." 当成正则通配符
✓ 对:String.replace 收的是字面字符,不走正则,点就是点
只有 replaceAll 才按正则解析,那时才需要把 "." 转义成 "\."
✗ 错:从左往右原地替换,导致后面字符下标错乱
✓ 对:从右往左改,或者另开一个新串来拼
"[.]" 比一个点长,从左原地插入会把后面字符顶到更后面,下标全乱
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def defangIPaddr(self, address: str) -> str:
return address.replace('.', '[.]')C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
string defangIPaddr(string address) {
for (int i = address.size(); i >= 0; --i) {
if (address[i] == '.') {
address.replace(i, 1, "[.]");
}
}
return address;
}
};Java
import java.util.*;
class Solution {
public String defangIPaddr(String address) {
return address.replace(".", "[.]");
}
}复杂度
时间
O(n)
n 为地址长度。每个字符只看一遍,判断是不是小数点再决定抄一个还是写三个,整体是线性扫描
空间
O(n)
要生成一个新字符串放结果。每个小数点会变成 3 个字符,所以新串长度最多约 3n,按峰值占用算是线性
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 IP 地址无效化 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么有的题解要从右往左替换,不能老老实实从左往右吗?+
问题出在 "[.]" 比一个点长两个字符。若在原串上从左往右就地改,改完前面的点,它后面还没处理的字符会被整体往右顶,下标全乱,再按旧下标找下一个点就找错了。从右往左改就绕开了:改过的都在右边,左边还没扫的部分下标不变。当然,只要另开一个新串来拼、或直接用语言内置的 replace,就完全不用操心下标,这也是实战里最常用的写法。
Java 里用 replace 会不会把点当成正则通配符?+
不会。String.replace 接收的是字面字符,点就是那个点,不走正则,直接一行 address.replace(".", "[.]") 即可。真正按正则解析的是 replaceAll,那时 "." 表示「任意一个字符」,会把每个字符都换掉,必须写成 "\\." 转义后才只匹配真正的小数点。这题用 replace 最稳,用不着 replaceAll。
时间和空间复杂度是多少?+
时间 O(n),n 是地址长度,每个字符只看一遍就决定照抄还是写成三个字符。空间 O(n),因为要生成一个新字符串;每个小数点会膨胀成三个字符,新串长度最多约是原串的三倍,量级上仍是线性。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 IP 地址无效化 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。