如何在堆数据结构中删除?
Posted
技术标签:
【中文标题】如何在堆数据结构中删除?【英文标题】:How to delete in a heap data structure? 【发布时间】:2012-02-01 01:40:13 【问题描述】:我了解如何从最大堆中删除根节点,但是从中间删除节点以重复删除和替换根节点直到删除所需节点的过程?
O(log n) 是这个过程的最佳复杂度吗?
这是否会影响大 O 复杂度,因为必须删除其他节点才能删除特定节点?
【问题讨论】:
为什么要删除最大堆中间的节点? @BrokenGlass:这种东西的一个非常实际的用途是调度作业的优先级队列的堆表示,并且有人取消了其中一个作业。 @BrokenGlass:我最近实现了 LPA* 寻路算法,这是一种基于 A* 的重新规划算法。它需要能够从优先级队列的中间移除。 一般来说,您希望发布一个新问题,而不是在一个四年前的帖子中添加新问题。但要回答您的问题:1)在标准二叉堆中,O(log n) 是最佳复杂度。 2)从堆中间删除永远不会比删除根节点更昂贵,并且该操作已经被证明是O(log n)。 O(log n) 是删除堆中任何位置的节点的最坏情况复杂度。但是请注意,我在原始答案中指出的内容仍然正确:find 要删除的节点需要 O(n)。 mathcs.emory.edu/~cheung/Courses/171/Syllabus/9-BinTree/… 【参考方案1】:从已知的堆数组位置删除元素具有O(log n)
复杂性(这对于堆来说是最佳的)。因此,此操作与提取(即删除)根元素具有相同的复杂性。
从堆A(带有n
元素)中删除第i个元素(其中0<=i<n
)的基本步骤是:
-
将元素
A[i]
与元素A[n-1]
交换
设置n=n-1
可能修复堆以使所有元素都满足堆属性
这与提取根元素的工作方式非常相似。
记住堆属性在最大堆中定义为:
A[parent(i)] >= A[i], for 0 < i < n
而在最小堆中它是:
A[parent(i)] <= A[i], for 0 < i < n
在下面我们假设一个最大堆来简化描述。但一切都与最小堆类似。
交换后我们要区分3种情况:
A[i]
中的新密钥等于旧密钥 - 没有任何变化,完成
A[i]
中的新密钥大于旧密钥。 i
的子树 l
和 r
没有任何变化。如果之前A[parent(i)] >= A[j]
为真,那么现在A[parent(i)]+c >= A[j]
也必须为真(对于j in (l, r)
和c>=0
)。但是元素i
的祖先可能需要修复。此修复过程与增加A[i]
时基本相同。
A[i]
中的新密钥小于旧密钥。元素 i 的祖先没有任何变化,因为如果先前的值已经满足堆属性,那么较小的值也会满足。但是现在可能需要修复子树,即与提取最大元素(即根)时的方式相同。
一个示例实现:
void heap_remove(A, i, &n)
assert(i < n);
assert(is_heap(A, i));
--n;
if (i == n)
return;
bool is_gt = A[n] > A[i];
A[i] = A[n];
if (is_gt)
heapify_up(A, i);
else
heapify(A, i, n);
其中heapifiy_up()
基本上是教科书increase()
函数-取模写键:
void heapify_up(A, i)
while (i > 0)
j = parent(i);
if (A[i] > A[j])
swap(A, i, j);
i = j;
else
break;
而heapify()
是教科书的筛选功能:
void heapify(A, i, n)
for (;;)
l = left(i);
r = right(i);
maxi = i;
if (l < n && A[l] > A[i])
maxi = l;
if (r < n && A[r] > A[i])
maxi = r;
if (maxi == i)
break;
swap(A, i, maxi);
i = maxi;
由于堆是(几乎)完全二叉树,它的高度在O(log n)
。两个 heapify 函数都必须访问所有树级别,在最坏的情况下,因此按索引删除是在O(log n)
。
请注意,在堆中查找具有特定键的元素是在O(n)
中。因此,由于查找的复杂性,一般来说,按键值删除是在O(n)
中。
那么我们如何跟踪我们插入的元素的数组位置呢?毕竟,进一步的插入/删除可能会移动它。
我们还可以通过在堆上为每个元素存储指向键旁边的元素记录的指针来进行跟踪。然后元素记录包含一个具有当前位置的字段 - 因此必须通过修改后的 heap-insert 和 heap-swap 函数来维护。如果我们在插入后保留指向元素记录的指针,我们可以在常数时间内得到元素在堆中的当前位置。这样,我们也可以在O(log n)
中实现元素移除。
【讨论】:
【参考方案2】:实际上,您可以毫不费力地从堆中间删除一个项目。
这个想法是取出堆中的最后一项,并从当前位置(即保存您删除的项的位置)开始,如果新项大于旧项的父项,则将其向上筛选。如果它不大于父级,则将其筛选下来。
这就是最大堆的过程。当然,对于最小堆,您可以颠倒更大和更少的情况。
在堆中查找项是 O(n) 操作,但如果您已经知道它在堆中的位置,则删除它是 O(log n)。
几年前,我为 DevSource 发布了一个基于堆的优先级队列。完整来源在http://www.mischel.com/pubs/priqueue.zip
更新
一些人询问是否可以在移动堆中的最后一个节点以替换已删除的节点后向上移动。考虑这个堆:
1
6 2
7 8 3
如果删除值为 7 的节点,则值为 3 替换它:
1
6 2
3 8
您现在必须将其向上移动以创建一个有效的堆:
1
3 2
6 8
这里的关键是,如果您要替换的项与堆中的最后一项位于不同的子树中,则替换节点可能会小于被替换节点的父节点。
【讨论】:
我不太确定我是否理解你。您是说我们删除任意元素并将其位置替换为堆树的最后一个元素,然后向上或向下筛选?我怀疑这就是你所说的,因为 1)它永远无法筛选 2)这棵树可能不再完整......你能详细说明一下吗?谢谢。 @RoronoaZoro:这正是我要说的。它确实有效。查看我的示例代码,或查看任何允许删除的堆实现。 @RoronoaZoro:它实际上可以筛选,因为它可能不是同一棵树的一部分。想象一下根——一开始,树被分成两个独立的部分。您可能正在从左子树中删除一个节点,但是如果您从堆的最后一级取出最后一项,则可能是右子树中的一项。子树之间没有排序,仅相对于它们的父级。 @SazzadHissainKhan 如果你想按值删除,你必须维护一个字典或哈希映射,按值键,包含堆中项目的索引。而且,是的,交换后价值可能会上升。查看我的更新。 @ptr_user7813604 是的,DevSource 文章不见了。不过,代码链接仍然很好。【参考方案3】:您想要实现的不是典型的堆操作,在我看来,一旦您将“删除中间元素”作为一种方法引入其他二叉树(例如红黑树或 AVL 树)是更好的选择。你有一个用某些语言实现的红黑树(例如 map 和 c++ 中的设置)。
否则删除中间元素的方法如rejj的回答中提出的那样:为元素分配一个大值(对于最大堆)或小值(对于最小堆),将其筛选直到它是根然后删除它.
这种方法仍然保持中间元素删除的 O(log(n)) 复杂度,但您建议的方法没有。它将具有复杂性 O(n*log(n)),因此不是很好。 希望对您有所帮助。
【讨论】:
【参考方案4】:从堆中删除任意元素的问题是你找不到它。
在堆中,寻找任意元素是O(n)
,因此删除一个元素[如果按值给出]也是O(n)
。
如果从数据结构中删除任意元素对您很重要,堆可能不是最佳选择,您应该考虑全排序数据结构,例如 balanced BST 或 skip list。
如果您的元素是通过引用给出的,则可以在O(logn)
中删除它,只需用最后一个叶子“替换”它[记住堆是作为完整的二叉树实现的,所以有最后一个叶子,并且您确切知道它在哪里],删除这些元素,并重新堆化相关的子堆。
【讨论】:
请注意,如果您愿意推出自己的堆实现,可以找到它。您只需要它来保存从对象到索引的映射,或者以侵入方式将索引存储在对象上,并在每次交换元素时更新它。更新并不昂贵,因为它只发生在交换上,你只需交换索引;您无需进行任何扫描。 @Joseph Garvin 我一直在寻找这样的答案。有一个面试问题“在流中找到 k 个最常见的元素”。您的自定义堆实现方法更适合。【参考方案5】:如果你有一个最大堆,你可以通过为要删除的项目分配一个大于任何其他值的值来实现这一点(例如,int.MaxValue
或inf
,无论你使用哪种语言)可能要删除的项目,然后重新-heapify 它将成为新的根。然后定期删除根节点。
这将导致另一个 re-heapify,但我看不到避免重复两次的明显方法。这表明如果您需要经常从堆的中间拉取节点,那么堆可能不适合您的用例。
(对于最小堆,您显然可以使用 int.MinValue
或 -inf
或其他)
【讨论】:
请参阅my answer,了解避免重复工作的直接方法。如果您考虑到与最大堆中的增加函数或构建堆函数中的中间步骤的相似之处 - 在将最大提取推广到任何节点提取时,它会变得很明显。以上是关于如何在堆数据结构中删除?的主要内容,如果未能解决你的问题,请参考以下文章