news 2026/10/17 10:03:31

蓝桥杯BASIC-12十六进制转八进制:大数进制转换的字符串分组解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
蓝桥杯BASIC-12十六进制转八进制:大数进制转换的字符串分组解法

蓝桥杯练习系统基础练习里的“BASIC-12 十六进制转八进制”,是我带过好几届同学刷题时绕不开的一道题。放在“基础练习”这个标签下,它看上去平平无奇,打开题目界面,描述只有短短几行,很多人心里先松一口气:进制转换,不是送分题吗?结果一提交,问题就来了——要么用错类型直接溢出,要么写出来的代码只够应付小数据,要么被输出的前导0折腾到怀疑人生。这道题真正想考察的,不是你会不会查表,而是你对进制本质的理解,以及面对“数据范围远超常规”时,敢不敢跳出惯性思维。我打算把这道题的完整拆解、实测代码和踩坑记录都整理出来,给正在刷题或者准备比赛的同学一份能直接用的参考。

1. 题目到底在考什么:拆解 BASIC-12 的隐含要求

1.1 从题目描述说起

题目内容很简单:输入一个十进制整数 n,表示接下来有 n 行,每行是一个十六进制数,要求把每个十六进制数转换成对应的八进制数输出。单看这两句话,你会觉得这是蓝桥杯入门级别的题,毕竟进制转换在人教版教材里就有,连计算机基础课都会讲。

但注意最后一句话:“每个十六进制数长度不超过 100000 位。”这句话才是整个题目的题眼。如果它只要求输入几个正常的十六进制数,比如“A5F”或者“1A2B3C”,那用Long.parseLong或者strtol都能轻松应付。可 100000 位是什么概念?就算全是最小的十六进制位,它的数值范围也远远超出了任何 64 位整数能表示的上限,64位整数最多也就表示 16 个十六进制位。这意味着你根本没机会把这个数读进一个整数变量里,只能把它当字符串处理。

这一下,题目就从“会不会查表”升级成了“有没有处理大数的思维”。

1.2 隐藏的深水区:为什么不能用常规进制转换函数

很多同学的第一反应是找现成函数:C++里没有直接把十六进制字符串转成八进制字符串的标准库函数,只有stoi、stol这类先转成整数的东西,而它们到 64 位就到头了。Java里虽然有个BigInteger,可以把任意长度的十六进制字符串转成十进制整数再转八进制,但这题放在“基础练习”里,目的绝不是让你打卡式秒过——用BigInteger当然能过,可它恰恰避开了题目真正想让你练的东西:手工处理大数、字符串、位模式之间的关系。

而且BigInteger也不是完全没有坑:100000 位十六进制换算成二进制大约是 400000 位,也就是 50KB 级别的整数,BigInteger能处理,但如果你是在 C/C++ 环境里考试,根本没有这个可依赖的现成大数类,所以手写算法是必须掌握的基本功。

注意:蓝桥杯的练习系统和正式比赛是同一套环境,做题时编译器不会给你任何“额外大数库”,能靠的只有语言自身特性和你自己的代码。

2. 先搞懂数学原理:16 和 8 之间为什么隔着个二进制

2.1 从“进制”的本质说起

我们在十进制里数数,满十进一;在十六进制里满十六进一;在八进制里满八进一。进制不同,本质上是对同一个数值的不同“记法”。理论上,任何进制之间都能互转,但互转的方式不一样,代价也不一样。十进制转二进制要不断除 2 取余,二进制转八进制却只要三位一组地分段,原因在于:8 是 2 的三次方,16 是 2 的四次方。

因为 16 = 2^4,所以十六进制的每一位,恰好都对应二进制的四位,不多不少。因为 8 = 2^3,所以二进制的每三位,恰好对应八进制的一位,也是不多不少。进制间有这种 2 的幂次关系时,转换就完全不需要做乘除法,只需要做“查表”加“分组”的字符串操作,这是整道题最核心的数学背景。

2.2 8421 法:十六进制到二进制的快捷通道

先把十六进制的每个字符展开成 4 位二进制。方法很简单:每一位对应 8、4、2、1 四个权值,加起来等于该位的值。比如字符 A,A 代表 10,拆成 8 + 0 + 2 + 0,所以二进制就是 1010。字符 F,代表 15,拆成 8 + 4 + 2 + 1,二进制就是 1111。数字 3 就是 0011。

我把完整的映射表列在这里,后面写代码时要反复用:

十六进制字符十进制值二进制展开(4位)
000000
110001
220010
330011
440100
550101
660110
770111
881000
991001
A101010
B111011
C121100
D131101
E141110
F151111

