From 1f45164c0f2875ebe25b76495e0a043403bfd777 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 22 May 2025 12:16:52 +0100 Subject: [PATCH 1/2] Solutions --- .../calculateSumAndProduct.js | 6 ++-- .../findCommonItems/findCommonItems.js | 19 +++++++---- .../hasPairWithSum/hasPairWithSum.js | 16 +++++----- .../removeDuplicates/removeDuplicates.mjs | 32 ++++--------------- .../calculate_sum_and_product.py | 6 ++-- .../find_common_items/find_common_items.py | 19 ++++++----- .../has_pair_with_sum/has_pair_with_sum.py | 16 ++++++---- .../remove_duplicates/remove_duplicates.py | 23 +++++++------ 8 files changed, 65 insertions(+), 72 deletions(-) diff --git a/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js b/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js index ce738c3..b73db76 100644 --- a/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js +++ b/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js @@ -9,9 +9,9 @@ * "product": 30 // 2 * 3 * 5 * } * - * Time Complexity: - * Space Complexity: - * Optimal Time Complexity: + * Time Complexity: O(n) - two loops over all of the input numbers, each of which is O(n). + * Space Complexity: O(1) - uses two numbers of additional space, regardless of input size. + * Optimal Time Complexity: The current implementation is asymptotically optimal - we fundamentally do need to look at every number in order to sum/product them. We could combine the two loops into two to avoid the overhead of tracking a second iteration, but this wouldn't change the complexity. * * @param {Array} numbers - Numbers to process * @returns {Object} Object containing running total and product diff --git a/Sprint-1/JavaScript/findCommonItems/findCommonItems.js b/Sprint-1/JavaScript/findCommonItems/findCommonItems.js index 5619ae5..7d5b0e8 100644 --- a/Sprint-1/JavaScript/findCommonItems/findCommonItems.js +++ b/Sprint-1/JavaScript/findCommonItems/findCommonItems.js @@ -1,14 +1,21 @@ /** * Finds common items between two arrays. * - * Time Complexity: - * Space Complexity: - * Optimal Time Complexity: + * Time Complexity: O(n^3): we effectively have three nested loops - the first over first_sequence, the second over second_sequence, and the third in the "not in" check which may contain the same number of elements as either of the sequences. + * Space Complexity: O(n): in the case of complete overlap we may store all of the elements from the sequences in common_items. + * Optimal Time Complexity: We could optimise this to O(n) by using data structures with O(1) insertion and contains look-up. * * @param {Array} firstArray - First array to compare * @param {Array} secondArray - Second array to compare * @returns {Array} Array containing unique common items */ -export const findCommonItems = (firstArray, secondArray) => [ - ...new Set(firstArray.filter((item) => secondArray.includes(item))), -]; +export const findCommonItems = (firstArray, secondArray) => { + const overlap = new Set(); + const secondSet = new Set(secondArray); + for (const element of firstArray) { + if (secondSet.has(element)) { + overlap.add(element); + } + } + return [...overlap]; +}; diff --git a/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js b/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js index dd2901f..1092026 100644 --- a/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js +++ b/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js @@ -1,20 +1,20 @@ /** * Find if there is a pair of numbers that sum to a given target value. * - * Time Complexity: - * Space Complexity: - * Optimal Time Complexity: + * Time Complexity: O(n^2): two nested loops both iterating across the input numbers. + * Space Complexity: O(1): uses only constant extra storage (to do an addition). + * Optimal Time Complexity: We can optimise this to O(n) by pre-computing a set of the numbers (O(n)), and for each number (O(n)) checking to see if the corresponding number is in the set (O(1)). * * @param {Array} numbers - Array of numbers to search through * @param {number} target - Target sum to find * @returns {boolean} True if pair exists, false otherwise */ export function hasPairWithSum(numbers, target) { - for (let i = 0; i < numbers.length; i++) { - for (let j = i + 1; j < numbers.length; j++) { - if (numbers[i] + numbers[j] === target) { - return true; - } + const numberSet = new Set(numbers); + + for (const number of numbers) { + if (numberSet.has(target - number)) { + return true; } } return false; diff --git a/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs b/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs index dc5f771..b77cb65 100644 --- a/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs +++ b/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs @@ -1,36 +1,18 @@ /** * Remove duplicate values from a sequence, preserving the order of the first occurrence of each value. * - * Time Complexity: - * Space Complexity: - * Optimal Time Complexity: + * Time Complexity: O(n^2) - Two nested loops, each looking over worst-case the whole input list. + * Space Complexity: O(n) - Stores the overlapping items, which may be proportional to all of them. + * Optimal Time Complexity: O(n) - If we use a data structure with O(1) insertion and contains-look-up to track which elements have already been seen. * * @param {Array} inputSequence - Sequence to remove duplicates from * @returns {Array} New sequence with duplicates removed */ export function removeDuplicates(inputSequence) { - const uniqueItems = []; + const uniqueItems = new Set(); - for ( - let currentIndex = 0; - currentIndex < inputSequence.length; - currentIndex++ - ) { - let isDuplicate = false; - for ( - let compareIndex = 0; - compareIndex < uniqueItems.length; - compareIndex++ - ) { - if (inputSequence[currentIndex] === uniqueItems[compareIndex]) { - isDuplicate = true; - break; - } - } - if (!isDuplicate) { - uniqueItems.push(inputSequence[currentIndex]); - } + for (const item of inputSequence) { + uniqueItems.add(item); } - - return uniqueItems; + return [...uniqueItems]; } diff --git a/Sprint-1/Python/calculate_sum_and_product/calculate_sum_and_product.py b/Sprint-1/Python/calculate_sum_and_product/calculate_sum_and_product.py index cfd5cfd..4d7463c 100644 --- a/Sprint-1/Python/calculate_sum_and_product/calculate_sum_and_product.py +++ b/Sprint-1/Python/calculate_sum_and_product/calculate_sum_and_product.py @@ -12,9 +12,9 @@ def calculate_sum_and_product(input_numbers: List[int]) -> Dict[str, int]: "sum": 10, // 2 + 3 + 5 "product": 30 // 2 * 3 * 5 } - Time Complexity: - Space Complexity: - Optimal time complexity: + Time Complexity: O(n) - two loops over all of the input numbers, each of which is O(n). + Space Complexity: O(1) - uses two numbers of additional space, regardless of input size. + Optimal time complexity: The current implementation is asymptotically optimal - we fundamentally do need to look at every number in order to sum/product them. We could combine the two loops into two to avoid the overhead of tracking a second iteration, but this wouldn't change the complexity. """ # Edge case: empty list if not input_numbers: diff --git a/Sprint-1/Python/find_common_items/find_common_items.py b/Sprint-1/Python/find_common_items/find_common_items.py index 478e2ef..e6ee0cc 100644 --- a/Sprint-1/Python/find_common_items/find_common_items.py +++ b/Sprint-1/Python/find_common_items/find_common_items.py @@ -1,5 +1,6 @@ from typing import List, Sequence, TypeVar + ItemType = TypeVar("ItemType") @@ -9,13 +10,15 @@ def find_common_items( """ Find common items between two arrays. - Time Complexity: - Space Complexity: - Optimal time complexity: + Time Complexity: O(n^3): we effectively have three nested loops - the first over first_sequence, the second over second_sequence, and the third in the "not in" check which may contain the same number of elements as either of the sequences. + Space Complexity: O(n): in the case of complete overlap we may store all of the elements from the sequences in common_items. + Optimal time complexity: We could optimise this to O(n) by using data structures with O(1) insertion and contains look-up. """ - common_items: List[ItemType] = [] + common_items: Set[ItemType] = set() + + second_set = set(second_sequence) for i in first_sequence: - for j in second_sequence: - if i == j and i not in common_items: - common_items.append(i) - return common_items + if i in second_set: + common_items.add(i) + + return list(common_items) diff --git a/Sprint-1/Python/has_pair_with_sum/has_pair_with_sum.py b/Sprint-1/Python/has_pair_with_sum/has_pair_with_sum.py index fe2da51..df5def8 100644 --- a/Sprint-1/Python/has_pair_with_sum/has_pair_with_sum.py +++ b/Sprint-1/Python/has_pair_with_sum/has_pair_with_sum.py @@ -7,12 +7,14 @@ def has_pair_with_sum(numbers: List[Number], target_sum: Number) -> bool: """ Find if there is a pair of numbers that sum to a target value. - Time Complexity: - Space Complexity: - Optimal time complexity: + Time Complexity: O(n^2): two nested loops both iterating across the input numbers. + Space Complexity: O(1): uses only constant extra storage (to do an addition). + Optimal time complexity: We can optimise this to O(n) by pre-computing a set of the numbers (O(n)), and for each number (O(n)) checking to see if the corresponding number is in the set (O(1)). """ - for i in range(len(numbers)): - for j in range(i + 1, len(numbers)): - if numbers[i] + numbers[j] == target_sum: - return True + number_set = set(numbers) + + for number in number_set: + if target_sum - number in number_set: + return True + return False diff --git a/Sprint-1/Python/remove_duplicates/remove_duplicates.py b/Sprint-1/Python/remove_duplicates/remove_duplicates.py index c9fdbe8..c4502ed 100644 --- a/Sprint-1/Python/remove_duplicates/remove_duplicates.py +++ b/Sprint-1/Python/remove_duplicates/remove_duplicates.py @@ -7,19 +7,18 @@ def remove_duplicates(values: Sequence[ItemType]) -> List[ItemType]: """ Remove duplicate values from a sequence, preserving the order of the first occurrence of each value. - Time complexity: - Space complexity: - Optimal time complexity: + Time complexity: O(n^2) - Two nested loops, each looking over worst-case the whole input list. + Space complexity: O(n) - Stores the overlapping items, which may be proportional to all of them. + Optimal time complexity: O(n) - If we use a data structure with O(1) insertion and contains-look-up to track which elements have already been seen. """ - unique_items = [] + unique_items = set() + # Python sets don't preserve insertion order, so we need to separately track the correct order. + # Use the set to gate adding but the list for keeping them in the correct order. + ordered_items = [] for value in values: - is_duplicate = False - for existing in unique_items: - if value == existing: - is_duplicate = True - break - if not is_duplicate: - unique_items.append(value) + if value not in unique_items: + unique_items.add(value) + ordered_items.append(value) - return unique_items + return ordered_items From b86e1f39ced9492e59aa33199b4814c3c1a4b15b Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Fri, 6 Jun 2025 14:19:42 +0100 Subject: [PATCH 2/2] Add solutions to sprint 2 exercises --- Sprint-2/implement_linked_list/linked_list.py | 39 +++++++++++++++ Sprint-2/implement_lru_cache/lru_cache.py | 47 +++++++++++++++++++ .../fibonacci/fibonacci.py | 10 ++-- .../making_change/making_change.py | 14 ++++-- .../common_prefix/common_prefix.py | 19 ++++---- .../count_letters/count_letters.py | 16 +++---- 6 files changed, 121 insertions(+), 24 deletions(-) diff --git a/Sprint-2/implement_linked_list/linked_list.py b/Sprint-2/implement_linked_list/linked_list.py index e69de29..f4a3217 100644 --- a/Sprint-2/implement_linked_list/linked_list.py +++ b/Sprint-2/implement_linked_list/linked_list.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass +from typing import Optional + +@dataclass +class Entry[V]: + value: V + next: "Optional[Entry[V]]" = None + previous: "Optional[Entry[V]]" = None + + +class LinkedList[V]: + def __init__(self): + self.head = None + self.tail = None + + def push_head(self, value: V) -> Entry[V]: + old_head = self.head + entry = Entry(value=value, next=self.head) + self.head = entry + if old_head: + old_head.previous = entry + if self.tail is None: + self.tail = entry + return entry + + def pop_tail(self) -> V: + tail = self.tail + self.remove(self.tail) + return tail.value + + def remove(self, entry: Entry[V]): + if entry.next: + entry.next.previous = entry.previous + if entry.previous: + entry.previous.next = entry.next + if self.head == entry: + self.head = entry.next + if self.tail == entry: + self.tail = entry.previous diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py index e69de29..d6c9017 100644 --- a/Sprint-2/implement_lru_cache/lru_cache.py +++ b/Sprint-2/implement_lru_cache/lru_cache.py @@ -0,0 +1,47 @@ +import os +import sys + +from dataclasses import dataclass +from typing import Dict, Optional + +sys.path.append(os.path.dirname(os.path.dirname(__file__))) +from implement_linked_list.linked_list import Entry, LinkedList + + +@dataclass +class KeyValuePair[K, V]: + key: K + value: V + + +class LruCache[K, V]: + def __init__(self, *, limit: int): + if limit < 1: + raise ValueError("limit must be non-negative") + self.limit = limit + self.entries: Dict[K, Entry[KeyValuePair[K, V]]] = dict() + self.list = LinkedList[Entry[KeyValuePair[K, V]]]() + + def get(self, key: K) -> Optional[V]: + entry = self.entries.get(key) + if entry is not None: + self.touch(entry) + return entry.value.value + return None + + def set(self, key: K, value: V): + if len(self.entries) == self.limit: + popped = self.list.pop_tail() + del self.entries[popped.key] + + entry = self.entries.get(key) + if entry is not None: + entry.value = KeyValuePair(key, value) + self.touch(entry) + else: + entry = self.list.push_head(KeyValuePair(key, value)) + self.entries[key] = entry + + def touch(self, entry: Entry[KeyValuePair[K, V]]): + self.list.remove(entry) + self.entries[entry.value.key] = self.list.push_head(entry.value) diff --git a/Sprint-2/improve_with_caches/fibonacci/fibonacci.py b/Sprint-2/improve_with_caches/fibonacci/fibonacci.py index 60cc667..bca8587 100644 --- a/Sprint-2/improve_with_caches/fibonacci/fibonacci.py +++ b/Sprint-2/improve_with_caches/fibonacci/fibonacci.py @@ -1,4 +1,8 @@ +_fibonacci_cache = {0: 0, 1: 1} + def fibonacci(n): - if n <= 1: - return n - return fibonacci(n - 1) + fibonacci(n - 2) + if n in _fibonacci_cache: + return _fibonacci_cache[n] + value = fibonacci(n - 1) + fibonacci(n - 2) + _fibonacci_cache[n] = value + return value 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..f0dcb22 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 Tuple def ways_to_make_change(total: int) -> int: @@ -7,16 +7,23 @@ 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]) + return ways_to_make_change_helper(total, (200, 100, 50, 20, 10, 5, 2, 1)) -def ways_to_make_change_helper(total: int, coins: List[int]) -> int: +ways_to_make_change_helper_cache = {} + + +def ways_to_make_change_helper(total: int, coins: Tuple[int]) -> int: """ Helper function for ways_to_make_change to avoid exposing the coins parameter to callers. """ if total == 0 or len(coins) == 0: return 0 + cache_key = (total, coins) + if cache_key in ways_to_make_change_helper_cache: + return ways_to_make_change_helper_cache[cache_key] + ways = 0 for coin_index in range(len(coins)): coin = coins[coin_index] @@ -29,4 +36,5 @@ def ways_to_make_change_helper(total: int, coins: List[int]) -> int: intermediate = ways_to_make_change_helper(total - total_from_coins, coins=coins[coin_index+1:]) ways += intermediate count_of_coin += 1 + ways_to_make_change_helper_cache[cache_key] = ways return ways 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..c7ba785 100644 --- a/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py +++ b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py @@ -2,17 +2,16 @@ def find_longest_common_prefix(strings: List[str]): - """ - find_longest_common_prefix returns the longest string common at the start of any two strings in the passed list. - - 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 "" 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 + sorted_strings = list(sorted(strings[:])) + for i in range(len(sorted_strings) - 1): + left = sorted_strings[i] + right = sorted_strings[i + 1] + common = find_common_prefix(left, right) + if len(common) > len(longest): + longest = common return longest 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..ca1c944 100644 --- a/Sprint-2/improve_with_precomputing/count_letters/count_letters.py +++ b/Sprint-2/improve_with_precomputing/count_letters/count_letters.py @@ -1,13 +1,13 @@ 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() + seen = set() for letter in s: - if is_upper_case(letter): - if letter.lower() not in s: - only_upper.add(letter) - return len(only_upper) + seen.add(letter) + + count = 0 + for letter in seen: + if is_upper_case(letter) and not letter.lower() in seen: + count += 1 + return count def is_upper_case(letter: str) -> bool: