数据流问题是一个比较常见的问题了。特点是数据是流的形式不断的加入的。因此我们在处理的时候需要逐渐添加数据到数据结构中。
这类题目的特点一般是需要寻找中位数,众数,均值,topK这几类。
295. 数据流的中位数
既然希望我们寻找数据流中的中位数,那我们比较好的做法就是使用两个堆分别维护一半的最小值,和一半的最大值。我们可以认为认为的允许保存最小的K个元素的最大堆比保存最大的K个元素的最小堆能最多多一个元素。否则就需要进行调整。(堆是排序以后吐出位置0的元素)
import java.util.PriorityQueue;
public class MedianFinder {
private int count;
// maxheap,相当于一个int[]递增的数组,nums[0]是peek,也是最小值
private PriorityQueue<Integer> maxheap;
// minheap,相当于一个int[]递减的数组,nums[0]是peek,也是最大值
private PriorityQueue<Integer> minheap;
public MedianFinder() {
count = 0;
maxheap = new PriorityQueue<>();
minheap = new PriorityQueue<>((x, y) -> y - x);
}
public void addNum(int num) {
count += 1;
if(minheap.size()!=0 && num>minheap.peek()) maxheap.offer(num);
else minheap.offer(num);
modify(minheap, maxheap);
}
public double findMedian() {
if ((count & 1) == 0) {
return (double) (maxheap.peek() + minheap.peek()) / 2;
} else {
return (double) minheap.peek();
}
}
private void modify(PriorityQueue<Integer> minheap, PriorityQueue<Integer> maxheap){
int maxsize = maxheap.size();
int minsize = minheap.size();
if(minsize-1>maxsize) maxheap.offer(minheap.poll());
if(maxsize>minsize) minheap.offer(maxheap.poll());
}
}
480. 滑动窗口中位数
补充一个也是中位数的问题。题目比较简单,直接放上例子吧
对于中位数问题,比较典型的方法还是维护两个pq,然后直接得到中位数。这里还一个思路,因为这里涉及到删除的操作,因此我们其实也可以使用有序的数据结构进行操作。比如C++中的MultiSet,但是java中没有相应的数据结构。我们可以用TreeMap凑活。
class Solution {
public double[] medianSlidingWindow(int[] nums, int k) {
int n = nums.length;
double[] ans = new double[n-k+1];
// Java中是不支持multiSet这一数据结构的,因此我们为了可以添加重复元素,有两种思路
// 引入其他的标识位进行区分;使用treeMap保存出现的次数。
// 如果是数字一样的,index小的在前;否则数值小的在前
Set<int[]> set = new TreeSet<>((a, b)->a[0]==b[0] ? Integer.compare(a[1], b[1]) : Integer.compare(a[0], b[0]));
for(int i=0; i<k; i++) set.add(new int[]{nums[i], i});
for(int i=k, j=0; i<n; i++, j++){
ans[j] = findMid(set);
set.add(new int[]{nums[i], i});
set.remove(new int[]{nums[i-k], i-k});
}
ans[n-k] = findMid(set);
return ans;
}
double findMid(Set<int[]> set){
int mid = (set.size() - 1) / 2;
// 我们希望得到set中间的答案,因此需要使用到迭代器。
//var it = set.iterator();
Iterator<int[]> it = set.iterator();
while(mid-->0) it.next();
return set.size()%2 == 0 ? ((double)it.next()[0] + it.next()[0]) / 2 : it.next()[0];
}
}
以及这个题目最经典的做法还是使用两个堆完成
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);
}
}
}