以输入样例“39”为例,3 展开成 0011,9 展开成 1001,拼起来就是二进制串 00111001,也就是十进制的 57。

2.3 二进制到八进制的三位分组

二进制转八进制的规则是从右往左,每三位一组。为什么是从右往左?因为八进制的最低位对应三位二进制的最低位,进位关系决定高位在前、低位在后。比如 111001,从右往左分组是:001 和 111 两组?不对,111001 从最右边开始:第一个三位是 001,左边剩下 111,所以分组结果是“111 001”。每一组分别算成十进制:111 是 7,001 是 1,拼在一起就是八进制数 71。这样 39(十六进制)就变成了 71(八进制),完全正确。

如果二进制串的长度不是 3 的倍数怎么办?从左端补 0。比如只有一个十六进制字符 F,展开是 1111,四位不是三的倍数,就在最左边补两个 0,变成 001111,再分成 001 和 111,得到八进制 17。补 0 是因为高位补 0 不改变数值大小,只是把位数凑整齐,方便分组。

3. 两条技术路线:暴力转换法与大数分组法

3.1 路线一:用 BigInteger 直接转(能过,但别依赖)

Java 里确实有一行秒杀的方案:

System.out.println(new BigInteger(hex, 16).toString(8));

BigInteger接收一个字符串和基数,把它解析成一个大整数,再输出成指定基数的字符串。这行代码能处理 100000 位十六进制数,实测不会超时,因为 JVM 内置的大整数运算做了很多底层优化,对于 10 万位的规模仍然在可接受范围内。

但我不推荐把它作为主要学习路径。原因很简单:你会了这个,说明你“会用库”,而不是“会这道题”。如果比赛环境不是 Java,或者题目稍作变化改成二进制转十进制,BigInteger也一样救不了你。另外,BigInteger内部是 32 位为单位存储的,转进制时要做大整数除法,本质上比字符串分组法更重,只是常数优化让它看起来还行。

3.2 路线二:字符串映射 + 按位分组(推荐方案)

推荐的方案就是前面原理里推导出来的方法:十六进制字符串 -> 二进制字符串 -> 三位一组 -> 八进制字符串。整个过程的时间复杂度是 O(n),n 是十六进制字符串长度,因为这个过程只是逐字符展开和逐三位拼接,没有进位、没有递归、没有大整数除法。

这个方案还有两个好处:一是 C++、Java、Python 甚至任意语言都能轻松实现,二是过程中你能清晰看到进制之间的位模式关系,对后续学习位运算、IP地址换算、字符编码都有帮助。我个人判断,这道题放在“基础练习”里,就是希望你掌握这种字符串分组处理的能力,而不是单纯考一个 API 调用。

4. 完整代码实现:C++ 和 Java 双版本

4.1 C++ 实现

#include <iostream> #include <string> using namespace std; int main() { int n; cin >> n; while (n--) { string hex; cin >> hex; // 第一步:十六进制转二进制 string bin = ""; for (char c : hex) { switch (c) { case '0': bin += "0000"; break; case '1': bin += "0001"; break; case '2': bin += "0010"; break; case '3': bin += "0011"; break; case '4': bin += "0100"; break; case '5': bin += "0101"; break; case '6': bin += "0110"; break; case '7': bin += "0111"; break; case '8': bin += "1000"; break; case '9': bin += "1001"; break; case 'A': bin += "1010"; break; case 'B': bin += "1011"; break; case 'C': bin += "1100"; break; case 'D': bin += "1101"; break; case 'E': bin += "1110"; break; case 'F': bin += "1111"; break; } } // 第二步:左边补0,让二进制长度是3的倍数 int pad = (3 - bin.size() % 3) % 3; if (pad == 1) bin = "0" + bin; else if (pad == 2) bin = "00" + bin; // 第三步:每三位一组转八进制 string oct = ""; for (size_t i = 0; i < bin.size(); i += 3) { int val = (bin[i] - '0') * 4 + (bin[i+1] - '0') * 2 + (bin[i+2] - '0'); oct += char('0' + val); } // 第四步:去掉前导0 size_t pos = oct.find_first_not_of('0'); if (pos == string::npos) { cout << 0 << endl; } else { cout << oct.substr(pos) << endl; } } return 0; }

