滑动窗口9:239. 滑动窗口最大值和480滑动窗口中位数

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了滑动窗口9:239. 滑动窗口最大值和480滑动窗口中位数相关的知识,希望对你有一定的参考价值。

这两个题虽然是滑动窗口的 问题,但是解决思路更多需要堆等结构,不一定是双指针。

题目要求:

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

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

示例1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

这种方法我们在基础算法的堆部分介绍过。对于「最大值」,K个最大这种场景,优先队列(堆)是首先应该考虑的思路,其中的大根堆可以帮助我们实时维护一系列元素中的最大值。

对于本题而言,初始时,我们将数组 nums 的前 k个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。

我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 (num,index),表示元素num 在数组中的下标为index。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] pair1, int[] pair2) {
                return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
            }
        });
        for (int i = 0; i < k; ++i) {
            pq.offer(new int[]{nums[i], i});
        }
        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];
        for (int i = k; i < n; ++i) {
            pq.offer(new int[]{nums[i], i});
            while (pq.peek()[1] <= i - k) {
                pq.poll();
            }
            ans[i - k + 1] = pq.peek()[0];
        }
        return ans;
    }
}

 除此之外,还可以使用双向队列、单调队列来解决,该方法,我们到《单调栈和单调队列》一章再看。

480. 滑动窗口中位数

中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。

给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数,每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。

示例:
给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。
窗口位置                      中位数
---------------               -----
[1  3  -1] -3  5  3  6  7       1
 1 [3  -1  -3] 5  3  6  7      -1
 1  3 [-1  -3  5] 3  6  7      -1
 1  3  -1 [-3  5  3] 6  7       3
 1  3  -1  -3 [5  3  6] 7       5
 1  3  -1  -3  5 [3  6  7]      6

  因此,返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]。

这道题官方的解释真是又臭又长,我们可以寻找一些更简洁易懂的思路来解决问题。

1.二分查找+冒泡排序

不要把问题想得太复杂,二分查找+冒泡排序完全可以解决

  1.  维护一个排过序的滑动窗口数组
  2. 使用二分查找检索删除的索引
  3. 将需要删除的值替换为需要插入的值
  4. 使用局部冒泡排序保证数组顺序

这个方法思想直接易懂,但是写起来有点长,而且要涉及多种算法,编写难度不低。

class Solution {
    public double[] medianSlidingWindow(int[] nums, int k) {
        double[] res = new double[nums.length - k + 1];
        int[] window = new int[k];
        //添加初始值
        for (int i = 0; i < k; i++) {
            window[i] = nums[i];
        }
        //初始的快排,懒得写直接调用
        Arrays.sort(window);
        res[0] = getMid(window);
        //窗口滑动
        for (int i = 0; i < nums.length - k; i++) {
            //需要删除的数
            int index = search(window, nums[i]);
            //替换为需要插入的数
            window[index] = nums[i + k];
            //向后冒泡
            while (index < window.length - 1 && window[index] > window[index + 1]) {
                swap(window, index, index + 1);
                index++;
            }
            //向前冒泡
            while (index > 0 && window[index] < window[index - 1]) {
                swap(window, index, index - 1);
                index--;
            }
            res[i + 1] = getMid(window);
        }
        return res;
    }

    //交换
    private void swap(int[] window, int i, int j) {
        int temp = window[i];
        window[i] = window[j];
        window[j] = temp;
    }

    //求数组的中位数
    private double getMid(int[] window) {
        int len = window.length;
        if (window.length % 2 == 0) {
            //避免溢出
            return window[len / 2] / 2.0 + window[len / 2 - 1] / 2.0;
        } else {
            return window[len / 2];
        }
    }

    //最简单的二分查找
    private int search(int[] window, int target) {
        int start = 0;
        int end = window.length - 1;
        while (start <= end) {
            int mid = start + (end - start) / 2;
            if (window[mid] > target) {
                end = mid - 1;
            } else if (window[mid] < target) {
                start = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
}

在这个题里,我们就可以使用jdk的库函数来简化工作了,二分查找和排序都可以直接用了。代码如下:

class Solution {
       public double[] medianSlidingWindow(int[] nums, int k) {
        int len = nums.length - k + 1;
        double[] res = new double[len];
        int[] arr = Arrays.copyOfRange(nums, 0, k);
        Arrays.sort(arr);
        for (int i = 0; i < len; i++) {
            if (k % 2 == 0) {
                res[i] = (0.00 + arr[k/2] + arr[k/2-1]) / 2;
            } else {
                res[i] = arr[k/2];
            }
            if (i == len - 1) break;
            update(arr, nums[i], nums[i + k]);
        }
        return res;
        
    }
    
    void update(int[] arr, int rm, int add) {
        if (rm == add) return;
        int idx = Arrays.binarySearch(arr, rm);
        int i = 0;
        if (add > rm) {
            for (i = idx; i < arr.length - 1; i++) {
                if (arr[i+1] >= add) break;
                arr[i] = arr[i+1];
            }
        } else {
            for (i = idx; i > 0; i--) {
                if (arr[i - 1] <= add) break;
                arr[i] = arr[i-1];
            }
        }
        arr[i] = add;
    }
}

 

 

以上是关于滑动窗口9:239. 滑动窗口最大值和480滑动窗口中位数的主要内容,如果未能解决你的问题,请参考以下文章

239. 滑动窗口最大值

LeetCode 239. 滑动窗口最大值

leetcode 239. 滑动窗口最大值(单调队列)

leetcode_239. 滑动窗口最大值

239. 滑动窗口最大值

leetcode 239. 滑动窗口最大值