滑动窗口-滑动窗口中位数

2020-01-10 16:16:41

问题描述

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

例如:

[2,3,4],中位数是 3

[2,3],中位数是 (2 + 3) / 2 = 2.5

给出一个数组 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]。

提示:

你可以假设 k 始终有效,即:k 始终小于输入的非空数组的元素个数。
与真实值误差在 10 ^ -5 以内的答案将被视作正确答案。

问题求解

解法一:双pq

注意!Java中比较Integer务必使用compareTo,直接相减会出错。

    public double[] medianSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        double[] res = new double[n - k + 1];
        // 排序后的右侧一半数据
        PriorityQueue<Integer> minheap = new PriorityQueue<Integer>((Integer o1, Integer o2) -> o1.compareTo(o2));
        // 排序后的左侧一半数据
        PriorityQueue<Integer> maxheap = new PriorityQueue<Integer>((Integer o1, Integer o2) -> o2.compareTo(o1));
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            if (maxheap.isEmpty() || num <= maxheap.peek()) maxheap.add(num);
            else minheap.add(num);
            if (i >= k) {
                int remove = nums[i - k];
                if (maxheap.contains(remove)) maxheap.remove(remove);
                else minheap.remove(remove);
            }
            while (minheap.size() > maxheap.size()) maxheap.add(minheap.poll());
            while (maxheap.size() > minheap.size() + 1) minheap.add(maxheap.poll());
            if (i >= k - 1) {
                if (k % 2 != 0) res[i - k + 1] = Double.valueOf(maxheap.peek());
                else res[i - k + 1] = 0.5 * minheap.peek() + 0.5 * maxheap.peek();
            }
        }
        return res;
    }

  

解法二:插入排序

leetcode上有一条非常类似的题目,叫sliding window max,那一题可以使用单调队列高效的进行求解。

本题和上述的题目还是有一点区别的,首先最优的时间复杂度不是O(n),而是O(nlogn),如果要达到O(nlogn)需要自定义bst这个是非常耗时的。有同学使用两个优先队列来进行求解,但是在这个过程中需要维护优先队列,尤其是删除队列中的元素,这个在实现的时候是O(n)的时间复杂度,因此使用两个优先队列并不能很好的降低时间复杂度。

如何简洁高效的解决问题才是关键。

首先对于获得中位数,显然需要对数字进行排序。如果每次都重新排序,则每个窗口都需要O(klogk)的时间复杂度,这个是难以接受的。

事实上,考虑到每次滑动窗口已然有序的事实,如果采用插入排序的思路,那么每次维护排序的性质,就只需要O(n)的时间复杂度,那么和使用两个优先队列的时间复杂度就是一样的了,并且在实际实现的时候要简单和容易很多。

下面给出使用insert sort的算法来解决该问题的算法,实际的算法表现还是非常不错的。

    public double[] medianSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        double[] res = new double[n - k + 1];
        int[] sorted = new int[k];
        for (int i = 0; i < k; i++) sorted[i] = nums[i];
        Arrays.sort(sorted);
        res[0] = sorted[(k - 1) / 2] * 0.5 + sorted[k / 2] * 0.5;
        for (int i = k; i < n; i++) {
            delete(sorted, nums[i - k]);
            add(sorted, nums[i]);
            res[i - k + 1] = sorted[(k - 1) / 2] * 0.5 + sorted[k / 2] * 0.5;
        }
        return res;
    }
    
    private void delete(int[] nums, int num) {
        int n = nums.length;
        int idx = 0; 
        for (; idx < n; idx++) {
            if (nums[idx] == num) break;
        }
        for (int i = idx + 1; i < n; i++) {
            nums[i - 1] = nums[i];
        }
    }
    
    private void add(int[] nums, int num) {
        int n = nums.length;
        int idx = n - 2;
        for (; idx >= 0; idx--) {
            if (nums[idx] > num) nums[idx + 1] = nums[idx];
            else {
                nums[idx + 1] = num;
                break;
            }
        }
        if (idx == -1) nums[0] = num;
    }

  

posted @ 2020-01-10 16:24  hyserendipity  阅读(694)  评论(0编辑  收藏  举报