资讯动态

HJ95人民币转换题:字符串处理与边界条件实战解析

发布时间:2026/10/16 1:55:08 来源:尧图企业网站定制
这道 HJ95 人民币转换题几乎每个准备大厂机试的人都会撞上。题目本身不复杂把151121.15这种金额翻译成中文大写壹拾伍万壹仟壹佰贰拾壹元壹角伍分。但真正动手写的时候你会发现它考的根本不是“会不会人民币大写”而是字符串处理、数位分组、边界判定这些基本功。我当年第一次刷这题代码写得又长又乱连续零到底读几个、10到底写“拾”还是“壹拾”硬生生调了一晚上。这篇文章就把我后来整理出的一套稳定写法完整讲清楚从规则拆解到可直接提交的代码再到我踩过的坑一次性给足。适合谁看正在准备机试和笔试的同学、想做工具类脚本的开发者以及任何想通过一道题补一补字符串处理思路的人。这题没有高级算法但特别能检验一个人对细节和边界条件的敏感度而这两项恰恰是最容易在真实面试里翻车的地方。1. 题目搞懂再动手HJ95 到底在问什么1.1 一条样例背后的完整规则题目给的经典样例是输入151121.15输出壹拾伍万壹仟壹佰贰拾壹元壹角伍分表面看只是把数字映射成中文实际上背后藏着一整套中文大写金额规范。先说最基础的映射数字映射0 到 9 对应零壹贰叁肆伍陆柒捌玖。单位映射拾、佰、仟、万、亿小数部分用角、分。整数部分以“元”收尾意思是无论整数部分有没有值后面都要接一个“元”。小数部分全为 0 时在“元”后面补一个“整”字。单看映射很简单但把规则组合起来就会碰出很多细节。这题是字符串处理题不是计算题。输入看起来是一个小数但你千万别想着用double去读、去乘、去取整后面我会重点解释为什么这种方式必挂。1.2 最容易忽略的三个隐形考点很多人上手就写结果不是这里多一个零就是那里少一个“零”。结合我做题和帮别人 review 代码的经验三个最容易被忽略的点是第一10要读成壹拾而不是拾。口语里我们说“十元”但人民币大写规定是“壹拾元整”。同理100000是壹拾万元整10.50是壹拾元伍角。这个“壹”字必须保留题目用例里会有意无意地卡这个点。第二连续零只能读一个“零”。比如1001是壹仟零壹元整中间不是零零壹而是只读一个零。10010是壹万零壹拾元整注意“万”和“壹拾”之间那个零是必须的因为万位和十位之间隔了一个千位和百位这两个位置都是空位。第三小数部分不是简单拼接。角位是 0 但分位非 0 时比如1.05必须输出壹元零伍分那个“零”字不能丢。反过来角位非 0、分位是 0比如1.50输出壹元伍角即可最后不需要补“整”。我在实际刷题时发现不同 OJ 对“角后是否加整”的判定不完全一致但主流的这批题解普遍采用“小数部分全零才加整”的规则。写代码时统一成一种约定然后围绕自己的约定把用例测透比一边写一边纠结某个题源的标准要实用得多。1.3 先列一个输入输出对照表再编码建议动手前先用表格把关键场景理一遍后面写代码和自测都拿它当基准。输入期望输出按本文约定关键点151121.15壹拾伍万壹仟壹佰贰拾壹元壹角伍分标准综合用例10.00壹拾元整壹拾而不是拾1001.00壹仟零壹元整中间补零10010.00壹万零壹拾元整万位与十位之间补零100000.00壹拾万元整高位壹拾0.15零元壹角伍分整数部分为空补零元1.05壹元零伍分角为零分非零要补零1.50壹元伍角分位为零不加整100000001.00壹亿零壹元整亿位与个位之间补零这张表不是随便列的它基本覆盖了这题 90% 的边界场景。你把代码跑完能跟这张表对上一一对应提交基本就稳了。2. 核心设计思路为什么按四位分组、为什么必须用字符串2.1 别用 double这一步就能避开大量精度坑先聊一个最反直觉的决定读入的金额不要转成浮点数处理。很多人写这题第一步就double money; cin money;然后int intPart (int)money; int decPart (int)(money * 100) % 100;。这套写法在本地测几个数看着没问题但一交就挂。原因很简单二进制浮点数没法精确表达0.1像0.29这种数在double内部存的是一个近似值乘 100 再取整结果可能是 28 也可能是 29 附近属于随机翻车。我见过一个很典型的翻车案例输入0.29本应输出零元贰角玖分由于浮点误差转出来的分位直接变成 28输出就成了零元贰角捌分。这种错误非常隐蔽你根本不会想到是精度问题。所以正确姿势是把它当字符串处理。输入进来之后用find(.)把整数部分和小数部分拆开左边是整数字符串右边是小数位字符串。这样既能保证精度又能天然规避小数点后面的位数不齐、输入末尾多 0 这类问题。另外还要注意不能用stod再转字符串。整个过程保持字符串操作这是这道题最重要的一个架构决策。2.2 四位一组顺着中文计数体系来处理整数部分时一个高效且不容易出错的办法是“四位一组”分组。为什么是四位因为中文计数体系是个、十、百、千、万、十万、百万、千万、亿基本单位是“万”而每隔四位就换一个更大的单位万、亿、兆。这和英文的thousand、million、billion三位分隔习惯完全不同。如果按三位分组处理起来会在单位转换上绕很多弯。把整数部分从右往左每四位切一组每组内部的读法完全一致高位的数字配仟次高位配佰再往下配拾最低位不配单位。然后在这一组后面根据它所在的组号补一个大单位第一组是“万”第二组是“亿”第三组是“兆”。举个例子151121从右往左切低四位是1121高两位是15。低四位内部读出壹仟壹佰贰拾壹高两位读出壹拾伍再给高两位末尾补一个“万”拼起来就是壹拾伍万壹仟壹佰贰拾壹。这个思路比逐位硬拼单位要清晰得多也不容易漏单位。分组还有一个好处组内逻辑完全一样你可以写一个函数处理“任意四位”再写一个循环把多组结果拼起来。代码结构会非常干净调试时也容易定位问题。2.3 零的处理先想清楚三类零这题真正的难点就一个字零。我总结下来需要处理的零一共有三类对应三种不同的处理逻辑。第一类是组内零。比如1001里千位是 1个位是 1中间百位和十位都是 0这种“中间零”要读出来但连续两个零只读一个。处理办法是遍历四位组时遇到 0 先记一个“待补零标记”遇到下一个非零数字时如果前面有标记就在输出这个数字前插入一个“零”。第二类是组间零。比如10010高组是1低组是0010拼接时如果低组的第一位是 0说明两组之间有跨组的空位要在两组之间补一个“零”。这个逻辑用一行判断就能搞定当前组拼出来的结果非空且当前组字符串以0开头就在前面插入“零”。第三类是末尾零。比如1000实际读法是壹仟末尾的零不读。这类零在组内遍历时自然会被跳过因为后面没有非零数字触发“待补零标记”所以不用额外处理。把这三类零在代码里分开处理比在一个大循环里各种if嵌套要清爽得多。很多人写乱就是因为把“读零”和“不读零”的判断搅在一起了。2.4 输出规范先定死后面少吵架这里我采用一套明确、可自测的输出约定整数部分为空时用“零元”占位。小数部分全零在“元”后加“整”。角位非零输出“X角”。角位为零、分位非零先补一个“零”再输出“X分”。角位非零、分位为零输出“X角”后直接结束不加“整”。这套约定不一定跟所有 OJ 的隐藏标准 100% 一致但逻辑自洽。关键是你要先定死规则再写代码最后用一套用例去验证。而不是边写边猜那样只会越调越乱。我在另一家刷题平台试过同样代码有的题源里1.50会判成壹元伍角整但这是极少见的差异。如果你担心可以在本地先跑几个自己构造的样例看看哪个输出格式更贴近题面描述再决定要不要在最后追加“整”。从笔试角度说大多数环境用的是我上面这套规则。3. 可直接复抄的实现分组函数、零逻辑、小数处理3.1 完整代码C 版已过样例先给出我整理后的完整实现语言用 C机试最常见的语言之一#include iostream #include string #include vector using namespace std; const string numCn[] {零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖}; const string unitCn[] {, 拾, 佰, 仟}; const string bigUnitCn[] {, 万, 亿, 兆}; // 处理一个四位组内的中文读法 string groupToCn(const string s) { string res; bool pendingZero false; // 遇到0时先挂起遇到非0再决定是否补零 bool hasOutput false; // 当前组是否已经输出过非零内容 int len (int)s.size(); for (int i 0; i len; i) { int d s[i] - 0; int unitIdx len - 1 - i; if (d 0) { if (hasOutput) pendingZero true; } else { if (pendingZero) { res 零; pendingZero false; } res numCn[d] unitCn[unitIdx]; hasOutput true; pendingZero false; } } return res; } // 整数部分转中文 string intToCn(string s) { // 去掉前导0但保留至少一位 while (s.size() 1 s[0] 0) { s.erase(0, 1); } if (s 0) return ; // 整数部分为0 // 从右往左每四位一组groups[0]是低位组 vectorstring groups; for (int i (int)s.size(); i 0; i - 4) { int start max(0, i - 4); groups.push_back(s.substr(start, i - start)); } string res; for (int g (int)groups.size() - 1; g 0; g--) { string cur groups[g]; string curCn groupToCn(cur); if (curCn.empty()) continue; // 整组为0跳过单位 // 组间补零当前组非空且高位有0说明两组之间有空位 if (!res.empty() cur[0] 0) { res 零; } res curCn bigUnitCn[g]; } return res; } // 小数部分转中文 string decToCn(const string s) { int jiao 0, fen 0; if (!s.empty()) jiao s[0] - 0; if (s.size() 2) fen s[1] - 0; if (jiao 0 fen 0) return ; string res; if (jiao ! 0) { res numCn[jiao] 角; } else if (fen ! 0) { res 零; // 角位为0分位非0要补零 } if (fen ! 0) { res numCn[fen] 分; } return res; } int main() { string line; while (getline(cin, line)) { size_t dot line.find(.); string intPart, decPart; if (dot string::npos) { intPart line; decPart ; } else { intPart line.substr(0, dot); decPart line.substr(dot 1); } string intCn intToCn(intPart); if (intCn.empty()) intCn 零; string ans intCn 元; string decCn decToCn(decPart); if (decCn.empty()) ans 整; else ans decCn; cout ans endl; } return 0; }这段代码不追求花哨核心就是四个函数groupToCn处理组内四位intToCn负责分组和拼单位decToCn负责角分main里做拆分和汇总。结构清晰每个函数只干一件事出了问题也容易定位。3.2 分组函数为什么不丢单位groupToCn是这个方案的核心它接收一个长度可能是 1 到 4 的字符串输出组内的中文读法。注意它不负责“万”“亿”这些大单位大单位由intToCn里的bigUnitCn[g]拼上去。函数内部从左往右遍历unitIdx是根据字符位置倒推的单位下标。比如字符串1121长度 4索引 0 对应4-1-03也就是 “仟”索引 3 对应0也就是空单位正好是个位。这个倒推保证无论字符串长度是 4 还是 2单位都能对得上。关键的零逻辑就在这里。遇到0时我不立即输出“零”而是先设置pendingZero true等到真的遇到下一个非零数字时再输出。这样做的好处是多个连续零不会重复输出多个“零”而末尾零因为在后面没有非零数字来触发输出天然被忽略。比如1000千位输出“壹仟”后后三个 0 都只是把pendingZero置为 true但循环结束也没有下一次非零数字所以结果就是“壹仟”。hasOutput这个变量也很重要它防止了组内前导零被错误读出来。比如低位组0010在组内遍历时前两个 0 出现时hasOutput还是 false所以不会触发“补零”。真正把“零”补出来的是外层拼接逻辑而不是组内函数。这种职责分离是这段代码能保持整洁的关键。3.3 整数部分拼接组间补零的判定技巧intToCn里的分组逻辑很直白用for (int i s.size(); i 0; i - 4)从右往左截取每次截 4 位放进groups。注意groups[0]是最低位组而groups[g]的单位是bigUnitCn[g]所以循环时要从最高位组往低位组遍历也就是倒序遍历groups。拼接时有一个非常关键的判断if (!res.empty() cur[0] 0) { res 零; }这个判断解决的是跨组空位补零问题。比如10001高组是1输出“壹万”低组是0001组内函数输出“壹”。但低组在自己的最高位之前有三个 0说明万位和个位之间有千位、百位、十位三个空位这时候必须在两组之间补一个“零”于是就有了“壹万零壹元”。某些更复杂的场景比如100000001高组是1输出“壹亿”中间组全 0 直接跳过低组0001输出“壹”同样因为cur[0]0触发补零最后得到“壹亿零壹元”。这个判断用得很省一行代码解决了好几种棘手的跨组零问题。3.4 小数部分为什么不直接拼字符串decToCn的逻辑看起来简单但有几个地方值得停下来想一下。第一它直接取s[0]和s[1]这是基于题目的约定小数部分最多两位第一位是角第二位是分。如果输入是15.1decPart是1那么角位是 1分位是 0输出“壹角”正确。第二角位为 0 但分位非 0 时代码输出一个“零”。比如1.05角位 0分位是 5这时会先拼一个“零”再拼“伍分”得到“壹元零伍分”。这个“零”和小数部分的补零逻辑是一体两面整数部分“元”和“分”之间不能直接跳空补一个字表示中间有个空位。第三如果角位和分位都是 0decToCn返回空串主函数就会在“元”后面追加“整”。这套处理比把小数当成int再取模要稳得多因为字符串天然保留了每一位的实际值根本不存在0.1 * 10不等于 1 这种精度问题。3.5 主函数里的最终装配主函数逻辑很顺读一行找小数点拆出整数部分和小数部分分别调用两个转换函数最后拼装。整数部分为空时intToCn返回空串这里统一补“零”保证0.15能正确输出为“零元壹角伍分”。这个细节很多人会漏因为单独测151121.15时根本不会走到这条分支。细心的话你会发现intToCn(0)返回空串但intToCn()在去掉前导零之前会先触发while (s.size() 1 s[0] 0)的循环这个循环对空字符串没有影响随后if (s 0)不会成立会继续往下走。但实际使用中intPart不会是空串因为输入至少会有一个数字。如果担心输入像是.15这种极端格式可以在拆分时做个额外判断不过机试数据一般不搞这种幺蛾子。4. 踩坑实录与问题排查提交挂了的常见原因4.1 最容易翻车的三个编码事故我在帮别人 review 这题代码时发现三个高频事故点每一个都值得单独拎出来说。事故一前导零没有去掉就分组。比如输入00010010如果不去掉前导零分组结果是0001、0010拼出来会变成“零壹万零壹拾”多了一个不该有的“零”。所以我特意在intToCn开头加了一个while循环把所有前导零吃掉而且保证至少保留一位。吃前导零不是可选项是必须做的前置处理。事故二10输出成“拾”。这个坑最隐蔽因为本地测10时如果只写了if (d 1 unit 拾) 省略壹这类优化就很容易翻车。我的做法是根本不省略任何位置的1都正常输出“壹”。在人民币大写里10必须写作“壹拾”100000必须写作“壹拾万”。如果不确定你把10.00和100000.00两个用例跑一遍立刻就能看出来。事故三组间补零漏掉。比如100000001如果没有在拼接低组时判断cur[0] 0结果会变成“壹亿壹元整”少了中间的零。这个错误尤其难看因为整数部分看起来好像没错但读起来就是不对。我建议写完代码后用一组包含跨组空位的用例集中测试比如10001、10010、100000001把它们放进同一个测试批次一眼就能看出零有没有漏。4.2 提交后常见的 Runtime Error 和 Wrong Answer除了逻辑错误代码本身可能因为字符串操作不当直接崩溃。一个常见问题是decToCn里访问s[1]时没有判断长度。如果输入是15.1decPart长度只有 1直接访问s[1]在 C 里不一定会崩但行为是未定义的某些环境下能跑某些环境就炸了。我在代码里加了if (s.size() 2)的判断就是为了堵这个口子。另一个问题是删字符串前导零时用了erase(0,1)这本身没问题但如果写成while (s[0] 0) s.erase(0,1);一旦字符串全零删到最后s变成空串再访问s[0]就会越界。所以我在条件里加了s.size() 1保证至少留一位。这些问题看起来都是小细节但在机试的判题环境里一次越界访问就能让你整道题零分。写字符串处理类代码时多问自己一句这个访问操作在字符串为空或长度不足时会不会崩4.3 常见问题速查表表现可能原因处理办法输入 10 输出“拾元整”特殊省略了壹所有数字统一输出不省略壹输入 10001 输出“壹万壹元整”组间补零缺失拼接时判断cur[0] 0并补“零”输入 1000 输出“壹仟零零零”末尾零被读出用 pendingZero 标记延迟输出零输入 0.15 输出“元壹角伍分”整数部分为空没处理整数中文为空时补“零元”输入 1.05 输出“壹元伍分”角位0分位非0没补零角位为0且分位非0时先输出“零”输入 0.29 输出角分错误用浮点数处理换成字符串解析输入 00010010 前导多零没去前导0循环清除前导0保留至少一位输入 15.1 直接崩溃访问s[1]越界判长度后再取分位这张表基本覆盖了我在实际测试中遇到的所有问题类型。每次写完代码按这张表的输入跑一遍如果有问题对照原因基本能立刻定位。4.4 自测用例清单别只拿题目样例跑很多同学刷题只跑题目给的样例过了就交这是最危险的做法。题目样例往往为了展示功能设计得很完整但不会覆盖边界。我推荐准备一组“边界炸弹”用例每次写完这类转换题都跑一遍151121.15 10.00 0.15 0.05 1.05 1.50 10001.00 10010.00 100000.00 100000001.00 0.00 00010010.00拿这组用例跑完如果输出都能和预期对上再提交。尤其是0.00和00010010.00这两个前者测整数部分为空和全零后者测前导零清理都是最容易藏坑的地方。如果你想验证自己对“壹拾”的处理把10.00、100000.00都放进去一眼就能判断出来。5. 从机试题到真实业务这题到底在训练什么5.1 真实财务系统里的大写金额转换这道题的场景并不是考试专用。在真实的财务软件、报销系统、电子票据系统里金额转中文大写是一个很常见的需求。只是真实场景通常比机试题更刁钻你可能会遇到负数金额需要输出“负”字比如-123.45要变成“负壹佰贰拾叁元肆角伍分”金额带货币单位比如美元、欧元小数位可能超过两位需要按业务规则四舍五入到分金额上限可能更大达到万亿级别。这些扩展点在机试里基本不考但当你把这道题的字符串处理功力学扎实之后再去接这些需求会顺手很多。比如我的代码里bigUnitCn只写了{, 万, 亿, 兆}如果你要支持“京”只需要在数组里追加一个单位即可分组逻辑完全不用改。另一个真实业务里常见的点是要求金额大写前拼上“人民币”三个字。题目里通常不要求但在实际票据里是标配。开发的时候一定要把“人民币”前缀当成独立配置别写死在转换函数内部否则接口复用会很痛苦。5.2 面试官可能会怎么追问面试官如果看到你写了这题很可能会顺着问几个扩展问题。第一个问题如果不用循环让你递归处理整数部分怎么写这个追问其实是想看你能不能把一个四位的“组处理”抽象成递归式处理当前组然后递归处理更高组最后拼接。我的代码改成递归版本也不难把intToCn里的 for 循环换成“取出最低四位处理剩余部分”的递归调用就行。建议你私下练一遍递归版本能帮你更清楚地理解分组的本质。第二个问题如果输入是负数怎么办这题按题目要求不需要但你可以提出“先接住符号再对绝对值转换”的思路。符号处理应该在拆数之前做避免影响后面的整数部分逻辑。第三个问题如果小数超过两位比如1.235应该保留几位这题默认最多两位但面试官可能故意问你。你至少要知道真实财务规则是四舍五入到分而不是直接截断。也就是说应该先看第三位小数决定是否给分位加一再做转换。这个细节能体现你对业务规则的敏感度。5.3 一道题带出了哪些基本功把 HJ95 整个做下来你会发现它其实在练三件事。第一是字符串边界处理。前导零、空字符串、越界访问这些是写任何解析类程序都绕不开的坎。第二是规则拆解能力。人民币大写的规则并不少但如果你能先把规则枚举成表格再对应到代码逻辑写起来就会非常顺。第三是自测能力。一个题能不能过很大程度上取决于你准备了多少边界用例。这些能力放到真实项目中比“会背某个算法模板”值钱得多。这也是为什么很多大厂机试喜欢出这种题目——它不考你能不能背出某个高级数据结构而是考你能不能把一个有细节、有规则、有边界的问题从头到尾理清楚。我自己的体会是这种题一旦你用“先拆规则、再写函数、后补测试”的方式走一遍之后遇到类似的日期转换、进制转换、中文数字转阿拉伯数字都会轻松很多。它们本质上共享同一套方法论把规则表建清楚把边界场景枚举出来再用干净的代码去填表。最后再分享一个小技巧提交前把代码里的每一个if都问一遍“如果这里是空串怎么办”“如果这里全零怎么办”。我后来刷题养成的习惯是跑完样例后自己补几个“反人类”输入比如0.00、.15如果平台允许、0000.50。这种输入能把隐藏的越界和空串问题逼出来比刷十道新题都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