数据流中的第 K 大元素

标签: 设计 二叉搜索树 二叉树 数据流 堆(优先队列)

难度: Easy

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val)val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

 

示例:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8

 

提示:
  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

Submission

运行时间: 58 ms

内存: 20.0 MB

class KthLargest:

    def __init__(self, k: int, nums: List[int]):
        self.number = k
        self.res = nums
        heapq.heapify(self.res)
        while len(self.res) > k:
            heapq.heappop(self.res)

    def add(self, val: int) -> int:
        if len(self.res) < self.number:
            heapq.heappush(self.res, val)
        elif val >= self.res[0]:
            heapq.heapreplace(self.res, val)
        #self.res.sort()
        return self.res[0]


# Your KthLargest object will be instantiated and called as such:
# obj = KthLargest(k, nums)
# param_1 = obj.add(val)

Explain

这个题解的思路是使用一个最小堆来维护数据流中的前 k 个最大的元素。初始时,将 nums 中的元素加入堆中,如果堆的大小超过 k,则弹出堆顶元素(最小的元素),以保证堆中始终保留 k 个最大的元素。当新的元素 val 被添加到数据流中时,如果堆的大小小于 k,则直接将 val 加入堆中;如果 val 大于等于堆顶元素,则将堆顶元素替换为 val,然后调整堆。这样,堆顶元素始终是第 k 大的元素。

时间复杂度: O(log k)

空间复杂度: O(k)

class KthLargest:

    def __init__(self, k: int, nums: List[int]):
        self.number = k  # 第k大的元素
        self.res = nums  # 初始化数组
        heapq.heapify(self.res)  # 将数组转换为最小堆
        while len(self.res) > k:  # 保持堆的大小为k
            heapq.heappop(self.res)  # 弹出堆顶元素

    def add(self, val: int) -> int:
        if len(self.res) < self.number:  # 如果堆的大小小于k
            heapq.heappush(self.res, val)  # 直接将val加入堆中
        elif val >= self.res[0]:  # 如果val大于等于堆顶元素
            heapq.heapreplace(self.res, val)  # 替换堆顶元素为val
        return self.res[0]  # 返回堆顶元素

# Your KthLargest object will be instantiated and called as such:
# obj = KthLargest(k, nums)
# param_1 = obj.add(val)

Explore

当初始数组 nums 中的元素个数少于 k 时,最小堆会包含 nums 中所有元素。因为堆的大小没有被限定为 k,所以堆顶元素(最小元素)就是这个堆中的第 k 大的元素。在这种情况下,每次调用 add 方法时,如果添加的新元素使得堆的大小仍然不超过 k,则直接加入堆中;如果超过 k,则维持堆的大小为 k,确保堆顶元素是目前已知的第 k 大元素。这样通过逐步添加元素,直到堆的大小达到 k,堆顶的元素始终正确地表示第 k 大的元素。

是的,最小堆的实现仍然可以正确处理包含重复元素的情况。在最小堆中,只要维持堆的大小为 k,堆顶元素就是第 k 大的元素,不论其中是否包含重复值。重复元素在堆中仅影响排序和堆顶元素的选择,但不影响实现第 k 大元素的功能。

选择使用最小堆而不是最大堆的主要原因是最小堆可以更高效地维护数据流中的前 k 个最大元素。在最小堆中,堆顶是所有元素中最小的,这样可以保证堆中的其他元素都是比堆顶大的元素。当新的元素加入时,如果它不足以进入前 k 的范围(即比堆顶元素小),可以直接忽略;如果它足够大,可以替换堆顶元素并重新调整堆。这种方式比使用最大堆,然后维护一个大小为 k 的结构要有效,因为最大堆需要更多的操作来确定哪些元素需要被移除出前 k 大的列表。

heapreplace 方法是一个优化的堆操作,用于替换堆顶元素。它首先将堆顶元素移除,然后将新元素添加到堆顶,最后重新调整堆以保持堆的属性。这个方法比单独调用 heappop 移除堆顶元素和 heappush 添加新元素更加高效,因为它仅需要一次重新调整堆的操作,而不是两次。这种方法在连续替换堆顶元素的操作中,尤其在维护固定大小的堆时,能更快地达到平衡状态。