2026/9/15 9:07:13

LeetCode 1317无零整数拆分:暴力枚举与边界处理的经典入门题

LeetCode 1317无零整数拆分:暴力枚举与边界处理的经典入门题 刷题群里有朋友在问 LeetCode 1317 这道题说思路一看就懂但自己写出来总是这里卡一下、那里错一下尤其是判断“无零整数”时边界一多就开始绕。这道题本身确实不难定位也就是入门级的“简单题”但它在实际刷题过程中很有代表性你说它考算法吧真没考什么高深东西你说它纯送分吧里面涉及的数字拆解、边界判断、代码风格这些细节又恰好是很多人写代码不扎实的缩影。如果你正在刷 LeetCode 热门 100 题或者刚开始按题号顺序刷题这道题很适合拿来练手。它能帮你把“将一个整数拆成两个数”这类问题的套路固定下来同时让你体会一下为什么有些题明明思路秒懂代码却总是修修补补。下面我把这道题从题意拆解、思路选型、多语言实现到耗时实测完整过一遍最后附上我实际踩过的一些坑和排查经验。1. 题目拆解到底在求什么1.1 题意与关键定义题目的英文名是 Convert Integer to the Sum of Two No-Zero Integers核心要求是给定一个整数 n返回一个长度为 2 的数组 [a, b]使得 a b n并且 a 和 b 的十进制表示中都不能包含数字 0。这里的“No-Zero Integer”是一个自造概念LeetCode 官方给出的定义是一个正整数其十进制表示中不包含任何 0 这个数字。举例来说1、2、9、11、12、21、99 都是无零整数而 10、20、101、1020 这些包含了数字 0就不是无零整数。题目的输入约束是 n 2这其实是一个很重要的信息。因为如果 n 1那么唯一能拆成的两个正整数组合是 1 0但 0 本身含 0不符合要求所以无解。题目直接从 n 2 开始等于提前帮你把“无解”的情况排除掉了这也算是 LeetCode 出题时的一种惯例先把边界堵死免得解题的人还要额外处理一堆异常分支。1.2 为什么一定有解很多人第一次看到这道题会有一个疑问是不是每个 n 都能拆成两个无零整数万一拆不出来怎么办结论是可以拆而且至少在题目给定的数据范围内n 最大到 10^4一定可以拆。最简单的构造思路是从 a 1 开始遍历检查 a 和 n - a 是否都不含 0找到一个满足条件的就返回。如果 n 最大是 10^4那么最坏情况下也就遍历 10^4 次每次判断数字是否含 0 最多看 5 位数总共 5 万次操作在 OJ 的评测环境里属于“瞬间完成”的量级。理论上还可以构造一个更强的结论任意 n 2都能找到一组解。因为如果 n - 1 不含 0那么 1 (n - 1) 就是一组解如果 n - 1 含 0那就尝试其他拆分。这里不展开严谨证明但你只需要知道这道题不存在“遍历完了找不到解”的情况所以暴力遍历在逻辑上是安全的。2. 思路选型暴力遍历为什么是第一选择2.1 从生活化例子理解拆分逻辑你可以把这道题理解成你有 n 块钱要分给两个人每个人分到的钱数在十进制写法里都不能出现数字 0。比如 n 101你不能直接给第一个人 1 块、第二个人 100 块因为 100 里有 0但你可以给第一个人 2 块、第二个人 99 块2 和 99 都是无零整数加起来正好 101。这个例子也顺带说明了为什么这道题不能只做一个简单的“n / 2”之类的划分你没法保证 n/2 不含 0更没法保证 n - n/2 不含 0。最稳妥的做法就是从一个方向开始逐个尝试这个思路在算法里叫枚举。2.2 暴力 vs 数学构造的取舍既然要拆分那就有两个方向一个是用数学方法直接构造出一组解另一个是从 1 到 n - 1 逐个枚举。数学构造的思路确实存在。比如可以先把 n 的各位数字过一遍遇到 0 的那一位就从相邻的高位借 1然后把这一位变成 1 或 9 之类的非零数字构造出一个接近 n 的无零整数 a再验证 n - a 是否无零不满足就继续调整。这种思路理论上可行但实现起来繁得很借位、进位、位数变化各种边界能写到你怀疑人生。反而暴力枚举是更适合这道题的做法。原因主要有三个n 范围只有 10^4暴力枚举的耗时上限极低不会超时判断“是否含 0”的逻辑简单清晰几乎不可能写错返回任意一组有效解即可不需要追求唯一解或最优解所以枚举到第一个合法组合就能直接 return。刷题经验里有一条很重要的原则先评估数据范围再决定算法的复杂程度。如果 n 最大是 10^4那么 O(n) 的遍历就已经足够了如果 n 最大是 10^9那才需要考虑数学构造或者更聪明的枚举方法。很多初学者一上来就想着“怎么优化”“有没有 O(1) 的数学公式”其实在这个题目规模下完全没必要。2.3 遍历起点为什么从 1 开始枚举起点选 1 而不是 0是因为“无零整数”的定义限定了是正整数而 0 这个数字本身带了 0不符合要求。另一个原因是如果从 0 开始你得额外跳过 0 这个特例完全没有必要。那为什么不从 2 开始或者从 n/2 开始呢从 2 开始当然也可以因为题目保证一定有解所以从任意位置开始都可能找到答案但起点越靠近 1越容易匹配到一些以 1 开头的组合比如 n 11 时1 10 不行10 含 02 9 可以。这其实没什么规律可循纯看数字结构。所以统一从 1 开始是逻辑上最干净的选择。3. 核心实现多语言代码与关键细节3.1 一个简洁的 Java 实现以 Java 为例整体代码可以写成这样class Solution { public int[] getNoZeroIntegers(int n) { for (int a 1; a n; a) { int b n - a; if (containsNoZero(a) containsNoZero(b)) { return new int[]{a, b}; } } return new int[]{}; // 理论到不了这里但编译器要求有返回 } private boolean containsNoZero(int x) { while (x 0) { if (x % 10 0) { return false; } x / 10; } return true; } }这个实现的核心思路就两步枚举 a同时算出 b分别判断 a 和 b 是否含 0。只要两个都不含 0立即返回。判断函数 containsNoZero 用的是“逐位取余 除以 10”的经典做法。x % 10 拿到当前最低位如果等于 0 就直接判否否则 x / 10把最低位去掉继续检查下一位。循环结束条件是 x 0说明所有位都检查完了且没遇到 0返回 true。这里有个小细节如果 x 本身等于 0while 循环一次都不会执行直接返回 true。但因为我们传入的参数是 a 和 b而 a 1b 1所以实际上不可能出现 x 0 的情况这个隐患被题目的枚举范围天然规避了。3.2 判断函数设计取余还是转字符串在实现“是否含 0”的判断时有两条路线一条是上面这种纯数字的取余判断另一条是先把整数转成字符串再用字符串的 contains 方法检查是否包含 0。比如 Python 可以这样写class Solution: def getNoZeroIntegers(self, n: int) - List[int]: for a in range(1, n): b n - a if 0 not in str(a) and 0 not in str(b): return [a, b]这行代码非常短因为在 Python 里0 not in str(a)天然就是一个完整的判断逻辑。C 里也可以用 to_string 转成字符串后找 find(0)。那到底用哪种方式好我的建议是刷题阶段看你想练习什么。如果目标是尽快 AC字符串转换更直观、更不容易写错如果目标是训练数字敏感度取余判断更底层能帮你建立“整数是由位组成的”这种直觉。从性能看取余判断略微快一点因为避免了字符串对象的创建和字符扫描过程。但这道题的 n 范围只有 10^4两种方式的耗时差异完全在误差范围内不需要纠结。3.3 面试/笔试中的扩展问法这道题在面试场景里出现的概率不高但它身上能延伸出一些常见变体理解了原题后可以顺手想想如果把“返回任意一组”改成“返回 a b 且 a、b 都是无零整数要求 |a - b| 最小”那暴力枚举就需要改成逼近 n/2 的双向扩散搜索或者对每个候选解计算差值复杂度变成 O(n)但代码会复杂不少如果改成“返回所有可能的拆分方案”那就需要把 return 改成收集到一个 List 里直到循环结束再返回如果 n 扩大到 10^9暴力枚举就不可行了需要引入数学构造法或者随机化尝试随机生成一个无零整数 a检查 n - a 是否无零。这些变体我不展开写代码但你可以自己动手练习一下。这也是刷题价值所在一道题吃透后它的变体就是你能力边界最好的探测工具。4. 耗时实测与性能观察4.1 复杂度精算这道题的复杂度分析并不难但很多人会忽略掉判断函数那部分的开销。假设 n 有 d 位数字那么枚举 a 的范围是从 1 到 n - 1一共 O(n) 次。每次枚举中containsNoZero(a) 需要扫描 a 的每一位containsNoZero(b) 需要扫描 b 的每一位这两个操作的耗时都在 O(d) 级别。所以总时间复杂度是 O(n * d)。在 n 10^4 的前提下n 最多 5 位数所以最坏情况是 10^4 * 5 5 万次基础操作。这个量级对现代 CPU 来说几乎是瞬间完成Java 环境下的耗时通常在 0ms 到 1ms 之间。空间复杂度是 O(1)只用了常数个变量。4.2 “耗时 100ms”是怎么来的有些朋友看到题目标题里写着“耗时 100”可能以为这道题在某些评测记录里耗时 100ms。实际上LeetCode 的耗时统计受很多因素影响服务器负载、编程语言、测试用例的随机顺序、是否开启 JIT 预热、系统环境差异等。同一个代码多次提交耗时可能在 0ms 到 十几ms 之间波动甚至有时候同一个答案过几天提交一次显示的时间都不一样。如果看到某个提交记录显示 100ms 左右那大概率不是这道题本身需要 100ms而是该次提交所在时间段评测服务器压力偏大或者某个用例的输入刚好命中了 n 比较大的情况。总之不必把单次耗时数字看得太重更不用为了“减少 1ms”去过度优化。真正值得关注的是算法复杂度是否符合题目的数据范围约束。4.3 有没有必要继续优化对这道题来说O(n * d) 已经足够快没有继续优化的必要。如果强行构造 O(d) 的数学解反而会引入一堆难维护的边界逻辑属于“为了优化而优化”在面试和实际工作中都不是好习惯。类似的优化取舍在 LeetCode 热门 100 题里很常见。比如“爱吃香蕉的狒狒”那道题如果你不知道二分法直接线性枚举每小时吃掉的香蕉数 k在极端数据下会超时而这道题的数据范围决定了线性枚举完全够用。所以关键不是“能不能优化”而是“有没有必要优化”。5. 常见问题、避坑指南与刷题建议5.1 高频错误逐项排查我在实际写这道题时最初也踩过几个坑这里整理成一张问题速查表症状原因解决办法输入 n2 时返回空数组或报错循环从 1 开始但containsNoZero判断逻辑写错把 1 判断成了含 0用 x % 10 判断每一位不要用 x % 10 0 作为跳出条件时漏掉最后一位返回结果顺序和预期不符返回了 [b, a] 而不是 [a, b]确认 return 的数组顺序和题目标注的 a b 一致某个用例始终找不到解枚举范围写成了a n导致 b 0 时被当作候选枚举范围应该是a n保证 a b n 且 b 1判断函数对 x0 返回 truewhile 循环没进直接 return true如果需要判断 0应该在函数开头加if (x 0) return false;第 3 个问题容易阴人。因为题目保证 n 2所以 a 最大到 n - 1 就够但如果有人惯性写成a n当 a n 时 b 0然后 containsNoZero(0) 在某些实现里会返回 true导致返回 [n, 0]这就不满足“无零整数”的定义了。5.2 把一道简单题玩出深度这道题虽然简单但可以顺着它做不少延伸练习改成输出所有方案把 return 改成 Listint[]每次找到合法组合就 add最后统一返回。改成求最小差值方案从 n / 2 开始向两边扩散找到第一组合法组合就是差值最小的。限制 a 和 b 都必须是质数难度直接上升一个等级变成“验证哥德巴赫猜想”的简化版。把 n 扩大到大数范围比如 n 有 100 位那纯数字取余就不行了得考虑用字符串处理这能顺带练一下大数思想。这些扩展不是让你全部做完而是提供一个思路每道题做完后试着改变一两个条件看看自己的解法还能不能应对。这个习惯比单纯刷题量重要得多。5.3 新手刷题的整体建议结合这道题我给刚开始刷题的朋友几条实用建议先看数据范围再想算法。数据范围是 (10^4)O(n) 就是最优数据范围是 (10^9)O(n) 就要慎重考虑 O(log n) 或 O(1)。不要追求一次写出“最优解”。第一版暴力可以过就先把暴力写下然后再想优化。很多时候写完了暴力你会自然发现哪里能优化。判断类的小函数单独抽出来写如这里判断无零整数。不要把一堆逻辑塞在主函数里否则后期调试会很痛苦。善用题目自带的样例和边界值。这道题的边界就是 n2返回值必须是 [1, 1]再有就是 n10、n100 这类带 0 的值能帮你快速暴露判断函数的问题。对了还有一个小技巧如果你担心 containsNoZero 写错可以在本地写一个简单的测试循环把 1 到 10000 之间所有“无零整数”都列出来和人工核对几个值确认无误后再提交。这样能省下不少提交试错的机会成本。5.4 刷题节奏与代码习惯很多人刷 LeetCode 热门 100 题刚开始热情很高一天刷五道到后面越来越吃力最后就不了了之。问题往往出在“重数量轻质量”上。以这道 1317 为例它即使简单也值得你在 AC 之后多花几分钟做两件事一是翻翻讨论区别人的解法看有没有你没想到的思路二是关掉代码重新写一遍看能不能一次通过。我自己的习惯是每道题 AC 之后会在注释里记录当时卡住的地方和最终的解法思路隔一个星期再回来重写一次。这套方法对巩固基础特别有效尤其是像“判断整数是否含 0”这种基础操作写多了之后在脑子里几乎能形成肌肉记忆。这种对细节点滴积累的感觉才是刷题真正能带给你的东西。LeetCode 1317 就是这样一个很好的起点它不难但它像一个枢纽站往左走是数字拆解与暴力枚举往右走是字符串处理和大数计算往上走是数学构造往下走是复杂度分析。把这一站的每一条岔路口都摸透了后面遇到类似问题你会比那些只背答案的人从容得多。