题目描述

:::fold-green 相关原题

✅ 阿拉伯数字转中文

牛客包含负数输入,本文实现的范围为非负 32 位整数。

:::

给定非负整数 num,请返回它的中文读法。

采用普通中文数字和数位单位,使用“二”而不是“两”,不使用财务大写。连续的缺位零只读一次,末尾的零不读;整数 0 本身写作“零”。

示例 1:

输入:num = 1234
输出:"一千二百三十四"

示例 2:

输入:num = 10001
输出:"一万零一"

提示:

  • 0 <= num <= 2^31 - 1。
  • 每四位为一节,节内使用“十、百、千”,节外使用“万、亿”。
  • 整串以“一十”开头时省略开头的“一”,例如 10 写作“十”。

题意分析

将数字转换成带有中文数位单位的读法,不是简单地把每个阿拉伯数字替换成汉字。连续的零只读一次,末尾的零不读;中文按四位分节,节内使用千、百、十,节外使用万、亿。

以下实现处理非负 32 位整数,最多涉及个、万、亿三个节,数字零本身返回 零。采用“二”和普通数字读法,不涉及负数、小数或财务大写。需要分别处理节内缺位、节与节之间缺位,以及整串开头 一十 中“一”的省略。

解法:四位分组转换

核心思路

[!blue]

中文大单位每四位变化一次,所以先按 10000 拆成三个节,再从高节到低节拼接。每一节都不超过四位,可以用同一套千、百、十、个的规则转换,最后加上这一节对应的万或亿。

节内从高位到低位读取。遇到非零数字就输出“数字 + 数位单位”;遇到零时先不输出,只用 zeroPending 记住“已有非零内容后出现了空缺”。后续再次遇到非零数字,才补一个 零 并清除标记。连续零只留下一个标记,末尾零则因为没有后续非零数字而永远不输出;节前面的零也不会建立这个标记。

节间用另一个标记 needZero 处理较大的空缺。高节已经输出后,若跳过了全零的中间节,就记录需要补零;即使没有空节,后面的非零节不足四位,也说明它与前一节之间缺少数位。因此,输出下一个非零节前,只要“跳过空节”或“当前节小于 1000”成立,就补一个 零。两个条件同时成立也只补一次。

全零节不直接输出,否则会带上不需要的大单位或尾零。最后一个非零节处理完后,后面即使还有空节,也只留下未使用的标记,不会给结果增加尾零。节内转换不会产生首零或尾零,因而与节间补零衔接时不会出现重复零。

各节先统一生成完整的 一十,拼完整个结果后,只在整串以 一十 开头时去掉第一个“一”。不能让每个节自行省略,否则低节中的十位会失去应有的完整读法。

解题步骤

  1. 输入为 0 时直接返回 零。
  2. 按四位拆出个节、万节、亿节,从亿节向个节遍历。
  3. 空节跳过;若此前已有输出,设置节间待补零标记。
  4. 处理非零节时,先根据空节标记和当前节是否不足四位决定是否补零。
  5. 在该节内部依次读取千、百、十、个,延迟补零,只为非零位写出数字及单位。
  6. 拼接节单位,清除节间标记;最后仅省略整串开头 一十 中的“一”。

代码实现

class Solution {
    private static final String[] DIGITS = {
        "零",
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九"
    };
    private static final String[] UNITS = {
        "千",
        "百",
        "十",
        ""
    };
    private static final String[] GROUPS = {
        "",
        "万",
        "亿"
    };

    public String numberToChinese(int num) {
        if (num == 0) {
            return "零";
        }

        int[] parts = {
            num % 10000,
            num / 10000 % 10000,
            num / 100000000
        };
        StringBuilder builder = new StringBuilder();
        boolean needZero = false;

        for (int group = 2; group >= 0; group--) {
            if (parts[group] == 0) {
                if (builder.length() > 0) {
                    needZero = true;
                }

                continue;
            }

            // 跨过空节或低节不足四位时,在下一段非零内容前补零。
            if (builder.length() > 0 && (needZero || parts[group] < 1000)) {
                builder.append("零");
            }

            builder.append(convertGroup(parts[group])).append(GROUPS[group]);
            needZero = false;
        }

        String result = builder.toString();

        // 只省略整串开头的一,不能在每个节内独立省略。
        if (result.startsWith("一十")) {
            result = result.substring(1);
        }

        return result;
    }

