239. 滑动窗口最大值
题目链接
状态:暴力ok
文档:programmercarl.com
1、(暴力算法)思路:
一共有两层循环,外层循环(N)迭代变量,内层循环(K)每一个窗口,并计算这个窗口的最大值。时间复杂度O(N*K)。
代码:
class Solution {
public:
//暴力
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//短路条件
int size = nums.size();
//存放结果的数组
vector<int> result;
if(size == 0) return result;
for(int i=0;i<=size-k;i++)
{
//最大值
int max = nums[i];
for(int j=i;j<=i+k-1;j++)
{
//计算每个窗口内的最大值
if(nums[j] > max) max = nums[j];
}
result.push_back(max);
}
return result;
}
};
2、(单调队列)思路:
可以看到,每移动一次窗口,原理上都是把左边的元素pop出去,右边新加一个元素进来。
但是有时候并不需要去pop那个被窗口抛弃的元素,因为在某些元素push进来之前,我们会先对队列进行一个判断大小的操作:
- 如果要push进来的元素比队列入口处的元素大,那么就要把这些元素一 一pop掉,(因此之后可能就不用去pop那个被窗口抛弃的元素,因为已经被pop掉了)这样就保证了队列是一个单调递减的队列。
- 之后若要pop出口处的元素,那么下一个元素一定是下一个窗口的最大值。
- 并且,若pop的元素==队列出口的元素,那么说明pop的元素是这个窗口的最大值。
代码:
class Solution {
private:
class MyQueue{
public:
//定义一个队列que,实现单调队列
deque<int> que;
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 如果不相等的话说明此时要pop的数比出口数小,在出口数push前该数就已经被pop了。
// 同时pop之前判断队列当前是否为空。
void pop(int value){
if(!que.empty() && value == que.front())
que.pop_front();
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value){
while(!que.empty() && value > que.back())
que.pop_back(); // 把小的数都pop
//小的数pop完再push新的大的数
que.push_back(value);
}
// 返回窗口最大值
int getWindowMax(){
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
//先把前k个数放到que中
for(int i=0;i<k;i++){
que.push(nums[i]); // 已经保证现在的队列是一个单调队列了
}
// 此时已经产生了一个窗口的最大值,记录下来
result.push_back(que.getWindowMax());
// 窗口移动时,左边的元素被移除,右边的元素会新加进来
for(int j=k;j<nums.size();j++){
// 左边的元素移除掉
que.pop(nums[j-k]);
// 右边的元素加进来
que.push(nums[j]);
// 形成了新的窗口,返回窗口的最大值
result.push_back(que.getWindowMax());
}
return result;
}
};
注意:
在push函数中,判断要加进来的value是否>队列入口处的值时,因为这个比较次数是不确定的,所以不能用if判断,而要用while判断才行。
347.前 K 个高频元素
思路:
要做的事情一共有三个:
- 要统计元素出现频率(这一类的问题可以使用map来进行统计。key:存放元素,value:元素出现的次数)
- 对频率排序(这里我们可以使用一种 容器适配器就是优先级队列。)
- 找出前K个高频元素
如果在对排序的时候不使用优先级队列,而是直接使用map呢? ----- 在map中以value为基准,进行一个从大到小的排序,然后把满足条件的value所对应的key输出就可以了。
但是按照快排来看,对所有的元素都进行一个排序,时间复杂度是nlogn。
但其实并没有必要对所有的元素进行一个排序,只需要对前k个集合的元素进行排序即可。
如何去维护k个高频元素的有序集合呢? ---- 大顶堆和小顶堆
大顶堆和小顶堆本质上是一个二叉树。
优先级队列是什么?
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。
堆是什么?
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。
大顶堆:根节点最大,父亲比左右孩子要大。
小顶堆:根节点最小,父亲比左右孩子要小。
选哪个堆?
用堆去遍历map中的元素,因为我要输出前k个高频元素,所以堆的大小就设置成k,也就是说堆里面就维持k个元素。遍历完之后,这个堆就维护了我们想要求的前k个高频元素。
- 如果要使用大顶堆的话:堆里目前已经满足k个元素了,此时如果push进一个新的元素,那么就意味着有一个元素要被pop掉了。对于这种数据结构,要pop的时候都是从栈顶开始pop的。大顶堆的栈顶是最大的数,那就意味着把最大的数给pop掉了,这肯定是不合理的。
- 反之如果使用小顶堆的话,pop掉的栈顶元素是最小的值,这样一次次的就把最小的值弄出去,留下的就是大的值。所以此题选用小顶堆。
用小顶堆的时间复杂度:把map中的元素遍历一遍(n),然后对二叉树进行调整(push+pop,logk)。所以小顶堆的时间复杂度是nlogk。
代码:
class Solution {
public:
//先构建一个小顶堆
class mycomparison{
public:
//要对键值对(<key,value>)进行排序,还要控制对谁(value)排序
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 要统计元素出现频率
unordered_map<int, int> map; // map[nums[i]],对应出现的次数
for(int i = 0;i<nums.size();i++){
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,遍历所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
vector<int> result(k);
for(int i = k-1;i>=0;i--){
result[i] = pri_que.top().first;
// 弹出栈顶元素 以便下一次继续输出top
pri_que.pop();
}
return result;
}
};