c – 为什么我的二进制堆插入在实践中表现如此?

我在C中实现了一个基于数组的二进制堆和一个基于指针的二进制堆.我进行了一个小实验,对于不同的输入大小n,我进行了n次插入.这些元素的类型为int32_t,并且每个元素都是随机选取的(使用mersenne twister)

{1,…,的std :: numeric_limits< int32_t> :: MAX()}

所以我每次都进行10次实验,并花费了完成实验所需的平均cpu时间.

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

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

这是运行时间

《c – 为什么我的二进制堆插入在实践中表现如此?》

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

《c – 为什么我的二进制堆插入在实践中表现如此?》

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

但为什么?它不应该收敛到对数函数?是不是每个插入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算法具有更好的最坏情况性能和更紧密的内循环,但由于缓存效应,重复插入算法实际上对于大堆实际上更快(平均).例如,见Jesper Bojesen,Jyrki Katajainen和Maz Spork撰写的1999年论文“Performance engineering case study: heap construction.

注意:

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

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

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

点赞