hard questions

This commit is contained in:
2025-04-28 23:19:49 +01:00
parent c58b0b60aa
commit 70dbc30262
3 changed files with 400 additions and 0 deletions

View 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.

View 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.

View 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".