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