【LeetCode刷题日志】:滑动窗口最大值、前 K 个高频元素

90 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

1、写在前面

大家好,这里是【LeetCode刷题日志】。今天的两道题分别是:

  • 滑动窗口最大值
  • 前 K 个高频元素

2、内容

2.1、题一

链接:239. 滑动窗口最大值 - 力扣(LeetCode)

(1) 描述

image.png

(2) 举例

image.png

image.png

image.png

(3) 解题

这道题如果用暴力方法,在遍历一遍的过程中每次在窗口内找到最大值,这样时间复杂度就是O(n×k)O(n×k)。下面是暴力算法的优化:

(一)优先队列

优先队列的解题思路是,利用大根堆来存放滑动窗口中的k个数字,然后取堆顶元素即可。具体如下:

  1. 初始化时,将nums数组中的前k个元素放入优先队列中,存入优先队列的元素实际上是二元组(num, index),这样可以方便后续判断栈顶元素的位置与滑动窗口的位置关系;
  2. 接着,每向右移动窗口时,我们就将一个新的元素放入优先队列中,此时堆顶元素就是最大值;
  3. 由于该最大值可能并不在滑动窗口内,因此我们加一层while循环,判断如果当前的堆中的最大值的索引值小于或等于i-k,则说明该最大值在数组中的位置是在滑动窗口的左侧,也就是不在滑动窗口内,此时可以将这个最大值删去;
  4. 当不断剔除掉不合适的最大值后,退出while循环时,当前的最大值就是滑动窗口内的最大值,此时存入结果即可。

具体代码如下:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        // 定义一个优先队列 q
        priority_queue<pair<int, int>> q;
        // 先将数组 nums 前 k 个元素存入队列 q 中
        for (int i = 0; i < k; ++i) {
            q.emplace(nums[i], i);
        }
        // 将第一个滑动窗口的最大值存入结果
        vector<int> rs = {q.top().first};
        // 遍历数组
        for (int i = k; i < nums.size(); ++i) {
            // 将新元素存入优先队列中
            q.emplace(nums[i], i);
            // 当堆顶元素(最大值)不在滑动窗口中时,则不断删除堆顶堆元素,直到最大值出现在滑动窗口里
            while (q.top().second <= i - k) {
                q.pop();
            }
            // 将堆顶元素存入结果中
            rs.push_back(q.top().first);
        }
        // 最后返回结果
        return rs;
    }
};

(二)单调队列

在本题中,单调队列的维护逻辑是:

  • 当队列不为空并且当前数组元素大于队尾元素时,移除队尾元素
  • 直到队列为空或者队尾元素大于当前数组元素时,再入队数组元素

按照这个逻辑就可以维护好本题的单调递减队列。

有了单调队列后,由于单调队列中的元素是单调递减的,因此队首元素必定是当前窗口中的最大值,因此将队头元素添加到结果中即可。

但需要注意,如果队首元素的下标小于滑动窗口左侧边界(即q.front()i-k)时,表明当前队首元素并不在滑动窗口内,因此需要再添加一条while循环处理,移除这种情况下的队首元素,直到退出while循环后,当前队首元素才是在窗口内的,将该队首元素添加到结果中即可。

具体代码如下:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        // 定义一个双端队列容器,适合于序列两端频繁的添加或删除元素的需求
        deque<int> q;
        // 先处理第一个窗口中的元素(注意,单调队列中的值为单调递减的)
        for (int i = 0; i < k; ++i) {
            // 维护单调递减的队列:
            // 当队列不为空并且当前元素`nums[i]`大于等于队尾元素时,移除队尾元素
            while (!q.empty() && nums[i] >= nums[q.back()]) {
                q.pop_back();
            }
            // 只有当数组元素小于队尾元素时,才能让数组元素入队(注意,这里入队的是元素的下标值)
            q.push_back(i);
        }
        // 定义一个向量容器,用于存放结果,result[0] = nums[q.front()]
        vector<int> result = {nums[q.front()]};
        // 接着遍历数组(移动窗口),处理后续元素
        for (int i = k; i < nums.size(); ++i) {
            // 一样的,当队列不为空并且当前元素`nums[i]`大于等于队尾元素时,移除队尾元素
            while (!q.empty() && nums[i] >= nums[q.back()]) {
                q.pop_back();
            }
            // 存入当前元素的索引值到队列中
            q.push_back(i);
            // 当队首元素的下标小于滑动窗口左侧边界`i-k`时,则表示队首元素不在滑动窗口内,因此移除队首元素
            while (q.front() <= i - k) {
                q.pop_front();
            }
            // 将队首元素存入结果中
            result.push_back(nums[q.front()]);
        }
        // 最后返回结果
        return result;
    }
};

2.2、题二

链接:347. 前 K 个高频元素 - 力扣(LeetCode)

(1) 描述

image.png

(2) 举例

image.png

image.png

(3) 解题

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 使用unordered_map容器来记录数组中元素出现的次数,即map<nums[i], 频数>
        unordered_map<int,int>map;
        // map<nums[i], 频数>,统计元素出现次数
        for(int& val : nums){
            map[val]++;
        }
        // 自定义一个排序规则,从小到大来排序,便于后续生成小根堆
        struct myComparison{
            bool operator()(pair<int,int>&p1, pair<int,int>&p2){
                return p1.second > p2.second;
            }
        };
        // 根据自定义的排序规则,创建一个优先级队列 q
        priority_queue<pair<int,int>, vector<pair<int,int>>, myComparison> q;
        // 遍历map,将map中的元素全部入队到优先级队列 q 中
        for(auto& i : map) {
            q.push(i);
        }
        // 不断删除掉队列中的最小值,直到留下最后k个元素(这k个元素就是前k个高频元素)
        while(q.size() > k) {
            q.pop(); 
        }
        // 定义一个向量容器来保存结果
        vector<int> result;
        // 当优先级队列不为空时,将元素添加到结果中
        while(!q.empty()){
            result.emplace_back(q.top().first);
            q.pop();
        }
        // 最后返回结果
        return result;
    }
};

3、写在最后

好的,今天就先刷到这里。