diff --git a/names/binary_search_tree.py b/names/binary_search_tree.py new file mode 100644 index 000000000..ed595097d --- /dev/null +++ b/names/binary_search_tree.py @@ -0,0 +1,146 @@ +""" +Binary search trees are a data structure that enforce an ordering over +the data they store. That ordering in turn makes it a lot more efficient +at searching for a particular piece of data in the tree. + +This part of the project comprises two days: +1. Implement the methods `insert`, `contains`, `get_max`, and `for_each` + on the BSTNode class. +2. Implement the `in_order_print`, `bft_print`, and `dft_print` methods + on the BSTNode class. +""" +import queue + +class BSTNode: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + + # Insert the given value into the tree + def insert(self, value): + # Case 1: value is less than self.value + if value < self.value: + if self.left: + # Insert into left subtree + self.left.insert(value) + else: + # Create new node + self.left = BSTNode(value) + + # Case 2: value is greater than or equal to self.value + else: + if self.right: + # Insert into right subtree + self.right.insert(value) + else: + self.right = BSTNode(value) + + # Return True if the tree contains the value + # False if it does not + def contains(self, target): + if target == self.value: + # target found, return True + return True + elif target < self.value: + # Less than, search left subtree + if self.left: + return self.left.contains(target) + else: + return False + elif target >= self.value: + # Greater than or equal, search right subtree + if self.right: + return self.right.contains(target) + else: + return False + + # Return the maximum value found in the tree + def get_max(self): + # The right-most node will be max, find it and return the value + if self.right: + return self.right.get_max() + else: + return self.value + + # Call the function `fn` on the value of each node + def for_each(self, fn, order="inorder"): + """ + Calls a function (fn) on all values in this binary search tree. + + order (str): The order in which to traverse the tree. Options are + "inorder", "preorder" and "postorder" + """ + if order == "inorder": + # Call fn on left subtree, then this node, then right subtree + if self.left: + self.left.for_each(fn, order) + fn(self.value) + if self.right: + self.right.for_each(fn, order) + elif order == "preorder": + # Call fn on this node, then left subtree and right subtree + fn(self.value) + if self.left: + self.left.for_each(fn, order) + if self.right: + self.right.for_each(fn, order) + elif order == "postorder": + # Call fn on left subtree and right subtree, then this node + if self.left: + self.left.for_each(fn, order) + if self.right: + self.right.for_each(fn, order) + fn(self.value) + else: + raise Warning(f'Unrecognized order parameter: "{order}"\n\tOptions are "inorder", "preorder" and "postorder"') + + # Part 2 ----------------------- + + # Print all the values in order from low to high + # Hint: Use a recursive, depth first traversal + def in_order_print(self, node): + self.for_each(fn=lambda x: print(x), order="inorder") + + # Print the value of every node, starting with the given node, + # in an iterative breadth first traversal + def bft_print(self, node): + # set up a queue to hold items as we traverse + q = queue.Queue() + q.put(node) + # while there are items in the queue + while q.qsize() > 0: + # get and print the next node + current = q.get() + print(current.value) + # enqueue the left and right children of that node + if current.left: + q.put(current.left) + if current.right: + q.put(current.right) + + # Print the value of every node, starting with the given node, + # in an iterative depth first traversal + def dft_print(self, node): + # store nodes in a stack as we traverse + stack = [node] + while len(stack) > 0: + # pop off next item and print it + current = stack.pop() + print(current.value) + # add left and right children of that node to the stack + if current.right: + stack.append(current.right) + if current.left: + stack.append(current.left) + + # Stretch Goals ------------------------- + # Note: Research may be required + + # Print Pre-order recursive DFT + def pre_order_dft(self, node): + self.for_each(fn=lambda x: print(x), order="preorder") + + # Print Post-order recursive DFT + def post_order_dft(self, node): + self.for_each(fn=lambda x: print(x), order="postorder") diff --git a/names/names.py b/names/names.py index ea158997f..833e9d467 100644 --- a/names/names.py +++ b/names/names.py @@ -1,3 +1,4 @@ +from binary_search_tree import BSTNode import time start_time = time.time() @@ -10,13 +11,42 @@ names_2 = f.read().split("\n") # List containing 10000 names f.close() -duplicates = [] # Return the list of duplicates in this data structure +# original file ran in 6.3 seconds on my machine +# original solution runs in 1.9 seconds after putting it in this function (interesting) +def original_brute_force(): + duplicates = [] # Return the list of duplicates in this data structure + for name_1 in names_1: + for name_2 in names_2: + if name_1 == name_2: + duplicates.append(name_1) + return duplicates -# Replace the nested for loops below with your improvements -for name_1 in names_1: - for name_2 in names_2: - if name_1 == name_2: - duplicates.append(name_1) +# runtime on my machine: 0.09 seconds +def binary_search_tree_approach(): + duplicates = [] + + # insert one of the lists into a binary search tree + # uses the binary search tree class we built earlier this week + bst = BSTNode(names_2[0]) + for name in names_2: + bst.insert(name) + + # search bst for matching names + for name in names_1: + if bst.contains(name): + duplicates.append(name) + + return duplicates + +# runtime on my machine: 0.005 seconds +def stretch_using_set(): + # add both lists to a set, then get the intersection + # convert back to a list and return + return list(set(names_1) & set(names_2)) + +# choose which function runs here +# duplicates = binary_search_tree_approach() +duplicates = stretch_using_set() end_time = time.time() print (f"{len(duplicates)} duplicates:\n\n{', '.join(duplicates)}\n\n") diff --git a/reverse/reverse.py b/reverse/reverse.py index 6116252d1..357790f78 100644 --- a/reverse/reverse.py +++ b/reverse/reverse.py @@ -39,4 +39,17 @@ def contains(self, value): return False def reverse_list(self, node, prev): - pass + # question: why are we passing in node and prev when this is already + # called on an isntance of a linked list? + + prev = None + current = self.head + while current is not None: + # save next and reverse current node + next = current.get_next() + current.set_next(prev) + # move pointers for next loop + prev = current + current = next + # Update head pointer to the last node we saw + self.head = prev diff --git a/ring_buffer/ring_buffer.py b/ring_buffer/ring_buffer.py index 37e9fb0dd..c3a1c2b41 100644 --- a/ring_buffer/ring_buffer.py +++ b/ring_buffer/ring_buffer.py @@ -1,9 +1,19 @@ class RingBuffer: def __init__(self, capacity): - pass + self.capacity = capacity + self.oldest = 0 + # By initializing the array here, we never hit the doubling cost for + # inserting to a list later, ensuring append is O(1) + self.storage = [None] * self.capacity def append(self, item): - pass + # save the item to the storage array + self.storage[self.oldest] = item + # increment the oldest counter, wrap to 0 if needed + self.oldest += 1 + if self.oldest == self.capacity: + self.oldest = 0 def get(self): - pass \ No newline at end of file + # return all non-None entries as described in README.md + return [x for x in self.storage if x is not None]