目录
题目一:最后一块石头重量
题目二:数据流中的第 K 大元素
题目三:前 K 个高频单词
题目四:数据流的中位数
题目一:最后一块石头重量
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0
。
示例:
输入:[2,7,4,1,8,1] 输出:1 解释: 先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1], 再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1], 接着是 2 和 1,得到 1,所以数组转换为 [1,1,1], 最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000
题意也非常好理解,就是每次都挑选两个最大的石头,如果这两块石头重量相同,就消除粉碎
如果这两块石头重量一大一小,就保留一个 (大 - 小) 的重量
因为每次都是挑选最大的数,在剩下的数中再挑选一个最大的
所以这时就可以引入大根堆的概念,先拿出堆顶元素,向下调整后再拿出堆顶元素,如果两者粉碎了,就不用管,如果两者变为一个了,就重新插入大根堆中即可
代码如下:
class Solution
{
public:int lastStoneWeight(vector<int>& stones) {// 创建一个大根堆priority_queue<int> heap;// 将所有元素放入大根堆中for(auto it : stones) heap.push(it);while(heap.size() >= 2){int num1 = heap.top();heap.pop();int num2 = heap.top();heap.pop();if(num1 > num2) heap.push(num1-num2); }if(heap.empty()) return 0;else return heap.top();}
};
题目二:数据流中的第 K 大元素
设计一个找到数据流中第 k
大元素的类(class)。注意是排序后的第 k
大元素,不是第 k
个不同的元素。
请实现 KthLargest
类:
KthLargest(int k, int[] nums)
使用整数k
和整数流nums
初始化对象。int add(int val)
将val
插入数据流nums
后,返回当前数据流中第k
大的元素。
示例:
输入: ["KthLargest", "add", "add", "add", "add", "add"] [[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]] 输出: [null, 4, 5, 5, 8, 8]解释: KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]); kthLargest.add(3); // return 4 kthLargest.add(5); // return 5 kthLargest.add(10); // return 5 kthLargest.add(9); // return 8 kthLargest.add(4); // return 8
提示:
1 <= k <= 104
0 <= nums.length <= 104
-104 <= nums[i] <= 104
-104 <= val <= 104
- 最多调用
add
方法104
次 - 题目数据保证,在查找第
k
大元素时,数组中至少有k
个元素
这道题就是求第K大的元素,topK问题,题意就是先在构造函数中给出一个k,再给出一个初始的数组
因为求的是第 k 大的数,所以需要创建一个大小 k 的小根堆,堆顶的元素就是第 k 大的数,因为小根堆是从小到大排序的,堆顶的就是最小的
所以接下来,每次插入后都判断堆中的元素是否是 k 个,如果元素个数大于 k,那说明堆顶元素肯定就不是第 k 大的,所以此时需要 pop 掉堆顶元素,当堆剩余 k 个元素时,就说明此时堆顶元素是最小的,return即可
由于 k 是在构造函数中给出的,所以为了 add 函数能够得到 k,需要创建成员时,除了优先级队列,还需要多创建一个变量存储 k 的大小
代码如下:
class KthLargest
{// 创建一个大小为 k 的堆(小根堆)priority_queue<int, vector<int>, greater<int>> heap;int _k;
public:KthLargest(int k, vector<int>& nums) {// 将 k 的值赋值给 _k,为了add函数能够得到 k 的值_k = k;for(auto it : nums) {// 每次先插入,再判断堆中的个数是否需要popheap.push(it);if(heap.size() > _k) heap.pop();}}int add(int val) {// 同样先插入,再判断个数是否需要popheap.push(val);if(heap.size() > _k) heap.pop();return heap.top();}
};
题目三:前 K 个高频单词
给定一个单词列表 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] 的数量]
前k个高频单词,也就是topk问题
解法一:利用堆解决topK问题
分为下面四步解决问题:
第一步:需要搞清楚出现的次数,也就是使用哈希表统计每一个单词出现的次数
第二步:创建一个大小为 k 的堆
按频次:小根堆
按字典序 :大根堆
第三步:让元素依次进堆,判断是否超过 K 个
第四步:提取结果
代码如下:
class Solution
{// 重命名,方便书写typedef pair<string, int> PSI;
public:struct cmp{bool operator()(const PSI& a, const PSI& b){// 频次相同,字典序按照大根堆的方式排序if(a.second == b.second)return a.first < b.first;elsereturn a.second > b.second;}};vector<string> topKFrequent(vector<string>& words, int k) {unordered_map<string, int> hash;// 统计出现的次数for(auto& it : words) hash[it]++;// 创建一个大小为 k 的堆,让元素依次进堆priority_queue<PSI, vector<PSI>, cmp> heap;for(auto& it : hash){heap.push(it);if(heap.size() > k) heap.pop();}// 提取结果vector<string> ret;while(!heap.empty()) {ret.push_back(heap.top().first);heap.pop();}// 小根堆,所以插入ret后是频次从小到大的,所以需要逆序reverse(ret.begin(), ret.end());return ret;}
};
解法二:巧妙运用multimap冗余特性与map排序
这里的解法二,巧就巧在multimap允许冗余的特性,且频次相同时不需要任何处理字典序升序的问题,理由如下:
我们可以 使用map 统计单词出现的次数后,之后设置multimap<int ,string>,将刚刚哈希表统计的数据的 first 和 second 反过来存入multimap中,此时将multimap设置为根据 int类型 值的大小 降序排序,就可以实现频次由高到低的要求
最巧妙的就是下面所说的频次相同时,为什么不需要处理:
最开始使用 map 统计单词出现的个数时,string 默认是按照字典序升序的方式排列的,所以依次将 map 中的元素插入 multimap 时,如果有频次相同的 string,先插入 multimap 的一定是字典序较小的那一个
所以频次相同时,插入 multimap 中的次序,本身就是按照字典序升序的方式排序的,所以不需要额外处理
代码如下:
class Solution
{
public:vector<string> topKFrequent(vector<string>& words, int k) {// 统计各个单词出现的次数,一定是map而不是unordered_map,因为string需要排序map<string, int> hash;for(auto& it : words) hash[it]++;// 利用multimap存储<int, string>,按照频次排序multimap<int, string, greater<int>> mp;for(auto& it : hash){mp.insert(make_pair(it.second, it.first));}// 提取结果vector<string> ret;auto it = mp.begin();while(k--){ret.push_back((*it).second);it++;}return ret;}
};
题目四:数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
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 <= 105
- 在调用
findMedian
之前,数据结构中至少有一个元素 - 最多
5 * 104
次调用addNum
和findMedian
解法一:每次add都sort
这道题首先最容易想到的就是每次插入一个数据,都sort,再找中位数,但是这种操作每次add函数的时间复杂度都是 O(N * logN)的
所以如果插入的数据比较多,一定会超时
解法二:插入排序思想
这里的插入排序,就不需要每次add都执行sort,因为每次插入前,此时数组的数字是有序的,所以只需要从后向前比较,直到遇到比插入数小的再插入,所以插入排序的思想的时间复杂度是O(N)的
这个解法与上面的直接sort排序一样, 如果add插入的数据非常多,也会导致超时
解法三:大小堆维护数据流的中位数
这个解法是需要知道的,具体如下:
大小堆维护中位数,也就是将前半段数据放入大根堆中,后半段数据放入小根堆中
如果偶数,那么大根堆的堆顶元素和小根堆的堆顶元素,刚好就是最中间的两个中位数,因为排好序后,大根堆的堆顶元素就是左边最大的,而小根堆的堆顶元素就是右边最小的
而如果是奇数,我们可以规定,左边的元素个数是大于右边元素个数的,所以如果是奇数,就去左边大根堆的堆顶元素即可
此时相比于上面的两种解法,优化了非常多,这里执行 add函数 时,相当于堆的插入,所以时间复杂度就是O(logN)
上述就是大小堆维护中位数的规则,下面说说这种解法需要注意的细节:
假设左边堆叫做left,左边堆的元素个数是m,左边堆的堆顶元素是x
右边堆叫做right,右边堆的元素个数是n,右边堆的堆顶元素是y
因为上面说了,m == n 或 m == n + 1,那么此时如果有一个数num想进入堆中,就有可能会破坏这个规定,例如 m == n 时,一个数num进入right,此时就会破坏掉 m 和 n 的规定,所以需要处理这个问题,当插入时分类讨论即可:
①m == n (左右元素相等) 时:
num <= x 或 m == 0,此时需要进入堆 left 中, 这时没问题满足规定
num > x,此时需要进入right,进入后right会比left多一个元素,此时需要将 y 放入left中,因为y 是right中最小的,放入left中,依旧满足left都是小的,right都是大的,并且也满足m和n的数量规定
②m == n + 1 (左边比右边多一个) 时:
num <= x 或 m == 0,此时需要进入堆 left 中,这时left会比right多两个元素,不符合规定,这时将x 放入right中即可,因为 x 时left最大的,进入right依旧满足,left都是小的,right都是大的,并且也满足规定
num > x,此时需要进入right,进入后left和right数量相等,并且也满足m和n的数量规定
代码如下:
class MedianFinder
{// left大根堆,right小根堆priority_queue<int> left;priority_queue<int, vector<int>, greater<int>> right;
public:MedianFinder() {}void addNum(int num) {// 分类讨论,左右两个堆的元素个数是否相等 if(left.size() == right.size()){// 放left,不需要调整if(left.empty() || num <= left.top())left.push(num);// 放right,需要调整else{right.push(num);left.push(right.top());right.pop();}}else{// 放left,需要调整if(left.empty() || num <= left.top()){left.push(num);right.push(left.top());left.pop();}// 放right,不需要调整else if(num > left.top())right.push(num);}}double findMedian() {// 左右堆加起来元素个数是偶数if(left.size() == right.size()){int x = left.top();int y = right.top();return (x + y) / 2.0;}// 奇数else{return left.top();}}
};
优先级队列相关习题到此结束