How can building a heap be O(n) time complexity?

364,688

Solution 1

I think there are several questions buried in this topic:

  • How do you implement buildHeap so it runs in O(n) time?
  • How do you show that buildHeap runs in O(n) time when implemented correctly?
  • Why doesn't that same logic work to make heap sort run in O(n) time rather than O(n log n)?

How do you implement buildHeap so it runs in O(n) time?

Often, answers to these questions focus on the difference between siftUp and siftDown. Making the correct choice between siftUp and siftDown is critical to get O(n) performance for buildHeap, but does nothing to help one understand the difference between buildHeap and heapSort in general. Indeed, proper implementations of both buildHeap and heapSort will only use siftDown. The siftUp operation is only needed to perform inserts into an existing heap, so it would be used to implement a priority queue using a binary heap, for example.

I've written this to describe how a max heap works. This is the type of heap typically used for heap sort or for a priority queue where higher values indicate higher priority. A min heap is also useful; for example, when retrieving items with integer keys in ascending order or strings in alphabetical order. The principles are exactly the same; simply switch the sort order.

The heap property specifies that each node in a binary heap must be at least as large as both of its children. In particular, this implies that the largest item in the heap is at the root. Sifting down and sifting up are essentially the same operation in opposite directions: move an offending node until it satisfies the heap property:

  • siftDown swaps a node that is too small with its largest child (thereby moving it down) until it is at least as large as both nodes below it.
  • siftUp swaps a node that is too large with its parent (thereby moving it up) until it is no larger than the node above it.

The number of operations required for siftDown and siftUp is proportional to the distance the node may have to move. For siftDown, it is the distance to the bottom of the tree, so siftDown is expensive for nodes at the top of the tree. With siftUp, the work is proportional to the distance to the top of the tree, so siftUp is expensive for nodes at the bottom of the tree. Although both operations are O(log n) in the worst case, in a heap, only one node is at the top whereas half the nodes lie in the bottom layer. So it shouldn't be too surprising that if we have to apply an operation to every node, we would prefer siftDown over siftUp.

The buildHeap function takes an array of unsorted items and moves them until they all satisfy the heap property, thereby producing a valid heap. There are two approaches one might take for buildHeap using the siftUp and siftDown operations we've described.

  1. Start at the top of the heap (the beginning of the array) and call siftUp on each item. At each step, the previously sifted items (the items before the current item in the array) form a valid heap, and sifting the next item up places it into a valid position in the heap. After sifting up each node, all items satisfy the heap property.

  2. Or, go in the opposite direction: start at the end of the array and move backwards towards the front. At each iteration, you sift an item down until it is in the correct location.

Which implementation for buildHeap is more efficient?

Both of these solutions will produce a valid heap. Unsurprisingly, the more efficient one is the second operation that uses siftDown.

Let h = log n represent the height of the heap. The work required for the siftDown approach is given by the sum

(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).

Each term in the sum has the maximum distance a node at the given height will have to move (zero for the bottom layer, h for the root) multiplied by the number of nodes at that height. In contrast, the sum for calling siftUp on each node is

(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).

It should be clear that the second sum is larger. The first term alone is hn/2 = 1/2 n log n, so this approach has complexity at best O(n log n).

How do we prove the sum for the siftDown approach is indeed O(n)?

One method (there are other analyses that also work) is to turn the finite sum into an infinite series and then use Taylor series. We may ignore the first term, which is zero:

Taylor series for buildHeap complexity

If you aren't sure why each of those steps works, here is a justification for the process in words:

  • The terms are all positive, so the finite sum must be smaller than the infinite sum.
  • The series is equal to a power series evaluated at x=1/2.
  • That power series is equal to (a constant times) the derivative of the Taylor series for f(x)=1/(1-x).
  • x=1/2 is within the interval of convergence of that Taylor series.
  • Therefore, we can replace the Taylor series with 1/(1-x), differentiate, and evaluate to find the value of the infinite series.

Since the infinite sum is exactly n, we conclude that the finite sum is no larger, and is therefore, O(n).

Why does heap sort require O(n log n) time?

If it is possible to run buildHeap in linear time, why does heap sort require O(n log n) time? Well, heap sort consists of two stages. First, we call buildHeap on the array, which requires O(n) time if implemented optimally. The next stage is to repeatedly delete the largest item in the heap and put it at the end of the array. Because we delete an item from the heap, there is always an open spot just after the end of the heap where we can store the item. So heap sort achieves a sorted order by successively removing the next largest item and putting it into the array starting at the last position and moving towards the front. It is the complexity of this last part that dominates in heap sort. The loop looks likes this:

for (i = n - 1; i > 0; i--) {
    arr[i] = deleteMax();
}

