滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
- 1 <= nums.length <= 10^5
- -10^4 <= nums[i] <= 10^4
- 1 <= k <= nums.length
直观想法是依次遍历每次滑动窗口里的k个元素在其中寻找最大值,然后依次返回滑动窗口中的最大值,时间复杂度显而易见是O(n*k),题目要求是用线性复杂度实现,所以不考虑暴力法;
将滑动窗口视为一个队列,则需要做的是先将k个元素全部push进行,然后pop出队列尾部的元素(滑动窗口读取到的一个元素),push进滑动窗口移动后识别的元素,然后返回窗口内的最大值;即需要实现拥有这种功能的自定义数据结构,大概长这样:
class MyQueue {
public:
void pop(int value) {
}
void push(int value) {
}
int front() {
return que.front();
}
};
如果使用大顶堆(底层为完全二叉树)实现的话,虽然能够实现返回滑动窗口内的最大值,但是只能弹出最大值,所以无法确保滑动窗口正常移动;
所以需要的这种自定义数据结构,用单纯的队列或者大顶堆肯定不行,而是需要实现单调数列,这样才能确保每次返回的值都是最大值;但一旦对队列进行排序,如何确定保持滑动窗口移动;
这时候就需要搞清楚题目的要求是什么了,得到滑动窗口中的最大值,那么并不需要维护整个滑动窗口;
如何确保出口处是最大值:只要push进来的值大于前面的值,就把前面的值全部pop_back弹出,然后再push进队列;
至于担心会不会直接把最大值pop出去:因为每次pop_front的时候都是处理完整个滑动窗口最大值,所以当pop出去最大值的时候,一定是滑动窗口已经离开了此时最大值的时候;
具体代码实现如下:
class Solution {
private:
class MyQue{
public:
deque<int> MyQue;
void pop(int val){
if(!MyQue.empty() && val == MyQue.front()){
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
MyQue.pop_front();
}
}
void push(int val){
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
while(!MyQue.empty() && val > MyQue.back()){
MyQue.pop_back();
}
MyQue.push_back(val);
}
int GetMax(){
return MyQue.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQue mq;
vector<int> res;
for(int i = 0; i < k; i++){
mq.push(nums[i]);
}
res.push_back(mq.GetMax());
for(int j = k; j < nums.size(); j++){//此时这个j的初始化位置对应的nums[k]就是滑动窗口即将滑到的元素
mq.push(nums[j]);//直接判断条件是否成立,然后push进滑动窗口
mq.pop(nums[j - k]);//依次寻找之前出现过的最大值,从nums[0]开始pop
res.push_back(mq.GetMax());
}
return res;
}
};
时间复杂度: O(n)
空间复杂度: O(k)
C++中deque是stack和queue默认的底层实现容器,deque是可以两边扩展的,且deque里元素并不是严格的连续分布的。
前k个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
- 输入: nums = [1,1,1,2,2,3], k = 2
- 输出: [1,2]
示例 2:
- 输入: nums = [1], k = 1
- 输出: [1]
提示:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 O ( n log n ) O(n \log n) O(nlogn) , n 是数组的大小。
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
- 你可以按任意顺序返回答案。
很直观的想法,map存储元素和出现次数,对多有元素进行排序,然后map的key存放元素值,value存放出现频率;
额,快排,时间复杂度O(n log n),寄!
换个思路,题目仅要求给出前k高的元素,在面临这种频率类的题的时候,考虑大顶堆小顶堆,由于求的是前k高的元素,所以此处用小顶堆实现;此时的时间复杂度是O(n log k),在n很大k较小的时候,性能有明显提升;
在C++中,可以用优先队列实现堆,代码如下:
class Solution {
public:
class MyComparison{
//重载运算符()实现大于操作达到升序排列的小顶堆
public:
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;
for(int i = 0; i < nums.size(); i++){
map[nums[i]]++;//记录元素频率
// map.insert(make_pair<int,int>(nums[i],1));
// 对于两种map容器std::map、std::unordered_map:
// insert(std::make_pair(key, value)) 和 emplace(std::make_pair(key, value))重复插入同一个key的操作,
// 二者都不会替换原先的key对应的value值,只有索引[]操作会改变value
// 所以我在Day7里的四数之和的问题里的注释有误!!!!
}
//定义优先队列实现小顶堆
//此处下去看怎么实现的
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();
}
}
vector<int> res(k);
for(int i = k - 1; i >= 0; i--){
res[i] = pri_que.top().first;
pri_que.pop();
}
return res;
}
};
栈与队列总结篇
在编译原理里,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构;
递归的实现也是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因;
使用抽象程度越高的语言,越容易忽视其底层实现;
同理,使用库函数、系统提供的数据结构的时候,也一定要去思考其底层实现是什么,只有做到真的学通了,才算学懂!