【专题总结】数据流问题

数据流问题是一个比较常见的问题了。特点是数据是流的形式不断的加入的。因此我们在处理的时候需要逐渐添加数据到数据结构中。
这类题目的特点一般是需要寻找中位数,众数,均值,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);
        }
    }
}

1825 求出 MK 平均值

参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值