239、滑动窗口最大值
题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回每次滑动窗口中的最大值 。
思路
- 选择数据结构:队列(实现单调队列)
- 队列应该具有的性质:
- 元素值递减(从队头到队尾)
- 队列只需要存储可能成为滑动窗口中最大值的元素即可,而不需要维护滑动窗口中所有的值**(这规定了队列功能的实现方式)**
- 队列应该具有的功能:
- pop()滑动窗口移动时移除元素
- push()滑动窗口移动时添加的数值
- front()返回最大值
- 算法:
- 数据结构的实现算法:
- push():当滑动窗口新元素大于队尾元素时,把这个队尾元素弹出;当这个新元素小于队尾元素(或队列为空)时,把这个元素添加进队尾
- pop():当滑动窗口移除的元素和队头元素相同时,再移除队头元素
- front():返回队头元素(也就是当前滑动窗口的最大值)
- 解决算法:
- 先将第一个滑动窗口中的元素加入单调队列中
- 使用遍历的方式移动滑动窗口
- 每到一个新的滑动窗口,在队列中移除因窗口移动而移除的值
- 每到一个新的滑动窗口,在队列中添加因窗口移动而添加的新值
- 每到一个新的滑动窗口,更新完单调队列后,将队头元素(就是当前滑动窗口中的最大值)添加进结果集
- 返回结果集
代码
class Solution {
private:
class MyQueue {//单调队列
public:
deque<int> que;//使用deque来实现单调队列
//如果滑动窗口移动时要移除的元素等于队头元素时,则移除队头元素
//value:滑动窗口移动时要移除的元素
void pop(int value) {
if (!que.empty() && value == que.front()) que.pop_front();//移除队头元素
}
//要保证队列从大到小
//value:滑动窗口移动时要添加的元素值
void push(int value) {
//当队尾的值小于滑动窗口要添加的值时,直到队尾元素大于value或队列为空
while (!que.empty() && value > que.back()) {
que.pop_back();//移除队尾元素
}
que.push_back(value);//把新元素添加进队尾
}
//返回队列最大值(队头)
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;//单调队列
vector<int> res;//存储结果
for (int i = 0; i < k; i++) {
//先将前k个元素放入单调队列 也就是第一个滑动窗口
que.push(nums[i]);
}
res.push_back(que.front());//记录第一个滑动窗口中的最大值
//滑动窗口向前移动
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]);//在队列中移除因滑动窗口移动而移除的元素
que.push(nums[i]);//在队列中加入滑动窗口中的新元素
res.push_back(que.front());//在结果集中添加新滑动窗口中的最大值
}
return res;
}
};
时间复杂度:O(n);nums中的元素最多被push()和pop()队列一次,因此总体时间复杂度为O(n);
空间复杂度:O(k);队列中的元素最多不会超过k个元素。
对单调队列的思考
- deque:双端队列。在C++中是queue的默认底层实现容器。在这里用deque作为底层容器实现单调队列。
- 单调队列:从队头到队尾递增或递减的队列。
- (不用排序维护队列时)单调队列的实现要依靠push()的具体操作实现:当要添加的元素大于队尾元素时,不断把队尾元素移除,直到要添加的元素小于队尾元素或队列为空。这样的push操作能够确保队列元素排列具有单调性。
347、前K个高频元素
题目描述
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
思路
- 题目分析:
- 需要统计nums中各元素出现的频率
- 因为需要返回频率前k高的元素,所以需要对频率排序
- 找出前k个元素返回
- 数据结构选择:
- 统计频率:unordered_map<int, int>
- 对频率排序:优先级队列(小顶堆)
- 算法:
- 先遍历nums统计出各数字的频率存储到map中
- 对统计出的频率进行排序:扫描map中所有频率的数值,用大小为k的小顶堆存储结果,最终小顶堆中的k个元素就是频率最高的k个元素
- 把小顶堆中的k个元素加入结果集返回
代码
class Solution {
public:
//小顶堆
class mycomparion {
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;//map<nums[i], 频数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
//对频率排序
//定义一个优先级队列(小顶堆),大小为K
//pair<nums[i],频数>
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparion> 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> res(k);//结果集的大小为k
for (int i = k - 1; i >= 0; i--) {
res[i] = pri_que.top().first;//first代表元素
pri_que.pop();//把已经加入结果集的元素弹出
}
return res;
}
};
时间复杂度:O(nlogk);先统计频数需要遍历nums花费O(n);之后遍历频率map最多花费O(n),其中每次堆操作至多花费O(logk);总体花费O(nlogk)。
空间复杂度:O(n);哈希表大小为O(n),堆大小为O(k)。
思考
- map常用来统计数字出现的频率。
- 除了常用的排序算法外,还可以使用优先级队列(堆)来堆数据排序。