☰
七进制数题解:短除法、负数边界与Integer.MIN_VALUE详解
2026/10/14 6:43:50 网站建设 项目流程

做算法题的人都知道,进制转换属于最基础的那一类内容。第504题“七进制数”,难度标着简单,但真到提交的时候,挂掉的人并不少。这道题的核心思路确实就是“不断除以7取余数”,可一旦牵扯到负数、0、Integer.MIN_VALUE这种边界值,稍微大意一点,结果就完全不对。今天把这题拆开揉碎了讲一遍,顺便把进制转换这件事彻底说透。不管你是刚开始刷题的新手,还是准备面试想快速过一遍基础算法的从业者,这篇都能给你一点实在的参考。

1. 题目到底想考你什么

1.1 一道“简单题”的隐藏门槛

很多人在评测平台上看到“七进制数”这个标题,第一反应是:这不就是把十进制的整数不断除以7、收集余数、再倒过来嘛。方向没错,但题目的考察点并不只是“会不会短除法”,而是能不能把边界情况处理干净。

这道题的要求很朴素:给定一个整数,返回它的七进制字符串表示。注意输入是 int 类型,范围在 -2147483648 到 2147483647 之间。输入可能是正数,可能是负数,也可能是0。这三种情况,代码路径完全不同。尤其是负数,如果直接套正数版本的逻辑,大概率会在极端输入上翻车。

刷题这么多年,我的体会是:越是标着“简单”的题,越要当成“边界训练”来做。因为它没有太复杂的算法设计,唯一的区分度就在于细节。你能不能在5分钟内写出一个不用库函数、不溢出、不丢负号的版本,是很能反映基本功的。

1.2 核心原理:短除法的数学本质

进制转换的本质,是“按基数展开”的逆过程。

一个十进制数 n,如果要转成七进制,就是找到一串系数 aₖ、aₖ₋₁、……、a₁、a₀,使得: n = aₖ × 7ᵏ + aₖ₋₁ × 7ᵏ⁻¹ + …… + a₁ × 7¹ + a₀

其中每个 aᵢ 的取值范围是 0 到 6。这个式子右边的每一项都“含有7的幂次”,唯独最后一项 a₀ 是独立的。所以,第一步提取 a₀ 的方法,就是对 n 取模 7。取完个位,剩下的是 aₖ × 7ᵏ⁻¹ + …… + a₁,也就是 n / 7 的整数部分。接下来对这个商继续取模7,就能得到 a₁,以此类推。

这就是“短除法”的数学本质:每一次取余,等于把当前最低位从数里剥离出来;每除一次7,数字整体“右移”一位。生活化的类比就是:你在清点一堆乒乓球,每7个装一盒,先数出最后一盒还差几个(取余),然后把装满的盒子再装箱(除以7),逐层往上归纳。

1.3 为什么倒着拼才是正确答案

短除法得到的是从低位到高位的顺序。比如 n=100:

  • 100 ÷ 7 = 14,余2,得到最低位 2
  • 14 ÷ 7 = 2,余0,得到下一位 0
  • 2 ÷ 7 = 0,余2,得到最高位 2

整个过程产生的余数序列是 [2, 0, 2],但正确的七进制数是“202”,必须把序列反转。这个反转操作很多人容易忘,或者在循环里处理不当。

一个小技巧是:在追加符号时,把负号最后追加进 StringBuilder,整体做完后再 reverse。这样正负号的位置就永远是第一位。我们后文会详细讲这个顺序为什么重要。

2. 从零开始写一个能过全部用例的版本

2.1 正数版本的完整实现与逐行说明

先写一个只考虑非负整数的版本,把核心逻辑跑通:

public String convertToBase7(int n) { if (n == 0) { return "0"; } StringBuilder sb = new StringBuilder(); while (n > 0) { sb.append(n % 7); n /= 7; } return sb.reverse().toString(); }

这段代码的逻辑很清晰:0走单独分支,避免进入不了 while 导致返回空串;每次取余、除以7;最后反转。这个版本能正确处理所有正数,也能处理0。

这里有个小细节:sb.append(n % 7)直接追加的是 int 类型,StringBuilder 会自动把它转换成对应的数字字符。不需要手动加 '0'。但如果你要写 C++,就得写成ans.push_back('0' + num % 7),因为在 C++ 里数字和字符是两回事,不转换会乱码。

2.2 负数处理的三个方案对比

