Skip to content

Commit 8aaddc8

Browse files
Implemented the test cases and Pyhon classes for the basic functionality.
1 parent 66c76fa commit 8aaddc8

2 files changed

Lines changed: 297 additions & 2 deletions

File tree

Lib/heapq.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@
128128

129129
__all__ = ['heappush', 'heappop', 'heapify', 'heapreplace', 'heappushpop',
130130
'heappush_max', 'heappop_max', 'heapify_max', 'heapreplace_max',
131-
'heappushpop_max', 'nlargest', 'nsmallest', 'merge']
131+
'heappushpop_max', 'nlargest', 'nsmallest', 'merge',
132+
'MinHeap', 'MaxHeap']
132133

133134
def heappush(heap, item):
134135
"""Push item onto heap, maintaining the heap invariant."""
@@ -592,6 +593,127 @@ def nlargest(n, iterable, key=None):
592593
result.sort(reverse=True)
593594
return [elem for (k, order, elem) in result]
594595

596+
597+
class MinHeap:
598+
"""Min-heap: a priority queue that serves its smallest item first.
599+
600+
Only the ``<`` operator is used to compare items.
601+
602+
If *iterable* is given, the heap is initialized from its items in linear
603+
time; otherwise it starts empty.
604+
605+
>>> h = MinHeap([3, 1, 2])
606+
>>> h.push(0)
607+
>>> [h.pop() for _ in range(len(h))]
608+
[0, 1, 2, 3]
609+
"""
610+
611+
def __init__(self, iterable=None):
612+
if iterable is None:
613+
self._queue = []
614+
else:
615+
self._queue = list(iterable)
616+
heapify(self._queue)
617+
618+
def push(self, item):
619+
"""Push item onto the heap, maintaining the heap invariant."""
620+
heappush(self._queue, item)
621+
622+
def pop(self):
623+
"""Pop and return the smallest item, maintaining the heap invariant.
624+
625+
Raises IndexError if the heap is empty.
626+
"""
627+
return heappop(self._queue)
628+
629+
def pushpop(self, item):
630+
"""Push item on the heap, then pop and return the smallest item.
631+
632+
The combined action runs more efficiently than a push() followed by
633+
a separate call to pop().
634+
"""
635+
return heappushpop(self._queue, item)
636+
637+
def replace(self, item):
638+
"""Pop and return the smallest item, and also push the new item.
639+
640+
The heap size doesn't change. Raises IndexError if the heap is
641+
empty. This is more efficient than a pop() followed by a push(),
642+
and can be more appropriate when using a fixed-size heap. Note that
643+
the value returned may be larger than item.
644+
"""
645+
return heapreplace(self._queue, item)
646+
647+
def __len__(self):
648+
return len(self._queue)
649+
650+
def __bool__(self):
651+
return bool(self._queue)
652+
653+
def __repr__(self):
654+
return f'{type(self).__name__}({self._queue!r})'
655+
656+
657+
class MaxHeap:
658+
"""Max-heap: a priority queue that serves its largest item first.
659+
660+
Only the ``<`` operator is used to compare items.
661+
662+
If *iterable* is given, the heap is initialized from its items in linear
663+
time; otherwise it starts empty.
664+
665+
>>> h = MaxHeap([1, 3, 2])
666+
>>> h.push(4)
667+
>>> [h.pop() for _ in range(len(h))]
668+
[4, 3, 2, 1]
669+
"""
670+
671+
def __init__(self, iterable=None):
672+
if iterable is None:
673+
self._queue = []
674+
else:
675+
self._queue = list(iterable)
676+
heapify_max(self._queue)
677+
678+
def push(self, item):
679+
"""Push item onto the heap, maintaining the heap invariant."""
680+
heappush_max(self._queue, item)
681+
682+
def pop(self):
683+
"""Pop and return the largest item, maintaining the heap invariant.
684+
685+
Raises IndexError if the heap is empty.
686+
"""
687+
return heappop_max(self._queue)
688+
689+
def pushpop(self, item):
690+
"""Push item on the heap, then pop and return the largest item.
691+
692+
The combined action runs more efficiently than a push() followed by
693+
a separate call to pop().
694+
"""
695+
return heappushpop_max(self._queue, item)
696+
697+
def replace(self, item):
698+
"""Pop and return the largest item, and also push the new item.
699+
700+
The heap size doesn't change. Raises IndexError if the heap is
701+
empty. This is more efficient than a pop() followed by a push(),
702+
and can be more appropriate when using a fixed-size heap. Note that
703+
the value returned may be smaller than item.
704+
"""
705+
return heapreplace_max(self._queue, item)
706+
707+
def __len__(self):
708+
return len(self._queue)
709+
710+
def __bool__(self):
711+
return bool(self._queue)
712+
713+
def __repr__(self):
714+
return f'{type(self).__name__}({self._queue!r})'
715+
716+
595717
# If available, use C implementation
596718
try:
597719
from _heapq import *

