4

I implemented in C++ an array based binary heap and a pointer based binary heap. I run a small experiment where for varying input sizes n, I did n insertions. The elements are of type int32_t and each one of them is picked uniformly at random (with mersenne twister) from

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

So I run every experiment 10 times and took the average cpu time it took to finish an experiment.

To compute the cpu time I used these functions:

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);

and

clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

Here are the running times

enter image description here

To me it seems like to insert n elements takes linear time instead of nlogn time. If I divide the running time by n, I get the following graph:

enter image description here

Both running times converge to a constant. So this confirms my assumption.

But, why? Shouldn't it be converging to the logarithmic function? Isn't each insertion O(logn)?

Petr
  • 9,812
  • 1
  • 28
  • 52
jsguy
  • 2,069
  • 1
  • 25
  • 36
  • 1
    0. was is pheap, what is aheap? 1. Is your implementation correct? 2. if it is, it looks like there is amortization at play, because the cost per insertion around 0 is huge 3. don't plot aheap and pheap on the same plot, as pheap is of a greater scale. 4. pheap second graph looks like a logarithm. – UmNyobe Oct 12 '15 at 12:35
  • Can you post the code of tests? Are you sure you are not doing anything like building a heap from leaves upward, which [is known](https://en.wikipedia.org/wiki/Binary_heap#Building_a_heap) to be linear in time? – Petr Oct 12 '15 at 12:38
  • I have tested the correctness of my heap implementations thoroughly, I would be surprised If I found any bugs in them. you can find my experiment code here: http://pastebin.com/Giii0jNy I create the random elements and then insert them to my heap one by one – jsguy Oct 12 '15 at 12:40
  • @jsguy can you go on a online plotter of your choice and plot logarithm from 0 to 100000 ? – UmNyobe Oct 12 '15 at 12:42
  • http://www.wolframalpha.com/input/?i=plot+log2%28x%29+for+x%3D0+to+100000 even for this size I think we should see some difference. However the following plot, which is actually exactly the running time of each experiment looks more like a line: http://www.wolframalpha.com/input/?i=plot+log2%281%29%2Blog2%282%29%2B...%2Blog2%28n%29+for+n%3D0+to+100000 I am confused at the moment – jsguy Oct 12 '15 at 12:46
  • 1
    no you may not see. the wolfram post scaled on high values, with the highest being 16 (as expected). Yours show all values. Growing from 0 to 16 over 100 000 values look like it is converging to a constant. – UmNyobe Oct 12 '15 at 12:50
  • Although the result is not unexpected, it is important to benchmark accurately, avoiding counting the cost of generating the random numbers. It is quite possible that the generation time is significant compared to the heap insertion time, with the consequence that the observed results are biased by the linear cost of generating random numbers. To avoid this effect, you could pregenerate the random array and then measure the cost of turning it into a heap. – rici Oct 12 '15 at 15:11

4 Answers4

1

It is indeed true that the expected time to build a binary heap from random data by repeated insertion is O(n), although the worst-case time (when the input is sorted) is O(n log n). This interesting result has been known for some time, although it is not apparently widely-known, presumably because of the popularity of the well-known guaranteed linear-time heapify algorithm due to R.W. Floyd.

Intuitively, one might expect the average insertion time for random elements to be O(1), based on the assumption that a randomly-built heap approximates a complete binary tree. The insertion algorithm consists of placing an element at the end of the heap and then advancing it by repeatedly swapping with its parent until the heap constraint is satisfied.

If the heap were a complete binary tree, the average insertion time would indeed by O(1), since at each point in the chain of swaps the probability that another swap will be necessary would be 0.5. Thus, in half of the cases no swap is necessary; a quarter of the time, one swap is needed, an eighth of the time, two swaps are needed; and so on. Hence, the expected number of swaps is 0 + 0.5 + 0.25 + ... == 1.

Since the heap is just an approximation of a complete binary tree, the above analysis is not sufficient. It is impossible to maintain a binary tree withou rebalancing, which has a nontrivial cost. But you can demonstrate that the heap is sufficiently similar to a binary tree that the expected insertion time is still O(1). The proof is non-trivial; one analysis available on-line is "Average Case Analysis of Heap Building by Repeated Insertion" (1991) by Ryan Hayward and Colin McDiarmid, which is available from the second author's online publication list.

While Floyd's heapify algorithm has better worst-case performance and a tighter inner loop, it is possible that the repeated-insertion algorithm is actually faster (on average) for large heaps because of cache effects. See, for example, the 1999 paper "Performance engineering case study: heap construction" by Jesper Bojesen, Jyrki Katajainen, and Maz Spork.


Note:

When performing experiments like this using random data, it is important to avoid counting the cost of generating the random numbers. For relatively fast algorithms like heap insertion, it is quite possible that the cost of calling the PRNG is significant compared to the cost of the algorithm, with the consequence that the observed results are biased by the linear cost of generating random numbers.

To avoid this effect, you should pregenerate the random array and then measure the cost of turning it into a heap.

As has often been observed, O(log n) is O(1) for all practical values of n; if what you have is c1O(1) + c2O(log n) where c1 is much larger than c2, the result will look a lot like O(1).

Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
0

It could be that nlog(n) is pretty close to linear for small n.

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

Community
  • 1
  • 1
xiaofeng.li
  • 8,237
  • 2
  • 23
  • 30
0

You cannot say it is not O(nlog(n))

  • The first graph shows measures f(n). It is incorrect because log(100000) is still quite small compared to the values shown in the y axis, like 6e+6.
  • The second graph shows measures of f(n)/n. It is an acceptable measure because it show the behavior of the logarithm component. But it is still unclear, as log2(10000) = 13.9 and log2(125000) = 16.9. Which give a ratio of 1.2 between the two values. With your eyes only it may not clear whether this comes from a logarithm or from another multiplicator.

What you need to do further to be clear :

  1. increase the maximum value of n
  2. Only show data points which are exponentially increasing, ie {2^0, 2^1,...,2^p,..., 2^n}. You will expect to obtain a straight line, not parallel to the x-axis to decide it is logarithmic.

My take is that nothing on your original post allow you to decide it is not nlog(n)

UmNyobe
  • 22,539
  • 9
  • 61
  • 90
0

If you plot the expected running time divided by n, you will see a plot very similar to your second plot of aheap. Note that the bigger n becomes, the smaller becomes the slope (as expected), so it indeed looks like converging to a constant, while it is in fact not. So I think that what you are observing is indeed the O(n log n) running time, just the log n part does not change much on big values, so it falsely looks as a straight line.

In fact, your plot for aheap looks like a straight line only starting from 25000 to 125000. However, the log(n) changes in this range only by 16% (ln(125000)/ln(25000)=1.1589...). You might not notice this change.

Petr
  • 9,812
  • 1
  • 28
  • 52