头一回在OJ上刷到 HJ95“人民币转换”的时候,我心想这不就是个数字转中文吗?结果提交了两次都挂在“零”的处理上。后来静下心把规则一条条拆开,才发现这题真正考的不是翻译,而是对字符串分组和边界情况的掌控力。今天把我调通的思路和完整代码放出来,给同样被这题折磨的朋友一个参考。
这题在很多大厂机试题里都能看到,新老题库都有收录。表面上是大小写转换,实际玩的是中文数字读法。你不仅要会写“壹贰叁”,还要知道什么时候加“零”,什么时候补“整”,什么时候高位的“壹拾”要读成“拾”。每一个细节都很容易出错,好在规则不算太多,一旦把逻辑捋顺,写起来反而很痛快。
1. 先把题目读懂:这题不是简单的大小写转换
1.1 题面到底在考什么
题目会给你一个小数金额,比如“151121.15”,要求输出对应的大写中文金额,标准答案是:
人民币拾伍万壹仟壹佰贰拾壹元壹角伍分
你可以把这道题理解成一个“中文金额翻译器”:输入阿拉伯数字,输出符合财务习惯的中文大写。它要求的不是单纯的数字映射,因为数字中间一旦出现0,读法就会变得很微妙。
举个生活例子,让你口算“100001”怎么读,大多数人会脱口而出“十万零一”。但让你把这条规则写成代码,麻烦事就来了:那个“零”为什么要出现?两个连续的0为什么只需要读一个?如果万位和亿位是0又该怎么处理?这些问题不提前想清楚,写出来的程序在OJ上很容易翻车。
从技术角度拆解,这道题主要考察四件事:
- 基本映射表的建立,包括数字映射和单位映射;
- 整数部分按“万”“亿”分组的处理;
- 组内和组间的“零”字规则;
- 小数部分“角”“分”的特殊读法。
适合刷题准备笔试的人,也适合想做财务类小工具、金额转大写功能的同学参考。难度中等偏下,但细节多,拿来练手非常合适。
1.2 先记住这几个样例答案
HJ95 的题目描述里没有把所有规则列成一条一条的,很多细节需要自己从测试样例里反推。刷题最忌讳“我以为”,所以我建议先把下面这几个测试用例当成锚点记住:
| 输入 | 期望输出 | 关键点 |
|---|---|---|
| 151121.15 | 人民币拾伍万壹仟壹佰贰拾壹元壹角伍分 | 高位的15要读成“拾伍”,不是“壹拾伍” |
| 1010.00 | 人民币壹仟零壹拾元整 | 中间0要读一次“零” |
| 1001.01 | 人民币壹仟零壹元零壹分 | 角位为0但分位不为0,要补“零” |
| 0.01 | 人民币零元零壹分 | 整数部分是0不能省略“零元” |
| 0.10 | 人民币零元壹角整 | 只有角没有分,末尾要写“整” |
这几个样例覆盖了最核心的规则。尤其是第一行“151121.15”,直接推翻了很多人的惯性写法:很多教材写大写金额时,十位为1要写“壹拾”,但这里最高段(万级)的“15”被写成了“拾伍”,说明OJ采用的是“高位十位为一,省略前面壹”的读法。这一点如果不看样例,很难猜中。
2. 核心思路拆解:分组、映射、补零
2.1 数字到中文的映射表
写这类题,第一步永远是建表。别试图用分支判断一个个写数字,那只会让代码膨胀得没法看。最简单的方式就是用两个数组:
数字映射:
0 -> 零 1 -> 壹 2 -> 贰 3 -> 叁 4 -> 肆 5 -> 伍 6 -> 陆 7 -> 柒 8 -> 捌 9 -> 玖单位映射(组内):
个位 -> 空 十位 -> 拾 百位 -> 佰 千位 -> 仟节单位(组间):
第1组 -> 空 第2组 -> 万 第3组 -> 亿 第4组 -> 万亿有人会把“元”“角”“分”也放进单位表,我的建议是不要。它们属于金额后缀,不属于整数数字的读数部分,分开处理会清爽很多。
2.2 四位一组的“节”与节单位
中文数字读法有个天然规律:每四位是一节。比如“123456789”从右往左数,前四位是“6789”,剩下的是“1”,加上节单位后读出来就是“壹亿贰仟叁佰肆拾伍万陆仟柒佰捌拾玖”。
在代码里,字符串处理比数值处理更稳,因为输入是“151121.15”这种带小数的字符串,一旦转成double再转回来,很容易出现“0.1+0.2”那种精度问题。所以建议直接用字符串切片,把整数部分从右往左每4位切一段。
比如“151121”切成两段:
高位段: 15 低位段: 1121“100001”切成:
高位段: 10 低位段: 0001“100000001”切成:
第1段: 1 第2段: 0000 第3段: 0001每一段内部按“千、百、十、个”处理,段与段之间加上“万”“亿”这样的节单位。这是整道题最核心的数据组织方式。
2.3 最关键的一串零:什么时候读“零”,什么时候不读
这题百分之八十的坑都藏在“零”字上。把“零”的规则总结成下面几条,基本就稳了。
第一条,段内规则:阿拉伯数字中间有0时,中文大写要写“零”。比如“1001”读“壹仟零壹”,中间的连续两个0只读一个“零”。
第二条,段间规则:如果某个4位段整体全是0,这一段不读任何内容,也不要补“零”字。比如“11000000”切出来的低位段是“0000”,结果就是“壹仟壹佰万元整”,中间不出现“零”。
第三条,段间补零:如果当前这一段非零,但它前面存在被跳过的全零段,或者当前段的最高位(千位)是0,那么在拼接当前段之前要补一个“零”。
举两个例子:
- “100001”切成“10”和“0001”。高位段读“拾万”;低位段“0001”的值是1,小于1000,说明低位段的千位是0,所以要在“壹”前面补“零”,变成“拾万零壹元整”。
- “100001000”切成“1”、“0000”、“1000”。高位段读“壹亿”;第二段全0被跳过,但第三段非零且中间隔了一个全零段,所以要在第三段前补“零”,变成“壹亿零壹仟元整”。
这个规则看起来简单,但在代码里如果不记录“是否有中间段被跳过”,很容易漏零。我用了一个计数器,每当遇到非零段,就根据这个计数器判断要不要补零,非常可靠。
2.4 高位“壹拾”诡异规则
这是这题最容易让人怀疑人生的地方。按传统财务大写规则,12应该读“壹拾贰”,15应该读“壹拾伍”。但HJ95的样例里,“151121.15”输出的是“拾伍万”,而不是“壹拾伍万”。
我的理解是:在整数部分的最高段(也就是从高位读起的第一段)里,如果这一段的开头是十位,并且十位是1,那么这个“壹”会被省略,直接读“拾”。比如:
- 10 读“拾元整”,不是“壹拾元整”;
- 12 读“拾贰元整”;
- 150000 读“拾伍万元整”。
但有一个前提:这个“1”必须是整个数字最高段的第一位,并且所在的位数是十位。如果它不是最高位,比如“1100”读“壹仟壹佰元整”,那个“壹”就不能省。再比如“1010”读“壹仟零壹拾元整”,中间的“壹拾”也必须保留,因为它前面已经读过“壹仟零”了,不是开头的十位。
把这条规则落到代码里时,我会给“段转换函数”传一个top参数,只有最高段才走“省略壹”的逻辑,其他段全部按正常规则处理。
3. 代码落地:Python实现与逐步讲解
3.1 主流程与输入处理
我习惯先处理输入,再写核心逻辑。题目输入是一行字符串,可能有小数点,也可能没有。用字符串读取可以避免浮点精度问题。
主流程其实只有三步:拆分整数和小数、转换整数、转换小数,最后拼起来。伪代码如下:
- 读取一行金额字符串;
- 按
.拆成整数部分和小数部分,没有小数点时小数部分记作"00"; - 整数部分调用整数转大写函数,返回中文数字;
- 小数部分调用小数转大写函数,返回角分读法;
- 输出
"人民币" + 整数中文 + "元" + 小数中文。
其中“元”这个字是固定的,即便整数部分是0,也要输出“零元”。比如输入“0.01”,输出“人民币零元零壹分”,不能因为是0就直接省略。
3.2 整数部分转换函数
整数部分转换是核心,我把它拆成两层:
外层函数负责把整数按4位分组,然后从高位到低位遍历,处理组间单位、补零逻辑;内层函数负责把一个4位段(可能是1到4位)转换成中文。
先说内层函数convert_section(s, top),输入一个数字字符串,比如"1121",输出"壹仟壹佰贰拾壹"。它要做几件事:
- 跳过前导0,找到第一个非零字符;
- 从第一个非零字符开始扫描,碰到0就标记一下,等下一个非零字符出现时先补一个“零”;
- 每一位根据它在段内的位置,拼接对应的“拾”“佰”“仟”;
- 如果
top为真,并且第一个非零字符就是十位的“1”,那么输出“拾”而不是“壹拾”。
外层函数int_to_upper(integer_str)的处理逻辑是这样的:
- 先把整数部分的字符串去前导0,如果全是0则直接返回“零”;
- 从右往左每4位切片,得到一个列表;
- 从高位到低位遍历这个列表;
- 如果当前段全为0,跳过,同时记录“中间隔了0段”;
- 如果当前段非零,先根据之前是否有非零段以及是否隔了0段决定要不要补“零”;
- 把段内转换结果拼上当前段对应的节单位(空、万、亿等);
- 更新“已经输出过非零段”的标记。
用代码写出来大概是:
def convert_section(s, top=False): first = 0 while first < len(s) and s[first] == '0': first += 1 if first == len(s): return '零' res = '' zero = False for i in range(first, len(s)): digit = int(s[i]) pos = len(s) - 1 - i if digit == 0: zero = True else: if zero: res += '零' zero = False if top and i == first and pos == 1 and digit == 1: res += '拾' else: res += num_map[digit] if pos > 0: res += unit_map[pos] return res注意这里i == first的含义是“当前字符是段内第一个非零字符”。最高段不会有前导0,所以这个条件天然成立。
外层函数:
def int_to_upper(integer_str): s = integer_str.lstrip('0') or '0' if s == '0': return '零' groups = [] while s: groups.append(s[-4:]) s = s[:-4] groups.reverse() result = '' printed = False zero_gap = 0 for idx, seg in enumerate(groups): seg_val = int(seg) if seg_val == 0: if printed: zero_gap += 1 continue if printed and (zero_gap > 0 or seg_val < 1000): result += '零' top = (idx == 0) sec_text = convert_section(seg, top) sec_unit = section_map[len(groups) - 1 - idx] result += sec_text + sec_unit printed = True zero_gap = 0 return resultzero_gap就是用来记录中间跳过了几个全零段的。只要它大于0,即使当前段千位不是0,也要补一个“零”。这个变量是我排查“100001000”这类用例时加上的,实测非常有效。
3.3 小数部分转换函数
小数部分只关心两位:十分位是“角”,百分位是“分”。函数输入小数部分字符串,输出角分读法。
逻辑也不复杂:
- 如果小数部分只有1位,补一个0变成2位;
- 如果超过2位,按题目要求只取前2位(通常输入最多两位);
- 角位和分位都为0,返回“整”;
- 角位不为0,拼“X角”;
- 角位为0但分位不为0,拼“零”,再拼“X分”;
- 角位不为0但分位为0,拼“X角整”。
写成函数:
def convert_fraction(fraction_str): if len(fraction_str) == 1: fraction_str += '0' if len(fraction_str) > 2: fraction_str = fraction_str[:2] jiao = int(fraction_str[0]) fen = int(fraction_str[1]) if jiao == 0 and fen == 0: return '整' res = '' if jiao != 0: res += num_map[jiao] + '角' elif fen != 0: res += '零' if fen != 0: res += num_map[fen] + '分' elif jiao != 0: res += '整' return res举个例子,输入“0.05”,小数部分是“05”,jiao为0,fen为5,输出“零伍分”。加上整数部分的“零元”,就是“人民币零元零伍分”,和财务习惯一致。
3.4 完整可运行代码
把上面的函数拼起来,就是一个能直接跑的版本:
import sys num_map = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'] unit_map = ['', '拾', '佰', '仟'] section_map = ['', '万', '亿', '万亿'] def convert_section(s, top=False): first = 0 while first < len(s) and s[first] == '0': first += 1 if first == len(s): return '零' res = '' zero = False for i in range(first, len(s)): digit = int(s[i]) pos = len(s) - 1 - i if digit == 0: zero = True else: if zero: res += '零' zero = False if top and i == first and pos == 1 and digit == 1: res += '拾' else: res += num_map[digit] if pos > 0: res += unit_map[pos] return res def int_to_upper(integer_str): s = integer_str.lstrip('0') or '0' if s == '0': return '零' groups = [] while s: groups.append(s[-4:]) s = s[:-4] groups.reverse() result = '' printed = False zero_gap = 0 for idx, seg in enumerate(groups): seg_val = int(seg) if seg_val == 0: if printed: zero_gap += 1 continue if printed and (zero_gap > 0 or seg_val < 1000): result += '零' top = (idx == 0) sec_text = convert_section(seg, top) sec_unit = section_map[len(groups) - 1 - idx] result += sec_text + sec_unit printed = True zero_gap = 0 return result def convert_fraction(fraction_str): if len(fraction_str) == 1: fraction_str += '0' if len(fraction_str) > 2: fraction_str = fraction_str[:2] jiao = int(fraction_str[0]) fen = int(fraction_str[1]) if jiao == 0 and fen == 0: return '整' res = '' if jiao != 0: res += num_map[jiao] + '角' elif fen != 0: res += '零' if fen != 0: res += num_map[fen] + '分' elif jiao != 0: res += '整' return res def convert(amount): if '.' in amount: integer_part, fraction_part = amount.split('.') else: integer_part, fraction_part = amount, '00' int_text = int_to_upper(integer_part) frac_text = convert_fraction(fraction_part) return '人民币' + int_text + '元' + frac_text if __name__ == '__main__': for line in sys.stdin: line = line.strip() if line: print(convert(line))这份代码在OJ上按顺序逐行读取,遇到一行输出一行。我本地用样例测过,全部符合预期。
4. 边界case和测试用例速查表
4.1 需要重点验证的输入
写完代码后,千万不要直接提交。先把下面这些容易出错的输入跑一遍:
| 输入 | 预期输出 | 用途 |
|---|---|---|
| 0.00 | 人民币零元整 | 全零金额 |
| 0.01 | 人民币零元零壹分 | 只有分 |
| 0.10 | 人民币零元壹角整 | 只有角 |
| 1.00 | 人民币壹元整 | 最小整数 |
| 10.00 | 人民币拾元整 | 高位壹拾省略 |
| 11.00 | 人民币拾壹元整 | 高位壹拾省略,后面还有数字 |
| 12.34 | 人民币拾贰元叁角肆分 | 普通带角分 |
| 100.00 | 人民币壹佰元整 | 百位正常 |
| 1001.01 | 人民币壹仟零壹元零壹分 | 段内连续零 |
| 10001.00 | 人民币壹万零壹元整 | 段间补零 |
| 10010.00 | 人民币壹万零壹拾元整 | 低位段前有零 |
| 100001000.00 | 人民币壹亿零壹仟元整 | 中间隔全零段 |
| 11000000.00 | 人民币壹仟壹佰万元整 | 低位全零段不补零 |
| 151121.15 | 人民币拾伍万壹仟壹佰贰拾壹元壹角伍分 | 题目样例 |
如果这些输入都能输出正确结果,说明整数部分的分组、补零、高位“壹拾”省略逻辑基本没有问题。
4.2 踩过的坑与排查思路
我实际调试时踩过几个比较隐蔽的坑,列出来给大家排雷。
第一个坑是“段内前导零重复补零”。我最开始写的段转换函数,扫描到前导0时会先置一个标记,等遇到非零字符时补“零”。这本身没问题,但放到“0001”这种低位段时,外层函数已经补过一个“零”了,内层函数还会把前导0再转成一个“零”,结果输出了“零壹”,而不是“壹”。后来我把段内逻辑改成“跳过前导0,从第一个非零字符开始扫描”,这个问题就解决了。
第二个坑是“中间隔了全零段但没有补零”。比如“100001000”,我在遇到“0000”这一整段时直接跳过,然后遇到了“1000”段,因为段值是1000,不满足“小于1000”的段间补零条件,导致漏了“零”,输出变成“壹亿壹仟元整”。正确应该是“壹亿零壹仟元整”。解决办法就是加一个zero_gap计数器,只要在非零段之间出现过全零段,无论当前段值多大,都要先补一个“零”。
第三个坑是“最高位壹拾省略条件写太宽”。一开始我写的是“只要段内十位是1且是第一次出现非零,就省略壹”,结果“1010”被读成了“壹仟零拾元整”,少了一个“壹”。后来我加了top参数,只在最高段并且是段内第一个字符时才触发省略逻辑,这样“1010”的十位1虽然在第一次出现非零之后出现了,但已经不是段内第一个非零字符,所以会正常输出“壹拾”,结果就对了。
第四个坑是小数部分“角位为0但分位不为0”时的补零。比如“0.01”,如果不补零,直接输出“人民币零元壹分”,这不符合样例。必须判断:角位是0、分位不是0时,在分位前面加一个“零”,变成“零壹分”。
这些坑都有一个共同特点:只看单个规则觉得都对,组合起来就出问题。所以调试的时候一定要把每一条规则当成独立的开关,多设计“复合条件”的测试用例,而不是只测最简单的情况。
5. 从这道题能带走的通用技能
5.1 字符串模拟题的五步法
刷完这题,我最大的感受是:字符串模拟题其实有一套稳定的解法,可以总结成五步。
第一步,完整读题,把样例答案当成规则的一部分。不要急着写代码,先用题目给的输出反推读法规则。
第二步,拆解数据结构。像“分组”“单位映射”这种设计必须在写代码前确定下来。这题如果不按4位分组,直接从头到尾扫字符,遇到“亿”“万”这种跨位单位会非常痛苦。
第三步,把规则拆成独立的“过滤器”。比如“段内补零”和“段间补零”是两个不同的逻辑,先各自实现,再考虑它们如何衔接。
第四步,构造边界用例。每想出一个规则,就找一个能触发它的输入,并把期望输出固化下来。先跑样例,再跑边界,最后跑复合场景。
第五步,稳定复现后,再考虑代码精简。不要一开始就写花哨的写法,重在逻辑正确。
这五步不只是对HJ95有用,任何“日期转换”“英文数字读法”“罗马数字转整数”之类的题目都能用上。
5.2 一个可复用的小技巧
最后分享一个我自己比较喜欢的小技巧:处理金额转大写这类问题时,永远不要把“元角分”直接拼进数字转换逻辑里。把它们当成后缀,在主流程里统一拼接。
这样做有几个好处:
- 数字转换函数可以单独测试,不关心后面是“元”还是“万”;
- 小数部分和整数部分互不干扰,改一个不会影响另一个;
- 以后如果要扩展成“港币”“美元”或者其他币种,只需要改后缀和币种名,核心读数逻辑不用动。
我在做其他项目时,也经常用这个思路把“数据转换”和“展示格式”分开。代码会变得更好维护,出错的概率也小很多。
如果你正在准备机试,建议把这道题反复做三遍:第一遍照着上面的思路自己写,第二遍不看代码纯手推逻辑,第三遍用随机生成的数字去和自己的财务常识对答案。三遍下来,你对“零”的处理会形成肌肉记忆,以后再遇到带“零”的字符串模拟题,基本不会慌。