如何在堆数据结构中删除?

时间:2012-01-02 20:38:55

标签: algorithm data-structures heap

我了解如何从最大堆中删除根节点,但是是否从中间删除节点以重复删除和替换根,直到删除所需节点为止?

  1. O(log n)是此程序的最佳复杂程度吗?

  2. 这是否会影响大O复杂性,因为必须删除其他节点才能删除特定节点?

5 个答案:

答案 0 :(得分:80)

实际上,您可以毫无困难地从堆中间删除项目。

想法是获取堆中的最后一项,并且从当前位置(即保留您删除的项目的位置)开始,如果新项目大于旧项目的父项,则将其筛选。如果它不大于父母,那么将其筛选下来。

这是最大堆的过程。当然,对于最小堆,你可以反转更多和更少的情况。

查找堆中的项是一个O(n)操作,但如果您已经知道它在堆中的位置,则将其删除为O(log n)。

几年前我为DevSource发布了一个基于堆的优先级队列。见A Priority Queue Implementation in C#。它有RemoveAt方法,完全符合我的描述。

完整来源位于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 :(得分:15)

从堆中删除任意元素的问题是您无法找到它。

在堆中,查找任意元素为O(n),因此删除元素[如果按值给出]也是O(n)

如果从数据结构中删除任意元素很重要,那么堆可能不是最佳选择,您应该考虑完整的排序数据结构,例如balanced BSTskip list

如果你的元素是通过引用给出的,那么可以通过简单地用最后一个叶子'替换'来在O(logn)中删除它[记住一个堆被实现为一个完整的二叉树,所以有一个最后一个叶子,你确切地知道它在哪里],删除这些元素,并重新堆积相关的子堆。

答案 2 :(得分:2)

如果你有一个最大堆,你可以通过为项目分配一个比任何其他值更大的值(例如你使用的任何语言中的int.MaxValueinf)来实现这一点。删除,然后重新堆积,它将是新的根。然后定期删除根节点。

这将导致另一个重新堆积,但我看不到一个明显的方法来避免两次这样做。这表明,如果您需要经常从中间拉出节点,那么堆可能不适合您的用例。

(对于最小堆,显然可以使用int.MinValue-inf或其他)

答案 3 :(得分:1)

你想要实现的不是典型的堆操作,在我看来,一旦你引入“删除中间元素”作为方法,一些其他二叉树(例如红黑或AVL树)是更好的选择。你有一些以某些语言实现的红黑树(例如map和用c ++设置)。

否则中间元素删除的方法与rejj的答案一样:为元素分配一个大值(对于最大堆)或小值(对于最小堆),将其筛选直到它是root然后删除它。

这种方法仍然保留了中间元素删除的O(log(n))复杂度,但是你提出的那个。它将具有复杂度O(n * log(n)),因此不是很好。 希望有所帮助。

答案 4 :(得分:1)

从已知的堆数组位置删除元素具有O(log n)复杂度(这对于堆来说是最佳的)。因此,此操作与提取(即删除)根元素具有相同的复杂性。

从堆A(带有0<=i<n元素)中删除第i个元素(其中n)的基本步骤是:

  1. 将元素A[i]与元素A[n-1]交换
  2. 设置n=n-1
  3. 可能修复堆,以便所有元素都满足堆属性

与提取根元素的工作原理非常相似。

请记住,堆属性在max-heap中定义为:

A[parent(i)] >= A[i], for 0 < i < n

在最小堆中是:

A[parent(i)] <= A[i], for 0 < i < n

在下面,我们假设使用max-heap来简化描述。但是,一切都类似地使用最小堆。

交换之后,我们必须区分3种情况:

    A[i]中的
  1. 新密钥等于旧密钥-完成任何更改
  2. A[i]中的
  3. 新密钥大于旧密钥。 l的子树ri不变。如果以前的A[parent(i)] >= A[j]是真实的,那么现在A[parent(i)]+c >= A[j]也必须是真实的(对于j in (l, r)c>=0)。但是元素i的祖先可能需要修复。此修复程序与增加A[i]时基本相同。
  4. A[i]中的
  5. 新密钥小于旧密钥。元素i的祖先没有任何变化,因为如果先前的值已经满足了heap属性,则较小的值也将执行此操作。但是子树现在可能需要修复,即,与提取最大元素(即根)时相同。

示例实现:

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)中按键值进行删除。

那么我们如何跟踪插入的元素的数组位置?毕竟,进一步的插入/移除操作可能会使其移动。

我们还可以通过在堆上每个元素的键旁边存储指向元素记录的指针来保持跟踪。然后,元素记录包含具有当前位置的字段-然后由修改后的堆插入和堆交换功能维护该字段。因此,如果在插入后保留指向元素记录的指针,则可以在恒定时间内获得元素在堆中的当前位置。