feat(content): initial question set
This commit is contained in:
144
backend/data/questions/merge-two-sorted-lists.yaml
Normal file
144
backend/data/questions/merge-two-sorted-lists.yaml
Normal file
@@ -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.
|
||||
117
backend/data/questions/two-sum.yaml
Normal file
117
backend/data/questions/two-sum.yaml
Normal file
@@ -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).
|
||||
134
backend/data/questions/valid-parentheses.yaml
Normal file
134
backend/data/questions/valid-parentheses.yaml
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user