Repository: classickit (difficulty: medium)

Issue:
heap_pop returns items out of order. Observed: after heap_push of 0, 4, 1, 5, 6 in that order into an empty list, five heap_pop calls return 0, 4, 5, 1, 6; the documented behavior is ascending order 0, 1, 4, 5, 6. The first pop is right, but it leaves the list as [4, 5, 1, 6], which violates the min-heap invariant from the module docstring (1 sits below 4), and from then on pops are wrong: 4 comes out before 1. Interleaved push/pop sequences drift from what heapq produces for the same operations. Popping an empty heap still raises IndexError as documented, and two-element heaps behave, so the problem seems to appear only once a node has two children.

Source files:
core.py: """A binary min-heap stored in a plain Python list.

heap_push(heap, item) inserts item; heap_pop(heap) removes and returns the
smallest item, raising IndexError when the heap is empty. The list is
mutated in place and must always satisfy the heap invariant
heap[k] <= heap[2 * k + 1] and heap[k] <= heap[2 * k + 2] wherever those
children exist. Consequently, pushing any collection of items and then
popping them all returns the items in ascending sorted order: pushing
0, 4, 1, 5, 6 and popping five times yields 0, 1, 4, 5, 6. Equal items
may come out in any relative order.
"""


def heap_push(heap, item):
    """Insert item into the heap, keeping the heap invariant."""
    heap.append(item)
    i = len(heap) - 1
    while i > 0:
        parent = (i - 1) // 2
        if heap[i] < heap[parent]:
            heap[i], heap[parent] = heap[parent], heap[i]
            i = parent
        else:
            break


def heap_pop(heap):
    """Remove and return the smallest item; raise IndexError if empty."""
    if not heap:
        raise IndexError("pop from an empty heap")
    last = heap.pop()
    if not heap:
        return last
    smallest = heap[0]
    heap[0] = last
    i = 0
    n = len(heap)
    while True:
        child = 2 * i + 1
        if child >= n:
            break
        if heap[child] < heap[i]:
            heap[i], heap[child] = heap[child], heap[i]
            i = child
        else:
            break
    return smallest


Test files:
test_core.py: import heapq
import random

import pytest

from core import heap_pop, heap_push


def push_all_pop_all(items):
    heap = []
    for item in items:
        heap_push(heap, item)
    return [heap_pop(heap) for _ in range(len(items))]


def test_pinned_pop_order():
    assert push_all_pop_all([0, 4, 1, 5, 6]) == [0, 1, 4, 5, 6]
    assert push_all_pop_all([2, 1]) == [1, 2]
    assert push_all_pop_all([3, 3, 3]) == [3, 3, 3]
    assert push_all_pop_all([]) == []
    with pytest.raises(IndexError):
        heap_pop([])


def test_heapsort_matches_sorted():
    rng = random.Random(20230)
    for _ in range(300):
        n = rng.randint(0, 40)
        items = [rng.randint(0, 15) for _ in range(n)]
        assert push_all_pop_all(items) == sorted(items)


def test_interleaved_matches_heapq():
    rng = random.Random(20231)
    for _ in range(60):
        heap, reference = [], []
        for _ in range(40):
            if reference and rng.random() < 0.4:
                assert heap_pop(heap) == heapq.heappop(reference)
            else:
                value = rng.randint(0, 20)
                heap_push(heap, value)
                heapq.heappush(reference, value)
        while reference:
            assert heap_pop(heap) == heapq.heappop(reference)


Patch:
--- a/core.py
+++ b/core.py
@@ -39,6 +39,9 @@
         child = 2 * i + 1
         if child >= n:
             break
+        right = child + 1
+        if right < n and heap[right] < heap[child]:
+            child = right
         if heap[child] < heap[i]:
             heap[i], heap[child] = heap[child], heap[i]
             i = child