处理负数,常见的有三种思路,我这里做个对比。

方案一:先转正数再处理。这是最直观的思路,先判断负号,再用n = -n把 n 变成正数,执行完正数逻辑后在结果前加负号。但问题是,如果 n 是 Integer.MIN_VALUE,-n还是 Integer.MIN_VALUE,因为 int 的对称范围差1,取绝对值会溢出。

方案二:用 long 接住。把 int 赋值给 long 再取绝对值,因为 long 的范围远大于 int,安全。这是我最推荐的做法,代码改动很小,思维负担低。

方案三:直接用负数做短除法。因为负数取模的结果在 Java 里也是负数,比如 -8 % 7 等于 -1,取绝对值后照样能得到正确的余数位。这个写法能绕开溢出问题,但可读性稍差,不太适合面试时快速讲明白。

2.3 采用long类型绕开Math.abs溢出

长期刷题的朋友肯定对 Integer.MIN_VALUE 这个坑印象深刻。它的绝对值是 2147483648,比 Integer.MAX_VALUE 还要大1,直接用 int 存会溢出成负数。

所以,处理负数时最稳妥的写法,就是把 int 提升到 long:

public String convertToBase7(int n) { if (n == 0) { return "0"; } boolean negative = n < 0; long num = n; if (negative) { num = -num; } StringBuilder sb = new StringBuilder(); while (num > 0) { sb.append(num % 7); num /= 7; } if (negative) { sb.append('-'); } return sb.reverse().toString(); }

这个版本的亮点在于:先把 n 赋值给 long 类型的 num,再做取绝对值。无论 n 取到 int 的哪个极端值,num 都不会溢出。后面再用sb.append('-')先把负号塞进缓冲区,最后统一 reverse,负号自然就到了最前面。

我实测过 Integer.MIN_VALUE 的七进制结果是 -104134211162,这个版本能正确输出。

2.4 递归写法看着简洁,坑却不少

网上还有一种很流行的递归写法:

public String convertToBase7(int n) { if (n < 0) { return "-" + convertToBase7(-n); } if (n < 7) { return String.valueOf(n); } return convertToBase7(n / 7) + String.valueOf(n % 7); }

它看起来代码量更少,逻辑也很“优雅”:小于7就直接返回,否则递归计算高位部分再拼接当前位。但问题在于,它对 Integer.MIN_VALUE 依然不安全。-n溢出成负数,递归进去又触发n < 0,然后再次调用convertToBase7(-n),形成死循环。

如果面试时想用递归,必须先在外面做一层 long 转换,或者明确把“输入范围是否包住极端负值”作为前提讲清楚。否则,这个版本在评测平台上是会超时的。

3. 多语言实现与现场验证

3.1 Java版本:面试中最稳妥的选择

综合可读性、安全性和性能,我给出一份可以直接背下来使用的最终版本:

class Solution { public String convertToBase7(int n) { if (n == 0) { return "0"; } boolean negative = n < 0; long num = n; if (negative) { num = -num; } StringBuilder sb = new StringBuilder(); while (num > 0) { sb.append(num % 7); num /= 7; } if (negative) { sb.append('-'); } return sb.reverse().toString(); } }

为什么不直接用String.valueOf(num % 7)来转字符串?因为 StringBuilder.append(int) 底层就已经完成了数字到字符的转换,没必要再绕一道。还有一个隐形优点:整个循环里只创建一个 StringBuilder 对象,内存抖动很小。

3.2 Python、C++与JavaScript的写法差异

Python 版最省心,因为 int 是任意精度的,没有溢出问题,abs 随便用:

class Solution: def convertToBase7(self, n: int) -> str: if n == 0: return "0" sign = '-' if n < 0 else '' n = abs(n) digits = [] while n > 0: digits.append(str(n % 7)) n //= 7 return sign + ''.join(reversed(digits))

C++ 版要特别注意 char 的转换,建议用 long 规避 INT_MIN:

class Solution { public: string convertToBase7(int n) { if (n == 0) return "0"; bool negative = n < 0; long num = n; if (negative) num = -num; string ans; while (num > 0) { ans.push_back('0' + num % 7); num /= 7; } if (negative) ans.push_back('-'); reverse(ans.begin(), ans.end()); return ans; } };

JavaScript 版本要注意的是:Math.abs(n)在 number 范围内不存在 int 溢出问题,因为 JS 的 Number 是双精度浮点,但Math.floor(num / 7)这一步不能省,否则高精度下除法的浮点误差会带来错误:

