From ad7a13f674edf7656dc501aa7ce8e82ea1f22708 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Thu, 16 Apr 2026 16:38:28 +0100 Subject: [PATCH 1/5] Implement O(1) linked list for Sprint 2 --- Sprint-2/implement_linked_list/linked_list.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Sprint-2/implement_linked_list/linked_list.py b/Sprint-2/implement_linked_list/linked_list.py index e69de29..e17b30c 100644 --- a/Sprint-2/implement_linked_list/linked_list.py +++ b/Sprint-2/implement_linked_list/linked_list.py @@ -0,0 +1,47 @@ +class Node: + __slots__ = ("value", "previous", "next") + + def __init__(self, value): + self.value = value + self.previous: "Node | None" = None + self.next: "Node | None" = None + + +class LinkedList: + def __init__(self): + self.head: Node | None = None + self.tail: Node | None = None + + def push_head(self, value) -> Node: + """Add a value to the front of the list. Returns a handle for O(1) removal.""" + node = Node(value) + if self.head is None: + self.head = self.tail = node + else: + node.next = self.head + self.head.previous = node + self.head = node + return node + + def pop_tail(self): + """Remove and return the value at the end of the list.""" + if self.tail is None: + raise IndexError("pop from empty list") + node = self.tail + self.remove(node) + return node.value + + def remove(self, node: Node) -> None: + """Remove a node from the list in O(1) using a handle from push_head""" + if node.previous is not None: + node.previous.next = node.next + else: + self.head = node.next + + if node.next is not None: + node.next.previous = node.previous + else: + self.tail = node.previous + + node.previous = None + node.next = None From 8a5c1934e1dd10c572d3d62316b5ca95c44aa3fd Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Thu, 16 Apr 2026 20:14:12 +0100 Subject: [PATCH 2/5] implement_lru_cache --- Sprint-2/implement_lru_cache/lru_cache.py | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index e69de29..dc1e357 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -0,0 +1,84 @@ +class Node: + __slots__ = ("key", "value", "prev", "next") + + def __init__(self, key, value): + self.key = key + self.value = value + self.prev = None + self.next = None + + +class LruCache: + def __init__(self, limit: int): + if limit < 1: + raise ValueError(f"limit must be at least 1, got {limit}") + + self._limit = limit + self.map: dict = {} + self.head = None + self.tail = None + + def _remove_node(self, node: Node): + """Remove a node from the linked list in O(1) time.""" + if node.prev: + node.prev.next = node.next + else: + self.head = node.next + + if node.next: + node.next.prev = node.prev + else: + self.tail = node.prev + + node.prev = None + node.next = None + + def _add_node_to_head(self, node: Node): + """Add a node to the head of the linked list in O(1) time.""" + node.next = self.head + node.prev = None + + if self.head: + self.head.prev = node + else: + self.tail = node + + self.head = node + + def get(self, key): + """Return the value for key, Non if not present""" + node = self.map.get(key) + if node is None: + return None + + self._touch(node) + return node.value + + def set(self, key, value) -> None: + """Combine value with key, evicting the LRU entry if necessary.""" + node = self.map.get(key) + + if node: + node.value = value + self._touch(node) + else: + if len(self.map) >= self._limit: + self._evict() + + new_node = Node(key, value) + self._add_node_to_head(new_node) + self.map[key] = new_node + + def _touch(self, node) -> None: + """Move an existing node to the head (most-recently-used position).""" + self._remove_node(node) + self._add_node_to_head(node) + + def _evict(self) -> None: + """Remove the least-recently-used entry (tail of the list).""" + if self.tail is None: + return + + lru = self.tail + self._remove_node(lru) + del self.map[lru.key] From 3ee144065bc4df921c706ae612c6faed50521b17 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Thu, 16 Apr 2026 20:31:14 +0100 Subject: [PATCH 3/5] skip list --- Sprint-2/implement_skip_list/skip_list.py | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Sprint-2/implement_skip_list/skip_list.py diff --git a/Sprint-2/implement_skip_list/skip_list.py b/Sprint-2/implement_skip_list/skip_list.py new file mode 100644 index 0000000..7d7c75d --- /dev/null +++ b/Sprint-2/implement_skip_list/skip_list.py @@ -0,0 +1,66 @@ +import math + + +class SkipList: + def __init__(self): + self.data = [] # sorted list + self.skips = [] # list of skip pointers, each is a tuple (index, value) + + def rebuild_skips(self): + """Rebuild the skip pointer so we can jump over sqrt(n) elements.""" + n = len(self.data) + if n == 0: + self.skips = [] + return + + step = int(math.sqrt(n)) or 1 + self.skips = list(range(0, n, step)) + + def _find_position(self, value): + """Find the position to insert value using skip pointers.""" + if not self.data: + return 0 + + # Use skip pointers to find the range where value should be + for i in range(len(self.skips) - 1): + a = self.skips[i] + b = self.skips[i + 1] + if self.data[a] <= value < self.data[b]: + # linear search between a and b + for j in range(a, b): + if self.data[j] >= value: + return j + return b + + # Check the last skip pointer + start = self.skips[-1] + for j in range(start, len(self.data)): + if self.data[j] >= value: + return j + return len(self.data) + + def insert(self, value): + """Insert value into the skip list, maintaining sorted order.""" + pos = self._find_position(value) + + if pos < len(self.data) and self.data[pos] == value: + return # value already exists, do not insert duplicates + self.data.insert(pos, value) + self.rebuild_skips() + + def __contains__(self, value): + if not self.data: + return False + + for i in range(len(self.skips) - 1): + a = self.skips[i] + b = self.skips[i + 1] + if self.data[a] <= value < self.data[b]: + return value in self.data[a:b] + + start = self.skips[-1] + return value in self.data[start::] + + def to_list(self): + """Return the skip list as a regular sorted list.""" + return self.data From 96cb1b1902852d319f2f75e2f638ca7f747c2a1b Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Thu, 16 Apr 2026 20:48:23 +0100 Subject: [PATCH 4/5] fibonacci --- .../fibonacci/fibonacci.py | 10 ++++- .../making_change/making_change.py | 37 +++++++++++-------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Sprint-2/improve_with_caches/fibonacci/fibonacci.py b/Sprint-2/improve_with_caches/fibonacci/fibonacci.py index 60cc667..fb79e72 100644 --- a/Sprint-2/improve_with_caches/fibonacci/fibonacci.py +++ b/Sprint-2/improve_with_caches/fibonacci/fibonacci.py @@ -1,4 +1,10 @@ -def fibonacci(n): +def fibonacci(n, cache=None): + if cache is None: + cache = {} + if n in cache: + return cache[n] if n <= 1: return n - return fibonacci(n - 1) + fibonacci(n - 2) + else: + cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache) + return cache[n] diff --git a/Sprint-2/improve_with_caches/making_change/making_change.py b/Sprint-2/improve_with_caches/making_change/making_change.py index 255612e..ef7ff87 100644 --- a/Sprint-2/improve_with_caches/making_change/making_change.py +++ b/Sprint-2/improve_with_caches/making_change/making_change.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple, Dict def ways_to_make_change(total: int) -> int: @@ -7,26 +7,33 @@ def ways_to_make_change(total: int) -> int: For instance, there are two ways to make a value of 3: with 3x 1 coins, or with 1x 1 coin and 1x 2 coin. """ - return ways_to_make_change_helper(total, [200, 100, 50, 20, 10, 5, 2, 1]) + coins = [200, 100, 50, 20, 10, 5, 2, 1] + cache: Dict[Tuple[int, int], int] = {} + return ways_to_make_change_helper(total, coins, 0, cache) -def ways_to_make_change_helper(total: int, coins: List[int]) -> int: +def ways_to_make_change_helper( + total: int, coins: List[int], coin_index: int, cache: Dict[Tuple[int, int], int] +) -> int: """ Helper function for ways_to_make_change to avoid exposing the coins parameter to callers. """ - if total == 0 or len(coins) == 0: + if total == 0: + return 1 + if total < 0 or coin_index == len(coins): return 0 + key = (total, coin_index) + if key in cache: + return cache[key] + + coin = coins[coin_index] ways = 0 - for coin_index in range(len(coins)): - coin = coins[coin_index] - count_of_coin = 1 - while coin * count_of_coin <= total: - total_from_coins = coin * count_of_coin - if total_from_coins == total: - ways += 1 - else: - intermediate = ways_to_make_change_helper(total - total_from_coins, coins=coins[coin_index+1:]) - ways += intermediate - count_of_coin += 1 + count = 0 + while count * coin <= total: + remaining = total - count * coin + ways += ways_to_make_change_helper(remaining, coins, coin_index + 1, cache) + count += 1 + + cache[key] = ways return ways From 9ca8efe353164fa15c441a4e4779d9121ea698d4 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Thu, 16 Apr 2026 21:06:30 +0100 Subject: [PATCH 5/5] common prefix, count letters --- .../common_prefix/common_prefix.py | 17 ++++++++++------ .../count_letters/count_letters.py | 20 +++++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py index f4839e7..6658cdb 100644 --- a/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py +++ b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py @@ -7,16 +7,21 @@ def find_longest_common_prefix(strings: List[str]): In the event that an empty list, a list containing one string, or a list of strings with no common prefixes is passed, the empty string will be returned. """ + if len(strings) < 2: + return "" + + sorted_strings = sorted(strings) + longest = "" - for string_index, string in enumerate(strings): - for other_string in strings[string_index+1:]: - common = find_common_prefix(string, other_string) - if len(common) > len(longest): - longest = common + + for i in range(len(sorted_strings) - 1): + common = common_prefix(sorted_strings[i], sorted_strings[i + 1]) + if len(common) > len(longest): + longest = common return longest -def find_common_prefix(left: str, right: str) -> str: +def common_prefix(left: str, right: str) -> str: min_length = min(len(left), len(right)) for i in range(min_length): if left[i] != right[i]: diff --git a/Sprint-2/improve_with_precomputing/count_letters/count_letters.py b/Sprint-2/improve_with_precomputing/count_letters/count_letters.py index 62c3ec0..e926ece 100644 --- a/Sprint-2/improve_with_precomputing/count_letters/count_letters.py +++ b/Sprint-2/improve_with_precomputing/count_letters/count_letters.py @@ -2,13 +2,17 @@ def count_letters(s: str) -> int: """ count_letters returns the number of letters which only occur in upper case in the passed string. """ - only_upper = set() - for letter in s: - if is_upper_case(letter): - if letter.lower() not in s: - only_upper.add(letter) - return len(only_upper) + uppers = set() + lowers = set() + for ch in s: + if ch.isupper(): + uppers.add(ch) + elif ch.islower(): + lowers.add(ch) -def is_upper_case(letter: str) -> bool: - return letter == letter.upper() + count = 0 + for i in uppers: + if i.lower() not in lowers: + count += 1 + return count