在 O(K*log(K)) 中打印给定堆中最大的 K 个元素?

Posted

技术标签:

【中文标题】在 O(K*log(K)) 中打印给定堆中最大的 K 个元素?【英文标题】:Print the biggest K elements in a given heap in O(K*log(K))? 【发布时间】:2012-06-27 21:39:03 【问题描述】:

鉴于以下问题,我不完全确定我目前的解决方案:

问题:

给定具有n 元素的最大堆,它存储在数组A 中,是否可以打印O(K*log(K)) 中所有最大的K 元素?

我的回答

是的,是的,因为搜索元素需要O(log(K)),因此这样做

对于K 元素将花费O(K * log(K)) 运行时间。

【问题讨论】:

O(klogk) time algorithm to find kth smallest element from a binary heap 的可能副本。也许不是骗子,因为链接的问题要求第 k 个元素而不是第 k 个最大元素的列表,但想法是一样的。 【参考方案1】:

我发现其他答案令人困惑,所以我决定用一个实际的示例堆来解释它。 假设原始堆大小为 N 并且您想找到第 k 个最大的元素, 该解决方案需要 O(klogk) 时间和 O(k) 空间。

    10 
   /  \
  5    3 
 / \   /\
4   1 2  0
Original Heap, N = 7 

想要找到第 5 个最大的元素。 k = 5 注意:在新堆中,您需要存储指向原始堆的指针。 这意味着,您不会删除或更改原始堆。原始堆是只读的。因此,您永远不必执行任何需要 O(logN) 时间的操作。

设 x' 是指向原始堆中值 x 的指针。

第一次迭代:获取根节点的指针到新堆中

第 1 步:添加指向节点 10 的指针

 10'
 New Heap, size = 1, root = 10', root->left = 5, root right->3

打印第一个最大的元素 = 10

第二次迭代:参考原始堆并将其两个子代插入新堆。 (存储指向它们的指针而不是值本身)。您想要存储指针的原因是,您可以稍后在 O(1) 中从原始堆访问它们以搜索它们的子代,而不是 O(N) 来搜索该值在原始堆中的位置。

