☰
蓝桥杯BASIC-12:大数十六进制转八进制的高效实现
2026/10/14 12:21:15 网站建设 项目流程

1. 这道题到底在考什么

蓝桥杯基础练习里有一道编号为 BASIC-12 的题目,名字叫“十六进制转八进制”。很多刚接触算法竞赛的朋友第一次看到这道题,第一反应是“进制转换嘛,不就是printf("%o")一行搞定的事”。如果你真这么想,那大概率会在提交后收到一个“运行错误”或者“答案错误”,然后对着屏幕发呆半天。

这道题的核心难点根本不在进制转换本身,而在于数据规模。题目给出的十六进制数长度可以达到 100000 个字符,这意味着这个数远远超出了任何标准整数类型能表示的范围。你用long long存不下,用unsigned long long也存不下,就算用__int128照样存不下。所以这道题真正考的是:如何在不把整个数转成机器整数的前提下,完成进制转换。

适合谁来参考这篇内容?如果你正在准备蓝桥杯、ACM 或者任何有类似大数处理需求的编程竞赛,这道题是一个非常好的练手素材。它涉及字符串处理、进制映射、位运算思维和边界条件处理,这些技能在后端开发、嵌入式数据处理、加密算法实现里都会反复用到。哪怕你只是想把编程基础打扎实,这道题也值得花时间吃透。

我当年第一次做这道题的时候,直接用了“十六进制转十进制再转八进制”的思路,结果因为中间结果太大直接溢出,后来才想明白应该走“十六进制转二进制再转八进制”的路线。这个思路转换的过程,其实就是这道题最有价值的地方。

2. 为什么不能走“先转十进制”的老路

2.1 数据规模决定了你必须放弃整数类型

先算一笔账。题目说十六进制数的长度不超过 100000 个字符。一个十六进制位对应 4 个二进制位,所以这个数转换成二进制后大约有 400000 位。400000 位的二进制数是什么概念?它大约相当于 10 的 120000 次方。而unsigned long long最大只能表示到 10 的 19 次方左右,差距是天文数字。

所以任何试图把输入解析成整数类型的方案,在这道题面前都是死路。你必须把整个数当成一个字符串来处理,逐位操作,逐位输出。

注意:有些朋友会想用大数库(比如 Java 的 BigInteger 或者 Python 的 int),这些确实能解决问题,但在蓝桥杯的 C/C++ 环境下没有现成的大数库可用,而且即使有,直接依赖大数库也失去了这道题训练字符串处理能力的意义。

2.2 十六进制到八进制的“桥梁”是二进制

为什么选二进制做中间桥梁?因为 16 和 8 都是 2 的幂次:16 等于 2 的 4 次方,8 等于 2 的 3 次方。这意味着十六进制和二进制之间的转换是“一对一”的,每一位十六进制数恰好对应 4 位二进制数,不需要做任何除法或取模运算。同样,二进制和八进制之间的转换也是“一对多”的,每 3 位二进制数恰好对应 1 位八进制数。

这个性质非常关键。如果你走“十六进制转十进制转八进制”的路线,中间需要做大量的乘法和除法,而且十进制数本身无法用整数存储。但走“十六进制转二进制转八进制”的路线,整个过程只需要做字符串的映射和拼接,完全不需要算术运算。

我个人的经验是:凡是进制转换题目,先看两个进制是不是都是 2 的幂次。如果是,一律走二进制桥梁;如果不是,才考虑其他方案。这个判断法则能帮你省下大量思考时间。

2.3 具体转换规则的推导

十六进制有 16 个符号:0-9 和 A-F。每一位对应的 4 位二进制如下表所示:

十六进制二进制十六进制二进制
0000081000
1000191001
20010A1010
30011B1011
40100C1100
50101D1101
60110E1110
70111F1111

八进制有 8 个符号:0-7。每 3 位二进制对应 1 位八进制:

二进制八进制
0000
0011
0102
0113
1004
1015
1106
1117

