From b4110d6abd714855f795d8955eec3b6a4b052161 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Mon, 21 Apr 2025 21:17:13 +0100 Subject: [PATCH] feat(content): initial question set --- .../questions/merge-two-sorted-lists.yaml | 144 ++++++++++++++++++ backend/data/questions/two-sum.yaml | 117 ++++++++++++++ backend/data/questions/valid-parentheses.yaml | 134 ++++++++++++++++ 3 files changed, 395 insertions(+) create mode 100644 backend/data/questions/merge-two-sorted-lists.yaml create mode 100644 backend/data/questions/two-sum.yaml create mode 100644 backend/data/questions/valid-parentheses.yaml diff --git a/backend/data/questions/merge-two-sorted-lists.yaml b/backend/data/questions/merge-two-sorted-lists.yaml new file mode 100644 index 0000000..0910cd3 --- /dev/null +++ b/backend/data/questions/merge-two-sorted-lists.yaml @@ -0,0 +1,144 @@ +title: Merge Two Sorted Lists +slug: merge-two-sorted-lists +difficulty: easy +leetcode_id: 21 +leetcode_url: https://leetcode.com/problems/merge-two-sorted-lists/ +categories: + - linked-lists + - recursion +patterns: + - two-pointers + +description: | + You are given the heads of two sorted linked lists `list1` and `list2`. + + Merge the two lists into one sorted list. The list should be made by splicing together + the nodes of the first two lists. + + Return the head of the merged linked list. + +constraints: | + - The number of nodes in both lists is in the range [0, 50]. + - -100 <= Node.val <= 100 + - Both list1 and list2 are sorted in non-decreasing order. + +examples: + - input: "list1 = [1,2,4], list2 = [1,3,4]" + output: "[1,1,2,3,4,4]" + explanation: "Merge by comparing heads and taking the smaller value each time." + - input: "list1 = [], list2 = []" + output: "[]" + explanation: "Both lists empty, result is empty." + - input: "list1 = [], list2 = [0]" + output: "[0]" + explanation: "One list empty, return the other." + +explanation: + approach: | + 1. Create a dummy node to simplify edge cases (avoids special handling for the head) + 2. Use a current pointer starting at the dummy node + 3. While both lists have nodes: + - Compare the values at the heads of both lists + - Attach the smaller node to current.next + - Advance the pointer of the list we took from + - Advance current to the newly attached node + 4. Attach any remaining nodes from the non-empty list + 5. Return dummy.next (the actual head of the merged list) + + intuition: | + Since both lists are already sorted, we can build the merged list by repeatedly taking + the smaller of the two current heads. This is the merge step from merge sort. + + The dummy node technique is a common pattern for linked list problems. It eliminates + the need for special logic to initialize the head of the result list. + + Think of it like merging two sorted piles of cards — always take the smaller top card. + + common_pitfalls: + - title: Forgetting to handle empty lists + description: | + One or both lists might be empty. The dummy node pattern handles this naturally, + but without it, you need explicit null checks. + wrong_approach: "Assuming both lists have at least one node" + correct_approach: "Use dummy node or check for null at the start" + + - title: Not linking remaining nodes + description: | + After the main loop, one list might still have nodes. Don't iterate through + them — just link the entire remaining portion. + wrong_approach: "Looping through remaining nodes one by one" + correct_approach: "current.next = list1 or list2" + + - title: Returning dummy instead of dummy.next + description: | + The dummy node is just a placeholder. The actual merged list starts at dummy.next. + wrong_approach: "return dummy" + correct_approach: "return dummy.next" + + key_takeaways: + - Dummy nodes simplify linked list construction + - This is the merge step of merge sort + - Comparing and advancing pointers is a fundamental linked list technique + - Can also be solved recursively with elegant code + + time_complexity: "O(n + m)" + space_complexity: "O(1)" + complexity_explanation: | + Time: We visit each node exactly once, where n and m are the lengths of the two lists. + Space: We only use a few pointers; we reuse existing nodes (no new nodes created). + +solutions: + - approach_name: Iterative with Dummy Node (Optimal) + is_optimal: true + code: | + class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + + def merge_two_lists( + list1: ListNode | None, + list2: ListNode | None, + ) -> ListNode | None: + dummy = ListNode() + current = dummy + + while list1 and list2: + if list1.val <= list2.val: + current.next = list1 + list1 = list1.next + else: + current.next = list2 + list2 = list2.next + current = current.next + + # Attach remaining nodes + current.next = list1 if list1 else list2 + + return dummy.next + explanation: | + Use a dummy node to build the result. Compare heads and attach the smaller one. + Finally, attach any remaining nodes from the non-empty list. + + - approach_name: Recursive + is_optimal: false + code: | + def merge_two_lists( + list1: ListNode | None, + list2: ListNode | None, + ) -> ListNode | None: + if not list1: + return list2 + if not list2: + return list1 + + if list1.val <= list2.val: + list1.next = merge_two_lists(list1.next, list2) + return list1 + else: + list2.next = merge_two_lists(list1, list2.next) + return list2 + explanation: | + Elegant recursive solution. Base case: return the non-null list. + Recursive case: attach smaller head and recurse on remaining lists. + Space is O(n+m) due to recursion stack. diff --git a/backend/data/questions/two-sum.yaml b/backend/data/questions/two-sum.yaml new file mode 100644 index 0000000..6d0f705 --- /dev/null +++ b/backend/data/questions/two-sum.yaml @@ -0,0 +1,117 @@ +title: Two Sum +slug: two-sum +difficulty: easy +leetcode_id: 1 +leetcode_url: https://leetcode.com/problems/two-sum/ +categories: + - arrays + - hash-tables +patterns: + - two-pointers + +description: | + Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`. + + You may assume that each input would have exactly one solution, and you may not use the same element twice. + + You can return the answer in any order. + +constraints: | + - 2 <= nums.length <= 10^4 + - -10^9 <= nums[i] <= 10^9 + - -10^9 <= target <= 10^9 + - Only one valid answer exists. + +examples: + - input: "nums = [2,7,11,15], target = 9" + output: "[0,1]" + explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]." + - input: "nums = [3,2,4], target = 6" + output: "[1,2]" + explanation: "Because nums[1] + nums[2] == 6, we return [1, 2]." + - input: "nums = [3,3], target = 6" + output: "[0,1]" + explanation: "Because nums[0] + nums[1] == 6, we return [0, 1]." + +explanation: + approach: | + 1. Create a hash map to store each number and its index as we iterate + 2. For each number, calculate its complement (target - current number) + 3. Check if the complement exists in the hash map + 4. If found, return the current index and the complement's index + 5. If not found, add the current number and its index to the hash map + 6. Continue until a pair is found + + intuition: | + The brute force approach would check every pair of numbers, resulting in O(n²) time. + Instead, we can use a hash map to achieve O(n) time by trading space for speed. + + The key insight is that for each number x, we're looking for (target - x). Rather than + scanning the entire array each time, we store seen numbers in a hash map for O(1) lookup. + + We build the hash map as we go, which elegantly handles the constraint of not using + the same element twice — when we check for a complement, it can only be a previously + seen element. + + common_pitfalls: + - title: Using the same element twice + description: | + Checking if complement exists before adding current element to the map prevents + using the same index twice. If you add first then check, you might match an + element with itself. + wrong_approach: "Adding to map before checking complement" + correct_approach: "Check complement first, then add to map" + + - title: Returning values instead of indices + description: | + The problem asks for indices, not the actual values. Make sure you store and + return the indices from the hash map. + wrong_approach: "return [num, complement]" + correct_approach: "return [seen[complement], i]" + + - title: Forgetting duplicate values + description: | + When there are duplicate values (e.g., [3,3] with target 6), the algorithm + still works because we check for complement before adding to the map. + + key_takeaways: + - Hash maps trade space for time, turning O(n) lookups into O(1) + - Building data structures incrementally can prevent edge cases + - Always clarify whether to return indices or values + - This pattern appears in many "find pair" problems + + time_complexity: "O(n)" + space_complexity: "O(n)" + complexity_explanation: | + Time: We iterate through the array once, and each hash map operation is O(1) average. + Space: In the worst case, we store all n elements in the hash map before finding a match. + +solutions: + - approach_name: Hash Map (Optimal) + is_optimal: true + code: | + def two_sum(nums: list[int], target: int) -> list[int]: + seen = {} + for i, num in enumerate(nums): + complement = target - num + if complement in seen: + return [seen[complement], i] + seen[num] = i + return [] # No solution found + explanation: | + Single pass through the array, storing each number's index. + For each number, check if its complement exists in the map. + + - approach_name: Brute Force + is_optimal: false + code: | + def two_sum(nums: list[int], target: int) -> list[int]: + n = len(nums) + for i in range(n): + for j in range(i + 1, n): + if nums[i] + nums[j] == target: + return [i, j] + return [] + explanation: | + Check every pair of numbers. Simple but inefficient for large inputs. + Time: O(n²), Space: O(1). diff --git a/backend/data/questions/valid-parentheses.yaml b/backend/data/questions/valid-parentheses.yaml new file mode 100644 index 0000000..11402d1 --- /dev/null +++ b/backend/data/questions/valid-parentheses.yaml @@ -0,0 +1,134 @@ +title: Valid Parentheses +slug: valid-parentheses +difficulty: easy +leetcode_id: 20 +leetcode_url: https://leetcode.com/problems/valid-parentheses/ +categories: + - strings + - stack +patterns: + - monotonic-stack + +description: | + Given a string `s` containing just the characters `'('`, `')'`, `'{'`, `'}'`, `'['` and `']'`, determine if the input string is valid. + + An input string is valid if: + 1. Open brackets must be closed by the same type of brackets. + 2. Open brackets must be closed in the correct order. + 3. Every close bracket has a corresponding open bracket of the same type. + +constraints: | + - 1 <= s.length <= 10^4 + - s consists of parentheses only '()[]{}' + +examples: + - input: 's = "()"' + output: "true" + explanation: "Single pair of matching parentheses." + - input: 's = "()[]{}"' + output: "true" + explanation: "Three separate valid pairs." + - input: 's = "(]"' + output: "false" + explanation: "Mismatched bracket types." + - input: 's = "([)]"' + output: "false" + explanation: "Incorrect nesting order." + +explanation: + approach: | + 1. Create a mapping of closing brackets to their opening counterparts + 2. Initialize an empty stack to track opening brackets + 3. Iterate through each character in the string: + - If it's an opening bracket, push it onto the stack + - If it's a closing bracket, check if the stack is empty (invalid) or if the top + of the stack matches the corresponding opening bracket + 4. After processing all characters, the stack should be empty for a valid string + + intuition: | + The key insight is that brackets must be closed in LIFO (Last-In-First-Out) order. + The most recently opened bracket must be closed first, which is exactly what a stack does. + + When we encounter a closing bracket, the most recent unclosed opening bracket (top of stack) + must match it. If they don't match, or if there's no opening bracket to match, the string + is invalid. + + Think of it like nested function calls — the innermost function must return before the + outer one can. + + common_pitfalls: + - title: Forgetting to check empty stack + description: | + When encountering a closing bracket, you must first check if the stack is empty. + If it is, there's no matching opening bracket. + wrong_approach: "Directly checking stack[-1] without empty check" + correct_approach: "Check if stack is empty before accessing stack[-1]" + + - title: Not checking if stack is empty at the end + description: | + After processing all characters, leftover opening brackets in the stack mean + they were never closed. Return stack is empty, not just True. + wrong_approach: "return True after the loop" + correct_approach: "return len(stack) == 0" + + - title: Confusing bracket mapping direction + description: | + Map closing brackets to opening brackets (not vice versa) because we encounter + closing brackets when we need to check for a match. + + key_takeaways: + - Stacks are ideal for matching nested structures + - LIFO order matches the nesting requirement of brackets + - Always check edge cases (empty string, only opening, only closing) + - This pattern extends to validating HTML tags, code blocks, etc. + + time_complexity: "O(n)" + space_complexity: "O(n)" + complexity_explanation: | + Time: We process each character exactly once. + Space: In the worst case (all opening brackets), the stack holds n/2 elements. + +solutions: + - approach_name: Stack (Optimal) + is_optimal: true + code: | + def is_valid(s: str) -> bool: + stack = [] + mapping = {')': '(', '}': '{', ']': '['} + + for char in s: + if char in mapping: + # Closing bracket + if not stack or stack[-1] != mapping[char]: + return False + stack.pop() + else: + # Opening bracket + stack.append(char) + + return len(stack) == 0 + explanation: | + Use a stack to track opening brackets. For each closing bracket, + verify it matches the most recent opening bracket. + + - approach_name: Stack with Early Return + is_optimal: true + code: | + def is_valid(s: str) -> bool: + # Quick check: odd length can never be valid + if len(s) % 2 != 0: + return False + + stack = [] + pairs = {'(': ')', '{': '}', '[': ']'} + + for char in s: + if char in pairs: + stack.append(pairs[char]) + elif not stack or stack.pop() != char: + return False + + return not stack + explanation: | + Optimization: push the expected closing bracket instead of the opening one. + This simplifies the comparison when we encounter a closing bracket.