DSA Interview Preparation Guide
DSA Interview Preparation Guide
75 DSA Questions
That Make DSA Easy
A curated, pattern-first path through the 75 problems that unlock
every major coding-interview archetype — built for beginners,
students, and engineers preparing for product-based companies.
75 14 30 Py
Curated Problems Core Patterns Day Plan Python Solutions
PATTERN UNLOCKED 🔓
Every question in this book carries a Pattern Used label. When you meet a NEW problem in a
real interview, your first job is to match it to one of the 14 patterns in the next section — that match
is 80% of the battle.
🔓 Two Pointers
Recognize it when: Array or string is sorted (or can be), and you're comparing/combining elements from
both ends or two speeds.
Signal phrases: “Sorted array” + “pair/triplet that sums to X” or “palindrome check”.
🔓 Sliding Window
Recognize it when: You need a contiguous subarray/substring satisfying a condition, and
shrinking/growing a window is cheaper than recomputation.
Signal phrases: “Longest/shortest/max subarray or substring with condition Y”.
🔓 Prefix Sum
Recognize it when: You need repeated range-sum queries or a running total to detect a target difference.
Signal phrases: “Subarray sums to K” or “range sum queries”.
🔓 Monotonic Stack
Recognize it when: You need the next/previous greater or smaller element for every position in one pass.
Signal phrases: “Next greater element”, “daily temperatures”, “largest rectangle”.
🔓 BFS
Recognize it when: You need the shortest path in an unweighted graph/grid, or level-by-level processing.
Signal phrases: “Shortest path”, “minimum steps”, “level order”, unweighted edges.
🔓 DFS
Recognize it when: You need to explore every path, detect connectivity, or recurse into a tree/graph
structure.
Signal phrases: “Number of islands”, “connected components”, tree traversal.
🔓 Backtracking
Recognize it when: You must generate all valid combinations/permutations/placements and can prune
invalid branches early.
Signal phrases: “All subsets/permutations”, “place N queens”, “word search on a grid”.
🔓 Greedy
Recognize it when: A locally optimal choice at each step provably leads to the global optimum — no
need to explore alternatives.
Signal phrases: “Minimum number of intervals/jumps”, “maximum non-overlapping”.
🔓 1D / 2D DP
Recognize it when: The problem has overlapping subproblems and optimal substructure — the brute
force recomputes the same state repeatedly.
Signal phrases: “Count ways to...”, “minimum/maximum cost to...”, “longest/shortest sequence”.
28 Greedy Q70–Q73
QUESTION 1 OF 75 Easy
Two Sum
Pattern: Hashing (index lookup)
Problem Statement
Given an array of integers nums and an integer target, return the indices of the two numbers that add up
to target. Each input has exactly one solution, and you may not use the same element twice.
Example Constraints
Input: nums = [2,7,11,15], target = 9 2 ≤ [Link] ≤ 10^4 · -10^9 ≤ nums[i] ≤ 10^9 ·
Output: [0,1] (because exactly one valid answer exists
nums[0]+nums[1] == 9)
Brute-Force Approach
Try every pair with two nested loops, checking if they sum to target. O(n²) time, O(1) space.
Optimal Approach
Walk the array once. For each number, check if (target - number) is already in a hash map of
value→index. If yes, you found the pair; otherwise store the current number and its index.
Step-by-Step Intuition
Instead of asking 'what pairs sum to target' after the fact, ask 'what number would I still need' at each
step, and remember every number you've already seen so the lookup is instant.
Dry Run
nums=[2,7,11,15], target=9. i=0, num=2, need=7, map={} → not found, store
{2:0}. i=1, num=7, need=2, map={2:0} → found! return [0,1].
Common Mistakes
KEY TAKEAWAY
When you need to find a complement of the current element, a hash map converts an O(n²) search
into O(n).
QUESTION 2 OF 75 Easy
Contains Duplicate
Pattern: Hashing (set membership)
Problem Statement
Given an integer array nums, return true if any value appears at least twice, and false if every element is
distinct.
Example Constraints
Input: nums = [1,2,3,1] → Output: true 1 ≤ [Link] ≤ 10^5 · -10^9 ≤ nums[i] ≤ 10^9
Input: nums = [1,2,3,4] → Output:
false
Brute-Force Approach
Compare every pair of elements, O(n²); or sort then scan adjacent pairs, O(n log n).
Optimal Approach
Add each number to a hash set; if a number is already in the set, return true immediately.
Step-by-Step Intuition
A set gives O(1) membership checks, so you can detect a repeat the moment it happens instead of
comparing every pair.
Common Mistakes
Sorting first when O(n) is achievable and expected; forgetting early-exit (checking membership before
insertion, not after).
def contains_duplicate(nums):
seen = set()
for num in nums:
if num in seen:
return True
[Link](num)
return False
KEY TAKEAWAY
'Have I seen this before' almost always means: reach for a hash set.
QUESTION 3 OF 75 Easy
Valid Anagram
Pattern: Hashing (frequency count)
Problem Statement
Given two strings s and t, return true if t is an anagram of s (same characters, same counts, possibly
different order).
Example Constraints
Input: s = "anagram", t = "nagaram" → 1 ≤ [Link], [Link] ≤ 5×10^4 · lowercase English
Output: true letters
Input: s = "rat", t = "car" → Output:
false
Brute-Force Approach
Sort both strings and compare, O(n log n).
Step-by-Step Intuition
An anagram means identical multisets of characters. A frequency table is a multiset with O(1) lookups.
Dry Run
s="rat" → counts {r:1,a:1,t:1}. Scan t="car": c not in counts → return False
immediately.
Common Mistakes
Not checking lengths first as a fast fail; using sorted() when interviewer wants O(n); ignoring
Unicode/uppercase edge cases.
KEY TAKEAWAY
Frequency maps let you compare 'same multiset of items' in linear time.
QUESTION 4 OF 75 Medium
Group Anagrams
Pattern: Hashing (canonical key)
Problem Statement
Given an array of strings strs, group the anagrams together. Return the answer in any order.
Example Constraints
1 ≤ [Link] ≤ 10^4 · 0 ≤ strs[i].length ≤ 100 ·
lowercase letters
Brute-Force Approach
Compare every string to every other string for the anagram property, O(n² · k log k).
Optimal Approach
For each string, compute a canonical key (sorted string, or a 26-length count tuple) and group strings
sharing a key in a hash map.
Step-by-Step Intuition
Anagrams share the same sorted form or the same letter-count signature — use that signature as a hash
map key so grouping becomes O(1) per insert.
Dry Run
"eat"→key "aet". "tea"→"aet" (same bucket). "tan"→"ant" (new bucket). Continue
until all strings are bucketed by key.
Common Mistakes
Using sorted string as key when k is large (count-tuple is faster); forgetting strings can be empty; mutating the
input list.
KEY TAKEAWAY
When grouping by a shared property, hash on a canonical signature of that property.
QUESTION 5 OF 75 Medium
Top K Frequent Elements
Pattern: Hashing + Bucket Sort
Problem Statement
Given an integer array nums and integer k, return the k most frequent elements.
Example Constraints
Input: nums = [1,1,1,2,2,3], k = 2 → 1 ≤ [Link] ≤ 10^5 · k is always valid (1 ≤ k ≤
Output: [1,2] number of distinct elements)
Brute-Force Approach
Count frequencies, sort by frequency descending, take top k. O(n log n).
Optimal Approach
Count frequencies with a hash map, then bucket elements by frequency (bucket index = frequency,
capped at n). Walk buckets from high to low frequency collecting k elements.
Step-by-Step Intuition
Frequency can never exceed n, so instead of sorting arbitrary values you can index directly into
'frequency buckets' — an O(n) counting sort.
Dry Run
nums=[1,1,1,2,2,3]. counts={1:3,2:2,3:1}. buckets[3]=[1], buckets[2]=[2],
buckets[1]=[3]. Walk from bucket 6 down: bucket[3]=[1] → take 1, bucket[2]=[2]
→ take 2. k=2 reached → [1,2].
Common Mistakes
Using a full sort when O(n) bucket sort is expected in a follow-up; off-by-one on bucket size (max frequency =
n).
KEY TAKEAWAY
When a value is bounded by n (like frequency), bucket sort beats comparison sort.
QUESTION 6 OF 75 Medium
Product of Array Except Self
Pattern: Prefix / Suffix Products
Problem Statement
Given an integer array nums, return an array answer where answer[i] is the product of all elements
except nums[i], without using division, in O(n) time.
Example Constraints
Input: nums = [1,2,3,4] → Output: 2 ≤ [Link] ≤ 10^5 · product of any prefix/suffix
[24,12,8,6] fits in a 32-bit integer
Brute-Force Approach
For each i, multiply all other elements: O(n²).
Optimal Approach
Build a prefix-product array (product of everything to the left of i) and a suffix-product array (product of
everything to the right), then answer[i] = prefix[i] * suffix[i]. Collapse suffix into a running variable to hit
O(1) extra space.
Step-by-Step Intuition
'Everything except index i' = 'everything before i' × 'everything after i'. Precompute both directions once
instead of recomputing per index.
Dry Run
Common Mistakes
Reaching for division then special-casing zeros (fragile); allocating a separate suffix array when the running-
variable trick avoids it.
def product_except_self(nums):
n = len(nums)
answer = [1] * n
prefix = 1
for i in range(n):
answer[i] = prefix
prefix *= nums[i]
suffix = 1
for i in range(n - 1, -1, -1):
answer[i] *= suffix
suffix *= nums[i]
return answer
KEY TAKEAWAY
When you can't use an operation directly (division), precompute both directions and combine.
QUESTION 7 OF 75 Medium
Subarray Sum Equals K
Pattern: Prefix Sum + Hashing
Problem Statement
Given an array of integers nums and an integer k, return the total number of contiguous subarrays whose
sum equals k.
Example Constraints
Brute-Force Approach
Check every subarray's sum with nested loops: O(n²).
Optimal Approach
Keep a running prefix sum and a hash map of {prefix_sum: count_seen}. At each index, if (running_sum -
k) exists in the map, add its count to the answer — that means some earlier prefix, when removed, leaves
exactly k.
Step-by-Step Intuition
sum(i..j) = prefix[j] - prefix[i-1]. So sum(i..j) == k is the same as prefix[i-1] == prefix[j] - k. Storing how
many times each prefix sum has occurred turns this into an O(1) lookup per index.
Dry Run
nums=[1,1,1], k=2. running=0, map={0:1}. i=0: running=1, need=1-2=-1, not in
map, map={0:1,1:1}. i=1: running=2, need=0, map[0]=1 → count+=1, map=
{0:1,1:1,2:1}. i=2: running=3, need=1, map[1]=1 → count+=1. Total=2.
Common Mistakes
Forgetting to seed the map with {0:1} (accounts for a prefix itself summing to k); recomputing sums from
scratch per subarray.
KEY TAKEAWAY
Whenever you see 'contiguous subarray sums to X', think prefix sum + hash map, not nested loops.
Two Pointers
Two indices moving toward or away from each other collapse nested loops into a single
linear pass.
QUESTION 8 OF 75 Easy
Valid Palindrome
Pattern: Two Pointers (converging)
Problem Statement
Given a string s, return true if it reads the same forward and backward after converting to lowercase and
removing all non-alphanumeric characters.
Example Constraints
Input: s = "A man, a plan, a canal: 1 ≤ [Link] ≤ 2×10^5 · s consists of printable ASCII
Panama" → Output: true characters
Brute-Force Approach
Build a cleaned, lowercased copy of the string, then compare it to its reverse: O(n) time, O(n) space.
Optimal Approach
Two pointers start at both ends; skip non-alphanumeric characters; compare lowercased characters;
converge to the middle.
Step-by-Step Intuition
You don't need a separate cleaned string — walk inward from both sides, skipping junk characters on the
fly, comparing as you go.
Dry Run
s="A man, a plan, a canal: Panama". left=0('A'), right=len-1('a'). Compare
'a'=='a' → match, move both inward, skipping spaces/punctuation as encountered,
until pointers cross — all comparisons match → True.
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
KEY TAKEAWAY
Converging two pointers avoid building an extra copy of the data whenever you're comparing from
both ends.
QUESTION 9 OF 75 Medium
Two Sum II — Sorted Array
Pattern: Two Pointers (converging)
Problem Statement
Given a 1-indexed array of integers numbers sorted in non-decreasing order, find two numbers that add
up to target and return their indices (1-indexed).
Example Constraints
Input: numbers = [2,7,11,15], target = 2 ≤ [Link] ≤ 3×10^4 · numbers is sorted
9 → Output: [1,2] ascending · exactly one solution exists
Brute-Force Approach
For each i, binary search for target - numbers[i]: O(n log n). Or use a hash map (ignoring sortedness):
O(n) time, O(n) space.
Optimal Approach
Step-by-Step Intuition
Because the array is sorted, moving the low pointer only increases the sum and moving the high pointer
only decreases it — so you can always tell which direction to move.
Dry Run
numbers=[2,7,11,15], target=9. left=0(2), right=3(15), sum=17>9 → right--.
right=2(11), sum=2+11=13>9 → right--. right=1(7), sum=2+7=9 → return [1,2] (1-
indexed).
Common Mistakes
Ignoring the sorted property and using the Two Sum hash-map solution (works, but wastes the O(1)-space
opportunity); forgetting the 1-indexed return.
KEY TAKEAWAY
A sorted array is a strong signal to try two pointers before reaching for extra memory.
QUESTION 10 OF 75 Medium
3Sum
Pattern: Two Pointers (fix + converge)
Example Constraints
Input: nums = [-1,0,1,2,-1,-4] → 3 ≤ [Link] ≤ 3000 · -10^5 ≤ nums[i] ≤ 10^5
Output: [[-1,-1,2],[-1,0,1]]
Brute-Force Approach
Three nested loops checking every triplet: O(n³), plus a set to dedupe.
Optimal Approach
Sort the array. Fix the first element, then use two pointers on the remaining sorted subarray to find pairs
summing to -nums[i], skipping duplicates at every level.
Step-by-Step Intuition
Sorting lets you fix one number and reduce the rest to a Two Sum II (converging two pointers) problem —
and sortedness also makes duplicate-skipping trivial (just compare to the previous element).
Dry Run
Sorted: [-4,-1,-1,0,1,2]. i=0(-4): need pair summing to 4 from rest — none
found. i=1(-1): need pair summing to 1: left=2(-1),right=5(2), sum=1 → triplet
[-1,-1,2]; move both, skip dup -1's. left=3(0),right=4(1), sum=1 → triplet
[-1,0,1].
Common Mistakes
Forgetting to skip duplicate values at all three positions (produces duplicate triplets); not sorting first; off-by-
one when advancing pointers past duplicates.
def three_sum(nums):
[Link]()
result = []
n = len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
[Link]([nums[i], nums[left], nums[right]])
left += 1
right -= 1
while left < right and nums[left] == nums[left - 1]:
left += 1
while left < right and nums[right] == nums[right + 1]:
right -= 1
return result
KEY TAKEAWAY
Sorting + fixing one element reduces k-Sum problems to a two-pointer subproblem.
QUESTION 11 OF 75 Medium
Container With Most Water
Pattern: Two Pointers (greedy converge)
Problem Statement
Given n non-negative integers height[i] representing vertical lines, find two lines that together with the x-
axis form a container holding the most water. Return the max area.
Example Constraints
Input: height = [1,8,6,2,5,4,8,3,7] → 2 ≤ [Link] ≤ 10^5 · 0 ≤ height[i] ≤ 10^4
Output: 49
Brute-Force Approach
Try every pair of lines and compute area: O(n²).
Optimal Approach
Step-by-Step Intuition
Width only shrinks as pointers move inward, so area can only grow by increasing the limiting (shorter)
height. Moving the taller line can't help — it's never the bottleneck — so always move the shorter one.
Dry Run
height=[1,8,6,2,5,4,8,3,7]. left=0(1), right=8(7): area=8*1=8, move left
(shorter). left=1(8), right=8(7): area=7*7=49, move right (shorter). Continue —
best found is 49.
Common Mistakes
Moving the taller pointer (breaks the greedy proof, misses the optimum); recomputing area with a nested
loop.
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
area = (right - left) * min(height[left], height[right])
best = max(best, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
KEY TAKEAWAY
When width shrinks monotonically, moving the limiting (smaller) side is the only move that can
improve the result.
QUESTION 12 OF 75 Hard
Trapping Rain Water
Pattern: Two Pointers (running max)
Problem Statement
Given n non-negative integers representing an elevation map, compute how much water it can trap after
raining.
Example Constraints
Input: height = 1 ≤ [Link] ≤ 2×10^4 · 0 ≤ height[i] ≤ 10^5
[0,1,0,2,1,0,1,3,2,1,2,1] → Output: 6
Brute-Force Approach
For each index, scan left and right to find the max wall on each side, water[i] = min(leftMax, rightMax) -
height[i]. O(n²).
Optimal Approach
Precompute leftMax and rightMax arrays in O(n), or use two pointers with running leftMax/rightMax,
moving the pointer on the side with the smaller running max (that side's water level is already
determined).
Step-by-Step Intuition
Water trapped at index i is bounded by the SHORTER of the two tallest walls on either side. Whichever
side currently has the smaller running max, its bound is already fixed — no wall further away on the other
side can lower it — so you can safely resolve that side immediately.
Dry Run
height=[0,1,0,2,1,0,1,3,2,1,2,1]. left=0,right=11, leftMax=0,rightMax=1.
height[left]=0<=leftMax=0 initial... proceeding pointer by pointer accumulates
water=6 total by the time pointers meet.
Common Mistakes
Only tracking one side's max (must track both leftMax and rightMax); off-by-one in when to add water vs
update max.
def trap(height):
if not height:
return 0
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
water = 0
while left < right:
if left_max < right_max:
left += 1
left_max = max(left_max, height[left])
water += left_max - height[left]
else:
right -= 1
right_max = max(right_max, height[right])
water += right_max - height[right]
return water
KEY TAKEAWAY
When a value depends on the min of two running maximums, resolve the side with the smaller max
first — it's already determined.
Sliding Window
A window that grows and shrinks over a sequence — the go-to pattern for
subarray/substring problems.
QUESTION 13 OF 75 Easy
Best Time to Buy and Sell Stock
Pattern: Sliding Window (single pass min-track)
Problem Statement
Given an array prices where prices[i] is the stock price on day i, choose a single day to buy and a later
day to sell to maximize profit. Return the max profit, or 0 if none is possible.
Example Constraints
Input: prices = [7,1,5,3,6,4] → 1 ≤ [Link] ≤ 10^5 · 0 ≤ prices[i] ≤ 10^4
Output: 5 (buy at 1, sell at 6)
Brute-Force Approach
Try every buy-sell pair with nested loops: O(n²).
Optimal Approach
Scan once, tracking the minimum price seen so far and the best profit (current price - min so far) at each
step.
Step-by-Step Intuition
The best sell day only matters relative to the lowest price seen BEFORE it — you never need to look
backward more than once, so a running minimum captures all the history you need.
Dry Run
prices=[7,1,5,3,6,4]. minPrice=7,profit=0. price=1: minPrice=1. price=5:
profit=max(0,5-1)=4. price=3: profit stays 4. price=6: profit=max(4,6-1)=5.
price=4: profit stays 5. Answer=5.
def max_profit(prices):
min_price = float('inf')
profit = 0
for price in prices:
min_price = min(min_price, price)
profit = max(profit, price - min_price)
return profit
KEY TAKEAWAY
A single running minimum (or maximum) turns an O(n²) comparison problem into O(n).
QUESTION 14 OF 75 Medium
Longest Substring Without Repeating Characters
Pattern: Sliding Window (variable size)
Problem Statement
Given a string s, find the length of the longest substring without repeating characters.
Example Constraints
Input: s = "abcabcbb" → Output: 3 0 ≤ [Link] ≤ 5×10^4 · s consists of English letters,
("abc") digits, symbols, and spaces
Brute-Force Approach
Check every substring for uniqueness: O(n³), or O(n²) with a set per starting index.
Optimal Approach
Expand a right pointer over the string; keep a hash set (or last-seen-index map) of characters in the
current window; when a repeat is found, shrink from the left until the repeat is resolved. Track the max
window size seen.
Step-by-Step Intuition
Instead of restarting the window at every repeat, jump the left pointer directly past the previous
occurrence — the window only ever grows or shrinks, it never restarts from scratch.
Dry Run
Common Mistakes
Using a naive left+=1 loop instead of jumping left directly to last_seen[char]+1 (still correct but can be less
efficient to reason about); forgetting the last-seen index might be outside the current window (must check left
<= last_seen[char]).
def length_of_longest_substring(s):
last_seen = {}
left = 0
best = 0
for right, ch in enumerate(s):
if ch in last_seen and last_seen[ch] >= left:
left = last_seen[ch] + 1
last_seen[ch] = right
best = max(best, right - left + 1)
return best
KEY TAKEAWAY
A variable-size window with a map of 'last seen index' avoids restarting the scan on every repeat.
QUESTION 15 OF 75 Medium
Longest Repeating Character Replacement
Pattern: Sliding Window (count-based)
Problem Statement
Given a string s and an integer k, you can replace up to k characters in the string with any other
character. Return the length of the longest substring containing the same letter after such replacements.
Example Constraints
Brute-Force Approach
Try every substring and check if (length - count of most frequent char) ≤ k: O(n²) or O(n³).
Optimal Approach
Sliding window with a character-count map. Track maxFreq (the most frequent character's count in the
current window). If (windowSize - maxFreq) > k, the window is invalid — shrink from the left. Track the
max valid window size.
Step-by-Step Intuition
A window is achievable if you only need to replace 'windowSize - maxFreq' characters — the ones that
AREN'T the majority character. As long as that's ≤ k, the window is valid; the window never needs to
shrink below its best-ever size, only slide.
Dry Run
s="ABAB", k=2. right=0 'A': counts{A:1}, maxFreq=1, size=1, 1-1=0≤2 valid.
right=1 'B': counts{A:1,B:1}, maxFreq=1, size=2, 2-1=1≤2 valid. right=2 'A':
counts{A:2,B:1}, maxFreq=2, size=3, 3-2=1≤2 valid. right=3 'B':
counts{A:2,B:2}, maxFreq=2, size=4, 4-2=2≤2 valid. Answer=4.
Common Mistakes
Shrinking the window even when it's still the best size so far (the window size only needs to be non-
decreasing, not always minimal); recomputing maxFreq by scanning all 26 counts every step (an approximate
running max, never decremented, is sufficient and correct here).
KEY TAKEAWAY
QUESTION 16 OF 75 Hard
Minimum Window Substring
Pattern: Sliding Window (need/have counters)
Problem Statement
Given strings s and t, return the minimum window substring of s that contains every character of t
(including duplicates). Return "" if no such substring exists.
Example Constraints
Input: s = "ADOBECODEBANC", t = "ABC" 1 ≤ [Link], [Link] ≤ 10^5 · s and t consist of
→ Output: "BANC" English letters
Brute-Force Approach
Check every substring of s for containing all of t's characters: O(n³) or O(n²) with counting.
Optimal Approach
Two pointers with a 'need' count map built from t and a 'have' count of the current window. Expand right
until the window satisfies all needs, then shrink left as far as possible while still valid, recording the
smallest valid window throughout.
Step-by-Step Intuition
Track how many distinct required characters are currently satisfied ('formed' counter). Only when formed
== required does shrinking make sense — grow to find validity, shrink to minimize, alternating like a
caterpillar.
Dry Run
s="ADOBECODEBANC", t="ABC". need={A:1,B:1,C:1}. Expand right until window
"ADOBEC" satisfies all three — formed=3. Shrink left: removing 'A' breaks it,
so record window length 6 first, then continue expanding/shrinking — eventually
the tightest valid window found is "BANC" (length 4).
Common Mistakes
Recomputing 'does window contain all of t' from scratch each time (must be an incremental 'formed' counter);
not handling duplicate characters in t correctly (need exact counts, not just presence).
KEY TAKEAWAY
Track validity with an incremental 'formed vs required' counter — never re-scan the whole window
to check a condition.
QUESTION 17 OF 75 Hard
Sliding Window Maximum
Pattern: Monotonic Deque
Problem Statement
Given an array nums and a window size k, return an array of the maximum value in each sliding window
as it moves from left to right across the array.
Example Constraints
Input: nums = [1,3,-1,-3,5,3,6,7], k = 1 ≤ [Link] ≤ 10^5 · 1 ≤ k ≤ [Link]
3 → Output: [3,3,5,5,6,7]
Optimal Approach
Maintain a deque of indices whose values are in decreasing order. For each new element, pop smaller
values from the back (they can never be the max again), push the new index, and pop the front if it's
outside the window. The front of the deque is always the current max.
Step-by-Step Intuition
An element can be discarded forever once a LARGER element appears after it within reach — it will
never be the max of any future window. The deque keeps only 'still possibly useful' candidates in
decreasing order.
Dry Run
nums=[1,3,-1,-3,5,3,6,7], k=3. Process 1,3 — pop 1 (smaller), deque=[3].
Process -1 — deque=[3,-1]. Window [1,3,-1] complete, front value=3 → output 3.
Slide: process -3 — deque=[3,-1,-3], front still index of 3 → output 3. Process
5 — pop -3,-1,3 (all smaller), deque=[5] → output 5. Continue similarly →
[3,3,5,5,6,7].
Common Mistakes
Storing values instead of indices in the deque (can't tell when an element falls outside the window); using a
max-heap (works but O(n log n), not optimal).
KEY TAKEAWAY
A monotonic deque of indices gives O(1) amortized access to a sliding window's max (or min).
Binary Search
Not just for sorted arrays — also for searching an answer space (Binary Search on Answer).
QUESTION 18 OF 75 Easy
Binary Search
Pattern: Binary Search (classic)
Problem Statement
Given a sorted array of distinct integers nums and a target, return the index of target, or -1 if it's not
present. Must run in O(log n).
Example Constraints
Input: nums = [-1,0,3,5,9,12], target 1 ≤ [Link] ≤ 10^4 · nums sorted ascending,
= 9 → Output: 4 distinct values
Brute-Force Approach
Linear scan: O(n).
Optimal Approach
Maintain low/high pointers over the sorted range; compare the midpoint to target; discard the half that
can't contain it; repeat.
Step-by-Step Intuition
Sortedness means comparing to the midpoint tells you definitively which half to discard — each
comparison halves the remaining search space.
Dry Run
nums=[-1,0,3,5,9,12], target=9. low=0,high=5,mid=2(val 3). 3<9 → low=3.
low=3,high=5,mid=4(val 9). Found → return 4.
Common Mistakes
KEY TAKEAWAY
Every binary search is: pick a midpoint, discard the half that can't hold the answer, repeat.
QUESTION 19 OF 75 Medium
Search in Rotated Sorted Array
Pattern: Binary Search (rotated)
Problem Statement
An ascending array is rotated at an unknown pivot. Given the rotated nums and a target, return its index,
or -1 if absent, in O(log n).
Example Constraints
Input: nums = [4,5,6,7,0,1,2], target 1 ≤ [Link] ≤ 5000 · all values distinct · nums
= 0 → Output: 4 is a rotation of an ascending array
Brute-Force Approach
Linear scan for target: O(n).
Optimal Approach
At each midpoint, determine which half (left or right of mid) is properly sorted, then check if target lies
within that sorted half's range to decide which side to discard.
Step-by-Step Intuition
Dry Run
nums=[4,5,6,7,0,1,2], target=0. low=0,high=6,mid=3(val7). Left half [4..7] is
sorted (nums[low]=4<=nums[mid]=7). Is target(0) in [4,7]? No → search right
half: low=4. low=4,high=6,mid=5(val1). Right half [1,2] sorted, target 0 not in
[1,2] → search left: high=4. low=4,high=4,mid=4(val0) → found, return 4.
Common Mistakes
Forgetting to handle the case where low == mid (single element range); comparing target against the wrong
half's bounds; not handling duplicates (which breaks this approach — needs linear fallback).
KEY TAKEAWAY
When an array isn't fully sorted, check which half IS sorted at each step — that half's bounds tell
you where to search.
QUESTION 20 OF 75 Medium
Find Minimum in Rotated Sorted Array
Pattern: Binary Search (rotated)
Problem Statement
Given a rotated ascending array with no duplicates, find the minimum element in O(log n).
Example Constraints
Input: nums = [4,5,6,7,0,1,2] → 1 ≤ [Link] ≤ 5000 · all values unique · array
Output: 0 was originally sorted ascending, then rotated
Brute-Force Approach
Linear scan for the minimum: O(n).
Optimal Approach
Binary search comparing nums[mid] to nums[high]. If nums[mid] > nums[high], the minimum is to the right
of mid; otherwise it's at mid or to the left.
Step-by-Step Intuition
The minimum is the one 'break point' where the ascending order resets. Comparing mid to the rightmost
element tells you which side that break point is on.
Dry Run
nums=[4,5,6,7,0,1,2]. low=0,high=6,mid=3(7). nums[mid]=7 > nums[high]=2 → min
is right of mid → low=4. low=4,high=6,mid=5(1). nums[mid]=1 <= nums[high]=2 →
min at mid or left → high=5. low=4,high=5,mid=4(0). nums[mid]=0<=nums[high]=1 →
high=4. low==high=4 → return nums[4]=0.
Common Mistakes
Comparing to nums[low] instead of nums[high] (breaks the logic in several rotation cases); not converging the
loop condition to low < high correctly.
def find_min(nums):
low, high = 0, len(nums) - 1
while low < high:
mid = (low + high) // 2
if nums[mid] > nums[high]:
low = mid + 1
else:
high = mid
return nums[low]
KEY TAKEAWAY
QUESTION 21 OF 75 Medium
Koko Eating Bananas
Pattern: Binary Search on Answer
Problem Statement
Koko has piles of bananas and h hours before the guards return. Each hour she picks one pile and eats
up to k bananas from it (if the pile has fewer than k, she finishes it and stops for that hour). Find the
minimum integer eating speed k such that she can eat all bananas within h hours.
Example Constraints
Input: piles = [3,6,7,11], h = 8 → 1 ≤ [Link] ≤ 10^4 · [Link] ≤ h ≤ 10^9 · 1 ≤
Output: 4 piles[i] ≤ 10^9
Brute-Force Approach
Try every speed k from 1 upward, checking feasibility each time: O(max(piles) · n).
Optimal Approach
Binary search k between 1 and max(piles). For each candidate k, compute hours needed = sum(ceil(pile /
k) for each pile); if hours ≤ h, k is feasible (try smaller); else try larger.
Step-by-Step Intuition
Feasibility is monotonic: if speed k works, every speed greater than k also works. That monotonic 'yes
region / no region' boundary is exactly what binary search finds — you're searching over possible
ANSWERS, not array indices.
Dry Run
piles=[3,6,7,11], h=8. low=1,high=11. mid=6:
hours=ceil(3/6)+ceil(6/6)+ceil(7/6)+ceil(11/6)=1+1+2+2=6≤8 → feasible, try
smaller: high=6. mid=3: hours=1+2+3+4=10>8 → infeasible, low=4. mid=5:
hours=1+2+2+3=8≤8 → high=5. mid=4: hours=1+2+2+3=8≤8 → high=4. low==high=4 →
answer 4.
Common Mistakes
Forgetting ceiling division (a partial pile still costs a full hour); searching the wrong bounds (low should be 1,
not 0, since speed 0 is invalid).
import math
def min_eating_speed(piles, h):
low, high = 1, max(piles)
while low < high:
mid = (low + high) // 2
hours = sum([Link](pile / mid) for pile in piles)
if hours <= h:
high = mid
else:
low = mid + 1
return low
KEY TAKEAWAY
When feasibility of an answer is monotonic (works → everything bigger also works), binary search
the answer space directly.
QUESTION 22 OF 75 Medium
Find First and Last Position
of Element in Sorted Array
Pattern: Binary Search (leftmost/rightmost bound)
Problem Statement
Given a sorted array nums and a target, find the starting and ending index of target's occurrences. Return
[-1,-1] if target is not found. Must run in O(log n).
Example Constraints
Input: nums = [5,7,7,8,8,10], target = 0 ≤ [Link] ≤ 10^5 · nums sorted ascending,
8 → Output: [3,4] may contain duplicates
Brute-Force Approach
Linear scan to find first and last matching index: O(n).
Optimal Approach
Run two modified binary searches: one that keeps moving left even after finding target (to find the
leftmost occurrence), another that keeps moving right (to find the rightmost).
Step-by-Step Intuition
Dry Run
nums=[5,7,7,8,8,10], target=8. Leftmost search: mid narrows down, each time
nums[mid]==8 records index and moves high=mid-1 to look further left —
converges to index 3. Rightmost search similarly converges to index 4. Output
[3,4].
Common Mistakes
Writing one binary search and trying to reuse it for both bounds without changing the tie-breaking direction;
off-by-one errors when narrowing after a match.
KEY TAKEAWAY
To find a boundary (not just any match), keep searching past a hit in the direction of the boundary
you want.
QUESTION 23 OF 75 Hard
Median of Two Sorted Arrays
Pattern: Binary Search (partition)
Problem Statement
Given two sorted arrays nums1 and nums2 of size m and n, return the median of the combined sorted
array in O(log(min(m,n))) time.
Example Constraints
Input: nums1 = [1,3], nums2 = [2] → 0 ≤ m, n ≤ 1000 · 1 ≤ m+n ≤ 2000 · arrays sorted
Output: 2.0 ascending
Brute-Force Approach
Merge both arrays and take the middle element(s): O(m+n) time, O(m+n) space.
Optimal Approach
Binary search on a PARTITION index in the smaller array. For each partition, compute the matching
partition in the other array such that left-side count == right-side count, then check the boundary values
line up correctly (max of lefts ≤ min of rights). Adjust the partition based on which boundary condition fails.
Step-by-Step Intuition
The median splits the combined array into two equal halves. Binary search directly for the partition point
in the smaller array where 'everything to the left of both partitions' is ≤ 'everything to the right of both
partitions' — no merging needed.
Dry Run
nums1=[1,3], nums2=[2]. Total=3, half=2. Binary search partition in nums1
(smaller). Try partitionX=1: left1=1(val1), right1=3. partitionY=1:
left2=2(val2), right2=+inf. Check max(left1,left2)=2 <= min(right1,right2)=3 →
valid partition. Combined length odd(3) → median = max(left1,left2) = 2.0.
Common Mistakes
Binary searching the larger array (still correct but slower — interviewers want the smaller one for the tight
bound); mishandling empty-partition edge cases (use ±infinity sentinels).
KEY TAKEAWAY
Binary search doesn't need to search 'in' a single array — it can search over a space of valid
partitions or configurations.
Strings
String-specific manipulation: parsing, expansion, and encoding tricks interviewers love.
QUESTION 24 OF 75 Easy
Longest Common Prefix
Pattern: String Scanning
Problem Statement
Given an array of strings strs, find the longest common prefix shared by all of them. Return "" if there is
none.
Example Constraints
Input: strs = 1 ≤ [Link] ≤ 200 · 0 ≤ strs[i].length ≤ 200
["flower","flow","flight"] → Output:
"fl"
Brute-Force Approach
Compare every string pairwise character by character, tracking the shortest common prefix seen: O(S)
where S is total characters, but implemented clumsily can be worse.
Optimal Approach
Scan column by column: for each character position, check if all strings share the same character at that
index; stop at the first mismatch or the shortest string's end.
Step-by-Step Intuition
A common prefix can never be longer than the shortest string, and it breaks at the first column where any
string disagrees — so scan vertically and stop at the first disagreement.
Dry Run
strs=["flower","flow","flight"]. col0: f,f,f match. col1: l,l,l match. col2:
o,o,i — mismatch → stop. Prefix="fl".
Common Mistakes
def longest_common_prefix(strs):
if not strs:
return ""
for i, ch in enumerate(strs[0]):
for s in strs[1:]:
if i >= len(s) or s[i] != ch:
return strs[0][:i]
return strs[0]
KEY TAKEAWAY
When comparing many strings, scan column by column and bail out at the first disagreement.
QUESTION 25 OF 75 Medium
Longest Palindromic Substring
Pattern: Expand Around Center
Problem Statement
Given a string s, return the longest palindromic substring in s.
Example Constraints
Input: s = "babad" → Output: "bab" (or 1 ≤ [Link] ≤ 1000 · s consists of digits and English
"aba") letters
Brute-Force Approach
Check every substring for being a palindrome: O(n³), or O(n²) with DP.
Optimal Approach
For each index, expand outward in both directions treating it as a palindrome center — once for odd-
length palindromes (single center) and once for even-length (between two characters). Track the longest
found.
Step-by-Step Intuition
Every palindrome has a center (a single character, or the gap between two characters). Instead of
checking all O(n²) substrings, try expanding from each of the 2n-1 possible centers and stop as soon as
the expansion breaks.
Common Mistakes
Forgetting even-length palindromes (centers between characters); off-by-one in the expansion bounds check.
def longest_palindrome(s):
def expand(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return s[l + 1:r]
best = ""
for i in range(len(s)):
odd = expand(i, i)
even = expand(i, i + 1)
for cand in (odd, even):
if len(cand) > len(best):
best = cand
return best
KEY TAKEAWAY
Expanding outward from every possible center is a simple O(n²) alternative to palindrome DP.
QUESTION 26 OF 75 Medium
Palindromic Substrings
Pattern: Expand Around Center
Problem Statement
Given a string s, return the number of palindromic substrings in it (different positions count separately
even if the substring text repeats).
Example Constraints
Brute-Force Approach
Check every substring for being a palindrome: O(n³).
Optimal Approach
For each of the 2n-1 centers, expand outward and count every successful expansion as one more
palindrome.
Step-by-Step Intuition
Every valid expansion step from a center IS a distinct palindromic substring — so counting successful
expansions directly counts palindromes, no separate verification needed.
Dry Run
s="aaa". Center0('a'): expands once (just "a") — count 1. Center1('a'): expands
to "a", then "aaa" — count 2. Center2('a'): "a" — count 1. Even centers (0,1):
"aa" — count1. Even centers(1,2): "aa" — count1. Total=1+2+1+1+1=6.
Common Mistakes
Forgetting even-length centers; double counting or under counting by mismanaging the expansion loop's
stopping condition.
def count_substrings(s):
def expand(l, r):
count = 0
while l >= 0 and r < len(s) and s[l] == s[r]:
count += 1
l -= 1
r += 1
return count
total = 0
for i in range(len(s)):
total += expand(i, i)
total += expand(i, i + 1)
return total
KEY TAKEAWAY
Counting successful center-expansions is equivalent to counting all palindromic substrings.
Problem Statement
Implement atoi to convert a string to a 32-bit signed integer, following: skip leading whitespace, an
optional +/- sign, then digits until a non-digit; clamp to the 32-bit signed integer range; return 0 if no valid
conversion exists.
Example Constraints
Input: s = " -42" → Output: -42 0 ≤ [Link] ≤ 200 · s may contain letters, digits,
Input: s = "4193 with words" → Output: spaces, '+', '-', '.'
4193
Brute-Force Approach
N/A — this problem is inherently about correct sequential parsing, not algorithmic optimization.
Optimal Approach
Walk the string once: skip leading spaces, capture an optional sign, accumulate digits while clamping
against INT_MAX/INT_MIN as you go, then stop at the first non-digit.
Step-by-Step Intuition
Treat it as a small state machine: whitespace → sign → digits → stop. Clamp during accumulation (not
after) to avoid overflow in languages with fixed-width integers.
Dry Run
s=" -42". Skip 3 spaces. See '-' → sign=-1. Accumulate digits '4' then '2' →
num=42. End of string → result = -1*42 = -42.
Common Mistakes
Not clamping to INT_MIN/INT_MAX; not stopping at the first non-digit character (e.g. "4193 with words" must
stop at the space); mishandling '+' vs '-' vs no sign; not skipping leading whitespace only (not embedded
whitespace).
def my_atoi(s):
i, n = 0, len(s)
while i < n and s[i] == ' ':
i += 1
if i == n:
return 0
sign = 1
if s[i] in '+-':
sign = -1 if s[i] == '-' else 1
i += 1
num = 0
INT_MAX, INT_MIN = 2**31 - 1, -2**31
while i < n and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
if sign * num > INT_MAX:
return INT_MAX
if sign * num < INT_MIN:
return INT_MIN
return sign * num
KEY TAKEAWAY
When a problem is 'implement this spec correctly', enumerate every edge case as an explicit state
before writing code.
QUESTION 28 OF 75 Medium
Encode and Decode Strings
Pattern: Delimiter Design
Problem Statement
Design an algorithm to encode a list of strings into a single string, and decode that string back into the
original list of strings. Any characters (including delimiters) may appear inside the strings.
Example Constraints
Input: ["hello","world"] → encode → strings may contain any character, including the
"5#hello5#world" → decode → delimiter you might otherwise choose · must round-
["hello","world"] trip exactly
Brute-Force Approach
Join with a common delimiter like a comma — breaks if a string itself contains a comma.
Optimal Approach
Step-by-Step Intuition
Any single-character delimiter can collide with the data. But a LENGTH is never ambiguous — encode
'read exactly N characters next' and you never need to worry about what's inside those N characters.
Dry Run
Encode ["hello","world"]: "5#hello" + "5#world" = "5#hello5#world". Decode:
read digits until '#' → 5, read next 5 chars → "hello", advance; read digits
until '#' → 5, read next 5 chars → "world". Result: ["hello","world"].
Common Mistakes
Choosing a fixed delimiter character that could appear in the data (fragile); forgetting to handle empty strings
in the list.
def encode(strs):
return ''.join(f"{len(s)}#{s}" for s in strs)
def decode(s):
result = []
i = 0
while i < len(s):
j = i
while s[j] != '#':
j += 1
length = int(s[i:j])
start = j + 1
[Link](s[start:start + length])
i = start + length
return result
KEY TAKEAWAY
Length-prefixing beats delimiter characters whenever the data itself can contain arbitrary
characters.
Linked Lists
Pointer manipulation without index access — reversal and fast/slow pointer tricks dominate.
QUESTION 29 OF 75 Easy
Reverse Linked List
Pattern: Iterative Pointer Reversal
Problem Statement
Given the head of a singly linked list, reverse the list and return the new head.
Example Constraints
Input: 1 -> 2 -> 3 -> nullptr → 0 ≤ number of nodes ≤ 5000
Output: 3 -> 2 -> 1 -> nullptr
Brute-Force Approach
Push all values onto a stack (or into an array), then rebuild a new list in reverse: O(n) time, O(n) space.
Optimal Approach
Walk the list once with a 'prev' pointer starting at None. At each node, save the next node, point
[Link] back to prev, then advance both prev and current.
Step-by-Step Intuition
You only need to flip each node's single outgoing pointer to point backward instead of forward — no extra
storage required, just careful pointer bookkeeping as you walk.
Dry Run
1->2->3->None. prev=None,cur=1. next=2, [Link]=None(prev), prev=1,cur=2.
next=3, [Link]=1, prev=2,cur=3. next=None, [Link]=2, prev=3,cur=None. Loop
ends, return prev=3 → 3->2->1->None.
Common Mistakes
class ListNode:
def __init__(self, val=0, next=None):
[Link] = val
[Link] = next
def reverse_list(head):
prev = None
cur = head
while cur:
nxt = [Link]
[Link] = prev
prev = cur
cur = nxt
return prev
KEY TAKEAWAY
Reversing a singly linked list is three pointers (prev, current, next) walked in lockstep — memorize
this cold.
QUESTION 30 OF 75 Easy
Linked List Cycle
Pattern: Fast & Slow Pointers
Problem Statement
Given the head of a linked list, determine if it has a cycle (some node's next eventually points back to a
previous node).
Example Constraints
Input: 3 -> 2 -> 0 -> -4 -> (back to 0 ≤ number of nodes ≤ 10^4
2) → Output: true
Brute-Force Approach
Store every visited node in a hash set; if you revisit a node, a cycle exists: O(n) time, O(n) space.
Optimal Approach
Step-by-Step Intuition
In a cycle, the faster pointer gains one step on the slower pointer every iteration and is trapped inside the
same finite loop — it's mathematically guaranteed to lap and meet the slow pointer eventually.
Dry Run
3->2->0->-4->(cycle to 2). slow=3,fast=3. Step1: slow=2,fast=0. Step2:
slow=0,fast=2(via cycle). Step3: slow=-4,fast=-4 → slow==fast → cycle detected,
return True.
Common Mistakes
Not checking fast and [Link] for None before advancing (causes a null pointer error); using a hash set
when O(1) space is explicitly requested.
def has_cycle(head):
slow = fast = head
while fast and [Link]:
slow = [Link]
fast = [Link]
if slow == fast:
return True
return False
KEY TAKEAWAY
A pointer moving twice as fast as another will always catch up inside a cycle — that's the whole
trick.
QUESTION 31 OF 75 Easy
Middle of the Linked List
Pattern: Fast & Slow Pointers
Problem Statement
Example Constraints
Input: 1 -> 2 -> 3 -> 4 -> 5 → Output: 1 ≤ number of nodes ≤ 100
node with value 3
Input: 1 -> 2 -> 3 -> 4 -> 5 -> 6 →
Output: node with value 4
Brute-Force Approach
Traverse once to count length n, traverse again to node n//2: O(n) time but two passes.
Optimal Approach
Fast and slow pointers both start at head; slow moves 1 step, fast moves 2 steps. When fast reaches the
end, slow is at the middle.
Step-by-Step Intuition
By the time the fast pointer (moving 2x speed) has covered the whole list, the slow pointer (moving 1x
speed) has covered exactly half of it — a single pass finds the midpoint.
Dry Run
1->2->3->4->5. slow=1,fast=1. step: slow=2,fast=3. step: slow=3,fast=5.
[Link] is None → stop. Return slow=3 (the middle).
Common Mistakes
Using two separate passes when one is expected; getting the 'first vs second middle' convention wrong for
even-length lists.
def middle_node(head):
slow = fast = head
while fast and [Link]:
slow = [Link]
fast = [Link]
return slow
KEY TAKEAWAY
Fast/slow pointers find the midpoint of a list in one pass without knowing its length in advance.
Problem Statement
Given the heads of two sorted linked lists, merge them into one sorted list by splicing the existing nodes
together, and return the head.
Example Constraints
Input: l1 = 1->2->4, l2 = 1->3->4 → 0 ≤ nodes in each list ≤ 50 · both lists sorted
Output: 1->1->2->3->4->4 ascending
Brute-Force Approach
Extract all values into an array, sort, rebuild a new list: O(n log n) time, O(n) space.
Optimal Approach
Use a dummy head node and a tail pointer. Repeatedly compare the fronts of both lists, splice the smaller
node onto the result, and advance that list's pointer. Attach whichever list remains at the end.
Step-by-Step Intuition
Both lists are already sorted, so you never need to look ahead — just always take whichever current
node is smaller, exactly like the merge step of merge sort.
Dry Run
l1=1->2->4, l2=1->3->4. dummy->_. Compare 1,1: take l1's 1 (tie-break
arbitrary) → result:1. Compare 2,1: take l2's 1 → result:1->1. Compare 2,3:
take 2 → result:...->2. Compare 4,3: take 3 → ...->3. Compare 4,4: take 4, then
attach remaining 4 → final: 1->1->2->3->4->4.
Common Mistakes
Building brand-new nodes instead of splicing existing ones (wastes memory); forgetting to attach the
remaining tail of whichever list isn't exhausted first.
KEY TAKEAWAY
A dummy head node simplifies list-building code by removing all 'is this the first node' special cases.
QUESTION 33 OF 75 Medium
Remove Nth Node From End of List
Pattern: Two Pointers (fixed gap)
Problem Statement
Given the head of a linked list, remove the nth node from the end of the list and return the head, in a
single pass.
Example Constraints
Input: head = 1->2->3->4->5, n = 2 → 1 ≤ number of nodes ≤ 30 · 0 ≤ n ≤ number of nodes
Output: 1->2->3->5
Brute-Force Approach
Traverse once to find length L, then traverse again to node (L - n - 1) to unlink the target: O(n) but two
passes.
Optimal Approach
Use a dummy head. Advance a 'fast' pointer n+1 steps ahead of a 'slow' pointer, then move both together
until fast reaches the end — slow now sits right before the node to remove.
Step-by-Step Intuition
Keeping a fixed gap of n+1 nodes between two pointers means that when the front pointer runs out of list,
the back pointer is exactly n nodes from the end — achieved in one pass.
Dry Run
Common Mistakes
Off-by-one on how far ahead to advance the fast pointer (should be n+1 steps from the dummy, not n);
forgetting the dummy head, which is what makes removing the actual head node (n == length) work cleanly.
KEY TAKEAWAY
A fixed-gap two-pointer walk finds a position relative to the END of a list without a length
precomputation.
QUESTION 34 OF 75 Medium
Reorder List
Pattern: Fast/Slow + Reverse + Merge
Problem Statement
Given the head of a singly linked list L0 -> L1 -> ... -> Ln, reorder it in place to: L0 -> Ln -> L1 -> Ln-1 ->
L2 -> Ln-2 -> ... You may not modify node values, only pointers.
Example Constraints
Input: 1->2->3->4 → Output: 1->4->2->3 1 ≤ number of nodes ≤ 5×10^4
Optimal Approach
1) Find the middle with fast/slow pointers. 2) Reverse the second half in place. 3) Merge the first half and
reversed second half by alternating nodes.
Step-by-Step Intuition
The target order is 'first half, interleaved with the reversed second half' — so split the list in half, reverse
the tail, and zipper-merge the two halves together.
Dry Run
1->2->3->4. Middle found at node 2 (slow) via fast/slow — split into 1->2 and
3->4. Reverse second half: 4->3. Merge alternating: take 1, take 4, take 2,
take 3 → 1->4->2->3.
Common Mistakes
Not cleanly splitting the list into two halves (off-by-one on where the split happens for odd vs even length);
forgetting to terminate the first half's list with None before merging (creates a cycle).
def reorder_list(head):
if not head or not [Link]:
return
slow = fast = head
while fast and [Link]:
slow = [Link]
fast = [Link]
second = [Link]
[Link] = None
prev = None
while second:
nxt = [Link]
[Link] = prev
prev = second
second = nxt
first, second = head, prev
while second:
n1, n2 = [Link], [Link]
[Link] = second
[Link] = n1
first, second = n1, n2
QUESTION 35 OF 75 Easy
Valid Parentheses
Pattern: Stack (matching pairs)
Problem Statement
Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid
(every open bracket is closed by the same type in the correct order).
Example Constraints
Input: s = "()[]{}" → Output: true 1 ≤ [Link] ≤ 10^4 · s consists only of bracket
Input: s = "(]" → Output: false characters
Brute-Force Approach
Repeatedly find and remove adjacent matching pairs like "()" from the string until no more can be
removed, then check if empty: O(n²) due to repeated scanning.
Optimal Approach
Push open brackets onto a stack. On a closing bracket, pop the stack and check it matches the expected
open bracket type; if not (or stack is empty), the string is invalid. At the end, the stack must be empty.
Step-by-Step Intuition
The most recently opened bracket must be the next one closed — that 'last opened, first closed' behavior
is exactly a stack (LIFO).
Dry Run
s="()[]{}": '(' push. ')' pop '(' — match. '[' push. ']' pop '[' — match. '{'
push. '}' pop '{' — match. Stack empty at end → True.
Common Mistakes
def is_valid(s):
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
if ch in pairs:
if not stack or [Link]() != pairs[ch]:
return False
else:
[Link](ch)
return not stack
KEY TAKEAWAY
Any 'most recent must be resolved first' matching problem is a stack problem.
QUESTION 36 OF 75 Medium
Min Stack
Pattern: Stack (auxiliary tracking)
Problem Statement
Design a stack that supports push, pop, top, and retrieving the minimum element, all in O(1) time.
Example Constraints
push(-2), push(0), push(-3), getMin() methods called: -2^31 ≤ val ≤ 2^31-1 ·
→ -3, pop(), top() → 0, getMin() → -2 pop/top/getMin always called on a non-empty stack
Brute-Force Approach
Recompute the minimum by scanning the whole stack every time getMin is called: O(n) per call.
Optimal Approach
Maintain a second stack that tracks the minimum value at each corresponding depth of the main stack;
push/pop both stacks in lockstep.
Step-by-Step Intuition
Instead of recomputing the min, just remember what the min WAS at every point in the stack's history —
a parallel min-stack gives O(1) access to 'the min if I were to pop back to here'.
Common Mistakes
Storing the min only once instead of once per push (breaks when popping); forgetting to pop the min-stack in
lockstep with the main stack.
class MinStack:
def __init__(self):
[Link] = []
self.min_stack = []
def pop(self):
[Link]()
self.min_stack.pop()
def top(self):
return [Link][-1]
def get_min(self):
return self.min_stack[-1]
KEY TAKEAWAY
A parallel auxiliary stack lets you answer 'what was true at this depth' in O(1), without
recomputation.
QUESTION 37 OF 75 Medium
Evaluate Reverse Polish Notation
Pattern: Stack (expression evaluation)
Problem Statement
Evaluate the value of an arithmetic expression given in Reverse Polish Notation (postfix), where tokens
are integers or one of + - * /.
Example Constraints
Input: tokens = ["2","1","+","3","*"] 1 ≤ [Link] ≤ 10^4 · division truncates toward
→ Output: 9 ((2+1)*3) zero · the expression is always valid
Brute-Force Approach
N/A — postfix evaluation is inherently a stack-based process; there isn't a meaningfully different brute
force.
Optimal Approach
Push numbers onto a stack. When an operator is seen, pop the top two numbers, apply the operator, and
push the result back.
Step-by-Step Intuition
Postfix notation is DESIGNED so that operands always appear before their operator, meaning the two
most recently pushed values are always exactly the operator's operands.
Dry Run
tokens=["2","1","+","3","*"]. push 2 → [2]. push 1 → [2,1]. '+' → pop 1,2 →
2+1=3, push → [3]. push 3 → [3,3]. '*' → pop 3,3 → 3*3=9, push → [9]. Result=9.
Common Mistakes
Popping operands in the wrong order for non-commutative operators (subtraction/division — the FIRST
popped value is the right operand, not the left); not truncating division toward zero as specified (Python's //
rounds toward negative infinity, requiring int(a/b) instead).
def eval_rpn(tokens):
stack = []
ops = {'+', '-', '*', '/'}
for token in tokens:
if token in ops:
b = [Link]()
a = [Link]()
if token == '+':
[Link](a + b)
elif token == '-':
[Link](a - b)
elif token == '*':
[Link](a * b)
else:
[Link](int(a / b))
else:
[Link](int(token))
return stack[0]
KEY TAKEAWAY
Postfix expressions evaluate directly with a stack — no parsing or precedence rules needed.
QUESTION 38 OF 75 Medium
Daily Temperatures
Pattern: Monotonic Stack
Problem Statement
Given an array of daily temperatures, return an array answer where answer[i] is the number of days you'd
have to wait after day i to get a warmer temperature. If none exists, answer[i] = 0.
Example Constraints
Input: temperatures = 1 ≤ [Link] ≤ 10^5 · 30 ≤
[73,74,75,71,69,72,76,73] → Output: temperatures[i] ≤ 100
[1,1,4,2,1,1,0,0]
Brute-Force Approach
For each day, scan forward until a warmer day is found: O(n²).
Optimal Approach
Maintain a stack of indices with decreasing temperatures. For each new day, while the current
temperature is warmer than the temperature at the stack's top index, pop it and record the day-difference
as the answer for that popped index; then push the current index.
Step-by-Step Intuition
Dry Run
temps=[73,74,75,71,69,72,76,73]. i=0(73): stack=[0]. i=1(74)>73 → pop 0,
answer[0]=1-0=1, stack=[1]. i=2(75)>74 → pop1, answer[1]=2-1=1, stack=[2].
i=3(71): stack=[2,3]. i=4(69): stack=[2,3,4]. i=5(72)>69,>71 →
pop4(answer[4]=1), pop3(answer[3]=2), stack=[2,5]. i=6(76)>72,>75 →
pop5(answer[5]=1), pop2(answer[2]=4), stack=[6]. i=7(73): stack=[6,7].
Remaining unresolved get 0. Final=[1,1,4,2,1,1,0,0].
Common Mistakes
Using the brute-force nested loop when O(n) is clearly expected; storing temperatures instead of indices on
the stack (you need the index to compute the day-difference).
def daily_temperatures(temperatures):
n = len(temperatures)
answer = [0] * n
stack = []
for i, temp in enumerate(temperatures):
while stack and temperatures[stack[-1]] < temp:
j = [Link]()
answer[j] = i - j
[Link](i)
return answer
KEY TAKEAWAY
'Next greater/smaller element' problems are solved in one pass with a monotonic stack of indices.
QUESTION 39 OF 75 Hard
Largest Rectangle in Histogram
Pattern: Monotonic Stack
Problem Statement
Example Constraints
Input: heights = [2,1,5,6,2,3] → 1 ≤ [Link] ≤ 10^5 · 0 ≤ heights[i] ≤ 10^4
Output: 10
Brute-Force Approach
For every pair of bars, find the shortest bar between them and compute the area: O(n²), or for each bar
expand outward while height stays sufficient: also O(n²) worst case.
Optimal Approach
Maintain a monotonic increasing stack of bar indices. When a shorter bar is encountered, pop taller bars
off the stack, computing the area they could form using the current index as the right boundary and the
new stack top as the left boundary.
Step-by-Step Intuition
A bar's maximal rectangle extends left and right until it hits a SHORTER bar. A monotonic stack naturally
identifies, for each bar being popped, exactly where the nearest shorter bar on both sides is — giving the
width directly.
Dry Run
heights=[2,1,5,6,2,3]. i=0(2): stack=[0]. i=1(1)<2 → pop0, width=i-(-1)=1(no
left bound so -1), area=2*1=2. stack=[1]. i=2(5): stack=[1,2]. i=3(6): stack=
[1,2,3]. i=4(2)<6 → pop3, width=4-2-1=1,area=6. <5 → pop2,width=4-1-1=2,area=10
(max so far). stack=[1,4]. i=5(3): stack=[1,4,5]. End: pop remaining —
5(h=3,width=6-4-1=1,area=3), 4(h=2,width=6-1-1=4,area=8), 1(h=1,width=6-
(-1)-1=6,area=6). Max area overall = 10.
Common Mistakes
Forgetting to process the remaining stack after the main loop (bars that never got popped during the scan);
miscalculating width when the stack becomes empty (no left boundary — use -1 as a sentinel).
def largest_rectangle_area(heights):
stack = []
max_area = 0
for i, h in enumerate(heights + [0]):
while stack and heights[stack[-1]] >= h:
height = heights[[Link]()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
[Link](i)
return max_area
KEY TAKEAWAY
A monotonic stack finds, for each bar, exactly how far it can extend before hitting a shorter neighbor
— in one pass.
QUESTION 40 OF 75 Easy
Invert Binary Tree
Pattern: DFS (recursive)
Problem Statement
Given the root of a binary tree, invert it (swap every left and right child) and return the root.
Example Constraints
Input: root = [4,2,7,1,3,6,9] → 0 ≤ number of nodes ≤ 100
Output: [4,7,2,9,6,3,1]
Brute-Force Approach
N/A — the recursive solution IS the natural/optimal approach here.
Optimal Approach
Recursively invert the left and right subtrees, then swap them at the current node.
Step-by-Step Intuition
Inverting a tree means every node's children get swapped, at every level — which is naturally expressed
as 'invert my children, then swap them' applied recursively down to the leaves.
Dry Run
root=4, left=2, right=7. invert(2)→swaps 2's children(1,3)→3 becomes left,1
becomes right. invert(7)→swaps(6,9)→9 left,6 right. Then swap root's children:
[Link]=invertedRight(7-tree), [Link]=invertedLeft(2-tree). Final tree
mirrored.
Common Mistakes
class TreeNode:
def __init__(self, val=0, left=None, right=None):
[Link] = val
[Link] = left
[Link] = right
def invert_tree(root):
if not root:
return None
[Link], [Link] = invert_tree([Link]), invert_tree([Link])
return root
KEY TAKEAWAY
Most tree transformations are 'recurse on children, then combine' — trust the recursion to handle
subtrees.
QUESTION 41 OF 75 Easy
Maximum Depth of Binary Tree
Pattern: DFS (recursive)
Problem Statement
Given the root of a binary tree, return its maximum depth (the number of nodes along the longest path
from root to the farthest leaf).
Example Constraints
Input: root = [3,9,20,null,null,15,7] 0 ≤ number of nodes ≤ 10^4
→ Output: 3
Brute-Force Approach
N/A — recursion is the natural approach; an iterative BFS level-count is an equally valid alternative.
Optimal Approach
Recursively compute the depth of the left and right subtrees, and return 1 + max(leftDepth, rightDepth).
Step-by-Step Intuition
Dry Run
root=3(children 9,20). depth(9)=1(leaf).
depth(20)=1+max(depth(15),depth(7))=1+max(1,1)=2. depth(3)=1+max(1,2)=3.
Common Mistakes
Off-by-one (counting edges instead of nodes, or vice versa — clarify the definition with the interviewer);
forgetting the base case for an empty tree (depth 0).
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth([Link]), max_depth([Link]))
KEY TAKEAWAY
Tree depth/height problems recurse to the leaves and combine with a simple '1 + max/min of
children'.
QUESTION 42 OF 75 Medium
Diameter of Binary Tree
Pattern: DFS (post-order accumulate global)
Problem Statement
Given the root of a binary tree, return the length (in edges) of the longest path between any two nodes in
the tree. This path may or may not pass through the root.
Example Constraints
Input: root = [1,2,3,4,5] → Output: 3 1 ≤ number of nodes ≤ 10^4
(path 4-2-1-3 or 5-2-1-3)
Brute-Force Approach
Optimal Approach
Single DFS pass: at each node, compute left height and right height recursively; update a global/nonlocal
'best diameter' with leftHeight + rightHeight (the longest path THROUGH this node); return 1 +
max(leftHeight, rightHeight) as this node's own height to the caller.
Step-by-Step Intuition
The height computation (needed anyway for recursion) is exactly what's needed at every node to also
check 'is the best path through me the new global best' — do both in the same single pass instead of
recomputing heights per node.
Dry Run
root=1(left=2,right=3), 2 has children 4,5. At leaf 4: height=1. At leaf5:
height=1. At node2: leftH=1,rightH=1, diameter candidate=1+1=2, returns
height=1+max(1,1)=2. At leaf3: height=1. At root1: leftH=2(from
node2),rightH=1(from node3), diameter candidate=2+1=3 → new global best=3.
Common Mistakes
Recomputing height as a separate top-level call per node (leads to O(n²)); confusing 'diameter in nodes' vs
'diameter in edges' (this problem counts edges).
def diameter_of_binary_tree(root):
best = [0]
def height(node):
if not node:
return 0
left = height([Link])
right = height([Link])
best[0] = max(best[0], left + right)
return 1 + max(left, right)
height(root)
return best[0]
KEY TAKEAWAY
When a recursive helper already computes what you need locally, piggyback a global best-tracker
on the same pass instead of a second traversal.
Problem Statement
Given the root of a binary tree, return the level order traversal of its nodes' values (i.e., from left to right,
level by level), as a list of lists (one list per level).
Example Constraints
Input: root = [3,9,20,null,null,15,7] 0 ≤ number of nodes ≤ 2000
→ Output: [[3],[9,20],[15,7]]
Brute-Force Approach
DFS while tracking depth and appending to the appropriate level's list — also valid, but BFS is the natural
fit.
Optimal Approach
Use a queue. Process one full level at a time: record the current queue size, pop that many nodes, collect
their values, and push their children for the next level.
Step-by-Step Intuition
A queue processes nodes in the order they were discovered (FIFO), which naturally corresponds to level-
by-level order — snapshotting the queue's size before pushing children isolates exactly one level per
iteration.
Dry Run
queue=[3]. Level size=1: pop 3, record [3], push 9,20. queue=[9,20]. Level
size=2: pop 9,20, record [9,20], push 20's children 15,7 (9 has none). queue=
[15,7]. Level size=2: pop 15,7, record [15,7], no children. Result=[[3],[9,20],
[15,7]].
Common Mistakes
Not snapshotting the queue's length before the inner loop (mixes levels together); using DFS without tracking
depth (produces the wrong grouping if not handled carefully).
KEY TAKEAWAY
Snapshot the queue's size before the inner loop to process exactly one BFS level at a time.
QUESTION 44 OF 75 Medium
Validate Binary Search Tree
Pattern: DFS (range bounds)
Problem Statement
Given the root of a binary tree, determine if it is a valid binary search tree (every node's value is strictly
greater than all values in its left subtree, and strictly less than all values in its right subtree).
Example Constraints
Input: root = [5,1,4,null,null,3,6] → 1 ≤ number of nodes ≤ 10^4 · -2^31 ≤ [Link] ≤
Output: false (4 is a right child of 2^31-1
5, but 4 < 5)
Brute-Force Approach
Just check [Link] < [Link] < [Link] locally at every node — WRONG, because it misses
violations from grandchildren further down (a node deep in the left subtree could still exceed the root).
Optimal Approach
Recursively pass down a valid (low, high) range for each node. A node must fall strictly within its allowed
range; recurse left with an updated upper bound (current value) and right with an updated lower bound
(current value).
Dry Run
root=5(range -inf,inf) → valid. left=1(range -inf,5) → valid. right=4(range
5,inf) → 4 is NOT > 5 → invalid → return False immediately.
Common Mistakes
Only checking a node against its immediate parent/children (the classic wrong-but-common mistake); not
handling duplicate values correctly (a BST here requires STRICT inequality).
KEY TAKEAWAY
BST validity depends on ALL ancestors, not just the parent — pass the accumulated valid range
down the recursion.
QUESTION 45 OF 75 Medium
Lowest Common Ancestor of a BST
Pattern: BST Property Traversal
Problem Statement
Given a binary search tree and two nodes p and q, find their lowest common ancestor (the deepest node
that has both p and q as descendants).
Example Constraints
Brute-Force Approach
Find the path from root to p and root to q separately (O(h) each), then compare the paths to find where
they diverge: O(h) time, O(h) space for storing paths.
Optimal Approach
Starting at root, if both p and q are smaller than the current node, go left; if both are larger, go right;
otherwise (they split, or one equals current), the current node IS the LCA.
Step-by-Step Intuition
The BST property tells you directionally where p and q live relative to any node — the LCA is exactly the
first node where p and q 'part ways' (one goes left, one goes right, or the node itself is one of them).
Dry Run
root=6, p=2, q=8. Both 2<6 and 8>6? No, they split (2<6, 8>6) → 6 is already
the LCA. Return 6 immediately.
Common Mistakes
Using the general binary tree LCA algorithm (which is O(n) and ignores the BST property entirely — correct
but not optimal here); not handling the case where p or q IS the current node.
KEY TAKEAWAY
In a BST, the LCA is the first node where the two targets' values 'split' to opposite sides.
QUESTION 46 OF 75 Medium
Problem Statement
Given the root of a binary search tree and an integer k, return the kth smallest value among all node
values in the tree.
Example Constraints
Input: root = [3,1,4,null,2], k = 1 → 1 ≤ number of nodes ≤ 10^4 · 1 ≤ k ≤ number of
Output: 1 nodes
Brute-Force Approach
Do a full in-order traversal collecting all values into a list, then return list[k-1]: O(n) time, O(n) space.
Optimal Approach
Do an in-order traversal (left, node, right) but stop early as soon as you've visited the kth node, using an
explicit stack (iterative) to avoid visiting the rest of the tree.
Step-by-Step Intuition
In-order traversal of a BST always visits values in strictly ascending order — so the kth value visited IS
the kth smallest, and you can stop the moment you reach it instead of collecting everything.
Dry Run
root=3(left=1(right=2),right=4), k=1. Iterative in-order using a stack: push
all left children of 3 → push 3, push1. Pop1 (no left) — that's the 1st node
visited — k=1 reached → return 1.
Common Mistakes
Collecting the entire tree into a list when early-stopping is expected as a follow-up; forgetting in-order visits
LEFT, then node, then RIGHT (not node-first).
KEY TAKEAWAY
In-order traversal of a BST yields values in sorted order — use it directly instead of sorting a
collected list.
QUESTION 47 OF 75 Medium
Kth Largest Element in an Array
Pattern: Heap (fixed-size min-heap)
Problem Statement
Given an integer array nums and an integer k, return the kth largest element in the array (the kth largest
in sorted order, not the kth distinct element).
Example Constraints
Input: nums = [3,2,1,5,6,4], k = 2 → 1 ≤ k ≤ [Link] ≤ 10^5
Output: 5
Brute-Force Approach
Sort the array descending and index k-1: O(n log n).
Optimal Approach
Maintain a min-heap of size k. Push every element; whenever the heap exceeds size k, pop the smallest.
At the end, the heap's smallest element (its root) is the kth largest overall.
Step-by-Step Intuition
You only ever need to remember the k LARGEST elements seen so far; a size-k min-heap automatically
discards anything that can't be in the top k, and its root is always the weakest of the current top-k —
exactly the kth largest.
Dry Run
nums=[3,2,1,5,6,4], k=2. push3→[3]. push2→[2,3]. push1→ size3>2 → pop
min(1)→[2,3]. push5→[2,3,5]>2→pop2→[3,5]. push6→[3,5,6]>2→pop3→[5,6].
push4→[4,5,6]>2→pop4→[5,6]. Heap root(min)=5 → answer=5.
Common Mistakes
import heapq
def find_kth_largest(nums, k):
heap = []
for num in nums:
[Link](heap, num)
if len(heap) > k:
[Link](heap)
return heap[0]
KEY TAKEAWAY
A fixed-size k min-heap is the standard way to track 'the top k so far' in O(log k) per update.
QUESTION 48 OF 75 Medium
K Closest Points to Origin
Pattern: Heap (fixed-size max-heap by distance)
Problem Statement
Given an array of points on the X-Y plane and an integer k, return the k closest points to the origin (0,0).
Example Constraints
Input: points = [[1,3],[-2,2]], k = 1 1 ≤ k ≤ [Link] ≤ 10^4 · -10^4 ≤ coordinates ≤
→ Output: [[-2,2]] 10^4
Brute-Force Approach
Compute all distances, sort by distance, take the first k: O(n log n).
Optimal Approach
Maintain a max-heap of size k keyed by squared distance (avoid sqrt for speed/precision). Push each
point; if the heap exceeds size k, pop the farthest. The heap holds the k closest at the end.
Step-by-Step Intuition
Symmetric to Kth Largest Element, but this time you want to KEEP the k smallest distances, so you evict
the largest whenever the heap overflows — hence a max-heap (Python's heapq is a min-heap, so negate
the distance).
Dry Run
Common Mistakes
Computing actual Euclidean distance with sqrt (unnecessary — squared distance preserves ordering and
avoids floating point); using a min-heap without negating distances (evicts the wrong element).
import heapq
def k_closest(points, k):
heap = []
for x, y in points:
dist = x * x + y * y
[Link](heap, (-dist, x, y))
if len(heap) > k:
[Link](heap)
return [[x, y] for _, x, y in heap]
KEY TAKEAWAY
When you need the k SMALLEST by some key, use a max-heap of size k (or negate the key with
Python's min-heap) and evict the largest.
QUESTION 49 OF 75 Hard
Merge K Sorted Lists
Pattern: Heap (k-way merge)
Problem Statement
Given an array of k linked lists, each sorted in ascending order, merge all the lists into one sorted linked
list and return it.
Example Constraints
Input: lists = [[1,4,5],[1,3,4],[2,6]] 0 ≤ k ≤ 10^4 · 0 ≤ total nodes ≤ 10^4
→ Output: [1,1,2,3,4,4,5,6]
Optimal Approach
Push the head of every list into a min-heap keyed by value. Repeatedly pop the smallest, append it to the
result, and push that node's next (if it exists) back into the heap.
Step-by-Step Intuition
At any moment, the next smallest overall value MUST be the head of one of the k lists (since each list is
individually sorted) — a heap gives O(log k) access to whichever list currently has the smallest head.
Dry Run
lists=[[1,4,5],[1,3,4],[2,6]]. heap=[(1,L0),(1,L1),(2,L2)]. Pop (1,L0) → append
1, push [Link]=4 → heap has (1,L1),(2,L2),(4,L0). Pop (1,L1) → append 1, push
[Link]=3 → heap has (2,L2),(3,L1),(4,L0). Pop(2,L2)→append2, push6 → continue
merging until all exhausted → [1,1,2,3,4,4,5,6].
Common Mistakes
Merging lists pairwise sequentially (correct but O(k·n), slower than the heap approach for large k); forgetting
Python's heapq needs a tiebreaker when node values are equal (compare a counter or list-index alongside
the value since ListNode isn't directly comparable).
import heapq
def merge_k_lists(lists):
heap = []
for i, node in enumerate(lists):
if node:
[Link](heap, ([Link], i, node))
dummy = ListNode()
tail = dummy
while heap:
val, i, node = [Link](heap)
[Link] = node
tail = [Link]
if [Link]:
[Link](heap, ([Link], i, [Link]))
return [Link]
KEY TAKEAWAY
A heap of 'current heads' generalizes two-way merging to k-way merging in O(n log k).
Problem Statement
Design a data structure that supports adding integers from a stream one at a time and finding the median
of all elements added so far, at any point.
Example Constraints
addNum(1), addNum(2), findMedian() → -10^5 ≤ num ≤ 10^5 · up to 5×10^4 calls to addNum
1.5, addNum(3), findMedian() → 2 and findMedian combined
Brute-Force Approach
Keep a sorted list and insert each new number in its correct position (or sort from scratch each time):
O(n) per insertion.
Optimal Approach
Maintain two heaps: a max-heap for the smaller half of numbers, and a min-heap for the larger half, kept
balanced in size (differing by at most 1). The median is the top of the larger heap, or the average of both
tops if they're equal size.
Step-by-Step Intuition
Splitting the data stream into 'smaller half' and 'larger half' lets each heap answer 'what's the boundary
value' in O(1) at its root — the median always sits right at that boundary.
Dry Run
addNum(1): small=[-1](max-heap negated), large=[]. addNum(2): push2 to large
first, then rebalance: small=[-1], large=[2]. Sizes equal(1,1) →
median=avg(1,2)=1.5. addNum(3): push to large or small based on comparison,
rebalance so small has one more: small=[-2,-1](i.e. holds1,2), large=[3].
Median = top of larger-sized heap = small's top = 2.
Common Mistakes
Letting the two heaps drift out of balance (must rebalance after every insertion); using a single sorted
structure with O(n) insertion when O(log n) is expected.
import heapq
class MedianFinder:
def __init__(self):
[Link] = [] # max-heap (negated)
[Link] = [] # min-heap
def find_median(self):
if len([Link]) > len([Link]):
return -[Link][0]
return (-[Link][0] + [Link][0]) / 2.0
KEY TAKEAWAY
Splitting a stream into a max-heap (lower half) and min-heap (upper half) answers running-median
queries in O(log n).
Backtracking
Systematic brute force with pruning — build a solution incrementally, undo, and try again.
QUESTION 51 OF 75 Medium
Subsets
Pattern: Backtracking (include/exclude)
Problem Statement
Given an integer array nums of unique elements, return all possible subsets (the power set). The solution
set must not contain duplicate subsets.
Example Constraints
Input: nums = [1,2,3] → Output: [[], 1 ≤ [Link] ≤ 10 · all elements distinct
[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Brute-Force Approach
Generate all 2^n bitmask combinations iteratively and build subsets from each: also O(2^n), essentially
equivalent in cost to backtracking.
Optimal Approach
Backtrack: at each index, recurse once WITH the current element included in the running subset, and
once WITHOUT it; record the current subset at every recursive call (every path is a valid subset).
Step-by-Step Intuition
Every subset corresponds to a sequence of n binary decisions ('include element i or not') — backtracking
simply explores both branches of that decision tree, undoing each choice before trying the other.
Dry Run
nums=[1,2,3]. Start path=[]. Record []. Include1→path=[1], record.
Include2→path=[1,2],record. Include3→path=[1,2,3],record.
Backtrack3,exclude3→already recorded[1,2]. Backtrack2, exclude2, include3→path=
[1,3],record. Continue exhaustively — 8 total subsets recorded.
Common Mistakes
Forgetting to record the subset at EVERY node of the recursion (not just at leaves); appending a reference to
the mutable path list instead of a copy (all recorded subsets end up identical/empty).
def subsets(nums):
result = []
path = []
def backtrack(start):
[Link](path[:])
for i in range(start, len(nums)):
[Link](nums[i])
backtrack(i + 1)
[Link]()
backtrack(0)
return result
KEY TAKEAWAY
Backtracking's core loop is: choose, recurse, undo (pop) — record a valid state at every step, not
just at the end.
QUESTION 52 OF 75 Medium
Permutations
Pattern: Backtracking (choose/unchoose with used-set)
Problem Statement
Given an array nums of distinct integers, return all possible permutations, in any order.
Example Constraints
Input: nums = [1,2,3] → Output: 1 ≤ [Link] ≤ 6 · all elements distinct
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],
[3,1,2],[3,2,1]]
Brute-Force Approach
N/A — generating all n! permutations inherently requires exploring the full decision tree; backtracking IS
the standard approach.
Step-by-Step Intuition
A permutation is an ordering, so at each position you choose from whatever elements remain — track
'used' elements explicitly, and undo the choice after exploring so the next branch starts clean.
Dry Run
nums=[1,2,3]. path=[],used={}. Choose1→path=[1]. Choose2→path=[1,2].
Choose3→path=[1,2,3],record,backtrack. Undo3→path=[1,2],no more
choices,backtrack. Undo2→path=[1]. Choose3→path=[1,3]. Choose2→path=
[1,3,2],record. Continue exhaustively for all 6 permutations.
Common Mistakes
Forgetting to 'unchoose' (remove from used-set and pop from path) before trying the next branch — corrupts
subsequent branches; not copying the path when recording the final result.
def permute(nums):
result = []
path = []
used = [False] * len(nums)
def backtrack():
if len(path) == len(nums):
[Link](path[:])
return
for i, num in enumerate(nums):
if used[i]:
continue
used[i] = True
[Link](num)
backtrack()
[Link]()
used[i] = False
backtrack()
return result
KEY TAKEAWAY
Permutation backtracking needs an explicit 'used' tracker, since order matters and every element
must appear exactly once per path.
Problem Statement
Given an array of distinct positive integers candidates and a target, return all unique combinations where
the chosen numbers sum to target. The same number may be chosen an unlimited number of times.
Example Constraints
Input: candidates = [2,3,6,7], target 2 ≤ [Link] ≤ 30 · 1 ≤ candidates[i] ≤ 200
= 7 → Output: [[2,2,3],[7]] · all candidates distinct · 1 ≤ target ≤ 500
Brute-Force Approach
Explore every possible combination of any length without pruning: exponential blow-up, often
impractically slow.
Optimal Approach
Backtrack starting from an index, trying to include candidates[i] repeatedly (allowing reuse), subtracting
from a remaining target, and only recursing while remaining ≥ 0; stop (prune) once remaining < 0.
Step-by-Step Intuition
Since numbers can repeat, always pass the SAME index forward (not index+1) when reusing a number,
but never go backward to earlier indices (that would create duplicate combinations in a different order).
Prune immediately once the running sum overshoots target.
Dry Run
candidates=[2,3,6,7], target=7.
Choose2(remaining5)→choose2(remaining3)→choose2(remaining1)→choose2(remaining-
1) prune. Backtrack, try3(remaining-2 from1) prune... eventually path[2,2,3]
sums to7 → record. Later path starting fresh with7(remaining0)→record[7].
Common Mistakes
Passing i+1 instead of i when recursing (would forbid reusing the same number, giving the wrong answer for
THIS variant); not pruning early (remaining < 0 check), leading to wasted exploration.
KEY TAKEAWAY
Prune backtracking branches the instant they can't succeed (remaining < 0) — don't wait to
discover failure at the leaf.
QUESTION 54 OF 75 Medium
Word Search
Pattern: Backtracking / DFS on Grid
Problem Statement
Given an m x n grid of characters board and a string word, return true if word exists in the grid, formed by
sequentially adjacent cells (horizontally or vertically neighboring), using each cell at most once.
Example Constraints
Input: board = [["A","B","C","E"], 1 ≤ m,n ≤ 6 · 1 ≤ [Link] ≤ 15
["S","F","C","S"],["A","D","E","E"]],
word = "ABCCED" → Output: true
Brute-Force Approach
Try starting the search from every cell and explore all 4 directions naively without marking visited cells:
leads to infinite loops or massively redundant exploration.
Optimal Approach
For each starting cell matching word[0], DFS in 4 directions, matching subsequent characters; temporarily
mark visited cells (e.g. overwrite with a sentinel), and restore them (unmark) after backtracking out of that
path.
Dry Run
board as given, word="ABCCED". Start at (0,0)='A' matches word[0]. Mark
visited. Try neighbor (0,1)='B' matches word[1]. Mark visited. Try (0,2)='C'
matches word[2]. Mark. Try (1,2)='C' matches word[3]. Mark. Try (2,2)='E'
matches word[4]. Mark. Try (2,1)='D' matches word[5] — full word matched →
return True (unmarking happens only if a branch fails, which none did here).
Common Mistakes
Forgetting to unmark (restore) a cell after backtracking out of a failed path (breaks other candidate paths); not
checking grid boundaries before indexing.
KEY TAKEAWAY
Grid backtracking marks a cell visited for the current path only, and always restores it before
returning — mark, recurse, unmark.
Problem Statement
Place n queens on an n x n chessboard so that no two queens attack each other (same row, column, or
diagonal). Return all distinct solutions, each as a board configuration.
Example Constraints
Input: n = 4 → Output: 2 solutions 1≤n≤9
(each a 4x4 board with one queen per
row/column, no shared diagonals)
Brute-Force Approach
Try placing queens in every possible cell combination and check all pairs for conflicts at the end:
extremely wasteful, effectively O(n^(2n)).
Optimal Approach
Place one queen per row, backtracking column by column. Track used columns and both diagonal
directions (row-col and row+col are constant along each diagonal) with sets, so conflict checks are O(1)
instead of scanning the board.
Step-by-Step Intuition
Since each row can hold exactly one queen, you only need to choose a COLUMN per row — reducing
the search space from all cells to just column choices, and diagonal conflicts can be checked instantly
using the row±col invariant instead of scanning.
Dry Run
n=4. Row0: try col0 — place, mark col0,diag(0-0)=0,diag(0+0)=0. Row1: try
col0(conflict,same col)→try col1(conflict,diag 1-1=0 matches)→try col2 (no
conflicts) — place. Row2: try each col, all conflict with existing queens —
backtrack row1. Try col1 fails to progress further... eventually valid full
placements found: columns [1,3,0,2] and [2,0,3,1] — 2 solutions for n=4.
Common Mistakes
Checking conflicts by scanning the whole board each time (O(n) per check instead of O(1) with sets — makes
the solution far too slow for larger n); forgetting one of the two diagonal directions.
def solve_n_queens(n):
result = []
cols, diag1, diag2 = set(), set(), set()
board = [['.'] * n for _ in range(n)]
def backtrack(row):
if row == n:
[Link]([''.join(r) for r in board])
return
for col in range(n):
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
[Link](col); [Link](row - col); [Link](row + col)
board[row][col] = 'Q'
backtrack(row + 1)
board[row][col] = '.'
[Link](col); [Link](row - col); [Link](row + col)
backtrack(0)
return result
KEY TAKEAWAY
Track constraints (columns, diagonals) with O(1)-checkable sets instead of rescanning the board —
the difference between fast and impossibly slow backtracking.
Graphs
DFS, BFS, topological sort, union-find, and shortest paths on general (non-tree) structures.
QUESTION 56 OF 75 Medium
Number of Islands
Pattern: DFS/BFS on Grid
Problem Statement
Given an m x n binary grid where '1' is land and '0' is water, return the number of islands (a group of
horizontally/vertically connected '1's).
Example Constraints
Input: grid = [["1","1","0"], 1 ≤ m,n ≤ 300
["1","0","0"],["0","0","1"]] → Output:
2
Brute-Force Approach
N/A — DFS/BFS flood fill is the natural and optimal approach.
Optimal Approach
Scan every cell; whenever an unvisited '1' is found, increment the island count and flood-fill (DFS or BFS)
to mark every connected '1' as visited so it isn't counted again.
Step-by-Step Intuition
Each flood-fill call consumes one entire connected component, so the number of times you START a new
flood-fill IS the number of islands.
Dry Run
grid as given. Scan (0,0)='1', unvisited → count=1, flood-fill marks (0,0),
(0,1),(1,0) visited. Scan continues, (0,2)='0' skip. (2,2)='1' unvisited →
count=2, flood-fill marks it. Total islands=2.
Common Mistakes
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '0'
dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1)
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
KEY TAKEAWAY
Counting connected components on a grid: scan for unvisited starts, flood-fill each one, count the
starts.
QUESTION 57 OF 75 Medium
Clone Graph
Pattern: DFS/BFS + Hash Map (visited copies)
Problem Statement
Given a reference node in a connected undirected graph, return a deep copy (clone) of the graph.
Example Constraints
Input: adjList = [[2,4],[1,3],[2,4], 0 ≤ number of nodes ≤ 100 · no repeated edges or
[1,3]] → Output: a structurally self-loops
identical cloned graph
Brute-Force Approach
Optimal Approach
DFS (or BFS) from the given node, using a hash map from original node → cloned node. Before
recursing into a neighbor, check if it's already been cloned (present in the map); if so, reuse that clone
instead of recursing again.
Step-by-Step Intuition
Because the graph can have cycles, a naive traversal would recurse forever — the visited map both
prevents infinite recursion AND lets you correctly wire up clones that reference each other in a cycle.
Dry Run
Start node1. map={}. Clone node1→map={1:clone1}. Visit neighbors 2,4. Clone2
not in map→clone it, map={1:c1,2:c2}, recurse into 2's neighbors(1,3): 1
already in map → reuse c1. Clone3→map adds 3:c3, recurse into 3's
neighbors(2,4): 2 in map→reuse. Continue until all nodes cloned and wired.
Common Mistakes
Forgetting to check the map BEFORE recursing (causes infinite recursion on any cycle); not copying the
neighbor list correctly (reusing original node references instead of cloned ones).
class Node:
def __init__(self, val=0, neighbors=None):
[Link] = val
[Link] = neighbors or []
def clone_graph(node):
if not node:
return None
visited = {}
def dfs(n):
if n in visited:
return visited[n]
copy = Node([Link])
visited[n] = copy
for neighbor in [Link]:
[Link](dfs(neighbor))
return copy
return dfs(node)
KEY TAKEAWAY
QUESTION 58 OF 75 Medium
Course Schedule
Pattern: Topological Sort (cycle detection)
Problem Statement
There are numCourses courses labeled 0 to numCourses-1. Given prerequisite pairs [a, b] meaning you
must take b before a, return true if you can finish all courses (i.e., no circular dependency exists).
Example Constraints
Input: numCourses = 2, prerequisites = 1 ≤ numCourses ≤ 2000 · 0 ≤ [Link] ≤
[[1,0]] → Output: true 5000
Input: numCourses = 2, prerequisites =
[[1,0],[0,1]] → Output: false
Brute-Force Approach
N/A — cycle detection inherently requires a graph traversal; there's no meaningfully simpler brute force.
Optimal Approach
Build an adjacency list and compute in-degrees (Kahn's algorithm). Start a queue with all in-degree-0
nodes; repeatedly remove a node, decrement its neighbors' in-degrees, and enqueue any that reach 0. If
all nodes get processed, there's no cycle.
Step-by-Step Intuition
A course can only be taken once all its prerequisites are satisfied — in-degree-0 nodes represent
'currently takeable' courses. If a cycle exists, some subset of nodes will NEVER reach in-degree 0, and
they'll be left unprocessed at the end.
Dry Run
numCourses=2, prereqs=[[1,0]] means edge 0→1. in-degree: node0=0, node1=1.
Queue=[0]. Process0, decrement node1's in-degree to0, enqueue1. Process1.
Processed count=2==numCourses → True (no cycle).
Common Mistakes
Using DFS with a simple visited set (doesn't distinguish 'currently in recursion stack' from 'fully processed',
which is needed to detect a cycle correctly — needs a 3-color/in-progress marker if going the DFS route);
miscounting in-degrees from the (a, b) pair direction.
KEY TAKEAWAY
Kahn's algorithm detects a cycle by checking whether every node eventually reaches in-degree 0
and gets processed.
QUESTION 59 OF 75 Medium
Course Schedule II
Pattern: Topological Sort (ordering)
Problem Statement
Given numCourses and prerequisite pairs, return an ordering of courses you could take to finish all of
them. If impossible, return an empty array.
Example Constraints
Input: numCourses = 4, prerequisites = 1 ≤ numCourses ≤ 2000 · 0 ≤ [Link] ≤
[[1,0],[2,0],[3,1],[3,2]] → Output: 5000
[0,1,2,3] (or [0,2,1,3])
Brute-Force Approach
N/A — same as Course Schedule; a valid order can only come from a proper topological traversal.
Optimal Approach
Step-by-Step Intuition
The ORDER in which Kahn's algorithm processes in-degree-0 nodes IS a valid topological order — the
cycle-detection check and the ordering come for free from the same process.
Dry Run
numCourses=4, edges: 0→1,0→2,1→3,2→3. in-degree:[0,1,1,2]. Queue=[0].
Process0→order=[0], decrement1,2 to0 → queue=[1,2]. Process1→order=[0,1],
decrement3 to1. Process2→order=[0,1,2], decrement3 to0→queue=[3].
Process3→order=[0,1,2,3]. All 4 processed → valid.
Common Mistakes
Forgetting to check that ALL nodes were processed before returning the order (a partial order from a graph
with a cycle is invalid and must return empty); building the order from a DFS post-order without reversing it
(DFS-based topological sort requires reversing the post-order stack).
KEY TAKEAWAY
Kahn's algorithm's processing order IS a valid topological sort — cycle detection and ordering are
the same computation.
Problem Statement
Given an m x n grid of heights, water can flow from a cell to a neighboring cell with height ≤ its own.
Return all cells from which water can reach BOTH the Pacific (top/left edges) and Atlantic (bottom/right
edges) oceans.
Example Constraints
Input: heights = [[1,2,2,3,5], 1 ≤ m,n ≤ 200
[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],
[5,1,1,2,4]] → Output: cells like
[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],
[4,0]
Brute-Force Approach
For every cell, run a DFS/BFS to check if it can reach BOTH oceans independently: O((m·n)²), far too
slow.
Optimal Approach
Reverse the problem: start DFS/BFS from every Pacific-adjacent cell (top row + left column) moving to
neighbors with height ≥ current (reverse flow), marking reachable cells; do the same from Atlantic-
adjacent cells (bottom row + right column). Cells reachable from BOTH searches are the answer.
Step-by-Step Intuition
Instead of checking 'can I reach the ocean' from every cell (expensive, redundant), check 'can the ocean
reach me' by flowing backward from each ocean's border — a single multi-source search per ocean
covers every cell in one pass.
Dry Run
Start Pacific search from all top-row and left-column cells simultaneously
(multi-source BFS/DFS), flowing to neighbors with height >= current (reverse of
the real flow direction). Mark visited set pacificReachable. Do the same for
Atlantic from bottom-row/right-column → atlanticReachable. Intersect both sets
for the final answer.
Common Mistakes
Running a separate traversal per cell instead of multi-source from the borders (the O((mn)²) brute-force
mistake); getting the flow-reversal comparison backward (should move to neighbors with height ≥ current,
since real water flows downhill).
def pacific_atlantic(heights):
if not heights:
return []
rows, cols = len(heights), len(heights[0])
pacific, atlantic = set(), set()
def dfs(r, c, visited, prev_height):
if (r, c) in visited or r < 0 or r >= rows or c < 0 or c >= cols or
heights[r][c] < prev_height:
return
[Link]((r, c))
for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
dfs(r + dr, c + dc, visited, heights[r][c])
for c in range(cols):
dfs(0, c, pacific, heights[0][c])
dfs(rows - 1, c, atlantic, heights[rows - 1][c])
for r in range(rows):
dfs(r, 0, pacific, heights[r][0])
dfs(r, cols - 1, atlantic, heights[r][cols - 1])
return [list(cell) for cell in pacific & atlantic]
KEY TAKEAWAY
When checking reachability FROM many targets, it's often cheaper to flow backward from those
targets instead of forward from every source.
QUESTION 61 OF 75 Medium
Graph Valid Tree
Pattern: Union-Find / DFS Cycle Check
Problem Statement
Given n nodes labeled 0 to n-1 and a list of undirected edges, determine if these edges form a valid tree
(connected, and exactly n-1 edges with no cycles).
Example Constraints
Input: n = 5, edges = [[0,1],[0,2], 1 ≤ n ≤ 2000 · 0 ≤ [Link] ≤ 5000 · no self-
[0,3],[1,4]] → Output: true loops or duplicate edges
Input: n = 5, edges = [[0,1],[1,2],
[2,3],[1,3],[1,4]] → Output: false
Optimal Approach
A graph is a valid tree if and only if it has exactly n-1 edges AND is fully connected. Use Union-Find: for
each edge, if both endpoints are already in the same set, a cycle exists (invalid); otherwise union them. At
the end, check exactly one connected component remains.
Step-by-Step Intuition
A tree is defined by having no cycles and being fully connected — Union-Find directly detects 'these two
nodes are already connected' (a cycle) in near O(1), and counting the final distinct sets confirms
connectivity.
Dry Run
n=5, edges=[[0,1],[0,2],[0,3],[1,4]]. Edge count=4=n-1 ✓. Union(0,1): different
sets→merge. Union(0,2): different→merge. Union(0,3): different→merge.
Union(1,4): different→merge. No cycle found, all edges processed. Final
component count=1 → valid tree → True.
Common Mistakes
Forgetting the edge-count check upfront (a graph can have n-1 edges but still be disconnected with a
separate cycle elsewhere — both conditions are necessary); not using path compression/union by rank,
degrading Union-Find's efficiency.
QUESTION 62 OF 75 Medium
Network Delay Time
Pattern: Dijkstra's Algorithm
Problem Statement
Given a network of n nodes with travel times as directed weighted edges times = [[u, v, w], ...], and a
starting node k, return the minimum time for a signal from k to reach all n nodes. Return -1 if impossible.
Example Constraints
Input: times = [[2,1,1],[2,3,1], 1 ≤ n ≤ 100 · 1 ≤ [Link] ≤ 6000 · 0 ≤ w ≤ 100
[3,4,1]], n = 4, k = 2 → Output: 2
Brute-Force Approach
Bellman-Ford: relax all edges n-1 times: O(V·E), correct but slower than Dijkstra when there are no
negative weights.
Optimal Approach
Dijkstra's algorithm with a min-heap: start at k with distance 0; repeatedly pop the closest unvisited node,
relax its outgoing edges, and push improved distances. Track the max finalized distance across all nodes.
Step-by-Step Intuition
Since all weights are non-negative, once a node is popped from the min-heap with its current best
distance, that distance can never be improved later — so processing nodes in increasing distance order
(a heap) greedily finalizes each node's shortest path exactly once.
Dry Run
times=[[2,1,1],[2,3,1],[3,4,1]], n=4,k=2. dist={2:0}. heap=[(0,2)]. Pop(0,2):
relax 2→1(dist1=1),2→3(dist3=1). heap=[(1,1),(1,3)]. Pop(1,1): no outgoing
edges. Pop(1,3): relax 3→4(dist4=2). heap=[(2,4)]. Pop(2,4): no edges. Final
dist={2:0,1:1,3:1,4:2}. Max=2 → answer=2.
Common Mistakes
Using Dijkstra on a graph with NEGATIVE edge weights (invalid — Dijkstra assumes non-negative weights;
Bellman-Ford is needed instead); forgetting to check that every node was actually reached before returning
the max distance.
import heapq
from collections import defaultdict
def network_delay_time(times, n, k):
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
dist = {}
heap = [(0, k)]
while heap:
d, node = [Link](heap)
if node in dist:
continue
dist[node] = d
for neighbor, weight in graph[node]:
if neighbor not in dist:
[Link](heap, (d + weight, neighbor))
return max([Link]()) if len(dist) == n else -1
KEY TAKEAWAY
Dijkstra's greedily finalizes the shortest distance to the closest unvisited node first — correct only
when all edge weights are non-negative.
Dynamic Programming
Break a problem into overlapping subproblems and cache the answers — 1D and 2D DP tables.
QUESTION 63 OF 75 Easy
Climbing Stairs
Pattern: 1D DP (Fibonacci-shaped)
Problem Statement
You are climbing a staircase with n steps. Each time you can climb 1 or 2 steps. In how many distinct
ways can you climb to the top?
Example Constraints
Input: n = 3 → Output: 3 (1+1+1, 1+2, 1 ≤ n ≤ 45
2+1)
Brute-Force Approach
Plain recursion: ways(n) = ways(n-1) + ways(n-2), recomputing the same subproblems exponentially:
O(2^n).
Optimal Approach
Bottom-up DP: build an array (or just two running variables) where dp[i] = dp[i-1] + dp[i-2], starting from
dp[1]=1, dp[2]=2.
Step-by-Step Intuition
The last move to reach step n was either a 1-step from n-1, or a 2-step from n-2 — so the total ways to
reach n is simply the sum of ways to reach those two prior steps, exactly the Fibonacci recurrence.
Dry Run
n=3. dp[1]=1, dp[2]=2. dp[3]=dp[2]+dp[1]=2+1=3. Answer=3.
Common Mistakes
def climb_stairs(n):
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
prev2, prev1 = prev1, prev1 + prev2
return prev1
KEY TAKEAWAY
Recognizing 'the answer depends on the previous 1-2 states' is the entry point into 1D DP —
replace recursion with a rolling computation.
QUESTION 64 OF 75 Medium
House Robber
Pattern: 1D DP (take/skip decision)
Problem Statement
Given an array nums representing money in houses arranged in a line, find the maximum amount you
can rob without robbing two adjacent houses.
Example Constraints
Input: nums = [2,7,9,3,1] → Output: 12 1 ≤ [Link] ≤ 100 · 0 ≤ nums[i] ≤ 400
(rob houses 0, 2, 4: 2+9+1=12)
Brute-Force Approach
Try every subset of non-adjacent houses: exponential, O(2^n).
Optimal Approach
DP where dp[i] = max(dp[i-1], dp[i-2] + nums[i]) — either skip house i (keep dp[i-1]) or rob it (dp[i-2] plus
its value). Collapse to two rolling variables.
Step-by-Step Intuition
At each house, you face a binary decision: rob it (and add to the best up to two houses back) or don't
(keep the best up to the previous house) — always take whichever is larger.
Dry Run
Common Mistakes
Forgetting that dp[i-2]+nums[i] competes with dp[i-1], not simply alternating houses greedily (greedy
alternating fails on inputs like [2,1,1,9]); off-by-one in base cases for small n (0 or 1 house).
def rob(nums):
prev2, prev1 = 0, 0
for num in nums:
prev2, prev1 = prev1, max(prev1, prev2 + num)
return prev1
KEY TAKEAWAY
'Take or skip, with a gap constraint' problems reduce to dp[i] = max(skip, take + dp[i-2]).
QUESTION 65 OF 75 Medium
Longest Increasing Subsequence
Pattern: 1D DP (with O(n log n) optimization)
Problem Statement
Given an integer array nums, return the length of the longest strictly increasing subsequence.
Example Constraints
Input: nums = [10,9,2,5,3,7,101,18] → 1 ≤ [Link] ≤ 2500 · -10^4 ≤ nums[i] ≤ 10^4
Output: 4 ([2,3,7,101] or [2,3,7,18])
Brute-Force Approach
Try every subsequence and check if increasing: O(2^n).
Optimal Approach
DP: dp[i] = length of the longest increasing subsequence ENDING at index i = 1 + max(dp[j] for all j < i
where nums[j] < nums[i]). Answer is max(dp). Optimize to O(n log n) with a 'tails' array + binary search,
where tails[k] = smallest possible tail value of an increasing subsequence of length k+1.
Dry Run
nums=[10,9,2,5,3,7,101,18]. tails=[]. 10→tails=[10]. 9<10→replace→tails=[9].
2<9→replace→tails=[2]. 5>2→append→tails=[2,5]. 3 fits between→replace5→tails=
[2,3]. 7>3→append→tails=[2,3,7]. 101>7→append→tails=[2,3,7,101]. 18 fits
before101→replace→tails=[2,3,7,18]. Length of tails=4 → answer=4.
Common Mistakes
Believing the 'tails' array IS a valid actual subsequence (it's not — it's only correct for tracking the LENGTH,
not reconstructing the sequence directly without extra bookkeeping); confusing strictly increasing with non-
decreasing.
import bisect
def length_of_lis(nums):
tails = []
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
[Link](num)
else:
tails[pos] = num
return len(tails)
KEY TAKEAWAY
When O(n²) DP is too slow, look for a greedy invariant (smallest tail per length) that binary search
can maintain in O(log n).
QUESTION 66 OF 75 Medium
Coin Change
Pattern: 1D DP (unbounded knapsack)
Example Constraints
Input: coins = [1,2,5], amount = 11 → 1 ≤ [Link] ≤ 12 · 1 ≤ coins[i] ≤ 2^31-1 · 0 ≤
Output: 3 (5+5+1) amount ≤ 10^4
Brute-Force Approach
Try every combination of coins recursively without memoization: exponential.
Optimal Approach
Bottom-up DP: dp[a] = minimum coins to make amount a. dp[0] = 0; for every amount from 1 to target,
dp[a] = min(dp[a - coin] + 1) over all coins that fit.
Step-by-Step Intuition
The minimum coins to make amount a is 1 (for whichever coin you pick last) plus the minimum coins to
make the remainder (a - that coin) — try every coin as 'the last one used' and take the best.
Dry Run
coins=[1,2,5], amount=11. dp[0]=0. dp[1]=dp[0]+1=1.
dp[2]=min(dp[1]+1,dp[0]+1)=1. dp[3]=min(dp[2]+1,dp[1]+1)=2. ...
dp[11]=min(dp[10]+1,dp[9]+1,dp[6]+1). Working through the table, dp[11]=3
(5+5+1).
Common Mistakes
Using a greedy 'always pick the largest coin' approach (fails for non-canonical coin systems, e.g. coins=
[1,3,4], amount=6 — greedy gives 4+1+1=3 coins but optimal is 3+3=2); forgetting to initialize unreachable
amounts to infinity (not 0).
KEY TAKEAWAY
QUESTION 67 OF 75 Medium
Unique Paths
Pattern: 2D DP (grid path counting)
Problem Statement
A robot is at the top-left of an m x n grid and can only move right or down. Return the number of unique
paths to reach the bottom-right corner.
Example Constraints
Input: m = 3, n = 7 → Output: 28 1 ≤ m, n ≤ 100
Brute-Force Approach
Recursively try both moves (right, down) from every cell without memoization: O(2^(m+n)).
Optimal Approach
DP table where dp[r][c] = dp[r-1][c] + dp[r][c-1] (paths from above plus paths from the left); first row and
first column are all 1 (only one way to reach them, moving straight).
Step-by-Step Intuition
To reach any cell, the robot's last move was either FROM ABOVE or FROM THE LEFT — so the number
of ways to reach a cell is the sum of ways to reach those two neighbors.
Dry Run
m=3,n=3 (smaller example). dp = [[1,1,1],[1,2,3],[1,3,6]]. Row0 and col0 are
all 1. dp[1][1]=dp[0][1]+dp[1][0]=1+1=2. dp[2][2]=dp[1][2]+dp[2][1]=3+3=6.
Bottom-right=6 unique paths.
Common Mistakes
Forgetting to initialize the first row/column to 1 (not 0); recomputing with plain recursion, causing exponential
blowup without memoization.
KEY TAKEAWAY
Grid path-counting DP sums contributions from 'above' and 'left' — a direct 2D extension of 1D DP.
QUESTION 68 OF 75 Medium
Longest Common Subsequence
Pattern: 2D DP (string alignment)
Problem Statement
Given two strings text1 and text2, return the length of their longest common subsequence (not
necessarily contiguous, but in order). Return 0 if none exists.
Example Constraints
Input: text1 = "abcde", text2 = "ace" 1 ≤ [Link], [Link] ≤ 1000
→ Output: 3 ("ace")
Brute-Force Approach
Try every subsequence of text1 and check if it's a subsequence of text2: exponential.
Optimal Approach
2D DP table dp[i][j] = LCS length of text1[:i] and text2[:j]. If the characters match, dp[i][j] = 1 + dp[i-1][j-1];
otherwise dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
Step-by-Step Intuition
At each pair of positions, either the characters match (extend the LCS found so far by 1) or they don't (the
LCS must come from ignoring one character from either string) — take whichever choice is better.
Dry Run
text1="abcde", text2="ace". Building the table: 'a'=='a'→dp grows by 1. 'b' vs
'c' mismatch→take max of neighbors. 'c'=='c'→extend. 'd' vs 'e' mismatch.
'e'=='e'→extend. Final dp[5][3]=3 → LCS length 3 ("ace").
KEY TAKEAWAY
2D string-alignment DP compares two sequences character by character, branching on match vs.
no-match at every cell.
QUESTION 69 OF 75 Medium
Word Break
Pattern: 1D DP + Hashing (segmentation)
Problem Statement
Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-
separated sequence of one or more dictionary words.
Example Constraints
Input: s = "leetcode", wordDict = 1 ≤ [Link] ≤ 300 · 1 ≤ [Link] ≤ 1000 ·
["leet","code"] → Output: true words consist of lowercase letters
Brute-Force Approach
Try every possible split point recursively without memoization: exponential, O(2^n).
Optimal Approach
DP where dp[i] = true if s[:i] can be segmented using dictionary words. dp[0] = true (empty prefix). For
each i, check every j < i: if dp[j] is true AND s[j:i] is in the word set, set dp[i] = true.
Dry Run
s="leetcode", dict={"leet","code"}. dp[0]=True. Check i=4: j=0, s[0:4]="leet"
in dict and dp[0]=True → dp[4]=True. Check i=8: j=4, s[4:8]="code" in dict and
dp[4]=True → dp[8]=True. dp[8] (full string length) is True → answer True.
Common Mistakes
Forgetting dp[0] = True as the base case (empty prefix is trivially breakable); recomputing substring
membership without a set (checking against a list is O(k) per check instead of O(1)).
KEY TAKEAWAY
Segmentation DP marks which PREFIXES are achievable, extending from each achievable prefix
by testing valid next chunks.
Greedy
Make the locally optimal choice at each step and prove it leads to a globally optimal answer.
QUESTION 70 OF 75 Medium
Jump Game
Pattern: Greedy (reachability frontier)
Problem Statement
Given an array nums where nums[i] is the maximum jump length from index i, return true if you can reach
the last index starting from index 0.
Example Constraints
Input: nums = [2,3,1,1,4] → Output: 1 ≤ [Link] ≤ 10^4 · 0 ≤ nums[i] ≤ 10^5
true
Input: nums = [3,2,1,0,4] → Output:
false
Brute-Force Approach
Try every combination of jumps recursively (backtracking through all reachable indices): exponential.
Optimal Approach
Greedily track the farthest index reachable so far while scanning left to right. If the current index ever
exceeds that farthest reach, it's unreachable — return false. Otherwise update farthest = max(farthest, i +
nums[i]).
Step-by-Step Intuition
You never need to know WHICH path gets you furthest, only the single best (furthest) reach achievable
by any index processed so far — a single greedy scan captures that.
Dry Run
nums=[2,3,1,1,4]. farthest=0. i=0: 0<=farthest(0) ok, farthest=max(0,0+2)=2.
i=1: 1<=2 ok, farthest=max(2,1+3)=4. i=2: 2<=4 ok, farthest=max(4,2+1)=4. i=3:
farthest=max(4,3+1)=4. i=4(last index): 4<=4 ok → reachable → True.
Common Mistakes
Using DP (O(n²)) when the greedy O(n) approach is expected and simpler; forgetting to check reachability of
the CURRENT index before updating farthest (an unreachable gap must fail immediately).
def can_jump(nums):
farthest = 0
for i, num in enumerate(nums):
if i > farthest:
return False
farthest = max(farthest, i + num)
return True
KEY TAKEAWAY
When only the BEST reach matters (not the path), track a single running frontier greedily instead of
exploring every option.
QUESTION 71 OF 75 Medium
Gas Station
Pattern: Greedy (running total reset)
Problem Statement
There are n gas stations in a circle, each with gas[i] fuel and cost[i] to travel to the next station. Return the
starting station index if you can complete the circuit exactly once, or -1 if impossible (the answer is
guaranteed unique if it exists).
Example Constraints
Input: gas = [1,2,3,4,5], cost = n == [Link] == [Link] · 1 ≤ n ≤ 10^5 · 0 ≤
[3,4,5,1,2] → Output: 3 gas[i], cost[i] ≤ 10^4
Brute-Force Approach
Try starting from every station and simulate the full circuit: O(n²).
Optimal Approach
Track a running tank total while scanning once. If the tank ever goes negative at station i, no start from
the previous candidate through i can work — reset the candidate start to i+1 and reset the tank to 0.
Separately verify total gas ≥ total cost overall (necessary for any solution to exist).
Dry Run
gas=[1,2,3,4,5], cost=[3,4,5,1,2]. total=0,tank=0,start=0. i=0: diff=1-3=-2,
total=-2,tank=-2<0→start=1,tank=0. i=1: diff=2-
4=-2,total=-4,tank=-2<0→start=2,tank=0. i=2: diff=3-
5=-2,total=-6,tank=-2<0→start=3,tank=0. i=3: diff=4-1=3,total=-3,tank=3. i=4:
diff=5-2=3,total=0,tank=6. Final total=0 (>=0, feasible) → answer=start=3.
Common Mistakes
Using the O(n²) brute-force simulation when O(n) greedy is expected; forgetting to check total gas ≥ total cost
overall (a candidate start could be found even when no solution exists, without this check).
KEY TAKEAWAY
When a running total goes negative, everything since the last reset point is provably a bad start —
skip past all of it at once.
QUESTION 72 OF 75 Medium
Non-overlapping Intervals
Pattern: Greedy (interval scheduling by end time)
Problem Statement
Example Constraints
Input: intervals = [[1,2],[2,3],[3,4], 1 ≤ [Link] ≤ 10^5 · intervals[i].length == 2
[1,3]] → Output: 1 (remove [1,3])
Brute-Force Approach
Try every subset of intervals to keep and check for non-overlap: exponential.
Optimal Approach
Sort intervals by END time. Greedily keep an interval if it starts at or after the end of the last kept interval;
otherwise it must be removed (increment a removal counter).
Step-by-Step Intuition
Among overlapping intervals, keeping the one that ends EARLIEST leaves the most room for future
intervals to also fit without overlapping — this is the provably optimal greedy choice (an exchange
argument).
Dry Run
Sorted by end: [1,2],[2,3],[1,3],[3,4]. lastEnd=-inf. [1,2]: 1>=lastEnd(-
inf)→keep,lastEnd=2. [2,3]: 2>=2→keep,lastEnd=3. [1,3]:
1<3→overlap→remove,count=1. [3,4]: 3>=3→keep,lastEnd=4. Total removed=1.
Common Mistakes
Sorting by START time instead of END time (the greedy proof only holds for sorting by end time); off-by-one
on the overlap condition (should be strictly less-than for the start vs lastEnd comparison).
def erase_overlap_intervals(intervals):
[Link](key=lambda x: x[1])
count = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
last_end = end
else:
count += 1
return count
KEY TAKEAWAY
QUESTION 73 OF 75 Medium
Merge Intervals
Pattern: Greedy + Sorting (interval merge)
Problem Statement
Given an array of intervals, merge all overlapping intervals and return the resulting non-overlapping
intervals covering all the input.
Example Constraints
Input: intervals = [[1,3],[2,6], 1 ≤ [Link] ≤ 10^4 · intervals[i].length == 2
[8,10],[15,18]] → Output: [[1,6],
[8,10],[15,18]]
Brute-Force Approach
Repeatedly scan all pairs, merging any that overlap, until no more merges are possible: O(n²) or worse.
Optimal Approach
Sort intervals by START time. Walk through, keeping a 'current merged interval'; if the next interval's start
is ≤ the current merged interval's end, extend the end (max of both ends); otherwise, close off the current
merged interval and start a new one.
Step-by-Step Intuition
Once sorted by start time, any interval that overlaps the current merge candidate MUST appear
immediately next (nothing between them in sorted order could overlap without also overlapping the
current one) — a single linear scan suffices.
Dry Run
Sorted: [1,3],[2,6],[8,10],[15,18]. current=[1,3]. Next[2,6]:
2<=3→merge→current=[1,6]. Next[8,10]: 8>6→close current, output[1,6], current=
[8,10]. Next[15,18]: 15>10→close, output[8,10], current=[15,18].
End→output[15,18]. Result=[[1,6],[8,10],[15,18]].
Common Mistakes
Sorting by end time instead of start time (breaks the linear-merge assumption); comparing with strict <
instead of ≤ for the overlap check (misses intervals that exactly touch, like [1,3] and [3,5], if the problem
considers touching as overlapping).
def merge_intervals(intervals):
[Link](key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
[Link]([start, end])
return merged
KEY TAKEAWAY
Sorting by START time turns interval merging into a single linear scan — overlaps can only occur
with the immediately preceding interval.
Bit Manipulation
XOR and bit tricks that solve problems in O(1) space that would otherwise need extra memory.
QUESTION 74 OF 75 Easy
Single Number
Pattern: Bit Manipulation (XOR)
Problem Statement
Given a non-empty array of integers nums where every element appears exactly twice except for one,
find that single one, in O(n) time and O(1) space.
Example Constraints
Input: nums = [4,1,2,1,2] → Output: 4 1 ≤ [Link] ≤ 3×10^4 · each element appears
twice except one, which appears once
Brute-Force Approach
Use a hash map to count occurrences, then find the one with count 1: O(n) time, O(n) space.
Optimal Approach
XOR every element together. Since a XOR a = 0 for any a, and XOR is commutative/associative, all
paired elements cancel out, leaving only the single unpaired element.
Step-by-Step Intuition
XOR is its own inverse (x^x=0) and order-independent, so XOR-ing the entire array collapses every
duplicate pair to zero, and zero XOR anything returns that thing unchanged — leaving exactly the
unpaired value.
Dry Run
nums=[4,1,2,1,2]. result=0. 0^4=4. 4^1=5. 5^2=7. 7^1=6 (since 5^2^1... let's
just trust associativity: 4^1^2^1^2 = 4^(1^1)^(2^2) = 4^0^0 = 4). Answer=4.
Common Mistakes
def single_number(nums):
result = 0
for num in nums:
result ^= num
return result
KEY TAKEAWAY
XOR cancels identical pairs to zero — whenever 'everything appears twice except one', XOR the
whole array.
QUESTION 75 OF 75 Easy
Number of 1 Bits
Pattern: Bit Manipulation (bit clearing trick)
Problem Statement
Write a function that takes an unsigned integer and returns the number of '1' bits it has (its Hamming
weight).
Example Constraints
Input: n = 11 (binary 1011) → Output: the input is a 32-bit unsigned integer
3
Brute-Force Approach
Check each of the 32 bits individually with a mask and shift: O(32), constant but does unnecessary work
for sparse numbers.
Optimal Approach
Repeatedly apply n = n & (n - 1), which clears the lowest set bit each time; count how many iterations it
takes until n becomes 0.
Step-by-Step Intuition
Subtracting 1 from n flips all trailing zero bits to 1 and the lowest set bit to 0; ANDing with the original n
then clears exactly that lowest set bit and nothing else — so each iteration removes exactly one '1' bit.
Dry Run
Common Mistakes
Checking all 32 bit positions unconditionally when a tighter loop (bounded by set-bit count) is expected as a
follow-up; not handling the 'unsigned' nature correctly in languages with signed integer quirks (less of an
issue in Python).
def hamming_weight(n):
count = 0
while n:
n &= (n - 1)
count += 1
return count
KEY TAKEAWAY
n & (n-1) clears the lowest set bit — a fast, elegant way to count or manipulate set bits without
checking every position.
SECTION 7
Q33 · Remove Nth Node From End of List Q64 · House Robber
Two Pointers · Sliding Window · Prefix Sum · Fast & Slow Pointers · Binary Search on Answer · Monotonic
Stack · BFS · DFS · Topological Sort · Heap / Priority Queue · Backtracking · Greedy · 1D DP · 2D DP
Hash map ops: O(1) avg · Sorting: O(n log n) · Binary search: O(log n) · Heap push/pop: O(log n) · Tree
traversal: O(n) · Graph traversal: O(V+E) · DP table fill: O(states × transitions)
Interview-Day Checklist
Before You Start Coding While Coding
☐ Repeat the problem back in your own words ☐ Think out loud, don't code in silence
☐ Confirm input size, constraints, and edge cases ☐ Use meaningful variable names, not single letters
☐ State the brute-force approach and its complexity ☐ Handle empty input / null / single-element edge
out loud cases
☐ Name the pattern you recognize before coding ☐ Dry-run your own code on the example before
☐ Jumping to code before agreeing on approach ☐ Skim the Pattern Cheat Sheet, not new problems
☐ Ignoring the interviewer's hints ☐ Re-read your Top 15 revision list
☐ Silence for more than 30 seconds ☐ Sleep — a rested brain outperforms one more
☐ Leaving obvious bugs unaddressed after a hint practice problem
Progress Tracker
Check off each question as you complete it without help.
☐ Q10 · 3Sum ☐ Q11 · Container With Most Water ☐ Q12 · Trapping Rain Water
☐ Q13 · Best Time to Buy and Sell ☐ Q14 · Longest Substring Without ☐ Q15 · Longest Repeating
Stock Repeating Characters Character Replacement
☐ Q16 · Minimum Window Substring ☐ Q17 · Sliding Window Maximum ☐ Q18 · Binary Search
☐ Q19 · Search in Rotated Sorted ☐ Q20 · Find Minimum in Rotated ☐ Q21 · Koko Eating Bananas
Array Sorted Array
☐ Q22 · Find First and Last Position ☐ Q23 · Median of Two Sorted ☐ Q24 · Longest Common Prefix
of Element in Sorted Array Arrays
☐ Q25 · Longest Palindromic ☐ Q26 · Palindromic Substrings ☐ Q27 · String to Integer (atoi)
Substring
☐ Q28 · Encode and Decode Strings ☐ Q29 · Reverse Linked List ☐ Q30 · Linked List Cycle
☐ Q31 · Middle of the Linked List ☐ Q32 · Merge Two Sorted Lists ☐ Q33 · Remove Nth Node From
End of List
☐ Q37 · Evaluate Reverse Polish ☐ Q38 · Daily Temperatures ☐ Q39 · Largest Rectangle in
Notation Histogram
☐ Q40 · Invert Binary Tree ☐ Q41 · Maximum Depth of Binary ☐ Q42 · Diameter of Binary Tree
Tree
☐ Q43 · Binary Tree Level Order ☐ Q44 · Validate Binary Search Tree ☐ Q45 · Lowest Common Ancestor
Traversal of a BST
☐ Q46 · Kth Smallest Element in a ☐ Q47 · Kth Largest Element in an ☐ Q48 · K Closest Points to Origin
BST Array
☐ Q49 · Merge K Sorted Lists ☐ Q50 · Find Median from Data ☐ Q51 · Subsets
Stream
☐ Q58 · Course Schedule ☐ Q59 · Course Schedule II ☐ Q60 · Pacific Atlantic Water Flow
☐ Q61 · Graph Valid Tree ☐ Q62 · Network Delay Time ☐ Q63 · Climbing Stairs
Completed all 75? You now recognize every major interview pattern on sight. Return to the Final
Revision Cheat Sheet and re-solve the Top 15 cold, without notes, one more time before your
interview.