这段代码有几个细节值得说。switch 映射是整个程序的根基,写的时候最容易出错的点集中在 B、D、F 三个字符上:B 是 1011,D 是 1101,F 是 1111,字母 A 到 F 的映射别背混。第二步补 0 只可能补 1 个或 2 个,因为二进制长度对 3 取余只可能是 0、1、2。第三步中(bin[i] - '0') * 4 + (bin[i+1] - '0') * 2 + (bin[i+2] - '0')本质上是把三位二进制数当成一个 0 到 7 的值来算,这里用的是权值 4、2、1,和十进制转二进制时的 8421 是同一个道理。第四步去掉前导0 的时候要小心:如果整个八进制串全是 0,find_first_not_of('0')会返回string::npos,此时必须输出一个“0”,否则会输出空行。

4.2 Java 实现

import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); while (n-- > 0) { String hex = sc.next(); StringBuilder bin = new StringBuilder(); for (char c : hex.toCharArray()) { switch (c) { case '0': bin.append("0000"); break; case '1': bin.append("0001"); break; case '2': bin.append("0010"); break; case '3': bin.append("0011"); break; case '4': bin.append("0100"); break; case '5': bin.append("0101"); break; case '6': bin.append("0110"); break; case '7': bin.append("0111"); break; case '8': bin.append("1000"); break; case '9': bin.append("1001"); break; case 'A': bin.append("1010"); break; case 'B': bin.append("1011"); break; case 'C': bin.append("1100"); break; case 'D': bin.append("1101"); break; case 'E': bin.append("1110"); break; case 'F': bin.append("1111"); break; } } int pad = (3 - bin.length() % 3) % 3; if (pad == 1) bin.insert(0, '0'); else if (pad == 2) bin.insert(0, "00"); StringBuilder oct = new StringBuilder(); for (int i = 0; i < bin.length(); i += 3) { int val = (bin.charAt(i) - '0') * 4 + (bin.charAt(i + 1) - '0') * 2 + (bin.charAt(i + 2) - '0'); oct.append((char) ('0' + val)); } String res = oct.toString().replaceFirst("^0+", ""); if (res.isEmpty()) res = "0"; System.out.println(res); } } }

Java 版的整体思路完全一样,唯一值得强调的差异是必须用StringBuilder而不是直接对String做+=。字符串是不可变对象,每做一次拼接都会产生一个新的字符串,10 万位的输入意味着至少要几万次拼接,用String会反复创建对象,性能和内存都很难看;StringBuilder底层是可变字符数组,append 是平摊 O(1) 的操作,这一点是 Java 写大字符串处理时的基本素养。去前导 0 我用的是正则replaceFirst("^0+", ""),如果全为 0,替换结果为空串,再单独补成“0”。

4.3 代码走读与复杂度分析

拿输入FFFF来走一遍流程。FFFF展开成二进制是 16 个 1:1111111111111111,长度 16,对 3 取余是 1,所以左边补 2 个 0,变成 001111111111111111。再从前往后每三位分组:001 111 111 111 111 111。每一组转成十进制数:1 7 7 7 7 7,拼起来是 177777。整个过程中没有出现任何进位借位操作,因为二进制本身就是十六进制和八进制之间的天然桥梁。

复杂度上,十六进制长度为 n,二进制长度为 4n,八进制长度为 4k/3,整体空间复杂度 O(n),时间复杂度 O(n)。对于 100000 的上限,这个复杂度非常安全,实测在练习系统上运行时间在个位数毫秒级别,性能没有任何压力。反倒是那些试图先转十进制再转八进制、用普通整型运算的人,会在测试点直接爆掉——不是运行错误就是答案错误。

5. 踩坑实录:测了 30 组数据才发现的细节

5.1 前导 0 的坑:最容易被忽略的边界

第一组边界数据:输入0000。按照正常流程,展开二进制是 16 个 0,补 0 后分组,八进制串是0000,如果直接输出就会在第一个测试点翻车。正确结果是输出单个0。这个坑我见过不止一个同学踩,原因很简单:人脑处理十进制时默认不会写前导 0,但字符串处理时前导 0 就是实实在在的字符。

第二个边界数据:输入0001。期望输出是1,但如果你只在处理全 0 时特判,在普通情况下忘记删除前导 0,那么分组结果是000 000 000 001,转出来是0001,照样会出错。所以无论什么情况,去前导 0 这一步都不能省。

5.2 输出格式的坑:Presentation Error 也是错

蓝桥杯的判题系统对格式很敏感,多一个空行、少一个空行都会提示格式错误。我见过有人把while (n--)写成while (n) { ... n--; }时不小心多输出了一行,也见过有人在每行结束多加了一个空格。这道题的要求就是每个结果占一行,不要有任何多余字符。你在本地测试时用diff比对输出文件就能发现这种问题,但在练习系统里就直接报 Presentation Error。

5.3 性能与内存:别在小字符串上栽大跟头

