在计算机科学中,数字可以用不同的进制表示。我们最熟悉的是十进制,但计算机内部使用二进制,网络协议可能使用十六进制,还有一些特殊应用会使用其他进制。在 Exercism 的 “all-your-base” 练习中,我们将实现一个通用的进制转换器,它可以在任意进制之间转换数字。这不仅能帮助我们理解数字的表示方式,还能深入学习 Rust 中的错误处理和算法实现。

问题描述

练习要求我们实现一个能够在任意进制之间转换数字的函数。让我们先看看函数签名:

#[derive(Debug, PartialEq)]
pub enum Error {
    InvalidInputBase,
    InvalidOutputBase,
    InvalidDigit(u32),
}

///
/// Convert a number between two bases.
///
/// A number is any slice of digits.
/// A digit is any unsigned integer (e.g. u8, u16, u32, u64, or usize).
/// Bases are specified as unsigned integers.
///
/// Return an `Err(.)` if the conversion is impossible.
/// The tests do not test for specific values inside the `Err(.)`.
///
///
/// You are allowed to change the function signature as long as all test still pass.
///
///
/// Example:
/// Input
///   number: &[4, 2]
///   from_base: 10
///   to_base: 2
/// Result
///   Ok(vec![1, 0, 1, 0, 1, 0])
///
/// The example corresponds to converting the number 42 from decimal
/// which is equivalent to 101010 in binary.
///
///
/// Notes:
///  * The empty slice ( "[]" ) is equal to the number 0.
///  * Never output leading 0 digits, unless the input number is 0, in which the output must be `[0]`.
///    However, your function must be able to process input with leading 0 digits.
///
pub fn convert(number: &[u32], from_base: u32, to_base: u32) -> Result<Vec<u32>, Error> {
    unimplemented!(
        "Convert {:?} from base {} to base {}",
        number,
        from_base,
        to_base
    )
}

我们需要实现一个函数,它能够:

  1. 将一个数字从[from_base]进制转换为[to_base]进制
  2. 正确处理各种边界情况
  3. 在输入无效时返回适当的错误

进制转换原理

在不同进制之间转换数字需要两个步骤:

  1. 将输入数字从源进制转换为十进制
  2. 将十进制数字转换为目标进制

例如,将二进制数 101010 转换为十进制:
1×2⁵ + 0×2⁴ + 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 32 + 0 + 8 + 0 + 2 + 0 = 42

然后将十进制数 42 转换为二进制:
42 ÷ 2 = 21 余 0
21 ÷ 2 = 10 余 1
10 ÷ 2 = 5 余 0
5 ÷ 2 = 2 余 1
2 ÷ 2 = 1 余 0
1 ÷ 2 = 0 余 1

从下往上读取余数:101010

实现步骤

1. 输入验证

首先我们需要验证输入参数的有效性:

// 检查进制是否有效(必须大于1)
if from_base < 2 {
    return Err(Error::InvalidInputBase);
}

if to_base < 2 {
    return Err(Error::InvalidOutputBase);
}

// 检查每个数字是否有效(必须小于进制基数)
for &digit in number {
    if digit >= from_base {
        return Err(Error::InvalidDigit(digit));
    }
}

2. 处理特殊情况

我们需要处理一些特殊情况:

// 空数组表示数字0
if number.is_empty() {
    return Ok(vec![0]);
}

// 全为0的数组也表示数字0
if number.iter().all(|&d| d == 0) {
    return Ok(vec![0]);
}

3. 进制转换算法

完整的进制转换实现:

#[derive(Debug, PartialEq)]
pub enum Error {
    InvalidInputBase,
    InvalidOutputBase,
    InvalidDigit(u32),
}

///
/// Convert a number between two bases.
///
/// A number is any slice of digits.
/// A digit is any unsigned integer (e.g. u8, u16, u32, u64, or usize).
/// Bases are specified as unsigned integers.
///
/// Return an `Err(.)` if the conversion is impossible.
/// The tests do not test for specific values inside the `Err(.)`.
///
///
/// You are allowed to change the function signature as long as all test still pass.
///
///
/// Example:
/// Input
///   number: &[4, 2]
///   from_base: 10
///   to_base: 2
/// Result
///   Ok(vec![1, 0, 1, 0, 1, 0])
///
/// The example corresponds to converting the number 42 from decimal
/// which is equivalent to 101010 in binary.
///
///
/// Notes:
///  * The empty slice ( "[]" ) is equal to the number 0.
///  * Never output leading 0 digits, unless the input number is 0, in which the output must be `[0]`.
///    However, your function must be able to process input with leading 0 digits.
///
pub fn convert(number: &[u32], from_base: u32, to_base: u32) -> Result<Vec<u32>, Error> {
    // 验证进制
    if from_base < 2 {
        return Err(Error::InvalidInputBase);
    }
    
    if to_base < 2 {
        return Err(Error::InvalidOutputBase);
    }
    
    // 验证数字
    for &digit in number {
        if digit >= from_base {
            return Err(Error::InvalidDigit(digit));
        }
    }
    
    // 处理空数组和全零数组
    if number.is_empty() || number.iter().all(|&d| d == 0) {
        return Ok(vec![0]);
    }
    
    // 第一步:将数字从 from_base 转换为十进制
    let mut value = 0u32;
    for &digit in number {
        value = value.checked_mul(from_base)
            .ok_or(Error::InvalidDigit(digit))?
            .checked_add(digit)
            .ok_or(Error::InvalidDigit(digit))?;
    }
    
    // 第二步:将十进制数转换为 to_base
    let mut digits = Vec::new();
    let mut n = value;
    
    while n > 0 {
        digits.push(n % to_base);
        n /= to_base;
    }
    
    // 由于我们是从低位到高位计算的,需要反转数组
    digits.reverse();
    
    Ok(digits)
}

