滑动窗口最大值
链接: 滑动窗口最大值
给你一个整数数组 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]
思路
这题是对队列的应用
本题比较有难度,需要我们自己去构造单调队列
我们需要求出滑动窗口里的最大值,我们可能会直接暴力,即在遍历一遍中每次从窗口中找到最大值,时间复杂度为O(n x k)。
更高效的做法是可以使用优先队列(大根堆)。
比优先队列更高效的做法就是单调队列。
解题方法
优先队列
大根堆可以帮助我们实时维护一系列元素中的最大值。
对于本题而言,初始时,我们将数组 nums
的前 k 个元素放入优先队列中。
每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。
但是这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums
中的位置出现在滑动窗口左边界的左侧。
因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。
我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。
为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 (num,index)
,表示元素 num
在数组中的下标为 index
单调队列
我们需要这样的一个队列:
放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
代码:
class MyQueue {
public:
void pop(int value) {
}
void push(int value) {
}
int front() {
return que.front();
}
};
每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
在C++中,可以使用 multiset 来模拟这个过程
但我们可以自己实现这个单调队列。
队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
接下来:
如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出?
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。
C++中没有直接支持单调队列,需要我们自己来实现一个单调队列
注:
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列就没有区别了。
此时我们可以使用deque
来实现单调队列,
因为:常用的queue在没有指定容器的情况下,deque就是默认底层容器。
复杂度
优先队列
- 时间复杂度:O(nlogn)
n是数组的长度,在最坏情况下,数组nums
中的元素单调递增,那么最终优先队列中包含了所有元素,没有元素被移除。
将一个元素放入优先队列的时间复杂度为 O(logn),因此总时间复杂度为 O(nlogn) - 空间复杂度:O(n)
优先队列使用的空间
单调队列
- 时间复杂度:O(n)
- 空间复杂度:O(k)
定义了一个辅助队列
Code
优先队列
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
priority_queue<pair<int, int>> q;
for (int i = 0; i < k; ++i) {
q.emplace(nums[i], i);
}
vector<int> ans = {q.top().first};
for (int i = k; i < n; ++i) {
q.emplace(nums[i], i);
while (q.top().second <= i - k) {
q.pop();
}
ans.push_back(q.top().first);
}
return ans;
}
};
单调队列
class Solution {
private:
class MyQueue {
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时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();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> re;
for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
que.push(nums[i]);
}
res.push_back(que.front()); // res 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
res.push_back(que.front()); // 记录对应的最大值
}
return res;
}
};
前 K 个高频元素
链接: 前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
- 1 <= nums.length <= 105
- k 的取值范围是 [1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
思路
我的想法是直接二维数组排序即可
还有一种写法是使用堆
解题方法
二维数组排序
- 使用哈希表统计每个数字的出现次数。
- 将哈希表的键值对转移到二维向量中。
- 对二维向量按照出现次数进行降序排序。
- 从排序后的向量中提取前k个元素,并存储到结果向量中。
- 返回结果向量。
堆
首先遍历整个数组,并使用哈希表记录每个数字出现的次数,并形成一个「出现次数数组」。找出原数组的前 k 个高频元素,就相当于找出「出现次数数组」的前 k 大的值。
最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数(其中 N 为原数组长度),故总的算法复杂度会达到 O(NlogN),不满足题目的要求。
在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:
如果堆的元素个数小于 k,就可以直接插入堆中。
如果堆的元素个数等于 k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 k 个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。
遍历完成后,堆中的元素就代表了「出现次数数组」中前 k 大的值。
复杂度
二维数组排序
- 时间复杂度:O(nlogn)
n 是数组 nums 的长度。
将统计结果存入二维向量 v 中,这部分的时间复杂度是 O(n),因为我们需要遍历整个哈希表。
对二维向量 v 进行排序,这部分的时间复杂度是 O(n log n),因为 v 的大小最多为 n(哈希表中的元素数量),而排序通常具有对数级别的复杂度。
最后,从排序后的 v 中取出前 k 个元素,这部分的时间复杂度是 O(k)。
综上所述,整个函数的时间复杂度是 O(n log n) - 空间复杂度:O(n)
堆
- 时间复杂度:O(nlog)k
- 空间复杂度:O(n)
Code
二维数组的排序
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
// 创建一个unordered_map(哈希表)mp,用于存储每个数字及其出现的次数
unordered_map<int, int> mp;
// 遍历nums数组
for(int i = 0; i < nums.size(); ++i) {
// 在哈希表中增加当前数字对应的计数
++mp[nums[i]];
}
// 创建一个二维向量v,用于存储哈希表中的键值对(数字及其出现次数)
vector<vector<int>> v;
// 将哈希表中的键值对转移到二维向量v中
for(auto& [x, y]: mp) {
// 向v中推入一个包含数字和其出现次数的向量
v.push_back({x, y});
}
// 对二维向量v进行排序,排序依据是每个子向量的第二个元素(即出现次数),从大到小排序
sort(v.begin(), v.end(), [](const vector<int>& a, const vector<int>& b) {
return a[1] > b[1];
});
// 创建一个结果向量res,用于存储频率最高的k个数字
vector<int> res;
// 从排序后的二维向量v中取出前k个元素(即出现次数最高的k个数字)
for(int i = 0; i < k && i < v.size(); ++i) {
// 将每个子向量的第一个元素(即数字)推入结果向量res中
res.push_back(v[i][0]);
}
// 返回结果向量res,它包含了频率最高的k个数字
return res;
}
};
堆
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> occurrences;
for (auto& v : nums) {
occurrences[v]++;
}
// pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
for (auto& [num, count] : occurrences) {
if (q.size() == k) {
if (q.top().second < count) {
q.pop();
q.emplace(num, count);
}
} else {
q.emplace(num, count);
}
}
vector<int> ret;
while (!q.empty()) {
ret.emplace_back(q.top().first);
q.pop();
}
return ret;
}
};