在 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(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 个元素?的主要内容,如果未能解决你的问题,请参考以下文章
O(klogk) 时间算法从二进制堆中找到第 k 个最小元素
2021-12-02:给定一个字符串str,和一个正数k。 返回长度为k的所有子序列中,字典序最大的子序列。 来自腾讯。