var convertToBase7 = function(n) { if (n === 0) return "0"; const negative = n < 0; let num = Math.abs(n); const digits = []; while (num > 0) { digits.push(num % 7); num = Math.floor(num / 7); } if (negative) digits.push('-'); return digits.reverse().join(''); };

3.3 用边界用例做一轮完整测试

写完代码不能直接提交,至少要过一轮测试。我列一组覆盖各类情况的用例:

输入输出说明
0"0"最容易漏掉的用例
7"10"刚好是 7 的一次方
100"202"官方示例,检验基本短除法
-7"-10"负号位置是否正确
-8"-11"负数取模为负,需要取绝对值
2147483647"104134211161"int 最大值,不溢出
-2147483648"-104134211162"int 最小值,最容易翻车

我在本地跑了一遍,这组用例全过,耗时几乎为0。尤其 -2147483648 这个用例,如果用了 int 取绝对值,得到的结果会完全不对,建议所有实现都拿它跑一遍。

4. 高频率翻车点与排查思路

4.1 0返回空字符串,提交直接红

这是这道题命中率最高的错误。很多版本的 while 循环条件写的是while (n > 0),一旦输入是0,循环不会执行,如果开头没做if (n == 0) return "0"的特殊判断,返回的就是空字符串,直接判错。

这个坑之所以这么常见,是因为许多人写代码时自然地把0归入“正数逻辑”,觉得0除以7还是0,循环一次就应该停了。实际操作中我的习惯是:凡是做数字转换,先把0分支处理掉,再看正负,最后才上循环。

4.2 负号加的时机不对导致结果错位

还有一类错误是负号位置不对。有人喜欢在循环前先拼接负号:

StringBuilder sb = new StringBuilder(); if (n < 0) { sb.append('-'); n = -n; } ... return sb.reverse().toString();

这个写法在 n 不是 Integer.MIN_VALUE 时没问题。但一旦 n 是 Integer.MIN_VALUE,-n溢出,循环永远跑不完。更重要的是,如果负号先入栈,反转后负号就跑到了末尾,结果变成数字串加负号的奇怪格式。

正确做法是:把负号最晚追加,然后统一 reverse。用 StringBuilder 就是因为最后一次性反转最方便,符号位置天然正确。

4.3 Integer.MIN_VALUE引发死循环

死循环问题在递归版里最容易出现。递归版本判断if (n < 0) return "-" + convertToBase7(-n);,当 n 是 Integer.MIN_VALUE 时,-n结果还是负数,导致无限递归,最后以栈溢出或超时收场。

反复踩过几次之后,我对 int 的边界已经形成了肌肉记忆:凡是要对 int 做取绝对值操作,先问自己一句“这个值可能是 Integer.MIN_VALUE 吗”。只要可能,就先用 long 接住。

4.4 把“取余”想当然,忽略了负数余数的坑

Java 中-8 % 7的结果是 -1,不是 1。这跟数学意义上的模运算不同。如果你直接sb.append(n % 7)而 n 是负数,得到的字符可能是负号,处理出来就是乱码。

解决办法有两个:要么先转正数,要么对余数取一次绝对值。前者思路简单,后者代码紧凑。我推荐前者,可读性更高。

提示:Python 的%结果永远非负,所以 Python 代码里不太会遇到负数余数的坑,但 Java、C++ 都需要留意。

5. 复杂度、性能与代码习惯

5.1 时间复杂度为什么是O(log₇n)

每次循环都将 num 除以7,所以循环执行次数约等于 log₇n。对于 int 范围内的任意整数,最多也就循环12次,因为 7¹² 已经超过 int 的最大范围了。时间复杂度 O(log₇n),空间复杂度主要来自 StringBuilder 或字符串,也是 O(log₇n)。

这个复杂度几乎不可能被优化得更低,因为输出的字符串本身就有这么多位。面试时如果被问到,直接说明这一点即可。

5.2 字符串拼接与StringBuilder的差距

有人习惯用 String 直接拼接:

String res = ""; while (num > 0) { res = num % 7 + res; num /= 7; }

这种写法时间复杂度最坏会退化到 O(k²),其中 k 是结果的位数。因为每次在头部插入字符,字符串的后续部分都要整体搬运。虽然 k 很小(不超过12位),看起来无所谓,但这是一个代码习惯问题。我见过有人写的核心逻辑不复杂,却因为大量字符串拼接在更长输入上跑得很慢。

