羊羊刷题笔记Day12/60 | 第五章 栈与队列P3 | 239. 滑动窗口最大值、347.前 K 个高频元素、栈与队列总结

106 阅读7分钟

239 滑动窗口最大值

自己写

思路:此题分为两种情况,

  • k == length:直接求最大数
  • k < length :用for循环继续遍历判断对第 k + 1个以及之后元素取最大值

但此方法时间复杂度O(n * k2)较大,对大量数据Timeout

public int[] maxSlidingWindow(int[] nums, int k) {
    int[] result = new int[nums.length - k + 1];

    // 长度等于k的
    if (nums.length == k) {
        int max = nums[0];
        for (int i = 1; i < nums.length; i++) {
            max = Math.max(max, nums[i]);
        }
        result[0] = max;
    }

        // 长度大于k的
    else  {
        Queue<Integer> queue = new LinkedList<>();


        // k个元素先进队列
        for (int i = 0; i < k; i++) {
            queue.add(nums[i]);
            int max = -Integer.MAX_VALUE - 1;
            for (Integer num : queue) {
                max = Math.max(max, num);
            }
            result[0] = max;
        }

        // 判断大小,并将max插入到result数组
        for (int i = k; i <= nums.length - 1; i++) {
            // 出队进队
            queue.poll();
            queue.add(nums[i]);

            // 选最大的
            int max = -Integer.MAX_VALUE - 1;
            for (Integer num : queue) {
                max = Math.max(max, num);
            }
            result[i - k + 1] = max;
        }
    }
    return result;
}

看答案:

对队列进行优化

  • 出队:遍历数组s后面的元素,只有出队元素是最大值时才出队
  • 进队:在出口处把小于进队元素的出队,再进队
  • 获取最大值(也是获取出队口第一个值):由于实现了单调数组,因此第一个数则为最大值

优点:去掉了不必维护的数,节省了时间复杂度 O(n)

class MyQueue{
    Deque<Integer> deque = new LinkedList<>();

    /**
* 出队
* @param val
*/
    void poll(int val){
        // 判空以及判断弹出值是否为最大值,相等则弹出
        if (!deque.isEmpty() && val == deque.peek()){
            deque.poll();
        }
    }

    /**
* 入队
* @param val
*/
    void add(int val){
        // 判空且如果入口处元素比入队元素小则弹出 - 保证队列元素单调递减(小的元素不需要维护)
        // 注意:此时不是严格的队列,是一个在出口和入口都能插入删除的特殊队列
        while (!deque.isEmpty() && val > deque.getLast()){
            deque.removeLast();
        }
        // 移除完后加入
        deque.add(val);
    }

    /**
* 获取最大元素
* @return
*/
    int getMaxValue(){
        // 实际上就是取队列第一个元素
        return deque.peek();
    }
}


class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 方法2 :自定义数组
        if (nums.length == 1){
            return nums;
        }

        // 初始化值以及队列
        int[] result = new int[nums.length - k + 1];
        int count = 0;
        MyQueue myQueue = new MyQueue();
        for (int i = 0; i < k; i++) {
            myQueue.add(nums[i]);
        }
        result[count++] = myQueue.getMaxValue();

        for (int i = k; i < nums.length; i++) {
            // 出队 - 数组最后元素
            myQueue.poll(nums[i - k]);
            // 入队 - 数组第k + 1个元素
            myQueue.add(nums[i]);
            // 获取最大值
            result[count++] = myQueue.getMaxValue();
        }

        return result;


    }

347 前 K 个高频元素

自己写:

思路:将nums每个元素放入HashMap,然后对map里的value降序排序,并将key取出来,取前k个元素放在res数组

局限性:缺点:sorted()方法本身受限**,只能进行全部排序**.而大小顶堆可以只维护前面k个元素(具体不太清楚,二刷要回头看)

