hard questions
This commit is contained in:
122
backend/data/questions/median-of-two-sorted-arrays.yaml
Normal file
122
backend/data/questions/median-of-two-sorted-arrays.yaml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
title: Median of Two Sorted Arrays
|
||||||
|
slug: median-of-two-sorted-arrays
|
||||||
|
difficulty: hard
|
||||||
|
leetcode_id: 4
|
||||||
|
leetcode_url: https://leetcode.com/problems/median-of-two-sorted-arrays/
|
||||||
|
categories:
|
||||||
|
- arrays
|
||||||
|
- binary-search
|
||||||
|
patterns:
|
||||||
|
- binary-search
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Given two sorted arrays `nums1` and `nums2` of size m and n respectively, return the median
|
||||||
|
of the two sorted arrays.
|
||||||
|
|
||||||
|
The overall run time complexity should be O(log(m+n)).
|
||||||
|
|
||||||
|
constraints: |
|
||||||
|
- nums1.length == m
|
||||||
|
- nums2.length == n
|
||||||
|
- 0 <= m <= 1000
|
||||||
|
- 0 <= n <= 1000
|
||||||
|
- 1 <= m + n <= 2000
|
||||||
|
- -10^6 <= nums1[i], nums2[i] <= 10^6
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- input: "nums1 = [1,3], nums2 = [2]"
|
||||||
|
output: "2.0"
|
||||||
|
explanation: "Merged array is [1,2,3]. Median is 2."
|
||||||
|
- input: "nums1 = [1,2], nums2 = [3,4]"
|
||||||
|
output: "2.5"
|
||||||
|
explanation: "Merged array is [1,2,3,4]. Median is (2+3)/2 = 2.5."
|
||||||
|
|
||||||
|
explanation:
|
||||||
|
approach: |
|
||||||
|
1. Binary search on the smaller array for partition point
|
||||||
|
2. Partition both arrays such that left half has (m+n+1)//2 elements
|
||||||
|
3. Check if partition is valid: max(left) <= min(right)
|
||||||
|
4. If valid, compute median from boundary elements
|
||||||
|
5. Adjust binary search bounds based on comparison
|
||||||
|
|
||||||
|
intuition: |
|
||||||
|
The median divides the combined array into two halves of equal size. We don't need to
|
||||||
|
actually merge; we just need to find the correct partition.
|
||||||
|
|
||||||
|
If we choose i elements from nums1 for the left half, we need (m+n+1)//2 - i from nums2.
|
||||||
|
Binary search on i (0 to m) to find where nums1[i-1] <= nums2[j] and nums2[j-1] <= nums1[i].
|
||||||
|
|
||||||
|
This is O(log min(m,n)) since we binary search on the smaller array.
|
||||||
|
|
||||||
|
common_pitfalls:
|
||||||
|
- title: Not handling edge cases at partition
|
||||||
|
description: |
|
||||||
|
When partition is at array boundary (i=0 or i=m), use -inf or inf for boundary values.
|
||||||
|
wrong_approach: "Accessing nums1[i-1] when i=0"
|
||||||
|
correct_approach: "Use float('-inf') if i == 0"
|
||||||
|
|
||||||
|
- title: Binary searching on the longer array
|
||||||
|
description: |
|
||||||
|
Always binary search on the shorter array to ensure valid partition exists
|
||||||
|
and for better efficiency.
|
||||||
|
|
||||||
|
- title: Odd vs even total length
|
||||||
|
description: |
|
||||||
|
For odd total, median is max of left half.
|
||||||
|
For even, it's average of max(left) and min(right).
|
||||||
|
|
||||||
|
key_takeaways:
|
||||||
|
- Binary search on partition, not on values
|
||||||
|
- Partition both arrays to have equal halves
|
||||||
|
- Handle boundary conditions with infinity
|
||||||
|
- O(log min(m,n)) is achievable
|
||||||
|
|
||||||
|
time_complexity: "O(log min(m,n))"
|
||||||
|
space_complexity: "O(1)"
|
||||||
|
complexity_explanation: |
|
||||||
|
Time: Binary search on the smaller array.
|
||||||
|
Space: Only constant extra variables.
|
||||||
|
|
||||||
|
solutions:
|
||||||
|
- approach_name: Binary Search on Partition (Optimal)
|
||||||
|
is_optimal: true
|
||||||
|
code: |
|
||||||
|
def find_median_sorted_arrays(nums1: list[int], nums2: list[int]) -> float:
|
||||||
|
# Ensure nums1 is the smaller array
|
||||||
|
if len(nums1) > len(nums2):
|
||||||
|
nums1, nums2 = nums2, nums1
|
||||||
|
|
||||||
|
m, n = len(nums1), len(nums2)
|
||||||
|
left, right = 0, m
|
||||||
|
half_len = (m + n + 1) // 2
|
||||||
|
|
||||||
|
while left <= right:
|
||||||
|
i = (left + right) // 2 # Partition in nums1
|
||||||
|
j = half_len - i # Partition in nums2
|
||||||
|
|
||||||
|
# Handle edge cases with infinity
|
||||||
|
nums1_left = float('-inf') if i == 0 else nums1[i - 1]
|
||||||
|
nums1_right = float('inf') if i == m else nums1[i]
|
||||||
|
nums2_left = float('-inf') if j == 0 else nums2[j - 1]
|
||||||
|
nums2_right = float('inf') if j == n else nums2[j]
|
||||||
|
|
||||||
|
if nums1_left <= nums2_right and nums2_left <= nums1_right:
|
||||||
|
# Found valid partition
|
||||||
|
if (m + n) % 2 == 1:
|
||||||
|
return max(nums1_left, nums2_left)
|
||||||
|
else:
|
||||||
|
return (max(nums1_left, nums2_left) +
|
||||||
|
min(nums1_right, nums2_right)) / 2
|
||||||
|
|
||||||
|
elif nums1_left > nums2_right:
|
||||||
|
# Too many elements from nums1 in left half
|
||||||
|
right = i - 1
|
||||||
|
else:
|
||||||
|
# Too few elements from nums1 in left half
|
||||||
|
left = i + 1
|
||||||
|
|
||||||
|
return 0.0 # Should never reach here
|
||||||
|
explanation: |
|
||||||
|
Binary search to find correct partition point in the smaller array.
|
||||||
|
Partition is valid when all left elements <= all right elements.
|
||||||
|
Compute median from the four boundary elements.
|
||||||
149
backend/data/questions/merge-k-sorted-lists.yaml
Normal file
149
backend/data/questions/merge-k-sorted-lists.yaml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
title: Merge k Sorted Lists
|
||||||
|
slug: merge-k-sorted-lists
|
||||||
|
difficulty: hard
|
||||||
|
leetcode_id: 23
|
||||||
|
leetcode_url: https://leetcode.com/problems/merge-k-sorted-lists/
|
||||||
|
categories:
|
||||||
|
- linked-lists
|
||||||
|
- heap
|
||||||
|
patterns:
|
||||||
|
- heap
|
||||||
|
|
||||||
|
description: |
|
||||||
|
You are given an array of k linked-lists `lists`, each linked-list is sorted in ascending order.
|
||||||
|
|
||||||
|
Merge all the linked-lists into one sorted linked-list and return it.
|
||||||
|
|
||||||
|
constraints: |
|
||||||
|
- k == lists.length
|
||||||
|
- 0 <= k <= 10^4
|
||||||
|
- 0 <= lists[i].length <= 500
|
||||||
|
- -10^4 <= lists[i][j] <= 10^4
|
||||||
|
- lists[i] is sorted in ascending order
|
||||||
|
- The sum of lists[i].length will not exceed 10^4
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- input: "lists = [[1,4,5],[1,3,4],[2,6]]"
|
||||||
|
output: "[1,1,2,3,4,4,5,6]"
|
||||||
|
explanation: "Merge three sorted lists into one."
|
||||||
|
- input: "lists = []"
|
||||||
|
output: "[]"
|
||||||
|
explanation: "No lists to merge."
|
||||||
|
- input: "lists = [[]]"
|
||||||
|
output: "[]"
|
||||||
|
explanation: "Single empty list."
|
||||||
|
|
||||||
|
explanation:
|
||||||
|
approach: |
|
||||||
|
1. Use a min-heap to track the smallest element among all list heads
|
||||||
|
2. Add the first node from each non-empty list to the heap
|
||||||
|
3. Pop the smallest node, add it to the result
|
||||||
|
4. If that node has a next, add it to the heap
|
||||||
|
5. Continue until heap is empty
|
||||||
|
|
||||||
|
intuition: |
|
||||||
|
At each step, we need to find the minimum among k candidates (the heads of each list).
|
||||||
|
A min-heap gives us this minimum in O(log k) time.
|
||||||
|
|
||||||
|
Since each node is pushed and popped from the heap exactly once, and we have N total nodes,
|
||||||
|
the overall complexity is O(N log k).
|
||||||
|
|
||||||
|
common_pitfalls:
|
||||||
|
- title: Not handling empty lists
|
||||||
|
description: |
|
||||||
|
Some lists might be empty (null). Filter them out or check before adding to heap.
|
||||||
|
wrong_approach: "Adding null to heap"
|
||||||
|
correct_approach: "if node: heappush(...)"
|
||||||
|
|
||||||
|
- title: Heap comparison with ListNode
|
||||||
|
description: |
|
||||||
|
Python's heapq can't compare ListNode objects directly.
|
||||||
|
Either use a tuple (value, index, node) or define __lt__ on ListNode.
|
||||||
|
|
||||||
|
- title: Not advancing the list pointer
|
||||||
|
description: |
|
||||||
|
After adding a node to result, push its next node to the heap, not the same node.
|
||||||
|
|
||||||
|
key_takeaways:
|
||||||
|
- Min-heap efficiently finds minimum among k elements
|
||||||
|
- This is a k-way merge algorithm
|
||||||
|
- Total work is O(N log k) where N is total nodes
|
||||||
|
- Same pattern works for merging k sorted arrays
|
||||||
|
|
||||||
|
time_complexity: "O(N log k)"
|
||||||
|
space_complexity: "O(k)"
|
||||||
|
complexity_explanation: |
|
||||||
|
Time: Each of N nodes is pushed and popped once, each operation is O(log k).
|
||||||
|
Space: Heap holds at most k nodes at any time.
|
||||||
|
|
||||||
|
solutions:
|
||||||
|
- approach_name: Min-Heap (Optimal)
|
||||||
|
is_optimal: true
|
||||||
|
code: |
|
||||||
|
import heapq
|
||||||
|
|
||||||
|
class ListNode:
|
||||||
|
def __init__(self, val=0, next=None):
|
||||||
|
self.val = val
|
||||||
|
self.next = next
|
||||||
|
|
||||||
|
def merge_k_lists(lists: list[ListNode | None]) -> ListNode | None:
|
||||||
|
heap = []
|
||||||
|
|
||||||
|
# Add first node from each list with index for tie-breaking
|
||||||
|
for i, node in enumerate(lists):
|
||||||
|
if node:
|
||||||
|
heapq.heappush(heap, (node.val, i, node))
|
||||||
|
|
||||||
|
dummy = ListNode()
|
||||||
|
current = dummy
|
||||||
|
|
||||||
|
while heap:
|
||||||
|
val, i, node = heapq.heappop(heap)
|
||||||
|
current.next = node
|
||||||
|
current = current.next
|
||||||
|
|
||||||
|
if node.next:
|
||||||
|
heapq.heappush(heap, (node.next.val, i, node.next))
|
||||||
|
|
||||||
|
return dummy.next
|
||||||
|
explanation: |
|
||||||
|
Use heap to always get the smallest current head.
|
||||||
|
Push next node when popping to maintain k candidates.
|
||||||
|
Index in tuple handles equal values (tie-breaking).
|
||||||
|
|
||||||
|
- approach_name: Divide and Conquer
|
||||||
|
is_optimal: true
|
||||||
|
code: |
|
||||||
|
def merge_k_lists(lists: list[ListNode | None]) -> ListNode | None:
|
||||||
|
if not lists:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def merge_two(l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
|
||||||
|
dummy = ListNode()
|
||||||
|
current = dummy
|
||||||
|
|
||||||
|
while l1 and l2:
|
||||||
|
if l1.val <= l2.val:
|
||||||
|
current.next = l1
|
||||||
|
l1 = l1.next
|
||||||
|
else:
|
||||||
|
current.next = l2
|
||||||
|
l2 = l2.next
|
||||||
|
current = current.next
|
||||||
|
|
||||||
|
current.next = l1 or l2
|
||||||
|
return dummy.next
|
||||||
|
|
||||||
|
while len(lists) > 1:
|
||||||
|
merged = []
|
||||||
|
for i in range(0, len(lists), 2):
|
||||||
|
l1 = lists[i]
|
||||||
|
l2 = lists[i + 1] if i + 1 < len(lists) else None
|
||||||
|
merged.append(merge_two(l1, l2))
|
||||||
|
lists = merged
|
||||||
|
|
||||||
|
return lists[0]
|
||||||
|
explanation: |
|
||||||
|
Pair up lists and merge, reducing k to k/2 each round.
|
||||||
|
Same complexity as heap approach but iterative merge logic.
|
||||||
129
backend/data/questions/trapping-rain-water.yaml
Normal file
129
backend/data/questions/trapping-rain-water.yaml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
title: Trapping Rain Water
|
||||||
|
slug: trapping-rain-water
|
||||||
|
difficulty: hard
|
||||||
|
leetcode_id: 42
|
||||||
|
leetcode_url: https://leetcode.com/problems/trapping-rain-water/
|
||||||
|
categories:
|
||||||
|
- arrays
|
||||||
|
- two-pointers
|
||||||
|
- stack
|
||||||
|
patterns:
|
||||||
|
- two-pointers
|
||||||
|
- monotonic-stack
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Given n non-negative integers representing an elevation map where the width of each bar is 1,
|
||||||
|
compute how much water it can trap after raining.
|
||||||
|
|
||||||
|
constraints: |
|
||||||
|
- n == height.length
|
||||||
|
- 1 <= n <= 2 * 10^4
|
||||||
|
- 0 <= height[i] <= 10^5
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- input: "height = [0,1,0,2,1,0,1,3,2,1,2,1]"
|
||||||
|
output: "6"
|
||||||
|
explanation: "6 units of water are trapped between the bars."
|
||||||
|
- input: "height = [4,2,0,3,2,5]"
|
||||||
|
output: "9"
|
||||||
|
explanation: "9 units of water are trapped."
|
||||||
|
|
||||||
|
explanation:
|
||||||
|
approach: |
|
||||||
|
1. Use two pointers from left and right
|
||||||
|
2. Track maximum height seen from each side
|
||||||
|
3. Move the pointer with smaller max height
|
||||||
|
4. Water at current position = max_height - current_height
|
||||||
|
5. Add to total and continue until pointers meet
|
||||||
|
|
||||||
|
intuition: |
|
||||||
|
Water at any position is determined by the minimum of the maximum heights to its left
|
||||||
|
and right, minus the current height.
|
||||||
|
|
||||||
|
With two pointers, we track left_max and right_max. If left_max < right_max, water at
|
||||||
|
the left pointer is limited by left_max (the right side is guaranteed to be at least
|
||||||
|
as tall). We process and move the pointer with the smaller maximum.
|
||||||
|
|
||||||
|
common_pitfalls:
|
||||||
|
- title: Only considering one side
|
||||||
|
description: |
|
||||||
|
Water level is determined by BOTH sides. You need to track maximum from left AND right.
|
||||||
|
wrong_approach: "Only tracking left_max"
|
||||||
|
correct_approach: "Track both left_max and right_max"
|
||||||
|
|
||||||
|
- title: Counting bars instead of water
|
||||||
|
description: |
|
||||||
|
Water trapped at position i is max_height - height[i], not max_height.
|
||||||
|
The bar itself takes up space.
|
||||||
|
|
||||||
|
- title: Not updating max heights
|
||||||
|
description: |
|
||||||
|
Update left_max or right_max before calculating water, not after.
|
||||||
|
|
||||||
|
key_takeaways:
|
||||||
|
- Two pointers eliminate need for O(n) precomputation
|
||||||
|
- Water level = min(left_max, right_max) - current_height
|
||||||
|
- Always process the side with smaller max (guaranteed bound)
|
||||||
|
- This can also be solved with monotonic stack or DP
|
||||||
|
|
||||||
|
time_complexity: "O(n)"
|
||||||
|
space_complexity: "O(1)"
|
||||||
|
complexity_explanation: |
|
||||||
|
Time: Single pass with two pointers.
|
||||||
|
Space: Only a few variables for pointers and max values.
|
||||||
|
|
||||||
|
solutions:
|
||||||
|
- approach_name: Two Pointers (Optimal)
|
||||||
|
is_optimal: true
|
||||||
|
code: |
|
||||||
|
def trap(height: list[int]) -> int:
|
||||||
|
if not height:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
left, right = 0, len(height) - 1
|
||||||
|
left_max, right_max = 0, 0
|
||||||
|
water = 0
|
||||||
|
|
||||||
|
while left < right:
|
||||||
|
if height[left] < height[right]:
|
||||||
|
if height[left] >= left_max:
|
||||||
|
left_max = height[left]
|
||||||
|
else:
|
||||||
|
water += left_max - height[left]
|
||||||
|
left += 1
|
||||||
|
else:
|
||||||
|
if height[right] >= right_max:
|
||||||
|
right_max = height[right]
|
||||||
|
else:
|
||||||
|
water += right_max - height[right]
|
||||||
|
right -= 1
|
||||||
|
|
||||||
|
return water
|
||||||
|
explanation: |
|
||||||
|
Process from both ends. Move the pointer with smaller max height.
|
||||||
|
Add water based on the difference between max height and current height.
|
||||||
|
|
||||||
|
- approach_name: Monotonic Stack
|
||||||
|
is_optimal: false
|
||||||
|
code: |
|
||||||
|
def trap(height: list[int]) -> int:
|
||||||
|
stack = [] # stores indices
|
||||||
|
water = 0
|
||||||
|
|
||||||
|
for i, h in enumerate(height):
|
||||||
|
while stack and h > height[stack[-1]]:
|
||||||
|
top = stack.pop()
|
||||||
|
|
||||||
|
if not stack:
|
||||||
|
break
|
||||||
|
|
||||||
|
width = i - stack[-1] - 1
|
||||||
|
bounded_height = min(h, height[stack[-1]]) - height[top]
|
||||||
|
water += width * bounded_height
|
||||||
|
|
||||||
|
stack.append(i)
|
||||||
|
|
||||||
|
return water
|
||||||
|
explanation: |
|
||||||
|
Stack stores indices of bars in decreasing height order.
|
||||||
|
When a taller bar is found, calculate water trapped in the "valley".
|
||||||
Reference in New Issue
Block a user