力扣刷题笔记:双端队列与优先队列(滑动串口最大值 & 前K个高频元素)

双端队列与优先队列(滑动串口最大值 & 前K个高频元素)

  • 知识点
  • 一、 滑动窗口最大值
    • 例题
    • 求解
    • 拓展
  • 二、前K个高频元素
    • 题目
    • 求解
  • 总结


知识点

栈与队列基础知识点

栈:
数据先进后出,可以通过stack.push(value)从栈顶添加元素,stack.top()访问栈顶元素,stack.pop()弹出栈顶元素;
队列:
数据先进先出,可以通过queue.push(value)从队尾添加元素,queue.front()访问队首元素,queue.pop()弹出队首元素;
双端队列:
同时具有栈和队列的性质,可以通过deque.push_front(value)从队首添加元素,deque.push_back(value)从队尾添加元素,deque.front()访问队首元素,deque.back()访问队尾元素,deque.pop_front()弹出队首元素,deque.pop_back()弹出队尾元素;
优先队列:
与队列相同,队首弹出元素,队尾添加元素,但会给元素赋予优先级,优先级大的元素最先弹出。


力扣例题:

一、 滑动窗口最大值

例题

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值 。
力扣刷题笔记:双端队列与优先队列(滑动串口最大值 & 前K个高频元素)_第1张图片
从题目描述和示例来说,如果不使用暴力解法不使用数组来求解的话,很显然本题很适合用队列的结构来求解。
然而普通队列和双端队列都没法对队内元素进行排序,而优先队列虽然能对队内元素进行排序,但是无法在窗口滑动时弹出比窗口内最大值小的元素(因为只能最先弹出优先级最高的元素)而导致之后判断窗口内最大值出错。

所以我们需要在元素入队之前提前处理进队元素

求解

(1). 情况一
由于题目要求求得滑动窗口内最大值的集合,如果入队元素比队首元素小就可以直接入队。

但如果在某个时刻入队的元素比队首元素大,那么可以直接先将队中的元素全部弹出,然后再入队,这样就能保证队首元素永远是最大值。并且因为只要求返回窗口中的最大值,所以提前弹出最大元素前的所有元素不会影响输出结果。
力扣刷题笔记:双端队列与优先队列(滑动串口最大值 & 前K个高频元素)_第2张图片

如图所示在弹出4之前,滑动窗口移除元素并不会影响窗口最大值等于队首的值

(2).情况二
入队元素小于队首值,但是大于队首后面的值。此时不能直接入队,因为如果队首值被弹出后,新队首值不是队列中最大的元素,如果访问的话就不符合题意了,那么参考情况一,我们就需要把队列前面比新入队元素小的元素全部弹出才能使队列保持单调性。

那么此时普通的队列就没办法满足我们的要求了,我们就需要一个两端都可以进行访问和弹出的线性数据存储的结构,所以我们可以使用dequeue来解决这个问题。

力扣刷题笔记:双端队列与优先队列(滑动串口最大值 & 前K个高频元素)_第3张图片
如图所示,即使窗口再继续向右滑动,将5弹出队列以后,新队首依然是窗口中的最大值。

利用dequeue实现的C++代码如下:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> result;
        Myqueue que;
        for(int i = 0 ; i < k ; i++ )
        {
            que.push(nums[i]);
        }
        result.push_back(que.max());//先创建第一个窗口,并获取窗口中的最大值
        for(int i = 0 ; i < nums.size()-k ;i++)
        {          
            que.pop(nums[i]);
            que.push(nums[i+k]);
            result.push_back(que.max());
        }
        return result;
    }
private:
    class Myqueue{
    public:
        deque<int> qt;
        void push(int x)
        {
            while(!qt.empty() && x > qt.back())//弹出比新入队元素小的元素
            {
                qt.pop_back();
            }
            qt.push_back(x);
        }
        
        void pop(int x)
        {
            if(x == qt.front())//当窗口移除最大值时再弹出队首元素
            {
                qt.pop_front();
            }
        }
        
        int max()
        {
            return qt.front();//返回队首元素
        }
    };
};

拓展

