原题:https://leetcode.com/problems/sliding-window-maximum/
这题的暴力解法很容易想到,没什么花头,就是遍历找最大值。
折中解法也不是很难想到,可以考虑用一个大顶堆来保存每次滑动一格后的情况(这里由于更新堆的时间复杂度为log(k),所以只能做到时间复杂度为O(nlog(k))。
可是题干偏偏有个follow up说存在O(n)的解法,这就比较难了,相当于说我需要用一个数据结构,维护一个集合,需要知道当下该集合中的最大值,同时做一次滑动窗口操作(删一个值,加一个值)之后,仍能知道最大值是谁,并且这个删&加的操作时间复杂度为O(1)时间复杂度,虽然这个需求理出来了,可是如何实现这么一个数据结构却犯了难。想了一阵子没想出来。后来看了 答案,是说用一个deque(双端队列)来实现以上要求的数据结构,而其本质思维是,不需要真的维护窗口大小k个元素来做最值判断,因为根据一些原则,是可以排除掉一部分元素的(剪枝的思维),例如窗口大小为3,右滑一步会发生新进入一个元素到窗口,同时推出一个元素出窗口,如果在滑动前,最大值为3,那么只要新滑入的值大于3,例如4,那么就可以确定未来这个3不可能成为最大值了(是指特定的这个3,而不是所有3),因为未来要么3被滑出窗口了,要么它后面还跟着个更大的4,所以怎么也轮不到它做老大,所以这种值就可以直接排除,而如果新进来的值比3小,例如是2,则在2被滑出前,还是有可能成为未来某窗口的最大值的,于是我们把这个2推入双端队列,总之最终的结果就是,双端队列最前端始终保持着当前窗口的最大值,后面的几个元素则保持着未来可能是最大值的候选项,每次滑动窗口时,按照如下原则更新这个双端队列,直到窗口滑到底,输出所有结果
更新双端队列的原则:
从队头开始往后扫描,只要扫到的元素小于新进入的元素,就把该元素踢出队
从堆尾开始往前扫描,只要扫到的元素小于新进元素,就把该元素踢出队
如队头元素已滑出窗口,则队头元素被踢出队
为了方便判断对头元素到底有没有被滑出窗口,双端队列可以记录数组元素的下标值,而不是记录数组的元素的真值,这样比较方便。
思路如上,代码如下:
package com.example.demo.leetcode; import java.util.ArrayDeque; import java.util.Deque; public class SlidingWindowMax { /** * sample input: nums = [1,3,-1,-3,5,3,6,7], and k = 3 * sample output: 3, 3, 5, 5, 6, 7 * @param nums * @param k * @return */ public int[] maxSlidingWindow(int[] nums, int k) { // ---边界条件考虑, nums为0或1, k>nums.length if(nums==null || nums.length<1){ int[] non = new int[0]; return non; } if(nums.length==1){ return nums; } if(k>nums.length){ k = nums.length; } int[] ret = new int[nums.length-k+1]; Deque<Integer> dq = new ArrayDeque<>(); for(int i=0;i<nums.length-k+1;i++){ if(i==0){ int initMaxIndex = initDequeue(nums, k, dq); ret[i] = nums[initMaxIndex]; }else{ // action: move from i-1 to i int maxIndex = slideWindow(nums, i, k, dq); ret[i] = nums[maxIndex]; } } return ret; } public int initDequeue(int[] nums, int k, Deque<Integer> dq){ for(int i=0;i<k;i++){ magicIntoq(dq, i, nums); } return dq.getFirst(); } private void magicIntoq(Deque<Integer> dq, Integer indexOfVal, int[] arr){ if(dq.isEmpty()){ dq.addFirst(indexOfVal); }else{ //用新插入的值,循环前往后比头部,把比新值小的全出队 while(!dq.isEmpty() && arr[dq.getFirst()]<=arr[indexOfVal]){ dq.removeFirst(); } //用新插入的值循环比尾部 while(!dq.isEmpty() && arr[dq.getLast()]<=arr[indexOfVal]){ dq.removeLast(); } // 入队新值 dq.addLast(indexOfVal); } } // assumption: i>0, 对应从窗口左侧从i-1滑动到i, public int slideWindow(int[] nums, int i, int k, Deque<Integer> dq){ // 先考虑dq要不要推出一个元素 if(dq.getFirst()==i-1){ dq.removeFirst(); } // 再尝试推入i+k-1下标 magicIntoq(dq, i+k-1 , nums); return dq.getFirst(); } public static void main(String[] args) { // int[] arr = {1,3,-1,-3,5,3,6,7}; // int[] arr = {3}; // int[] arr = {}; int[] arr = {7,2,4}; SlidingWindowMax demo = new SlidingWindowMax(); int[] ret = demo.maxSlidingWindow(arr, 2); for(int i=0;i<ret.length;i++){ System.out.println(ret[i]); } } }
反思:
当时间复杂度怎么看都降不下去时,可以考虑从其他角度减枝,排除一些不可能的数据或选项,则可能降低时间复杂度!