目录

题目描述

✅ 补充题 14. 阿拉伯数字转中文数字

将一个整数转换为中文表示。例如:

  • 0
  • 1234一千二百三十四
  • 100010001一亿零一万零一
  • 10001一万零一
  • 1000一千

规则

  • 连续的零只读一个"零"
  • 末尾的零不读
  • 每4位一组,分别对应:个位、万位、亿位

题意分析

输入是一个非负整数,输出是它的中文读法字符串。真正要交付的不是「逐位翻译」,而是符合中文口语习惯的完整读法。

从给出的样例里可以读出三条硬性规则。第一,连续的零无论有多少个,都只读出一个 ;第二,末尾的零一律不读,1000 读作 一千 而不是 一千零零零;第三,数位单位以四位为一个周期, 在每个周期内重复使用,跨周期的单位只有 亿

约束信号在于取值范围:题面给的是 int,最大约 21 亿,因此四位一节最多只有三节——个节、万节、亿节,节数是固定的常数,不需要写通用的循环分节逻辑。

边界要单独列清:数字 0 必须输出 ,这是唯一一个「零要读出来」的场景;10001 这类中间整节缺位的数;100010001 这类连续跨两个节都缺位的数;以及 1000 这类末尾整片是零的数。另外,十几这一档在中文里习惯读作 十五 而不是 一十五,也需要照顾。

解法:四位分组转换

核心思路

中文数位以四位为一节:节内复用 千、百、十、个,节间再连接 万、亿。按 32 位整数处理时最多三节,因此把数字拆成“亿节、万节、个节”,从高到低拼接即可。

零不能遇到就写,而要延迟写入。节内只有在已经输出过非零位、并且后面又出现非零位时才补一个 ;节间只有在前面已有内容,且中间跳过空节或当前低节不足四位时补 。这样同一套规则同时消除连续零和末尾零。

循环不变量是:每处理完一个非零节,结果串恰好是截至该节的正确中文读法,needZero 只表示它与下一个非零节之间存在尚未输出的零。空节只置标记,下一节真正有内容时才消费标记,所以零既不会漏也不会重复。

“十几省略一”只发生在整个读法的开头。先让所有节统一输出 一十,最后仅当完整结果以 一十 开头时删掉第一个 ;因此 10 → 十,而 1000010 → 一百万零一十。以下实现遵循题意,输入范围是非负 32 位整数。

解题步骤

  1. 0 直接返回
  2. 每四位拆成一节,得到个、万、亿三节,再从高节向低节处理。
  3. 空节不输出;若前面已有内容,只设置 needZero,等待后续非零节。
  4. 处理非零节前,若已有高节且 needZero == true,或当前节小于 1000,就先补一个
  5. 节内从千位到个位扫描:零只设置待补标记;下一次遇到非零位时,先补一个 ,再输出“数字 + 单位”。
  6. 拼上节单位并清除 needZero。全部完成后,只对完整结果开头的 一十 省略

边界自测:0 → 零10 → 十110 → 一百一十1000 → 一千10001 → 一万零一1000010 → 一百万零一十100010001 → 一亿零一万零一

代码实现

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(d)$,$d$ 为十进制位数;每一位只处理一次。对 32 位整数,$d \le 10$,实际为常数时间。
  • 空间复杂度:不计返回字符串为 $O(1)$;结果字符串长度为 $O(d)$。三张单位表和三个分节都是固定大小。

关键点总结

  • 先确认口径:是否支持负数、最大单位到哪里、十位是否省略 、是否需要金额大写。
  • 四位分节把“无限数位单位”化成固定的节内单位和节间单位。
  • 延迟输出零是核心:看到零只记状态,确认后面有非零内容时才写。
  • 一十 → 十 是整串首部规则,不能在每个四位节内独立省略。
  • 当前代码按非负 32 位整数实现;若允许负数或更高单位,应先确认输出口径再扩展。

易错点总结

  • 没有特判 0:所有节都会被跳过,结果是空串而不是
  • 遇零立即输出1000 会多出尾部零,100010001 会出现连续零;必须延迟到下一个非零位再写。
  • 节间只看空节10001 的个节虽非空但不足四位,仍要补零,否则会写成 一万一
  • 在每节省略“一十”:会把 1000010 错写成 一百万零十;只允许删除完整结果开头的
  • 未确认输入范围:当前代码按非负 32 位整数实现;若允许负数或更大范围,需先约定 兆/万亿 的读法再扩展。

相似题目

题目 难度 考察点
273. 整数转换英文表示 困难 英文按三位一节,且节内有 teens 这类不规则读法
面试题 16.08. 整数的英语表示 困难 同为英文读法,节间空格与连字符的拼接更琐碎
12. 整数转罗马数字 中等 无进位周期,靠面值表贪心减法构造
13. 罗马数字转整数 简单 反向解析,靠相邻字符大小关系判断加减
字节面试题-阿拉伯数字转中文 中等 同一模型的面试变体,可对照不同口径的零处理