diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1252176..db0e6f4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,3 +9,6 @@ contact_links: - name: CYF Slack url: codeyourfuture.slack.com about: Come to #cyf-curriculum and chat + - name: CYF Tech Ed + url: https://github.com/orgs/CodeYourFuture/teams/mentors + about: CYF mentors on Github diff --git a/.github/ISSUE_TEMPLATE/pd-assignment.yml b/.github/ISSUE_TEMPLATE/pd-assignment.yml new file mode 100644 index 0000000..19f15fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pd-assignment.yml @@ -0,0 +1,59 @@ +name: PD Coursework +description: Assign a piece of PD coursework +title: "[PD] " +labels: [PD, 🏝 Priority Stretch, 🐇 Size Small, 📅 Sprint 1] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to assign this coursework! + - type: input + attributes: + label: Coursework content + validations: + required: true + - type: input + attributes: + label: Estimated time in hours + description: (PD has max 4 per week total) + validations: + required: true + - type: textarea + attributes: + label: What is the purpose of this assignment? + description: Clearly explain the purpose of this assignment and how trainees can evaluate this. + validations: + required: true + - type: textarea + attributes: + label: How to submit + description: State in clear steps how a trainee can submit this assignment. + placeholder: | + Copy the Google doc to your own Google Drive + Complete the work assigned + When you are ready, move your document to your class Drive + validations: + required: true + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give more context + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + - type: markdown + attributes: + value: | + **Thank you so much.** + + Please now complete this ticket by filling in the options on the sidebar. + + 1. Update labels + - priority -- is this coursework key, mandatory, or stretch? + - size -- help trainees plan their time with rough estimation + 2. Add to project backlog + - add to the project named the same as this repo + - fill in custom fields -- priority, size, hours, week -- to match this issue + + Once your ticket is complete, you may like to check it out on the example project board attached to this repo. + This is so you understand how trainees will use your work. diff --git a/.github/ISSUE_TEMPLATE/coursework.yml b/.github/ISSUE_TEMPLATE/tech-ed-assignment.yml similarity index 88% rename from .github/ISSUE_TEMPLATE/coursework.yml rename to .github/ISSUE_TEMPLATE/tech-ed-assignment.yml index 74d2f7d..343b4d6 100644 --- a/.github/ISSUE_TEMPLATE/coursework.yml +++ b/.github/ISSUE_TEMPLATE/tech-ed-assignment.yml @@ -1,20 +1,20 @@ -name: Coursework -description: Assign a piece of coursework +name: Tech Ed Coursework +description: Assign a piece of technical coursework title: "<title>" -labels: [Tech Ed, 🏕 Priority Mandatory, 🐂 Size Medium, 📅 Module 1] +labels: [Tech Ed, 🏕 Priority Mandatory, 🐂 Size Medium, 📅 Sprint 1] body: - type: markdown attributes: value: | - Thanks for taking the time to assign this coursework! + Thanks for taking the time to assign this coursework! - To support our trainees with planning and prioritising their own learning journey, we want our coursework assignments to be more informative. + To support our trainees with planning and prioritising their own learning journey, we want our coursework assignments to be more informative. We don't just want to tell them what to do, we want to tell them stuff like: - why we are doing it - what it's "for" (problem-solving, debugging, etc) - how long they should spend on it, maximum - - how to get help - - how to review it with answers + - how to get help + - how to review it with answers - how to get it reviewed from mentors and peers - type: input attributes: @@ -26,7 +26,7 @@ body: label: Learning Objectives description: https://common.codeyourfuture.io/common-theme/shortcodes/objectives/ placeholder: | - <!--{{<objectives}}--> + <!--{{<objectives}}--> - [ ] CYF format, task list formatting <!--{{</objectives}}--> - type: textarea @@ -38,6 +38,7 @@ body: - type: input attributes: label: Maximum time in hours + description: (Tech has max 16 per week total) validations: required: true - type: textarea @@ -79,7 +80,7 @@ body: 1. Update labels - priority -- is this coursework key, mandatory, or stretch? Pick one. - size -- help trainees plan their time with rough estimation. Pick one - - topics -- add all that seem relevant to you. + - topics -- add all that seem relevant to you. 2. Add a Sprint label to add to the backlog view - sprint 1,2,3,4 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7d2576b..0d8ee1d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,14 +2,19 @@ You must title your PR like this: -REGION | COHORT_NAME | FIRST_NAME LAST_NAME | PROJ_NAME +Region | Cohort | FirstName LastName | Sprint | Assignment Title For example, -London | March-2025 | Carol Owen | Wireframe +London | 25-ITP-May | Carol Owen | Sprint 1 | Alarm Clock -Complete the task list below this message. -If your PR is rejected, check the task list. +Fill in the template below - remove any sections that don't apply. + +Complete the self checklist - replace each empty box in the checklist [ ] with a [x]. + +Add the label "Needs Review" and you will get review. + +Respond to volunteer reviews until the volunteer marks it as "Complete". --> @@ -17,12 +22,15 @@ If your PR is rejected, check the task list. Self checklist -- [ ] I have committed my files one by one, on purpose, and for a reason -- [ ] I have titled my PR with REGION | COHORT_NAME | FIRST_NAME LAST_NAME | PROJ_NAME +- [ ] I have titled my PR with Region | Cohort | FirstName LastName | Sprint | Assignment Title +- [ ] My changes meet the requirements of the task - [ ] I have tested my changes - [ ] My changes follow the [style guide](https://curriculum.codeyourfuture.io/guides/reviewing/style-guide/) -- [ ] My changes meet the [requirements](./README.md) of this task + +## Changelist + +Briefly explain your PR. ## Questions -Ask any questions you have for your reviewer. Delete this section if there are none. +Ask any questions you have for your reviewer. diff --git a/.github/workflows/validate-pr-metadata.yml b/.github/workflows/validate-pr-metadata.yml new file mode 100644 index 0000000..10ef3c7 --- /dev/null +++ b/.github/workflows/validate-pr-metadata.yml @@ -0,0 +1,18 @@ +name: Validate PR Metadata +on: + pull_request_target: + types: + - labeled + - unlabeled + - opened + - edited + - reopened + +jobs: + validate_pr_metadata: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: CodeYourFuture/actions/validate-pr-metadata@main + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js b/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js index ce738c3..0199894 100644 --- a/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js +++ b/Sprint-1/JavaScript/calculateSumAndProduct/calculateSumAndProduct.js @@ -9,26 +9,53 @@ * "product": 30 // 2 * 3 * 5 * } * - * Time Complexity: - * Space Complexity: - * Optimal Time Complexity: + * Time Complexity: O(n) - Single pass through the array + * Space Complexity: O(1) - Only using constant extra space + * Optimal Time Complexity: O(n) - Cannot do better than linear time * * @param {Array<number>} numbers - Numbers to process * @returns {Object} Object containing running total and product */ export function calculateSumAndProduct(numbers) { - let sum = 0; - for (const num of numbers) { - sum += num; - } + // OPTIMIZED IMPLEMENTATION: Single pass algorithm + // Previous implementation used two separate loops (2n operations) + // This version combines both calculations in one loop (n operations) + + let sum = 0; // O(1) space + let product = 1; // O(1) space - let product = 1; + // Single pass through array: O(n) time complexity for (const num of numbers) { - product *= num; + sum += num; // O(1) operation per element + product *= num; // O(1) operation per element } - return { - sum: sum, - product: product, - }; + // Return optimized object syntax: O(1) space + return { sum, product }; } + +/* + * ORIGINAL IMPLEMENTATION (for comparison): + * + * export function calculateSumAndProduct(numbers) { + * let sum = 0; + * for (const num of numbers) { // First pass: O(n) + * sum += num; + * } + * + * let product = 1; + * for (const num of numbers) { // Second pass: O(n) + * product *= num; + * } + * + * return { // Total: O(2n) = O(n) time + * sum: sum, // O(1) space + * product: product, + * }; + * } + * + * IMPROVEMENTS MADE: + * 1. Reduced from 2n to n operations (50% fewer iterations) + * 2. Better cache locality (single pass through memory) + * 3. Same O(n) time complexity but with better constant factors + */ diff --git a/Sprint-1/JavaScript/findCommonItems/findCommonItems.js b/Sprint-1/JavaScript/findCommonItems/findCommonItems.js index 5619ae5..2e9cdd5 100644 --- a/Sprint-1/JavaScript/findCommonItems/findCommonItems.js +++ b/Sprint-1/JavaScript/findCommonItems/findCommonItems.js @@ -1,14 +1,46 @@ /** * Finds common items between two arrays. * - * Time Complexity: - * Space Complexity: - * Optimal Time Complexity: + * Time Complexity: O(n + m) - Single pass through both arrays + * Space Complexity: O(min(n, m)) - Set size bounded by smaller array + * Optimal Time Complexity: O(n + m) - Cannot do better than linear time * * @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) => { + // OPTIMIZED IMPLEMENTATION: O(n + m) time complexity + // Previous implementation: O(n × m) due to nested includes() calls + + // Convert second array to Set for O(1) lookup: O(m) time, O(m) space + const secondSet = new Set(secondArray); + + // Find common items using Set lookup: O(n) time + const commonItems = firstArray.filter((item) => secondSet.has(item)); + + // Remove duplicates: O(n) time in worst case + return [...new Set(commonItems)]; +}; + +/* + * ORIGINAL IMPLEMENTATION (for comparison): + * + * export const findCommonItems = (firstArray, secondArray) => [ + * ...new Set(firstArray.filter((item) => secondArray.includes(item))), + * ]; + * + * COMPLEXITY ANALYSIS OF ORIGINAL: + * - firstArray.filter(): O(n) iterations + * - secondArray.includes(): O(m) for each iteration + * - Total: O(n × m) time complexity + * - Space: O(n) for Set creation + * + + * IMPROVEMENTS MADE: + * 1. Reduced from O(n × m) to O(n + m) time complexity + * 2. Set lookup is O(1) vs Array.includes() O(m) + * 3. Significant performance gain for large arrays + * 4. Same functionality with better algorithmic efficiency + + */ diff --git a/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js b/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js index dd2901f..50b7c43 100644 --- a/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js +++ b/Sprint-1/JavaScript/hasPairWithSum/hasPairWithSum.js @@ -1,21 +1,59 @@ /** * 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) - Single pass through the array + * Space Complexity: O(n) - Set to store seen numbers + * Optimal Time Complexity: O(n) - Cannot do better than linear time * * @param {Array<number>} 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; - } + // OPTIMIZED IMPLEMENTATION: O(n) time complexity + // Previous implementation: O(n²) due to nested loops + + const seen = new Set(); // O(n) + + // O(n) time complexity + for (const num of numbers) { + const complement = target - num; + // O(1) lookup + if (seen.has(complement)) { + return true; } + + // O(1) operation + seen.add(num); } return false; } +console.log(hasPairWithSum([3, 2, 3, 4, 5], 9)); +/* + * ORIGINAL IMPLEMENTATION (for comparison): + * + * export function hasPairWithSum(numbers, target) { + * for (let i = 0; i < numbers.length; i++) { // O(n) iterations + * for (let j = i + 1; j < numbers.length; j++) { // O(n) iterations each + * if (numbers[i] + numbers[j] === target) { // O(1) comparison + * return true; + * } + * } + * } + * return false; + * } + * + * COMPLEXITY ANALYSIS OF ORIGINAL: + * - Outer loop: O(n) iterations + * - Inner loop: O(n) iterations for each outer iteration + * - Total: O(n²) time complexity + * - Space: O(1) - only using loop variables + * + * PERFORMANCE ISSUES: + * - Quadratic time complexity O(n²) + * + * IMPROVEMENTS MADE: + * 1. Reduced from O(n²) to O(n) time complexity + * 2. Single pass through array instead of nested loops + * 3. Set lookup is O(1) vs nested iteration O(n) + */ diff --git a/Sprint-1/JavaScript/package-lock.json b/Sprint-1/JavaScript/package-lock.json new file mode 100644 index 0000000..37ca6f0 --- /dev/null +++ b/Sprint-1/JavaScript/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "module-complexity-sprint-1", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "module-complexity-sprint-1", + "version": "1.0.0" + } + } +} diff --git a/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs b/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs index dc5f771..3d701cd 100644 --- a/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs +++ b/Sprint-1/JavaScript/removeDuplicates/removeDuplicates.mjs @@ -1,36 +1,65 @@ /** * 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) - Single pass through the array + * Space Complexity: O(n) - Set to track seen elements + * Optimal Time Complexity: O(n) - Cannot do better than linear time * * @param {Array} inputSequence - Sequence to remove duplicates from * @returns {Array} New sequence with duplicates removed */ export function removeDuplicates(inputSequence) { - const uniqueItems = []; + // OPTIMIZED IMPLEMENTATION: O(n) time complexity + // Previous implementation: O(n²) due to nested loops checking each element - 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]); + const seen = new Set(); // O(n) + const uniqueItems = []; // O(n) + + // O(n) time complexity + for (const item of inputSequence) { + // O(1) lookup + if (!seen.has(item)) { + seen.add(item); // O(1) operation + uniqueItems.push(item); // O(1) operation } } return uniqueItems; } +console.log(removeDuplicates([1, 2, 3, 4, 5, 1, 2, 3, 4, 5])); +/* + * ORIGINAL IMPLEMENTATION (for comparison): + * + * export function removeDuplicates(inputSequence) { + * const uniqueItems = []; + * + * 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]); + * } + * } + * + * return uniqueItems; + * } + * + * COMPLEXITY ANALYSIS OF ORIGINAL: + * - Outer loop: O(n) iterations through input array + * - Inner loop: O(k) iterations through uniqueItems array (k grows with each unique element) + * - Worst case: O(n²) when all elements are unique + * - Space: O(n) for uniqueItems array + * + * PERFORMANCE ISSUES: + * - Quadratic time complexity O(n²) in worst case + * + * IMPROVEMENTS MADE: + * 1. Reduced from O(n²) to O(n) time complexity + * 2. Set lookup is O(1) vs linear search O(k) + * 3. Single pass through input array + */ 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..36bc8f3 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,20 +12,50 @@ 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) - Single pass through the list + Space Complexity: O(1) - Only using constant extra space + Optimal Time Complexity: O(n) - Cannot do better than linear time """ + # OPTIMIZED IMPLEMENTATION: O(n) time complexity + # Previous implementation: O(2n) due to two separate loops + + sum_total = 0 # O(1) space + product = 1 # O(1) space + + # O(n) time complexity + for current_number in input_numbers: + sum_total += current_number # O(1) + product *= current_number # O(1) + + return {"sum": sum_total, "product": product} + + +# ORIGINAL IMPLEMENTATION (for comparison): +""" +def calculate_sum_and_product(input_numbers: List[int]) -> Dict[str, int]: # Edge case: empty list if not input_numbers: return {"sum": 0, "product": 1} sum = 0 - for current_number in input_numbers: + for current_number in input_numbers: # First pass: O(n) sum += current_number product = 1 - for current_number in input_numbers: + for current_number in input_numbers: # Second pass: O(n) product *= current_number - return {"sum": sum, "product": product} + return {"sum": sum, "product": product} # Total: O(2n) = O(n) time + +COMPLEXITY ANALYSIS OF ORIGINAL: +- First loop: O(n) iterations to calculate sum +- Second loop: O(n) iterations to calculate product +- Total: O(2n) = O(n) time complexity +- Space: O(1) - only using loop variables + +IMPROVEMENTS MADE: +1. Reduced from 2n to n operations (50% fewer iterations) +2. Single pass through list instead of two separate loops +3. Same O(n) time complexity but with better constant factors +""" 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..b94bc63 100644 --- a/Sprint-1/Python/find_common_items/find_common_items.py +++ b/Sprint-1/Python/find_common_items/find_common_items.py @@ -9,13 +9,49 @@ def find_common_items( """ Find common items between two arrays. - Time Complexity: - Space Complexity: - Optimal time complexity: + Time Complexity: O(n + m) - Single pass through both sequences + Space Complexity: O(min(n, m)) - Set size bounded by smaller sequence + Optimal Time Complexity: O(n + m) - Cannot do better than linear time """ + # OPTIMIZED IMPLEMENTATION: O(n + m) time complexity + # Previous implementation: O(n × m) due to nested loops with linear search + + # Convert second sequence to set for O(1) lookup: O(m) time, O(m) space + second_set = set(second_sequence) + + # Find common items using set lookup: O(n) time + common_items = [] + for item in first_sequence: + if item in second_set and item not in common_items: + common_items.append(item) + + return common_items + + +# ORIGINAL IMPLEMENTATION (for comparison): +""" +def find_common_items( + first_sequence: Sequence[ItemType], second_sequence: Sequence[ItemType] +) -> List[ItemType]: common_items: List[ItemType] = [] - for i in first_sequence: - for j in second_sequence: - if i == j and i not in common_items: + for i in first_sequence: # O(n) iterations + for j in second_sequence: # O(m) iterations each + if i == j and i not in common_items: # O(k) linear search common_items.append(i) return common_items + +COMPLEXITY ANALYSIS OF ORIGINAL: +- Outer loop: O(n) iterations through first_sequence +- Inner loop: O(m) iterations through second_sequence +- Linear search: O(k) for 'i not in common_items' check +- Total: O(n × m × k) time complexity in worst case +- Space: O(n) for common_items list + +PERFORMANCE ISSUES: +- Quadratic time complexity O(n × m) from nested loops + +IMPROVEMENTS MADE: +1. Reduced from O(n × m × k) to O(n + m) time complexity +2. Set lookup is O(1) vs nested iteration O(m) +3. Single pass through first_sequence +""" 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..c333a6a 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,48 @@ 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) - Single pass through the list + Space Complexity: O(n) - Set to store seen numbers + Optimal Time Complexity: O(n) - Cannot do better than linear time """ - for i in range(len(numbers)): - for j in range(i + 1, len(numbers)): - if numbers[i] + numbers[j] == target_sum: + # OPTIMIZED IMPLEMENTATION: O(n) time complexity + # Previous implementation: O(n²) due to nested loops + + seen = set() # O(n) space for storing seen numbers + + # Single pass through list: O(n) time complexity + for num in numbers: + complement = target_sum - num + + # Check if complement exists in seen numbers: O(1) lookup + if complement in seen: + return True + + # Add current number to seen set: O(1) operation + seen.add(num) + + return False + +# ORIGINAL IMPLEMENTATION (for comparison): +""" +def has_pair_with_sum(numbers: List[Number], target_sum: Number) -> bool: + for i in range(len(numbers)): # O(n) iterations + for j in range(i + 1, len(numbers)): # O(n) iterations each + if numbers[i] + numbers[j] == target_sum: # O(1) comparison return True return False + +COMPLEXITY ANALYSIS OF ORIGINAL: +- Outer loop: O(n) iterations +- Inner loop: O(n) iterations for each outer iteration +- Total: O(n²) time complexity +- Space: O(1) - only using loop variables + +PERFORMANCE ISSUES: +- Quadratic time complexity O(n²) + +IMPROVEMENTS MADE: +1. Reduced from O(n²) to O(n) time complexity +2. Single pass through list instead of nested loops +3. Set lookup is O(1) vs nested iteration O(n) +""" diff --git a/Sprint-1/Python/remove_duplicates/remove_duplicates.py b/Sprint-1/Python/remove_duplicates/remove_duplicates.py index c9fdbe8..029f3ae 100644 --- a/Sprint-1/Python/remove_duplicates/remove_duplicates.py +++ b/Sprint-1/Python/remove_duplicates/remove_duplicates.py @@ -7,19 +7,53 @@ 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) - Single pass through the sequence + Space Complexity: O(n) - Set to track seen elements + Optimal Time Complexity: O(n) - Cannot do better than linear time """ + # OPTIMIZED IMPLEMENTATION: O(n) time complexity + # Previous implementation: O(n²) due to nested loops checking each element + + seen = set() # O(n) + unique_items = [] # O(n) + + # O(n) time complexity + for value in values: + # O(1) lookup + if value not in seen: + seen.add(value) # O(1) + unique_items.append(value) # O(1) + + return unique_items + + +# ORIGINAL IMPLEMENTATION (for comparison): +""" +def remove_duplicates(values: Sequence[ItemType]) -> List[ItemType]: unique_items = [] - for value in values: + for value in values: # O(n) iterations is_duplicate = False - for existing in unique_items: - if value == existing: + for existing in unique_items: # O(k) iterations (k grows with unique elements) + if value == existing: # O(1) comparison is_duplicate = True break if not is_duplicate: - unique_items.append(value) + unique_items.append(value) # O(1) operation return unique_items + +COMPLEXITY ANALYSIS OF ORIGINAL: +- Outer loop: O(n) iterations through values +- Inner loop: O(k) iterations through unique_items (k grows with each unique element) +- Worst case: O(n²) when all elements are unique +- Space: O(n) for unique_items list + +PERFORMANCE ISSUES: +- Quadratic time complexity O(n²) in worst case + +IMPROVEMENTS MADE: +1. Reduced from O(n²) to O(n) time complexity +2. Set lookup is O(1) vs linear search O(k) +3. Single pass through input sequence +""" diff --git a/Sprint-2/implement_linked_list/README.md b/Sprint-2/implement_linked_list/README.md new file mode 100644 index 0000000..50c44a8 --- /dev/null +++ b/Sprint-2/implement_linked_list/README.md @@ -0,0 +1,11 @@ +# Implement a linked list + +Implement a linked list data structure in Python. + +It should support the following operations. Each operation should have a O(1) worst-case time complexity. + +* `push_head` should add an element to the start of the list. It should return something that can be passed to `remove` to remove that element in the future. +* `pop_tail` should remove an element from the end of the list. +* `remove` takes a handle from `push_head`, and removes that element from the list. + +There are some tests in `linked_list_test.py` for your implementation - feel free to write more. diff --git a/Sprint-2/implement_linked_list/linked_list.py b/Sprint-2/implement_linked_list/linked_list.py new file mode 100644 index 0000000..e69de29 diff --git a/Sprint-2/implement_linked_list/linked_list_test.py b/Sprint-2/implement_linked_list/linked_list_test.py new file mode 100644 index 0000000..d59d9c5 --- /dev/null +++ b/Sprint-2/implement_linked_list/linked_list_test.py @@ -0,0 +1,39 @@ +import unittest + +from linked_list import LinkedList + +class LinkedListTest(unittest.TestCase): + def test_pushes_then_pops(self): + l = LinkedList() + l.push_head("a") + l.push_head("b") + l.push_head("c") + + self.assertEqual(l.pop_tail(), "a") + self.assertEqual(l.pop_tail(), "b") + self.assertEqual(l.pop_tail(), "c") + + def test_remove(self): + l = LinkedList() + l.push_head("a") + b = l.push_head("b") + l.push_head("c") + + l.remove(b) + + self.assertEqual(l.pop_tail(), "a") + self.assertEqual(l.pop_tail(), "c") + + def test_remove_tail(self): + l = LinkedList() + a = l.push_head("a") + b = l.push_head("b") + l.remove(a) + self.assertEqual(l.head, b) + self.assertEqual(l.tail, b) + self.assertIsNone(b.next) + self.assertIsNone(b.previous) + + +if __name__ == "__main__": + unittest.main() diff --git a/Sprint-2/implement_lru_cache/README.md b/Sprint-2/implement_lru_cache/README.md new file mode 100644 index 0000000..b4adf21 --- /dev/null +++ b/Sprint-2/implement_lru_cache/README.md @@ -0,0 +1,11 @@ +# Implement an LRU cache + +Implement a cache in Python with an LRU eviction policy - this means that if an item needs to be evicted, the item which was least recently used will be evicted. Both setting or getting a value counts as a use. + +It should support the following operations. Each operation should have a O(1) worst-case time complexity. + +* `LruCache(limit)` should construct an LRU cache which never stores more than `limit` entries. +* `set(key, value)` should associate `value` with the passed `key`. +* `get(key)` should look-up the value previously associated with `key`. + +There are some tests in `lru_cache_test.py` for your implementation - feel free to write more. diff --git a/Sprint-2/implement_lru_cache/lru_cache.py b/Sprint-2/implement_lru_cache/lru_cache.py new file mode 100644 index 0000000..e69de29 diff --git a/Sprint-2/implement_lru_cache/lru_cache_test.py b/Sprint-2/implement_lru_cache/lru_cache_test.py new file mode 100644 index 0000000..d37df01 --- /dev/null +++ b/Sprint-2/implement_lru_cache/lru_cache_test.py @@ -0,0 +1,59 @@ +import unittest + +from lru_cache import LruCache + +class LruCacheTest(unittest.TestCase): + def test_zero_limit_is_error(self): + self.assertRaises(ValueError, lambda: LruCache(limit=0)) + + def test_set_then_get(self): + cache = LruCache(limit=10) + + self.assertIsNone(cache.get("greeting")) + self.assertIsNone(cache.get("parting")) + + cache.set("greeting", "hello") + + self.assertEqual(cache.get("greeting"), "hello") + self.assertIsNone(cache.get("parting")) + + def test_limit(self): + limit = 3 + cache = LruCache(limit=limit) + + keys = ["a", "b", "c", "d", "e"] + + for key in keys: + cache.set(key, f"{key}-1") + + hits = 0 + for key in keys: + if cache.get(key) is not None: + hits += 1 + + self.assertEqual(hits, limit) + + def test_eviction_order_just_inserts(self): + cache = LruCache(limit=2) + + cache.set("a", 1) + cache.set("b", 2) + cache.set("c", 3) + self.assertIsNone(cache.get("a")) + + def test_eviction_order_after_gets(self): + cache = LruCache(limit=2) + + cache.set("a", 1) + cache.set("b", 2) + cache.get("a") + cache.get("b") + cache.get("a") + cache.set("c", 3) + self.assertIsNone(cache.get("b")) + self.assertEqual(cache.get("a"), 1) + self.assertEqual(cache.get("c"), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/Sprint-2/implement_skip_list/README.md b/Sprint-2/implement_skip_list/README.md new file mode 100644 index 0000000..9330d05 --- /dev/null +++ b/Sprint-2/implement_skip_list/README.md @@ -0,0 +1,8 @@ +A [Skip list](https://brilliant.org/wiki/skip-lists/) is a data structure. It is similar to a linked list, but is optimised for being able to quickly insert or find an element in a sorted list. This is a form of pre-computing - we _sort_ the data, and we keep _indexes_ to be able to quickly jump to different points within our data. Doing this pre-computing speeds up the run-time of other operations. + +Implement your own skip list. It should support three operations: +* Inserting (which should be faster than O(n)) +* Contains checks (which should be faster than O(n)) +* Converting to a list (which should be O(n)). + +There are some tests in `skip_list_test.py` for your implementation - feel free to write more. diff --git a/Sprint-2/implement_skip_list/skip_list_test.py b/Sprint-2/implement_skip_list/skip_list_test.py new file mode 100644 index 0000000..d20b102 --- /dev/null +++ b/Sprint-2/implement_skip_list/skip_list_test.py @@ -0,0 +1,31 @@ +import unittest + +from skip_list import SkipList + +class SkipListTest(unittest.TestCase): + def test_single_item(self): + sl = SkipList() + sl.insert("a") + self.assertEqual(sl.to_list(), ["a"]) + self.assertIn("a", sl) + self.assertNotIn("b", sl) + + def test_general_usage(self): + sl = SkipList() + sl.insert(1) + sl.insert(2) + sl.insert(3) + sl.insert(4) + sl.insert(10) + sl.insert(5) + + self.assertIn(5, sl) + self.assertIn(4, sl) + self.assertNotIn(6, sl) + self.assertNotIn(7, sl) + + self.assertEqual(sl.to_list(), [1, 2, 3, 4, 5, 10]) + + +if __name__ == "__main__": + unittest.main() diff --git a/Sprint-2/improve_with_caches/README.md b/Sprint-2/improve_with_caches/README.md new file mode 100644 index 0000000..ea00917 --- /dev/null +++ b/Sprint-2/improve_with_caches/README.md @@ -0,0 +1,5 @@ +Each directory contains implementations that can be made faster by using caching/memoisation, at the expense of using more memory. + +Speed up the solutions by introducing a cache for each. + +You must not used the `@cache` decorator, and you don't need to worry about the size of your cache. diff --git a/Sprint-2/improve_with_caches/fibonacci/fibonacci.py b/Sprint-2/improve_with_caches/fibonacci/fibonacci.py new file mode 100644 index 0000000..60cc667 --- /dev/null +++ b/Sprint-2/improve_with_caches/fibonacci/fibonacci.py @@ -0,0 +1,4 @@ +def fibonacci(n): + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) diff --git a/Sprint-2/improve_with_caches/fibonacci/fibonacci_test.py b/Sprint-2/improve_with_caches/fibonacci/fibonacci_test.py new file mode 100644 index 0000000..8494e53 --- /dev/null +++ b/Sprint-2/improve_with_caches/fibonacci/fibonacci_test.py @@ -0,0 +1,25 @@ +import unittest + +from fibonacci import fibonacci + +class FibonacciTest(unittest.TestCase): + def test_0(self): + self.assertEqual(fibonacci(0), 0) + + def test_1(self): + self.assertEqual(fibonacci(1), 1) + + def test_2(self): + self.assertEqual(fibonacci(2), 1) + + def test_3(self): + self.assertEqual(fibonacci(3), 2) + + def test_10(self): + self.assertEqual(fibonacci(10), 55) + + def test_200(self): + self.assertEqual(fibonacci(200), 280571172992510140037611932413038677189525) + +if __name__ == "__main__": + unittest.main() diff --git a/Sprint-2/improve_with_caches/making_change/making_change.py b/Sprint-2/improve_with_caches/making_change/making_change.py new file mode 100644 index 0000000..255612e --- /dev/null +++ b/Sprint-2/improve_with_caches/making_change/making_change.py @@ -0,0 +1,32 @@ +from typing import List + + +def ways_to_make_change(total: int) -> int: + """ + Given access to coins with the values 1, 2, 5, 10, 20, 50, 100, 200, returns a count of all of the ways to make the passed total value. + + 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]) + + +def ways_to_make_change_helper(total: int, coins: List[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 + + 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 + return ways diff --git a/Sprint-2/improve_with_caches/making_change/making_change_test.py b/Sprint-2/improve_with_caches/making_change/making_change_test.py new file mode 100644 index 0000000..e4e0b74 --- /dev/null +++ b/Sprint-2/improve_with_caches/making_change/making_change_test.py @@ -0,0 +1,31 @@ +import unittest + +from making_change import ways_to_make_change + +class MakingChangeTest(unittest.TestCase): + def test_1(self): + # 1x 1p + self.assertEqual(ways_to_make_change(1), 1) + + def test_2(self): + # 1x 2p + # 2x 1p + self.assertEqual(ways_to_make_change(2), 2) + + def test_7(self): + # 1x 5p, 1x 2p + # 1x 5p, 2x 1p + # 3x 2p, 1x 1p + # 2x 2p, 3x 1p + # 1x 2p, 5x 1p + # 7x 1p + self.assertEqual(ways_to_make_change(7), 6) + + def test_17(self): + self.assertEqual(ways_to_make_change(17), 28) + + def test_9176(self): + self.assertEqual(ways_to_make_change(9176), 628431158425225) + +if __name__ == "__main__": + unittest.main() diff --git a/Sprint-2/improve_with_precomputing/README.md b/Sprint-2/improve_with_precomputing/README.md new file mode 100644 index 0000000..aaccd50 --- /dev/null +++ b/Sprint-2/improve_with_precomputing/README.md @@ -0,0 +1,5 @@ +Each directory contains implementations that can be made faster by using pre-computing. + +The problem they solve is described in docstrings and tests. + +Speed up the solutions by introducing precomputing. You may rewrite as much of the solution as you want, and may add any tests, but must not modify existing tests. diff --git a/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py new file mode 100644 index 0000000..f4839e7 --- /dev/null +++ b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix.py @@ -0,0 +1,24 @@ +from typing import List + + +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. + """ + 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 + return longest + + +def find_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]: + return left[:i] + return left[:min_length] diff --git a/Sprint-2/improve_with_precomputing/common_prefix/common_prefix_test.py b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix_test.py new file mode 100644 index 0000000..a22ef79 --- /dev/null +++ b/Sprint-2/improve_with_precomputing/common_prefix/common_prefix_test.py @@ -0,0 +1,40 @@ +import random +import string +import unittest + +from common_prefix import find_longest_common_prefix + +class CommonPrefixTest(unittest.TestCase): + def test_finds_longest_common_prefix(self): + strings = [ + "hello Steve", + "cheese", + "hello world", + "hi", + "cheddar", + ] + self.assertEqual(find_longest_common_prefix(strings), "hello ") + + def test_empty_list(self): + self.assertEqual(find_longest_common_prefix([]), "") + + def test_single_item_list(self): + self.assertEqual(find_longest_common_prefix(["hello"]), "") + + def test_no_common_prefix(self): + strings = ["hi", "bye"] + self.assertEqual(find_longest_common_prefix(strings), "") + + def test_case_sensitivity(self): + strings = ["Hello", "hello"] + self.assertEqual(find_longest_common_prefix(strings), "") + + def test_really_long_list(self): + strings = [] + for _ in range(1000000): + strings.append("hello" + "".join(random.choices(string.ascii_lowercase, k=20))) + common_prefix = find_longest_common_prefix(strings) + self.assertRegex(common_prefix, "^hello.*$") + +if __name__ == "__main__": + unittest.main() diff --git a/Sprint-2/improve_with_precomputing/count_letters/count_letters.py b/Sprint-2/improve_with_precomputing/count_letters/count_letters.py new file mode 100644 index 0000000..62c3ec0 --- /dev/null +++ b/Sprint-2/improve_with_precomputing/count_letters/count_letters.py @@ -0,0 +1,14 @@ +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) + + +def is_upper_case(letter: str) -> bool: + return letter == letter.upper() diff --git a/Sprint-2/improve_with_precomputing/count_letters/count_letters_test.py b/Sprint-2/improve_with_precomputing/count_letters/count_letters_test.py new file mode 100644 index 0000000..fe88477 --- /dev/null +++ b/Sprint-2/improve_with_precomputing/count_letters/count_letters_test.py @@ -0,0 +1,37 @@ +import random +import string +import unittest + +from count_letters import count_letters + +class CommonPrefixTest(unittest.TestCase): + def test_only_upper(self): + self.assertEqual(count_letters("ABC"), 3) + + def test_only_lower(self): + self.assertEqual(count_letters("abc"), 0) + + def test_both(self): + self.assertEqual(count_letters("aABCbc"), 0) + + def test_mixed(self): + self.assertEqual(count_letters("aABCbcDEeFGhI"), 4) + + def test_long_string(self): + s = "" + + only_upper = set("aeiou") + alphabet = list(set(string.ascii_letters) - only_upper) + still_to_include = set(letter for letter in alphabet) + + while len(s) < 10000000 or len(still_to_include) > 0: + next = random.choice(alphabet) + s += next + if next in still_to_include: + still_to_include.remove(next) + + self.assertEqual(count_letters(s), 5) + + +if __name__ == "__main__": + unittest.main()