步骤 2a:从原始堆中寻找新堆根节点的左子节点。 为左孩子(在本例中为 5')添加一个指向新堆的指针。

  10' 
 /
5'
New Heap, size = 2, root = 10', root->left = 5, root right->3

步骤 2b:从原始堆中寻找新堆根节点的右子节点。 将左孩子的指针(在本例中为 3')添加到新堆。

  10' 
 / \
5'  3'
New Heap, size = 3, root = 10', root->left = 5, root right->3

步骤 2c:从新堆中删除根节点。 (用最右边的叶子交换最大节点,删除根节点并向下冒泡当前根以保持堆属性)

  10'   swap    3'  remove & bubble   5'    
 / \     =>    / \       =>          /
5'  3'        5'  10'               3'
New Heap, size = 2, root = 5', root->left = 4, root right->1

打印第二大元素 = 5

步骤 3a:从原始堆中寻找新堆根节点的左子节点。 为左孩子(在本例中为 4')添加一个指向新堆的指针。

  5'
 / \
3'  4'
New Heap, size = 3, root = 5', root->left = 4, root right->1

步骤 3b:从原始堆中寻找新堆根节点的右子节点。 将左孩子的指针(在本例中为 1')添加到新堆。

    5'
   / \
  3'  4'
 /
1'
New Heap, size = 4, root = 5', root->left = 4, root right->1

步骤 3c:从新堆中删除根节点。 (将新堆的最大节点(5')与其最右边的从新堆的原始堆(1')交换,移除根节点并向下冒泡当前根以保持堆属性)

    5'        Swap     1' remove & bubble     4'
   / \         =>     / \       =>           / \
  3'  4'            3'   4'                 3'  1'
 /                 / 
1'                5'
New Heap, size = 3, root = 4', root->left = NULL, root right->NULL

打印第三大元素 = 4

步骤 4a 和步骤 4b 什么都不做,因为在这种情况下,根节点没有来自原始堆的任何子节点。

步骤 4c:从新堆中删除根节点。 (将最大节点与最右边的叶子交换,删除根节点并向下冒泡当前根以保持新堆中的堆属性)

    4'        Swap     1' remove & bubble     3'
   / \         =>     / \       =>           / 
  3'  1'            3'   4'                 1'  
New Heap, size = 2, root = 3', root->left = 2, root right->0

打印第 4 个最大的元素 = 3

步骤 5a:从原始堆中寻找新堆根节点的左子节点。 为左孩子(在本例中为 2')添加一个指向新堆的指针。

     3'
    / \
   1'  2'
New Heap, size = 3, root = 3', root->left = 2, root right->0

步骤 5b:从原始堆中寻找新堆根节点的右子节点。 将左孩子的指针(在本例中为 0')添加到新堆。

     3'
    / \
   1'  2'
  /
 0'
New Heap, size = 4, root = 3', root->left = 2, root right->0

步骤 5c:从新堆中删除根节点。 (将最大节点(3')与新堆中原始堆(即0')的最右边离开,移除根节点并向下冒泡当前根以保持新堆中的堆属性)

     3'    Swap        0'  Remove & Bubble      2'
    / \     =>        / \         =>           / \
   1'  2'            1'  2'                   1'  0'
  /                 /
 0'                3'
New Heap, size = 3, root = 2', root->left = NULL, root->right = NULL

打印第 5 个最大的元素 = 2

最后,由于我们经历了 k 次迭代,k = 5。我们现在可以从新堆中提取根元素的值。在这种情况下,值为 2。 因此,我们从原始堆中找到了第 k 个最大值。

时间复杂度,T(N,k) = O(klogk) 空间复杂度,S(N,k) = O(k)

希望这会有所帮助!

宋志龙,

多伦多大学。

【讨论】:

在步骤 3c 和 5c 中,您说将最大节点与最右边的叶子交换,但您将它与最左边的叶子交换? @user881300 原始堆中最右边的叶子。谢谢,会在我的解释中澄清。【参考方案2】:

在大小为 N 的堆中搜索元素不是 O(K)。首先,查找 one 元素的时间复杂度取决于您尝试提取的元素数量(这是 K 表示的),这是没有意义的。此外,没有在堆中搜索这样的事情 - 除非您将标准的查看每个元素的搜索计算在 O(N) 中。

但是,按照设计,在堆中找到最大元素是 O(1)(我显然假设它是一个最大堆,因此最大元素位于堆的顶部),然后从大小为 N 的堆是 O(log(N))(将其替换为叶元素,并让该叶从堆中向下渗透)。

因此,从堆中提取 K 个元素,并返回未提取元素的堆,将花费 O(K·log(N)) 时间。

如果你非破坏性地从堆中提取 K 个元素会发生什么?您可以通过保留堆堆来做到这一点(其中堆的值是其最大元素的值)。最初,这个堆只包含一个元素(原始堆)。要提取下一个最大元素,请提取顶部堆,提取其顶部元素(即最大值),然后将两个子堆重新插入堆中。

这会使堆堆在每次删除时增加一个(删除一个,添加两个),这意味着 它永远不会容纳超过 K 个元素,因此 remove-one-add -two 将采用 O(log(K))。重复这个,你会得到一个实际的 O(K·log(K)) 算法,它确实返回了前 K 个元素,但无法返回未提取元素的堆。

【讨论】:

请注意,我已经更新了问题 - 堆确实是最大堆,但它是在数组中给出的。 它是一个数组这一事实并没有改变任何东西。 数组是堆的存储策略,但不管如何存储,堆仍然是一棵树。当您删除堆的顶部元素时,您会留下两个子堆,直到那时该元素的两个子堆。在数组的情况下,这两个子堆恰好与原始堆存储在同一个数组中,但这只是一个意外——探索它们的规则保持不变。 谁能解释一下“返回未提取元素的堆”和“从堆中破坏性地提取 K 个元素”之间的区别?? @Prashant 应该是非破坏性【参考方案3】:
It is a simple and elegant algorithm to get first k elements of a max heap in k log(k) time.

steps:-

1.construct another max heap name it auxiliary heap
2.add root element of main heap to auxiliary heap
3.pop out the element from auxiliary heap and add it's 2 children to the heap
4.do step 2 and 3 till k elements have been popped out from auxiliary heap. Add the popped element's children to the auxiliary heap.

【讨论】:

与@Victor Nicollet's answer中描述的算法相同【参考方案4】:

这在最大堆中是可能的,因为您只是从树中打印元素,而不是提取它们。

首先确定位于根节点的最大元素。形成一个指向节点的指针并将其添加到一个空的“最大值”列表中。然后,对于每个k 值,循环执行以下步骤。

从列表中弹出最大元素,耗时 O(1)。 打印其值,取 O(1)。 将此最大元素的每个子元素插入到列表中。插入时保持排序,花费 O(log(size of list)) 时间。这个列表的最大大小,因为我们正在执行这个循环 k 次,是 branch-size*k。因此,这一步需要 O(log(k)) 时间。

总之,运行时间为 O(klog(k)),如所愿。

【讨论】:

第三步能在 O(log(k)) 时间内完成吗?如果数据结构是链表,那么二分查找就不可能(至少在 log(k) 时间内不可能)?如果数据结构是一个数组,那么插入不会是 O(1)。如果我遗漏了什么,请纠正我。 我觉得先把元素复制到一个数组中再对数组进行排序比较好。 @ShubhamGoyal 数据结构本身可以是堆,支持 O(log k) 插入和最大删除。同意认为答案中关于操作复杂性的个人主张是不可能实现的【参考方案5】:

确实太容易了,提取最大元素是O(log(N)),其中N是堆的大小。和N≠K

我会补充一点,搜索随机元素是O(N) 而不是O(Log(N)),但在这种情况下我们要提取最大值。

【讨论】:

@ron 我的回答仍然有效。

以上是关于在 O(K*log(K)) 中打印给定堆中最大的 K 个元素?的主要内容,如果未能解决你的问题,请参考以下文章

ARTS Week 26

TOP K问题

O(klogk) 时间算法从二进制堆中找到第 k 个最小元素

寻找最大的K个数

合并k个有序数组

2021-12-02:给定一个字符串str,和一个正数k。 返回长度为k的所有子序列中,字典序最大的子序列。 来自腾讯。