出现频率最高的字符
题目描述
给定一个字符串,找出其中出现频率最高的字符以及对应的出现次数。
注意点:
- 字符串可能包含空格、符号或数字。
- 可能存在多个字符出现的频率并列最高,此时应当返回所有符合条件的字符。
- 如果字符串为空,应返回合理的默认值(如空数组和计数 0)。
示例:
- 输入:
"aabbc"-> 输出: 频率为 2,字符为['a', 'b'] - 输入:
"apple"-> 输出: 频率为 2,字符为['p']
思路分析
核心的思路是利用哈希表或者字典来记录每个字符出现的次数,然后通过一次遍历找到最大值。即先统计,再筛选。
- 遍历字符串,用一个对象或者是
Map记录每个字符出现的次数,用字符作为键,出现次数作为值。 - 在遍历过程中,维护两个变量记录出现次数最多的字符以及该字符出现的次数。分别记作
maxChar和maxCount。 - 每轮循环中,将字符出现的次数和
maxCount进行比较和更新。
解题方案
复杂度分析
- 时间复杂度是
O(n)。主要是对字符串的遍历,虽然使用了Object.keys和filter,但这是属于对频率表的二次遍历,它通常远小于字符串的长度。 - 空间复杂度是
O(k)。用于存储频率表和结果数组。
进阶优化
使用更现代的风格对代码进行重构,让代码看起来更简洁,更高级。
上述代码为什么显得 “高级” ?
- 使用解构赋值、展开运算符、
Map、reduce等更现代的语法和 API。 - 所有的逻辑都封装在数据转换的过程中,而不是通过修改外部变量来完成。
- 它是一段声明式逻辑,它告诉程序 “我要把这个对象
reduce成一个结果”,而不是让机器 “先做第一步,再做第二步”。
注意:虽然这 reduce 用起来很简洁,但是面对超大规模数据时,传统的 for..of 循环性能会更好。
问题扩展
当需求从 “找第 1 名” 变为 “找前 n 名” 时,问题的性质就从简单的最大值变成了排序或堆的问题了。
核心思路:
- 统计频率,依然使用
Map记录每个字符出现的次数。 - 转换为数组,将
Map转换为键值对数组。 - 排序,依据出现的次数对数组进行降序排序。
- 截取出排序后的前 n 个元素。
常规解法
虽然这个代码看起简洁,但是存在一些性能问题。首先统计频率的时间复杂度为 O(n),在排序阶段假设有 m 个不重复的字符,由于 sort 的时间复杂度为 O(mlogm)。这样一来,整体的复杂度来到了 O(k + mlogm)。
如果字符串非常大,但是我们只需要前三名,用 sort 去排序就会很浪费。
小顶堆解法
可以采用小顶堆(Min-Heap)来进行优化。维护一个大小只有 n 的小顶堆,遍历频率表的时候,如果当前字符频率比堆顶大,就替换堆顶并重新调整。
这样可以将复杂度降低到 O(mlogn)。由于 n 远小于 m,这在处理超长字符时更有优势。
为什么选用小顶堆来找 “最大” ?
想象一个只有 n 个座位的房间,我们需要找到最强的 n 个人:
- 让前 n 个人进去。
- 用里面最弱的那个人(堆顶)守在大门口。
- 门来每来一个新人,只跟门里最弱的那个人比较。
- 如果新人更强,就把最弱的踢走,新人进去。
- 最后房间里剩下的就是最强的 n 个人。
注:如果使用大顶堆,则需要对所有的数据进行堆排序后才能知道哪些是最大的。