LeetCode 692. 前K个高频单词:多种解法与实现技巧
文章目录
- 题目描述
- 1. 使用堆(Priority Queue)解决TopK问题
- 2. 利用排序实现
- 知识复习:稳定排序和非稳定排序
- 稳定排序(Stable Sort)
- 非稳定排序(Unstable Sort)
- 1. 非稳定排序(快排)
- 2. 稳定排序
- 4. 借助multimap反转键值对
题目描述
给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
示例 1:
输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 "i" 在 "love" 之前。
示例 2:
输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。
注意:
- 1 <= words.length <= 500
- 1 <= words[i] <= 10
- words[i] 由小写英文字母组成。
- k 的取值范围是 [1, 不同 words[i] 的数量]
在解决LeetCode 692题时,我们需要找到出现频率最高的前K个单词,并按频率降序排列。如果频率相同,则按字典序升序排列。本文将介绍多种解法,包括堆、排序(稳定/非稳定)、以及multimap反转键值对的方法,帮助读者深入理解问题并掌握相关技巧。
1. 使用堆(Priority Queue)解决TopK问题
- 用map统计每个单词出现次数
- 自己实现仿函数实现优先级规则,把所有pair(单词,频率)放入一个优先级队列
- 回归topK问题,取出前k个数据
关键:仿函数Less
- 频率不同,出现频率低的优先级低
- 出现频率相同,则字典顺序大的优先级低
本题建大堆,less,实现less即可。先考虑value的比较(出现频率),若相同,比较key(string的字典顺序)
class Solution {
public:
//less这里是比较的优先级的大小
struct myLess{
//最好+const,因为可能这里是生成的临时对象,具有常性
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const{
//1.比较频率
if(kv1.second < kv2.second){
return true;
}
//2.比较字典顺序 字典序大于 看作 优先级小于
if(kv1.second==kv2.second && kv1.first>kv2.first){
return true;
}
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
//先统计次数
map<string,int> mp;
for(const auto& str:words){
mp[str]++;
}
//topK问题
//因为要考虑 出现频率和字典顺序的两个优先级,因此自己实现一个仿函数
//这里是priority_queue的构造函数弊端:容器适配器默认采用vector一般不需要更改,但是缺放在了第二个参数位置,所以自己实现仿函数的时候就需要把容器也写上;按理应该把仿函数放在第二个位置
priority_queue<pair<string,int> , vector<pair<string,int>> ,myLess> maxHeap(mp.begin(),mp.end());
vector<string> res;
//取maxHeap中前k个数据的first放入 vector
while(k--){
res.push_back(maxHeap.top().first);
maxHeap.pop();
}
return res;
}
};
2. 利用排序实现
知识复习:稳定排序和非稳定排序
稳定排序和非稳定排序是排序算法中两个重要的概念,它们主要关注在排序过程中,相等元素的相对顺序是否保持不变。
稳定排序(Stable Sort)
- 定义:如果排序算法能够保证相等元素在排序后的相对顺序与排序前的相对顺序一致,则该算法是稳定排序。
- 举例:假设有一个序列
[A, B, C, D]
,其中B
和C
的值相等(即B = C
)。如果排序后B
仍然在C
的前面(即[A, B, C, D]
),则该排序是稳定的。 - 常见的稳定排序算法:
- 冒泡排序(Bubble Sort)
- 插入排序(Insertion Sort)
- 归并排序(Merge Sort)
- 计数排序(Counting Sort)
- 基数排序(Radix Sort)
非稳定排序(Unstable Sort)
- 定义:如果排序算法不能保证相等元素在排序后的相对顺序与排序前的相对顺序一致,则该算法是非稳定排序。
- 举例:同样假设有一个序列
[A, B, C, D]
,其中B
和C
的值相等(即B = C
)。如果排序后C
可能在B
的前面(即[A, C, B, D]
),则该排序是非稳定的。 - 常见的非稳定排序算法:
- 快速排序(Quick Sort)
- 堆排序(Heap Sort)
- 选择排序(Selection Sort)
- 希尔排序(Shell Sort)
1. 非稳定排序(快排)
回到本题:
因为单词作为key构建的mapmap中序遍历本身是满足字典顺序的,排序如果只按照 pair(单词,频率)中的频率进行比较,那么频率相同的单词可能会改变相对顺序!因此需要自己控制仿函数
注意:排序的迭代器是 Random-access Iterator
(随机访问迭代器),而map的迭代器是Bidirectional Iterator
(双向迭代器), 所以要把map中的元素先导入到vector中
- 利用map计数每一个单词出现次数
- map中的元素pair放入vector
- 对vector调用sort(快排)进行排序(降序)
降序排序 pair<单词,频率>,需要实现Greater
- 频率大的优先级高
- 频率相同,字典顺序小的优先级高
class Solution {
public:
//less这里是比较的优先级的大小
struct myGreater{
//最好+const,因为可能这里是生成的临时对象,具有常性
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const{
//1.比较频率
if(kv1.second > kv2.second){
return true;
}
//2.比较字典顺序 字典序小于 看作 优先级大于
if(kv1.second==kv2.second && kv1.first < kv2.first){
return true;
}
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
//先统计次数
map<string,int> mp;
for(const auto& str:words){
mp[str]++;
}
//数据放入vector
vector<pair<string,int>> sortV(mp.begin(),mp.end());
//利用sort非稳定排序+控制仿函数(降序)
sort(sortV.begin(),sortV.end(),myGreater());
//排完序前k个放入结果
vector<string> res;
for(int i = 0;i<k;++i){
res.push_back(sortV[i].first);
}
return res;
}
};
2. 稳定排序
如上面的非稳定排序可以知道,如果利用的排序算法是稳定排序,那么仿函数就不需要强制考虑频率相同时,字典顺序的规则了,只需要考虑频率的大小来比较即可。因为本身序列就是借助map中序得到的,是字典有序的!
对于稳定排序,STL的<algorithm>
头文件里是给出的stable_sort
class Solution {
public:
struct myGreater{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const{
//比较频率
if(kv1.second > kv2.second){
return true;
}
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
//先统计次数
map<string,int> mp;
for(const auto& str:words){
mp[str]++;
}
//数据放入vector
vector<pair<string,int>> sortV(mp.begin(),mp.end());
//利用sort非稳定排序+控制仿函数(降序)
stable_sort(sortV.begin(),sortV.end(),myGreater());
//排完序前k个放入结果
vector<string> res;
for(int i = 0;i<k;++i){
res.push_back(sortV[i].first);
}
return res;
}
};
4. 借助multimap反转键值对
由于排序的思想影响,想到了 multimap也可以用来排序:
把出现频率作为key,允许多个出现次数相同的单词,降序即可得到前k个出现次数最大的!
补充知识:
在STL的multimap
中无论是升序(默认的 std::less
)还是降序(std::greater
),当key相同时,新插入的元素都会放在已有相同key元素的右边。
multimap<int,string>是按出现频率int,中序升序排列,key相同时,默认实现是插入到右边。中序时满足频率和字典顺序的要求
如:
遍历map:(apple 2),(love,2),并放入multimap
先插入(2,apple), 后插入(2,love),默认插入到(2,apple)的右边,因此中序的时候可以保证 出现频率key相同时,单词value依然按照字典顺序(得益于遍历map时已经是按照单词的升序排列了)
不过上面的情况是升序,multimap仿函数默认用less是按key升序排列。本题要求前k频率最高的单词,需要用greater降序排列(只需考虑频率,字典顺序已满足)
multimap默认使用key进行排序,而key又是int,因此直接使用库里面的即可greater<int>
注意:
-
不能使用升序+反向迭代器遍历,因为倒着遍历时否则单词的字典顺序又无法保证了
-
multimap
的仿函数(比较函数)只能作用于键(Key
),而不能作用于整个pair<Key, Value>
。这是由multimap
的设计决定的。其他类似priority_queue
存在比较的容器,仿函数都是作用于所存放数据类型(T)。如果需求需要同时比较键和值,可以使用vector
+ 自定义排序,或者手动处理相同键的情况
- map<单词,频率> 统计单词出现次数
- 把map中的key和value反转,放入multimap<频率,单词>
- 通过控制multimap的仿函数Greater实现降序排列:频率大的优先级高
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
//先统计次数
map<string,int> countMap;
for(const auto& str:words){
countMap[str]++;
}
//利用multimap,反转key和value的位置
multimap<int,string,greater<int>> sortMap;
for(const auto& kv : countMap){
//频率作为key,单词作为value
sortMap.insert(make_pair(kv.second,kv.first));
}
vector<string> res;
//取multimap前k个
multimap<int,string,greater<int>>::iterator it = sortMap.begin();
while(it!=sortMap.end() && k--){
res.push_back(it->second);
++it;
}
return res;
}
};