100000 位十六进制转成二进制串是 400000 个字符,如果切换语言不规范,比如 Java 用String + String拼接,就可能触发大量字符串对象创建,导致时间超限。C++ 的+=在 C++11 之后通常有优化,但如果你非要用insert(0, ...)在头部反复插入,最坏情况下会退化成 O(n^2)。正确做法是在尾部拼接,最后只在开头补一次 0,或者干脆先算好总长度再一次性处理。

5.4 映射表的记忆技巧

实际写代码时不用死记硬背整张表,有一个推导方法:四位二进制的权值是 8、4、2、1,把十六进制字符对应的十进制数拆成这四个权值的和。A 是 10,10 = 8 + 2,所以是 1010;B 是 11,11 = 8 + 2 + 1,所以是 1011;C 是 12,12 = 8 + 4,所以是 1100;D 是 13,13 = 8 + 4 + 1,所以是 1101;E 是 14,14 = 8 + 4 + 2,所以是 1110;F 是 15,15 = 8 + 4 + 2 + 1,所以是 1111。数字 0 到 9 则是字面量转四位二进制,比如 5 = 4 + 1,就是 0101。现场推导比背表可靠得多,还不容易错。

6. 举一反三:从这道题延伸出去的技能点

6.1 分组转换法的迁移能力

“十六进制 -> 二进制 -> 八进制”这个方法,本质上利用了 2 的幂次关系。同理,八进制转十六进制也很简单:先把八进制每一位展开成 3 位二进制,再把这个二进制串从右往左每 4 位一组,转成十六进制。只要两个进制都是 2 的幂次,转换就能用“展开 + 分组”两个字符串操作完成,不需要任何算术运算。

比如八进制数 177777,每一位展开:1 -> 001,7 -> 111,7 -> 111,7 -> 111,7 -> 111,7 -> 111,拼起来是 001111111111111111,去掉前导 0 就是 1111111111111111,每 4 位一组:1111 1111 1111 1111,每个都是 F,得到 FFFF。正好和前面的例子互为逆过程。

6.2 对比:十六进制转十进制为何不能这样展开

如果把题目改成“十六进制转十进制”,10 不是 2 的幂次,你就没法用简单的三位分组或四位分组来完成。此时 100000 位的十六进制转换成十进制,必须依赖大数乘法,模拟竖式每一位乘以 16 然后累加,或者直接用内置大整数库。这也是为什么这道题偏偏是转八进制而不是转十进制——它设计得让你用位模式关系来解,而不是硬算乘除法。认识到这一层,你才算真正理解出题人的意图。

6.3 用 Python 写对拍脚本验证正确性

本地测试时,我强烈建议写一个 Python 脚本做对拍。Python 自带大整数和进制转换,三行就能求出标准答案:

def hex_to_oct(hex_str): return oct(int(hex_str, 16))[2:]

对拍流程有两种方式。一种是你把 C++ 程序编译好,Python 用subprocess调用它,给同一份输入然后对比输出;另一种是你先随机生成大量十六进制字符串,用 Python 算出期望结果存成文件,再和 C++ 程序的输出文件做diff。随机数据里要特意包含全 0、超长 AAAA、以 0 开头等边界情况,这样能快速找出前导 0 和映射错误。

7. 写在最后的一点心得

我刷了很多年的算法题,也帮不少同学看过代码,说实话,BASIC-12 这道题论难度排不上号,但它之所以能在“基础练习”里占一个位置,就是因为它在最简单的外壳下藏了一个“大数”内核。很多人贪图快,直接调用库函数转完交上去,代码只有一行,也能拿满分——这当然没问题,比赛就是看结果。但如果只是练手,我仍然建议你亲手写一遍字符串分组版本,哪怕只用一次,你也会对十六进制和二进制之间的 8421 关系有肌肉记忆。以后遇到 IP 地址换算、Modbus 协议报文解析、字符编码转换,这些看似八竿子打不着的场景里,你都会发现当年这道基础题教给你的东西一直在起作用。最后提醒一句:做题前一定先看清数据范围,它往往才是题目的真正难度所在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 10:03:15

Markdown中LaTeX公式渲染问题排查与跨平台兼容写法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 10:03:14

Zephyr内存池Memory Pools参数配置与踩坑实践:从k_mem_pool到sys_heap

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 10:02:05

xlrd/xlwt/xlutils三件套:Python处理旧版.xls文件的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 10:00:19

从业务问题到统计结论:数据分析思路的完整拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 9:59:31

MP2642GR-Z与PIC18F45K80实现双节锂电主动均衡设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华