测试用例分析

通过查看测试用例,我们可以更好地理解需求:

#[test]
fn single_bit_one_to_decimal() {
    let input_base = 2;
    let input_digits = &[1];
    let output_base = 10;
    let output_digits = vec![1];
    assert_eq!(
        ayb::convert(input_digits, input_base, output_base),
        Ok(output_digits)
    );
}

最简单的用例,将二进制的 1 转换为十进制的 1。

#[test]
fn binary_to_single_decimal() {
    let input_base = 2;
    let input_digits = &[1, 0, 1];
    let output_base = 10;
    let output_digits = vec![5];
    assert_eq!(
        ayb::convert(input_digits, input_base, output_base),
        Ok(output_digits)
    );
}

将二进制的 101 转换为十进制的 5。

#[test]
fn trinary_to_hexadecimal() {
    let input_base = 3;
    let input_digits = &[1, 1, 2, 0];
    let output_base = 16;
    let output_digits = vec![2, 10];
    assert_eq!(
        ayb::convert(input_digits, input_base, output_base),
        Ok(output_digits)
    );
}

将三进制的 1120 转换为十六进制的 2A(这里 10 代表十六进制的 A)。

#[test]
fn empty_list() {
    let input_base = 2;
    let input_digits = &[];
    let output_base = 10;
    let output_digits = vec![0];
    assert_eq!(
        ayb::convert(input_digits, input_base, output_base),
        Ok(output_digits)
    );
}

空数组应该转换为 [0]。

#[test]
fn leading_zeros() {
    let input_base = 7;
    let input_digits = &[0, 6, 0];
    let output_base = 10;
    let output_digits = vec![4, 2];
    assert_eq!(
        ayb::convert(input_digits, input_base, output_base),
        Ok(output_digits)
    );
}

需要正确处理前导零。

#[test]
fn invalid_positive_digit() {
    let input_base = 2;
    let input_digits = &[1, 2, 1, 0, 1, 0];
    let output_base = 10;
    assert_eq!(
        ayb::convert(input_digits, input_base, output_base),
        Err(ayb::Error::InvalidDigit(2))
    );
}

当数字大于或等于进制基数时,应该返回错误。

错误处理的重要性

这个练习展示了三种不同的错误情况:

#[derive(Debug, PartialEq)]
pub enum Error {
    InvalidInputBase,    // 输入进制无效(小于2)
    InvalidOutputBase,   // 输出进制无效(小于2)
    InvalidDigit(u32),   // 数字无效(大于等于进制基数)
}

使用枚举来表示不同类型的错误是 Rust 中的惯用法,它比使用简单的错误码更加明确和安全。

优化版本

考虑到大数的情况,我们可以提供一个更安全的版本:

#[derive(Debug, PartialEq)]
pub enum Error {
    InvalidInputBase,
    InvalidOutputBase,
    InvalidDigit(u32),
}

pub fn convert(number: &[u32], from_base: u32, to_base: u32) -> Result<Vec<u32>, Error> {
    // 验证进制
    if from_base < 2 {
        return Err(Error::InvalidInputBase);
    }
    
    if to_base < 2 {
        return Err(Error::InvalidOutputBase);
    }
    
    // 验证数字并处理特殊情况
    if number.is_empty() {
        return Ok(vec![0]);
    }
    
    // 检查数字有效性,并同时检查是否全为0
    let mut all_zero = true;
    for &digit in number {
        if digit >= from_base {
            return Err(Error::InvalidDigit(digit));
        }
        if digit != 0 {
            all_zero = false;
        }
    }
    
    if all_zero {
        return Ok(vec![0]);
    }
    
    // 第一步:将数字从 from_base 转换为十进制(使用u32防止溢出)
    let mut value: u32 = 0;
    for &digit in number {
        value = value.checked_mul(from_base)
            .ok_or(Error::InvalidDigit(digit))?
            .checked_add(digit)
            .ok_or(Error::InvalidDigit(digit))?;
    }
    
    // 第二步:将十进制数转换为 to_base
    let mut digits = Vec::new();
    let mut n = value;
    
    while n > 0 {
        digits.push(n % to_base);
        n /= to_base;
    }
    
    // 反转数组以获得正确的顺序
    digits.reverse();
    
    Ok(digits)
}

性能和边界考虑

在实现进制转换时,我们需要考虑:

  1. 溢出处理:使用 checked_mulchecked_add 防止算术溢出
  2. 内存效率:预分配合适大小的 Vec 可以提高性能
  3. 算法复杂度:时间复杂度为 O(n log m),其中 n 是输入数字位数,m 是数值大小

总结

通过 all-your-base 练习,我们学到了:

  1. 进制转换算法:理解了不同进制之间转换的数学原理
  2. 错误处理:学会了如何使用 Result 和自定义错误类型
  3. 边界条件处理:掌握了如何处理空数组、前导零等特殊情况
  4. 数值安全:了解了如何防止算术溢出
  5. 算法实现:实践了基础的数学算法实现

这些技能在实际开发中非常有用,无论是处理不同格式的数据,还是实现编解码器,都需要类似的技能。进制转换虽然看起来简单,但涉及到了许多计算机科学的基础概念,是学习更高级主题的良好起点。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