算法(单调队列、优先队列)

引言

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

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

第一题

这一题我们将引入一个单调队列,可能大家对于单调栈并不陌生,但是单调队列的题目可能接触的比较少。单调队列解决的问题就是维护一个窗口的数据,类似于大顶堆的作用,但是并不是大顶堆。比如说大顶堆(优先队列),对于1,3,-1来说,就变成了3,1,-1,但是我们可以发现,当我们窗口移动的时候,我们需要把1弹出去,但是无论我们是从头还是尾,都无法弹出1,所以我们需要自己构造一个单调队列。我们这个队列里面只需要维护最大的元素,还是1,3,-1,当我们3进入之后,1就会自动的被我们弹出去,因为我们要求的是最大的元素,既然已经有3进来了,那么1的存在已经没有任何的意义了,所以只要有比前面大的元素进来,那么就会自动把前面那些比这个元素小的元素卷走,不需要我们在主函数里面手动调用pop()函数。这样子,我们就可以永远保证队头的元素是最大值,并且从队头到队尾,元素的大小依次减小。所以每次我们需要知道这个队列里面最大值是多少的时候,就访问队头元素即可。

我们这个队列是双端队列,也就是用deque实现的,而我们的队头是出队列的,队尾是入队列的,pop的含义就是每一次移动窗口的时候把多余的那一个元素从队头弹出去**(注意:这个操作并不包含我们加入一个比之前的大的元素然后把前面那些元素弹走,原理就是我们只需要维护这个队列里面最大的元素,一定要把最大的元素放在队头)**

push()函数就是加入元素,不过在加入元素之前要比较末尾的元素,为什么是末尾的元素呢?因为我们的队列是递减的,比如5,3要加入4,那么如果比较队头,就无法把3出队,而比较队尾,就是把3出队,然后把4入队,变成5,4

最后在主函数里面,我们先把队列里面的元素全部初始化好,然后窗口开始往后面移动

至于我们为什么要值传递,是因为方便我们直接操作元素,用numsi-k

cpp 复制代码
class MyQueue {
public:
    deque<int> que;
    void pop(int val) {
        if (!que.empty() && val == que.front()) {
            que.pop_front();
        }
    }

    void push(int val) {
        while (!que.empty() && val > que.back()) {
            que.pop_back();
        }
        que.push_back(val);
    }

    int getMaxValue () {
        return que.front();
    }
};

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue que;
        vector<int> res;
        for (int i = 0; i < k; i++) {
            que.push(nums[i]);
        }
        res.push_back(que.getMaxValue());
        for (int i = k; i < nums.size(); i++) {
            que.pop(nums[i - k]);
            que.push(nums[i]);
            res.push_back(que.getMaxValue());
        }
        return res;
    }
};

第二题

这一题用的是优先队列来解决前k个元素的问题,优先队列的底层是大顶堆和小顶堆来实现的,也就是我们在使用优先队列的时候是需要传入我们使用的是那个堆的cmp函数。这一题主要的难点就是我们到底是使用大顶堆还是小顶堆,大顶堆的根节点是最大值,我们每一次插入结点都会把堆顶的元素弹出去,然后把新加入的元素放入堆顶,然后从根节点开始重新调整这个堆,小顶堆也是一样。所以如果我们使用的是大顶堆,我们每一次弹出去的是最大频率的那一个,最后维护了k个最小的频率元素。所以我们应该使用小顶堆

cpp 复制代码
class Solution {
public:
    class cmp {
    public:
        bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
            return a.second > b.second;
        }   
    };

    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> umap;
        for(int i = 0; i < nums.size(); i++) {
            umap[nums[i]]++;
        }
        priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> que;
        for(auto iter = umap.begin(); iter != umap.end(); iter++) {
            que.push(*iter);
            if (que.size() > k) {
                que.pop();
            }
        }
        vector<int> res(k, 0);
        for (int i = k - 1; i >= 0; i--) {
            res[i] = que.top().first;
            que.pop();
        }
        return res;
    }
};

对于堆的调整代码,我这里也给出来了:这里传递的i就是1,因为是堆顶,然后不断的向下调整

cpp 复制代码
void DownAdjust(int a[], int i, int n) { 
    // 向下调整
    int now = i; // 当前结点
    int next;   // 值最大的孩子 
    while(2 * now <= n) { // now的左子树存在,也就是说当前结点至少有一个孩子
        next = 2 * now;
        if(2 * now + 1 <= n && a[2 * now + 1] > a[next]) {
            // 右子树存在,且右孩子值更大
            next = 2 * now + 1;
        }
        if(a[now] < a[next]) { // 父亲比孩子小
            std::swap(a[now], a[next]);
            now = next; // 再继续向下调整 
        }else { // 如果父亲比孩子大,说明已经是大顶堆了,不需要调整了
            break;
        }
    }
}