diff --git a/Data_Structures_Answers.md b/Data_Structures_Answers.md index e39545492..3a2a576db 100644 --- a/Data_Structures_Answers.md +++ b/Data_Structures_Answers.md @@ -2,17 +2,57 @@ Add your answers to the questions below. 1. What is the runtime complexity of your ring buffer's `append` method? +``` +if self.current == self.capacity: + self.storage[0] = item # O(1) + self.current = 1 # O(1) +else: + self.storage[self.current] = item # O(1) + self.current += 1 # O(1) +``` + +Both conditions have a runtime of O(2) or const time which is O(1) + 2. What is the space complexity of your ring buffer's `append` function? +The size of self.storage will always be self.capacity + 3. What is the runtime complexity of your ring buffer's `get` method? +Linear time O(n) because Python's filter function creates an iterator that checks every element in self.storage to see if the value is None + +https://docs.python.org/3/library/functions.html#filter + 4. What is the space complexity of your ring buffer's `get` method? +The get method does not create a new list to return. The elements returned are in the self.storage list. 5. What is the runtime complexity of the provided code in `names.py`? +O(n^2) due to nested for loops + 6. What is the space complexity of the provided code in `names.py`? +names_1 is a list with n elements where n is the number of lines in names_1.txt +names_2 is a list with n elements where n is the number of lines in names_2.txt +duplicates is a list with n elements where n is the number of + 7. What is the runtime complexity of your optimized code in `names.py`? +``` +duplicates = [] #O(1) +bst = BinarySearchTree(names_1[0]) #O(1) + +for name1 in names_1[1:]: #O(n-1) + bst.insert(name1) #O(1) + +for name2 in names_2: #O(n) + if bst.contains(name2): #O(log n) + duplicates.append(name2) #O(1) +``` + +Create duplicates list and bst is O(2). Looping through names_1 is O(n). Looping through names_2 and checking if bst contains name2 is O(n log n). The runtime complexity is O(n log n) + 8. What is the space complexity of your optimized code in `names.py`? + +Both names_1 & names_2 are lists where the size is the number of names in names_1.txt and names_1.txt respectively. Then bst is the size of the names_1 list where every element in the list is a node in a BST. Duplicates is the size of the number of duplicate names which at maximum could be the size of the smaller list. \ No newline at end of file diff --git a/names/bst.py b/names/bst.py new file mode 100644 index 000000000..f8e8cb2e7 --- /dev/null +++ b/names/bst.py @@ -0,0 +1,40 @@ +from dll import DoublyLinkedList + + +class BinarySearchTree: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + + def insert(self, value): + if value < self.value: + if self.left == None: + self.left = BinarySearchTree(value) + else: + self.left.insert(value) + else: + if self.right == None: + self.right = BinarySearchTree(value) + else: + self.right.insert(value) + + def contains(self, target): + if self.value == target: + return True + elif target < self.value: + if self.left is not None: + return self.left.contains(target) + else: + return False + elif target > self.value: + if self.right is not None: + return self.right.contains(target) + else: + return False + + def get_max(self): + if self.right is not None: + return self.right.get_max() + else: + return self.value diff --git a/names/dll.py b/names/dll.py new file mode 100644 index 000000000..d6171f31a --- /dev/null +++ b/names/dll.py @@ -0,0 +1,135 @@ +"""Each ListNode holds a reference to its previous node +as well as its next node in the List.""" + + +class ListNode: + def __init__(self, value, prev=None, next=None): + self.value = value + self.prev = prev + self.next = next + + """Wrap the given value in a ListNode and insert it + after this node. Note that this node could already + have a next node it is point to.""" + + def insert_after(self, value): + current_next = self.next + self.next = ListNode(value, self, current_next) + if current_next: + current_next.prev = self.next + + """Wrap the given value in a ListNode and insert it + before this node. Note that this node could already + have a previous node it is point to.""" + + def insert_before(self, value): + current_prev = self.prev + self.prev = ListNode(value, current_prev, self) + if current_prev: + current_prev.next = self.prev + + """Rearranges this ListNode's previous and next pointers + accordingly, effectively deleting this ListNode.""" + + def delete(self): + if self.prev: + self.prev.next = self.next + if self.next: + self.next.prev = self.prev + + +"""Our doubly-linked list class. It holds references to +the list's head and tail nodes.""" + + +class DoublyLinkedList: + def __init__(self, node=None): + self.head = node + self.tail = node + self.length = 1 if node is not None else 0 + + def __len__(self): + return self.length + + def add_to_head(self, value): + new_node = ListNode(value) + self.length += 1 + + if not self.head and not self.tail: # if there's no head or tail. E.g. the list is empty + self.head = new_node + self.head.prev = new_node + self.tail = new_node + else: + new_node.next = self.head + self.head.prev = new_node + self.head = new_node + + def remove_from_head(self): + value = self.head.value + self.delete(self.head) + return value + + def add_to_tail(self, value): + new_node = ListNode(value) + self.length += 1 + + if not self.head and not self.tail: # if there's no head or tail. E.g. the list is empty + self.head = new_node + self.tail = new_node + else: + new_node.prev = self.tail + self.tail.next = new_node + self.tail = new_node + + def remove_from_tail(self): + value = self.tail.value + self.delete(self.tail) + return value + + def move_to_front(self, node): + if node is self.head: + return + + value = node.value + + if node is self.tail: + self.remove_from_tail() + else: + node.delete() + self.length -= 1 + + self.add_to_head(value) + + def move_to_end(self, node): + if node is self.tail: + return + + value = node.value + + if node is self.head: + self.remove_from_head() + self.add_to_tail(value) + else: + node.delete() + self.length -= 1 + self.add_to_tail(value) + + def delete(self, node): + if not self.head and not self.tail: # if there's no head or tail. E.g. the list is empty + return + + self.length -= 1 + if self.head == self.tail: # if there's only one node in the list + self.head = None # head is none + self.tail = None # tail is none + elif self.head == node: # if head is the node + self.head = self.head.next + node.delete() + elif self.tail == node: + self.tail = self.node.prev + node.delete() + else: + node.delete() + + def get_max(self): + pass diff --git a/names/names.py b/names/names.py index 586e8393e..89d32180f 100644 --- a/names/names.py +++ b/names/names.py @@ -1,4 +1,5 @@ import time +from bst import BinarySearchTree start_time = time.time() @@ -10,13 +11,22 @@ names_2 = f.read().split("\n") # List containing 10000 names f.close() +# duplicates = [] +# for name_1 in names_1: +# for name_2 in names_2: +# if name_1 == name_2: +# duplicates.append(name_1) + duplicates = [] -for name_1 in names_1: - for name_2 in names_2: - if name_1 == name_2: - duplicates.append(name_1) +bst = BinarySearchTree(names_1[0]) -end_time = time.time() -print (f"{len(duplicates)} duplicates:\n\n{', '.join(duplicates)}\n\n") -print (f"runtime: {end_time - start_time} seconds") +for name1 in names_1[1:]: + bst.insert(name1) +for name2 in names_2: + if bst.contains(name2): + duplicates.append(name2) + +end_time = time.time() +print(f"{len(duplicates)} duplicates:\n\n{', '.join(duplicates)}\n\n") +print(f"runtime: {end_time - start_time} seconds") diff --git a/ring_buffer/ring_buffer.py b/ring_buffer/ring_buffer.py index 35fd33cac..797121433 100644 --- a/ring_buffer/ring_buffer.py +++ b/ring_buffer/ring_buffer.py @@ -1,11 +1,26 @@ class RingBuffer: - def __init__(self, capacity): - self.capacity = capacity - self.current = 0 - self.storage = [None]*capacity + def __init__(self, capacity): + self.capacity = capacity + self.current = 0 + self.storage = [None]*capacity - def append(self, item): - pass + def append(self, item): + # self.current pointer is at end of self.storage + if self.current == self.capacity: + # assign item to the first element of self.storage + self.storage[0] = item + # set self.current pointer to index 1 + self.current = 1 + else: + # assign item to self.current pointer index of self.storage + self.storage[self.current] = item + # increment self.current pointer + self.current += 1 - def get(self): - pass \ No newline at end of file + # return self.storage with all occurences of None removed + def get(self): + return list(filter(lambda a: a != None, self.storage)) + + # helper debugging method + def debug(self): + return f"Storage: {self.storage}, Current: {self.current}" diff --git a/ring_buffer/test_ring_buffer.py b/ring_buffer/test_ring_buffer.py index 6508c10af..66dcb1f89 100644 --- a/ring_buffer/test_ring_buffer.py +++ b/ring_buffer/test_ring_buffer.py @@ -1,6 +1,7 @@ import unittest from ring_buffer import RingBuffer + class RingBufferTests(unittest.TestCase): def setUp(self): self.buffer = RingBuffer(5) @@ -31,4 +32,4 @@ def test_ring_buffer(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()