有了这两张表,整个转换过程就变成了纯粹的字符串操作:先把十六进制字符串逐位展开成二进制字符串,再把二进制字符串从右往左每 3 位一组转换成八进制。听起来很简单,但实际操作中有几个坑必须提前说清楚。

3. 手把手拆解完整实现流程

3.1 第一步:读入与预处理

输入格式通常是先给一个整数 n,表示接下来有 n 行十六进制数。每行一个十六进制字符串,长度可能很长。读入的时候要注意,不能用scanf("%s")直接读,因为字符串可能非常长,需要确保缓冲区足够大。我一般用cin或者scanf配合足够大的字符数组。

#include <iostream> #include <string> using namespace std; int main() { int n; cin >> n; while (n--) { string hexStr; cin >> hexStr; // 后续处理 } return 0; }

这里有一个细节:输入可能包含前导零,比如000A。这些前导零在转换后不应该出现在结果里,但转换过程中不能提前去掉,因为它们会影响二进制展开的对齐。我的做法是先把所有位都展开成二进制,最后再去掉结果的前导零。

3.2 第二步:十六进制展开成二进制

这一步的核心是建立一个映射表。我习惯用字符串数组来存:

const string hexToBin[16] = { "0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111" };

然后遍历十六进制字符串的每一位,把对应的 4 位二进制拼接到一个大的二进制字符串里:

string binStr = ""; for (char c : hexStr) { int val; if (c >= '0' && c <= '9') val = c - '0'; else val = c - 'A' + 10; binStr += hexToBin[val]; }

这一步的时间复杂度是 O(n),其中 n 是十六进制字符串的长度。因为每个字符只处理一次,每次拼接 4 个字符,所以总的二进制字符串长度是 4n。

实操心得:拼接字符串的时候,如果用+=操作符,在 C++ 里效率是可以接受的,因为string会动态扩容。但如果追求极致性能,可以预先分配好空间,或者用一个字符数组来手动管理。不过在蓝桥杯的场景下,+=完全够用。

3.3 第三步:补齐二进制串的长度

二进制转八进制需要每 3 位一组,所以二进制字符串的长度必须是 3 的倍数。如果不是,需要在左边补零。这一步非常关键,漏掉的话结果会完全错误。

int remainder = binStr.length() % 3; if (remainder == 1) { binStr = "00" + binStr; } else if (remainder == 2) { binStr = "0" + binStr; }

为什么是在左边补零而不是右边?因为二进制数的高位在左边,低位在右边。补零的目的是让分组从高位开始对齐,如果不补零,最高位的那一组会少几位,导致整个数值错误。

举个例子:二进制串1010,长度是 4,4 除以 3 余 1,所以需要在左边补两个零变成001010。然后从右往左每 3 位分组:001和010,对应八进制1和2,结果是12。如果不在左边补零,直接分组会变成1和010,结果就错了。

3.4 第四步:二进制转八进制并输出

补齐之后,从左往右每 3 位一组,转换成对应的八进制数字:

string octStr = ""; for (int i = 0; i < binStr.length(); i += 3) { int val = (binStr[i] - '0') * 4 + (binStr[i+1] - '0') * 2 + (binStr[i+2] - '0'); octStr += (char)('0' + val); }

这里用了一个小技巧:3 位二进制数abc对应的十进制值是a*4 + b*2 + c。因为 4、2、1 正好是 2 的幂次,所以可以直接用乘法和加法算出来,不需要额外的映射表。

最后一步是去掉结果的前导零。注意,如果结果全是零,要保留一个零:

int pos = 0; while (pos < octStr.length() - 1 && octStr[pos] == '0') { pos++; } cout << octStr.substr(pos) << endl;

3.5 完整代码整合

把上面的步骤串起来,完整的代码如下:

