1. 进制转换3(清华大学/厦门大学机试题)解析
在计算机科学和编程竞赛中,进制转换是一个基础但极其重要的概念。这道来自清华大学和厦门大学的机试题"进制转换3"考察了学生对不同进制数相互转换的理解和实现能力。作为参加过多次ACM竞赛的老手,我发现很多初学者在这个看似简单的问题上容易栽跟头。
进制转换的核心在于理解"位权"的概念。每个数字在不同位置代表不同的值,这个值等于数字本身乘以基数的位置次方。比如十进制数123,1的位权是100(10^2),2的位权是10(10^1),3的位权是1(10^0)。
1.1 问题描述与要求分析
根据题目名称和相关信息推断,这道"进制转换3"应该是要求实现一个支持任意进制(2-36)相互转换的程序。与简单的十进制转二进制或十六进制不同,它可能需要处理:
- 输入一个M进制的数
- 将其转换为N进制的数
- 输出转换结果
其中M和N都是在2到36之间的整数,因为超过36后,我们缺乏足够的符号来表示数字(通常0-9用数字表示,10-35用字母A-Z表示)。
1.2 进制转换的基本原理
进制转换通常有两种基本方法:
-
直接转换法:适用于某些特定进制间的转换,如二进制和八进制、二进制和十六进制之间的转换,因为它们之间存在幂次关系(8=2^3, 16=2^4)。
-
通用转换法(通过十进制中转):
- 先将M进制数转换为十进制
- 再将十进制数转换为N进制
对于这道机试题,显然需要采用通用转换法,因为M和N都是任意给定的。
1.2.1 M进制转十进制
转换公式为:
decimal = Σ (digit_value × M^position)
例如,将二进制数1101转换为十进制:
1×2^3 + 1×2^2 + 0×2^1 + 1×2^0 = 8 + 4 + 0 + 1 = 13
1.2.2 十进制转N进制
采用"除N取余法",将十进制数不断除以N,记录余数,直到商为0,然后将余数倒序排列。
例如,将十进制数13转换为二进制:
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
倒序排列余数得到1101
1.3 边界条件与特殊处理
在实际编程实现时,需要考虑以下特殊情况:
-
负数处理:题目是否要求支持负数的转换?通常机试题会明确说明,如果没有说明,可以假设只处理非负数。
-
字母大小写:对于10-35的数字,是用大写字母A-Z表示还是小写a-z表示?题目应该会明确,如果没有,建议统一使用大写。
-
前导零:转换后的结果是否允许有前导零?通常应该去除前导零。
-
非法输入:如果输入的M进制数包含非法字符(比如二进制数中出现'2'),应该如何处理?可以返回错误或抛出异常。
-
零的处理:输入为零时,无论什么进制输出都应该是"0"。
2. 算法设计与实现
2.1 数据结构选择
对于这个问题,最合适的数据结构是字符串,因为:
- 输入的数字可能很大,超过基本数据类型的表示范围(如C++的long long)
- 需要处理字母表示的数字(A-Z)
- 方便进行逐字符处理和拼接
2.2 核心算法步骤
2.2.1 M进制转十进制实现
cpp复制long long mToDecimal(const string& num, int M) {
long long decimal = 0;
for (char c : num) {
int digit;
if (c >= '0' && c <= '9') digit = c - '0';
else if (c >= 'A' && c <= 'Z') digit = 10 + c - 'A';
else if (c >= 'a' && c <= 'z') digit = 10 + c - 'a';
else throw invalid_argument("Invalid character in input number");
if (digit >= M) throw invalid_argument("Digit exceeds base M");
decimal = decimal * M + digit;
}
return decimal;
}
2.2.2 十进制转N进制实现
cpp复制string decimalToN(long long decimal, int N) {
if (decimal == 0) return "0";
string result;
while (decimal > 0) {
int remainder = decimal % N;
char digit;
if (remainder < 10) digit = '0' + remainder;
else digit = 'A' + remainder - 10;
result.push_back(digit);
decimal /= N;
}
reverse(result.begin(), result.end());
return result;
}
2.2.3 完整转换函数
cpp复制string convertBase(const string& num, int M, int N) {
// 先转换为十进制
long long decimal = mToDecimal(num, M);
// 再转换为N进制
return decimalToN(decimal, N);
}
2.3 大数处理优化
上述实现有一个限制:当输入的M进制数很大时,转换为十进制可能会溢出long long的范围。对于机试题,通常给定的测试用例不会太大,但如果是实际应用或某些极端测试用例,我们需要处理大数。
2.3.1 大数表示
可以使用字符串或数组来表示大数,实现大数的加减乘除运算。不过这会显著增加代码复杂度。
2.3.2 直接转换法(不经过十进制)
更高效的方法是直接从M进制转换为N进制,而不经过十进制中转。这需要实现大数的除法和取模运算。
cpp复制string directBaseConversion(const string& num, int M, int N) {
string result;
string quotient = num;
while (!quotient.empty()) {
string newQuotient;
int remainder = 0;
// 模拟除法过程:quotient / N
for (char c : quotient) {
int digit;
if (c >= '0' && c <= '9') digit = c - '0';
else if (c >= 'A' && c <= 'Z') digit = 10 + c - 'A';
else if (c >= 'a' && c <= 'z') digit = 10 + c - 'a';
else throw invalid_argument("Invalid character in input number");
int temp = remainder * M + digit;
if (!newQuotient.empty() || temp / N != 0) {
char qDigit;
if (temp / N < 10) qDigit = '0' + temp / N;
else qDigit = 'A' + (temp / N) - 10;
newQuotient.push_back(qDigit);
}
remainder = temp % N;
}
// 处理余数
char rDigit;
if (remainder < 10) rDigit = '0' + remainder;
else rDigit = 'A' + remainder - 10;
result.push_back(rDigit);
quotient = newQuotient;
}
reverse(result.begin(), result.end());
// 去除前导零
size_t nonZero = result.find_first_not_of('0');
if (nonZero != string::npos) {
return result.substr(nonZero);
}
return "0";
}
这种方法虽然复杂,但可以处理任意大的数字,不会受到数据类型范围的限制。
3. 测试用例设计
为了验证我们的进制转换程序的正确性,需要设计全面的测试用例:
3.1 常规测试用例
- 二进制转十进制:"1010" (2) → "10" (10)
- 十进制转十六进制:"255" (10) → "FF" (16)
- 八进制转二进制:"17" (8) → "1111" (2)
- 十六进制转十进制:"A1" (16) → "161" (10)
- 三进制转五进制:"210" (3) → "102" (5)
3.2 边界测试用例
- 最小进制2:"1" (2) → "1" (10)
- 最大进制36:"Z" (36) → "35" (10)
- 零测试:"0" (任意进制) → "0" (任意进制)
- 单个数字测试:"9" (10) → "9" (10)
- 大数测试:"100000000000000000000" (10) → "5F5E100" (16)
3.3 异常测试用例
- 非法字符:"12G" (16) → 应该报错
- 数字超出进制范围:"3" (2) → 应该报错
- 进制超出范围:"10" (37) → 应该报错
- 空字符串:"" (10) → 应该报错
- 负数字:"-10" (10) → 根据题目要求处理
4. 性能优化与技巧
4.1 预处理字符转换
可以预先建立字符到数字和数字到字符的映射表,避免重复计算:
cpp复制int charToDigit(char c) {
static const unordered_map<char, int> map = {
{'0',0},{'1',1},{'2',2},{'3',3},{'4',4},{'5',5},{'6',6},{'7',7},{'8',8},{'9',9},
{'A',10},{'B',11},{'C',12},{'D',13},{'E',14},{'F',15},{'G',16},{'H',17},{'I',18},
{'J',19},{'K',20},{'L',21},{'M',22},{'N',23},{'O',24},{'P',25},{'Q',26},{'R',27},
{'S',28},{'T',29},{'U',30},{'V',31},{'W',32},{'X',33},{'Y',34},{'Z',35},
{'a',10},{'b',11},{'c',12},{'d',13},{'e',14},{'f',15},{'g',16},{'h',17},{'i',18},
{'j',19},{'k',20},{'l',21},{'m',22},{'n',23},{'o',24},{'p',25},{'q',26},{'r',27},
{'s',28},{'t',29},{'u',30},{'v',31},{'w',32},{'x',33},{'y',34},{'z',35}
};
auto it = map.find(c);
if (it == map.end()) throw invalid_argument("Invalid character");
return it->second;
}
char digitToChar(int d) {
static const char chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if (d < 0 || d >= 36) throw invalid_argument("Digit out of range");
return chars[d];
}
4.2 去除前导零的优化
在直接转换法中,我们可以在生成结果时就避免前导零,而不是最后再处理:
cpp复制// 在directBaseConversion的除法循环中修改:
if (!newQuotient.empty() || temp / N != 0) { // 只有商不为零时才添加字符
newQuotient.push_back(digitToChar(temp / N));
}
4.3 输入验证
在开始转换前,应该先验证输入的有效性:
cpp复制void validateInput(const string& num, int M, int N) {
if (M < 2 || M > 36 || N < 2 || N > 36) {
throw invalid_argument("Base must be between 2 and 36");
}
if (num.empty()) {
throw invalid_argument("Empty input number");
}
for (char c : num) {
int d = charToDigit(c);
if (d >= M) {
throw invalid_argument("Digit exceeds source base");
}
}
}
5. 常见错误与调试技巧
在实现进制转换时,容易犯以下错误:
-
字母大小写混淆:忘记处理小写字母或大小写不统一。建议统一转换为大写或小写后再处理。
-
余数顺序错误:在十进制转N进制时,忘记最后要反转余数顺序。
-
零的处理不当:特别是输入为零时,可能返回空字符串而不是"0"。
-
前导零问题:转换后的结果包含不必要的前导零。
-
边界溢出:使用直接转换法时,中间结果可能溢出整数范围。
调试技巧:
-
打印中间结果:在转换过程中打印关键的中间变量,如每次除法的商和余数。
-
单元测试:为每个辅助函数(如charToDigit、mToDecimal等)编写独立的测试用例。
-
使用已知结果验证:先用简单的例子手动计算预期结果,再与程序输出对比。
-
逐步验证:先确保M进制转十进制正确,再验证十进制转N进制正确,最后组合起来。
-
大数测试:特别测试大数输入,确保不会溢出或产生错误结果。