Clearly, the loop runs O(n) times (n - 1 to be precise, the last item is already in place). The complexity of deleteMax for a heap is O(log n). It is typically implemented by removing the root (the largest item left in the heap) and replacing it with the last item in the heap, which is a leaf, and therefore one of the smallest items. This new root will almost certainly violate the heap property, so you have to call siftDown until you move it back into an acceptable position. This also has the effect of moving the next largest item up to the root. Notice that, in contrast to buildHeap where for most of the nodes we are calling siftDown from the bottom of the tree, we are now calling siftDown from the top of the tree on each iteration! Although the tree is shrinking, it doesn't shrink fast enough: The height of the tree stays constant until you have removed the first half of the nodes (when you clear out the bottom layer completely). Then for the next quarter, the height is h - 1. So the total work for this second stage is

h*n/2 + (h-1)*n/4 + ... + 0 * 1.

Notice the switch: now the zero work case corresponds to a single node and the h work case corresponds to half the nodes. This sum is O(n log n) just like the inefficient version of buildHeap that is implemented using siftUp. But in this case, we have no choice since we are trying to sort and we require the next largest item be removed next.

In summary, the work for heap sort is the sum of the two stages: O(n) time for buildHeap and O(n log n) to remove each node in order, so the complexity is O(n log n). You can prove (using some ideas from information theory) that for a comparison-based sort, O(n log n) is the best you could hope for anyway, so there's no reason to be disappointed by this or expect heap sort to achieve the O(n) time bound that buildHeap does.

Solution 2

Your analysis is correct. However, it is not tight.

It is not really easy to explain why building a heap is a linear operation, you should better read it.

A great analysis of the algorithm can be seen here.


The main idea is that in the build_heap algorithm the actual heapify cost is not O(log n)for all elements.

When heapify is called, the running time depends on how far an element might move down in the tree before the process terminates. In other words, it depends on the height of the element in the heap. In the worst case, the element might go down all the way to the leaf level.

Let us count the work done level by level.

At the bottommost level, there are 2^(h)nodes, but we do not call heapify on any of these, so the work is 0. At the next level there are 2^(h − 1) nodes, and each might move down by 1 level. At the 3rd level from the bottom, there are 2^(h − 2) nodes, and each might move down by 2 levels.

As you can see not all heapify operations are O(log n), this is why you are getting O(n).

Solution 3

Intuitively:

"The complexity should be O(nLog n)... for each item we "heapify", it has the potential to have to filter down once for each level for the heap so far (which is log n levels)."

Not quite. Your logic does not produce a tight bound -- it over estimates the complexity of each heapify. If built from the bottom up, insertion (heapify) can be much less than O(log(n)). The process is as follows:

( Step 1 ) The first n/2 elements go on the bottom row of the heap. h=0, so heapify is not needed.

( Step 2 ) The next n/22 elements go on the row 1 up from the bottom. h=1, heapify filters 1 level down.

( Step i ) The next n/2i elements go in row i up from the bottom. h=i, heapify filters i levels down.

( Step log(n) ) The last n/2log2(n) = 1 element goes in row log(n) up from the bottom. h=log(n), heapify filters log(n) levels down.

NOTICE: that after step one, 1/2 of the elements (n/2) are already in the heap, and we didn't even need to call heapify once. Also, notice that only a single element, the root, actually incurs the full log(n) complexity.


Theoretically:

The Total steps N to build a heap of size n, can be written out mathematically.

At height i, we've shown (above) that there will be n/2i+1 elements that need to call heapify, and we know heapify at height i is O(i). This gives:

enter image description here

The solution to the last summation can be found by taking the derivative of both sides of the well known geometric series equation:

enter image description here

Finally, plugging in x = 1/2 into the above equation yields 2. Plugging this into the first equation gives:

enter image description here

Thus, the total number of steps is of size O(n)

Solution 4

It would be O(n log n) if you built the heap by repeatedly inserting elements. However, you can create a new heap more efficiently by inserting the elements in arbitrary order and then applying an algorithm to "heapify" them into the proper order (depending on the type of heap of course).

See http://en.wikipedia.org/wiki/Binary_heap, "Building a heap" for an example. In this case you essentially work up from the bottom level of the tree, swapping parent and child nodes until the heap conditions are satisfied.

Solution 5

There are already some great answers but I would like to add a little visual explanation

enter image description here

Now, take a look at the image, there are
n/2^1 green nodes with height 0 (here 23/2 = 12)
n/2^2 red nodes with height 1 (here 23/4 = 6)
n/2^3 blue node with height 2 (here 23/8 = 3)
n/2^4 purple nodes with height 3 (here 23/16 = 2)
so there are n/2^(h+1) nodes for height h
To find the time complexity lets count the amount of work done or max no of iterations performed by each node
now it can be noticed that each node can perform(atmost) iterations == height of the node

