目录
1.最后一块石头的重量
题解
2.数据流中的第 K 大元素
题解
3.前K个高频单词
题解
代码
⭕4.数据流的中位数
题解
在C++中,使用标准库中的priority_queue
,默认情况下它是一个最大堆(即大堆排序),这意味着最大的元素总是位于队列的前面。具体来说,默认使用的比较器是std::less<T>
1.最后一块石头的重量
链接:1046. 最后一块石头的重量
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 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],这就是最后剩下那块石头的重量。
题解
- 每一回合,从中选出两块 最重的 石头,x == y,那么两块石头都会被完全粉碎;
- 如果 x != y,那么重量较小的 x 石头将会完全粉碎,而重量较大的 y 的石头新重量为 y-x。
- 最多只会剩下一块石头。
返回此石头的重量。如果没有石头剩下,就返回 0。
每次挑选的是先挑一堆数中最大的那个数,然后再挑一个剩下数中最大的数。
这不正好符合大根堆的数据结构吗。
解法:用堆来模拟这个过程
先拿数组的数创建一个大根堆,每次从堆里面 选择最大和次大两个数
- 如果相等就是0不用加入到大根堆里
- 如果不相等用最大的数减去次大的数,把结果加入到堆里面。
如果最后堆里还剩下一个数,返回这个数。如果堆为空说明石头全都粉碎了返回0即可。
class Solution {
public:
int lastStoneWeight(vector<int>& stones)
{
priority_queue<int> heap;
for(auto& n:stones)
heap.push(n);
while(heap.size()>1)
{
int n1=heap.top();
heap.pop();
int n2=heap.top();
heap.pop();
if(n1==n2) continue;
else
{
int tmp=n1>n2?n1-n2:n2-n1;
heap.push(tmp);
}
}
return heap.empty()?0:heap.top();
}
};
2.数据流中的第 K 大元素
链接:703. 数据流中的第 K 大元素
设计一个找到数据流中第 k
大元素的类(class)。注意是排序后的第 k
大元素,不是第 k
个不同的元素。
请实现 KthLargest
类:
KthLargest(int k, int[] nums)
使用整数k
和整数流nums
初始化对象。int add(int val)
将val
插入数据流nums
后,返回当前数据流中第k
大的元素。
示例 1:
输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:[null, 4, 5, 5, 8, 8]
题解
设计一个找到数据流中第 k 大元素的类(class)。
注意是 排序后的第 k 大元素,不是第 k 个不同的元素。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
这道题其实考察的是一个非常经典的问题, TopK问题。
- 关于TopK问题有两种解题思路,一种是堆 O(nlogk),一种是前面刚学的快速选择算法 O(n)(前文回顾:[Lc7_分治-快排] 快速选择排序 | 数组中的第K个最大元素 | 库存管理 III。
- 快速选择排序虽然快,但是对于海量的数据内存根本放不下。
- 所以在海量数据情况最优的还是堆。
解法:用 堆 来解决
- 创建一个大小为 k 的堆(大根堆 or 小根堆)
- 循环
- 1.依次进堆
- 2.判断堆的大小是否超过 K
在堆的实现,画图和代码分析建堆,堆排序,时间复杂度以及TOP-K问题,对于求第K个最大元素
- 我们也是将前K个数建个小堆,然后将剩下的N-K个元素和堆顶元素做比较
- 如果大于堆顶元素,就先把栈顶元素pop掉,也就是把堆中最小元素删除,然后将它放进去。
- 这样一直循环,直到所有元素都比较完成。
因为是一直把堆中最小的pop掉,那堆的元素就是N个元素中最大的,而堆顶就是第K个最大的元素
别忘记我们这是一个小堆!
- sum: eg. TOP_K 大,建一个 K 小堆,大于 top 就 pop,push, 最后的就是第 K 大了,因为是一个 数值为 K 的小堆
这里我们也是建小堆,依次进堆,当堆大小超过K个,pop堆顶元素,把堆中最小元素pop掉,并且使堆保持K个大小。
每次都是堆中最小元素去掉。那最后堆中剩下的就是前K个最大元素,而堆顶就是第K个最大元素。
考虑一下下面两个问题
- 用大根堆还是小根堆
- 为什么要用大根堆(小根堆)
class KthLargest {
public:
priority_queue<int,vector<int>,greater<int>> heap;
int _k;
KthLargest(int k, vector<int>& nums)
{
//第k大,建小堆
_k=k;
for(auto& num:nums)
{
heap.push(num);
if(heap.size()>k) heap.pop();//删除最小的
}
}
int add(int val)
{
heap.push(val);
if(heap.size()>_k) heap.pop();//删除最小的
return heap.top();
}
};
3.前K个高频单词
链接:692. 前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 次。
题解
这是一个TopK的问题,注意,返回的答案应该按单词出现频率由高到低排序。
如果不同的单词有相同出现频率, 按字典顺序(由低向高) 排序。
解法:利用 “堆” 来解决 TopK 问题
- 1. 预处理一下原始的字符串数组: 用一个哈希表,统计一下每一个单词出现的频次。
- 2. 创建一个大小为 k 的堆,类提供的比较函数满足不了要求,我们要自己定义一个!
(返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序(由低向高) 排序。)
如果比较频次创建一个小根堆,如果比较字典序(频次相同的时候),创建一个大根堆。
所以说创建堆写比较函数的时候必须要考虑这两点
- 当频次相同的时候字典序按照大根堆方式比较
- 当频次不同的时候按照小根堆方式比较。
- 3. 循环
1.依次进堆
2.判断堆的大小是否超过 K
- 4. 提取结果
- 因为求前K大,所以建的是一个小根堆,然后提取堆顶元素在pop是一个升序的。
- 逆序一下取前K个
代码
#和 ds 讨论后的优化代码,用lambda和emplace
⭕4.数据流的中位数
链接:295. 数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
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
题解
给一个数据流,让返回每次当前已经加入到数组的数据流的中位数。
中位数是有序整数列表中的中间值。
- 如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 表的大小是奇数,直接返回中间的数。
解法一:直接sort
每次从数据流中来一个数插入数组中之后,都对当前数组进行sort
然后通过下标的方式找到中间数。
每次add加入一个数都要sort,时间复杂度是O(nlogn)。
- 总体时间复杂度是非常恐怖的。
- 因为是下标访问,find 时间复杂度是 O(1)
解法二:插入排序的思想
[0,end] 有序,插入 end + 1,使 [0, end + 1]有序。
这道题正好就是这样的思想。
相当于打扑克,找到合适的位置。
add函数,每次插入一个数的时候都需要从后往前扫描找一个插入的位置
- 因此时间复杂度是O(n)
- find 也是通过下标去找 时间复杂度是O(1)
解法三:大小堆来维护数据流的中位数
此时有一个数轴已经按照从小到大的排好序了,这个时候想找中间数的时候。
- 把这些数的前半部分放到一个大根堆里面,后半部分放到小根堆里面。
- 此时找中间值也很快,前面较小的数放到大根堆里面,堆顶元素是数轴中这些较小元素种最右边的值。
- 后面较大的数放到小根堆里面,堆顶元素是数轴中这些较大元素最左边。
此时我们仅需根据数组中的元素的个数就可以求出中位数是多少了。
如果数组是偶数
- 大根堆和小根堆正好把数轴平分
- 然后 大堆堆顶元素和小堆堆顶元素相加 /2就是这个数组的中位数。
如果数组是奇数个。
- 我们就先人为规定一下,数轴左边元素是m个,右边是n个
- 人为规定左边大根堆多方一个元素,m > n (m = n + 1)
- 此时中位数就是 左边大根堆的堆顶元素。
向这样用大根堆存左边较小部分,小根堆存右边较大部分。
- find 时间复杂度也是O(1),而add快了很多
- 因为我们是从堆存这些元素的,插入和删除每次调整堆仅需O(logn)
细节问题:
add如何实现:
- 假设现在有两个堆了。
- 一个大根堆left,一个小根堆right。
- left元素个数m个,right元素个数n个,left堆顶元素x,right堆定元素y。
如果此时来了一个数num,num要么放在left里,要么放在right里。
但是放好之后可能会破坏之前的规则:
- m == n
- m > n —> m == n + 1
我们必须维护上面的规则,才能正确找到数组中位数。
接下来分情况讨论:
m == n
- num要么插入left,要么插入right。
- 如果num要进入left,那么num <= x,但是别忘记 m == n 有可能两个堆全为空,num也是直接进入left。
- 此时 m 比 n 多了一个。没关系直接进就行。
如果num进入right,那条件是 num > x。
- 此时就有问题了。n > m了,而 n > m是不被允许的,所以我们要把右边堆调整一下,就是拿right堆顶元素放到left里。
- 因为right是一个小根堆,堆顶就是最小值。
- 拿到left,还是能保证left堆里元素是较小的,right堆里元素是较大的。
- 拿right堆顶元素放到left里正好满足 m == n + 1。
这里有一个细节问题,必须num先进right堆,然后再拿right堆定元素放到left
因为 x < num < y,如果直接把y拿过去了,就破坏了left都是较小元素。right都是较大元素。
m > n —> m == n + 1
如果num进入left,那么num <= x , 但是此时不满足 m == n + 1
- 因此 进栈后将栈顶元素给right。
- 如果num进入right,那么num > x , m == n了,直接进就行了
注意: 上面都是 先进栈 再拿栈顶移动
顺便复习了大顶堆小顶堆,红黑树,avl树,排序。很好的题
class MedianFinder {
private:
std::priority_queue<int> left; // 大顶堆存较小的一半
std::priority_queue<int, std::vector<int>, std::greater<int>> right; // 小顶堆存较大的一半
public:
MedianFinder() {} // 构造函数不需要初始化队列
void addNum(int num) {
int m=left.size();
int n=right.size();
if(m==n)
{
if(m==0 || num<=left.top())//注意为空
left.push(num);
else
{
right.push(num);
left.push(right.top());
right.pop();
}
}
if(m>n)
{
if(num<=left.top())
{
left.push(num);
right.push(left.top());
left.pop();
}//先 进栈 再拿栈顶移动
else
right.push(num);
}
}
double findMedian() {
if(left.size() > right.size()) {
return left.top();
}
return (left.top() + right.top()) / 2.0; // 2.0确保浮点运算
}
};