所以我一般建议:循环里用 StringBuilder 收集结果,最后统一反转;如果要做头部插入,就用待反转的方案。

5.3 小习惯:反转操作的两种写法

Java 里sb.reverse().toString()是标准写法。C++ 里是reverse(ans.begin(), ans.end())。JavaScript 里是digits.reverse().join('')。

有人会问,既然都要反转,为什么不能直接在循环里用索引从高位往低位填充?可以,但这需要预先知道位数,或者先反转一个数组再按序输出,本质上没差。结论就是:别在循环里做头部插入,老老实实先收集后反转,代码更清晰。

6. 七进制之外:一通百通的进制转换

6.1 写一个适配2到36进制的通用方法

掌握了七进制,其实就掌握了任意进制转换。把除数从7换成 base 即可,再准备一张数字表处理超过10的位:

public static String convert(int n, int base) { if (base < 2 || base > 36) { throw new IllegalArgumentException("base must in [2, 36]"); } if (n == 0) { return "0"; } boolean negative = n < 0; long num = n; if (negative) { num = -num; } StringBuilder sb = new StringBuilder(); String digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; while (num > 0) { sb.append(digits.charAt((int)(num % base))); num /= base; } if (negative) { sb.append('-'); } return sb.reverse().toString(); }

这个函数既能转二进制、八进制、七进制,也能转十六进制甚至三十六进制。核心逻辑和七进制完全一样,只是把除数参数化了。面试时如果能顺手写出来,比只会背一道题要加分不少。

6.2 七进制字符串如何转回十进制

逆向操作是乘法迭代:从左到右扫描字符串,每次result = result * 7 + digit。

public static int parseBase7(String s) { if (s == null || s.length() == 0) { throw new IllegalArgumentException("input is empty"); } boolean negative = false; int i = 0; if (s.charAt(0) == '-') { negative = true; i++; } int result = 0; for (; i < s.length(); i++) { char c = s.charAt(i); if (c < '0' || c > '9') { throw new NumberFormatException("invalid digit"); } int digit = c - '0'; if (digit >= 7) { throw new NumberFormatException("digit cannot be larger than 6"); } result = result * 7 + digit; } return negative ? -result : result; }

这里唯一要注意的是溢出问题。严谨的写法需要在乘法前检查result > (Integer.MAX_VALUE - digit) / 7,否则输入一大串“666666666666”会溢出成负数。这种细节在真实业务中处理字符串输入时尤其常见。

6.3 同类算法题对比与面试追问

进制相关的题目除了七进制,还有二进制求和、Excel 列号转换、十六进制转换等。它们底层都是一套东西:短除法、按权展开、边界处理。

面试官如果在这道题上深入追问,方向通常是这些:

  1. 不用库函数,你如何自己实现 Integer.toString(n, 7)?这就是本题的要求。
  2. 输入是负数,你的实现会不会出问题?对应我们讨论的 long 方案。
  3. 如何扩展到任意进制?对应通用方法。
  4. 如何从七进制字符串反解析成十进制?对应 parseBase7。

还会有人会问,能不能用位运算加速?七进制的基数不是2的幂,没法做按位拆分,位运算没有直接帮助。这是跟二进制、十六进制本质不同的地方。

6.4 业务场景里哪些地方真用得上七进制

虽然刷题遇到的进制转换看起来偏理论,但类似的逻辑在业务里其实很常见。比如生成短码时用 62 进制压缩 ID,分库分表时把请求 ID 的某几位取模映射到指定库表,以及把长整型 ID 做可读性编码。

我参与过的一个模拟项目里,就遇到过给用户生成邀请码的需求。产品要求码值尽量短、不能有歧义字符。当时就是用典型的进制转换思路,把数据库自增 ID 转成 36 进制字符串,同时过滤掉容易混淆的字符。那次的实现跟今天这道题的核心几乎一模一样,只是除数是36而不是7。

所以这道题的价值从来不在于“七进制”本身,而在于它把进制转换的最基本套路练熟了。以后碰上任何 base 的转换,都能直接套用同一套骨架,只是换一个除数和一张数字表而已。

我在实际写作代码时,还有一个习惯:每次写完这种基础题,都会把通用版本保存到自己的工具库。遇到类似需求直接复用,省得每次重新处理负数边界。这道题也一样,建议你把上面的通用 convert 函数存一份,以后真能用上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询