#include <iostream> #include <string> using namespace std; const string hexToBin[16] = { "0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111" }; int main() { int n; cin >> n; while (n--) { string hexStr; cin >> hexStr; // 十六进制转二进制 string binStr = ""; for (char c : hexStr) { int val; if (c >= '0' && c <= '9') val = c - '0'; else val = c - 'A' + 10; binStr += hexToBin[val]; } // 补齐长度到 3 的倍数 int remainder = binStr.length() % 3; if (remainder == 1) binStr = "00" + binStr; else if (remainder == 2) binStr = "0" + binStr; // 二进制转八进制 string octStr = ""; for (int i = 0; i < binStr.length(); i += 3) { int val = (binStr[i] - '0') * 4 + (binStr[i+1] - '0') * 2 + (binStr[i+2] - '0'); octStr += (char)('0' + val); } // 去掉前导零 int pos = 0; while (pos < octStr.length() - 1 && octStr[pos] == '0') pos++; cout << octStr.substr(pos) << endl; } return 0; }

这段代码在蓝桥杯的评测环境下可以稳定通过,时间复杂度是 O(n),空间复杂度也是 O(n),对于长度 100000 的输入来说完全够用。

4. 踩过的坑和排查技巧

4.1 前导零处理不当导致答案错误

这是最常见的错误。很多人写完了转换逻辑,结果发现输出多了一个零或者少了一个零。问题出在两个地方:一是二进制补齐的时候补错了方向,二是在最后输出的时候没有正确去掉前导零。

我的建议是:在二进制转八进制的循环结束后,先不要急着输出,单独写一段逻辑来处理前导零。处理的时候要注意边界情况,比如输入是0的时候,结果应该是0而不是空字符串。

4.2 输入长度超出预期导致数组越界

有些朋友用固定大小的字符数组来存输入,比如char hexStr[100],结果遇到长输入直接越界。蓝桥杯的题目明确说了长度可能到 100000,所以数组至少要开到 100005 才安全。如果用string就没这个问题,因为string会自动扩容。

4.3 大小写敏感问题

题目里的十六进制数可能包含大写字母 A-F,也可能包含小写字母 a-f。虽然蓝桥杯的测试数据通常是大写,但为了保险起见,最好在代码里同时处理两种情况:

if (c >= '0' && c <= '9') val = c - '0'; else if (c >= 'A' && c <= 'F') val = c - 'A' + 10; else val = c - 'a' + 10;

4.4 常见问题速查表

问题现象可能原因解决方法
答案错误,结果多一位二进制补齐方向错误检查是否在左边补零
答案错误,结果少一位前导零去掉太多保留至少一位数字
运行错误数组越界或栈溢出用 string 替代字符数组
超时用了低效的字符串拼接改用 += 或预分配空间
输入 0 时输出为空前导零逻辑没处理边界特判全零情况

4.5 一个容易被忽略的优化点

如果你用 C++ 的cin和cout,在数据量大的时候可能会比较慢。可以在main函数开头加上这两行:

ios::sync_with_stdio(false); cin.tie(0);

这两行代码的作用是关闭 C++ 标准流和 C 标准流的同步,可以显著提升输入输出速度。对于这道题来说,输入可能有 100000 个字符,加上这两行能省下不少时间。

5. 从这道题延伸出去的东西

5.1 任意进制转换的通用思路

这道题虽然只涉及十六进制到八进制,但它的思路可以推广到任意进制转换。核心逻辑是:如果两个进制的基数都是 2 的幂次,就用二进制做桥梁;否则,用“短除法”或者“大数除法”来处理。

比如十进制转二进制,如果数很大,就需要用字符串模拟除法,每次除以 2 取余数。这个思路在“大数进制转换”类题目里非常常见。

5.2 字符串模拟大数运算的通用套路

这道题本质上是一道“字符串模拟”题。在竞赛里,凡是遇到超出整数范围的运算,基本都要用字符串来模拟。常见的操作包括:大数加法、大数减法、大数乘法、大数除法、大数取模。这些操作的实现思路都是类似的:把数字当成字符数组,逐位处理,处理进位和借位。

