CC++ & Algorithm

为什么你背了那么多进制转换公式,却还是不会做八进制转十六进制?

先抛一个观点:进制转换根本不需要背那么多公式,你只需要记住一个"中转站"就够了。

很多人在学进制转换的时候,习惯把二进制转八进制、八进制转十六进制、十六进制转二进制……每一种组合都当成独立的题型去记。结果就是,题目稍微一变——比如让你把五进制转十二进制——整个人就懵了。

但如果你理解了"十进制中转"这个思路,所有的进制互转都会坍缩成两个动作:先转成十进制,再从十进制转出去。

这就像国际汇款。你要把英镑换成日元,银行不会直接帮你换,而是先按汇率换成美元,再从美元换成日元。美元在这里就是"通用货币",十进制就是进制世界里的"通用货币"。


先搞清楚方向:两种方法,别搞反了

进制转换的方法其实只有两个方向,但初学者最容易犯的错误就是把它们搞混:

  • R进制 → 十进制:用"按权展开求和法"
  • 十进制 → R进制:用"除基取余法"

有个判断题说"将R进制数转换为十进制数,通用的方法是除基取余法"——这是错的。除基取余是反方向用的。

为什么容易搞混?因为两个方法都涉及"基"和"余"这些字眼。但只要你记住一个逻辑,就不会再弄反:

从R进制到十进制,是把每一位"拆开"算总账;从十进制到R进制,是把总数"打包"成每一位。

一个是展开,一个是收缩。方向感有了,方法就不会用错。


按权展开:为什么每一位要乘基数的幂次?

先看R进制转十进制。

一个二进制数 1011,它为什么等于11?

1×2³ + 0×2² + 1×2¹ + 1×2⁰ = 8 + 0 + 2 + 1 = 11

这个式子背后的逻辑是:每一位数字本身不代表它的大小,它乘上所在位置的"权值"才代表真实大小。 就像你在公司里的头衔——"经理"这个头衔在不同规模的公司里含金量完全不同。位置就是进制数的"公司规模"。

这个思路推广到任意进制都成立。八进制 777:

7×8² + 7×8¹ + 7×8⁰ = 448 + 56 + 7 = 511

十六进制 1A3F:

1×16³ + 10×16² + 3×16¹ + 15×16⁰ = 4096 + 2560 + 48 + 15 = 6719

道理完全一样,只是基数从2换成了8或16。


代码怎么写?别用pow函数,用秦九韶算法

很多人的第一反应是用 pow(base, n) 去算每一位的权值,然后再累加。思路没错,但不够优雅,而且 pow 返回浮点数,处理大数时可能丢精度。

更好的做法是从左到右遍历,每次把之前的结果乘基数再加上当前位:

int decimal = 0;
for (char c : num) {
    decimal = decimal * base + charToDigit(c);
}

就这么两行。以二进制 1011 为例:

  • 读到 1:decimal = 0×2+1 = 1
  • 读到 0:decimal = 1×2+0 = 2
  • 读到 1:decimal = 2×2+1 = 5
  • 读到 1:decimal = 5×2+1 = 11

结果和按权展开一模一样,但不需要预先知道字符串长度,也不需要算幂次。这就是秦九韶算法(也叫霍纳法则),是计算机里处理多项式求值的经典技巧。


除基取余:为什么余数要倒着读?

反方向,十进制转R进制,用的是短除法。

十进制 511 转二进制:

511 ÷ 2 = 255 ... 余1
255 ÷ 2 = 127 ... 余1
127 ÷ 2 =  63 ... 余1
 63 ÷ 2 =  31 ... 余1
 31 ÷ 2 =  15 ... 余1
 15 ÷ 2 =   7 ... 余1
  7 ÷ 2 =   3 ... 余1
  3 ÷ 2 =   1 ... 余1
  1 ÷ 2 =   0 ... 余1

余数从下往上读:111111111。

为什么倒着读?因为第一次除出来的余数是最低位(它对应的是2⁰这一位),最后一次除出来的余数是最高位。就像你叠盘子,第一个放上去的在最底下,最后放上去的才在最上面。

代码实现时,有两种写法:

// 写法一:每次把新余数拼到结果前面
result = digitToChar(remainder) + result;

// 写法二:先拼到后面,最后反转
result += digitToChar(remainder);
// 最后 reverse(result.begin(), result.end());

两种都行,但写法一更直观,不容易忘记反转。


题目在考什么?不只是公式

回到那道选择题:在C++中,将任意进制字符串转换为十进制整数时,通用算法是什么?

选项里给了四个:按权展开累加法、除基取余法、递归分解法、位运算法。

正确答案是按权展开累加法。

这道题表面上考的是方法名称,实际上考的是你对转换方向的理解。除基取余是反方向的,位运算只适用于二进制(你没法用位移操作处理八进制字符串),递归分解虽然能实现但不是"通用算法"这个语境下的标准答案。

而按权展开累加法,对应的就是上面那个 decimal = decimal * base + digit 的循环。它之所以"通用",是因为不管你输入的是二进制、八进制还是十六进制,只要把基数和字符到数字的映射改一下,核心逻辑完全不用动。

这就是"十进制中转站"的威力——两个通用函数,覆盖所有进制组合。


三个容易踩的坑

第一个坑:0的处理。 如果输入是 "0",toDecimal 返回0,然后 fromDecimal(0, base) 如果while循环条件是 decimal > 0,循环体一次都不会执行,返回空字符串。所以入口处必须加一句 if (decimal == 0) return "0";。

第二个坑:余数拼反。 这个前面说过了,result + digitToChar(remainder) 得到的是颠倒的结果。13转二进制会输出 1011 而不是 1101。

第三个坑:非法字符。 如果用户输入了 "G",charToDigit 返回-1,然后 decimal * base + (-1) 会把结果算错。实际工程中要做输入校验,但学习阶段可以先假设输入合法。


进阶方向

掌握了"十进制中转"这个通用方法之后,你可以继续深入几个方向:

快捷转换:因为 2³=8、2⁴=16,所以二进制和八进制、十六进制之间有"分组转换"的快捷方法。三位二进制对应一位八进制,四位二进制对应一位十六进制。这在底层调试时非常实用。

小数进制转换:整数部分用除基取余,小数部分用"乘基取整"。比如0.625转二进制:0.625×2=1.25取整1,0.25×2=0.5取整0,0.5×2=1.0取整1,结果是0.101。

负数表示:原码、反码、补码。这是理解计算机如何存储负数的关键,也是位运算的基础。

实际应用场景:内存地址用十六进制表示、颜色值用 #FF5733 这样的十六进制、Linux文件权限用八进制——进制转换不是课本里的练习题,它渗透在计算机的每一个角落。

回到最开始那句话:你不需要背那么多公式,你只需要理解"中转"这个思路。 把R进制转成十进制,再把十进制转成目标进制——两步走,任意进制互转都能搞定。


关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

成为第一个评价的人

评论0

还没有评论,来抢沙发~

评论加载中...

想系统学习这个知识点?查看完整知识点 →