题目描述

✅ 补充题 9. 36 进制加法

给定两个字符串 num1 和 num2,分别表示两个 36 进制正整数。请计算它们的和,并返回结果的 36 进制字符串。

36 进制使用 0 到 9 表示数值 0 到 9,使用小写字母 a 到 z 表示数值 10 到 35。

不能先把两个字符串整体转换为十进制整数,完成加法后再转换回 36 进制。

示例 1:

输入:num1 = "1b", num2 = "2x"
输出:"48"
解释:1b、2x 和 48 分别表示十进制的 47、105 和 152。

示例 2:

输入:num1 = "z", num2 = "1"
输出:"10"

提示:

  • 两个字符串均非空,只包含 0-9 和 a-z。
  • 输出继续使用小写字母表示大于 9 的数位。

题意分析

原题的两个输入字符串表示正整数,数位字符为 0..9 和 a..z,分别对应数值 0..9 和 10..35。求它们的和,并按同样的 36 进制规则返回字符串,字母仍使用小写。下方实现也能处理表示零的字符串,这是对原题输入范围的扩展。

字符串长度可以不同;运算针对整个整数的值,不是把对应字符直接相加。逐位计算即可处理超出整数类型范围的输入,不需要先将整串转换为 int 或 long。

解法:逐位模拟 36 进制加法

核心思路

[!blue]

加法的进位从低位传向高位,而字符串最低位在末尾,所以用 i、j 分别从两个字符串末尾向左扫描。carry 表示此前低位相加传来的进位,某个字符串提前耗尽后,该侧后续数位按零参与。

每轮先把当前字符转为数值。数字字符减去 '0';字母字符减去 'a' 再加 10。令 sum = first + second + carry,根据 sum = 36 * carryNext + digit,本位为 sum % 36,下一位进位为 sum / 36。

两个数位最大均为 35,旧进位最大为 1,因此 sum 最多为 71,新的进位仍只可能是零或一。运算过程中保存的都是单个数位和进位,不会随输入总长度增大而溢出。

将余数按相反的映射转回字符,追加到结果末尾。这样每轮都正确确定一个低位,并将尚未处理的高位问题交给下一轮。只有两串都耗尽且进位为零时才结束,最后反转按低位到高位生成的字符,得到正常顺序。

解题步骤

  1. 令 i、j 指向两串末尾,carry = 0,准备结果缓冲区。
  2. 当任一指针仍有效或还有进位时继续。读取有效数位并转换到 0..35,耗尽的一侧取零。
  3. 计算 sum,将 sum % 36 编码成字符追加,令 carry = sum / 36,有效输入指针各自左移。
  4. 所有位与最终进位处理完成后,反转缓冲区并返回。

代码实现

class Solution {
    public String add36Strings(String num1, String num2) {
        int i = num1.length() - 1;
        int j = num2.length() - 1;
        int carry = 0;
        StringBuilder builder = new StringBuilder();

        // 两串耗尽后若仍有进位,还要生成最高位。
        while (i >= 0 || j >= 0 || carry != 0) {
            int first = 0;

            if (i >= 0) {
                first = toDigit(num1.charAt(i));
                i--;
            }

            int second = 0;

            if (j >= 0) {
                second = toDigit(num2.charAt(j));
                j--;
            }

            int sum = first + second + carry;

            // 余数写入当前位,商作为下一位的进位。
            builder.append(toChar(sum % 36));
            carry = sum / 36;
        }

        return builder.reverse().toString();
    }

    private int toDigit(char ch) {
        if (ch >= '0' && ch <= '9') {
            return ch - '0';
        }

        return ch - 'a' + 10;
    }

    private char toChar(int value) {
        if (value < 10) {
            return (char) ('0' + value);
        }

        return (char) ('a' + value - 10);
    }
}
func add36Strings(num1 string, num2 string) string {
    i := len(num1) - 1
    j := len(num2) - 1
    carry := 0
    res := make([]byte, 0)

    // 两串耗尽后若仍有进位,还要生成最高位。
    for i >= 0 || j >= 0 || carry != 0 {
        first := 0
        if i >= 0 {
            first = toDigit(num1[i])
            i--
        }

        second := 0
        if j >= 0 {
            second = toDigit(num2[j])
            j--
        }

        sum := first + second + carry
        // 余数写入当前位,商作为下一位的进位。
        res = append(res, toChar(sum%36))
        carry = sum / 36
    }

    for left, right := 0, len(res)-1; left < right; left, right = left+1, right-1 {
        res[left], res[right] = res[right], res[left]
    }
    return string(res)
}

func toDigit(ch byte) int {
    if ch >= '0' && ch <= '9' {
        return int(ch - '0')
    }
    return int(ch-'a') + 10
}

func toChar(value int) byte {
    if value < 10 {
        return byte('0' + value)
    }
    return byte('a' + value - 10)
}

复杂度分析

  • 时间复杂度:$O(\max(m,n))$,逐位扫描一次,最后再线性反转。
  • 空间复杂度:$O(\max(m,n))$,用于保存最多比较长输入多一位的结果,包括构造结果的缓冲区。

关键点总结

[!green]

  • 大数不能整体转换,逐位运算让中间值始终不超过 71。
  • 数字字符映射到 0..9,字母映射到 10..35;反向转换的偏移必须对应。
  • 循环条件包含最终进位,可统一处理结果多一位的情况。
  • 低位先生成时应“末尾追加 + 最后反转”;每次头插会退化为 $O(n^2)$。

易错点总结

[!yellow]

  • 整串转换为固定宽度整数会受数值范围限制,失去逐位处理长整数的作用。
  • 字母 'a' 对应 10,字符转数值和数值转字符必须使用互为逆运算的偏移。
  • 本位与进位都以 36 为基数,不能沿用十进制的取模和整除。
  • 循环条件必须包含 carry,否则会丢掉输入耗尽后的最高位进位。
  • 两个输入条件要用“或”,否则短串耗尽时会提前结束,漏掉长串的剩余部分。
  • 当前生成顺序从低位到高位,最后必须反转;逐次头插则会增加搬移成本。

相似题目

题目 难度 关联与区别
415. 字符串相加 简单 十进制逐位加法可直接推广,本题只需把进位基数改为36并映射字母数码。
补充题 10. 36 进制减法 中等 相同进制的减法还需比较大小、处理借位及结果负号,可对照检查数码转换。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/70208463
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!