我个人的经验是:先把大数运算的模板写熟,遇到类似题目直接套模板,能省下大量时间。比如大数加法,核心就是一个循环加一个进位变量,十行代码就能搞定。

5.3 位运算思维在进制转换中的应用

这道题里用到的“每 4 位二进制对应 1 位十六进制”和“每 3 位二进制对应 1 位八进制”,本质上是一种位运算思维。在底层开发里,这种思维非常常见。比如处理颜色值的时候,RGB 三个通道各占 8 位,拼在一起就是一个 24 位的整数。又比如网络协议里的字段,经常需要按位来解析。

掌握这种“按位分组”的思维方式,对你理解计算机底层原理会有很大帮助。

5.4 如果题目改成十六进制转十进制怎么办

如果题目要求十六进制转十进制,而且数很大,那就不能用二进制桥梁了,因为十进制不是 2 的幂次。这时候需要用“大数乘法加法”的思路:遍历十六进制每一位,把当前结果乘以 16 再加上当前位的值。乘法和加法都要用字符串模拟。

这个思路比二进制桥梁复杂一些,但也是必须掌握的技能。我建议你把这两种方案都实现一遍,对比一下差异,理解会更深刻。

5.5 实际工程中的应用场景

进制转换在实际工程里用得非常多。比如:

  • 嵌入式开发:寄存器配置经常用十六进制表示,但调试输出可能用八进制或二进制。
  • 网络编程:IP 地址、端口号、协议字段经常需要在不同进制之间转换。
  • 加密算法:哈希值、密钥、签名经常以十六进制字符串形式出现,但内部运算可能用二进制。
  • 文件格式解析:很多二进制文件格式的字段是用十六进制定义的,解析时需要转换成可读的十进制或八进制。

所以这道题虽然是一道竞赛题,但它训练的技能在实际工作中是有真实价值的。你花时间把它吃透,绝对不亏。

5.6 一个值得思考的变体

如果题目改成“八进制转十六进制”,思路是一样的,只是方向反过来:先把八进制展开成二进制(每 1 位八进制对应 3 位二进制),然后补齐到 4 的倍数,再每 4 位一组转成十六进制。你可以自己试着实现一下,看看能不能一次通过。

我在实际教学中发现,很多同学做完十六进制转八进制之后,反过来做八进制转十六进制还是会卡住。原因是对“补齐”这一步的理解不够深入。补齐的本质是对齐分组边界,只要想清楚这一点,正反两个方向都不会出错。

5.7 关于性能的进一步思考

这道题的时间限制通常是 1 秒,内存限制通常是 256MB。对于长度 100000 的输入,O(n) 的算法完全够用。但如果你用了 O(n²) 的算法,比如每次拼接字符串都重新分配内存,就可能超时。

我实测下来,用string的+=操作符,处理 100000 个字符的输入,耗时大约在 10ms 以内,远远低于时间限制。所以不需要过度优化,把逻辑写清楚才是最重要的。

如果你真的想追求极致性能,可以用char数组手动管理内存,避免string的动态扩容开销。但在竞赛场景下,这种优化通常没有必要,除非你遇到了非常苛刻的时间限制。

5.8 调试技巧分享

最后分享一个我常用的调试技巧:对于进制转换类题目,先用小数据手动验证,再用大数据测试性能。

比如你可以用1、A、FF、1000这种小数据来验证逻辑是否正确。确认小数据没问题之后,再生成一个长度为 100000 的随机十六进制字符串来测试性能。这样能快速定位问题是出在逻辑上还是出在性能上。

另外,如果你用的是本地编译器,可以在代码里加一些临时的输出语句,把中间结果打印出来。比如把二进制字符串打印出来看看长度对不对,把补齐后的字符串打印出来看看分组对不对。这些临时输出在提交前记得删掉,否则会影响评测结果。

我在实际比赛中见过不少同学因为忘记删调试输出而丢分,这个坑一定要避开。

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

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

立即咨询