Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 100 additions & 3 deletions heaps/heap_sort.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,105 @@
''' i tried to import MinHeap from the other file but it wasn't passing pytest tests

this is what i was writing:
from min_heap import MinHeap

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could either do

from .min_heap import MinHeap

or

from heaps.min_heap import MinHeap


anyway i copy and pasted the functions from previous work, but removed the Heap Node part since these tests just look at a list of numbers.

see line 76 for the heap_sort function!

'''

class MinHeap:

def __init__(self):
self.store = []

def add(self, key):
self.store.append(key)
last_index = len(self.store) - 1
self.heap_up(last_index)
return

def remove(self):
if self.empty():
return
self.swap(0, -1)
result = self.store.pop()
if not self.empty():
self.heap_down(0)
return result

def empty(self):
if len(self.store) == 0:
return True
return False

def heap_up(self, index):
parent_index = int((index - 1) / 2)
current = self.store[index]
parent = self.store[parent_index]

while current < parent:
self.swap(index, parent_index)
index = parent_index
parent_index = int((index - 1) / 2)
current = self.store[index]
parent = self.store[parent_index]

def has_left_child(self, index):
left_child_index = int((2 * index) + 1)
if left_child_index <= len(self.store) - 1:
return True
return False

def has_right_child(self, index):
right_child_index = int((2 * index) + 2)
if right_child_index <= len(self.store) - 1:
return True
return False

def heap_down(self, index):
if self.has_left_child(index) and (self.store[index] > self.store[int((2 * index) + 1)]):
left_child_index = (2 * index) + 1
self.swap(index, left_child_index)
self.heap_down(left_child_index)

if self.has_right_child(index) and (self.store[index] > self.store[int((2 * index) + 2)]):
right_child_index = (2 * index) + 2
self.swap(index, right_child_index)
self.heap_down(right_child_index)
return

def swap(self, index_1, index_2):
temp = self.store[index_1]
self.store[index_1] = self.store[index_2]
self.store[index_2] = temp

def heap_sort(list):
""" This method uses a heap to sort an array.
Time Complexity: ?
Space Complexity: ?
Time Complexity: ? O(n)
Space Complexity: ? O(n)
Comment on lines +80 to +81

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 The theoretical best time complexity for a general sorting algorithm (like heap sort) is O(n log n). Here, we can see that we add n things to the heap (which each take O(log n)), then we remove n things from the heap, which again takes O(log n) each, for a total of O(2 n log n) → O(n log n). The space complexity is O(n) due to the internal store that MinHeap uses.

"""
pass
result = []
if not list:
return result

da_heap = MinHeap()

for num in list:
da_heap.add(num)

while len(result) != len(list):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the empty helper

    while not da_heap.empty():

top_of_heap = da_heap.remove()
result.append(top_of_heap)

return result


nums = [5, 27, 3, 16, 50]
print(f"{nums} <---given nums")
result = heap_sort(nums)
print(f"{result} <---result of heap_sort")
expected = [3, 5, 16, 27, 50]
print(f"{expected} <--- expected result")
print(f"do they match? {result == expected}")
75 changes: 58 additions & 17 deletions heaps/min_heap.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,29 @@ def __init__(self):
def add(self, key, value = None):
""" This method adds a HeapNode instance to the heap
If value == None the new node's value should be set to key
Time Complexity: ?
Space Complexity: ?
Time Complexity: ? O(log n)
Space Complexity: ? O(1)
Comment on lines +22 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ Great! In the worst case, the new value we're inserting is the new root of the heap, meaning it would need to move up the full height of the heap (which is log n levels deep). Your implementation of the heap_up helper is iterative, meaning that while we do have to loop log n times, we don't have any recursive stack overhead. So the space complexity is constant (O(1)).

"""
pass
if value == None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 Prefer using is to compare to None

value = key
self.store.append(HeapNode(key, value))
last_index = len(self.store) - 1
self.heap_up(last_index)
return

def remove(self):
""" This method removes and returns an element from the heap
maintaining the heap structure
Time Complexity: ?
Space Complexity: ?
Time Complexity: ? O(log n)
Space Complexity: ? O(1)
Comment on lines +35 to +36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the cost of add mostly comes from heap_up, the main cost of remove comes from heap_down. However, while you heap_up is implemented iteratively, achieving O(1) space complexity (and O(log n) time), your heap_down is implemented recursively. It still at worst must move the repositioned value from the top of the heap to the bottom (O(log n) operations), but for each move, it also makes a recursive call, adding stack space to the complexity. So this is O(log n) for both time and space complexity.

"""
pass


if self.empty():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ Nice use of your own helper method!

return
self.swap(0, -1)
result = self.store.pop()
if not self.empty():
self.heap_down(0)
Comment on lines +42 to +43

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling heap_down even on an empty list shouldn't need to be treated specially. Since an empty list would results in finding that the item has neither a left nor right child, it would essentially do nothing.

