如何在进行最多 3N 次比较的同时实现 std::make_heap?

发布于 2024-11-14 22:01:23 字数 2310 浏览 3 评论 0原文

我查看了 C++0x 标准,发现 make_heap 的比较次数不应超过 3*N 次。

即heapify一个无序集合可以在O(N)内完成,

   /*  @brief  Construct a heap over a range using comparison functor.

这是为什么呢?

源代码没有给我任何线索(g++ 4.4.3)

while (true) + __parent == 0 不是线索,而是对 O(N) 行为的猜测

template<typename _RandomAccessIterator, typename _Compare>
void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
          _Compare __comp)
{

  const _DistanceType __len = __last - __first;
  _DistanceType __parent = (__len - 2) / 2;
  while (true)
    {
      _ValueType __value = _GLIBCXX_MOVE(*(__first + __parent));
      std::__adjust_heap(__first, __parent, __len, _GLIBCXX_MOVE(__value),
                 __comp);
      if (__parent == 0)
        return;
      __parent--;
    }
}

__adjust_heap 看起来像一个 log N 方法:

while ( __secondChild < (__len - 1) / 2)
{
    __secondChild = 2 * (__secondChild + 1);

Is a bog standard log N to我。

  template<typename _RandomAccessIterator, typename _Distance,
       typename _Tp, typename _Compare>
    void
    __adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
          _Distance __len, _Tp __value, _Compare __comp)
    {
      const _Distance __topIndex = __holeIndex;
      _Distance __secondChild = __holeIndex;
      while (__secondChild < (__len - 1) / 2)
      {
        __secondChild = 2 * (__secondChild + 1);
          if (__comp(*(__first + __secondChild),
             *(__first + (__secondChild - 1))))
          __secondChild--;
          *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __secondChild));
          __holeIndex = __secondChild;
      }
      if ((__len & 1) == 0 && __secondChild == (__len - 2) / 2)
      {
        __secondChild = 2 * (__secondChild + 1);
        *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first
                             + (__secondChild - 1)));
        __holeIndex = __secondChild - 1;
      }
      std::__push_heap(__first, __holeIndex, __topIndex, 
               _GLIBCXX_MOVE(__value), __comp);      
      }

任何关于为什么这是 O <= 3N 的线索将不胜感激。
编辑:

实验结果:

这个实际实现使用

  • <2N 比较来堆化堆
  • <1.5N 以相反的顺序堆化堆。

I looked in to the C++0x standard and found the requirement that make_heap should do no more than 3*N comparisons.

I.e. heapify an unordered collection can be done in O(N)

   /*  @brief  Construct a heap over a range using comparison functor.

Why is this?

The source gives me no clues (g++ 4.4.3)

The while (true) + __parent == 0 are not clues but rather a guess for O(N) behaviour

template<typename _RandomAccessIterator, typename _Compare>
void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
          _Compare __comp)
{

  const _DistanceType __len = __last - __first;
  _DistanceType __parent = (__len - 2) / 2;
  while (true)
    {
      _ValueType __value = _GLIBCXX_MOVE(*(__first + __parent));
      std::__adjust_heap(__first, __parent, __len, _GLIBCXX_MOVE(__value),
                 __comp);
      if (__parent == 0)
        return;
      __parent--;
    }
}

__adjust_heap looks like a log N method:

while ( __secondChild < (__len - 1) / 2)
{
    __secondChild = 2 * (__secondChild + 1);

Is a bog standard log N to me.

  template<typename _RandomAccessIterator, typename _Distance,
       typename _Tp, typename _Compare>
    void
    __adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
          _Distance __len, _Tp __value, _Compare __comp)
    {
      const _Distance __topIndex = __holeIndex;
      _Distance __secondChild = __holeIndex;
      while (__secondChild < (__len - 1) / 2)
      {
        __secondChild = 2 * (__secondChild + 1);
          if (__comp(*(__first + __secondChild),
             *(__first + (__secondChild - 1))))
          __secondChild--;
          *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __secondChild));
          __holeIndex = __secondChild;
      }
      if ((__len & 1) == 0 && __secondChild == (__len - 2) / 2)
      {
        __secondChild = 2 * (__secondChild + 1);
        *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first
                             + (__secondChild - 1)));
        __holeIndex = __secondChild - 1;
      }
      std::__push_heap(__first, __holeIndex, __topIndex, 
               _GLIBCXX_MOVE(__value), __comp);      
      }

Any clues to why this is O <= 3N will be appreciated.
EDIT:

Experimental results:

This actual implementation uses

  • <2N comparisons for heapifying heaps
  • <1.5N for heapifying heaps in the reverse order.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

筱果果 2024-11-21 22:01:23

使用巧妙的算法和巧妙的分析,可以在 O(n) 时间内创建包含 n 个元素的二叉堆。接下来,我将讨论假设您有显式节点和显式左子指针和右子指针的情况,这是如何工作的,但是一旦将其压缩到数组中,此分析仍然完全有效。

该算法的工作原理如下。首先获取大约一半的节点并将它们视为单例最大堆 - 由于只有一个元素,因此仅包含该元素的树必须自动成为最大堆。现在,将这些树配对。对于每一对树,取一个尚未使用的值并执行以下算法:

  1. 使新节点成为堆的根,使其左子节点和右子节点指针指向两个 max-

  2. 当此节点有一个比它大的子节点时,请将子节点与其较大的子节点交换。

我的主张是,这个过程最终会生成一个新的最大堆,其中包含两个输入最大堆的元素,并且在时间 O(h) 内完成,其中 h 是两个堆的高度。证明是对堆高度的归纳。作为基本情况,如果子堆的大小为零,则算法会立即以单例最大堆终止,并且会在 O(1) 时间内完成此操作。对于归纳步​​骤,假设对于某些 h,此过程适用于任何大小为 h 的子堆,并考虑在两个大小为 h + 1 的堆上执行它时会发生什么。当我们添加一个新根以将两个大小为 h 的子树连接在一起时,会发生什么情况h + 1,有三种可能:

  1. 新的根比两个子树的根都大。然后在这种情况下,我们有一个新的最大堆,因为根大于任一子树中的任何节点(通过传递性)

  2. 新根比一个子树大,比另一个子树小。然后我们将根与较大的子树交换,并使用旧根和子树的两个子树(每个子树的高度均为 h)再次递归执行此过程。根据归纳假设,这意味着我们交换到的子树现在是最大堆。因此,整个堆是一个最大堆,因为新的根大于我们交换的子树中的所有内容(因为它大于我们添加的节点并且已经大于该子树中的所有内容),并且它也大于所有内容在另一个子树中(因为它比根大,而根比另一个子树中的所有内容都大)。

  3. 新根比它的两个子根都小。然后使用上述分析的稍微修改版本,我们可以表明生成的树确实是一个堆。

此外,由于每一步子堆的高度都会减少 1,因此该算法的总体运行时间必须为 O(h)。


此时,我们有一个简单的创建堆的算法:

  1. 获取大约一半的节点并创建单例堆。 (您可以明确计算此处需要多少个节点,但大约是一半)。
  2. 将这些堆配对,然后使用未使用的节点之一和上述过程将它们合并在一起。
  3. 重复步骤 2,直到只剩下一个堆。

由于在每一步中我们都知道到目前为止我们拥有的堆是有效的最大堆,因此最终会产生一个有效的总体最大堆。如果我们巧妙地选择要创建多少个单例堆,这最终也会创建一个完整的二叉树。

然而,看起来这应该在 O(n lg n) 时间内运行,因为我们进行 O(n) 次合并,每个合并都在 O(h) 中运行,并且在最坏的情况下我们要合并的树的高度是 O(lg n)。但这个界限并不严格,我们可以通过更精确的分析做得更好。

特别是,让我们考虑一下我们合并的所有树有多深。大约一半的堆的深度为零,然后剩下的一半的深度为一,然后剩下的一半的深度为二,依此类推。如果我们将其相加,我们就得到了总和

0 * n/2 + 1 * n/4 + 2 * n/8 + ... + nk/(2k) = Σk = 0 ⌈log n⌉ (nk / 2k) = n Σk = 0⌈log n⌉ (k / 2k+1)

这是交换次数的上限。每次交换最多需要两次比较。因此,如果我们将上述总和乘以 2,我们将得到以下总和,该总和确定了交换次数的上限:

n Σk = 0 (k / 2k)

这里的求和就是求和 0 / 20 + 1 / 21 + 2 / 22 + 3 / 23 + ... .这是一个著名的总结,可以通过多种不同的方式进行评估。给出了一种评估方法 在这些讲座幻灯片中,幻灯片 45-47。最终结果恰好为 2n,这意味着最终进行的比较次数肯定以 3n 为界。

希望这有帮助!

A binary heap over n elements can be created in O(n) time using a clever algorithm and a clever analysis. In what follows I'm just going to talk about how this works assuming that you have explicit nodes and explicit left and right child pointers, but this analysis is still perfectly valid once you compress it into an array.

The algorithm works as follows. Start off by taking about half of the nodes and treating them as singleton max-heaps - since there's only one element, the tree containing just that element must automatically be a max-heap. Now, take these trees and pair them off with one another. For each pair of trees, take one of the values that you haven't used yet and execute the following algorithm:

  1. Make the new node the root of the heap, having its left and right child pointers refer to the two max-heaps.

  2. While this node has a child that's larger than it, swap the child with its larger child.

My claim is that this procedure ends up producing a new max heap containing the elements of the two input max-heaps, and it does so in time O(h), where h is the height of the two heaps. The proof is an induction on the height of the heaps. As a base case, if the subheaps have size zero, then the algorithm terminates immediately with a singleton max-heap, and it does so in O(1) time. For the inductive step, assume that for some h, this procedure works on any subheaps of size h and consider what happens when you execute it on two heaps of size h + 1. When we add a new root to join together two subtrees of size h + 1, there are three possibilities:

  1. The new root is larger than the roots of both subtrees. Then in this case we have a new max-heap, since the root is larger than any of the nodes in either subtree (by transitivity)

  2. The new root is larger than one child and smaller than the other. Then we swap the root with the larger subchild and recursively execute this procedure again, using the old root and the child's two subtrees, each of which are of height h. By the inductive hypothesis, this means that the subtree we swapped into is now a max-heap. Thus the overall heap is a max-heap, since the new root is larger than everything in the subtree we swapped with (since it's larger than the node we added and was already larger than everything in that subtree), and it's also larger than everything in the other subtree (since it's larger than the root and the root was larger than everything in the other subtree).

  3. The new root is smaller than both its children. Then using a slightly modified version of the above analysis, we can show that the resulting tree is indeed a heap.

Moreover, since at each step the heights of the child heaps decreases by one, the overall runtime for this algorithm must be O(h).


At this point, we have a simple algorithm for making a heap:

  1. Take about half the nodes and create singleton heaps. (You can compute explicitly how many nodes will be needed here, but it's about half).
  2. Pair those heaps off, then merge them together by using one of the unused nodes and the above procedure.
  3. Repeat step 2 until a single heap remains.

Since at each step we know that the heaps we have so far are valid max-heaps, eventually this produces a valid overall max-heap. If we're clever with how we pick how many singleton heaps to make, this will end up creating a complete binary tree as well.

However, it seems like this should run in O(n lg n) time, since we do O(n) merges, each of which runs in O(h), and in the worst case the height of the trees we're merging is O(lg n). But this bound is not tight and we can do a lot better by being more precise with the analysis.

In particular, let's think about how deep all the trees we merge are. About half the heaps have depth zero, then half of what's left has depth one, then half of what's left has depth two, etc. If we sum this up, we get the sum

0 * n/2 + 1 * n/4 + 2 * n/8 + ... + nk/(2k) = Σk = 0⌈log n⌉ (nk / 2k) = n Σk = 0⌈log n⌉ (k / 2k+1)

This upper-bounds the number of swaps made. Each swap requires at most two comparisons. Therefore, if we multiply the above sum by two, we get the following summation, which upper-bounds the number of swaps made:

n Σk = 0 (k / 2k)

The summation here is the summation 0 / 20 + 1 / 21 + 2 / 22 + 3 / 23 + ... . This is a famous summation that can be evaluated in multiple different ways. One way to evaluate this is given in these lecture slides, slides 45-47. It ends up coming out to exactly 2n, which means that the number of comparisons that end up getting made is certainly bounded from above by 3n.

Hope this helps!

む无字情书 2024-11-21 22:01:23

@templatetypedef 已经给出了一个很好的答案为什么 < code>build_heap 的时间复杂度为O(n)CLRS 第二版的第 6 章也有一个证明。

至于为什么 C++ 标准要求最多使用 3n 次比较:

从我的实验(参见下面的代码)来看,实际上需要的比较少于 2n 次。事实上,这些讲义包含一个证明build_heap 仅使用 2(n-⌈log n⌉) 比较。

标准的界限似乎比要求的更宽松。


def parent(i):
    return i/2

def left(i):
    return 2*i

def right(i):
    return 2*i+1

def heapify_cost(n, i):
    most = 0
    if left(i) <= n:
        most = 1 + heapify_cost(n, left(i))
    if right(i) <= n:
        most = 1 + max(most, heapify_cost(n, right(i)))
    return most

def build_heap_cost(n):
    return sum(heapify_cost(n, i) for i in xrange(n/2, 1, -1))

一些结果:

n                     10  20  50  100  1000  10000
build_heap_cost(n)     9  26  83  180  1967  19960

@templatetypedef has already given a good answer for why the asymptotic run time of build_heap is O(n). There is also a proof in chapter 6 of CLRS, 2nd edition.

As for why the C++ standard requires that at most 3n comparisons are used:

From my experiments (see code below), it appears that actually less than 2n comparisons are needed. In fact, these lecture notes contain a proof that build_heap only uses 2(n-⌈log n⌉) comparisons.

The bound from the standard seems to be more generous than required.


def parent(i):
    return i/2

def left(i):
    return 2*i

def right(i):
    return 2*i+1

def heapify_cost(n, i):
    most = 0
    if left(i) <= n:
        most = 1 + heapify_cost(n, left(i))
    if right(i) <= n:
        most = 1 + max(most, heapify_cost(n, right(i)))
    return most

def build_heap_cost(n):
    return sum(heapify_cost(n, i) for i in xrange(n/2, 1, -1))

Some results:

n                     10  20  50  100  1000  10000
build_heap_cost(n)     9  26  83  180  1967  19960
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文