滑动窗口中位数

112 阅读3分钟

题目

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

例如:

[2,3,4],中位数是 3 [2,3],中位数是 (2 + 3) / 2 = 2.5 给你一个数组 nums,有一个大小为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数,每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。

暴力

import java.util.Arrays;

public class Main {


    public static void main(String[] args) {

        Main main = new Main();
        int [] nums = new int[] {1, 4, 2, 3};
        main.medianSlidingWindow(nums, 4);
    }


    public double[] medianSlidingWindow(int[] nums, int k) {
        double [] doubleNums = new double[nums.length];
        for (int i = 0; i < nums.length; i ++) {
            doubleNums[i] = nums[i];
        }

        double [] ans = new double[nums.length - k + 1];
        int left = 0;
        int right = left + k - 1;
        double [] tempArray = new double[k];
        if (k % 2 != 0) {
            // 如果窗口是奇数
            while (right < nums.length) {
                int midIndex = (right - left) / 2;
                tempArray = Arrays.copyOfRange(doubleNums, left, right + 1);
                Arrays.sort(tempArray);
                ans[left] = tempArray[midIndex];
                left ++;
                right ++;
            }
        } else {
            // 如果窗口是偶数
            while (right < nums.length) {
                int midIndex1 = (right - left - 1) / 2;
                int midIndex2 = (right - left + 1) / 2;
                tempArray = Arrays.copyOfRange(doubleNums, left, right + 1);
                Arrays.sort(tempArray);
                ans[left] = (tempArray[midIndex1] + tempArray[midIndex2]) / 2;
                left ++;
                right ++;
            }

        }
        return ans;
    }

}

基本思路

  1. 利用两个指针实现窗口, 每次窗口内的元素重新排序取中位数

缺点

  1. 暴力解法, 会超时

双优先队列 + 延迟删除

class Solution {
    public double[] medianSlidingWindow(int[] nums, int k) {
        DualHeap dh = new DualHeap(k);
        for (int i = 0; i < k; ++i) {
            dh.insert(nums[i]);
        }
        double[] ans = new double[nums.length - k + 1];
        ans[0] = dh.getMedian();
        for (int i = k; i < nums.length; ++i) {
            dh.insert(nums[i]);
            dh.erase(nums[i - k]);
            ans[i - k + 1] = dh.getMedian();
        }
        return ans;
    }
}

class DualHeap {
    // 大根堆,维护较小的一半元素
    private PriorityQueue<Integer> small;
    // 小根堆,维护较大的一半元素
    private PriorityQueue<Integer> large;
    // 哈希表,记录「延迟删除」的元素,key 为元素,value 为需要删除的次数
    private Map<Integer, Integer> delayed;

    private int k;
    // small 和 large 当前包含的元素个数,需要扣除被「延迟删除」的元素
    private int smallSize, largeSize;

    public DualHeap(int k) {
        this.small = new PriorityQueue<Integer>(new Comparator<Integer>() {
            public int compare(Integer num1, Integer num2) {
                return num2.compareTo(num1);
            }
        });
        this.large = new PriorityQueue<Integer>(new Comparator<Integer>() {
            public int compare(Integer num1, Integer num2) {
                return num1.compareTo(num2);
            }
        });
        this.delayed = new HashMap<Integer, Integer>();
        this.k = k;
        this.smallSize = 0;
        this.largeSize = 0;
    }

    public double getMedian() {
        return (k & 1) == 1 ? small.peek() : ((double) small.peek() + large.peek()) / 2;
    }

    public void insert(int num) {
        if (small.isEmpty() || num <= small.peek()) {
            small.offer(num);
            ++smallSize;
        } else {
            large.offer(num);
            ++largeSize;
        }
        makeBalance();
    }

    public void erase(int num) {
        delayed.put(num, delayed.getOrDefault(num, 0) + 1);
        if (num <= small.peek()) {
            --smallSize;
            if (num == small.peek()) {
                prune(small);
            }
        } else {
            --largeSize;
            if (num == large.peek()) {
                prune(large);
            }
        }
        makeBalance();
    }

    // 不断地弹出 heap 的堆顶元素,并且更新哈希表
    private void prune(PriorityQueue<Integer> heap) {
        while (!heap.isEmpty()) {
            int num = heap.peek();
            if (delayed.containsKey(num)) {
                delayed.put(num, delayed.get(num) - 1);
                if (delayed.get(num) == 0) {
                    delayed.remove(num);
                }
                heap.poll();
            } else {
                break;
            }
        }
    }

    // 调整 small 和 large 中的元素个数,使得二者的元素个数满足要求
    private void makeBalance() {
        if (smallSize > largeSize + 1) {
            // small 比 large 元素多 2 个
            large.offer(small.poll());
            --smallSize;
            ++largeSize;
            // small 堆顶元素被移除,需要进行 prune
            prune(small);
        } else if (smallSize < largeSize) {
            // large 比 small 元素多 1 个
            small.offer(large.poll());
            ++smallSize;
            --largeSize;
            // large 堆顶元素被移除,需要进行 prune
            prune(large);
        }
    }
}

基本思路

  1. 利用两个队列分别维护窗口中较大的一半元素(小顶堆, 即这个队列最小的元素在顶部)和较小的一半元素(大顶堆), 每个队列中的元素是有序的, 约定小顶堆的元素和大顶堆的元素数目相同(窗口为偶数), 或者小顶堆的元素多一个(窗口为奇数). 这样获取中位数就是小顶堆的顶部元素或者两个堆顶元素的平均值

优先队列是有序的, 使用优先队列来保证堆内部的有序性

  1. 窗口滑动主要是新加元素和减去元素

新加元素: 与两个堆顶元素比较, 然后决定加入哪个堆(加入后优先队列的特点维护有序), 然后两个堆的数目不符合要求, 需要将元素在两个堆之间转移一下

减去元素: 本身优先队列是不支持删除元素的, 所以我们在需要删除元素的时候, 并不从队列中删除元素, 而是记录下来, 需要利用一个map记录元素的值和需要删除的次数. 每次在使用堆顶元素的时候, 都判断该元素是不是需要被删除, 如果需要就删除该元素, 调整两个堆的数目, 再次判断, 也就是延迟删除

  1. 两个堆的数目

维护两个堆的数目符合要求是否重要, 即约定小顶堆的元素和大顶堆的元素数目相同(窗口为偶数), 或者小顶堆的元素多一个(窗口为奇数), 每次增加或者删除元素后, 都需要调整数量, 来维持平衡