1

So I've seen various posts where users get "unusual/unexpected" results when trying to heappop from a regular list. (ex: Unusual result from heappop?) The solution of course is to heapify it first.

However, for this LeetCode solution heapq methods are used on a regular list that isn't heapified prior to using the methods yet still return the correct expected results. Is this because when you use heappop/heappush onto a regular list it just pops/adds the first element in the list?

chadlei
  • 179
  • 1
  • 11

2 Answers2

4

In the example they are using heappop on a list that initially contains a single element (the source), so it satisfies the heap property.
It is not mandatory to use heapify on a list before using functions such as heappop or heappush. Indeed the list might be empty, contain a single element, or be a list that already satisfies the heap property.

Example:

>>> l = [1, 3, 2, 5, 4] # initial list that respects the heap property
>>> heappop(l)
1
>>> heappop(l)
2
>>> heappop(l)
3
>>> heappop(l)
4
>>> heappop(l)
5
abc
  • 11,579
  • 2
  • 26
  • 51
  • so using those 2 heapq methods on a regular list would just affect the first element of the list correct? – chadlei Jan 05 '21 at 19:48
  • No, heappop will pop the element at index 0 from the list, but it will then need to make sure that the heap invariant holds for the rest of the list. – abc Jan 05 '21 at 19:53
  • ok yes that's what I meant, the pop will pop heap[0] because the heap should always be maintained where the first element is the smallest. push will insert the element into the list where it belongs according to the sorting property, so not necessarily the first element. is this correct? – chadlei Jan 05 '21 at 22:26
  • heappush will insert the element in a position such that the heap invariant property is maintained. If the element you are pushing is the smallest one, over all the min-heap, then it will be added at index 0. – abc Jan 05 '21 at 22:30
4

The heapq module doesn't define any new data type to represent heaps. A heap is simply a list (or really, any sequence), that obeys the heap invariant:

Heaps are arrays for which a[k] <= a[2*k+1] and a[k] <= a[2*k+2] for all k, counting elements from 0.

In order for a list to not be a heap, it is necessary and sufficient to find an index k for which the invariant is false.

This is impossible for empty and single-item lists, because the required list elements simply do not exist, so both are trivially heaps.

For lists with 2 or more elements, there is always at least one condition that can be false, namely a[0] <= a[1].

heappush and heappop are both documented as "maintaining the heap invariant": if each function's first argument is a heap before the function is called, it remains a heap after the function returns. (If the first argument is not a heap, their behavior is essentially undefined.)

Here are the definitions of each:

def heappush(heap, item):
    """Push item onto heap, maintaining the heap invariant."""
    heap.append(item)
    _siftdown(heap, 0, len(heap)-1)

The private function _siftdown is responsible for restoring the heap invariant after appending item could have potentially violated it.

def heappop(heap):
    """Pop the smallest item off the heap, maintaining the heap invariant."""
    lastelt = heap.pop()    # raises appropriate IndexError if heap is empty
    if heap:
        returnitem = heap[0]
        heap[0] = lastelt
        _siftup(heap, 0)
        return returnitem
    return lastelt

The private function _siftup is responsible for restoring the heap invariant after replacing heap[0] with lastelt may have violated it.


In the code you linked to, pq is initialized to a single-item list, which as we previously noted is already a heap. Since pq is only subsequently modified by calls to heappop and heappush, it remains a heap for the duration of the function call.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • lets say pop is called. it removes the first element from the list (heap[0]) since according to the invariant the smallest item should be in the front. after the element is popped, the heap will sort itself out again to maintain the invariant, correct? – chadlei Jan 05 '21 at 22:27
  • Yes, `heappop` doesn't *just* remove the first element; it reorders the remaining elements as necessary to maintain the invariant. (Typically, it does so by swapping the first and last elements, popping the new last element, then performing swaps with the new first element until it satisfies the invariant.) – chepner Jan 06 '21 at 13:50
  • very interesting. this was something that i had trouble understanding from the docs, thanks! – chadlei Jan 06 '21 at 17:11