Skip to content

题目链接 · 灵神原题解(署名来源)

比如现在有 6 个数:1,5,6,2,3,4,要计算中位数,可以把这 6 个数从小到大排序,得到 1,2,3,4,5,6,中间 34 的平均值 3.5 就是中位数。

回顾一下百科中关于中位数的定义:

中位数……可将数值集合划分为相等的两部分。

中位数把这 6 个数均分成了左右两部分,小的那一组记作 left=[1,2,3],大的那一组记作 right=[4,5,6]。我们要计算的中位数,就来自 left 中的最大值,以及 right 中的最小值

随着 addNum 不断地添加数字,我们需要:

  • 保证 left 的大小和 right 的大小尽量相等。规定:在有奇数个数时,leftright1 个数。
  • 保证 left 的所有元素都小于等于 right 的所有元素。

只要时时刻刻满足以上两个要求(满足中位数的定义),我们就可以用 left 中的最大值以及 right 中的最小值计算中位数。

分类讨论:

  • 如果当前 left 的大小和 right 的大小相等:
    • 如果添加的数字 num 比较大,比如添加 7,那么把 7 加到 right 中。现在 leftright1 个数,不符合前文的规定,所以必须把 right 的最小值从 right 中去掉,添加到 left 中。如此操作后,可以保证 left 的所有元素都小于等于 right 的所有元素。
    • 如果添加的数字 num 比较小,比如添加 0,那么把 0 加到 left 中。
    • 这两种情况可以合并:无论 num 是大是小,都可以先把 num 加到 right 中,然后把 right 的最小值从 right 中去掉,并添加到 left 中。
  • 如果当前 leftright1 个数:
    • 如果添加的数字 num 比较大,比如添加 7,那么把 7 加到 right 中。
    • 如果添加的数字 num 比较小,比如添加 0,那么把 0 加到 left 中。现在 leftright2 个数,不符合前文的规定,所以必须把 left 的最大值从 left 中去掉,添加到 right 中。如此操作后,可以保证 left 的所有元素都小于等于 right 的所有元素。
    • 这两种情况可以合并:无论 num 是大是小,都可以先把 num 加到 left 中,然后把 left 的最大值从 left 中去掉,并添加到 right 中。

最后,我们需要什么样的数据结构?这个数据结构要能高效地执行如下操作:

  • 添加元素。
  • 找到最大(小)值。
  • 删除最大(小)值。

这个数据结构是

left最大堆right最小堆

  • 如果当前有奇数个元素,中位数是 left 的堆顶。
  • 如果当前有偶数个元素,中位数是 left 的堆顶和 right 的堆顶的平均值。
python
class MedianFinder:
    def __init__(self):
        self.left = []  # 最大堆
        self.right = []  # 最小堆

    def addNum(self, num: int) -> None:
        if len(self.left) == len(self.right):
            heappush_max(self.left, heappushpop(self.right, num))
        else:
            heappush(self.right, heappushpop_max(self.left, num))

    def findMedian(self) -> float:
        if len(self.left) > len(self.right):
            return self.left[0]
        return (self.left[0] + self.right[0]) / 2
cpp
// C++ 版待补充
cpp
class MedianFinder {
    priority_queue<int> left; // 最大堆
    priority_queue<int, vector<int>, greater<>> right; // 最小堆

public:
    void addNum(int num) {
        if (left.size() == right.size()) {
            right.push(num);
            left.push(right.top());
            right.pop();
        } else {
            left.push(num);
            right.push(left.top());
            left.pop();
        }
    }

    double findMedian() {
        if (left.size() > right.size()) {
            return left.top();
        }
        return (left.top() + right.top()) / 2.0;
    }
};

复杂度分析

  • 时间复杂度:初始化和 findMedian 都是 O(1)addNumO(logq),其中 qaddNum 的调用次数。每次操作堆需要 O(logq) 的时间。
  • 空间复杂度:O(q)

思考题

如果还有删除操作(删除数据流中的任意元素),要求仍然用堆实现,要怎么做?

480. 滑动窗口中位数

专题训练

见下面数据结构题单的「§5.7 对顶堆」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

本文整理自灵茶山艾府(endlesscheng)的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传