- bitworld 的博客
【CSP-J初赛】进制转换
- 2025-7-28 16:07:26 @
进制
1. 什么是进制
我们日常生活中最熟悉的数是十进制数。数字 究竟代表什么?它并不仅仅是符号 , , 的简单排列,它的值是:
这种表示法被称为按权展开或位值表示法。其中,每个位置上的数字(数码)所代表的实际大小,取决于它所处的位置。每个位置都有一个关联的“权重”,这个权重是“基数”的幂。
- 数码 (Digit):在特定位置上使用的符号,例如在十进制中是 。
- 基数 (Base/Radix):每个数位上能使用的数码的个数。十进制的基数是 。
- 权 (Weight):每个位置的价值,是基数的整数次幂。从右往左,个位的权是 ,十位的权是 ,百位的权是 ,以此类推。
通用范式
这个概念可以推广到任意进制。对于一个 进制数,其基数为 。如果一个 进制数写作 ,其中 是该数在第 位上的数码(),那么它对应的十进制值 为:
$$N = d_{k-1} \times R^{k-1} + d_{k-2} \times R^{k-2} + \dots + d_1 \times R^1 + d_0 \times R^0 = \sum_{i=0}^{k-1} d_i \times R^i $$这个公式是所有进制转换的理论基石。
2. 常见进制
在计算机科学中,除了十进制,我们还频繁使用以下几种进制:
-
二进制 (Binary, Base-2):基数为 ,数码仅为 和 。这是计算机的母语,因为计算机硬件中的晶体管等基本单元只有“开”和“关”两种状态,天然对应 和 。
-
八进制 (Octal, Base-8):基数为 ,数码为 。在早期,它被用作二进制的一种紧凑表示法。
-
十六进制 (Hexadecimal, Base-16):基数为 。由于数码需要 个,除了 之外,还引入了字母来表示 及以上的数码:
- 十六进制是目前最常用的二进制紧凑表示法,广泛应用于内存地址、颜色代码等场景。
3. 核心转换方法
所有进制转换问题都可以归结为两种基本操作。
3.1 任意进制转十进制
方法:按权展开求和
直接应用我们在第一节中建立的通用范式 。
示例 1:二进制转十进制 将 转换为十进制。 $N = 1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = 8 + 4 + 0 + 1 = 13$。 所以 。
示例 2:十六进制转十进制 将 转换为十进制。(记住 ) $N = 2 \times 16^2 + 10 \times 16^1 + 15 \times 16^0 = 2 \times 256 + 10 \times 16 + 15 \times 1 = 512 + 160 + 15 = 687$。 所以 。
在程序实现中,一个更高效的计算方式是使用秦九韶算法 (Horner's method)。对于数 ,其值为:
$$N = ((...((d_{k-1} \times R + d_{k-2}) \times R + d_{k-3})...)\times R + d_0) $$这个过程避免了计算高次幂,只需进行一系列的乘法和加法。
代码实现:任意进制转十进制
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
// 题意:将一个b进制的数s(字符串形式)转换为十进制数。
ll toDec(string s, int b) {
ll res = 0;
for (char c : s) {
int d;
if (c >= '0' && c <= '9') {
d = c - '0';
} else {
d = c - 'A' + 10;
}
res = res * b + d; // 秦九韶算法核心
}
return res;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
string s;
int b;
cin >> s >> b;
cout << toDec(s, b) << endl;
return 0;
}
- 复杂度分析:
- 时间复杂度: ,其中 是输入字符串
s
的长度。我们需要遍历一次字符串。 - 空间复杂度: ,除了存储输入和结果外,只使用了常数个额外变量。
- 时间复杂度: ,其中 是输入字符串
3.2 十进制转任意进制
方法:除基取余法(短除法)
将一个十进制数 转换为 进制数,过程如下:
- 用 除以基数 ,得到商 和余数 。这个余数 就是 进制数的最右边一位(第 位)。
- 用商 代替 ,重复步骤 1,得到的余数是下一位(第 位)。
- 持续这个过程,直到商为 。
- 将所有得到的余数逆序排列,即为转换结果。
示例:十进制转二进制 将 转换为二进制。
商为 ,停止。将余数从下往上读:。 所以 。
代码实现:十进制转任意进制
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
// 题意:将一个十进制数n转换为b进制数,并以字符串形式输出。
string fromDec(ll n, int b) {
if (n == 0) return "0"; // 特殊处理0
string res = "";
const string tbl = "0123456789ABCDEF"; // 数码查找表
while (n > 0) {
res += tbl[n % b]; // 取余得到当前位,并追加
n /= b; // 更新n为商
}
reverse(res.begin(), res.end()); // 结果需要反转
return res;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n;
int b;
cin >> n >> b;
cout << fromDec(n, b) << endl;
return 0;
}
- 复杂度分析:
- 时间复杂度: ,其中 是输入的十进制数,
b
是目标基数。循环的次数取决于 被b
整除多少次才能变为 。 - 空间复杂度: ,用于存储结果字符串。
- 时间复杂度: ,其中 是输入的十进制数,
4. 特殊进制转换的捷径
当转换的两个基数之间存在幂关系时(如 和 ),存在非常高效的快捷方法。最典型的就是二进制、八进制和十六进制之间的转换。
- ,所以一位八进制数码正好对应三位二进制数码。
- ,所以一位十六进制数码正好对应四位二进制数码。
方法:分组转换
4.1 二进制 十六进制
- 二进制转十六进制:从二进制数的右侧开始,每四位分为一组(不足四位的在左侧补 ),然后将每组独立转换为一位十六进制数码。
示例:转换 。
- 分组: (左侧补了两个 )
- 转换每组:
- 合并:
- 十六进制转二进制:将每一位十六进制数码独立转换为四位二进制数码(不足四位要在左侧补 ),然后拼接。
示例:转换 。
- 转换每位:
- 拼接:
4.2 二进制 八进制
转换规则与十六进制完全相同,只是分组的长度变为三位。
- 二进制转八进制:每三位一组。 示例:转换 。
- 分组:
- 转换:
- 合并:
- 八进制转二进制:每位转三位。 示例:转换 。
- 转换:, ,
- 拼接:
5. 任意进制之间的转换
对于两个任意进制(例如 进制转 进制)之间的转换,最通用、最不易出错的方法是:
使用十进制作为“桥梁”
- 将源进制数(如 进制)使用“按权展开法”转换为十进制。
- 将得到的十进制数使用“除基取余法”转换为目标进制(如 进制)。
代码实现:任意进制到任意进制
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
// 将b进制数s转为十进制
ll toDec(string s, int b) {
ll res = 0;
for (char c : s) {
int d;
if (c >= '0' && c <= '9') d = c - '0';
else d = c - 'A' + 10;
res = res * b + d;
}
return res;
}
// 将十进制数n转为b进制
string fromDec(ll n, int b) {
if (n == 0) return "0";
string res = "";
const string tbl = "0123456789ABCDEF";
while (n > 0) {
res += tbl[n % b];
n /= b;
}
reverse(res.begin(), res.end());
return res;
}
// 题意:将b1进制的数s,转换为b2进制的数。
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int b1, b2;
string s;
cin >> b1 >> s >> b2;
// 步骤1: 源进制 -> 十进制
ll dec_val = toDec(s, b1);
// 步骤2: 十进制 -> 目标进制
string ans = fromDec(dec_val, b2);
cout << ans << endl;
return 0;
}
- 复杂度分析:
- 设输入字符串长度为 ,数值大小为 。
toDec
复杂度为 。fromDec
复杂度为 。- 由于 的值与 和 相关 (),。
- 总时间复杂度: 。
- 空间复杂度: ,即 ,主要由最终结果字符串占据。
6. 例题
6.1 选择/判断题
这类题通常考察基本概念和快速手动计算能力。
例题:在下列数值中,最大的是哪一个? A. B. C. D.
解法:将所有选项都转换为十进制进行比较,这是最可靠的方法。 A. $(100)_{16} = 1 \times 16^2 + 0 \times 16^1 + 0 \times 16^0 = 256$ B. 已是十进制,值为 C. $(377)_8 = 3 \times 8^2 + 7 \times 8^1 + 7 \times 8^0 = 3 \times 64 + 56 + 7 = 192 + 56 + 7 = 255$ D. 可以利用特殊转换。它比 (即 )小 。具体地,它是 。或者直接按权展开计算。 比较 ,最大的是 。答案:A。
6.2 程序阅读题
这类题会给出一个实现进制转换的代码片段,要求你分析其功能或预测输出。
例题:分析以下C++函数,f(21, 8)
的输出是什么?
void f(int n, int r) {
if (n == 0) return;
f(n / r, r);
cout << n % r;
}
解法:这是一个递归实现的十进制转任意进制的函数。我们来追踪执行过程:
f(21, 8)
调用f(21/8, 8)
即f(2, 8)
,然后等待打印21 % 8
(即5
)。f(2, 8)
调用f(2/8, 8)
即f(0, 8)
,然后等待打印2 % 8
(即2
)。f(0, 8)
直接返回。f(2, 8)
的调用栈返回,打印2
。f(21, 8)
的调用栈返回,打印5
。 由于递归调用在打印之前,这个过程实际上是正序生成了余数,但因为函数返回的顺序是从内到外,所以打印的顺序是2
然后是5
。 最终输出是25
。这恰好是 转换成八进制的结果 。
6.3 程序补全题
这类题会提供一个不完整的代码框架,要求你填入关键部分。
例题:补全以下将十六进制字符串转为十进制整数的函数。
ll hexToDec(string s) {
ll res = 0;
for (char c : s) {
int d;
if (c >= '0' && c <= '9') {
d = c - '0';
} else if (c >= 'A' && c <= 'F') {
// [--- 待填充代码 ---]
} else {
return -1; // 非法字符
}
res = res * 16 + d;
}
return res;
}
解法:需要填充的部分是将字符 'A' 到 'F' 转换为对应的数值 10 到 15。字符 'A' 的ASCII码比 '0' 大,但它们不连续。一个稳健的转换方式是利用 'A' 本身的ASCII码。
d
应该等于c - 'A' + 10
。- 当
c
是 'A' 时,'A' - 'A' + 10 = 10
。 - 当
c
是 'B' 时,'B' - 'A' + 10 = 11
。 - ...
- 当
c
是 'F' 时,'F' - 'A' + 10 = 15
。
- 当
所以,填充的代码是 d = c - 'A' + 10;
。这考察了对字符编码和进制转换逻辑细节的掌握。