如何在进行最多3N个比较的同时实现std::make_heap
How can std::make_heap be implemented while making at most 3N comparisons?
我查看了C++0x标准,发现make_heap的比较不应超过3*N。
也就是说,堆积一个无序的集合可以在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看起来像logN方法:
while ( __secondChild < (__len - 1) / 2)
{
__secondChild = 2 * (__secondChild + 1);
对我来说是沼泽地的标准日志。
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,用于按相反顺序堆堆
使用巧妙的算法和分析,可以在O(n)时间内创建一个包含n个元素的二进制堆。在接下来的内容中,我将讨论假设您有显式节点和显式左右子指针,这是如何工作的,但一旦您将其压缩到数组中,这种分析仍然完全有效。
该算法的工作原理如下。首先,取大约一半的节点,并将它们视为单例最大堆——由于只有一个元素,因此仅包含该元素的树必须自动成为最大堆。现在,把这些树摘下来,把它们一对一对地摘下来。对于每对树,取一个尚未使用的值,并执行以下算法:
-
使新节点成为堆的根,使其左右子指针指向两个最大堆。
-
如果此节点有一个子节点比它大,请将该子节点与其较大的子节点交换。
我的主张是,这个过程最终会生成一个新的最大堆,其中包含两个输入最大堆的元素,并且它在时间O(h)中这样做,其中h是两个堆的高度。证据是对堆的高度的归纳。作为一种基本情况,如果子堆的大小为零,那么算法会立即终止为单例最大堆,并在O(1)时间内完成。对于归纳步骤,假设对于某个h,此过程适用于任何大小为h的子堆,并考虑当您在两个大小为h+1的堆上执行它时会发生什么。当我们添加一个新的根来连接两个大小为h+1的子树时,有三种可能性:
-
新根大于两个子树的根。在这种情况下,我们有一个新的最大堆,因为根比任意子树中的任何节点都大(通过传递性)
-
新根比一个子项大,比另一个子项小。然后,我们用较大的子树交换根,并再次递归执行此过程,使用旧根和子树的两个子树,每个子树的高度都为h。根据归纳假设,这意味着我们交换到的子树现在是一个最大堆。因此,整个堆是一个最大堆,因为新的根比我们交换的子树中的所有根都大(因为它比我们添加的节点大,并且已经比该子树中的任何根都大),而且它也比其他子树中的一切根都大。
-
新根比它的两个子根都小。然后使用上面分析的一个稍微修改过的版本,我们可以表明生成的树确实是一个堆。
此外,由于每一步子堆的高度都会降低一个,因此该算法的总体运行时间必须为O(h)。
在这一点上,我们有一个制作堆的简单算法:
- 占用大约一半的节点并创建单例堆。(您可以明确计算这里需要多少个节点,但大约是一半)
- 将这些堆配对,然后使用其中一个未使用的节点和上面的过程将它们合并在一起
- 重复步骤2,直到剩下一个堆
由于在每一步我们都知道到目前为止我们拥有的堆是有效的最大堆,因此最终会产生一个有效的整体最大堆。如果我们在如何选择要生成的单例堆方面很聪明,那么最终也会创建一个完整的二叉树。
然而,这似乎应该在O(n-lgn)时间内运行,因为我们进行O(n)合并,每个合并都在O(h)中运行,在最坏的情况下,我们正在合并的树的高度是O(lgn)。但这个界限并不严格,我们可以通过更精确的分析做得更好。
特别是,让我们想想我们合并的所有树有多深。大约一半的堆深度为零,剩下的一半深度为一,剩下的半深度为二,等等。如果我们把这些加起来,我们得到的总和
0*n/2+1*n/4+2*n/8+…+nk/(2k)=Σk=0⌈日志n⌉(nk/2k)=nΣk=0⌈日志n⌉(k/2k+1)
这是交换次数的上限。每次交换最多需要两次比较。因此,如果我们将上述总和乘以2,我们就会得到以下总和,即掉期数量的上限:
nΣk=0∞(k/2k)
这里的总和是0/20+1/21+2+3/23+…的总和。这是一个著名的总结,可以用多种不同的方式进行评估。评估这一点的一种方法在这些讲座幻灯片,幻灯片45-47中给出。它最终精确到2n,这意味着最终进行的比较的数量肯定是由3n限定的。
希望这能有所帮助!
@templateypepedef已经给出了一个很好的答案来解释为什么build_heap
的渐近运行时间是O(n)。CLRS第2版第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
- C++标准是否允许<double>在没有开销的情况下实现 std::可选
- glibcxx STL 在实现 std::valarray::sum() 时是否不正确?
- 在 x86 上实现 std::atomic_thread_fence(std::memory_order_seq_cst
- 尝试实现std::tie和std::tuple的小版本
- 在实现 std::vector 时,我必须拥有 end() 的"end"指针吗?
- 如何正确实现 std::all_of 函数来验证字符串的一部分?
- 如何为向量实现 std::erase ?
- 如何实现std ::何时_any而不进行轮询
- 如何正确安全地实现 std::array 的C++算术运算符
- 与自定义命名空间一起使用时实现 std::error_category、名称解析问题
- 如何为模板类实现std ::哈希
- 为什么标准库不以无锁的方式为 8 字节以下的结构实现 std::atomic?
- 为什么 MSVC 在实现 std::bitset::count 时不使用 __popcnt?
- 实现 std::tuple 的详细信息
- 如何实现std :: set(红色/黑色树)前迭代
- 以C 标准的方式实现STD :: Malloc
- 如何为自定义模板化迭代器实现 std::d istance()
- 如何实现 std::map::查找包含两个结构'Pos'结构的比较逻辑,每个结构包含 x 和 y 坐标
- 实现 std::vector::p ush_back 强异常安全
- 是否可以实现std ::移动和清除功能