最近刷题碰到一道很有意思的题目,叫《来自异国的客人》,分值100分,题目后面还特意标注了“Java & JS & Python & C”四种语言。乍看名字还以为是什么文化背景题,结果点进去才发现,内核就是一道非常经典的进制转换题。题面大体是:一位异国客人习惯用 k 进制记数,你给他一个十进制整数 n,要你算出这个数在他熟悉的 k 进制写法里,某个指定数字 m 出现了几次。说白了,就是“十进制转 k 进制,再数一数目标数字出现多少次”。
我第一次看到这道题时,脑子里冒出的第一个想法是:会不会涉及什么国家文化、数字忌讳?后来静下心把样例一推,发现完全不是那么回事。出题人故意用“异国客人”这个场景把算法包了一层,实际上就是想考察三件事:进制转换的基本功、循环取余的熟练度、以及面对边界条件时的细心程度。这篇文章我会把四种语言的解法都拆开讲一遍,顺便把我自己在实际调试中踩过的坑也列出来,希望能帮到正在刷题、准备机考或者面试的朋友。
1. 题目到底在考什么:一场披着故事外衣的进制转换
1.1 先还原一下题目的真实面貌
我没法把原题一字不差地贴出来(毕竟是别人的测评题目),但核心信息非常固定,就三个输入一个输出:
- 输入:十进制整数 n,目标进制 k,要统计的数字 m
- 输出:n 转换成 k 进制后,数字 m 出现的次数
举个例子,假设输入:
n = 10 k = 2 m = 0十进制的 10 转换成二进制是1010,其中数字0出现了 2 次,所以输出应该是2。
这种题目本身不难,但它很有代表性。因为它把“进制转换”这个最基础的知识点,包装成了一道有剧情、有分值的编程题。你要是被“异国客人”这个设定带跑了,真的会浪费时间想什么“客人国家用的是几进制”“他喜欢哪个数字”,其实这些都是干扰信息。
1.2 为什么这道题值 100 分
很多刷题平台会把题目按分值和难度分级,100分的题通常属于“中等偏简单”,但也绝不是白给。我个人的理解是,这道题能拿满分的人不少,但能稳定满分的人不多。为什么?因为它藏了几个一眼看不出来的边界条件。
比如说,n = 0这种情况。很多人一看到 while 循环,就默认 n 一定大于 0,结果n=0时循环一次都不执行,直接返回 0。但 0 的 k 进制表示就是0,如果 m 也是 0,那答案应该是 1。这一下就能筛掉一批粗心的人。
再比如说,m会不会大于等于k?如果 m 是 7,k 是 2,那 2 进制里根本不可能出现数字 7,答案必然是 0。如果程序里没有提前判断,就会白白循环一遍,而且结果还是 0,虽然碰巧对了,但逻辑上不严谨。
所以说,这 100 分考的不是你会不会写while循环,而是你能不能把所有边角情况都考虑进去。
1.3 适合谁来参考这篇拆解
如果你正在准备机考、校招笔试、或者单纯想巩固基础算法,这篇内容都很合适。我会把四种语言的代码都贴出来,并且逐行解释。你不需要多聪明,只要跟着思路走一遍,然后自己动手敲一遍,基本就能把这 100 分稳稳拿住。之后同类题,比如“求某进制下各位数字之和”“判断某进制下是否包含特定数字”,都可以用一个模板套进去。
2. 核心思路:除 k 取余法,以及它为什么不会错
2.1 进制转换的数学原理
十进制转 k 进制,最经典的方法就是除 k 取余法。你拿 n 除以 k,得到的余数就是 k 进制表示里的最低位;然后把商继续除以 k,得到的余数是下一位;一直循环到商为 0 为止。
这里面的数学逻辑其实很朴素。十进制数 n 可以写成:
n = q1 * k + r1其中 r1 是 n 除以 k 的余数,q1 是商。r1 的范围一定是 0 到 k-1,正好对应 k 进制最低位上的数字。接着对 q1 做同样的操作:
q1 = q2 * k + r2r2 就是第二低位的数字。重复这个过程,直到某一步商为 0。最后把所有余数逆序排列,就是完整的 k 进制表示。
举个例子,把十进制 13 转成二进制:
- 13 ÷ 2 = 6 余 1,最低位是 1
- 6 ÷ 2 = 3 余 0,第二位是 0
- 3 ÷ 2 = 1 余 1,第三位是 1
- 1 ÷ 2 = 0 余 1,最高位是 1
把余数从下往上排,得到1101,这就是 13 的二进制表示。验证一下:1×8 + 1×4 + 0×2 + 1×1 = 13,完全正确。
2.2 边转换边统计,比“先转完再数”更省事
这道题要求统计某个数字 m 出现的次数。有两种实现路线:
第一种:先把 n 完整转换成 k 进制,得到一个字符串,然后遍历这个字符串去数 m。这种方法直观,但要多开辟一份字符串空间,而且在不同语言里还要处理字符和数字之间的转换。
第二种:在循环取余的过程中,每得到一个余数 digit,就立即判断它是不是等于 m,是就计数加一。这种方法空间复杂度是 O(1),代码也更短。
我强烈推荐第二种做法。因为它复用了一个本来就存在的循环,没有任何额外开销。你只需要在 while 循环里加一个 if 判断就行。下面是这个思路的伪代码:
if n == 0: return 1 if m == 0 else 0 count = 0 while n > 0: digit = n % k if digit == m: count = count + 1 n = n / k # 注意这里是整除 return count这个模板适用于所有语言,后续只要照着语法改一改就行。
2.3 复杂度分析
时间复杂度是 O(log_k n)。因为 n 每循环一次就除以 k,循环次数就是 n 在 k 进制下的位数。比如十进制 100 转二进制,大概要循环 7 次,因为 2^7 = 128。这个复杂度对于日常输入来说完全够快,哪怕 n 是 10 亿,也就循环 30 多次。
空间复杂度是 O(1),因为我们只用了几个临时变量,没有存整个转换结果。这一点在 C 语言里尤其重要,因为如果先转成字符串,你就得提前开一个足够大的字符数组,还得处理数组越界问题。
3. 四种语言逐个击破:Java & JS & Python & C
3.1 Java 实现
Java 版本我写得很直白,就用一个 count 变量累加,完全不需要引入字符串处理。很多 Java 新手喜欢先转成字符串再用indexOf或者charAt去数,其实完全没必要,反而容易踩字符转数字的坑。
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int k = sc.nextInt(); int m = sc.nextInt(); System.out.println(countDigit(n, k, m)); } public static int countDigit(int n, int k, int m) { // 如果 m 不在 k 进制的合法数字范围内,直接返回 0 if (m < 0 || m >= k) { return 0; } // n == 0 时,k 进制表示就是 "0",需要单独处理 if (n == 0) { return m == 0 ? 1 : 0; } int count = 0; while (n > 0) { int digit = n % k; if (digit == m) { count++; } n /= k; } return count; } }这里面有两点值得解释。第一,m >= k的提前判断不是可有可无的,它能让逻辑更严谨。第二,n == 0必须放在 while 循环前面处理,否则循环不执行,答案就会是 0,而正确答案应该是当 m 为 0 时输出 1。
如果你担心 n 特别大,可以把int换成long,方法签名也改成long n。在 Java 里用long做除法和取余没有任何问题,只是在读入时要用nextLong()。
3.2 JavaScript 实现
JavaScript 版本同样建议手动实现进制转换。虽然 JS 内置了toString(radix)方法可以很方便地把数字转成任意进制字符串,但它有两个问题:第一,toString得到的是字符串,你仍然得遍历字符串去数字符,底层还是得多做一层;第二,toString(2)这样的调用在数字很大时可能会有精度溢出问题,因为 JS 的 Number 是浮点数,超过Number.MAX_SAFE_INTEGER就不精确了。所以老老实实写循环,反而最稳。
function countDigit(n, k, m) { // 输入合法性检查 if (m < 0 || m >= k) { return 0; } if (n === 0) { return m === 0 ? 1 : 0; } let count = 0; while (n > 0) { const digit = n % k; if (digit === m) { count++; } // 注意这里必须用 Math.floor,确保是整数除法 n = Math.floor(n / k); } return count; } // 测试 console.log(countDigit(10, 2, 0)); // 2JS 新手最容易犯的错误是直接写n = n / k。在 JS 里,/运算符做的是浮点数除法,比如10 / 2得到 5,这没问题,但9 / 2会得到 4.5。如果你不Math.floor,下一轮17 % 2之类的结果就会全乱掉。所以务必写成Math.floor(n / k)。
如果你真的很喜欢toString,那我也给一个参考写法:
function countDigitWithToString(n, k, m) { if (n === 0) { return m === 0 ? 1 : 0; } const s = n.toString(k); let count = 0; for (const ch of s) { // 将字符转成数字,注意处理 a-f 这类字母 const digit = parseInt(ch, k); if (digit === m) { count++; } } return count; }这个写法在 k 小于等于 10 时能正常工作,但如果 k 大于 10,就会遇到字母。比如十六进制里a代表 10,parseInt('a', 16)会得到 10,但你要统计的 m 如果是 10,逻辑上仍然成立。只是这种写法多了一层字符解析,性能不如手动取余。
3.3 Python 实现
Python 的写法是最干净的,因为它语法简洁,而且整数除法//和取余%的语义非常明确。尤其要注意,Python 3 里/是浮点除法,//才是整除,一定要用//。
def count_digit(n: int, k: int, m: int) -> int: # 如果 m 不可能是 k 进制中的数字,直接返回 0 if m < 0 or m >= k: return 0 # n == 0 的特殊处理 if n == 0: return 1 if m == 0 else 0 count = 0 while n > 0: digit = n % k if digit == m: count += 1 n //= k return count if __name__ == "__main__": print(count_digit(10, 2, 0)) # 2Python 里还可以用divmod一次性同时得到商和余数,让代码更紧凑:
def count_digit_divmod(n: int, k: int, m: int) -> int: if m < 0 or m >= k: return 0 if n == 0: return 1 if m == 0 else 0 count = 0 while n: n, digit = divmod(n, k) if digit == m: count += 1 return countdivmod(n, k)返回一个元组,第一个是整除后的商,第二个是余数。把n重新赋值为商,digit保存余数,一行代码完成两件事。不过这个写法对不熟的读者可能有点绕,建议在面试中还是用%和//更直观。
Python 还有一个优势,就是它原生支持大整数,不会像 C/Java 那样担心int溢出。所以即使 n 给到 10^18,这个函数照样能算出正确结果。
3.4 C 语言实现
C 语言是这个题目的“原教旨”解法。因为进制转换本来就是计算机底层最常做的事情,用 C 写反而最顺手。C 的整数除法/本身就是向零取整,所以不用担心n = n / k会变成小数。
#include <stdio.h> int countDigit(int n, int k, int m) { if (m < 0 || m >= k) { return 0; } if (n == 0) { return m == 0 ? 1 : 0; } int count = 0; while (n > 0) { int digit = n % k; if (digit == m) { count++; } n /= k; } return count; } int main() { int n, k, m; scanf("%d %d %d", &n, &k, &m); printf("%d\n", countDigit(n, k, m)); return 0; }这里没什么花哨的技巧,唯一要提醒的是变量类型。如果 n 的范围可能超过int的最大值(约 21 亿),建议把int改成long long,对应的scanf和printf也要改成%lld。例如:
long long countDigit(long long n, int k, int m) { ... }C 语言不像 Java 或 Python 那样有字符串的便利方法,但这道题根本不需要转换成字符串,直接用余数比较数字大小即可。这也再次说明了“边转换边统计”的思路有多省事。
3.5 四种语言横向对比
| 语言 | 核心代码量 | 主要易错点 | 推荐场景 |
|---|---|---|---|
| Java | 中 | 输入输出模板稍长;int 可能溢出 | 企业应用开发、机考主流语言 |
| JavaScript | 短 | /是浮点除法,忘记Math.floor会导致死循环 | 前端岗笔试、算法练习 |
| Python | 最短 | 忘记用//整除;想当然用内置函数处理带前缀的字符串 | 快速验证思路、算法学习 |
| C | 短 | 数据范围控制;scanf/printf 格式符 | 底层原理学习、竞赛基础 |
其实四种语言的核心逻辑一模一样,差别只在于语法表达。这就是为什么我建议刷题时同一道题用不同语言各写一遍,能帮你把语言特性记得更牢固。
4. 边界情况与隐藏陷阱:这些坑我帮你们踩过了
4.1 边界情况速查表
下面这个表是我整理出来的常见边界情况,基本覆盖了这道题 80% 的隐藏失分点。
| 输入情况 | 正确答案 | 常见错误 |
|---|---|---|
| n=0, k=2, m=0 | 1 | 0(因为 while 循环没执行) |
| n=0, k=2, m=1 | 0 | 1(误认为 0 的二进制包含 1) |
| n=5, k=2, m=3 | 0 | 死循环(忘记 m 不在 k 进制中) |
| n=100, k=10, m=0 | 2 | 1(100 的十进制里有 2 个 0) |
| n=1, k=2, m=1 | 1 | 0(二进制 1 中恰好有 1 个 1) |
| n=255, k=16, m=15 | 1 | 0(十六进制 FF 中的 F 代表 15) |
第 5 行看起来简单,但确实有人会在 n=1 的时候把答案写成 0,因为循环只执行一次,而他们把条件写成了digit == m && n != 1这种多余判断。
第 6 行比较特殊。如果 k=16,m=15,那么 n=255 转十六进制是FF,每一位都是 15,所以答案应该是 2。如果你用的是字符串遍历方案,要注意parseInt('F', 16)的结果确实是 15,而不是 0。当然,大部分题目里 m 都是 0-9,k 也不超过 10,这个例子主要是提醒大家进制扩展后的处理逻辑。
4.2 为什么 n=0 是最大的坑
我特地把它单独拎出来说,因为网上的题解里,十个有八个会忘记处理 n=0。很多人一看到n % k就默认 n 是正整数,结果样例全是正数,自测也全过,等交上去发现某个隐藏用例挂了。
0 在任何进制下都写作0。所以:
- 如果 m=0,答案是 1
- 如果 m 不等于 0,答案是 0
最简单的处理方式,就是在函数开头单独判断:
if (n == 0) { return m == 0 ? 1 : 0; }这行代码你可以在所有语言里都写上。没坏处,还能防止后面循环直接跳过。
4.3 m 的合法性判断
我遇到不少人在拿到题目后,没仔细看数据范围,直接开始写循环。假如 m 大于等于 k,比如 k=2, m=3,那么 while 循环里永远不可能出现 digit == 3,但程序会老老实实地把整个循环跑完,然后返回 0。虽然结果碰巧对,但万一 m 是个负数,或者在某些特殊进制下 m 的表示涉及字母,就会出麻烦。
稳妥的做法是在开头加一句:
if (m < 0 || m >= k) { return 0; }别小看这行代码,它能让你的逻辑闭环,并且向阅卷人传递一个信号:你注意到了数字范围这个关键约束。
4.4 语言层面的数据溢出问题
这道题如果输入范围只是 10^9 以内,用 int 完全够。但有些机考题目会悄悄把数据范围拉大,尤其是 Python 玩家往往容易忽略其他语言的溢出问题。
- C 语言:
int最大约 21 亿(有符号 32 位),如果 n 可能超过 10^9,建议直接用long long,宁可多占 4 个字节也别冒险。 - Java:
int同样有 21 亿上限,读入时可以用long,但Scanner.nextLong()和nextInt()别用混了。 - JavaScript:JS 的 Number 是双精度浮点数,超过
2^53 - 1就不安全了。如果题目说 n 可以达到 10^18,JS 需要借助BigInt类型,写成BigInt(n),然后所有除法、取余都要换用 BigInt 的方法,这就复杂多了。好在大多数考 JS 的场景,数据范围不会那么变态。 - Python:自带大整数,无限位,不用管溢出,这是 Python 在这道题里最大的优势。
4.5 内置进制转换方法的隐藏坑
Python 的bin(n)会返回'0b1010'这种带前缀的字符串,oct(n)返回'0o...',hex(n)返回'0x...'。如果你直接拿bin(n)去数 '0',前缀里的0b里的 0 也会被数进去,结果就会多 1。
JavaScript 的n.toString(2)没有前缀,但得到的是字符串,你要数 '0' 就得遍历字符串,而且如果 m 是数字 10,你还得先把字符'a'转成数字 10,多一层麻烦。
C 语言没有内置进制转换函数,一切都要自己写,所以反而不会踩这种坑。
Java 的Integer.toString(n, k)可以转成指定进制字符串,但同样返回字符串,需要遍历字符。
所以我的最终建议是:在笔试或机考中,手动实现除 k 取余法,不要依赖内置的进制转换方法。内置方法看着方便,一旦涉及统计,反而会让你多处理很多边角情况。
5. 从这道题出发:一个通用模板和它的变体
5.1 把核心逻辑抽成函数
这道题的价值不在于背代码,而在于你能掌握“进制转换 + 逐位处理”这个万能模板。下面这个模板适用于所有语言:
func countDigit(n, k, m): if m < 0 or m >= k: return 0 if n == 0: return (m == 0) ? 1 : 0 count = 0 while n > 0: digit = n % k if digit == m: count += 1 n = n / k # 整除 return count只要把n、k、m换成别的变量名,就能套用到很多相似题目上。比如:“给定 n 和 k,求 n 在 k 进制下各位数字之和”“求 n 在 k 进制下最高位是什么”“判断 n 在 k 进制下是否包含某个数字”。
5.2 三个典型变体
变体一:直接求 k 进制下各位数字之和。
int digitSum(int n, int k) { if (n == 0) { return 0; } int sum = 0; while (n > 0) { sum += n % k; n /= k; } return sum; }变体二:求 k 进制表示的最高位数字。这需要先算出有多少位,或者用对数估算,但最简单的做法是先循环一遍求出所有位,存到数组里,最后一个余数就是最高位。
变体三:判断 n 在 k 进制下是不是回文数。比如十进制 121 转二进制是1111001,不是回文;但十进制 5 转二进制是101,是回文。这类题同样只需要在循环里把每位数字收集起来,最后前后比较。
所以你看,一道“来自异国的客人”真正训练的是你处理进制的底层能力,背景故事再花哨,核心永远不变。
5.3 自己动手做一次完整测试
不管用哪种语言,我建议你写完代码后,至少跑下面这 5 组测试用例,全对才说明这题你真的拿稳了。
n=10, k=2, m=0 -> 2 n=0, k=2, m=0 -> 1 n=0, k=2, m=1 -> 0 n=255, k=16, m=15 -> 2 n=100, k=10, m=0 -> 2如果最后一组你觉得奇怪,解释一下:100 在十进制下是三位数100,其中数字 0 出现了 2 次,所以输出 2。很多人在这个用例上会错写成 1,因为他们只考虑了末尾的 0,忘了十位上的 0。
6. 实操心得与答题策略
6.1 机考/面试时如何稳拿这 100 分
我在实际机考中总结了一套固定流程,拿到这种题先不读故事,直接做三件事:
第一,在草稿纸上写下输入输出和示例数据。把题面里的样例手动算一遍,确保自己理解正确。
第二,判断数据范围。如果 n 可能是 0,先写特殊处理;如果 m 可能越界,先写合法性判断;如果 n 很大,选long或BigInt。
第三,写一个最朴素的循环版本,不要一上来想优化。这种 100 分的题,能用 O(log_k n) 的时间解出来已经满分了,不需要什么奇技淫巧。
按照这个流程,我基本可以在 5 分钟内写完并调试完毕。剩下时间可以用来检查其他题。
6.2 四种语言的调试差异
这道题在四种语言里的调试体验差别很大,我说一下自己的体感。
C 语言最容易出问题的是格式符写错。scanf里写%d却传入long long的地址,轻则读入错乱,重则段错误。所以我在 C 里用到long long时,会非常刻意地检查scanf和printf的格式符。
Java 的调试麻烦主要在代码结构。你总不能把方法写在类外面,所以我会先在本地写好一个完整的Main类结构,然后专注于方法体逻辑。
JavaScript 最经典的坑是浮点数除法,我在本地跑的时候习惯在这个位置加一行console.log(n),如果发现出现小数,就知道忘记Math.floor了。
Python 的调试最舒服,因为报错信息直观,而且//和%的优先级很清晰。唯一要留意的是别把//写成/,否则循环会退化成浮点数除法,导致 n 永远减不到 0,最后形成死循环。
6.3 从这道题能学到什么
很多同学刷题喜欢“广撒网”,今天做链表,明天做二叉树,后天做动态规划,结果每样都只懂个皮毛。像这种 100 分的简单题,反而是吃透语言细节的好素材。用四种语言各写一遍,你会不自觉地思考:为什么 Python 写起来这么快?为什么 C 没有字符串也能解?为什么 Java 的int会有上限?这些问题比题目本身更能提升你的工程能力。
我个人刷题的习惯是,每遇到一道值得做的题,就会在本地建一个文件夹,用solution.c、Solution.java、solution.js、solution.py四个文件分别保存同一份逻辑。几个月后再回看这些代码,对比各语言的差异,那种收获不是刷几道难题能比的。
6.4 一个小技巧:把 m 的判断写在最前面
最后再分享一个个人小习惯。我会在countDigit函数的第一行就先判断m的合法性,而不是等循环结束再处理。这不是为了性能,而是为了让自己在读代码时一眼就能确认边界条件已经处理完。写这类基础题,代码顺序很重要,边界条件先处理,主逻辑放中间,这样不管你一个月后回来看,还是交给其他人 review,都不会觉得混乱。
而且这个习惯在面试白板编程时特别加分。面试官如果看到你第一行就处理了m >= k和n == 0,一般会认为你考虑问题比较全面,哪怕后面代码有小瑕疵,印象分也会高不少。
以上总结一下我对“来自异国的客人”这道题的所有经验。题目本身不难,但它是一面很好的镜子,能照出你对进制转换、边界判断和多语言语法的熟悉程度。如果你在机考或面试里碰到它,希望这篇文章能帮你稳拿满分。