代码随想录Day13|滑动窗口最大值、前k个高频元素

滑动窗口最大值

原题链接:滑动窗口最大值

题目设定:给定一个数组,有一个大小为k的滑动窗口从数组的最左侧移动到数组最右侧。你只可以看到滑动窗口内的k个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

解答思路:

1.暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n*k)的算法。

2.使用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了,但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。

此时我们需要一个队列,把窗口里的元素放进这个队列里,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。

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++中没有直接支持单调队列,需要我们自己来实现一个单调队列。

注意,这里实现单调队列不是简单地进行排序,否则就和优先队列一样了。

设计单调队列时,pop,push操作要保持如下规则:

1.pop(value):如果窗口溢出的元素value等关于单调队列的出口元素,那么队列弹出元素,否则不用进行任何操作。

2.push(value):如果oush的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止。

保持如上过则,每次进行窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

单调队列的实现可以考虑的数据结构——deque(双向队列)最为合适,常用的queue(队列)在没有指定容器的情况下,deque就是底层默认容器。

代码实现:

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();
    }
}

这就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题:

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>result;
        for(int i = 0; i < k;i ++){ //先将前k的元素放进队列
             que.push(nums[i]);
        }
        result.push_back(que.front());//result记录前k的元素的最大值
        for(int i = k; i < nums.size(); i++){
            que.pop(nums[i - k]); //滑动窗口移除最前面元素
            que.push(nums[i]); //滑动窗口前加入最后面的元素
            result.push_back(que.front()); //记录对应的最大值
        }
        return result;
       }
};

时间复杂度O(n)空间复杂度O(k)

单调队列的实现中,nums中的每个元素最多也就被push_back和pop_back各一次,没有任何多余操作,所以整体的复杂度还是O(n).

空间复杂度因为我们定义一个辅助队列,所以是O(k)。

题解中单调队列里的pop和push接口,仅适用于本题。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫单调队列。

C++中deque是stack和queue默认的底层实现容器。deque是可以两边扩展的,而且deque里元素并不是严格的连续分布。

前k个高频元素

原题:给定一个整数数组nums和一个整数k,请你返回其中出现频率前k高的元素。你可以按任意顺序返回答案。

解法1:堆

解题思路:

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

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

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

 如果堆的元素个数小于k,就可以直接插入堆中。

如果堆的元素个数等于k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有k个数字出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。

遍历完成后,堆中的元素就代表了[出现次数数组]中前k大的值。

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]++;
        }

    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;
    }
};

   解法2:基于快速排序

解题思路:使用基于快速排序的方法,求出[出现次数数组]的前k大的值。

在堆数组arr[l...r]做快速排序的过程中,我们首先将数组分为两个部分arr[i...q-1]与arr[q+1...j],并使arr[i ... q-1]中的每一个值都不超过arr[q],且arr[q+1 ... j]中的每一个值都大于arr[q].

于是,我们根据k与左侧子数组arr[i ... q-1]的长度(为q-i)的大小关系:

如果k<=q-i,则数组arr[l .. r]前k大的值,就等于子数组arr[i .. q-1]前k大的值。

否则,数组arr[l ... r]前k大的值,就等于左侧子数组全部元素,加上右侧子数组arra[q+1 ... j]中前k-(q-i)大的值。

         

class Solution {
public:
    void qsort(vector<pair<int,int>>&v,int start,int end,vector<int>&ret,int k){
        int picked = rand() % (end - start + 1) + start;
        swap(v[picked],v[start]);

        int pivot = v[start].second;
        int index = start;
        for(int i = start + 1;i <= end; i++)
        {
            if(v[i].second >= pivot){
                swap(v[index + 1],v[i]);
                index++;
            }
        }
        swap(v[start],v[index]);

        if(k <= index - start){
            qsort(v,start,index - 1,ret,k);
        }else{
            for(int i = start; i <= index;i++){
                ret.push_back(v[i].first);
            }
            if(k > index - start + 1){
                qsort(v,index+1,end,ret,k - (index - start + 1));
            }
        }
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
         unordered_map<int,int>occurrences;
         for(auto& v: nums){
             occurrences[v]++;
         }

         vector<pair<int,int>>values;
         for(auto& kv: occurrences){
             values.push_back(kv);
         }
         vector<int>ret;
         qsort(values,0,values.size() - 1,ret,k);
         return ret;
    }
};

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值