Lib/test/test_heapq.py

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import doctest
66

77
from test.support import import_helper
8-
from unittest import TestCase, skipUnless
8+
from unittest import TestCase, mock, skipUnless
99
from operator import itemgetter
1010

1111
py_heapq = import_helper.import_fresh_module('heapq', blocked=['_heapq'])
@@ -637,5 +637,178 @@ class TestErrorHandlingC(TestErrorHandling, TestCase):
637637
module = c_heapq
638638

639639

640+
#==============================================================================
641+
# Object-oriented interface: MinHeap and MaxHeap.
642+
#
643+
# The classes add no new heap algorithm; they are thin wrappers that delegate
644+
# to the module-level functions. They are tested in two ways:
645+
#
646+
# * Functional "sanity check" tests, run against both the pure-Python and the
647+
# C classes (like the rest of this file). They are end-to-end, so a
648+
# mis-wired method (say, a MaxHeap that pushed with the min-heap routine)
649+
# changes the observable output and is caught even for the C class.
650+
#
651+
# * Delegation tests that patch the module-level functions and check that each
652+
# method forwards to the right one. These only apply to the pure-Python
653+
# class, since the C class calls the underlying routines directly and cannot
654+
# be intercepted; its wiring is covered by the functional tests instead.
655+
656+
657+
class HeapClassSanityTest:
658+
"""Functional sanity checks shared by the MinHeap/MaxHeap test cases.
659+
660+
Subclasses set:
661+
662+
* *cls* -- the heap class under test (e.g. ``py_heapq.MinHeap``)
663+
* *ordered* -- a small list already satisfying the heap invariant
664+
* *reverse* -- ``True`` for max-heaps (the order ``pop()`` yields items in)
665+
"""
666+
667+
def in_heap_order(self, data):
668+
return sorted(data, reverse=self.reverse)
669+
670+
def test_construct_empty(self):
671+
h = self.cls()
672+
self.assertEqual(len(h), 0)
673+
self.assertFalse(h)
674+
675+
def test_construct_from_iterable(self):
676+
data = [random.randrange(1000) for _ in range(100)]
677+
# Accept any iterable, not just a sequence.
678+
h = self.cls(iter(data))
679+
self.assertEqual(len(h), len(data))
680+
drained = [h.pop() for _ in range(len(data))]
681+
self.assertEqual(drained, self.in_heap_order(data))
682+
683+
def test_push_then_pop_sorts(self):
684+
data = [random.randrange(1000) for _ in range(100)]
685+
h = self.cls()
686+
for item in data:
687+
self.assertIsNone(h.push(item))
688+
self.assertEqual(len(h), len(data))
689+
drained = [h.pop() for _ in range(len(data))]
690+
self.assertEqual(drained, self.in_heap_order(data))
691+
692+
def test_len_and_bool(self):
693+
h = self.cls()
694+
self.assertEqual(len(h), 0)
695+
self.assertFalse(h)
696+
h.push(1)
697+
self.assertEqual(len(h), 1)
698+
self.assertTrue(h)
699+
700+
def test_pop_empty_raises(self):
701+
h = self.cls()
702+
self.assertRaises(IndexError, h.pop)
703+
704+
def test_pushpop(self):
705+
h = self.cls(self.ordered)
706+
result = h.pushpop(2)
707+
expected = self.in_heap_order(list(self.ordered) + [2])[0]
708+
self.assertEqual(result, expected)
709+
self.assertEqual(len(h), len(self.ordered))
710+
711+
def test_replace(self):
712+
h = self.cls(self.ordered)
713+
expected = self.in_heap_order(self.ordered)[0]
714+
new_item = -1000 if self.reverse else 1000
715+
self.assertEqual(h.replace(new_item), expected)
716+
self.assertEqual(len(h), len(self.ordered))
717+
718+
def test_repr(self):
719+
h = self.cls(self.ordered)
720+
self.assertEqual(repr(h), f'{self.cls.__name__}({self.ordered!r})')
721+
722+
723+
class TestMinHeapPython(HeapClassSanityTest, TestCase):
724+
cls = py_heapq.MinHeap
725+
ordered = [1, 2, 3]
726+
reverse = False
727+
728+
729+
@skipUnless(c_heapq, 'requires _heapq')
730+
class TestMinHeapC(HeapClassSanityTest, TestCase):
731+
cls = c_heapq.MinHeap if c_heapq else None
732+
ordered = [1, 2, 3]
733+
reverse = False
734+
735+
736+
class TestMaxHeapPython(HeapClassSanityTest, TestCase):
737+
cls = py_heapq.MaxHeap
738+
ordered = [3, 2, 1]
739+
reverse = True
740+
741+
742+
@skipUnless(c_heapq, 'requires _heapq')
743+
class TestMaxHeapC(HeapClassSanityTest, TestCase):
744+
cls = c_heapq.MaxHeap if c_heapq else None
745+
ordered = [3, 2, 1]
746+
reverse = True
747+
748+
749+
class HeapClassDelegationTest:
750+
"""Check that each method forwards to the right module-level function.
751+
752+
Delegation only makes sense for the pure-Python class, so these tests
753+
always patch ``py_heapq``.
754+
755+
Subclasses set *cls* and the names of the functions each method is
756+
expected to forward to (*heapify_func*, *push_func*, *pop_func*,
757+
*pushpop_func* and *replace_func*).
758+
"""
759+
760+
def test_init_delegates_to_heapify(self):
761+
with mock.patch.object(py_heapq, self.heapify_func) as m:
762+
h = self.cls([3, 1, 2])
763+
m.assert_called_once_with(h._queue)
764+
765+
def test_init_empty_does_not_heapify(self):
766+
with mock.patch.object(py_heapq, self.heapify_func) as m:
767+
self.cls()
768+
m.assert_not_called()
769+
770+
def test_push_delegates(self):
771+
h = self.cls()
772+
with mock.patch.object(py_heapq, self.push_func) as m:
773+
h.push(42)
774+
m.assert_called_once_with(h._queue, 42)
775+
776+
def test_pop_delegates(self):
777+
h = self.cls([1, 2, 3])
778+
with mock.patch.object(py_heapq, self.pop_func) as m:
779+
h.pop()
780+
m.assert_called_once_with(h._queue)
781+
782+
def test_pushpop_delegates(self):
783+
h = self.cls([1, 2, 3])
784+
with mock.patch.object(py_heapq, self.pushpop_func) as m:
785+
h.pushpop(42)
786+
m.assert_called_once_with(h._queue, 42)
787+
788+
def test_replace_delegates(self):
789+
h = self.cls([1, 2, 3])
790+
with mock.patch.object(py_heapq, self.replace_func) as m:
791+
h.replace(42)
792+
m.assert_called_once_with(h._queue, 42)
793+
794+
795+
class TestMinHeapDelegation(HeapClassDelegationTest, TestCase):
796+
cls = py_heapq.MinHeap
797+
heapify_func = 'heapify'
798+
push_func = 'heappush'
799+
pop_func = 'heappop'
800+
pushpop_func = 'heappushpop'
801+
replace_func = 'heapreplace'
802+
803+
804+
class TestMaxHeapDelegation(HeapClassDelegationTest, TestCase):
805+
cls = py_heapq.MaxHeap
806+
heapify_func = 'heapify_max'
807+
push_func = 'heappush_max'
808+
pop_func = 'heappop_max'
809+
pushpop_func = 'heappushpop_max'
810+
replace_func = 'heapreplace_max'
811+
812+
640813
if __name__ == "__main__":
641814
unittest.main()

0 commit comments

Comments
 (0)