代码随想录算法训练营day13 | 239. 滑动窗口最大值、347.前 K 个高频元素

滑动窗口最大值

链接: 滑动窗口最大值

给你一个整数数组 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(log⁡n),因此总时间复杂度为 O(nlog⁡n)
  • 空间复杂度: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 个高频元素的集合是唯一的

思路

我的想法是直接二维数组排序即可
还有一种写法是使用堆

解题方法

二维数组排序

  1. 使用哈希表统计每个数字的出现次数。
  2. 将哈希表的键值对转移到二维向量中。
  3. 对二维向量按照出现次数进行降序排序。
  4. 从排序后的向量中提取前k个元素,并存储到结果向量中。
  5. 返回结果向量。

首先遍历整个数组,并使用哈希表记录每个数字出现的次数,并形成一个「出现次数数组」。找出原数组的前 k 个高频元素,就相当于找出「出现次数数组」的前 k 大的值。

最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数(其中 N 为原数组长度),故总的算法复杂度会达到 O(Nlog⁡N),不满足题目的要求。

在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:

如果堆的元素个数小于 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;
    }
};

总结

  • 第一题比较有难度,需要自己去构造单调队列
  • 第二题的二维数组排序比较巧妙,第二种解法不是很熟练,后面二刷在理解。
  • 参考文档:
    链接: 滑动窗口最大值
    链接: 前 K 个高频元素
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值