public int[] topKFrequent(int[] nums, int k) {
    HashMap<Integer, Integer> hm = new HashMap<>();
    // 将nums每个元素放入HashMap
    for (int num : nums) {
        hm.put(num, hm.getOrDefault(num,0) + 1);
    }

    int[] res = new int[k];
    int count = 0;

    // 对map里的value降序排序,并将key取出来,取前k个元素放在res数组
    // 缺点:sorted本身方法受限,只能进行全部排序.大小顶堆可以只维护前面k个元素(具体不太清楚,二刷要回头看)
    int[] ints = hm.entrySet().stream().sorted(new Comparator<Map.Entry<Integer, Integer>>() {
        @Override
        public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) {

            System.out.println("o1.getValue():" + o1.getValue());
            System.out.println("o2.getValue():" + o2.getValue());
            return o2.getValue() - o1.getValue();
        }
    }).mapToInt(x -> x.getKey()).toArray();

    for (int i = 0; i < k; i++) {
        res[i] = ints[i];
    }
    return res;
}

看答案:

使用了小顶堆,优化性能 - 排序不需要全部排序 只需要排序前k个元素

二刷理解

// 方法2: 优化性能 - 排序不需要全部排序 只需要排序前k个元素
Map<Integer,Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
for(int num:nums){
    map.put(num,map.getOrDefault(num,0)+1);
}
//在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry:map.entrySet()){//小顶堆只需要维持k个元素有序
    if(pq.size()<k){//小顶堆元素个数小于k个时直接加
        pq.add(new int[]{entry.getKey(),entry.getValue()});
    }else{
        if(entry.getValue()>pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
            pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
            pq.add(new int[]{entry.getKey(),entry.getValue()});
        }
    }
}
int[] ans = new int[k];
for(int i=k-1;i>=0;i--){//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
    ans[i] = pq.poll()[0];
}
return ans;

栈与队列总结

栈与队列的理论基础

可以出一道面试题:栈里面的元素在内存中是连续分布的么? 这个问题有两个陷阱:

  • 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
  • 陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。

在 用栈与队列:栈实现队列栈与队列:队列实现栈 中,值得一提的是,用栈与队列:队列实现栈 中,其实只用一个队列就够了。 一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。

栈经典题目

括号匹配问题

栈与队列:系统中处处都是栈的应用中我们讲解了括号匹配问题。
括号匹配是使用栈解决的经典问题。 先来分析一下 这里有三种不匹配的情况,

  1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
  2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
  3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。

这里还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!

字符串去重问题

1047. 删除字符串中的所有相邻重复项中讲解了字符串去重问题,特别像对对碰游戏,入栈时元素相同 -> 删除 思路就是可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了

逆波兰表达式问题

150. 逆波兰表达式求值中讲解了求逆波兰表达式。本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,要注意的是减法和除法两个数位置问题。和1047. 删除字符串中的所有相邻重复项非常相似

队列经典题目

滑动窗口最大值问题(单调队列)

239. 滑动窗口最大值中讲解了一种数据结构:单调队列。(与暴力解法对比,优化了维护问题,比push进的元素还要小的元素出栈)

主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。 那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。 然而没有现成的数据结构,需要自己构造

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

  1. pop(value):当出队元素(value == max)为最大值时,才出队。
  2. push(value):将入口处比value小的元素出队,再进队

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

单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本地中的单调队列实现就是固定的写法。 我们用deque作为单调队列的底层数据结构,deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。

求前 K 个高频元素(优先级队列)

347.前 K 个高频元素中讲解了求前 K 个高频元素。通过求前 K 个高频元素,引出另一种队列就是优先级队列

什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。 而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢? 利用max-heap(大顶堆)完成对元素的排序(具体看链接)

此部分需要二刷理解,大顶堆小顶堆

什么是堆呢? 堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用PriorityQueue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。 本题就要使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序! 所以排序的过程的时间复杂度是O(\log k),整个算法的时间复杂度是O(n\log k)。

总结

在栈与队列系列中,我们强调栈与队列的基础,也是很多同学容易忽视的点。 使用抽象程度越高的语言,越容易忽视其底层实现。 我们用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。 接着,通过括号匹配问题字符串去重问题逆波兰表达式问题来系统讲解了栈在系统中的应用以及使用技巧。 通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。

学习资料:

239. 滑动窗口最大值

347.前 K 个高频元素(二刷理解大小顶堆)

栈与队列总结