算法训练营Day12

#Java #Review

开源学习资料

Feeling and experiences:

滑动窗口最大值:力扣题目链接

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

返回滑动窗口中的最大值。

目前为止出现的第一道Hard。

最开始想到的就是暴力解法,而且很容易能写出来

class Solution {
     public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0) {
            return new int[0];
        }

        int n = nums.length;
        int[] result = new int[n - k + 1];

        for (int i = 0; i <= n - k; i++) {
            int max = Integer.MIN_VALUE;
            for (int j = i; j < i + k; j++) {
                max = Math.max(max, nums[j]);
            }
            result[i] = max;
        }

        return result;
    }

}

但是这样提交就超时了

算法训练营Day12_第1张图片

作为Hard题,肯定不会这么容易就能通过。

但是在这之后,我没有想到更好的办法了,直接看了代码随想录的视频题解:单调队列!

视频中的思路使用的单调栈。

单调栈的特点是栈内的元素保持调性,可以是单调递增或者单调递减。

严格来说的话应该用的是单调双端队列(思路是一样的), 思路归纳:


1. 维护递减栈:
   - 对于单调递减栈,栈内的元素是递减的。这意味着栈底元素是整个栈中的最大值,而栈顶元素是最小的。这是因为当遇到一个新元素时,我们需要将栈中比它小的元素弹出,以便新元素成为当前窗口的最大值。

2. 栈中存储元素的特殊含义:
   - 在滑动窗口问题中,我们并不直接在栈中存储元素的值,而是存储元素的索引。这是为了在维护递减栈的过程中,能够方便地判断元素是否在当前窗口范围内,以及计算窗口的大小。

3. 遍历数组并维护栈:
   - 从左到右遍历数组,对于每个元素:
      - 移除过期的元素:检查栈顶元素是否超出当前窗口的范围,如果是则弹出。
      - 保持递减性:将栈中比当前元素小的元素弹出,以保持递减栈的性质。
      - 将当前元素的索引入栈。

4. 记录结果:
   - 当遍历到形成一个完整窗口的位置时,即窗口大小达到设定值时,可以从栈底获取当前窗口的最大值。这是因为栈底元素对应的索引是最早进入窗口的元素,同时它是窗口中的最大值。

创建一个Deque(双端队列),来表示一个动态且元素严格单调的滑动窗口。

创建一个result数组来存放结果。

注意:双端队列中存放的下标,而不是原数组中的数。

模拟如下:

假如题目原数组:【2,5,3,4,1】,k = 3(滑动窗口的容量)

下标分别为0,1,2,3,4

算法训练营Day12_第2张图片

在双端队列中进行入队出队操作(单调递减):

注意:操作的是下标值!

算法训练营Day12_第3张图片

队列最左边的数就是当前窗口下的最大值!

代码解释如下:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
    //创建一个双端队列,进行入队出队操作(注意:操作的是下标)
    Deque deque = new LinkedList<>();
    int index =0;
    //创建一个数组来记录结果,容量为num.length-k+1; 因为手动模拟发现,到length-k个数时就有length-k个最大值,最后k个数里面再出1个,这样就会有length-k+1个最大值
    int []res = new int[nums.length-k+1];

    for(int i =0;i nums[deque.peekLast()]){
            deque.removeLast();//严格递减
        }
        deque.add(i);
        

        //记录结果
        if(i >=k-1)
        res[index++] = nums[deque.peek()];
    }
    return res;
    }
}

其实思路很简单,只要能手动模拟出来,代码是很简单的。

第一要注意到双端队列存的是下标!(最开始就没注意,导致绕了很久)

第二要理解到双端队列其实也是一个动态的滑动窗口!

剖析细节:

res为存放结果的数组,为什么容量为nums的长度-k+1,用图模拟如下:

算法训练营Day12_第4张图片

为什么用deque.peek() == i- k;当作条件来判断它是否超过生命周期?

要知道是deque.peek() 是当前双端队列中的头(最左边),上述也说了双端队列的头(最左边)是存放的最大元素下标,当遍历到 i 时,当前位置 i 减去窗口容量 k 都还等于双端队列的头部存放的下标,说明已经超过了生命周期,删除该值。

算法训练营Day12_第5张图片

要手动模拟才能深入理解!

为什么要当 i >= k-1时才开始存放结果?

因为当移动到 k-1 这个位置时,滑动窗口刚好到达最大容量。

在B站上有一个视频把该题的原理解释的很明白:B站视频链接

在纯文字中,不好描述与解释这个Deque的深刻含义!(抽象为另一个动态的滑动窗口)

时刻回顾~

前 K 个高频元素:力扣题目链接

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

该题先大致理解,后续再对细节进行深入理解,对未接触的知识进行补充

回顾学习优先队列,priorityQueue 的用法 , 堆排序。

lass Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map occurrences = new HashMap();
        for (int num : nums) {
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
        }

        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        PriorityQueue queue = new PriorityQueue(new Comparator() {
            public int compare(int[] m, int[] n) {
                return m[1] - n[1];
            }
        });
        for (Map.Entry entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {
                    queue.poll();
                    queue.offer(new int[]{num, count});
                }
            } else {
                queue.offer(new int[]{num, count});
            }
        }
        int[] ret = new int[k];
        for (int i = 0; i < k; ++i) {
            ret[i] = queue.poll()[0];
        }
        return ret;
    }

将频率数组排序,并找到一个阈值,该阈值是第k个高频元素的频率。然后,我们遍历哈希表,将频率大于或等于阈值的元素添加到结果列表中。

今日主要学习了单调栈,单调队列的原理,及其运用。(深刻理解花费了很多时间)

常回顾,多理解~

一声梧叶一声秋,

一点芭蕉一点愁,

三更归梦三更后......

Fighting!
 

你可能感兴趣的:(算法)