c++ - 为什么我的二进制堆插入在实践中会以这种方式运行?

标签 c++ algorithm heap asymptotic-complexity

我用 C++ 实现了一个基于数组的二叉堆和一个基于指针的二叉堆。我进行了一个小实验,其中对于不同的输入大小 n,我进行了 n 次插入。这些元素是 int32_t 类型的,它们中的每一个都是随机(使用梅森扭曲器)从

{1,...,std::numeric_limits<int32_t>::max()}

所以我将每个实验运行 10 次,并计算完成实验所需的平均 CPU 时间。

为了计算 cpu 时间,我使用了这些函数:

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

这是运行时间

enter image description here

对我来说,插入 n 个元素似乎需要线性时间而不是 nlogn 时间。如果我将运行时间除以 n,我会得到下图:

enter image description here

两个运行时间都收敛到一个常数。所以这证实了我的假设。

但是,为什么?它不应该收敛于对数函数吗?不是每次插入都是O(logn)吗?

最佳答案

通过重复插入从随机数据构建二进制堆的预期时间确实是O(n),尽管最坏情况时间(当输入已排序)是 O(n log n)。这个有趣的结果已经为人所知有一段时间了,虽然它显然不是广为人知的,大概是因为著名的保证线性时间堆化算法的流行是由于 R.W. Floyd。

直觉上,基于随机构建的堆近似于完整二叉树的假设,人们可能期望随机元素的平均插入时间为 O(1)。插入算法包括将一个元素放在堆的末尾,然后通过与它的父元素反复交换来推进它,直到满足堆约束。

如果堆是一棵完整的二叉树,平均插入时间确实是 O(1),因为在交换链中的每个点,需要进行另一次交换的概率为 0.5。因此,在一半的情况下不需要交换;四分之一的时间需要一次交换,八分之一的时间需要两次交换;等等。因此,预期的交换次数为 0 + 0.5 + 0.25 + ... == 1。

由于堆只是一个完全二叉树的近似,上面的分析是不够的。没有重新平衡就不可能维护二叉树,这具有不小的成本。但是您可以证明堆与二叉树非常相似,因此预期的插入时间仍然是 O(1)。证明是不平凡的;在线提供的一项分析是 Ryan Hayward 和 Colin McDiarmid 的“重复插入堆构建的平均案例分析”(1991 年),可从第二作者的 online publication list. 获得。

虽然 Floyd 的 heapify 算法具有更好的最坏情况性能和更紧密的内循环,但由于缓存效应,重复插入算法实际上对于大型堆可能更快(平均而言)。例如,参见 1999 年的论文 "Performance engineering case study: heap construction "作者:Jesper Bojesen、Jyrki Katajainen 和 Maz Spork。


备注:

当使用随机数据进行此类实验时,重要的是要避免计算生成随机数的成本。对于像堆插入这样相对较快的算法,调用 PRNG 的成本与算法的成本相比很可能是显着的,结果是观察到的结果因生成随机数的线性成本而有偏差。

为避免这种影响,您应该预先生成随机数组,然后测量将其变成堆的成本。

正如人们经常观察到的那样,对于 n 的所有实际值,O(log n) 是 O(1);如果你有 c1O(1) + c2O(log n) 其中 c1c2 大得多,结果看起来很像 O (1).

关于c++ - 为什么我的二进制堆插入在实践中会以这种方式运行?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33081082/

相关文章:

algorithm - 复制一个字符串 n 次最少需要多少次操作?

algorithm - 用于与多个任意值进行比较的存储算法

algorithm - GoLang 堆和堆排序

algorithm - 如何索引分层数据?

import - SQL Developer 不会尝试导入 .xlsx 文件,因为它太大

合并两个最大堆的算法?

c++ - 设置射线(原点,方向)和三角形交点(无glm)

c++ - OSX 64 位 C++ 逐行反汇编

c++ - 以编程方式获取环回接口(interface)/C++

c++ - 在 Apple Color Emoji 中使用 Harfbuzz 和 Freetype 的表情符号修饰符和 ZWJ 序列