return str(result)

def __str__(self):
""" This method lets you print the heap, when you're testing your app.
Expand All @@ -44,11 +53,12 @@ def __str__(self):

def empty(self):
""" This method returns true if the heap is empty
Time complexity: ?
Space complexity: ?
Time complexity: ? O(1)
Space complexity: ? O(1)
Comment on lines +56 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""
pass

if len(self.store) == 0:
return True
return False
Comment on lines +59 to +61

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that an empty list is falsy

        return not self.store


def heap_up(self, index):
""" This helper method takes an index and
Expand All @@ -57,20 +67,51 @@ def heap_up(self, index):
property is reestablished.

This could be **very** helpful for the add method.
Time complexity: ?
Space complexity: ?
Time complexity: ? O(log n)
Space complexity: ? O(1)
Comment on lines +70 to +71

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ Yes, this function is where the complexity in add comes from.

"""
pass
parent_index = int((index - 1) / 2)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using truncating division

        parent_index = (index - 1) // 2

current_key = self.store[index].key
parent_key = self.store[parent_index].key

while current_key < parent_key:
self.swap(index, parent_index)
index = parent_index
parent_index = int((index - 1) / 2)
current_key = self.store[index].key
parent_key = self.store[parent_index].key
Comment on lines +77 to +82

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ Nice use of iteration to avoid incurring extra space complexity.

Notice that this code ends up repeating the calculations for parent_index, current_key, and parent_key. We could rearrange a bit to avoid this:

        while index > 0:
            parent_index = (index - 1) // 2
            current_key = self.store[index].key
            parent_key = self.store[parent_index].key 

            if current_key < parent_key:
                self.swap(index, parent_index)
                index = parent_index


def has_left_child(self, index):
left_child_index = int((2 * index) + 1)
if left_child_index <= len(self.store) - 1:
return True
return False
Comment on lines +86 to +88

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 Performing a boolean check (if condition) then returning a boolean is an anti-pattern. We can return the value of the condition directly.

        return left_child_index <= len(self.store) - 1


def has_right_child(self, index):
right_child_index = int((2 * index) + 2)
if right_child_index <= len(self.store) - 1:
return True
return False

def heap_down(self, index):
""" This helper method takes an index and
moves the corresponding element down the heap if it's
larger than either of its children and continues until
the heap property is reestablished.
time complexity O(log n)
space complexity O(1)
Comment on lines +101 to +102

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 Nice of you to include the complexity here when we forgot to ask for it!

This is where the O(log n) time and space complexity in remove comes from. Since heap_down calls itself recursively, the space complexity will grow with the depth of the stack, which is the height of the heap, or O(log n).

"""
pass
if self.has_left_child(index) and (self.store[index].key > self.store[int((2 * index) + 1)].key):
left_child_index = (2 * index) + 1
self.swap(index, left_child_index)
self.heap_down(left_child_index)
Comment on lines +106 to +107

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before deciding to swap here, we would also need to compare the left child to the right (if present). We only want to swap the one child which is the smallest.

Currently, if the left child were smaller then the parent, but larger than the right, first we would continue swapping the parent down the left subtree, then potentially need to swap the new parent down the right sub tree. Consider the arrangement [3, 2, 1]. 3 > 2 so it would swap with the left child → [2, 3, 1]. But notice the heap property is not re-established since 2 > 1. This code will continue and swap 2 and 1 → [1, 3, 2]. But if we had compare the values of the children first, we could see that 1 < 2 and swap that with 3 directly → [1, 2, 3] which re-establishes the heap in a single swap.



if self.has_right_child(index) and (self.store[index].key > self.store[int((2 * index) + 2)].key):
right_child_index = (2 * index) + 2
self.swap(index, right_child_index)
self.heap_down(right_child_index)
return

def swap(self, index_1, index_2):
""" Swaps two elements in self.store
at index_1 and index_2
Expand Down
4 changes: 2 additions & 2 deletions tests/test_min_heap.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ def test_it_can_remove_nodes_in_proper_order(heap):
heap.add(0, "Donuts")
heap.add(16, "Cookies")


# Act
returned_items = ["Donuts", "Pizza", "Pasta", "Soup", "Cookies", "Cake"]
# assert heap.store == returned_items

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't make a check like this since the store is a list of heap nodes, not just the values. We also wouldn't generally expect the order of the items being returned to match the final retrieved order, since a heap is only loosely ordered. We only know that the smallest (or largest if a max heap) value is at the front, but we can't say with certainty the specific ordering of the remaining items.


for item in returned_items:
assert heap.remove() == item

def test_removing_a_node_from_an_empty_heap_is_none(heap):
assert heap.remove() == None
assert heap.remove() == None