diff --git a/README.md b/README.md index 2e2c87e..6c727e7 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,13 @@ A catalogue of data structures implementation + algorithms and coding problems a ## Linked Lists -- [Doubly linked list and all operations](linked-lists/doubly_linked_list.py) -- [Find whether linked list has loop](linked-lists/find_loop.ipynb) +- [Doubly linked list and all operations](linked-lists/doubly_linked_list.md) +- [Find whether linked list has loop](linked-lists/find_loop.md) - [Linked list and all operations](linked-lists/linked_list.py) -- [LRU Cache implementation](linked-lists/least_recently_used_cache.ipynb) -- [Remove kth node from end](linked-lists/remove_kth_node_from_end.ipynb) -- [Reverse linked list](linked-lists/reverse_linked_list.ipynb) -- [Sort linked list in O(N log N) time and constant space](linked-lists/sorting_linked_list.ipynb) +- [LRU Cache implementation](linked-lists/least_recently_used_cache.md) +- [Remove kth node from end](linked-lists/remove_kth_node_from_end.md) +- [Reverse linked list](linked-lists/reverse_linked_list.md) +- [Sort linked list in O(N log N) time and constant space](linked-lists/sorting_linked_list.md) ## Recursion diff --git a/linked-lists/doubly_linked_list.md b/linked-lists/doubly_linked_list.md new file mode 100644 index 0000000..d9a562a --- /dev/null +++ b/linked-lists/doubly_linked_list.md @@ -0,0 +1,146 @@ +```python +class Node: + def __init__(self, value): + self.value = value + self.prev = None + self.next = None + + +class DoubleLinkedList: + def __init__(self): + self.head = None + self.tail = None + + def set_head(self, node): + """O(1) time | O(1) space""" + if self.head is None: + self.head = node + self.tail = node + return + self.insert_before(self.head, node) + + def set_tail(self, node): + """O(1) time | O(1) space""" + if self.tail is None: + self.set_head(node) + return + self.insert_after(self.tail, node) + + def insert_before(self, node, node_to_insert): + """O(1) time | O(1) space""" + if node_to_insert == self.head and node_to_insert == self.tail: + return + # first, remove node if it exists + self.remove(node_to_insert) + node_to_insert.prev = node.prev + node_to_insert.next = node + + if node.prev is None: + self.head = node_to_insert + else: + node.prev.next = node_to_insert + node.prev = node_to_insert + + def insert_after(self, node, node_to_insert): + """O(1) time | O(1) space""" + if node_to_insert == self.head and node_to_insert == self.tail: + return + # first remove the node to insert if it currently exists in the linked list + self.remove(node_to_insert) + node_to_insert.prev = node + node_to_insert.next = node.next + + if node.next is None: + self.tail = node_to_insert + else: + node.prev.next = node_to_insert + node.next = node_to_insert + + def insert_at_position(self, position, node_to_insert): + """O(1) space | O(p) time - since we are iterating up until position p""" + if position == 1: + self.set_head(node_to_insert) + return + node = self.head + current_position = 1 + while node is not None and current_position != position: + node = node.next + current_position += 1 + if node is not None: + self.insert_before(node, node_to_insert) + else: + # node is None so we are at the tail + self.set_tail(node_to_insert) + + def remove_nodes_with_value(self, value): + """O(n) time | O(1) space""" + node = self.head + while node is not None: + # temporary save the current node, to not lose it while we check + # if it needs to be removed + node_to_remove = node + # update the node to be the next node + node = node.next + if node_to_remove.value == value: + self.remove(node_to_remove) + + def remove(self, node): + """O(1) time | O(1) space""" + if node == self.head: + self.head = self.head.next + if node == self.tail: + self.tail = self.tail.prev + self.update_node_bindings(node) + + def contains_node_with_value(self, value): + """O(n) time | O(1) space""" + node = self.head + while node is not None and node.value != value: + node = node.next + return node is not None + + def update_node_bindings(self, node): + """Update the pointers of a node when a node is removed from the doubly linked list. + """ + # the order in which you remove pointers matters. otherwise, we might delete nodes + # and never recover them forever. + if node.prev is not None: + node.prev.next = node.next + if node.next is not None: + node.next.prev = node.prev + node.prev = None + node.next = None +``` + + +```python + +doubly_linked_list = DoubleLinkedList() +first_node = Node(1) +second_node = Node(2) +third_node = Node(3) + +doubly_linked_list.set_head(first_node) +doubly_linked_list.set_tail(third_node) +doubly_linked_list.insert_before(third_node, second_node) +another_node = Node(7) +doubly_linked_list.insert_after(third_node, another_node) + +``` + + +```python +doubly_linked_list.head.value +``` + + + + + 1 + + + + +```python + +``` diff --git a/linked-lists/find_loop.md b/linked-lists/find_loop.md new file mode 100644 index 0000000..1a78840 --- /dev/null +++ b/linked-lists/find_loop.md @@ -0,0 +1,47 @@ +### Looped linked list +Write a function that takes in the head of a linked list and returns the node from which the loop originates, in constant space. + +Sample input: +``` +1 -> 2 -> 3 -> 4 + ^ V + 6 <- 5 +``` + +Sample output: +``` +3 -> 4 +| | +6 <- 5 +``` + + +```python +class LinkedList: + def __init__(self, value): + self.value = value + self.next = None + +def findLoop(head): + """Use two pointers: Traverse the list with one iterating once, and the other jumping one node. + They eventually overlap at a point D, where the distance is equivalent to start of the list to the point where the loop is. + We can therefore obtain that loop point by taking the first pointer back to the head, then iterating both pointers one step until they converge at that point. + + Time: O(n) | Space: O(1), where n = number of nodes in linked list""" + first = head.next + second = head.next.next + + while first != second: + first = first.next + second = second.next.next + first = head + while first != second: + first = first.next + second = second.next + return first +``` + + +```python + +``` diff --git a/linked-lists/least_recently_used_cache.md b/linked-lists/least_recently_used_cache.md new file mode 100644 index 0000000..8fccb60 --- /dev/null +++ b/linked-lists/least_recently_used_cache.md @@ -0,0 +1,125 @@ +## Problem +Implement a class to define an LRU(Least Recently Used) Cache. The class should allow for insertion of key-value pairs, retrieve a value using a key, and getting the most recent key. +When a key/value is inserted, the key should become the most recently used key. + +- The LRUCache should store a maximum capacity (max_size) which indicates the maximum key/value pairs the cache can hold at once. It will is also be passed when the class is instantiated. + +- If a key/value pair is added to the cache when the maximum capacity is reached, the cache should evict the least recently used key/value pair. + +- If inserting a key/value pair that already exists in the cache, the cache should only update the value and not remove the key/value pair. + +- If you attempt to retrieve a value of a key that's not in the cache should return a null value. + + + +```python +class LRUCache: + def __init__(self, max_size): + self.cache = {} + self.current_size = 0 + self.max_size = max_size or 1 + self.doubly_linked_list = DoublyLinkedList() + + def evict_least_recent(self): + key_to_remove = self.doubly_linked_list.tail.key + # remove tail from doubly linked list + self.doubly_linked_list.remove_tail() + # remove key from hash table pointing to the former tail we just removed + del self.cache[key_to_remove] + + def get(self, key): + """O(1) time | O(1) space.""" + + node = self.cache.get(key, None) + if not node: + return None + self.update_most_recent(node) + return node.value + + def get_most_recent_key(self): + """O(1) time | O(1) space.""" + return self.doubly_linked_list.head.key + + def insert(self, key, value): + """O(1) time | O(1) space.""" + + if key not in self.cache: + # If no capacity, evict tail + if self.current_size == self.max_size: + self.evict_least_recent() + else: + self.current_size += 1 + # Put new key in cache to point to new node + self.cache[key] = DoublyLinkedListNode(key, value) + else: + # Update already existing node + self.update_key(key, value) + # Update the doubly linked list + self.update_most_recent(self.cache[key]) + + def update_key(self, key, value): + # update existing key with new value + if key not in self.cache: + raise Exception("The provided key is not in the cache!") + self.cache[key].value = value + + def update_most_recent(self, node): + self.doubly_linked_list.set_head(node) + + +class DoublyLinkedList: + def __init__(self): + self.head = None + self.tail = None + + def set_head(self, node): + if self.head == node: + return + elif self.head is None: + self.head = node + self.tail = node + elif self.head == self.tail: + self.head.previous = node + node.next = self.head + self.head = node + else: + if self.tail == node: + self.remove_tail() + node.remove_bindings() + self.head.previous = node + node.next = self.head + self.head = node + + def remove_tail(self): + if self.tail is None: + return + if self.tail == self.head: + self.head = None + self.tail = None + return + + # Update new tail to be previous node, & set tail to point to Null value. + self.tail = self.tail.previous + self.tail.next = None + + +class DoublyLinkListNode: + def __init__(self, key, value): + self.key = key + self.value = value + self.next = None + self.previous = None + + def remove_bindings(self): + if self.previous is not None: + self.previous.next = self.next + if self.next is not None: + self.next.previous = self.previous + self.previous = None + self.next = None +``` + + +```python + +``` diff --git a/linked-lists/remove_kth_node_from_end.md b/linked-lists/remove_kth_node_from_end.md new file mode 100644 index 0000000..4c04b3e --- /dev/null +++ b/linked-lists/remove_kth_node_from_end.md @@ -0,0 +1,103 @@ +## Problem +Write a function that takes in the head of a singly linked list, and an integer k (assume the list has at least k nodes). The function should remove the kth node from the end of the list. + +Every node has a value property to store its value and a next property to point to the next node in the linked list. + +Sample input: 1 -> 2 -> 3 -> 4 -> 5, 2 +Sample output: 1 -> 2 -> 3 -> 5 + + +```python +class LinkedList: + def __init__(self, value): + self.value = value + self.next = None + +def remove_kth_node_from_end(head, k): + # Space: O(1) | Time: O(n) + counter = 1 + first = head + second = head + + while counter <= k: + second = second.next + counter += 1 + + if second is None: + # we're removing the head (first node) + head.value = head.next.value + head.next = head.next.next + + while second.next is not None: + first = first.next + second = second.next + # remove the kth node by skipping it (garbage collected) + first.next = first.next.next +``` + + +```python +class LinkedList: + def __init__(self, value): + self.value = value + self.next = None + + def add_many(self, values): + # utility function to add many values in a linked list + current = self + while current.next is not None: + current = current.next + for value in values: + current.next = LinkedList(value) + current = current.next + return self + + def get_nodes_in_array(self): + current = self + nodes = [] + while current is not None: + nodes.append(current.value) + current = current.next + return nodes +``` + + +```python +li = LinkedList(0).add_many([1, 2, 3, 4, 5, 6, 7]) +remove_kth_node_from_end(li, 4) +li.get_nodes_in_array() +``` + + + + + [0, 1, 2, 3, 5, 6, 7] + + + + +```python +# feel free to run these unit tests +import unittest + +class TestLinkedListKthRemoval(unittest.TestCase, LinkedList): + def test_case_0(self): + linked_list = LinkedList(0).add_many(list(range(1,9))) + result = LinkedList(0).add_many(list(range(1, 8))) + remove_kth_node_from_end(linked_list, 1) + self.assertEqual(linked_list.get_nodes_in_array(), result.get_nodes_in_array()) + + def test_case_1(self): + test = LinkedList(0).add_many([1, 2, 3, 4, 5, 6, 7, 8, 9]) + remove_kth_node_from_end(test, 2) + expected = LinkedList(0).add_many([1, 2, 3, 4, 5, 6, 7, 9]) + self.assertEqual(test.get_nodes_in_array(), expected.get_nodes_in_array()) + +if __name__ == "__main__": + unittest.main(argv=['first-arg-is-ignored'], exit=False) +``` + + +```python + +``` diff --git a/linked-lists/reverse_linked_list.md b/linked-lists/reverse_linked_list.md new file mode 100644 index 0000000..bcd117d --- /dev/null +++ b/linked-lists/reverse_linked_list.md @@ -0,0 +1,23 @@ +```python +### Reverse a linked-list, given the head of the linkedlist. Return the new head. +def reverse(head): + """Reverse In place. + O(1) space | O(n) time, where n = number of nodes in the linked list. + """ + previous_node = None + current_node = head + + while current_node is not None: + next_node = current_node.next + current_node.next = previous_node + previous_node = current_node + current_node = next_node + + # we return the previous node because the current is now None (new tail) + return previous_node +``` + + +```python + +``` diff --git a/linked-lists/sorting_linked_list.md b/linked-lists/sorting_linked_list.md index fd01567..e06e02a 100644 --- a/linked-lists/sorting_linked_list.md +++ b/linked-lists/sorting_linked_list.md @@ -1,4 +1,3 @@ - ## Problem Given a linked list, sort it in **O(n log N)** time and **constant** space. For example: the linked list 4 -> 1 -> -3 -> 99 should become @@ -134,11 +133,6 @@ traverse(sorted_list) -```python - -``` - - ```python ```