leetcode
leetcode 251 ~ 300
数据流的中位数

数据流的中位数

难度:

标签:

题目描述

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3 。
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。

  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

示例 1:

输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]

解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);    // arr = [1]
medianFinder.addNum(2);    // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3);    // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0

提示:

  • -105 <= num <= 105
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 104 次调用 addNum 和 findMedian

代码结果

运行时间: 272 ms, 内存: 37.0 MB


/*
 * 思路:我们使用Stream API来简化插入排序的操作。
 * 1. 使用一个ArrayList来存储元素。
 * 2. 每次插入新元素时,将其插入到正确的位置以保持有序。
 * 3. 使用Stream来实现排序和中位数计算。
 */
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
 
class MedianFinder {
    private List<Integer> nums;
 
    public MedianFinder() {
        nums = new ArrayList<>();
    }
 
    public void addNum(int num) {
        nums.add(num);
        nums = nums.stream().sorted().collect(Collectors.toList());
    }
 
    public double findMedian() {
        int size = nums.size();
        if (size % 2 == 1) {
            return nums.get(size / 2);
        } else {
            return (nums.get(size / 2 - 1) + nums.get(size / 2)) / 2.0;
        }
    }
}
 

解释

方法:

这个题解使用了两个堆来实现数据流的中位数查找。左边的堆是一个大顶堆,存储较小的一半数据;右边的堆是一个小顶堆,存储较大的一半数据。通过维护两个堆的大小平衡,并且保证左边堆的最大值小于等于右边堆的最小值,就可以在 O(1) 的时间复杂度内找到中位数。当数据总数为奇数时,中位数就是左边堆的堆顶元素;当数据总数为偶数时,中位数就是左右两个堆堆顶元素的平均值。

时间复杂度:

O(log n)

空间复杂度:

O(n)

代码细节讲解

🦆
为什么在`addNum`方法中,选择将数字添加到大顶堆或小顶堆时,需要比较数字与小顶堆的最小值和大顶堆的最大值?
在`addNum`方法中,比较数字与小顶堆的最小值和大顶堆的最大值是为了维护两个堆的性质,即大顶堆存储较小的一半数据,小顶堆存储较大的一半数据。通过这种比较,我们可以正确地将新数放入适当的堆中。此外,这种比较确保大顶堆中的任何元素都不会大于小顶堆中的任何元素,这是计算中位数的关键前提。
🦆
在`addNum`方法中,当新加入的数字需要调整两个堆之间的元素时,具体的步骤是怎样的,能否详细说明这种调整的必要性和过程?
当新元素加入时,可能破坏两个堆的大小平衡或堆的性质。如果新元素属于当前较小堆(大顶堆)的范围但两堆大小已不平衡,需要将大顶堆的最大元素移至小顶堆,并将新元素放入大顶堆,以保持平衡。反之亦然。这种调整确保了两个堆的大小最多相差一个元素,同时保持大顶堆的所有值仍然小于或等于小顶堆的所有值,这对于快速找到正确的中位数是必要的。
🦆
如何保证在整个过程中大顶堆的所有值始终小于或等于小顶堆的所有值?
通过在`addNum`方法中适当地比较和调整元素,确保只有当新元素大于大顶堆的最大值时,它才可能被添加到小顶堆;如果它小于小顶堆的最小值,则被添加到大顶堆。若需要调整以保持堆的性质和大小平衡,将大顶堆的最大值移到小顶堆,或将小顶堆的最小值移到大顶堆,从而始终保持大顶堆的所有值小于或等于小顶堆的所有值。
🦆
在实现`findMedian`方法时,为什么在两个堆大小相等的情况下直接取两个堆顶元素的平均值就是中位数?
当两个堆的大小相等时,它们各包含了一半的元素。在这种情况下,大顶堆的堆顶元素是较小一半中的最大值,小顶堆的堆顶元素是较大一半中的最小值。因此,这两个堆顶元素的平均值代表了整个数据流的中间值,即中位数。这种方法可以快速有效地计算出中位数,无需对数据进行完全排序。

相关问题

滑动窗口中位数

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

例如:

  • [2,3,4],中位数是 3
  • [2,3],中位数是 (2 + 3) / 2 = 2.5

给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数,每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。

 

示例:

给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。

窗口位置                      中位数
---------------               -----
[1  3  -1] -3  5  3  6  7       1
 1 [3  -1  -3] 5  3  6  7      -1
 1  3 [-1  -3  5] 3  6  7      -1
 1  3  -1 [-3  5  3] 6  7       3
 1  3  -1  -3 [5  3  6] 7       5
 1  3  -1  -3  5 [3  6  7]      6

 因此,返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]

 

提示:

  • 你可以假设 k 始终有效,即:k 始终小于等于输入的非空数组的元素个数。
  • 与真实值误差在 10 ^ -5 以内的答案将被视作正确答案。