Green  = n/2^1 * 0 (no iterations since no children)  
red    = n/2^2 * 1 (heapify will perform atmost one swap for each red node)  
blue   = n/2^3 * 2 (heapify will perform atmost two swaps for each blue node)  
purple = n/2^4 * 3 (heapify will perform atmost three swaps for each purple node)   

so for any nodes with height h maximum work done is n/2^(h+1) * h

Now total work done is

->(n/2^1 * 0) + (n/2^2 * 1)+ (n/2^3 * 2) + (n/2^4 * 3) +...+ (n/2^(h+1) * h)  
-> n * ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

now for any value of h, the sequence

-> ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

will never exceed 1
Thus the time complexity will never exceed O(n) for building heap

Share:
364,688
Alex
Author by

Alex

Updated on August 06, 2022

Comments

  • Alex
    Alex almost 2 years

    Can someone help explain how can building a heap be O(n) complexity?

    Inserting an item into a heap is O(log n), and the insert is repeated n/2 times (the remainder are leaves, and can't violate the heap property). So, this means the complexity should be O(n log n), I would think.

    In other words, for each item we "heapify", it has the potential to have to filter down (i.e., sift down) once for each level for the heap so far (which is log n levels).

    What am I missing?

  • hba
    hba about 11 years
    This is a great explanation...but why is it then that the heap-sort runs in O(n log n). Why doesn't the same reasoning apply to heap-sort?
  • The111
    The111 almost 11 years
    @hba I think the answer to your question lies in understanding this image from this article. Heapify is O(n) when done with siftDown but O(n log n) when done with siftUp. The actual sorting (pulling items from heap one by one) has to be done with siftUp so is therefore O(n log n).
  • Lukas Greblikas
    Lukas Greblikas over 10 years
    I really like your external document's intuitive explanation at the bottom.
  • Jeremy West
    Jeremy West over 9 years
    I edited my answer to use a max heap since it seems that most other people are referring to that and it is the best choice for heap sort.
  • Vicky Chijwani
    Vicky Chijwani over 9 years
    This is what made it intuitively clear to me: "only one node is at the top whereas half the nodes lie in the bottom layer. So it shouldn't be too surprising that if we have to apply an operation to every node, we would prefer siftDown over siftUp."
  • cellepo
    cellepo over 8 years
    @hba the Answer below by Jeremy West addresses your question in more fine, easy-to-understand detail, further explaining The111's comment answer here.
  • cellepo
    cellepo over 8 years
    [As was commented by The111 to the Answer by emre nevayeshirazi] this wiki image also captures the comparative efficiency of just 1) making the heap with heapify (left diagram) over then 2) removing nodes to complete the heapSorting (right diagram): On the right diagram, think of the numbers as the max number of swaps for that Node when its turn comes to replace the root node that was deleted, in order for that swapping Node to then siftDown to its correct place (thereby completing 1 heap deletion [for the heapSort]).
  • aste123
    aste123 over 8 years
    @JeremyWest "One is to start at the top of the heap (the beginning of the array) and call siftUp on each item." - Did you mean to start at the bottom of the heap?
  • Jeremy West
    Jeremy West over 8 years
    @aste123 No, it is correct as written. The idea is to maintain a barrier between the part of the array that satisfies the heap property and the unsorted portion of the array. You either start at the beginning moving forward and calling siftUp on each item or start at the end moving backward and calling siftDown. No matter which approach you choose, you are selecting the next item in the unsorted portion of the array and performing the appropriate operation to move it into a valid position in the ordered portion of the array. The only difference is performance.
  • Sid
    Sid over 7 years
    A question. It seems to me that the # comparisons made for a node at height i from the bottom of a tree of height h must make 2* log(h-i) comparisons as well and should be accounted for as well @The111. What do you think?
  • Siddhartha
    Siddhartha about 7 years
    @hba HeapSort is nlogn because it involves extracting the minimum from the heap out (which lies at the root) n times, and each time, heapify down is called at the root which could at worst take log-n time. This is as opposed to emre's (OP's) answer that heapify down is called on all elements, only 1 of which lies at the root.
  • ntakouris
    ntakouris over 6 years
    youtube.com/… At about 38:00
  • gnasher729
    gnasher729 over 6 years
    @hba Building the heap takes 0 operations for half the values, 1 operation for a quarter of values, 2 operations for 1/8th of the values etc. with a total O (N). When doing heapsort, all the operations take log n because you always remove an item from the top, then insert a new item at the top where it is most expensive to insert.
  • Abhijit Sarkar
    Abhijit Sarkar over 5 years
    Great answer, upvoted. One thing that's not clear to me is, in order to implement the heap as an array, you'd start from the end of the k arrays, and insert n/2 elements in the heap array from the end. After that, where do you insert the next element, which is the parent of one of the n/2 elements at height h?
  • Jeremy West
    Jeremy West over 5 years
    @AbhijitSarkar Hi Abhijit, I'm not sure I understand your question. There aren't k arrays in question, just one. If you're thinking of one array per row of the heap (perhaps?), that's not necessary. You can just number the elements in the heap so that you know how to find the parents and children of each node. It's roughly something like 2n+1, 2n+2 are the children of node n, depending on where you start your indices.
  • Abhijit Sarkar
    Abhijit Sarkar over 5 years
    @JeremyWest I'm talking about building the heap from k arrays each of max size n, which would be initial step of a k-way merge. I was referred to your answer from my question here
  • Jeremy West
    Jeremy West over 5 years
    @Abhijit That is a pretty different problem. But the idea there is to put the first item from each of the k sorted arrays into the heap, build the heap, then as you return the item at the top of the heap, you add a new item from the stream it came from.
  • temporary_user_name
    temporary_user_name over 5 years
    This answer would be greatly improved in its utility to most readers if it included a pseudocode implementation of O(n) heap construction.
  • Jeremy West
    Jeremy West over 5 years
    @temporary_user_name Sure, I can add some code. The algorithm is pretty standard and pseudocode and real implementations are available all over the place.
  • temporary_user_name
    temporary_user_name over 5 years
    In theory that is totally true but it took a surprising amount of time for me to find a clean implementation of the O(n) approach. It would just be beneficial, since this is a high-ranking search result, to bring the resources together into one place.
  • A is for Ambition
    A is for Ambition almost 4 years
    "The terms are all positive, so the finite sum must be smaller than the infinite sum." Isn't this not true in general? For example, the classic 1 + 2 + 3 + ... = -1/12.
  • Jeremy West
    Jeremy West almost 4 years
    @AisforAmbition You're correct; it is only true if the series converges, which is true in this case. Your example is a reminder that all bets are off for divergent series!
  • Telescope
    Telescope over 3 years
    Somebody correct me if I'm wrong, but isn't using siftUp also O(N)? After all, every time an element is added to the bottom, there is a 1/2 chance that it needs to sift up 1 level, a 1/4 chance that it needs to sift up 2 levels, and so on, which sums to an average of 2 levels needed to sift up no matter the size of the tree, meaning siftUp is O(1) for 1 element and thus O(N) for making a heap. Am I making some logical mistake here?
  • Jeremy West
    Jeremy West over 3 years
    @Telescope I haven't thought about it too carefully, but your analysis does seem correct if we're talking about average case performance. For the siftDown implementation, the worst case behavior is guaranteed to be linear.
  • Rex
    Rex over 3 years
    It may be true ShiftDown seemed cheaper assuming each shift costs the same as ShiftUp. However if we consider ShiftDown require two comparisons per shift(To shift with Larger child in a Max Heap or smaller child in a Min Heap), while ShiftUp only requires a single comparison with its parent per shift, we could argue ShiftUp is in fact cheaper?
  • Jeremy West
    Jeremy West over 3 years
    @Rex Yes, the worst case cost for siftDown on an arbitrary node is likely to be roughly twice that of siftUp. However, this constant factor has no impact on the big-O analysis for buildHeap as an algorithm. More importantly, we're going to apply one of these two functions to every node in the heap. As mentioned in the answer, siftDown is asymptotically less expensive when applied over the entire heap than siftUp. If you like, think of the siftDown implementation as O(2n) vs. the siftUp implementation with a cost of O(nlog n). 2 < log n even for very small values of n.
  • Rex
    Rex over 3 years
    @Jeremy Thank you! Crystal clear. Logn is good, but 2 is much better.
  • João Manuel Rodrigues
    João Manuel Rodrigues about 3 years
    This is a very complete and clear explanation. Well done!
  • Avv
    Avv almost 3 years
    Thank you very much. Why you decided to plug in x=1/2 please?
  • YQ.Wang
    YQ.Wang over 2 years
    There are two approaches one might take for buildHeap that is begin from the top using siftUp and begin from the end using siftDown. Why can't we begin from the top and use siftDown? Or begin from the end using siftUp
  • Sayed
    Sayed over 2 years
    Brilliant answer, specially the last point "Why does heap sort require O(n log n) time".
  • juancamilo87
    juancamilo87 over 2 years
    Because of the formula of the sumation to infinity of i*x^i = x/ (1-x)^2. so i*(1/2)^i is the same as i*(x)^i when x = 1/2
  • Nitin Jain
    Nitin Jain about 2 years
    Such a well written answer. Kudos!
  • user56202
    user56202 about 2 years
    @YQ.Wang Suppose you started from the top, and used sift down. Everything has been working fine, and you are about to sift down (the key of) node k. But now one of the children of node k is a super giga huge number. It's not enough to just swap it with k. You have to sift it up a lot, potentially all the way to the root. So If you start reading your array from the top, and use sift down, you will probably have to mix in lots of sift up's as well.