【问题标题】:How can std::make_heap be implemented while making at most 3N comparisons?在进行最多 3N 次比较时如何实现 std::make_heap ?
【发布时间】:2011-09-12 02:14:49
【问题描述】:

我查看了 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);

对我来说是一个沼泽标准日志 N。

  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 编辑:

实验结果:

这个实际实现使用

【问题讨论】:

  • 指出并解释为什么您发布的来源支持您的猜想。我并不是说它没有,只是我懒得去趟过它。
  • 您可能想在Wikipedia 上查看为什么 heapify 是 O(n) 的解释。
  • @trutheality O(n) 在那篇文章中没有提到,我错过了什么。
  • @truth 因子 3 从何而来?
  • @Mike:它在“构建堆”部分。

标签: c++ algorithm stl big-o binary-heap


【解决方案1】:

使用巧妙的算法和巧妙的分析,可以在 O(n) 时间内创建一个包含 n 个元素的二进制堆。接下来我将讨论它是如何工作的,假设你有明确的节点和明确的左右子指针,但是一旦你把它压缩成一个数组,这个分析仍然是完全有效的。

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

  1. 使新节点成为堆的根,使其左右子指针指向两个最大堆。

  2. 虽然此节点有一个比它大的子节点,但将子节点与其更大的子节点交换。

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

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

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

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

此外,由于每一步子堆的高度都会减一,因此该算法的总体运行时间必须为 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 + ... .这是一个著名的总结,可以用多种不同的方式进行评估。评估这一点的一种方法是in these lecture slides, slides 45-47。它最终精确到 2n,这意味着最终进行的比较次数肯定会以 3n 为界。

希望这会有所帮助!

【讨论】:

  • CLRS第2版第6章也有证明。
  • 太棒了!但是为什么“如果我们很聪明地选择了多少个单例堆,这最终也会创建一个完整的二叉树。”为什么是因子 3?
  • @Captain Giraffe:CLRS 中的证明表明__adjust_heapO(h),其中 h 是节点在那个树。然后它表明,由于节点高度的分布,这总计 O(n)。现在,书中的版本每次迭代使用 2 次比较(找到最大的父、左子和右子),因此您可以使用相同的参数来表示如果 __adjust_heap 最多需要 2h i> 比较,make_heap 最多需要 2n。我猜这 3 是为了给实施者一些松懈,尽管似乎只需要 2。
  • @hammar 这可能是 C++ 标准文档第一次给实现者“松懈”,但我一直在同一思路上。
  • @Mason- 这个想法是,对于足够大的 k,k sqrt(2)^k 可能是真的。由于只有有限多个较小的项,因此这里的“+ O(1)”项确保我们吸收了 k > sqrt(2)^k 的时间。这有意义吗?
【解决方案2】:

@templatetypedef 已经给出了a good answer 为什么build_heap 的渐近运行时间是O(n)。在CLRS,第2版的第6章中也有一个证明。

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

从我的实验(见下面的代码)看来,实际上需要少于 2n 个比较。事实上,these lecture notes 包含一个证明 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

【讨论】:

  • 很高兴看到 Python 在意想不到的地方使用。感谢示例代码!
  • 感谢您发布讲义。第 7 页显示了一个非常简洁的证明。
猜你喜欢
  • 2011-06-30
  • 2022-08-18
  • 2014-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多