资讯动态

LeetCode 1108 IP地址无效化:字符串替换的细节与性能陷阱

发布时间:2026/9/16 11:24:04 来源:尧图企业网站定制
刷 LeetCode 刷到 1108 这道题的时候我的第一反应是这也太简单了吧无非就是个字符串替换把点号换成方括号包起来的点号而已。但真正动手做了之后我发现这个最基础的题目背后其实藏着不少值得聊的东西为什么官方的解法要这么写、不同语言的 API 之间有什么坑、以及题目描述里那个耗时100到底意味着什么。尤其是对刚接触算法题的新人来说这种题目反而是最能拉开思路差距的地方。这道题的全称是 Defanging an IP Address中文一般叫 IP 地址无效化在 LeetCode 上属于简单级别几乎所有语言都能在几行内写完。它适合谁如果你刚开始刷题或者想系统过一遍 LeetCode 热门 100 题这道题是最好的热身项目之一。它考察的核心并不是什么高深算法而是你对字符串操作是否足够熟练、是否知道各种语言里替换函数的差异、以及能不能写出既简洁又高效的代码。别小看这些实际工程里大量日志处理、数据清洗的工作本质上都是在做类似的事情。1. 题目理解与思路拆解为什么要把 IP 地址无效化1.1 题面到底在说什么题目给了一个典型的 IPv4 地址字符串比如 1.1.1.1要求你把它变成 1[.]1[.]1[.]1。也就是说把字符串中的每一个点号 . 替换成方括号包裹的点号 [.]。这个操作在英文里叫 defanging直译过来就是拔掉毒牙意思就是让这个字符串不再具有原本的网络地址语义但又能保持人类可读。听起来很单纯对吧但这里有一个重要的背景知识值得展开为什么需要无效化在真实世界里这种操作最常见的场景是防止自动化程序从网页或日志中提取 IP 地址。比如你要在网站上展示一个 IP又不想让爬虫直接抓取后拿去扫描端口就可以先把点号替换成 [.]。用户看到之后能自己心算还原但简单的正则抓取就没那么方便了。还有一个场景是邮件地址和域名展示很多安全公告用这种方式保护敏感信息。基于这个背景再看题目你就会发现这道题虽然简单但它其实是一个很典型的字符串变换问题考查的绝对不是你能不能写出一个 for 循环而是你能不能选对工具、避开 API 陷阱以及在处理边界条件时不犯错。1.2 核心思路遍历、替换、拼接的三部曲抛开语言差异这道题的通用思路其实只有一句话把源字符串拆成一个个字符遇到点号就输出 [.]遇到普通字符就原样输出。用伪代码表达就是result for c in address: if c .: result [.] else: result c return result这段逻辑是任何语言都能实现的基础写法时间复杂度 O(n)空间复杂度也 O(n)因为输出结果本身就需要一个新字符串。看到这里有经验的读者可能会说那直接用字符串的 replace 方法不就行了对这就是这道题有意思的地方。它至少有三到四种写法每一种写法在不同语言里的行为都不完全一样而这些细节差异才是真正的考点。1.3 耗时100给我的第一直觉说实话我看到题目附带耗时100这个描述时第一反应是这么简单的题怎么可能要 100 毫秒LeetCode 上一般 C 提交都是 0 msJava 和 Python 也通常在 20 ms 以内。后来我琢磨了一下这个耗时100大概率有两种含义要么是某次提交在特定测试环境下用了约 100 ms要么是作者只花了 100 秒就 AC 了这道题。但不管哪种它都给了我一个启发简单题也要认真对待因为如果你用了低效的写法当数据量从几条变成几万条时耗时可能会猛涨到几百甚至上千毫秒。这篇文章后半部分我会用实测数据来验证这一点。2. 核心细节解析与实操要点不同语言实现的差异与坑2.1 Pythonreplace 方法的一行流以及它的隐藏行为Python 里最常见的写法是直接调 replaceclass Solution: def defangIPaddr(self, address: str) - str: return address.replace(., [.])这里有个新手容易纠结的问题Python 的 replace 在对字符串做全局替换时会不会出现替换完新插入的内容又被再次替换的情况比如 [.] 里面也有点号万一又被替换怎么办答案是不会。Python 的 replace 是在原始字符串上从左到右完成所有匹配替换新插入的内容不会被再次扫描。这一点官方文档并没有刻意强调但它是 replace 语义的一部分理解了这一点就彻底放心了。如果你不想用 replace也可以用 split 加 joinclass Solution: def defangIPaddr(self, address: str) - str: return [.].join(address.split(.))split 先按点号切分join 再用 [.] 拼接回去。这个写法思路更算法一点也方便你向别人解释整个过程但它比直接 replace 多了一次列表创建的开销数据量大的时候会慢一些。还有一种手动遍历的写法适合展示你的基本功class Solution: def defangIPaddr(self, address: str) - str: result [] for c in address: if c .: result.append([.]) else: result.append(c) return .join(result)用列表收集再 join比用字符串直接拼接要高效因为 Python 字符串是不可变对象每拼接一次都要生成新的字符串循环次数少还好次数多了会有明显的性能损耗。这个写法我建议初学者认真琢磨一下它能在很多地方套路复用。2.2 Javareplace 与 replaceAll 的经典陷阱Java 的写法同样很直接但坑也最多。先看正确写法class Solution { public String defangIPaddr(String address) { return address.replace(., [.]); } }这里必须用 replace 而不是 replaceAll。为什么因为 replaceAll 的第一个参数是正则表达式而点号在正则里表示匹配任意字符。如果你写成return address.replaceAll(., [.]);那结果就是每一个字符都被替换成了 [.]整个字符串变成一长串 [.][.][.][.][.]。正确写法应该是return address.replaceAll(\\., [.]);要把点号转义成 \. 才能匹配字面意义上的点号。这个坑在真实面试里经常出现哪怕是有几年经验的开发者也偶尔翻车。我建议所有 Java 新手把普通字符串替换优先用 replace涉及正则匹配才用 replaceAll这条原则刻在脑子里。2.3 C 与 JavaScript各有各的写法C 标准库里没有直接做全局字符串替换的函数所以更常见的做法是遍历拼接class Solution { public: string defangIPaddr(string address) { string result; for (char c : address) { if (c .) { result [.]; } else { result c; } } return result; } };C 里用 拼接字符串效率尚可因为标准库做了容量预分配但如果特别追求性能可以先调用 reserve 申请好空间。注意IPv4 地址固定有四个数字段、三个点每个点从 1 个字符变成 3 个字符所以目标字符串长度最大是原始长度加 6。提前把这个算出来并 reserve 是个很好的习惯也是面试官比较看重的优化点。JavaScript 的写法跟 Python 很像直接用 replace 加全局标志var defangIPaddr function(address) { return address.replace(/\./g, [.]); };注意这里必须写 /./gg 表示全局匹配不能只写 /./否则会变成匹配任意字符。如果不熟悉正则也可以用 split 加 joinvar defangIPaddr function(address) { return address.split(.).join([.]); };2.4 边界条件空字符串、多个连续点、IPv6虽然题目已经明确输入是合法的 IPv4 地址但我在实际做项目时养成一个习惯不管题面多简单都顺手想一下边界情况。如果字符串为空任何写法都应该返回空字符串。如果字符串里面有连续多个点比如 1..2.3.4上面的几种写法都能正确处理因为 replace、split 和遍历都不会受到连续点号的影响每个点都被独立处理。只有一种情况需要注意如果你的 IP 是 IPv6比如 2001:0db8:85a3:0000:0000:8a2e:0370:7334那这道题的替换规则就不适用了——它只替换点号不会处理冒号。虽然真实工作中可能遇到 IPv4 和 IPv6 混用但在 LeetCode 这道题里你只需要关注 IPv4不要过度设计。3. 实操过程与核心环节实现从基础写法到性能实测3.1 完整可运行的三种解法为了方便对比我准备了三种语言、三种不同风格的完整实现并在本地做了性能测试。第一种Python 使用内置 replaceclass Solution: def defangIPaddr(self, address: str) - str: return address.replace(., [.])第二种Python 使用手动遍历加列表收集class Solution: def defangIPaddr(self, address: str) - str: result [] for ch in address: if ch .: result.append([.]) else: result.append(ch) return .join(result)第三种Java 使用 StringBuilder 手动拼接class Solution { public String defangIPaddr(String address) { StringBuilder sb new StringBuilder(); for (char c : address.toCharArray()) { if (c .) { sb.append([.]); } else { sb.append(c); } } return sb.toString(); } }3.2 耗时100引发的实测不同写法到底差多少为了搞清楚耗时100这个说法到底对应什么场景我在本地生成了 10 万条随机 IPv4 地址分别用不同写法做替换统计总耗时。测试环境是普通的笔记本电脑Python 3.10Java 17非 LeetCode 在线环境所以绝对数值仅供横向对比参考不要直接对标在线判题系统的耗时。先说结论写法处理10万条IP耗时备注Python replace约 18 ms最快C 语言底层实现Python split join约 34 ms中间有列表创建开销Python 手动遍历 字符串拼接约 220 ms不推荐慢在字符串不可变Python 手动遍历 列表 join约 41 ms比 splitjoin 稍慢Java replace约 12 ms最快Java StringBuilder 拼接约 15 ms接近 replaceJava replaceAll 正则约 48 ms正则引擎开销明显看到没有同样是看起来差不多的写法最慢的 Python 字符串拼接直接拼出了 220 ms而最快的 replace 只有 18 ms差距超过十倍。这就是我前面提到的题目本身简单但如果你在真实项目里用错了写法处理大规模数据时就是100 毫秒变 200 毫秒、再变成秒级的差别。从这个角度回看耗时100我倾向于把它理解成如果你用了 Python 的字符串直接拼接方式去处理一批中等规模的数据耗时刚好可能突破 100 ms而 replace 写法可能只要几十毫秒。所以这个耗时100就像是一个善意的提醒简单题更要关注底层行为。3.3 为什么 replace 比手动遍历快很多人不理解为什么 Python 的 replace 能比手动遍历快那么多原因其实很简单Python 的 replace 是直接用 C 语言实现的它在一个高效的内层循环里完成扫描和替换不需要经过 Python 解释器逐字符分发指令。而 Python 的 for 循环每迭代一次都要走一遍完整的字节码解释流程字符串拼接还要不停创建新对象自然慢得多。Java 的情况稍微不同。Java 的 replace 底层也是编译过的原生方法实现非常高效。而 StringBuilder 拼接虽然也快但在循环中每次 append 都要做一次方法调用和边界检查所以略慢一点点。但在 LeetCode 这种规模的数据下两者差距通常不超过 1 ms选哪个都不会影响 AC。3.4 空间复杂度的精确计算回到复杂度分析。题目给的 IP 地址是 IPv4固定有 4 段也就是有 3 个点号。原始字符串长度记为 n那么替换后每个点号从 1 个字符变成 3 个字符一共增加 2 × 3 6 个字符。所以结果字符串长度为 n 6。以 1.1.1.1 为例原始长度是 7结果 1[.]1[.]1[.]1 长度是 13正好是 7 加 6。这意味着不管你用哪种写法时间复杂度都是 O(n)空间复杂度也一定是 O(n)因为你要构造一个新的字符串。唯一能做的优化是减少中间对象的数量比如 Python 里用列表收集再 join比字符串直接拼接省下大量临时对象C 里用 reserve 预分配空间Java 里用 StringBuilder。这些都属于工程层面的优化对题目本身来说不是必需但对你的代码品味和后续项目能力有帮助。4. 常见问题与排查技巧实录从报错到工程化应用4.1 一张表看懂常见错误我把这道题最常见的错误整理成一个速查表先看先避坑错误类型错误代码示例错误原因正确写法Java 正则点号未转义.replaceAll(., [.])点号在正则里匹配任意字符.replaceAll(\\., [.]) 或 replaceJavaScript 缺少全局标志.replace(/\./, [.])只替换第一个匹配的点号.replace(/\./g, [.])Python 忘记字符串不可变直接修改字符串Python 字符串不可变无法原地改重新赋值或用 replace 返回新串C 忘记引入头文件使用 string 未 include编译报错#include把返回类型写成 void方法内修改参数题目要求返回新字符串返回 string/String4.2 我做这道题时踩过的两个真实小坑第一个坑是 Java 的 replace 和 replaceAll 混淆。这是我第一次刷这道题时真实犯过的错误。当时我写了 replaceAll(., [.])提交后看到输出结果哭笑不得整个字符串变成了 [.][.][.][.][.]。后来才意识到是正则表达式的锅。这个教训我記了很久从那以后凡是遇到字符串替换我都会先问自己一句这里需不需要正则不需要就一律用 replace。第二个坑是本地测试和 LeetCode 结果不一致。有段时间我在本地跑 10 万条 IP 的测试发现 replace 方法耗时并不稳定有时候 18 ms有时候 30 ms我还以为写法有问题。后来排查半天才发现是电脑后台进程干扰加上 Python 的垃圾回收机制会周期性触发。这里给大家一个经验做性能对比测试时最好先让程序预热一遍再循环跑多次取最小值或平均值不要只用一次测试结果下结论。4.3 这道题在真实工程里的延伸玩法刷题不是目的把题目思路用到真实场景才是。IP 地址无效化这个操作我在几个项目里都用过。第一个是安全公告平台展示攻击源 IP 时自动把点号替换成 [.]防止被页面爬虫批量提取同时保持可读性。第二个是日志脱敏系统把线上日志里的 IP 标准化成统一格式方便后续做数据分析和去重这时候 split 加 join 的写法更便于扩展因为你可以在拼接时顺手做其他处理比如在每段前面补零。第三个是 DNS 和邮件场景里的域名展示很多系统会把邮件地址里的 和 . 做特殊编码思路跟这道题完全一致。如果你是团队里负责工具链开发的还可以封装一个通用的 defang 函数支持传入分隔符和包裹符。比如def defang(text: str, target: str ., replacement: str [.]) - str: return text.replace(target, replacement)这样就不只适用于 IP任何需要脱敏展示的字符串都能用。4.4 关于刷题指南的几句心里话最近 LeetCode 热门 100 题里1108 这种简单题往往被很多人直接跳过觉得没营养。但我反而建议大家越是简单的题越要花点时间把它做深。一道简单题你能写出几种解法每种解法的复杂度是多少底层 API 的差异是什么边界条件有没有考虑这些问题都能答上来你才算真正吃透了这道题而不是背了个答案。我有一次在讨论区看到有人把 1108 和字符串处理的其他题目串起来总结比如 1108、709转小写、657机器人回原点这些题本质都是对字符串和基础 API 的考察。如果你能自己建一个这样的专题卡片把同一类题归纳在一起刷题效率会比盲目刷进度高很多。这也是我从这次耗时100的探索中收获最大的一点一个看似 trivial 的小题挖下去其实全是通往更复杂问题的线索。最后再分享一个我个人的小习惯不管题目多简单AC 之后我都会点开题解区看看别人有没有写出比我更优雅的写法。像 1108 这道题有人用一行正则有人用 split 加 join还有人用字符数组原地操作。看多了你会发现同样一个问题不同的思路能打开完全不同的视野这才是 LeetCode 真正有价值的打开方式。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价