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

Why do my binary heap insertions behave this way in practice?

我用 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);

这是运行宁次

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

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

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

对于较小的 n,nlog(n) 可能非常接近线性。

O(N log N) Complexity - Similar to linear?

不能说不是O(nlog(n))

  • 第一个图表显示了措施 f(n)。这是不正确的,因为 log(100000) 与中显示的值相比仍然很小 y 轴,如 6e+6.
  • 第二张图显示了 f(n)/n 的度量。这是一个可接受的度量,因为它显示了对数分量的行为。但目前还不清楚,如log2(10000) = 13.9log2(125000) = 16.9。这两个值之间的比率为 1.2。仅凭您的眼睛可能不清楚这是来自对数还是来自另一个乘数。

您还需要做些什么才能弄清楚:

  1. 增加最大值n
  2. 只显示呈指数增长的数据点,即{2^0, 2^1,...,2^p,..., 2^n}。您将期望获得一条不平行于 x 轴的直线,以确定它是对数的。

我的看法是,您的原始 post 没有任何内容允许您决定它不是 nlog(n)

如果您 plot the expected running time divided by n,您会看到与 aheap 的第二个情节非常相似的情节。请注意,n 越大,斜率越小(正如预期的那样),因此看起来确实收敛于一个常数,但实际上并非如此。所以我认为你观察到的确实是 O(n log n) 运行 时间,只是 log n 部分在大值上没有太大变化,所以它错误地看起来是一条直线。

事实上,您的 aheap 曲线看起来像是一条从 25000 到 125000 的直线。但是,log(n) 在此范围内仅变化了 16%(ln(125000)/ln(25000)=1.1589... ).您可能没有注意到这一变化。

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

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

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

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

获得

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


注:

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

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

正如经常观察到的那样,O(log n) 对于 n 的所有实际值都是 O(1);如果你有 c1O(1) + c2O(log n) 其中 c1c2,结果会很像O(1).