剑指 Offer 41. 数据流中的中位数(295. 数据流的中位数)

题目:

 

思路:

【1】大小顶堆的方式(优先队列):

 

【2】有序集合 + 双指针(差不多就是采用双指针加有序集合模拟两个堆的感觉)

代码展示:

有序集合 + 双指针的方式:

复制代码
class MedianFinder {
    // 第一个Integer存放数值 第二个Integer存放该数值的个数
    TreeMap<Integer, Integer> nums;
    int n;// 记录数据个数 用于判断奇偶
    int[] left;
    int[] right;

    public MedianFinder() {
        nums = new TreeMap<Integer, Integer>();
        n = 0;
        // 由于这里的有序集合是通过TreeMap(基于红黑树)实现的
        // 所以TreeMap中的数据不存在空间上的相临关系
        // 那么我们就需要对指针进行一些改造(increase和decrease方法)
        // 指针[0]处存放指针指向的数据的值
        // [1]处存放 由指针记录的该数值的个数 后面简称计数索引
        left = new int[2];
        right = new int[2];
    }

    public void addNum(int num) {
        // 若不为重复数字 则num的映射为1
        // 若为重复数字 num的映射为num+1
        nums.put(num, nums.getOrDefault(num, 0) + 1);
        // n为数据个数
        if (n == 0) {
            left[0] = right[0] = num;
            //索引记录下该数值个数为1
            left[1] = right[1] = 1;
        } else if ((n & 1) != 0) {//奇数 123
            //0123
            if (num < left[0]) {
                decrease(left);
                //1234
            } else {
                increase(right);
            }
        } else {//偶数 1 23 4
            //12334
            if (left[0] < num && num < right[0]) {
                increase(left);
                decrease(right);
                //12345
            } else if (num >= right[0]) {
                increase(left);
                //01234
            } else {// num <= left[0]情况
                decrease(right);
                //如果num恰等于left指向的值,那么num将被插入到left右侧,使得left和right间距增大
                //所以我们还需要额外让left指向移动后的right
                //不能直接increase(left) 因为increase(left)只适用于如果num=left[0]的情况 若num<left[0] 只需左移right,left不变 此时若再左移left则会出错
                System.arraycopy(right, 0, left, 0, 2);
            }
        }
        n++;
    }

    public double findMedian() {
        return (left[0] + right[0]) / 2.0;
    }

    /**
     * 此函数的关键是:是否要将指针[0]处重新赋值
     * 相当于先假设:指针移动之后[0]处所存放的数与移动之前是相同的,那么计数索引++
     * 如果是不同的,那么计数索引应该大于Map中记录的数值对应的个数 则需要将指针[0]处重新赋值 并将计数索引重置为1
     * 如果是相同的,那么Map中记录的数值对应的个数应该与计数索引++后相等 则无需将指针[0]处重新赋值
     * @param iterator
     */
    private void increase(int[] iterator) {
        iterator[1]++;//计数索引++
        // get方法获取iterator[0]存放的值 也就是索引++之前对应的数值
        // 若计数索引>数值的映射值(即Map中记录的数值对应的个数)
        // 说明该数字为唯一数字
        if (iterator[1] > nums.get(iterator[0])) {
            // 则从一群比原数值大的值中选出最小值 将该最小值赋给iterator[0]
            // ceilingKey() 返回大于或等于给定键的最小键
            iterator[0] = nums.ceilingKey(iterator[0] + 1);
            // 将计数索引值重置为1 方便进行下一次指针操作
            iterator[1] = 1;
        }
        // 若计数索引<=数值的映射值(说明该数字为重复数字)
        // 则只进行前面的计数索引++ 不需要对指针[0]处重新赋值
    }

    /**
     * 此函数的关键是:是否要将指针[0]处重新赋值
     * 相当于先假设:指针移动之后[0]处所存放的数与移动之前是不同的,那么计数索引--
     * 如果是不同的,那么计数索引--后==0 则需要将指针[0]处重新赋值 并将计数索引设置为移动后的数值对应的个数
     * 如果是相同的,那么计数索引--后!=0 则无需将指针[0]处重新赋值
     * @param iterator
     */
    private void decrease(int[] iterator) {
        iterator[1]--;
        // 若计数索引--之后==0(说明该数字为唯一数字)
        if (iterator[1] == 0) {
            // 则从一群比原数值小的值中选出最大值 将该最大值赋给iterator[0]
            // floorKey() 返回小于或等于给定键的最大键
            iterator[0] = nums.floorKey(iterator[0] - 1);
            // 将该数值的个数赋值给计数索引
            iterator[1] = nums.get(iterator[0]);
        }
        // 若计数索引--之后!=0(说明该数字为重复数字)
        // 则只进行前面的计数索引-- 不需要对指针[0]处重新赋值
    }
}
复制代码

 

大小顶堆的方式:

复制代码
//时间117 ms击败27.30%
//内存70.3 MB击败6.27%
//时间复杂度:查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间;
//添加数字 O(log⁡N) : 堆的插入和弹出操作使用 O(log⁡N) 时间。
//空间复杂度 O(N) : 其中 N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。
class MedianFinder {
    Queue<Integer> A, B;

    public MedianFinder() {
        A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
        B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
    }

    public void addNum(int num) {
        if(A.size() != B.size()) {
            A.add(num);
            B.add(A.poll());
        } else {
            B.add(num);
            A.add(B.poll());
        }
    }

    public double findMedian() {
        return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
    }
}

//速度更快的写法,思路是一样的,写法也差不多,当时下面貌似更快
//时间98 ms击败81.53%
//内存70.2 MB击败9.96%
class MedianFinder {
    PriorityQueue<Integer> left = new PriorityQueue<>((a, b) -> b - a);
    PriorityQueue<Integer> right = new PriorityQueue<>();

    public MedianFinder() {}
    
    public void addNum(int num) {
        if (left.size() == right.size()) {
            if (left.isEmpty() || num <= right.peek()) {
                left.add(num);
            } else {
                right.add(num);
                left.add(right.poll());
            }
        } else {
            if (num >= left.peek()) {
                right.add(num);
            } else {
                left.add(num);
                right.add(left.poll());
            }
        }
    }
    
    public double findMedian() {
        if (left.size() != right.size()) {
            return left.peek();
        } else {
            return (left.peek() + right.peek()) / 2.0;
        }
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */
复制代码

 

posted @   忧愁的chafry  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示