
给定一个单词列表 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 <= 5001 <= words[i] <= 10words[i] 由小写英文字母组成。k 的取值范围是 [1, 不同 words[i] 的数量]**进阶:**尝试以 O(n log k) 时间复杂度和 O(n) 空间复杂度解决。
这道题很明显还是要使用 topk 问题的处理方式来解决,也就是使用堆来解决,只不过这道题稍微要多处理其它的内容!
首先因为需要按单词出现频率由高到低排序,那么就需要统计单词的出现频率,就可以 使用哈希表来统计!
然后将哈希表内容按 topk 模式进行处理,这里有细节,就是因为我们既要获得堆中的字符串,又得根据频次来维护堆,所以优先级队列存放的是一个键值对 pair<string, int>,还有就是因为==频次从高到小排序,那么我们就要建立一个小根堆==,所以要自己写一个比较器 compare,这里用仿函数为例!
在比较器中还要特别注意一个细节,我们需要特殊判断一下次数相等的情况,此时要根据字典顺序从低到高排序,所以 对于字典序列要搞个大堆才行,和频次是相反的,一定要弄清楚比较器中大于和小于的区别,大于表示的是建小堆,小于表示的是建大堆,别搞错了!
接着就是一个循环,先将哈希表中的键值对插入,然后判断一下优先级队列是否超过了 k 个元素,是的话直接让堆顶 pop 掉即可,因为我们建立的堆是与序列相反的,那么 pop 的就是不满足要求的!以此类推,直到哈希表遍历完毕!
最后就是将堆中元素取出,然后放到数组中返回,注意因为堆中次序和返回的次序是相反的,所以我们要先逆序,再返回结果!
struct compare
{
bool operator()(pair<string, int>& p1, pair<string, int>& p2)
{
// 特殊判断一下次数相等的情况
if(p1.second == p2.second)
return p1.first < p2.first; // 注意细节,字典顺序是从低到高,所以字典要搞个大堆才行,和频次相反
return p1.second > p2.second;
}
};
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
// 先统计每个单词出现次数
unordered_map<string, int> hash;
for(auto& word : words)
hash[word]++;
// 将哈希表内容按topk模式进行处理,使用小根堆
priority_queue<pair<string, int>, vector<pair<string, int>>, compare> heap;
for(auto& e: hash)
{
heap.push(e);
if(heap.size() > k)
heap.pop();
}
// 将堆中元素取出
vector<string> str;
while(!heap.empty())
{
str.push_back(heap.top().first);
heap.pop();
}
reverse(str.begin(), str.end()); // 别忘了最后是从大到小排序,所以要逆序一下
return str;
}
};中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
arr = [2,3,4] 的中位数是 3 。arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。void addNum(int num) 将数据流中的整数 num 添加到数据结构中。double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。示例 1:
输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0提示:
-105 <= num <= 105findMedian 之前,数据结构中至少有一个元素5 * 104 次调用 addNum 和 findMedian 这种思路比较简单,就是直接在 addNum() 函数中,将 num 插入到数组后直接调用 sort() 函数对数组进行排序,然后在 findMedian() 中就能直接通过索引来得到平均值或者中值!
在处理的时候时间复杂度为 O(nlogn),在查找中位数的时候时间复杂度为 O(1),但是在这道题中处理的时候是会超时的,所以不考虑这种方式!
因为每次我们在 addNum() 的时候,其实就只插入了一个元素,那我们可以考虑使用插入排序,因为我们可以让原数组保持有序的情况,只需要处理一个新元素的排序即可,而不需要像解法一中每次调用 addNum() 的时候都需要重新去排序!
那么使用插入排序的时间复杂度对于一个元素来说,是 O(n),相比解法一有了提升,然后在查找中位数的时候时间复杂度依然是 O(1),但这道题在处理插入的时候,也耐不住大量极端的数据,所以也是会超时的!
可以看到上面两种解法,其实关键点都在 addNum() 这个函数上,所以我们要想办法优化一下插入的处理!
其实这道题要想到维护大小堆并不简单,但是学会了这种思路,对于相同的问题来说是可以提供一种巧妙的方法的,就是使用一个大堆和小堆来分别维护中位数左右的区间!
说的有些抽象,其实可以参考这幅图:

上图摆的很像一个沙漏的形状,其实最重要的就是两个堆的堆顶元素,对于大堆来说,它要存放的是比中位数小的那一半的值,然后堆顶作为边界,存放的就是这些数中的最大值;对于小堆来说,它要存放的是比中位数大的那一半的值,然后堆顶也是作为边界,存放的这些数中的最小值!
在插入元素 num 的时候,我们要判断一下 num 和两个堆顶元素的大小关系,我们这里优先判断的是大堆(优先小堆也是可以的,请自行尝试),因为大堆中存放的是较小的那些元素,所以如果 num 小于等于大堆堆顶元素的话,我们就让 num 放入大堆中,反之则放入小堆中!
插入完元素后还没有结束,因为我们在维护这两个堆的时候,要让两个堆的元素个数之差不超过一个元素,超过的话我们需要将元素个数多的那个堆的堆顶 pop 掉,然后放到另一个堆中,这样子就能一直保证两个堆的元素个数之差不超过一个元素!
这样子插入函数 addNum() 就完成了,时间复杂度为 O(logn),就是堆的插入时间消耗,这可比 O(n) 要快得多!
接着就是 findMedian() 函数,如果两个堆的元素个数总和为奇数的话直接取元素个数多的堆的堆顶返回;如果两个堆的元素总和为偶数,那么取两个堆的堆顶元素的平均值即可!
可以看出查找中位数的时间复杂度依然是 O(1),是非常优秀的一种结构,既兼顾到了插入,又能快速根据堆顶来索引中位数!
class MedianFinder {
private:
priority_queue<int> bigheap; // 大堆
priority_queue<int, vector<int>, greater<int>> smallheap; // 小堆
public:
MedianFinder() {}
void addNum(int num) {
// 如果大堆没有元素或者num小于等于大堆堆顶的话直接入大堆即可,否则就往小堆中插入
if(bigheap.empty() || num <= bigheap.top())
bigheap.push(num);
else
smallheap.push(num);
// 插入完后判断两个堆的元素个数差距是否大于1
if(bigheap.size() > smallheap.size() + 1)
{
// 如果大堆元素超出要求,则将大堆中堆顶元素放到小堆中去
int top = bigheap.top();
bigheap.pop();
smallheap.push(top);
}
else if(bigheap.size() + 1 < smallheap.size())
{
// 如果小堆元素超出要求,则将小堆中堆顶元素放到大堆中去
int top = smallheap.top();
smallheap.pop();
bigheap.push(top);
}
}
double findMedian() {
// 如果是奇数的话直接取元素个数多的堆的堆顶返回即可!
if(bigheap.size() > smallheap.size())
return bigheap.top();
else if(bigheap.size() < smallheap.size())
return smallheap.top();
return (bigheap.top() + smallheap.top()) / 2.0; // 如果两个堆的元素总和为偶数,那么取两个堆的堆顶元素的平均值
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/