题目
中位数是有序序列最中间的那个数。如果序列的大小是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
例如:
[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;
}
}
基本思路
- 利用两个指针实现窗口, 每次窗口内的元素重新排序取中位数
缺点
- 暴力解法, 会超时
双优先队列 + 延迟删除
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);
}
}
}
基本思路
- 利用两个队列分别维护窗口中较大的一半元素(小顶堆, 即这个队列最小的元素在顶部)和较小的一半元素(大顶堆), 每个队列中的元素是有序的, 约定小顶堆的元素和大顶堆的元素数目相同(窗口为偶数), 或者小顶堆的元素多一个(窗口为奇数). 这样获取中位数就是小顶堆的顶部元素或者两个堆顶元素的平均值
优先队列是有序的, 使用优先队列来保证堆内部的有序性
- 窗口滑动主要是新加元素和减去元素
新加元素: 与两个堆顶元素比较, 然后决定加入哪个堆(加入后优先队列的特点维护有序), 然后两个堆的数目不符合要求, 需要将元素在两个堆之间转移一下
减去元素: 本身优先队列是不支持删除元素的, 所以我们在需要删除元素的时候, 并不从队列中删除元素, 而是记录下来, 需要利用一个map记录元素的值和需要删除的次数. 每次在使用堆顶元素的时候, 都判断该元素是不是需要被删除, 如果需要就删除该元素, 调整两个堆的数目, 再次判断, 也就是延迟删除
- 两个堆的数目
维护两个堆的数目符合要求是否重要, 即约定小顶堆的元素和大顶堆的元素数目相同(窗口为偶数), 或者小顶堆的元素多一个(窗口为奇数), 每次增加或者删除元素后, 都需要调整数量, 来维持平衡