    private String convertGroup(int value) {
        int[] divisors = {
            1000,
            100,
            10,
            1
        };
        StringBuilder builder = new StringBuilder();
        boolean zeroPending = false;

        for (int i = 0; i < divisors.length; i++) {
            int digit = value / divisors[i];

            value %= divisors[i];

            if (digit == 0) {
                zeroPending = builder.length() > 0;
                continue;
            }

            // 节内的零延迟到下一个非零位输出,避免连续零和尾零。
            if (zeroPending) {
                builder.append("零");
                zeroPending = false;
            }

            builder.append(DIGITS[digit]).append(UNITS[i]);
        }

        return builder.toString();
    }
}
var chineseDigits = []string{
    "零",
    "一",
    "二",
    "三",
    "四",
    "五",
    "六",
    "七",
    "八",
    "九",
}
var chineseUnits = []string{
    "千",
    "百",
    "十",
    "",
}
var chineseGroups = []string{
    "",
    "万",
    "亿",
}

func numberToChinese(num int) string {
    if num == 0 {
        return "零"
    }

    parts := []int{
        num % 10000,
        num / 10000 % 10000,
        num / 100000000,
    }
    result := ""
    needZero := false

    for group := 2; group >= 0; group-- {
        if parts[group] == 0 {
            if len(result) > 0 {
                needZero = true
            }
            continue
        }

        // 跨过空节或低节不足四位时,在下一段非零内容前补零。
        if len(result) > 0 && (needZero || parts[group] < 1000) {
            result += "零"
        }
        result += convertChineseGroup(parts[group]) + chineseGroups[group]
        needZero = false
    }

    // 只省略整串开头的一,不能在每个节内独立省略。
    if len(result) >= len("一十") && result[:len("一十")] == "一十" {
        result = result[len("一"):]
    }
    return result
}

func convertChineseGroup(value int) string {
    divisors := []int{
        1000,
        100,
        10,
        1,
    }
    result := ""
    zeroPending := false

    for i, divisor := range divisors {
        digit := value / divisor
        value %= divisor
        if digit == 0 {
            zeroPending = len(result) > 0
            continue
        }
        // 节内的零延迟到下一个非零位输出,避免连续零和尾零。
        if zeroPending {
            result += "零"
            zeroPending = false
        }
        result += chineseDigits[digit] + chineseUnits[i]
    }
    return result
}

复杂度分析

  • 时间复杂度:$O(1)$。限定非负 32 位整数,最多处理三个节,每节只扫描四个数位。
  • 空间复杂度:$O(1)$。数字表、单位表、分节数组和结果长度都有固定上界。

关键点总结

[!green]

  • 四位分节把大数读法分成“节内转换”和“节间连接”两个独立问题。
  • 零只先记状态,后续非零内容出现时才输出,同时解决连续零和尾零。
  • 节间补零既要考虑跳过空节,也要考虑低节不足四位。
  • 一十 的省略只作用于最终结果开头。

易错点总结

[!yellow]

  • 输入零若不特判,所有节都会被跳过,结果会成为空字符串。
  • 一遇到零就输出,会产生连续零或尾零;应等到后续非零内容出现再补。
  • 节间只检查是否有全零节,会漏掉非零低节不足四位时需要补的零。
  • 每个节都省略开头的“一”,会错误改变低位节的完整读法。
  • 当前接口及分节单位只覆盖声明的非负 32 位范围,不能直接套用到负数或任意长度整数。

相似题目

题目 难度 关联与区别
补充题 13. 中文数字转阿拉伯数字 中等 原题按单位解析为64位数值,本题以非负32位整数按四位节输出,往返输入需在共同范围内,零处理规则相对应。
273. 整数转换英文表示 困难 同样将整数分组转成自然语言读法,本题是四位一节,英文是三位一组。
补充题 200. 十进制数的中文读法 中等 整数部分可复用四位分节与补零规则;该题还需逐位读取小数,并输出小数点后的零,本题仅处理非负整数。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/95391996
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!