行业资讯
C++实现人民币大写转换:华为OD机试经典题解与工程实践
1. 项目概述与核心价值最近在准备华为OD机试的朋友应该都绕不开“人民币转换”这道经典题目。它不仅是B卷的常客更是检验一个程序员基本功和思维严谨性的绝佳试金石。题目本身并不复杂给你一个不超过两位小数的数字要求你将其转换为符合中文阅读习惯的大写金额表示。听起来简单对吧但真正动手实现尤其是用C这种需要精细控制细节的语言你会发现从“能跑通”到“完全符合规范”之间隔着无数个“坑”。我之所以花时间深入研究并实现它是因为这道题完美融合了字符串处理、逻辑分支、边界条件处理以及对业务规则金融规范的深刻理解。它不像纯粹的算法题那样比拼奇技淫巧更像是一个微型的工程项目要求你的代码健壮、清晰、无懈可击。无论是为了通过机试还是为了提升自己处理复杂业务逻辑的编码能力吃透这道题都大有裨益。接下来我将结合我的实现拆解其中的每一个技术细节和思维过程。2. 问题解析与设计思路2.1 题目要求深度解读首先我们必须百分百明确题目的要求任何歧义都会导致失分。核心规则如下输入一个字符串表示一个数字例如15005.08。题目保证数字在合理范围内且最多两位小数。输出中文大写金额字符串例如壹万伍仟零伍元捌分。关键规则单位元、角、分。角、分仅在对应数值不为0时输出。例如0.05元输出“伍分”而非“零元伍分”。整数部分采用“万、亿”为单位分段处理。每四位一组从低位到高位依次是“元”、“万”、“亿”。单位只在当前段有数值时才出现。零的读法这是最易错的部分。连续的多个0只读一个“零”。某一位是0但其所在的“段”个、十、百、千全部为0时这个“零”不读。例如1000读作“壹仟”而不是“壹仟零”。万位和亿位是0时需要特殊处理。例如100001000元应读作“壹亿零壹仟元”中间的“万”单位因为万位段全零而被省略但需要用“零”连接亿和千。开头结尾不能有多余的“零”也不能缺少“元”字当整数部分为0时如0.12元输出“壹角贰分”还是“零元壹角贰分”根据常见规范是前者。2.2 核心算法设计思路面对这样的规则直接对数字进行数学运算是非常麻烦的因为我们需要精确知道每一位的数字。更优雅的做法是将输入字符串作为核心处理对象。我的设计思路分为以下几个清晰步骤预处理与分割将输入字符串以小数点.为界分割为整数部分字符串intPart和小数部分字符串decPart。decPart直接补齐或截断到2位代表角和分。整数部分转换核心这是难点。我采用“分治”思想将整数部分字符串从右至左每4位分成一组即个、万、亿段。对每一组独立进行“千位到个位”的中文转换然后根据组的位置0-元1-万2-亿加上段单位。小数部分转换简单映射0-1位是角1-2位是分。注意处理decPart长度不足2的情况。拼接与后处理将整数部分结果、“元”、小数部分结果拼接。然后应用一系列正则化规则处理多余的“零”、开头结尾的“元”等问题得到最终结果。这个思路的优势在于模块化清晰每一部分职责单一方便调试和验证。2.3 数据结构与函数规划基于上述思路我们需要以下核心数据与函数常量映射表const std::vectorstd::string digitMap {零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖}; const std::vectorstd::string unitMap {, 拾, 佰, 仟}; // 个十百千 const std::vectorstd::string sectionUnit {, 万, 亿}; // 段单位核心函数std::string convertSection(const std::string section, bool isLastSection)将一个4位数字符串或不足4位转换为中文isLastSection标识是否为最后一段个位段用于处理末尾“零”的规则。std::string convertIntegerPart(const std::string intPart)协调分段调用convertSection并添加段单位。std::string convertDecimalPart(const std::string decPart)处理角和分。std::string normalizeResult(const std::string rawResult)对拼接后的原始结果进行清洗处理多个“零”、开头结尾等问题。3. 核心模块实现详解3.1 整数部分分段转换实现convertSection函数是基石。它接收一个字符串例如0301或12输出如“零叁佰零壹”或“壹拾贰”。std::string convertSection(const std::string section, bool isLastSection) { std::string result; int len section.length(); bool lastZero false; // 标记上一位是否是零 bool hasNonZero false; // 标记当前段是否有非零数字 for (int i 0; i len; i) { int digit section[i] - 0; int pos len - 1 - i; // 0-个位1-十位2-百位3-千位 if (digit 0) { // 当前位是0 if (!lastZero hasNonZero) { // 如果上一位不是0且前面已经出现过非零数字则添加一个“零” // 避免开头是零也避免连续多个零 result digitMap[0]; } lastZero true; } else { // 当前位非0 hasNonZero true; // 添加数字和单位。注意当数字是1且单位是“拾”时通常省略“壹”读作“拾X” if (!(digit 1 pos 1 i 0)) { // i0 表示是当前段的第一个字符 result digitMap[digit]; } result unitMap[pos]; lastZero false; } } // 后处理如果整个段都是0返回空字符串。 // 如果不是最后一段且末尾是“零”需要去掉这个零因为段单位前不应有零。 // 例如 “零叁佰零” “万”应该变成 “零叁佰” “万” if (!hasNonZero) { return ; } // 移除末尾的“零”除非是最后一段的个位需要保留如“壹仟零” if (!result.empty() result.back() 零 !(isLastSection len 4)) { result.pop_back(); // 假设“零”是一个字符。实际是UTF-8需按字符串处理。 // 更严谨的做法 if (result.size() 3 result.substr(result.size()-3) 零) { result.erase(result.size()-3); } } // 注意上面关于“零”字符的处理是简化版。实际中文字符在UTF-8下占3字节需要用std::string的find/substr处理。 // 为清晰起见下文会给出处理中文字符串的辅助函数。 return result; }注意这里暴露了C处理中文的一个关键坑点。std::string存储的是字节一个UTF-8中文字符占3个字节。像result.back() 零这样的比较是错误的。我们必须以字符串为单位进行操作。这是机试中极易忽略导致错误的地方。因此我们需要一个辅助函数来处理中文“字”// 判断字符串str是否以后缀suffix结尾 bool endsWith(const std::string str, const std::string suffix) { if (suffix.size() str.size()) return false; return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin()); } // 移除字符串末尾的指定后缀 void removeSuffix(std::string str, const std::string suffix) { if (endsWith(str, suffix)) { str.erase(str.size() - suffix.size()); } }然后在代码中将result.back() 零替换为endsWith(result, 零) 将result.pop_back()替换为removeSuffix(result, 零)。3.2 协调分段与添加段单位convertIntegerPart函数负责将整个整数部分字符串按4位分组从右向左处理并调用convertSection。std::string convertIntegerPart(const std::string intPart) { std::string result; int len intPart.length(); int sectionIndex 0; // 0-元段1-万段2-亿段 std::string tempIntPart intPart; // 处理前导零但需要保留至少一位如果全部是零 size_t nonZeroPos tempIntPart.find_first_not_of(0); if (nonZeroPos std::string::npos) { return ; // 整数部分全零 } else if (nonZeroPos 0) { tempIntPart tempIntPart.substr(nonZeroPos); } len tempIntPart.length(); int pos 0; while (pos len) { // 确定当前段的结束位置字符串的左端 int end len - pos; int start std::max(0, end - 4); // 每次取最多4位 std::string section tempIntPart.substr(start, end - start); // 反转section因为convertSection是按从左到右高位到低位处理的而substr取出来的是高位部分。 // 更好的方法是让convertSection适应从右向左的位次或者这里先补零对齐。 // 让我们调整一下保证section是4位不足左补零。 section std::string(4 - section.length(), 0) section; bool isLastSection (sectionIndex 0); // 当前段是否是个位段最后一段 std::string sectionStr convertSection(section, isLastSection); if (!sectionStr.empty()) { // 段结果 段单位 之前的累计结果 result sectionStr sectionUnit[sectionIndex] result; } else { // 当前段全零 // 如果亿段为空万段不为空且元段不为空需要在万和元之间加“零” // 这个逻辑比较复杂我们可以在最后的normalizeResult中统一处理连续的零和段单位缺失。 // 这里暂时不做处理仅当当前段非空时才加段单位。 } pos 4; sectionIndex; } // 如果最终结果为空说明整数部分为0 if (result.empty()) { result 零; } return result; }这个版本仍有瑕疵特别是在处理段间零和段单位逻辑时。更稳健的方法是先按4位一组分割好存入一个vector然后从左到右从高位到低位处理每一组这样逻辑更顺。3.3 小数部分与最终拼接小数部分处理相对直接std::string convertDecimalPart(const std::string decPart) { std::string result; std::string dec decPart; // 补齐或截断到2位 if (dec.length() 2) { dec.append(2 - dec.length(), 0); } else if (dec.length() 2) { dec dec.substr(0, 2); // 题目保证不会但防御性编程 } int jiao dec[0] - 0; int fen dec[1] - 0; if (jiao 0) { result digitMap[jiao] 角; } if (fen 0) { result digitMap[fen] 分; } return result; }主函数负责流程串联std::string convertCurrency(const std::string input) { // 1. 分割整数小数 size_t dotPos input.find(.); std::string intPart, decPart; if (dotPos ! std::string::npos) { intPart input.substr(0, dotPos); decPart input.substr(dotPos 1); } else { intPart input; decPart ; } // 2. 转换各部分 std::string intStr convertIntegerPart(intPart); std::string decStr convertDecimalPart(decPart); // 3. 拼接 std::string rawResult; if (intStr 零 decStr.empty()) { rawResult 零元; } else if (intStr 零) { rawResult decStr; // 例如 0.12 - “壹角贰分” } else { rawResult intStr 元 decStr; } // 4. 规范化 return normalizeResult(rawResult); }4. 字符串规范化与边界处理这是将“基本正确”的代码提升到“完全正确”的关键步骤。normalizeResult函数需要处理一系列棘手的规则。4.1 规范化规则与实现std::string normalizeResult(const std::string raw) { std::string result raw; // 规则1合并连续的“零” std::string from 零零; std::string to 零; size_t pos 0; while ((pos result.find(from, pos)) ! std::string::npos) { result.replace(pos, from.length(), to); pos to.length(); // 继续查找避免替换后产生新的连续零 } // 规则2处理“零万”、“零亿” - 通常应为“零”但需结合上下文。 // 例如 “壹亿零万叁仟元” - “壹亿零叁仟元”。实际上在convertIntegerPart中应避免生成“零万”。 // 更常见的错误是 “X亿零万Y”应删除“零万”。 from 零万; to 零; pos 0; while ((pos result.find(from, pos)) ! std::string::npos) { // 检查“零万”前面是不是数字如果是则替换为“零” result.replace(pos, from.length(), to); } // 同理处理“零亿” from 零亿; pos 0; while ((pos result.find(from, pos)) ! std::string::npos) { result.replace(pos, from.length(), to); } // 规则3处理开头是“零”的情况如“零壹拾...”应去掉开头的零除非整个就是“零元” if (result.size() 3 result.substr(0, 3) 零 result ! 零元) { // 但需注意“零元”是合法的输入为0.00。而“零壹角”是不合法的应去掉零。 // 简单判断如果“零”后面跟的是单位元、角、分、拾、佰、仟、万、亿则不能删。 // 这里简化处理如果以“零”开头且长度3且第二个字符不是单位字则删除。 // 单位字集合元角分拾佰仟万亿 std::string units 元角分拾佰仟万亿; if (result.size() 3 units.find(result.substr(3, 3)) std::string::npos) { result result.substr(3); } } // 规则4处理结尾是“零”的情况如“壹仟零” - “壹仟” if (endsWith(result, 零) !endsWith(result, 元零)) { // “元零”如“壹仟元零”是不存在的应去掉零 removeSuffix(result, 零); } // 规则5确保“元”的存在。如果经过上述处理结果不含“元”且整数部分不为空即不是纯小数则在末尾加“元”。 // 但更常见的情况是纯小数如0.12的结果是“壹角贰分”没有元这是正确的。 // 我们需要判断原始整数部分是否为“零”。这步最好在拼接时处理这里做最终检查。 // 如果结果以“角”或“分”开头且前面没有“元”那是正确的纯小数格式。 // 如果结果包含“万”、“亿”等单位但结尾没有“元”则需要补“元”。 // 这个逻辑比较复杂一个更简单粗暴但有效的办法是在convertCurrency的拼接阶段就定好格式。 return result; }4.2 边界条件测试用例设计全面的测试用例是验证代码正确性的唯一途径。以下是一些关键用例输入期望输出测试点0零元零值处理0.00零元零值带小数0.12壹角贰分纯小数无“元”5.6伍元陆角一位小数10.03壹拾元零叁分角为0分不为01000壹仟元整千末尾无零1005壹仟零伍元中间有零1010壹仟零壹拾元中间有零末尾是十位11000壹万壹仟元万位段非零100001000壹亿零壹仟元万位段全零需用“零”连接100000000壹亿元中间多个零段123456789.12壹亿贰仟叁佰肆拾伍万陆仟柒佰捌拾玖元壹角贰分综合大数100000001.01壹亿零壹元零壹分极端零值测试在实现过程中务必用这些用例反复测试调整convertIntegerPart的分段逻辑和normalizeResult的规则顺序。5. 常见陷阱与调试心得这道题在机试环境如牛客网、华为OJ中提交很容易因为一些隐蔽的细节导致无法AC全部用例通过。以下是我踩过或见过的坑5.1 字符编码与字符串处理这是最大的坑没有之一。C的std::string是字节串而题目输入输出都是UTF-8编码的中文。错误做法if (str[i] 零)。一个中文字符在UTF-8下占3个字节直接比较单个char毫无意义。正确做法所有涉及中文字符的比较、查找、替换都必须以3个字节为单位进行。这就是为什么前面要定义endsWith和removeSuffix函数。在查找“零零”时实际要查找的是6个连续的特定字节。实操心得在开始编码前先写好两个辅助函数bool startsWithCN(const string str, const string prefix)和bool endsWithCN(const string str, const string suffix)以及对应的replaceCN函数。这会节省大量调试时间。5.2 “壹拾”的简化读法规则要求对于“10-19”元中的“10”应读作“拾”而不是“壹拾”。但注意这个规则通常只适用于“元”单位前。例如10-“拾元”(正确)110-“壹佰壹拾元”(十位上的1不能省略)100010-“壹拾万零壹拾元”(十万位上的1不能省略十位上的1也不能省略这里“壹拾万”的“拾”前有“壹”而“零壹拾”的“拾”前没有“壹”) 实际上常见的中文金额大写规范是只有位于整数部分最高位的“十位”上的“1”可以省略。例如10: 最高位是十位省略“壹” -“拾元”110000: 最高位是十万位即“壹拾万”这个“壹”不能省 -“壹拾壹万元”不对是“壹拾壹万”让我们分解11,0000 - “壹拾壹万”。这里“壹拾”的“壹”是十万位不能省。 所以简化规则是在转换一个4位段千位到个位时如果该段的第一个非零数字出现在十位上且这个数字是1则省略这个“壹”。这个逻辑需要在convertSection函数中实现即我代码中的if (!(digit 1 pos 1 i 0))判断。i0表示这是当前段转换过程中遇到的第一个非零数字。5.3 段间“零”的处理逻辑这是逻辑最复杂的地方。当“万”位段全为0时我们不应该输出“万”但需要在“亿”和“元”之间加一个“零”。例如100001000-“壹亿[零]壹仟元”。 我的建议是在convertIntegerPart中不要急于将段结果和段单位拼接。而是先收集每一段的转换结果可能为空然后从左到右从高位到低位扫描这些段结果智能地添加“零”和段单位。重构后的convertIntegerPart思路将整数部分字符串按4位一组从右向左分割并存储到vectorstring中每组4位左补零。对每一组调用convertSection得到中文串sectionStr。创建一个新的vectorstring用于存放最终结果段。从高位段向低位段遍历如果sectionStr非空将其加入结果并加上对应的段单位“亿”、“万”、“”。如果sectionStr为空即全零段如果这不是最后一个非空段之后的第一个全零段则标记需要加“零”。更简单的策略记录上一个加入结果的段是否是非空段。如果是且当前段全零则在结果中加入一个“零”并标记“已加零”直到遇到下一个非空段才清除标记防止连续加零。最后将结果段的字符串连接起来。这个逻辑实现起来需要仔细设计状态机但一旦实现就能干净利落地处理所有零值边界。5.4 输入输出格式机试系统通常是标准输入输出。输入可能包含多组测试用例直到文件结束EOF。你的程序需要能循环读取。输出每个结果一行严格按照要求不能有多余空格或换行。一个健壮的主函数框架#include iostream #include string #include vector #include algorithm // ... 其他头文件和函数声明 int main() { std::string line; while (std::getline(std::cin, line)) { // 可能需要对line做trim去除首尾空白符 line.erase(0, line.find_first_not_of( \t\n\r)); line.erase(line.find_last_not_of( \t\n\r) 1); if (line.empty()) continue; // 处理空行 std::string result convertCurrency(line); std::cout result std::endl; } return 0; }6. 完整代码实现与优化建议综合以上所有讨论下面给出一个经过重构、相对健壮的实现框架。由于篇幅限制这里只勾勒核心结构关键函数已在前文详细阐述。#include iostream #include string #include vector #include algorithm #include cctype class RMBConverter { private: const std::vectorstd::string digitMap {零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖}; const std::vectorstd::string unitMap {, 拾, 佰, 仟}; const std::vectorstd::string sectionUnit {, 万, 亿}; // 辅助函数处理中文字符串 bool endsWithCN(const std::string str, const std::string suffix) { // ... 实现见前文 } void removeSuffixCN(std::string str, const std::string suffix) { // ... 实现见前文 } void replaceAllCN(std::string str, const std::string from, const std::string to) { // ... 实现类似但按3字节单位查找替换 } std::string convertSection(const std::string section, bool isFirstSection, bool isLastSection) { // 重构增加 isFirstSection 参数标识是否是最高位段用于处理“壹拾”省略 // ... 实现逻辑特别注意零的处理和“壹拾”规则 } std::string convertIntegerPart(const std::string intPart) { // 重构使用vector存储段结果和段单位智能添加“零” // 1. 去除前导零 // 2. 按4位分组不足补零 // 3. 遍历每组调用convertSection // 4. 从高到低组装结果处理段间零 // 5. 如果最终为空返回零 } std::string convertDecimalPart(const std::string decPart) { // ... 同前 } std::string normalizeResult(const std::string raw) { // ... 集成前述所有规则注意调用replaceAllCN处理中文 } public: std::string convert(const std::string input) { // ... 主流程同前 } }; int main() { RMBConverter converter; std::string line; while (std::getline(std::cin, line)) { // 输入清理 line.erase(line.find_last_not_of( \n\r\t) 1); line.erase(0, line.find_first_not_of( \n\r\t)); if (line.empty()) continue; std::cout converter.convert(line) std::endl; } return 0; }最后的优化建议提前构造测试集将第4.2节的测试用例写成数组在本地反复测试确保每个都能通过。使用调试输出在开发阶段可以在关键函数内打印中间结果例如打印每一段的转换结果观察拼接过程。关注性能虽然本题数据量小但良好的习惯是使用std::string的reserve预分配空间避免频繁内存分配。代码风格即使机试时间紧也要保持函数功能单一、命名清晰。这不仅能减少错误也能让阅卷人或面试官留下好印象。人民币转换这道题就像一面镜子照出程序员对细节的掌控力和系统化思维的能力。它没有高深的算法但想把所有边界条件都处理得滴水不漏需要耐心、严谨和大量的测试。希望这篇详细的拆解能帮助你不仅通过这道题更能理解其中蕴含的软件工程思想。在机试中遇到时祝你一次AC。
郑州网站建设
网页设计
企业官网