ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

华为机试真题解析:字符统计与频率排序完整题解

华为机试真题解析:字符统计与频率排序完整题解 华为机试这四个字在准备校招和跳槽的人眼里基本等于“刷题门槛”。我见过不少基础不错的同学LeetCode刷了两三百题结果栽在华子的机试上原因不是不会写而是不熟悉考试形式、不习惯OJ的判题逻辑或者栽在输入输出这些细节上。华为机试尤其是OD和部分校招岗位的在线笔试一般是在牛客网或自研OJ上做三道算法题总分不一定完全固定核心按用例通过率给分。这篇文章我不打算灌鸡汤就用一道高频考点的模拟题把从审题、设计用例、写代码到排错的完整过程拆给你看。内容适合三类人正在准备华为OD机试的、马上要参加校招上机的、还有想系统练一遍字符串排序这类基础题的。1. 华为机试到底考什么从考核形式到评分逻辑很多第一次参加机试的人上来就刷题结果连考试规则都没搞明白。华为机试不是一个统一标准的考试不同岗位、不同部门、不同招聘批次都可能不一样。但从我接触过的情况来看形式上有很强的共性一般是三道算法题考试时长在150分钟左右涉及字符串、数组、排序、栈、队列、贪心、动态规划和简单图论。1.1 三道题的难度分布与答题节奏常见布局是第一题偏简单第二题中等第三题偏难。分值配置往往是100、200、200也就是说第三题虽然难但分值占比不低。这里有一个容易被忽视的点第一题简单不代表可以随便写它是你整场考试的定心丸。如果第一题因为细节没调完挂了后面心态很容易崩。我建议的答题节奏是这样的第一题拿到后先读两遍题确认输入输出格式然后快速实现建议在30分钟内搞定并运行通过。第二题控制在40到50分钟第三题留足时间思考。如果第三题实在没思路先写暴力解拿部分分不要空着。很多人总想一口吃成胖子第三题追求最优解结果时间耗光了前面的简单题反而没拿满这属于战略失误。1.2 评分规则里容易被忽略的隐藏逻辑在线OJ判题通常是把测试用例拆成多个测试点每个测试点单独跑最终得分是“通过用例数 / 总用例数 × 该题分值”。这意味着什么意味着你只把题目给的示例输入跑通了大概率只能拿到很少的分。我见过的真实案例是有人代码逻辑完全正确但因为没处理空字符串输入导致好几个边界用例直接崩溃最终只拿了30%的分。另外有些场次会有“最低用例通过率门槛”比如某道题必须通过至少60%的用例否则该题计0分。这个比例不一定具体看当时考试通知。但不管门槛是多少暴力解法都值得写上去。还有一种情况是输入可能有多个测试用例循环读取直到EOF。如果题目没说明“只处理一组输入”你就得考虑多组输入的情况。比如C的while(cin s)、Java的while(sc.hasNext())、Python的sys.stdin.read().split()这些都是基本功。2. 模拟题设计字符统计与频率排序选题我纠结了很久最终选了一道“字符统计与频率排序”。原因很简单这类题是华为机试里最经典的入门题型既有字符串处理又有自定义排序还有边界条件判断一道题能串起好几个高频考点。2.1 完整题目描述与样例题目描述如下题目字符统计与频率排序 给定一个由大小写字母和数字组成的字符串 s1 len(s) 1000请统计每种字符出现的次数并按“出现次数从高到低、次数相同时按字符ASCII码从小到大”的顺序输出所有出现过的字符及其次数。 输出格式每行一个“字符:次数”全部输出结束后程序结束。 如果输入的字符串为空输出 EMPTY。示例输入aabbbcA示例输出b:3 a:2 A:1 c:1注意输出顺序里A和c都是出现一次按ASCII码排A的ASCII是65c的ASCII是99所以A排在c前面。这个陷阱如果不仔细看题很容易写反。2.2 审题阶段的关键判断到底考什么拿到这道题我脑子里会快速过一遍它涉及的考点第一字符计数。最直接的想法是用哈希表但更合适的做法是用一个长度为128的数组因为ASCII码范围就这么大。数组计数的时间复杂度是O(n)空间是O(1)写起来也简单不需要引入额外的hash结构。第二自定义排序。排序规则是“次数降序次数相同按ASCII升序”。这个规则在C里要写lambda表达式在Java里要写Comparator在Python里可以直接用元组作为排序key。关键是搞清楚排序的优先级先比次数次数相同才比ASCII码。第三边界条件。字符串为空时输出EMPTY这很多人会忽略。还有字符串可能包含数字字符数字字符也是有ASCII码的不要和整数值搞混。比如字符0的ASCII码是48不是0。审题这件事看起来简单实际很考验人。我见过不少人拿到题就开始写写到一半发现排序规则理解错了又回头改一来一回浪费十几分钟。正确的做法是先把输入、输出、规则、边界四个要素圈出来在草稿纸上写清楚再动键盘。2.3 测试用例设计先想清楚再动键盘很多人写代码之前不设计用例直接凭感觉写。真正的机试老手会先在脑子里过一遍测试用例尤其是边界用例。这题我通常设计下面几个用例用例输入预期输出验证点基础用例aabbbcAb:3 / a:2 / A:1 / c:1计数与排序正确性空字符串空行EMPTY边界处理单字符zz:1单元素场景全部相同aaaaa:4去重合并大小写混合AaA:1 / a:1ASCII排序逆序输入zyxx:1 / y:1 / z:1排序稳定性含数字1a2aa:2 / 1:1 / 2:1数字字符处理构造测试用例的能力往往比写代码本身更能决定机试成绩。因为用例越全提交前的自查越到位。你不需要把用例实际跑一遍但必须在心里过一遍每个用例的输出。3. 核心实现三种主流语言的完整写法这一节我会分别给出Python、C、Java三种语言的实现并讲解关键代码背后的思路。不是为了凑篇幅而是因为机试时你只能用一种语言但不管选哪种核心逻辑都是一致的。3.1 Python 版本最适合快速解题Python在机试里的优势是代码短、写起来快尤其适合字符串处理。下面是完整的参考实现def solve(s: str) - None: if not s: print(EMPTY) return cnt [0] * 128 for ch in s: cnt[ord(ch)] 1 items [(chr(i), cnt[i]) for i in range(128) if cnt[i] 0] items.sort(keylambda x: (-x[1], x[0])) for ch, c in items: print(f{ch}:{c}) if __name__ __main__: s input() solve(s)这里重点讲一下排序key的写法(-x[1], x[0])。Python的排序默认是升序为了让次数从大到小排最简单的方式是取负数。次数相同的时候再按ASCII码升序也就是按x[0]排字符本身就是ASCII码顺序。用数组而不是字典在这个题里是更优的选择。字典当然也可以但数组的代码更直接遍历一次字符串ord(ch)拿到ASCII码对应下标加一。最后用列表推导式把计数大于0的字符和次数筛出来再排序输出。整体时间复杂度和空间复杂度都很优秀。3.2 C 版本最贴近生产环境如果是C岗位的机试用C写更符合岗位预期。参考实现如下#include bits/stdc.h using namespace std; int main() { string s; if (!getline(cin, s)) return 0; if (s.empty()) { cout EMPTY endl; return 0; } int cnt[128] {0}; for (char c : s) { cnt[(unsigned char)c]; } vectorpairchar, int items; for (int i 0; i 128; i) { if (cnt[i] 0) { items.push_back({(char)i, cnt[i]}); } } sort(items.begin(), items.end(), [](const auto a, const auto b) { if (a.second ! b.second) return a.second b.second; return a.first b.first; }); for (auto p : items) { cout p.first : p.second endl; } return 0; }几个细节值得说明。第一读取用getline(cin, s)而不是cin s因为输入可能包含空格。第二循环遍历时用(unsigned char)c原因在于char类型在部分编译器下是有符号的如果字符串里出现ASCII码大于127的扩展字符直接转int可能变成负数数组下标会越界。这题的输入范围是字母和数字不会出现扩展字符但养成这个习惯没坏处。第三lambda表达式的返回值先比较次数次数不同直接按次数降序次数相同再按字符升序。3.3 Java 版本习惯Java的人不要临时换语言Java在OJ上的模板性比较强写熟了也不会慢。参考实现如下import java.util.*; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); String s sc.nextLine(); if (s.isEmpty()) { System.out.println(EMPTY); return; } int[] cnt new int[128]; for (char c : s.toCharArray()) { cnt[c]; } Listint[] list new ArrayList(); for (int i 0; i 128; i) { if (cnt[i] 0) { list.add(new int[]{i, cnt[i]}); } } list.sort((a, b) - { if (a[1] ! b[1]) return Integer.compare(b[1], a[1]); return Integer.compare(a[0], b[0]); }); for (int[] p : list) { System.out.println((char) p[0] : p[1]); } } }Java这里我用了Listint[]来存字符的ASCII码和次数而不是存char这样比较器里可以直接对整数排序。排序规则用lambda表达式逻辑一目了然先按次数降序再按ASCII升序。Integer.compare避免了直接相减可能带来的溢出问题虽然这道题不会有溢出但写成Integer.compare更保险。Java机试最常见的坑是类名和包名。OJ要求主类名必须是Main不要加package也不要用public class Solution之类的名字否则编译直接报错。还有一个坑是Scanner读取时如果第一行是空行nextLine()会返回空字符串所以判空逻辑要放在最前面。4. 机试现场最容易踩的坑与排查技巧这部分是我最想分享的。代码本身不难但机试现场的坑几乎都在代码之外。4.1 输入读取的四大典型坑输入读取是机试翻车的第一重灾区我总结成四个典型问题问题表现正确姿势字符串含空格cin s只读到空格前C用getline(cin, s)Python用input()默认读整行没问题多个测试用例只处理了一组就结束C用while(getline(cin, s))Java用while(sc.hasNextLine())空行输入直接读取到空串后续逻辑崩溃先判空再做处理数字和字符串混合nextInt()后nextLine()读不到正确内容在nextInt()后补一个nextLine()消费换行符或者全部按字符串读再解析说实话输入输出这块很多题库里刷题时不会遇到问题因为力扣已经把输入输出封装好了。但华为机试是传统的ACM模式输入输出全部自己处理。这就是为什么很多人刷题很顺一到机试就拉胯。平时练习一定要用牛客或者Online Judge去练别只在力扣上刷。4.2 边界条件与输出格式低分重灾区这道题我设计的时候特意加了空字符串输出EMPTY的条件。这个条件在面试官看来是合理的边界处理但在很多考生眼里是“题目没明说的隐藏条件”。实际上题目写了“如果输入的字符串为空”那就是要处理。输出格式方面常见的扣分点包括多输出空格、少输出换行、字母大小写不对、数字和字符顺序颠倒。有些OJ对末尾换行不敏感但为了保险建议每行输出都带换行符。还有如果题目要求“每行一个”不要为了方便把所有结果用空格拼在一起容易被判格式错误。4.3 性能与复杂度的判断标准这题的数据范围是1000随便怎么写都能过。但机试现场的题目经常会在数据范围上做文章比如10^5、10^6。遇到这种情况你的实现必须能撑得住。数组计数的复杂度是O(n)排序的复杂度是O(k log k)其中k是出现过的字符种类数最大也就128所以整体可以认为近似O(n)。这是很优秀的复杂度。如果换一种实现方式比如用HashMap统计再用列表排序复杂度也一样但常数更大代码也更长。需要注意的一点是如果题目数据范围更大比如字符串长度达到10^6其实还有更快的做法因为字符集大小固定可以先用桶计数然后直接按“次数降序、ASCII升序”的规则把桶里的数据倒出来排序。由于桶最多128个排序就没必要用快排了可以按次数从高到低遍历每个次数内部按ASCII码从低到高输出。这样连排序都省了。5. 从这道题延伸机试备考的核心策略一道题讲完了但备考是个系统工程。最后这部分我把自己总结的核心策略分享出来不一定适合所有人但方向应该不会错。5.1 常见考点清单优先刷哪些结合我这些年看到的题目华为机试常见考点大概有这些字符串处理反转、统计、去重、子串查找、进制转换数组与简单数据结构栈、队列、哈希表、堆排序与自定义比较器按规则排序、稳定排序、局部排序双指针与滑动窗口连续子数组问题贪心算法区间调度、活动安排简单动态规划背包问题、最长上升子序列、编辑距离模拟题按照题目要求走流程考验细心程度简单图论图的遍历、最短路径偶尔考如果你是零基础或者时间紧优先掌握前五种。第一题基本就是字符串和数组第二题经常是排序贪心或者数据结构第三题才会上动态规划这类更复杂的东西。5.2 练习“5分钟审题”和“一次编译通过”我练习机试时有一个习惯拿到题先给自己5分钟只做三件事第一把输入输出格式圈出来第二看数据范围判断能不能暴力第三想清楚所有边界情况。这三件事做完再开始写代码。大多数人的问题是审题没有结构化写到一半才发现漏了条件。另外一个很重要的能力是“一次编译通过”。机试环境的IDE往往不带智能提示或者即使带了也会因为网速、浏览器卡顿影响手感。平时练习时我建议关掉代码补全先在纸上写一遍再敲到编辑器里编译运行。刚开始会很痛苦但练上十几道题后手写代码的准确率会明显提高。5.3 时间分配与部分分策略再回到时间分配。假设总分是100200200考试时间150分钟。我推荐这样分配时间段任务目标0-30分钟第一题满分不留隐患30-80分钟第二题争取满分至少部分分80-130分钟第三题最优解或暴力解130-150分钟检查全局复查不轻易改代码最后留出的20分钟检查时间很重要。检查时优先看变量名是否打错、数组下标是否越界、有没有多输出或少输出、特殊输入是否处理了。我见过有人提前半小时交卷结果因为一个小小的输出空格错误丢了十几分非常可惜。第三题如果完全没思路先把暴力写法敲上去。比如有些题要用动态规划你没思路但可以用DFS回溯枚举所有可能虽然会超时但能过小数据用例拿30%到50%的分并不难。机试不是竞赛拿满所有用例才叫满分而部分分也是分。我个人做机试模拟时有一个习惯一道题做完不会立刻跳下一道而是问自己三个问题。换一种输入方式还能不能过数据量放大一百倍还能不能过让我在纸上重新写一遍能不能写出来这三个问题比多刷十道题都管用。毕竟机试考的不只是算法更是你在限时压力下把问题理清楚、把代码写稳的能力。希望这篇对正在准备华为机试的你有所帮助。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进