如果说只需要首位能够访问并且能新增和删除的结构就能做到这个效果的话,那么我们使用双链表也能做到。

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> result;
        Mylist list_slide;
        for(int i = 0 ; i < k ; i++ )
        {
            list_slide.push(nums[i]);
        }
        result.push_back(list_slide.max());
        for(int i = 0 ; i < nums.size()-k ;i++)
        {          
            list_slide.pop(nums[i]);
            list_slide.push(nums[i+k]);
            result.push_back(list_slide.max());
        }
        return result;
    }
private:
    class Mylist{
    public:
        list<int> slide_list;
        void push(int x)
        {
            while(!slide_list.empty() && x > slide_list.back())
            {
                slide_list.pop_back();
            }
            slide_list.push_back(x);
        }
        
        void pop(int x)
        {
            if(x == slide_list.front())
            {
                slide_list.pop_front();
            }
        }
        
        int max()
        {
            return slide_list.front();
        }
    };
};

但是占内存空间和速度不如dequeue来的好,也有可能是我写的垃圾。

二、前K个高频元素

题目

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
力扣刷题笔记:双端队列与优先队列(滑动串口最大值 & 前K个高频元素)_第4张图片
看到本题,高频这个关键词很容易就联想到了哈希表,但是如果用数组来统计频率的话题目没有给元素大小范围,盲目设置数组大小的话很浪费内存空间,所以我们使用unordered_map可能更合适一点,使用unordered_map将每个元素都标记成key值,并且统计它们出现的次数。

统计完元素的出现频率以后就剩下排序了,排序的方法有很多,对大规模数据进行排序,时间复杂度是 O(nlogn) 的算法更加高效,O(nlogn) 的算法有堆排序和归并排序,但是归并排序的空间复杂度是O(n),所以我们尝试用堆排序的方法解这道题。

求解

堆排序,我们可以使用数组等来实现,但是我们自己写很麻烦,我们可以直接使用STL库中的priority_queue优先队列来实现我们这个一个堆。

priority_queue<T, Container, Compare>
priority_queue<T>        //直接输入元素则使用默认容器和比较函数

T:输入元素类型;
Container:容器类型(vector或dequeue等);
Compare:比较函数;
默认比较函数是大根堆,默认容器是vecto;

根据题目要求,我们可以自己设计一个堆

struct _Node{
        int key;//输入的键值
        int weight;//出现的频率
        _Node(int _key,int _weight)//初始化
        {
            key = _key;
            weight = _weight;
        }
        friend bool operator<(_Node a,_Node b)//符号重载,自定义操作符
        {
            return a.weight < b.weight;//频率高的排前面
        }
    };

priority_queue<_Node> Myqueue;//创建新的堆

以上操作等同于

struct _Node{
        int key;//输入的键值
        int weight;//出现的频率
        _Node(int _key,int _weight)//初始化
        {
            key = _key;
            weight = _weight;
        }
   };
class compare{
        bool operator()(const _Node &a, const _Node &b) 
        {
             return a.weight < b.weight;
        }

  };

priority_queue<_Node,vector<_Node>,compare> Myqueue;

此时我们以及成功创建了一个以元素出现频率排序的大根堆,出现频率最高的元素为队首元素,弹出队首元素后第二高频率的元素就会成为新的队首,那么代码就能很容易完成了。

class Solution {
public:
    struct _Node{
        int key;
        int weight;
        _Node(int _key,int _weight)
        {
            key = _key;
            weight = _weight;
        }
        friend bool operator<(_Node a,_Node b)
        {
            return a.weight < b.weight;
        }
    };

    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> map;
        priority_queue<_Node>Myqueue;
        vector<int> result;
        for(int s:nums)
        {
            map[s]++;
        }
        for(auto iter = map.begin();iter != map.end();++iter)
        {
            _Node new_Node(iter->first,iter->second);
            Myqueue.push(new_Node);
        }
        for(int i = 0 ; i < k; i++)
        {
            result.push_back(Myqueue.top().key);
            Myqueue.pop();
        }
        return result;
    }

};

总结

队列和栈都是很常见的数据结构,灵活运用栈和队列还是必须要熟悉它们的性质和底层结构。

你可能感兴趣的:(leetcode,算法,数据结构)