Hot100 Algorithm Notes
LeetCode Hot100 algorithm notes
LeetCode Algorithm Notes
General Problem-Solving Strategies Overview
What features to look for → which algorithm to use
| Problem Feature | Go-to Algorithm | Typical Problems |
|---|---|---|
| Sorted array + search | Binary Search | 33, 34, 35, 74, 153 |
| Max/min of a contiguous subarray | Sliding Window / Prefix Sum | 3, 53, 209, 239 |
| Merge two sorted structures | Two Pointers | 11, 88, 167 |
| O(n) lookup / deduplication / grouping | Hash Table | 1, 49, 128, 560 |
| Linked list: find middle / cycle / k-th from end | Fast & Slow Pointers | 19, 141, 142, 876 |
| Tree traversal / level order | DFS recursion / BFS queue | 94, 102, 104, 199 |
| Parenthesis matching / expression evaluation | Stack | 20, 32, 394, 735 |
| Next greater/smaller element | Monotonic Stack | 84, 739, 239 |
| Find all solutions / combinations / permutations | Backtracking | 17, 22, 39, 46, 51, 78 |
| Optimal solution + overlapping subproblems | Dynamic Programming | 53, 62, 70, 72, 198, 300 |
| Local optimum at each step → global optimum | Greedy | 11, 45, 55, 121, 135 |
| Top K / Median | Heap | 23, 215, 295, 347 |
| 2D grid search / connected components | BFS / DFS | 200, 207, 547, 994 |
| String prefix matching | Trie | 208 |
| Interval merging / insertion | Sort + traversal | 56, 763 |
How to Recognize DP
| Keyword | DP Type | State Definition |
|---|---|---|
| Longest / shortest / fewest / most | Optimization DP | dp[i] = optimal value of the first i elements |
| Whether possible / yes-or-no | Feasibility DP | dp[i] = whether the first i elements are feasible |
| Number of ways | Counting DP | dp[i] = number of ways for the first i elements |
| Knapsack / coin change | Knapsack DP | dp[j] = optimal value at capacity j |
| Two sequences | Two-sequence DP | dp[i][j] = result of first i of s1 and first j of s2 |
| Matrix path | Matrix DP | dp[i][j] = optimal value to reach (i,j) |
Universal Linked List Playbook
| Scenario | Technique |
|---|---|
| Reverse | Three pointers (pre, cur, next) |
| Find middle | Fast & slow pointers (fast steps 2, slow steps 1) |
| Find k-th from end | Fast pointer moves N steps first, then move together |
| Detect cycle | Fast & slow pointers, meet means a cycle exists |
| Find cycle entry | After meeting, one returns to head, move together to the next meet |
| Intersection | Two pointers traverse each other’s list, meet at the intersection |
| Delete node | Dummy sentinel + pre pointer |
I. Arrays and Hash Tables
0001. Two Sum Easy
Problem: Given an array, find two numbers whose sum equals target and return their indices.
Core idea: As you traverse, record each visited value and its index in a hash table; for each nums[i], check whether target - nums[i] is already in the table.
func twoSum(nums []int, target int) []int { mp := map[int]int{} for i, val := range nums { if index, exist := mp[target-val]; exist { return []int{index, i} // found a pair } mp[val] = i // record current value } return []int{}}- Key insight: A hash table reduces the pairing lookup from O(n) to O(1), giving total O(n) complexity.
- Pitfall: Query before storing, to avoid
nums[i]pairing with itself (e.g.target=6, nums=[3,...]).
Execution example: nums = [2,7,11,15], target = 9
| Step | i | nums[i] | Look up 9-nums[i] | Hash table | Operation |
|---|---|---|---|---|---|
| 1 | 0 | 2 | look up 7 → not found | {} | store {2:0} |
| 2 | 1 | 7 | look up 2 → found (index 0) | {2:0} | return [0,1] |
- ⚠️ Common failure: Writing to the map before looking up, causing
val == target-valto match itself (e.g.nums=[3,2,4], target=6returns[0,0]instead of[1,2]). You must query before storing.
0013. Roman to Integer Easy
Problem: Convert a Roman numeral to an integer (IV=4, VI=6).
Core idea: While traversing, if the current value is less than the next value (e.g. I in IV), subtract the current value; otherwise add it.
func romanToInt(s string) int { ans := 0 for i := range s { value := symbolValues[s[i]] if i < n-1 && value < symbolValues[s[i+1]] { ans -= value // smaller precedes larger → subtract (e.g. I in IV) } else { ans += value } } return ans}- Key insight: In Roman numerals, a smaller symbol before a larger one means subtraction (IV = 5 - 1 = 4).
- Pitfall: Only compare the relative order of two adjacent characters.
0041. First Missing Positive Hard
Problem: Find the smallest missing positive integer in an unsorted array in O(n) time and O(1) space.
Core idea: In-place hashing — place value v at index v-1. After traversing, the first position where nums[i] != i+1 is the answer.
for i := 0; i < n; i++ { for nums[i] >= 1 && nums[i] <= n && nums[i] != nums[nums[i]-1] { nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1] // swap into correct position }}// find the first one out of placefor i := 0; i < n; i++ { if nums[i] != i+1 { return i + 1 }}return n + 1- Key insight: Use the array itself as a hash table, where value
vmaps to indexv-1. - Pitfall: The swap condition must be
nums[i] != nums[nums[i]-1](to avoid infinite loops on duplicates), notnums[i] != i+1.
Execution example: nums = [3,4,-1,1], the goal is to place value v at index v-1.
| Step | i | nums[i] | Operation | Array state |
|---|---|---|---|---|
| 1 | 0 | 3 | 3∈[1,4], swap nums[0]↔nums[2] | [-1,4,3,1] |
| 2 | 0 | -1 | -1 not in [1,4], skip | [-1,4,3,1] |
| 3 | 1 | 4 | 4∈[1,4], swap nums[1]↔nums[3] | [-1,1,3,4] |
| 4 | 1 | 1 | 1∈[1,4], swap nums[1]↔nums[0] | [1,-1,3,4] |
| 5 | 1 | -1 | skip | [1,-1,3,4] |
| 6 | 2 | 3 | 3 already at position 2, skip | [1,-1,3,4] |
| 7 | 3 | 4 | 4 already at position 3, skip | [1,-1,3,4] |
Scan result: nums[1] = -1 ≠ 2, return 2.
graph TD
A["Traverse each position i"] --> B{"nums[i] in [1,n] and
nums[i] ≠ nums[nums[i]-1]?"}
B -->|Yes| C["Swap nums[i] ↔ nums[nums[i]-1]"]
C --> B
B -->|No| D["i++"]
D --> E{"i < n?"}
E -->|Yes| B
E -->|No| F["Scan for the first nums[i] ≠ i+1"]
style C fill: #fff9c4, color: #1a1a1a
style F fill: #c8e6c9, color: #1a1a1a0049. Group Anagrams Medium
Problem: Group words that contain the same letters but in a different order.
Core idea: Sort each string and use it as the hash key; group strings with the same key together.
for _, str := range strs { arr := strings.Split(str, "") sort.Strings(arr) key := strings.Join(arr, "") // sorted string as key m[key] = append(m[key], str)}- Key insight: After sorting, anagrams become the same string, which is naturally suitable as a hash key.
- Pitfall: You can also use a character-count array (convert
[26]intto a string) as the key, costing O(k) instead of O(k log k).
0128. Longest Consecutive Sequence Medium
Problem: Find the length of the longest consecutive sequence of numbers in an unsorted array in O(n).
Core idea: Put everything into a hash set, and only count forward starting from a “sequence start” (where num-1 is not in the set).
mp := map[int]bool{}for _, v := range nums { mp[v] = true }for key := range mp { if !mp[key-1] { // only count starting from the start count := 1 for mp[key+1] { count++; key++ } res = max(res, count) }}- Key insight: Only count from sequence starts (where
num-1is absent), guaranteeing each number is visited at most twice. - Pitfall: Counting from every number gives O(n²); you must skip non-starts.
Execution flow (nums = [100,4,200,1,3,2]):
| Number checked | num-1 in set? | Is start? | Count forward | Sequence length |
|---|---|---|---|---|
| 100 | 99 not in → yes | ✅ start | 100→(101 not in) | 1 |
| 4 | 3 in → no | ❌ skip | - | - |
| 200 | 199 not in → yes | ✅ start | 200→(201 not in) | 1 |
| 1 | 0 not in → yes | ✅ start | 1→2→3→4→(5 not in) | 4 ✓max |
| 3 | 2 in → no | ❌ skip | - | - |
| 2 | 1 in → no | ❌ skip | - | - |
Result = 4 (sequence [1,2,3,4]). We only count from 3 starts, skipping the other 3.
0136. Single Number Easy
Problem: Find the number that appears exactly once (all others appear twice) in O(n) time and O(1) space.
Core idea: XOR everything; paired values cancel to 0, and what remains is the answer.
single := 0for _, num := range nums { single ^= num }return single- Key insight: XOR self-inverse:
a ^ a = 0,a ^ 0 = a. - Pitfall: The initial value must be 0.
0169. Majority Element Easy
Problem: Find the element that appears more than n/2 times in O(n) time and O(1) space.
Core idea: Boyer-Moore voting — a candidate plus a counter; increment on the same, decrement on different, and switch candidate when the count hits zero.
count := 0; maj := nums[0]for i := 1; i < n; i++ { if count == 0 { maj = nums[i] } // switch candidate if nums[i] == maj { count++ } else { count-- }}- Key insight: The majority element is over half, so it must be the last one standing after cancellation.
- Pitfall: When the count hits zero, switch candidate first, then compare.
0238. Product of Array Except Self Medium
Problem: Without using division, compute answer[i] = the product of all elements except nums[i] in O(n).
Core idea: Two passes — left to right to store the left cumulative product, right to left to multiply by the right cumulative product.
answer[0] = 1for i := 1; i < n; i++ { answer[i] = answer[i-1] * nums[i-1] } // left cumulativetemp := 1for i := n-1; i >= 0; i-- { answer[i] *= temp // multiply by right cumulative temp *= nums[i] // update right cumulative}- Key insight:
answer[i]= product of all on the left × product of all on the right, done in two passes. - Pitfall: In the second (right-to-left) pass,
tempstarts at 1; updateanswerbefore updatingtemp.
0283. Move Zeroes Easy
Problem: Move all 0s in the array to the end, keeping the relative order of non-zero elements.
l := 0for r := 0; r < len(nums); r++ { if nums[r] != 0 { nums[l], nums[r] = nums[r], nums[l]; l++ }}- Key insight: The slow pointer marks where the next non-zero element should go.
0560. Subarray Sum Equals K Medium
Problem: Count the number of contiguous subarrays whose sum equals k.
Core idea: Prefix sum + hash table. When at position j, the number of times pre[j] - k has appeared before equals the number of subarrays ending at j whose sum is k.
Why prefix sum: The sum of a contiguous subarray nums[i..j] = pre[j] - pre[i-1]. For it to equal k, we need pre[i-1] = pre[j] - k. So when at j, we just need to count how many prefix sums equal pre[j] - k appeared before.
m := map[int]int{0: 1}; pre := 0; count := 0for _, num := range nums { pre += num count += m[pre-k] // how many prefix sums = pre-k appeared before m[pre]++ // record how many times the current prefix sum appears}Execution example (nums = [1,1,1], k = 2):
| Step | num | pre | Look up pre-k=pre-2 | count added | Hash table m |
|---|---|---|---|---|---|
| 1 | 1 | 1 | look up -1 → 0 | 0 | {0:1, 1:1} |
| 2 | 1 | 2 | look up 0 → 1 | 1 | {0:1, 1:1, 2:1} |
| 3 | 1 | 3 | look up 1 → 1 | 2 | {0:1, 1:1, 2:1, 3:1} |
Step 2: pre=2, found 1 prefix sum of 0 (subarray nums[0..1]=[1,1]); Step 3: pre=3, found 1 prefix sum of 1 (subarray nums[1..2]=[1,1]). Total 2.
- Key insight: The difference of two prefix sums equals
k, i.e. count how many previous prefix sums equalpre - k. - Pitfall: Initialize
m[0]=1to handle subarrays that start from the beginning (whenpreis exactlyk,pre-k=0must be found); the array contains negatives, so you cannot use a sliding window — you must use prefix sum + hash table.
0724. Find Pivot Index Easy
Problem: Find an index where the sum of the left side equals the sum of the right side.
for _, num := range nums { right += num }for i, num := range nums { right -= num // first subtract current element if left == right { return i } left += num // then add it to the left}- Key insight: Get the right-side sum by subtracting from the total, no extra array needed.
1207. Unique Number of Occurrences Easy
Problem: Determine whether the occurrence counts of each number are all distinct.
mp := map[int]int{}for _, v := range arr { mp[v]++ } // first pass: count frequenciesmp2 := map[int]bool{}for _, v := range mp { // second pass: check for duplicate frequencies if mp2[v] { return false } mp2[v] = true}1431. Kids With the Greatest Number of Candies Easy
Problem: Whether each kid can reach the maximum after receiving extra candies.
maxVal := candies[0]for _, v := range candies { if v > maxVal { maxVal = v } }for i, v := range candies { res[i] = v + extraCandies >= maxVal }1679. Max Number of K-Pairs Medium
Problem: Remove pairs that sum to k each step; return the maximum number of operations.
cnt := map[int]int{}for _, x := range nums { if cnt[k-x] > 0 { cnt[k-x]--; ans++ } // found a pair else { cnt[x]++ }}1732. Find the Highest Altitude Easy
Problem: gain[i] is the change in altitude from point i to i+1. Starting from altitude 0, return the highest altitude reached along the way.
h := 0for _, g := range gain { h += g; ans = max(ans, h) }2215. Find the Difference of Two Arrays Easy
Problem: Return two lists: elements in nums1 but not in nums2, and elements in nums2 but not in nums1 (deduplicated).
Core idea: Two hash sets, take the set difference of each from the other.
func findDifference(nums1 []int, nums2 []int) [][]int { m1 := make(map[int]bool) for _, num := range nums1 { m1[num] = true } // nums1 → set m2 := make(map[int]bool) for _, num := range nums2 { m2[num] = true } // nums2 → set ans := make([][]int, 2) for k := range m1 { if !m2[k] { ans[0] = append(ans[0], k) } } // unique to nums1 for k := range m2 { if !m1[k] { ans[1] = append(ans[1], k) } } // unique to nums2 return ans}- Key insight: Hash set O(1) lookup, naturally suited for set difference.
- Pitfall: Map keys auto-deduplicate, no extra handling needed.
2352. Equal Row and Column Pairs Medium
Problem: Number of pairs of rows and columns that are completely identical in an n×n matrix.
Core idea: Serialize rows into strings and store in a hash table; query columns for the number of matches.
func equalPairs(grid [][]int) int { mp := make(map[string]int) for i := 0; i < len(grid); i++ { // serialize each row s := "" for j := 0; j < len(grid[0]); j++ { s += strconv.Itoa(grid[i][j]) + " " } mp[s]++ } ans := 0 for j := 0; j < len(grid[0]); j++ { // query each column s := "" for i := 0; i < len(grid); i++ { s += strconv.Itoa(grid[i][j]) + " " } ans += mp[s] // add the number of matching rows } return ans}- Key insight: Comparing rows and columns becomes string matching, with hash table O(1) lookup.
- Pitfall: Add a separator (e.g. space) between elements when serializing, to prevent
[1,23]and[12,3]from mismatching.
3345. Minimum Divisible Digit Product I Easy
Problem: Find the smallest integer ≥ n whose digit product is divisible by t.
func smallestNumber(n int, t int) int { for { n1 := 1; num := n for num > 0 { n1 = num % 10 * n1 // multiply each digit num /= 10 if n1 == 0 { break } // contains 0 → product is 0, definitely divisible } if n1 % t == 0 { return n } n++ }}- Key insight: When a digit is 0, the product is 0 and the condition is satisfied directly; the data range is small so brute force enumeration works.
3731. Find Missing Elements Easy
Problem: Numbers missing from a range of consecutive integers, returned as a sorted list.
func findMissingElements(nums []int) []int { slices.Sort(nums) // sort ans := []int{} for i := 0; i < len(nums)-1; i++ { for j := nums[i]; j != nums[i+1]-1; j++ { // fill the missing gaps ans = append(ans, j+1) } } return ans}Execution example: nums = [1,4,2,5]
| Step | nums[i] | nums[i+1] | Missing range | ans |
|---|---|---|---|---|
| 1 | 1 | 2 | none | [] |
| 2 | 2 | 4 | 3 | [3] |
| 3 | 4 | 5 | none | [3] |
- Key insight: After sorting, check the gaps where adjacent elements differ by more than 1.
II. Two Pointers
0011. Container With Most Water Medium
Problem: Two vertical lines plus a base form a container; find the maximum capacity.
Core idea: Left and right pointers move inward from the two ends; the shorter side moves (because moving the taller side can never increase the area).
l, r := 0, len(height)-1for l < r { if height[l] < height[r] { res = max(res, (r-l)*height[l]); l++ } else { res = max(res, (r-l)*height[r]); r-- }}- Key insight: Area = base × height; the base is shrinking, so only increasing the height can grow the area → move the shorter side.
- Pitfall: When both sides are equal, it doesn’t matter which moves.
graph LR
A[Left and right pointers start at the two ends] --> B{Which side is shorter?}
B -->|Left shorter| C[Compute area, move left pointer right]
B -->|Right shorter| D[Compute area, move right pointer left]
C --> E{l < r?}
D --> E
E -->|Yes| B
E -->|No| F[Return max area]0015. 3Sum Medium
Problem: Find all unique triplets that sum to 0.
Core idea: After sorting, fix one number and use two pointers to find the other two. Deduplicate: if the fixed number equals the previous one, skip it.
sort.Ints(nums)for l := 0; l < n; l++ { if nums[l] > 0 { break } // min > 0 → no solution possible if l > 0 && nums[l] == nums[l-1] { continue } // deduplicate m, r := l+1, n-1 for m < r { sum := nums[l] + nums[m] + nums[r] if sum == 0 { res = append(res, []int{nums[l], nums[m], nums[r]}) m++; r-- for m < r && nums[m] == nums[m-1] { m++ } // deduplicate for m < r && nums[r] == nums[r+1] { r-- } // deduplicate } else if sum > 0 { r-- } else { m++ } }}- Key insight: Sort + fix one + two pointers for the other two, turning the triple loop into O(n²).
- Pitfall: Three levels of deduplication (fixed number, left pointer, right pointer must all skip duplicates).
Execution flow (nums = [-1,0,1,2,-1,-4], sorted = [-4,-1,-1,0,1,2]):
| Fixed l | nums[l] | m | r | sum | Action |
|---|---|---|---|---|---|
| 0 | -4 | 1 | 5 | -4+(-1)+2=-3<0 | m++ |
| 0 | -4 | 2 | 5 | -4+(-1)+2=-3<0 | m++ |
| 0 | -4 | 3 | 5 | -4+0+2=-2<0 | m++ |
| 0 | -4 | 4 | 5 | -4+1+2=-1<0 | m++, m≥r exit |
| 1 | -1 | 2 | 5 | -1+(-1)+2=0 ✅ | found [-1,-1,2], m++,r— |
| 1 | -1 | 3 | 4 | -1+0+1=0 ✅ | found [-1,0,1], m++,r— |
| 1 | -1 | 4 | 3 | m≥r exit | - |
| 2 | -1(dup) | skip | l=2, nums[2]==nums[1] | ||
| 3 | 0 | 4 | 5 | 0+1+2=3>0 | r—, r<m exit |
Result = [[-1,-1,2], [-1,0,1]]
- ⚠️ Common failure: In the
sum == 0branch, mistakenly executingl++(outer loop variable) instead ofm++(inner left pointer), causing valid solutions to be skipped.
0027. Remove Element Easy
Problem: Remove all elements with value val in place.
cur, lst := 0, len(nums)-1for cur <= lst { if nums[lst] == val { lst-- } else if nums[cur] == val { nums[cur] = nums[lst]; lst-- } else { cur++ }}return lst + 1- Key insight: Two-end pointers; find
valon the left, find non-valon the right, swap them. - Why
curdoes not advance after swapping: The swap isnums[cur] = nums[lst], and the element coming from the right might also beval(e.g.nums=[3,2,2,3], val=3). Ifcur++is done here, this swappedvalstays at the front and is not removed. So in the swap branch we only dolst--, keepingcurin place to re-check the newly arrived element next round; only the “else branch” wherenums[cur] != valdoescur++. - ⚠️ Common failure: When
lst <= 0(array length 1) directly returning 0, but ifnums[0] != valit should return 1; or wrongly doingcur++after a swap, leaving the swappedvalbehind.
0042. Trapping Rain Water Hard
Problem: Given an array of bar heights, compute how much rainwater can be trapped.
Core idea: Two pointers + left/right maximums. The shorter side determines the water level at that position.
Key intuition (why the shorter side determines the water level): The rainwater at position i = min(left tallest wall, right tallest wall) - height[i] (barrel principle — water overflows from the shorter side). The trick of the two pointers is: when height[l] < height[r], there must be a wall ≥ height[r] to the right of position l (at least height[r] itself), so l’s “right max” is necessarily ≥ height[r] > height[l] ≥ maxLeft, meaning the water at l depends only on maxLeft, regardless of how tall the right side actually is! Similarly, when height[l] ≥ height[r], the water at r depends only on maxRight. That’s why the shorter side moves first.
l, r := 0, len(height)-1maxLeft, maxRight := 0, 0for l < r { maxLeft = max(maxLeft, height[l]) // first update the left max at current position maxRight = max(maxRight, height[r]) // first update the right max at current position if height[l] < height[r] { res += maxLeft - height[l]; l++ // left is shorter → water level decided by maxLeft } else { res += maxRight - height[r]; r-- // right is shorter/equal → water level decided by maxRight }}- Key insight: Rainwater at each position = min(left max, right max) - own height; two pointers let the shorter side move first.
- Pitfall: Update max first, then compute rain (use the latest maxLeft/maxRight, including current
height[l]/height[r]).
Bars and rainwater visualization (height = [0,1,0,2,1,0,1,3,2,1,2,1]):
Black = bar height, blue = trapped rainwater. Total rainwater = 6.
Execution example (two-pointer process):
| Step | left | right | maxL | maxR | h[l]<h[r]? | Rain this step | Total |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 11 | 0 | 1 | yes | 0-0=0 | 0 |
| 2 | 1 | 11 | 1 | 1 | no | 1-1=0 | 0 |
| 3 | 1 | 10 | 1 | 2 | yes | 1-1=0 | 0 |
| 4 | 2 | 10 | 1 | 2 | yes | 1-0=1 | 1 |
| 5 | 3 | 10 | 2 | 2 | no | 2-2=0 | 1 |
| 6 | 3 | 9 | 2 | 1 | no | 1-1=0 | 1 |
| 7 | 3 | 8 | 2 | 2 | no | 2-2=0 | 1 |
| 8 | 3 | 7 | 2 | 3 | yes | 2-2=0 | 1 |
| 9 | 4 | 7 | 2 | 3 | yes | 2-1=1 | 2 |
| 10 | 5 | 7 | 2 | 3 | yes | 2-0=2 | 4 |
| 11 | 6 | 7 | 2 | 3 | yes | 2-1=1 | 5 |
| 12 | 7 | 7 | 3 | 3 | met | 3-3=0 | 6 |
graph TD
S["Init: l=0, r=n-1, maxL=0, maxR=0"] --> C{"height[l] < height[r] ?"}
C -->|"Yes: left is shorter
right must have a wall ≥ height[r]
→ right max > left max"| L["Water at l depends only on maxL
res += maxL - height[l]
l++, update maxL"]
C -->|"No: right shorter or equal
left must have a wall ≥ height[l]
→ left max ≥ right max"| R["Water at r depends only on maxR
res += maxR - height[r]
r--, update maxR"]
L --> C
R --> C
C -->|"End when l >= r"| E["Return total rainwater = 6"]
style S fill: #e3f2fd, color: #1a1a1a
style E fill: #c8e6c9, color: #1a1a1a0088. Merge Sorted Array Easy
Problem: Merge nums2 into nums1 (nums1 has empty space at the end).
Core idea: Fill from the back; place the larger one at the back.
// p1 points to the end of nums1's valid elements, p2 points to the end of nums2, tail points to the write positionfor p1, p2, tail := m-1, n-1, m+n-1; p1 >= 0 || p2 >= 0; tail-- { if p1 == -1 { // nums1 exhausted, can only fill nums2 nums1[tail] = nums2[p2]; p2-- } else if p2 == -1 { // nums2 exhausted, can only fill nums1 nums1[tail] = nums1[p1]; p1-- } else if nums1[p1] > nums2[p2] { // both have elements, fill the larger (back-to-front keeps order) nums1[tail] = nums1[p1]; p1-- } else { nums1[tail] = nums2[p2]; p2-- }}- Key insight: Filling from the back avoids overwriting nums1 elements not yet processed.
0189. Rotate Array Medium
Problem: Rotate the array right by k positions (move the last k elements to the front), in place.
Core idea: Three reversals — reverse the whole array → reverse the first k → reverse the last n-k.
How to think of reversals: Rotating right by k = moving the last k elements as a block to the front. Reversal has a “symmetric” property: first reverse the whole array, which flips the last k elements to the front (but in reverse order); then reverse the first k and the last n-k back into correct order, yielding the right result. Three reversals neatly avoid an extra array.
k %= len(nums) // prevent k > n; rotating by k equals rotating by k%nreverse(nums) // ① reverse the whole array: the last k are flipped to the front (in reverse order)reverse(nums[:k]) // ② reverse the first k back into orderreverse(nums[k:]) // ③ reverse the last n-k back into orderExample (nums = [1,2,3,4,5,6,7], k = 3):
| Step | Operation | Array change |
|---|---|---|
| Initial | - | [1,2,3,4,5,6,7] |
| ① | reverse whole | [7,6,5,4,3,2,1] |
| ② | reverse first 3 | [5,6,7,4,3,2,1] |
| ③ | reverse last 4 | [5,6,7,1,2,3,4] ✓ |
The last 3 (5,6,7) successfully moved to the front.
- Key insight:
reverse(nums)+reverse(nums[:k])+reverse(nums[k:])is equivalent to rotating right by k.
0345. Reverse Vowels of a String Easy
Problem: Reverse all vowels in the string (aeiou, case-insensitive).
l, r := 0, len(s)-1b := []byte(s)for l < r { for l < r && !vowels[b[l]] { l++ } for l < r && !vowels[b[r]] { r-- } b[l], b[r] = b[r], b[l]; l++; r--}0392. Is Subsequence Easy
Problem: Determine whether s is a subsequence of t.
i, j := 0, 0for j < len(t) { if i < len(s) && s[i] == t[j] { i++ } j++ if i == len(s) { return true }}- Key insight:
t’s pointer only advances, never moves back.
0763. Partition Labels Medium
Problem: Partition the string so that each letter appears in at most one segment.
Core idea: First record the last occurrence of each letter; while traversing, keep extending the right boundary to “the final occurrence of every letter in the current segment”; once you reach the segment’s right boundary, that letter won’t appear again, so you can cut.
mp := map[rune]int{}for idx, ch := range s { mp[ch] = idx } // ① record the last index of each letterstart, end := 0, 0for idx, ch := range s { if mp[ch] > end { end = mp[ch] } // ② extend right boundary: if this letter goes further, the segment must reach there if idx == end { // ③ reached boundary: letters in segment won't appear again, can cut ans = append(ans, idx+1-start) // length = right boundary - left boundary + 1 start = idx + 1 // next segment starts at left boundary + 1 }}- Key insight: Segment right boundary = max of the “final occurrence positions” of all letters in the segment; cut when reaching the boundary, ensuring each letter lands in only one segment.
- Why it works: As long as the right boundary keeps extending, the current segment hasn’t “closed up” yet — its letters have more to come, so we must continue; once
idxcatches up toend, this segment is self-consistent and cutting it off doesn’t affect what follows.
1768. Merge Strings Alternately Easy
Problem: Merge word1 and word2 alternately, appending the remainder to the end.
i, j := 0, 0for i < m && j < n { sb.WriteByte(word1[i]); sb.WriteByte(word2[j]); i++; j++ }if m > n { sb.WriteString(word1[i:]) } else if n > m { sb.WriteString(word2[j:]) }III. Sliding Window
General template: Right pointer expands window → check window validity → left pointer shrinks window → update answer.
graph LR
A["right expands window"] --> B{"Window invalid?"}
B -->|Yes| C["left shrinks window"]
C --> B
B -->|No| D["Update answer"]
D --> A0003. Longest Substring Without Repeating Characters Medium
Problem: Find the length of the longest substring without repeating characters.
Core idea: Sliding window + hash table recording character positions. When a duplicate character is encountered, jump the left pointer to just after the duplicate.
mp := map[byte]int{}; l, res := 0, 0for r := 0; r < len(s); r++ { if idx, exists := mp[s[r]]; exists && idx >= l { l = idx + 1 // jump left pointer just after the duplicate } mp[s[r]] = r res = max(res, r-l+1)}- Key insight: The left pointer doesn’t step one by one, but jumps directly to just after the duplicate character.
- Pitfall: Check
idx >= l, because the position in the hash table may be before the left pointer (no longer in the window). - ⚠️ Common failure: Writing
res = max(res, r-1-l)instead ofr-l+1when updating the result, causing an off-by-one that counts 1 short.
0076. Minimum Window Substring Hard
Problem: Find the shortest substring in s that contains all characters of t (with sufficient counts), and return it.
Core idea: Sliding window + a count variable tracking how many character types still need matching.
tmap := map[byte]int{} // how many of each char in t are neededfor _, ch := range t { tmap[ch]++ }count := len(tmap) // number of character types still to satisfysmap := map[byte]int{}l, ansL, ansR := 0, 0, len(s)for r := 0; r < len(s); r++ { smap[s[r]]++ if smap[s[r]] == tmap[s[r]] { count-- } // this char just met its quota → one fewer to satisfy for count == 0 { // window valid, shrink as much as possible if r-l < ansR-ansL { ansL, ansR = l, r } // update shortest smap[s[l]]-- if smap[s[l]] < tmap[s[l]] { count++ } // after shrinking, this char is insufficient → one more to satisfy l++ }}Example (s = “ADOBECODEBANC”, t = “ABC”): count initially = 3 (A/B/C types). When right expands to r=5 (“ADOBEC”), A, B, C are all met → count=0, window valid; shrink left to l=3 (“BEC”) still valid and shorter; shrink further to l=4 (“EC”) missing A → count=1, stop shrinking, keep expanding right. Final shortest = “BANC”.
- Key insight: Use a single
countvariable (instead of scanning the hash table every round) to check validity in O(1). - Pitfall: When shrinking, the condition for “breaking validity” is
smap[s[l]] < tmap[s[l]](count goes from enough to not enough), not==.
0239. Sliding Window Maximum Hard
Problem: For a sliding window of size k, return the maximum within each window in order.
Core idea: Monotonically decreasing deque storing indices; the front is always the current window’s maximum.
Why a monotonic deque (key intuition): When the window moves right, if an old element is smaller than the new element val, it can never be the maximum again — because val is larger and further right (stays in the window longer). So whenever a new element arrives, pop all old elements smaller than it from the back, keeping the deque monotonically decreasing from front to back. The front is popped once it slides out of the window’s left boundary.
q := []int{} // store indices (not values! makes expiry easy to check)for i, val := range nums { // ① pop all old indices ≤ current value from the back: they can never be max again for len(q) > 0 && nums[q[len(q)-1]] <= val { q = q[:len(q)-1] } q = append(q, i) // ② current index enters the back if q[0] < i-k+1 { q = q[1:] } // ③ front index has slid out of window, pop it if i >= k-1 { ans = append(ans, nums[q[0]]) } // ④ window full, front is the max}- Key insight: Monotonically decreasing deque — old elements smaller than the new one are permanently eliminated, front is always the window maximum.
- Pitfall: Store indices in the deque, not values, so
q[0] < i-k+1can check whether it slid out of the window.
Execution flow (nums = [1,3,-1,-3,5,3,6,7], k = 3):
| Step | i | nums[i] | Deque (indices) | Deque (values) | Expiry check | Output |
|---|---|---|---|---|---|---|
| 1 | 0 | 1 | [0] | [1] | i<2, no output | - |
| 2 | 1 | 3 | [1] | [3] | i<2, no output | - |
| 3 | 2 | -1 | [1,2] | [3,-1] | q[0]=1≥0, OK | nums[1]=3 |
| 4 | 3 | -3 | [1,2,3] | [3,-1,-3] | q[0]=1≥1, OK | nums[1]=3 |
| 5 | 4 | 5 | [4] | [5] | q[0]=4≥2, OK | nums[4]=5 |
| 6 | 5 | 3 | [4,5] | [5,3] | q[0]=4≥3, OK | nums[4]=5 |
| 7 | 6 | 6 | [6] | [6] | q[0]=6≥4, OK | nums[6]=6 |
| 8 | 7 | 7 | [7] | [7] | q[0]=7≥5, OK | nums[7]=7 |
Output = [3,3,5,5,6,7], each time popping old elements smaller than the new one to keep the deque monotonically decreasing.
0438. Find All Anagrams in a String Medium
Problem: An anagram = strings with the exact same characters and counts, just arranged differently (e.g. “abc” and “cba”). Return the starting indices of all substrings in s of length len(p) that are anagrams of p.
Core idea: Fixed-length window + difference counting. Use cnt[ch] = (count of char in window) - (count of char in p), and diff = number of char types where cnt is non-zero. diff == 0 means the window matches p character-for-character, i.e. it’s an anagram.
cnt := [26]int{}; diff := 0// ① initialize: compare the first len(p) chars window of s with pfor i := 0; i < len(p); i++ { cnt[s[i]-'a']++ // one more in window cnt[p[i]-'a']-- // one more in p (subtract to get the difference)}for i := 0; i < 26; i++ { if cnt[i] != 0 { diff++ } }if diff == 0 { res = append(res, 0) }// ② slide window right: left out, right in, incrementally update cnt and difffor i := len(p); i < len(s); i++ { out := s[i-len(p)] - 'a' // character sliding out cnt[out]-- if cnt[out] == 0 { diff-- } else if cnt[out] == -1 { diff++ } // from unbalanced → balanced / from balanced → more lacking in := s[i] - 'a' // character sliding in cnt[in]++ if cnt[in] == 0 { diff-- } else if cnt[in] == 1 { diff++ } // from more → balanced / from balanced → more if diff == 0 { res = append(res, i-len(p)+1) } // balanced means anagram}- Key insight: Use a difference array +
diff(count of non-zero entries) to avoid comparing 26 letters every slide (O(1) check). - ⚠️ Common failure: Wrong
diffupdate logic —diffshould only change whencntcrosses the “0 ↔ non-zero” boundary; going from 1→2 or -2→-1 doesn’t count (already unbalanced, the change in quantity doesn’t change balance).
0643. Maximum Average Subarray I Easy
Problem: Find the maximum average of a contiguous subarray of length k.
for i := 0; i < k; i++ { sum += nums[i] }ans := float64(sum) / float64(k)for i := k; i < len(nums); i++ { sum += nums[i] - nums[i-k]; ans = max(ans, float64(sum)/float64(k)) }1004. Max Consecutive Ones III Medium
Problem: Flip at most k 0s to 1s in the array, and find the length of the longest consecutive-1 subarray achievable. Equivalent to: find the longest window containing at most k 0s.
Example: nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2 → flip any 2 of the 3 middle 0s to 1, connecting to the right-side 1s, giving longest consecutive-1 length = 6 (indices 0~5 all 1).
Core idea: Sliding window; valid while the number of 0s in the window ≤ k; shrink the left boundary once 0s exceed k.
for right := 0; right < n; right++ { if nums[right] == 0 { zeroCnt++ } for zeroCnt > k { if nums[left] == 0 { zeroCnt-- }; left++ } maxLen = max(maxLen, right-left+1)}- Key insight: “Flip at most k 0s” = “at most k 0s in the window”.
1456. Maximum Number of Vowels in a Substring of Given Length Medium
for i := 0; i < k; i++ { if vowels[s[i]] { count++ } }ans := countfor i := k; i < len(s); i++ { if vowels[s[i-k]] { count-- } if vowels[s[i]] { count++ } ans = max(ans, count)}- ⚠️ Common failure: Loop condition written as
i < len(s)-kinstead ofi <= len(s)-k, missing the last window. E.g.s="aeiou", k=5would skip the loop and return 0.
1493. Longest Subarray of 1’s After Deleting One Element Medium
Core idea: At most 1 zero in the window; the answer is window length - 1 (must delete one element).
for right := 0; right < n; right++ { if nums[right] == 0 { zeroCnt++ } for zeroCnt > 1 { if nums[left] == 0 { zeroCnt-- }; left++ } maxLen = max(maxLen, right-left) // note right-left not +1}- Key insight: Since you must delete one element, the answer is
right - left, notright - left + 1.
IV. Binary Search
General template:
left + (right-left)/2to avoid overflow, loop conditionleft <= right.
When can you use binary search (prerequisite: monotonicity): The fundamental prerequisite for binary search to “halve the search space each time” is that the search space is monotonic — there is a critical point splitting the interval into two segments (one satisfying a property, the other not, either left-satisfies/right-doesn’t or vice versa).
- An array itself being ascending is the most intuitive monotonicity (e.g. 0035, 0074).
- The more general “binary search on answer” requires the objective function to be monotonic in the variable, e.g. in 0875 the larger the speed the less time it takes (
total(k)decreases withk), so “whether it can finish within h hours” has a clear “left illegal / right legal” boundary.- If the data itself is unordered and no monotonic relationship with the index can be found, binary search has no basis and will miss solutions — don’t force it in such cases.
Why sometimes use
left < right:
left <= right(closed-interval template):[left, right]is always a valid closed interval,midmay itself be the answer, return on hit; when the loop exitsleft > right(they cross). Suitable for “find the exact index equal to target / the element definitely exists” (e.g. ordinary search besides 0033, 0287).left < right(boundary / lower-bound template): Loop exits only whenleft == right, at which pointleft(i.e.right) is the answer, no extra check needed. Commonly used for “find the first position satisfying a property” — e.g. the left boundaryleftBoundin 0034, the insertion position in 0035. Note that in this stylemidtakesleft + (right-left)/2(floor division), and updates usuallyright = mid(keepmidas a candidate), otherwiseleft = mid + 1, to avoid infinite loops.- One-line memory: Use
<=to “hit a specific value”; use<to “approach a boundary / lower bound”.
0033. Search in Rotated Sorted Array Medium
Problem: Find target in a rotated sorted array.
Core idea: In binary search, determine which half is sorted, then check whether target is in the sorted half.
left, right := 0, n-1for left <= right { mid := left + (right-left)/2 if nums[mid] == target { return mid } if nums[mid] >= nums[left] { // left half sorted if target >= nums[left] && target < nums[mid] { right = mid - 1 } else { left = mid + 1 } } else { // right half sorted if target > nums[mid] && target <= nums[right] { left = mid + 1 } else { right = mid - 1 } }}- Key insight: A rotated array always has one sorted half; check whether
targetis in the sorted half. - Pitfall: Use
>=innums[mid] >= nums[left]becausemidmay equalleft.
Execution flow (nums = [4,5,6,7,0,1,2], target = 0):
| Round | left | right | mid | nums[mid] | Judgment | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 7 | 7≥4, left sorted | target=0 not in [4,7), left=4 |
| 2 | 4 | 6 | 5 | 1 | 1<4, right sorted | target=0 not in (1,2], right=4 |
| 3 | 4 | 4 | 4 | 0 | 0==target | ✅ return 4 |
graph TD
A["binary search mid"] --> B{"nums[mid] == target?"}
B -->|Yes| C["return mid"]
B -->|No| D{"nums[mid] >= nums[left]?"}
D -->|Yes| E["left half sorted"]
D -->|No| F["right half sorted"]
E --> G{"target in left half?"}
F --> H{"target in right half?"}
G -->|Yes| I["right = mid-1"]
G -->|No| J["left = mid+1"]
H -->|Yes| J
H -->|No| I0034. Find First and Last Position of Element in Sorted Array Medium
Problem: In an ascending array, find the start and end indices (both inclusive) of target; if absent, return [-1,-1].
Core idea: Two binary searches — find the position of “first ≥ target” (left boundary) and “first > target” (right boundary = that position − 1).
How to maintain the boundary (binary search template): Use the standard “left = first ≥ target” form: when nums[mid] < target shrink the left half (left = mid+1), otherwise shrink the right half (right = mid-1). When done, left points to the first index ≥ target.
// left boundary = position of first >= targetleftBound := func() int { l, r := 0, len(nums)-1 for l <= r { mid := l + (r-l)/2 if nums[mid] < target { l = mid+1 } else { r = mid-1 } } return l // first >= target}// right boundary = position of first > target - 1 = position of first >= target+1 - 1rightBound := func() int { l, r := 0, len(nums)-1 for l <= r { mid := l + (r-l)/2 if nums[mid] <= target { l = mid+1 } else { r = mid-1 } // note <= } return l - 1}Example (nums = [5,7,7,8,8,10], target = 8):
Left boundary: first ≥ 8 → index 3; right boundary: first > 8 (i.e. first ≥ 9) → index 5, then −1 = 4. Result [3,4] ✓
Key insight: Sorted + O(log n) → must use binary search; one search for each boundary, avoiding linear scan after finding that degrades to O(n).
Pitfall: The two searches use different conditions — left boundary uses
nums[mid] < target, right boundary usesnums[mid] <= target.
0035. Search Insert Position Easy
Problem: Find target in a sorted array; return its index if found, otherwise return where it should be inserted.
for left <= right { mid := left + (right-left)/2 if nums[mid] == target { return mid } else if nums[mid] > target { right = mid - 1 } else { left = mid + 1 }}return left // left is the insertion position- Key insight: At the end of binary search,
leftpoints exactly to the insertion position.
0074. Search a 2D Matrix Medium
Problem: Each row of the matrix is ascending left to right, and the first element of each row is greater than the last element of the previous row (so the whole thing can be seen as one ascending 1D array). Determine whether target is in it.
Core idea: Two binary searches — treat the matrix as a 1D ascending array, first binary search for “the row target might be in”, then binary search within that row for target.
// map (r,c) to 1D index idx = r*cols + c, binary search the wholem, n := len(matrix), len(matrix[0])l, r := 0, m*n-1for l <= r { mid := l + (r-l)/2 val := matrix[mid/n][mid%n] // restore 2D coordinates from 1D index if val == target { return true } else if val < target { l = mid+1 } else { r = mid-1 }}return falseWhy it can be treated as 1D: Because of the “each row’s first > previous row’s last” condition, the matrix’s lexicographic order is exactly the same as its size order when flattened to 1D, so searching the whole is equivalent to finding the row first, then the column.
- Key insight: When the matrix satisfies the condition it is equivalent to a 1D ascending array; binary searching the whole is cleaner.
- Pitfall: Use
mid/n(row) andmid%n(column) to restore coordinates; watch for out-of-bounds.
0153. Find Minimum in Rotated Sorted Array Medium
Problem: An originally ascending array is rotated at some “pivot” (e.g. [0,1,2,4,5,6,7] → [4,5,6,7,0,1,2]). Find the minimum element. This version guarantees no duplicate elements in the array.
Core idea: After rotation, the array is split by the minimum into two segments — the left half is all >= the original first element, the right half is all < the original first element. When binary searching, comparing nums[mid] with nums[left] tells you which segment mid falls in:
nums[mid] >= nums[left]→midis in the left (sorted) segment, so the minimum must be to the right ofmid(inclusive ofmid+1), soleft = mid + 1, and treatnums[left]as a candidate minimum.nums[mid] < nums[left]→midis in the right (unsorted) segment, so the minimum must be to the left ofmid(possiblymiditself), soright = mid - 1, and treatnums[mid]as a candidate minimum.
if nums[mid] >= nums[left] { left = mid + 1; ans = min(ans, nums[left]) }else { right = mid - 1; ans = nums[mid] }Example nums = [3,4,5,1,2] (first element nums[0]=3, minimum should be 1):
| left | right | mid | nums[mid] vs nums[left] | Judgment | Action |
|---|---|---|---|---|---|
| 0 | 4 | 2 | 5 >= 3 | left segment sorted | ans=min(3,3)=3, left=3 |
| 3 | 4 | 3 | 1 >= 1 | left segment sorted | ans=min(3,1)=1, left=4 |
| 4 | 4 | 4 | 2 >= 2 | left segment sorted | ans=min(1,2)=1, left=5 → exit |
Return ans = 1.
- Key insight: A rotated array’s “sortedness” is broken at only one place (the pivot); binary search cuts away the sorted half each time.
- Pitfall: This version has no duplicates, so comparing with
nums[left]works; if the array has duplicates (problem 154) you must compare withnums[right]instead, or it will misjudge.
0240. Search a 2D Matrix II Medium
Problem: Find target in an m×n matrix where each row increases left to right and each column increases top to bottom.
Core idea: Start from the top-right corner; if too big, move left; if too small, move down.
r, c := 0, len(matrix[0])-1for r < len(matrix) && c >= 0 { if matrix[r][c] == target { return true } if matrix[r][c] > target { c-- } else { r++ }}- Key insight: The top-right corner has the BST property “smaller on the left, larger below”, eliminating a row or column each step.
- Pitfall: Can’t start from the top-left, because both right and down are larger so direction is ambiguous.
- ⚠️ Common failure: When binary searching row by row, conditions written as
target < matrix[r][0]andtarget > matrix[r][0]; whentarget == matrix[r][0]neither is satisfied, so the row is skipped. Should use>=.
0287. Find the Duplicate Number Medium
Problem: Given an array nums of length n+1, elements range over [1, n] (i.e. the value range is one less than the index range). Exactly one number repeats (possibly multiple times). Find the duplicate. Constraints: may not modify the original array, use only O(1) extra space, and run faster than O(n²).
What is cycle detection (Floyd’s Tortoise and Hare): Treat the array as a “directed graph”: i → nums[i] means an edge from index i to the index where the value nums[i] lives. Since values range over 1..n and indices are 0..n, starting from 0 you must enter some cycle — and that cycle’s entrance is exactly the repeating number.
Why must there be a cycle, and why is the entrance = the duplicate?
- Since the value range is only
1..n, every value except0maps to a valid index, so the path can’t diverge forever and must loop. - The repeating number
dupeis pointed to by at least two different indices (because two positions holddupe), so the nodedupehas two incoming edges → it’s the cycle entrance.
Cycle detection has two phases:
- Find the meeting point (slow steps 1, fast steps 2):
slow = nums[slow],fast = nums[nums[fast]]. If a cycle exists, they must meet at some point inside it. - Find the cycle entrance: one pointer starts from the start
head=0, slow starts from the meeting point, both step 1 each time; where they meet again is the cycle entrance = the duplicate.
Mathematical intuition: Let distance from “start to entrance” be a, distance from “entrance to meeting point” be b, cycle length be c. At meeting, slow walked a+b, fast walked 2(a+b)=a+b+k·c, giving a+b=k·c. This means walking a more steps from the meeting point lands exactly back at the entrance (k full loops); walking a steps from the start also reaches the entrance, so the two pointers must meet at the entrance.
slow, fast := 0, 0for { slow = nums[slow]; fast = nums[nums[fast]]; if slow == fast { break } }head := 0for slow != head { slow = nums[slow]; head = nums[head] }return slowExample nums = [1,3,4,2,2] (duplicate is 2):
Pointer path: 0 → 1 → 3 → 2 → 4 → 2 → 4 → … (2→4→2 forms a cycle, entrance is node 2)
- Phase 1: slow=0,fast=0 → slow=1,fast=3 → slow=3,fast=4 → slow=2,fast=4 → slow=4,fast=4 (meet at node 4)
- Phase 2: head=0,slow=4 → head=1,slow=2 → head=3,slow=4 → head=2,slow=2 (meet at node 2)
Return 2, the duplicate.
- Key insight: Treat the “value” as a “pointer to an index”; the duplicate value = the cycle entrance.
- Pitfall: Cannot sort the array or use a hash table (violates “don’t modify array / O(1) space”); cycle detection is the only approach satisfying both constraints.
0374. Guess Number Higher or Lower Easy
Problem: Guess a number between 1 and n, call guess() to get a hint (-1 too small / 0 correct / 1 too big), return the guessed number.
for { mid := left + (right-left)/2 res := guess(mid) if res == 0 { return mid } else if res == 1 { left = mid + 1 } else { right = mid - 1 }}0875. Koko Eating Bananas Medium
Problem: There are n piles of bananas, piles[i] is the count in the i-th pile. Koko must finish all bananas within h hours; each hour she picks one pile and eats k bananas (if fewer than k remain, she finishes that pile), and cannot eat from another pile that hour. Find the minimum speed k at which she can finish.
Why binary search works: The key property is “the larger the speed, the less total time” — total time total(k) is a monotonically decreasing function of k. This gives two conclusions:
- There is a threshold
K: whenk ≥ K,total(k) ≤ h(can finish); whenk < K,total(k) > h(can’t finish). - Since the boundary is monotonically clear (“everything left of the answer is illegal, everything right is legal”), we can binary search directly toward this critical point
Kinstead of trying from 1.
Search range: k is at least 1, at most maxPiles (slower than that still works, but no need to be larger).
left, right := 1, maxPile // speed range [1, max pile]for left <= right { mid := left + (right-left)/2 total := 0 for _, p := range piles { total += (p + mid - 1) / mid } // round up if total <= h { ans = mid; right = mid - 1 } else { left = mid + 1 }}Example piles = [3,6,7,11], h = 8:
| Tried speed k | Time per pile ⌈p/k⌉ | Total time | Can finish in 8h? |
|---|---|---|---|
| 4 | ⌈3/4⌉+⌈6/4⌉+⌈7/4⌉+⌈11/4⌉ = 1+2+2+3 | 8 | just enough |
| 3 | 1+2+3+4 | 10 | no |
Minimum legal speed is 4.
- Key insight: Banana-eating time decreases monotonically with speed → the answer has a “left illegal / right legal” boundary → binary search on answer.
- Pitfall: Time per pile must round up
(p+mid-1)/mid, notp/middirectly, or you’ll undercount a pile’s time.
0004. Median of Two Sorted Arrays Hard
Problem: Find the median of two sorted arrays, in O(log(m+n)).
Core idea: Median = the k-th smallest problem. Each binary search skips the smaller half of k/2, which can’t contain the k-th smallest.
func getKth(a, b []int, k int) int { if len(a) == 0 { return b[k-1] } // a exhausted if len(b) == 0 { return a[k-1] } // b exhausted if k == 1 { return min(a[0], b[0]) } // only 1 left midA := min(k/2-1, len(a)-1) // prevent overflow midB := min(k/2-1, len(b)-1) if a[midA] < b[midB] { return getKth(a[midA+1:], b, k-(midA+1)) // skip first half of a } else { return getKth(a, b[midB+1:], k-(midB+1)) // skip first half of b }}
func findMedianSortedArrays(a, b []int) float64 { total := len(a) + len(b) if total%2 == 1 { return float64(getKth(a, b, (total+1)/2)) // odd: take middle } return float64(getKth(a,b,total/2)+getKth(a,b,total/2+1)) / 2.0 // even: take average}Execution example: a = [1,3], b = [2], total length 3, find the 2nd smallest.
| Step | a | b | k | midA | midB | a[midA] | b[midB] | Operation |
|---|---|---|---|---|---|---|---|---|
| 1 | [1,3] | [2] | 2 | 0 | 0 | 1 | 2 | 1 < 2, skip a[0], k=1 |
| 2 | [3] | [2] | 1 | - | - | - | - | k=1, return min(3,2)=2 |
- Key insight: Reduce median to the k-th smallest problem, eliminate k/2 elements each time → O(log(m+n)).
- Pitfall:
midA = min(k/2-1, len(a)-1)prevents out-of-bounds when the array is shorter than k/2.
V. Linked List
Universal linked list opening move: Add a dummy sentinel node to avoid special-casing the head.
0002. Add Two Numbers Medium
Problem: Two reverse-stored linked lists (each node holds one digit, ones digit first), representing two non-negative integers. Return the result list of their sum, also reverse-stored. For example l1 = 2→4→3 is 342, l2 = 5→6→4 is 465, result should be 807 → 7→0→8.
What it does (simulating elementary addition digit by digit):
dummysentinel node: the “fake head” of the result list, so we don’t special-case assigning the “first node”;tailalways points to the end of what’s been built, and new nodes attach totail.Next. Finally returndummy.Nextas the real head.carrycarries the carry: each digit addition may produce a carry (e.g. 7+8=15, keep 5 in the ones place, carry 1), whichcarrybrings to the next digit.- Loop condition
l1 != nil || l2 != nil: keep going as long as either list still has nodes (or there’s a carry); pad with 0 for missing digits.
dummy := &ListNode{}; tail := dummy; carry := 0for l1 != nil || l2 != nil { sum := carry if l1 != nil { sum += l1.Val; l1 = l1.Next } if l2 != nil { sum += l2.Val; l2 = l2.Next } sum, carry = sum%10, sum/10 // current digit = sum%10, carry = sum/10 tail.Next = &ListNode{Val: sum}; tail = tail.Next}if carry > 0 { tail.Next = &ListNode{Val: carry} } // if there's still a carry at the end, add a digitreturn dummy.NextDigit-by-digit walkthrough l1 = 2→4→3 (342), l2 = 5→6→4 (465), correct sum 807:
| Round | l1.Val | l2.Val | carry in | Raw sum | Current digit (sum%10) | New carry (sum/10) | Node attached |
|---|---|---|---|---|---|---|---|
| 1 | 2 | 5 | 0 | 7 | 7 | 0 | 7 |
| 2 | 4 | 6 | 0 | 10 | 0 | 1 | 0 |
| 3 | 3 | 4 | 1 | 8 | 8 | 0 | 8 |
| 4 | - | - | 0 | done | - | - | - |
Gets 7→0→8 (i.e. 807), correct.
- Key insight: dummy sentinel unifies head handling + carry flows through every digit.
- Pitfall: Don’t write the loop condition as
l1 != nil && l2 != nil, or the remaining digits of the longer list would be dropped; and after the loop, check whethercarryis still non-zero.
0019. Remove Nth Node From End of List Medium
Core idea: Fast pointer moves N steps first, then they move together. When the fast pointer reaches the tail, the slow pointer is at the (N+1)-th from the end.
dummy := &ListNode{Next: head}left, right := dummy, headfor i := 0; i < n; i++ { right = right.Next }for right != nil { left = left.Next; right = right.Next }left.Next = left.Next.Next // deletereturn dummy.Next- Key insight: Fast pointer moves N steps first to create the gap; dummy avoids deleting the head.
0021. Merge Two Sorted Lists Easy
dummy := &ListNode{}; tail := dummyfor l1 != nil && l2 != nil { if l1.Val < l2.Val { tail.Next = l1; l1 = l1.Next } else { tail.Next = l2; l2 = l2.Next } tail = tail.Next}if l1 != nil { tail.Next = l1 } else { tail.Next = l2 }return dummy.Next0023. Merge k Sorted Lists Hard
Core idea: Divide and conquer — merge in pairs, like merge sort.
for len(lists) > 1 { var merged []*ListNode for i := 0; i < len(lists); i += 2 { if i+1 >= len(lists) { merged = append(merged, lists[i]); continue } merged = append(merged, mergeTwo(lists[i], lists[i+1])) } lists = merged}- Key insight: Pairwise merging reduces merging K lists to O(N log K).
- Pitfall: You can also use a min-heap to take the smallest node each time.
Divide-and-conquer merge diagram (K=4 lists):
graph TD
subgraph "Round 0"
L1["List1: 1→4→5"]
L2["List2: 1→3→4"]
L3["List3: 2→6"]
L4["List4: 3→7"]
end
subgraph "Round 1: merge in pairs"
M12["Merge 1+2: 1→1→3→4→4→5"]
M34["Merge 3+4: 2→3→6→7"]
end
subgraph "Round 2: final merge"
F["Merge M12+M34: 1→1→2→3→3→4→4→5→6→7"]
end
L1 --> M12
L2 --> M12
L3 --> M34
L4 --> M34
M12 --> F
M34 --> F
style F fill: #c8e6c9, color: #1a1a1aNumber of lists halves each round, log K rounds total, O(N) comparisons per round.
0024. Swap Nodes in Pairs Medium
Problem: Swap every two adjacent nodes, return the head.
dummy := &ListNode{Next: head}; pre := dummyfor head != nil && head.Next != nil { first, second := head, head.Next pre.Next = second first.Next = second.Next second.Next = first pre = first; head = first.Next}return dummy.Next0025. Reverse Nodes in k-Group Hard
Core idea: Take each group of k, detach → reverse → reconnect. Use pre and tail to mark the predecessor and tail of each group.
dummy := &ListNode{Next: head}; pre, tail := dummy, dummyfor { for i := 0; i < k; i++ { tail = tail.Next if tail == nil { return dummy.Next } // fewer than k } next := tail.Next; groupHead := pre.Next tail.Next = nil reversedHead := reverseList(groupHead) // reverse pre.Next = reversedHead groupHead.Next = next // original head becomes tail, connect to next group pre, tail = groupHead, groupHead}- Key insight: Detach → reverse → reconnect in three steps;
preandtailare the connecting bridges.
0138. Copy List with Random Pointer Medium
Problem: Deep-copy a linked list with random pointers, return the head of the new list.
Core idea: In-place interleaved copy (O(1) space).
// 1. Insert a copy node after each node: A→A'→B→B'// 2. Set random: now.Next.Random = now.Random.Next// 3. Split and detach- Key insight: After interleaved copy,
original.Random.Nextis the copy node corresponding to random.
0141. Linked List Cycle Easy
Problem: Determine whether the linked list has a cycle.
fast, slow := head, headfor fast != nil && fast.Next != nil { fast = fast.Next.Next; slow = slow.Next if fast == slow { return true }}return false- Key insight: Fast and slow pointers must meet inside the cycle.
0142. Linked List Cycle II Medium
Problem: Find the node where the cycle begins, or null if none.
Core idea: After fast and slow meet, one returns to head and they move together; the next meeting is the cycle entrance.
// Phase 1: find the meeting point// Phase 2: head and slow move at the same speedfor head != slow { head = head.Next; slow = slow.Next }return head- Key insight: It’s proven mathematically that
a = c(head to entrance = meeting point to entrance).
Pointer trace (list: 3→2→0→-4→(back to 2), cycle entrance = node 2):
graph LR
H["head(3)"] -->|" a=1 step "| E["entrance(2)"]
E -->|" b=1 step "| N0["(0)"]
N0 -->|" 1 step "| M["meeting point(-4)"]
M -->|" c=1 step "| E
style E fill: #c8e6c9, stroke: #333, color: #1a1a1a
style M fill: #fff9c4, stroke: #333, color: #1a1a1aExecution flow:
| Phase | fast | slow | Explanation |
|---|---|---|---|
| Initial | 3 | 3 | both start from head |
| Step 1 | 2→0 | 2 | fast steps 2, slow steps 1 |
| Step 2 | -4→2→0 | 0 | fast steps 2, slow steps 1 |
| Step 3 | -4 | -4 | ✅ meet at -4 |
| Phase 2 | head=3 | slow=-4 | one back to head, same speed |
| Step 1 | 2 | 2 | ✅ meet at node 2 = cycle entrance |
Key:
a = c, so moving from head and the meeting point at the same speed must meet at the entrance.
0146. LRU Cache Medium
Problem: Implement an LRU cache with O(1) get and put, evicting the least recently used when over capacity.
Core idea: Hash table + doubly linked list. Hash table for O(1) lookup, doubly linked list for O(1) reordering.
type LRUCache struct { cache map[int]*DLinkedNode head, tail *DLinkedNode // virtual head and tail capacity int}// Get: move to head after lookup// Put: add new node at head, evict tail if over capacity- Key insight: Hash table locates, doubly linked list orders; together = O(1) LRU.
- Pitfall: After evicting the tail node, you must also
deleteit from the map; virtual head/tail nodes simplify boundary handling.
Doubly linked list diagram (capacity=2, operations: put(1,1), put(2,2), get(1), put(3,3)):
graph LR
subgraph "after put(1,1)"
H1["head⟷"] --> N1["key=1,val=1"] --> T1["⟷tail"]
end
subgraph "after put(2,2)"
H2["head⟷"] --> N2A["key=2,val=2"] --> N2B["key=1,val=1"] --> T2["⟷tail"]
end
subgraph "after get(1): 1 moved to head"
H3["head⟷"] --> N3A["key=1,val=1"] --> N3B["key=2,val=2"] --> T3["⟷tail"]
end
subgraph "after put(3,3): evict tail key=2"
H4["head⟷"] --> N4A["key=3,val=3"] --> N4B["key=1,val=1"] --> T4["⟷tail"]
end
style N1 fill: #e3f2fd, color: #1a1a1a
style N2A fill: #e3f2fd, color: #1a1a1a
style N3A fill: #c8e6c9, color: #1a1a1a
style N4A fill: #fff9c4, color: #1a1a1aGreen = just accessed, moved to head; yellow = newly inserted; blue = ordinary node. Head = most recently used, tail = least recently used.
0148. Sort List Medium
Problem: Sort the linked list in O(n log n) time.
Core idea: Merge sort — fast & slow pointers to find the middle, sort recursively, then merge.
slow, fast := head, head.Next // fast starts from head.Nextfor fast != nil && fast.Next != nil { slow = slow.Next; fast = fast.Next.Next }rightHead := slow.Next; slow.Next = nil // detachreturn merge(sortList(head), sortList(rightHead))- Key insight: Linked-list merge sort is naturally suited — find middle with fast/slow pointers, merge with two pointers.
- Pitfall:
fastmust start athead.Nextnothead, otherwise even-length splits are uneven.
0160. Intersection of Two Linked Lists Easy
Problem: Find the starting node where two singly linked lists intersect, or null if they don’t.
Core idea: Two pointers traverse each other’s list; with equal total distance, they must meet at the intersection.
a, b := headA, headBfor a != b { if a == nil { a = headB } else { a = a.Next } if b == nil { b = headA } else { b = b.Next }}return a- Key insight: Both pointers reach the intersection or nil after
lenA + lenBsteps.
0206. Reverse Linked List Easy
var pre *ListNode; cur := headfor cur != nil { temp := cur.Next // save temporarily cur.Next = pre // reverse pre = cur // advance cur = temp}return pre- Key insight: Save next to prevent breaking the chain; three pointers advance step by step.
0234. Palindrome Linked List Easy
Core idea: Convert to array + two-pointer comparison (simple version). Advanced: fast/slow find middle + reverse second half + compare.
0328. Odd Even Linked List Medium
odd := head; even := head.Next; evenHead := evenfor even != nil && even.Next != nil { odd.Next = odd.Next.Next; even.Next = even.Next.Next odd = odd.Next; even = even.Next}odd.Next = evenHead // odd list connects to even list2095. Delete the Middle Node of a Linked List Medium
slow, fast := head, head; var pre *ListNodefor fast != nil && fast.Next != nil { fast = fast.Next.Next; pre = slow; slow = slow.Next}pre.Next = pre.Next.Next2130. Maximum Twin Sum of a Linked List Medium
Problem: Pair the first half of the list with the reversed second half position by position, and find the maximum sum of each pair.
Core idea: Fast/slow find middle → reverse second half → two pointers move together to find the max sum.
// 1. fast/slow find middle// 2. reverse second half// 3. x, y := head, reversedHead; find max(x.Val + y.Val)VI. Stacks and Monotonic Stacks
0020. Valid Parentheses Easy
m := map[byte]byte{')': '(', ']': '[', '}': '{'}stk := []byte{}for i := 0; i < len(s); i++ { if cur, exist := m[s[i]]; exist { // right parenthesis if len(stk) == 0 || stk[len(stk)-1] != cur { return false } stk = stk[:len(stk)-1] } else { stk = append(stk, s[i]) } // left parenthesis onto stack}return len(stk) == 00032. Longest Valid Parentheses Hard
Core idea: Stack stores indices; the bottom always holds the last unmatched position.
stack := []int{-1} // initial -1 as baselinefor i, c := range s { if c == '(' { stack = append(stack, i) } else { stack = stack[:len(stack)-1] // pop if len(stack) == 0 { stack = append(stack, i) } // stack empty, update baseline else { ans = max(ans, i-stack[len(stack)-1]) } // compute length }}- Key insight: The bottom of the stack holds the “last unmatched position”; current index minus stack top is the valid length.
- Pitfall: When the stack is empty, push the current right parenthesis index as the new baseline.
- ⚠️ Common failure: Accumulating all matched parenthesis pairs in
ansinstead of tracking the longest contiguous valid substring length. E.g."()(()"returns 4 instead of the correct 2.
0084. Largest Rectangle in Histogram Hard
Core idea: Monotonically increasing stack. When the current bar is shorter than the stack top, pop the top and compute the area using the popped bar as height.
stack := []int{}for i := 0; i <= n; i++ { for len(stack) > 0 && (i == n || heights[i] < heights[stack[len(stack)-1]]) { h := heights[stack[len(stack)-1]]; stack = stack[:len(stack)-1] left := -1; if len(stack) > 0 { left = stack[len(stack)-1] } ans = max(ans, h*(i-left-1)) // width = i - left - 1 } stack = append(stack, i)}- Key insight: The largest rectangle with a given bar as height is bounded by the first shorter bar on its left and right; monotonic stack finds them in O(1).
- Pitfall: After the loop, clear the remaining stack (right boundary is n); add a sentinel
heights[n]=0to simplify.
Histogram visualization (heights = [2,1,5,6,2,3]):
Execution flow (heights = [2,1,5,6,2,3], sentinel heights[6]=0):
| Step | i | Current height | Stack (indices) | Action | Area computed |
|---|---|---|---|---|---|
| 1 | 0 | 2 | [] | push | - |
| 2 | 1 | 1 | [0] | 1<2, pop 0 | 2×(1-(-1)-1)=2 |
| 3 | 1 | 1 | [] | push | - |
| 4 | 2 | 5 | [1] | 5>1, push | - |
| 5 | 3 | 6 | [1,2] | 6>5, push | - |
| 6 | 4 | 2 | [1,2,3] | 2<6, pop 3 | 6×(4-2-1)=6 |
| 7 | 4 | 2 | [1,2] | 2<5, pop 2 | 5×(4-1-1)=10 ✓max |
| 8 | 4 | 2 | [1] | 2>1, push | - |
| 9 | 5 | 3 | [1,4] | 3>2, push | - |
| 10 | 6 | 0(sentinel) | [1,4,5] | 0<3, pop 5 | 3×(6-4-1)=3 |
| 11 | 6 | 0 | [1,4] | 0<2, pop 4 | 2×(6-1-1)=8 |
| 12 | 6 | 0 | [1] | 0<1, pop 1 | 1×(6-(-1)-1)=6 |
Max area = 10 (bar height 5, width 2, i.e. heights 5 and 6 at indices 2-3).
0155. Min Stack Medium
Problem: Implement a stack with push/pop/top/getMin all O(1).
Core idea: An auxiliary stack maintains the minimum synchronously.
func Push(value int) { num = append(num, value) minNum = append(minNum, min(value, minNum[top])) // synchronously push current minimum}func GetMin() int { return minNum[top] }0394. Decode String Medium
Problem: Decode “3[a2[c]]” → “accaccacc”; the number indicates how many times the string in the following brackets repeats.
Core idea: On [, push context to stack; on ], pop context and concatenate.
if s[i] == '[' { strStack = append(strStack, currStr) // save current string numStack = append(numStack, currNum) // save repeat count currStr = ""; currNum = 0 // reset} else if s[i] == ']' { prev := strStack[len(strStack)-1]; strStack = strStack[:len(strStack)-1] times := numStack[len(numStack)-1]; numStack = numStack[:len(numStack)-1] currStr = prev + strings.Repeat(currStr, times) // concatenate}- Key insight:
[pushes context,]pops and restores context. - Pitfall: Numbers may be multi-digit, need
currNum = currNum*10 + int(s[i]-'0').
Execution flow (s = “3[a2[c]]”):
| Step | Char | Action | currStr | currNum | strStack | numStack |
|---|---|---|---|---|---|---|
| 1 | ‘3’ | accumulate number | "" | 3 | [] | [] |
| 2 | ’[’ | push context, reset | "" | 0 | [""] | [3] |
| 3 | ‘a’ | append to currStr | “a” | 0 | [""] | [3] |
| 4 | ‘2’ | accumulate number | “a” | 2 | [""] | [3] |
| 5 | ’[’ | push context, reset | "" | 0 | ["",“a”] | [3,2] |
| 6 | ‘c’ | append to currStr | “c” | 0 | ["",“a”] | [3,2] |
| 7 | ’]’ | pop: prev=“a”, times=2 → “a”+“cc”=“acc” | “acc” | 0 | [""] | [3] |
| 8 | ’]’ | pop: prev="", times=3 → ""+“accaccacc”=“accaccacc” | “accaccacc” | 0 | [] | [] |
Result = “accaccacc”
0735. Asteroid Collision Medium
Problem: Asteroid array, positive goes right, negative goes left; same direction doesn’t collide, opposite directions collide where the larger destroys the smaller, equal sizes destroy both; return the asteroids remaining after collisions.
Core idea: Stack simulation. Only “stack top goes right + current goes left” collides.
for _, a := range asteroids { alive := true for alive && a < 0 && len(st) > 0 && st[len(st)-1] > 0 { alive = st[len(st)-1] < -a // whether current survives if st[len(st)-1] <= -a { st = st[:len(st)-1] } // stack top explodes } if alive { st = append(st, a) }}- Key insight: Only opposite directions (right←left) collide; same direction or reversed don’t collide.
0739. Daily Temperatures Medium
Problem: For each day’s temperature, find how many days until the next higher temperature; 0 if none.
Core idea: Monotonically decreasing stack (stores indices); when a higher temperature arrives, pop and fill.
for i := 0; i < len(T); i++ { for len(idx) > 0 && T[i] > T[idx[len(idx)-1]] { ans[idx[len(idx)-1]] = i - idx[len(idx)-1] idx = idx[:len(idx)-1] } idx = append(idx, i)}- Key insight: The stack holds indices “still waiting for a higher temperature”; pop when a larger one arrives.
graph TD
A["i=0, T[0]=73
stack empty, push"] --> B["i=1, T[1]=74 > 73
pop 73, ans[0]=1
push 74"]
B --> C["i=2, T[2]=75 > 74
pop 74, ans[1]=1
push 75"]
C --> D["i=3, T[3]=71 < 75
push directly"]
D --> E["i=4, T[4]=69 < 71
push directly"]
E --> F["i=5, T[5]=72 > 69
pop 69, ans[4]=1
72 > 71, pop 71, ans[3]=2
72 < 75, push 72"]
F --> G["i=6, T[6]=76 > 72
pop 72, ans[5]=1
76 > 75, pop 75, ans[2]=4
push 76"]
style A fill: #e8f5e9, color: #1a1a1a
style G fill: #fff9c4, color: #1a1a1a2390. Removing Stars From a String Medium
Problem: In the string a star * deletes the nearest non-star character to its left; return the final string.
Core idea: Stack simulation — on *, pop the top; on a letter, push.
func removeStars(s string) string { var res []rune for _, c := range s { if c != '*' { res = append(res, c) // letter onto stack } else { res = res[:len(res)-1] // star pops top } } return string(res)}Execution example: s = “leet**cod*e”
| Step | Char | Operation | Stack state |
|---|---|---|---|
| 1 | l | push | l |
| 2 | e | push | le |
| 3 | e | push | lee |
| 4 | t | push | leet |
| 5 | * | pop t | lee |
| 6 | * | pop e | le |
| 7 | c | push | lec |
| 8 | o | push | leco |
| 9 | d | push | lecod |
| 10 | * | pop d | leco |
| 11 | e | push | lecoe |
- Key insight: A star deleting the character to its left = a stack pop, naturally suited for stack simulation.
VII. Binary Tree
Universal tree playbook: Think clearly about three things — ① what the current node does ② what the left subtree’s recursion returns ③ what the right subtree’s recursion returns.
graph TD
A["Tree problem type decision"] --> B{"Need to traverse the whole tree?"}
B -->|Yes| C["DFS/BFS traversal"]
B -->|No| D{"Need info from left & right subtrees?"}
D -->|Yes| E["Post-order traversal
return value to parent"]
D -->|No| F["Pre-order traversal
pass value to children"]
C --> G{"Need by level?"}
G -->|Yes| H["BFS level order"]
G -->|No| I["DFS recursion"]0094. Binary Tree Inorder Traversal Easy
func dfs(node *TreeNode, res *[]int) { if node == nil { return } dfs(node.Left, res) // left *res = append(*res, node.Val) // root dfs(node.Right, res) // right}- Key insight: Inorder = left→root→right; BST inorder traversal is an ascending sequence.
0098. Validate Binary Search Tree Medium
Core idea: Recursion + range constraint. Each node must lie within the (lower, upper) open interval.
func helper(node *TreeNode, lower, upper int) bool { if node == nil { return true } if node.Val <= lower || node.Val >= upper { return false } return helper(node.Left, lower, node.Val) && helper(node.Right, node.Val, upper)}- Key insight: The BST constraint is about the range of the whole subtree, not just parent-child comparison.
- Pitfall: The interval is open, so the condition uses
<=and>=. - ⚠️ Common failure: Initial bounds use
-1<<31and1<<31-1(int32 range), but node values range exactly over[-2^31, 2^31-1], causing boundary values to be misjudged. You should usemath.MinInt64/math.MaxInt64.
0101. Symmetric Tree Easy
Problem: Determine whether a binary tree is “axis-symmetric” — i.e. the whole tree is mirrored left-right about the central axis (not checking that left and right subtrees have the same structure, but that they are mirror images of each other).
Why we compare the two swapped at the end: To judge “symmetric” you can’t just compare left subtree == right subtree; you must compare left child of left subtree ↔ right child of right subtree (outer to outer) and right child of left subtree ↔ left child of right subtree (inner to inner). That’s the “interleaved” recursion of check(left.Left, right.Right) and check(left.Right, right.Left) — comparing the left subtree “flipped” against the right subtree.
func check(left, right *TreeNode) bool { if left == nil && right == nil { return true } if left == nil || right == nil { return false } return left.Val == right.Val && check(left.Left, right.Right) && // interleaved comparison check(left.Right, right.Left)}Mirror comparison diagram (node values labeled, arrows show comparison pairings):
1 / \ 2 2 ← compare 2 and 2's values / \ / \ 3 4 4 3 ← left 2's left 3 ↔ right 2's right 3 (outer) ← left 2's right 4 ↔ right 2's left 4 (inner)- Key insight: Symmetric = left and right subtrees are mirror images; recurse with “left-left pairs right-right, left-right pairs right-left” interleaved comparison.
- Pitfall: Easy to write
check(left, right)comparing the two subtrees directly (that judges “equal” not “symmetric”); be sure to cross-pairLeft/Right.
0102. Binary Tree Level Order Traversal Medium
Core idea: BFS with a queue, or DFS passing a level parameter.
// DFS versionfunc level(node *TreeNode, l int) { if node == nil { return } if l >= len(res) { res = append(res, []int{}) } // new level res[l] = append(res[l], node.Val) level(node.Left, l+1) level(node.Right, l+1)}0104. Maximum Depth of Binary Tree Easy
func maxDepth(root *TreeNode) int { if root == nil { return 0 } return max(maxDepth(root.Left), maxDepth(root.Right)) + 1}- Key insight:
max(left depth, right depth) + 1.
0105. Construct Binary Tree from Preorder and Inorder Traversal Medium
Core idea: The first of preorder is the root; find the root’s position in inorder to split left and right.
root := &TreeNode{Val: preorder[0]}i := indexOf(inorder, preorder[0])root.Left = buildTree(preorder[1:1+i], inorder[:i])root.Right = buildTree(preorder[1+i:], inorder[i+1:])- Key insight: Preorder determines the root, inorder splits left and right.
- Pitfall: Slice boundaries are easy to get wrong; left subtree preorder range is
preorder[1 : 1+left subtree length].
0108. Convert Sorted Array to Binary Search Tree Easy
func bst(l, h int) *TreeNode { if l > h { return nil } mid := (l + h) / 2 return &TreeNode{Val: nums[mid], Left: bst(l, mid-1), Right: bst(mid+1, h)}}- Key insight: Taking the middle as root naturally guarantees balance.
0114. Flatten Binary Tree to Linked List Medium
Problem: “In place” flatten a binary tree into a singly linked list with only right pointers, keeping the node order as the tree’s preorder traversal (root→left→right). Modify in place, don’t create new nodes.
Before and after flattening (preorder should be 1→2→3→4→5→6):
Before: After (linked list with only right pointers): 1 1 / \ \ 2 5 2 / \ \ \3 4 6 3 \ 4 \ 5 \ 6How to do it: For the current node cur, if it has a left subtree:
- Find the rightmost node
prein the left subtree (it’s the last node of the left subtree’s preorder). - Attach
cur’s original right subtree topre.Right(preserve the right side). - Change
cur.Righttocur.Left(move left subtree to the right), and setcur.Leftto nil. - Move
curone step right, repeat until all left subtrees are “moved” onto the right subtree chain.
cur := rootfor cur != nil { if cur.Left != nil { pre := cur.Left for pre.Right != nil { pre = pre.Right } // find rightmost node of left subtree pre.Right = cur.Right // attach original right subtree cur.Right = cur.Left // left becomes right cur.Left = nil } cur = cur.Right}- Key insight: Like Morris traversal, insert the left subtree between the current node and the right subtree, keeping preorder order.
- Pitfall: Loop condition is
cur != nilnotcur.Left != nil. - ⚠️ Common failure: Loop condition written as
for cur.Left != nil; when the current node has no left subtree the loop exits, but a deeper right subtree may still have a left subtree that needs flattening.
0124. Binary Tree Maximum Path Sum Hard
Core idea: Recursion returns the “single-side maximum”, while at each node compute the “full path” to update the answer.
func dfs(root *TreeNode) int { if root == nil { return 0 } left := max(dfs(root.Left), 0) // discard negative branch right := max(dfs(root.Right), 0) ans = max(ans, left+right+root.Val) // full path (current as vertex) return max(left, right) + root.Val // single-side path (to parent)}- Key insight: The return value and the answer update are two different things — return single-side (can be chained), answer takes double-side (full path).
- Pitfall: Discard negative branches with
max(x, 0).
Single-side vs double-side path diagram:
graph TD
subgraph "Return value = single-side (to parent)"
A["Node(10)"] --> B["Left child: max(left,0)+10"]
A --> C["Right child: max(right,0)+10"]
A --> D["Return: max(B,C)"]
end
subgraph "Answer = double-side (full path, current as vertex)"
E["Node(10)"] --> F["Left branch"]
E --> G["Right branch"]
F --> H["ans update: left+right+10"]
end
style D fill: #e3f2fd, color: #1a1a1a
style H fill: #c8e6c9, color: #1a1a1aBlue = single-side path returned to parent (can take only left or right); green = double-side path computed at current node (take both, update global answer).
0199. Binary Tree Right Side View Medium
Problem: Looking from the right side of the binary tree, return the values of the rightmost node at each level (top to bottom).
Core idea: Level order traversal, take the last node of each level.
// BFS: the last node of each level is the right-side viewres = append(res, values[len(values)-1])0226. Invert Binary Tree Easy
root.Left, root.Right = root.Right, root.LeftinvertTree(root.Left)invertTree(root.Right)return root0230. Kth Smallest Element in a BST Medium
Core idea: BST inorder traversal is ascending; the k-th smallest = the k-th in inorder.
func dfs(node *TreeNode) bool { if node == nil { return false } if dfs(node.Left) { return true } k-- if k == 0 { res = node.Val; return true } // found, stop return dfs(node.Right)}0236. Lowest Common Ancestor of a Binary Tree Medium
Problem: Find the lowest common ancestor (LCA) of two nodes p and q in a binary tree.
if root == nil || root == p || root == q { return root }left := lowestCommonAncestor(root.Left, p, q)right := lowestCommonAncestor(root.Right, p, q)if left != nil && right != nil { return root } // both found → current is LCAif left == nil { return right }return left- Key insight: When p and q are in the left and right subtrees respectively, the current node is the LCA.
Recursion return value diagram (root=3, p=5, q=1):
graph TD
N3["3 (root)"] --> N5["5"]
N3 --> N1["1"]
N5 --> N6["6"]
N5 --> N2["2"]
N1 --> N0["0"]
N1 --> N8["8"]
style N5 fill: #c8e6c9, stroke: #333, color: #1a1a1a
style N1 fill: #fff9c4, stroke: #333, color: #1a1a1a
style N3 fill: #ffcdd2, stroke: #333, color: #1a1a1a| Node | Left returns | Right returns | Judgment |
|---|---|---|---|
| 6 | nil | nil | return nil |
| 2 | nil | nil | return nil |
| 5 | nil | nil | 5==p → return 5 |
| 0 | nil | nil | return nil |
| 8 | nil | nil | return nil |
| 1 | nil | nil | 1==q → return 1 |
| 3 | 5(non-nil) | 1(non-nil) | both found → 3 is LCA |
Green = p, yellow = q, red = LCA. p and q in left and right subtrees respectively → root is the LCA.
0437. Path Sum III Medium
Problem: In a binary tree, find the number of paths whose path sum equals targetSum. Here a “path” doesn’t need to start at the root or end at a leaf — it just needs to be a top-down, contiguous, non-bending chain (any start, any end).
Prefix-sum idea (analogy to the array “two-sum”):
- Let
s(X)= the sum of all node values along the path from the root to nodeX(prefix sum). - If a path goes from an ancestor
A’s child all the way toX, its path sum =s(X) - s(A)(A is some ancestor of X,s(A)is the prefix sum up to A). - We want
s(X) - s(A) == targetSum, i.e.s(A) == s(X) - targetSum. So: the number of paths ending at X with sum targetSum = the number of ancestors whose prefix sum equalss(X)-targetSum, tracked in real time with a hash tablecnt.
Why backtrack: cnt should only record prefix sums that appeared on “the one path from root to current X”. When DFS finishes X’s left subtree and turns to the right subtree, it must undo X’s contribution cnt[s]-- — otherwise ancestors of other sibling branches would be wrongly counted.
cnt := map[int]int{0: 1} // 0:1 handles the path "starting from the current node itself"func dfs(node *TreeNode, s int) { s += node.Val ans += cnt[s-targetSum] // however many ancestors have prefix sum = s-targetSum, that many valid paths cnt[s]++ dfs(node.Left, s); dfs(node.Right, s) cnt[s]-- // backtrack! leaving this node, undo its contribution to cnt}Example targetSum = 8, tree: 10 / \ 5 -3 / \ \ 3 2 11 / \ 3 -2 1
At node
3(left subtree, path 10→5→3, prefix sums=18): look incntfor18-8=10, the root’s prefix sum is exactly 10 → count 1 path (path 5→3, sum 8).At leaf
3(10→5→3→3,s=21): look for21-8=13, which appeared as 13 (root 10 + left 5) → +1 (path 5→3→3, sum 8).At
11(prefix sum10-3+11=18): look for18-8=10→ +1 (path -3→11, sum 8). And so on. Total 3 valid paths.Key insight: Prefix-sum difference = targetSum; hash table stores ancestor prefix sums; backtrack to undo counting and prevent cross-subtree miscount.
Pitfall:
cntinitially holds{0:1}to cover the case “path starts from the current node itself”; forgetting to backtrack lets ancestors of different branches interfere with each other.
0543. Diameter of Binary Tree Easy
Core idea: The longest path through each node = left depth + right depth.
func depth(node *TreeNode) int { if node == nil { return 0 } l := depth(node.Left); r := depth(node.Right) ans = max(ans, l+r) // update diameter return max(l, r) + 1 // return depth}- Key insight: The diameter doesn’t necessarily pass through the root; compute for every node.
0872. Leaf-Similar Trees Easy
func find(cur *TreeNode) { if cur == nil { return } if cur.Left == nil && cur.Right == nil { leaf = append(leaf, cur.Val); return } find(cur.Left); find(cur.Right)}1161. Maximum Level Sum of a Binary Tree Medium
Problem: Given a binary tree, return the level number of the level whose sum of node values is the largest (root is level 0). If multiple levels tie for the largest sum, return the smallest level number.
Core idea: BFS level order; when popping a whole level, sum all node values in that level, record the max and its level.
func maxLevelSum(root *TreeNode) int { q := []*TreeNode{root} bestLevel, bestSum := 0, root.Val level := 0 for len(q) > 0 { size := len(q) sum := 0 for i := 0; i < size; i++ { // process current level all at once node := q[0]; q = q[1:] sum += node.Val if node.Left != nil { q = append(q, node.Left) } if node.Right != nil { q = append(q, node.Right) } } if sum > bestSum { bestLevel = level } // use > to guarantee smallest level on ties level++ } return bestLevel}Example tree:
1 (level 0, sum=1) / \ 7 0 (level 1, sum=7) / \ 7 -8 (level 2, sum=7-8=-1)Level0=1, level1=7, level2=-1 → max level sum is 7, at level 1, return 1.
- Key insight: BFS processes by level,
sizecontrols “take exactly one whole level at a time” then sum. - Pitfall: Compare with strict
>not>=, so ties return the smallest level; initializebestSumto level 0’s value not 0 (otherwise all-negative would be wrong).
1372. Longest ZigZag Path in a Binary Tree Medium
Core idea: DFS records direction and length; same direction resets to 1, alternating direction +1.
func dfs(node *TreeNode, toLeft bool, length int) { if node == nil { return } ans = max(ans, length) if toLeft { dfs(node.Right, false, length+1) // alternate dfs(node.Left, true, 1) // same direction resets } else { dfs(node.Left, true, length+1) dfs(node.Right, false, 1) }}1448. Count Good Nodes in Binary Tree Medium
Problem: A node is “good” if no value along the path from root to it is larger than it; count the total number of good nodes.
func dfs(node *TreeNode, curmax int) { if node == nil { return } if node.Val >= curmax { curmax = node.Val; ans++ } // equal value also good dfs(node.Left, curmax); dfs(node.Right, curmax)}VIII. Graphs and BFS/DFS
Universal graph playbook: Build adjacency list → DFS/BFS traversal → visited array to avoid repeats.
0200. Number of Islands Medium
Core idea: Traverse the grid; on encountering ‘1’, increment count +1, and DFS changes all connected land to ‘0’ (sinking islands).
func find(r, c int) { if r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == '0' { return } grid[r][c] = '0' // sink island find(r-1, c); find(r+1, c); find(r, c-1); find(r, c+1)}// main loopif grid[i][j] == '1' { ans++; find(i, j) }- Key insight: Sinking islands — modify the grid in place instead of a visited array.
- Pitfall:
grid[r][c] = '0'must come before the four-directional recursion.
Grid visualization (4×5 grid, 2 islands):
Execution flow:
- Traverse to (0,0)=‘1’ → ans=1, DFS sinks islands → (0,0)(0,1)(1,1)(1,2)(2,2) all changed to ‘0’.
- Traverse to (2,4)=‘1’ → ans=2, DFS sinks islands → (2,4)(3,4) all changed to ‘0’.
- Remaining all ‘0’ → result = 2.
0207. Course Schedule Medium
Problem: There are numCourses courses (numbered 0..n-1), and each entry in prerequisites [a, b] means “to take course a you must first finish course b”. Ask: can all courses be taken in order (i.e. no “prerequisite dependency cycle”)?
How to convert to a graph: Each course is a node, and a dependency is a directed edge. The key is the edge direction — [a, b] means “b is a prerequisite of a”, i.e. “b must come before a”, so the edge is b → a (from prerequisite to the subsequent course). Build the adjacency list:
graph := make([][]int, numCourses)for _, p := range prerequisites { a, b := p[0], p[1] // take a requires taking b first graph[b] = append(graph[b], a) // edge: b → a}Core idea: Topological sort to detect whether the directed graph has a cycle. Three-color marking: 0=unvisited, 1=visiting, 2=done.
func dfs(cur int) bool { // returns true if there's a cycle if status[cur] == 1 { return true } // encounter while visiting → cycle if status[cur] == 2 { return false } // done → no cycle status[cur] = 1 for _, next := range graph[cur] { if dfs(next) { return true } } status[cur] = 2 return false}- Key insight: Encountering a node that is “being visited” means there’s a cycle.
- Pitfall: Set status to 1 before recursion, set status to 2 after recursion.
Intuitive mapping of how to build edges:
| prerequisites entry | Meaning | Directed edge |
|---|---|---|
[1, 0] | take 1 requires taking 0 first | 0 → 1 |
[2, 1] | take 2 requires taking 1 first | 1 → 2 |
[3, 2] | take 3 requires taking 2 first | 2 → 3 |
[1, 3] | take 1 requires taking 3 first | 3 → 1 |
Execution flow (numCourses=4, prerequisites=[[1,0],[2,1],[3,2],[1,3]):
Graph structure: 0→1→2→3→1 (cycle 1→2→3→1 exists)
| Step | DFS node | status change | Neighbors | Result |
|---|---|---|---|---|
| 1 | 0 | 0→1 | [1] | recurse 1 |
| 2 | 1 | 0→1 | [2,3] | recurse 2 |
| 3 | 2 | 0→1 | [3] | recurse 3 |
| 4 | 3 | 0→1 | [1] | recurse 1 |
| 5 | 1 | status==1! | - | ⚠️ encounter visiting → cycle! |
graph TD
A["Unvisited (0)"] -->|" start DFS "| B["Visiting (1)"]
B -->|" encounter visiting node "| C["Cycle! return false"]
B -->|" all neighbors done "| D["Done (2)"]
D --> E["No cycle, return true"]
style C fill: #f99, color: #1a1a1a
style E fill: #9f9, color: #1a1a1a0547. Number of Provinces Medium
Core idea: DFS to count the number of connected components.
func dfs(from int) { vis[from] = true for to, conn := range isConnected[from] { if conn == 1 && !vis[to] { dfs(to) } }}for i := range vis { if !vis[i] { ans++; dfs(i) } }0841. Keys and Rooms Medium
Problem: There are n rooms, numbered 0..n-1. Initially only room 0 is open. Each room i holds a set of keys rooms[i] (a list of room numbers); getting a key opens the corresponding room. Ask: can you start from 0, open and enter all rooms?
Core idea: Treat “rooms” as nodes and “rooms a key can open” as outgoing edges — this is essentially a graph traversal starting from 0. If a DFS/BFS from 0 covers all nodes, then you can enter all rooms.
func canVisitAllRooms(rooms [][]int) bool { n := len(rooms) visited := make([]bool, n) var dfs func(int) dfs = func(u int) { visited[u] = true for _, v := range rooms[u] { // rooms[u] are the rooms the key in room u can open if !visited[v] { dfs(v) } } } dfs(0) for _, ok := range visited { // check whether every room was entered if !ok { return false } } return true}Example rooms = [[1],[2],[3],[]]:
- Room 0 has key
[1]→ open 1 - Room 1 has key
[2]→ open 2 - Room 2 has key
[3]→ open 3 - Room 3 empty
Visit order 0→1→2→3, all 4 rooms entered → return true.
If rooms = [[1],[2],[],[1]] (room 0 only reaches 1, 1 reaches 2, but 3 can’t be opened by anyone) → room 3 missed → return false.
- Key insight: Keys are adjacency edges; the problem is equivalent to “can you traverse the whole graph from 0”.
- Pitfall: Set
visitedwhen entering a node, to avoid re-pushing the same room; don’t miss the final “all visited?” check.
0994. Rotting Oranges Medium
Problem: Rotten oranges infect adjacent fresh oranges (up/down/left/right) every minute; find the minimum minutes until all are rotten, or -1 if impossible.
Core idea: Multi-source BFS, infect all in each round simultaneously. You must mark first then update uniformly, to avoid this round’s newly rotten oranges infecting again.
- Key insight: Multi-source BFS = all initially rotten oranges enqueued at once, expanding level by level.
- Pitfall: Use a marking array to record first, then update uniformly.
0649. Dota2 Senate Medium
Core idea: Two-queue simulation. The smaller index acts first; the winner adds index +n to enter the next round.
for len(radiant) > 0 && len(dire) > 0 { if radiant[0] < dire[0] { radiant = append(radiant, radiant[0]+n) // enter next round } else { dire = append(dire, dire[0]+n) } radiant = radiant[1:]; dire = dire[1:]}- Key insight: Compare indices to decide who acts first; the winner adds n and enqueues to represent entering the next round.
- ⚠️ Common failure: Simply counting R’s and D’s to take the majority winner, completely ignoring the order strategy of banning. E.g. in
"DDRRR"D=2<R=3 but D acts first and consecutively bans R, so D actually wins.
3310. Remove Methods Until Safe Medium
Problem: Method k has a bug and must be removed along with all methods it directly/indirectly calls. But it can only be removed if this set of methods is not called externally.
Core idea: ① DFS to mark all suspicious methods reachable from k. ② Check whether there exists a call edge “non-suspicious → suspicious”. ③ If no external call, remove; otherwise keep all.
func remainingMethods(n int, k int, invocations [][]int) []int { graph := make([][]int, n) // build adjacency list for _, edge := range invocations { graph[edge[0]] = append(graph[edge[0]], edge[1]) }
isFault := make([]bool, n) // suspicious mark var dfs func(int) dfs = func(cur int) { if isFault[cur] { return } isFault[cur] = true for _, next := range graph[cur] { dfs(next) } // mark all call chains } dfs(k) // mark from k
canRemove := true for _, edge := range invocations { // check external calls if !isFault[edge[0]] && isFault[edge[1]] { canRemove = false; break } // external → suspicious }
var res []int if !canRemove { // can't remove, return all for i := 0; i < n; i++ { res = append(res, i) } } else { // can remove, return non-suspicious for i := 0; i < n; i++ { if !isFault[i] { res = append(res, i) } } } return res}Execution example: n=5, k=0, invocations=[[1,2],[0,2],[0,1],[3,4]]
Call graph: 0→2, 0→1, 1→2, 3→4Suspicious set (reachable from 0): {0, 1, 2}Check external calls: 3→4 (3 non-suspicious, 4 non-suspicious) → no external→suspicious edgeCan remove! Return [3, 4]graph TD
A["DFS from k
mark suspicious set"] --> B["traverse all call edges"]
B --> C{"Exists edge non-suspicious → suspicious?"}
C -->|Yes| D["Cannot remove
return all methods"]
C -->|No| E["Can remove
return non-suspicious methods"]
style D fill: #ffcdd2, color: #1a1a1a
style E fill: #c8e6c9, color: #1a1a1a- Key insight: It’s not “can’t remove if there’s a cycle”, but “can’t remove if there’s an external node calling a suspicious node”.
- Pitfall: DFS marking and external-call checking are two independent steps, neither can be omitted.
IX. Dynamic Programming
DP problem solving in four steps: ① define state ② write the transition equation ③ initialize ④ determine traversal order.
0005. Longest Palindromic Substring Medium
Core idea: Center expansion. Each position expands outward in two cases (odd and even length).
for i := 0; i < n; i++ { // aba type (odd length) for l, r := i-1, i+1; l >= 0 && r < n && s[l] == s[r]; l, r = l-1, r+1 {} // abba type (even length) for l, r := i, i+1; l >= 0 && r < n && s[l] == s[r]; l, r = l-1, r+1 {}}- Key insight: The palindrome center can be 1 character (odd) or 2 characters (even).
0042. Trapping Rain Water Hard (DP approach)
Core idea: Rainwater at position i = min(left tallest wall, right tallest wall) - height[i]. First precompute the maximum-to-the-left and maximum-to-the-right arrays for each position, then sum per position.
n := len(height)// 1. preprocess: max height up to and including each position on the leftleftMax := make([]int, n)leftMax[0] = height[0]for i := 1; i < n; i++ { leftMax[i] = max(leftMax[i-1], height[i]) }// 2. preprocess: max height up to and including each position on the rightrightMax := make([]int, n)rightMax[n-1] = height[n-1]for i := n-2; i >= 0; i-- { rightMax[i] = max(rightMax[i+1], height[i]) }// 3. accumulate rainwater at each positionres := 0for i := 0; i < n; i++ { res += max(0, min(leftMax[i], rightMax[i]) - height[i]) }Execution example (height = [0,1,0,2,1,0,1,3,2,1,2,1]):
| i | height | leftMax | rightMax | min(l,r) | rain = min-height |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 3 | 0 | 0 |
| 1 | 1 | 1 | 3 | 1 | 0 |
| 2 | 0 | 1 | 3 | 1 | 1 |
| 3 | 2 | 2 | 3 | 2 | 0 |
| 4 | 1 | 2 | 3 | 2 | 1 |
| 5 | 0 | 2 | 3 | 2 | 2 |
| 6 | 1 | 2 | 3 | 2 | 1 |
| 7 | 3 | 3 | 3 | 3 | 0 |
| 8 | 2 | 3 | 2 | 2 | 0 |
| 9 | 1 | 3 | 2 | 2 | 1 |
| 10 | 2 | 3 | 2 | 2 | 0 |
| 11 | 1 | 3 | 1 | 1 | 0 |
Total rain = 1+1+2+1+1 = 6 (positions 8, 10, 11 have rightMax smaller than leftMax, showing “right side shorter”).
- Time O(n), space O(n); also two-pointer O(1) space (see Two Pointers chapter).
0045. Jump Game II Medium
Problem: Each element of the array is the maximum jump length; find the minimum number of jumps to reach the end.
Core idea: Greedy + BFS-level idea. Maintain the current jump boundary and the farthest reachable.
for i := 0; i < n; i++ { pos = max(pos, nums[i]+i) if i == end { end = pos; ans++; if end >= n-1 { break } }}- Key insight: Treat jumps as BFS level traversal; when reaching the boundary, take one jump.
- ⚠️ Common failure: When
n=1(only one element),i==endimmediately triggersans++returning 1, but the correct answer is 0 (already at the end, no jump needed). Should addif n == 1 { return 0 }special case.
0053. Maximum Subarray Medium
Problem: Find the contiguous subarray with the largest sum, return the maximum sum.
Core idea: dp[i] = max(nums[i], dp[i-1]+nums[i]); discard the prefix sum if it’s negative.
res := nums[0]for i := 1; i < n; i++ { if nums[i-1] > 0 { nums[i] += nums[i-1] } // in-place DP res = max(res, nums[i])}- Key insight: Max sum ending at i = max(itself, itself + max sum before it).
- Pitfall:
resinitializes tonums[0]not 0 (array may be all negative).
Execution flow (nums = [-2,1,-3,4,-1,2,1,-5,4]):
| i | nums[i] original | prefix sum > 0? | nums[i] updated | res |
|---|---|---|---|---|
| 0 | -2 | - | -2 | -2 |
| 1 | 1 | -2≤0, don’t add | 1 | 1 |
| 2 | -3 | 1>0, add | 1+(-3)=-2 | 1 |
| 3 | 4 | -2≤0, don’t add | 4 | 4 |
| 4 | -1 | 4>0, add | 4+(-1)=3 | 4 |
| 5 | 2 | 3>0, add | 3+2=5 | 5 |
| 6 | 1 | 5>0, add | 5+1=6 | 6 ✓max |
| 7 | -5 | 6>0, add | 6+(-5)=1 | 6 |
| 8 | 4 | 1>0, add | 1+4=5 | 6 |
Result = 6 (subarray [4,-1,2,1]). Extend while prefix sum is positive, restart from the beginning when negative.
0062. Unique Paths Medium
Problem: How many paths from top-left to bottom-right in an m×n grid, moving only right or down.
for i := 0; i < m; i++ { dp[i][0] = 1 }for j := 0; j < n; j++ { dp[0][j] = 1 }for i := 1; i < m; i++ { for j := 1; j < n; j++ { dp[i][j] = dp[i-1][j] + dp[i][j-1] }}- Key insight: The first row and first column have only one path each.
0064. Minimum Path Sum Medium
Problem: Minimum sum path from top-left to bottom-right in an m×n grid, moving only right or down.
// modify in placefor i := 1; i < m; i++ { grid[i][0] += grid[i-1][0] }for j := 1; j < n; j++ { grid[0][j] += grid[0][j-1] }for i := 1; i < m; i++ { for j := 1; j < n; j++ { grid[i][j] += min(grid[i-1][j], grid[i][j-1]) }}0070. Climbing Stairs Easy
Problem: Climb 1 or 2 steps each time; how many ways to reach the n-th step.
p, q := 1, 2 // f(1)=1, f(2)=2for i := 3; i <= n; i++ { p, q = q, p+q }return q- Key insight: Essentially Fibonacci; use rolling variables to save space.
0072. Edit Distance Medium
Problem: Minimum number of operations to turn word1 into word2 (insert/delete/replace one character).
Core idea: dp[i][j] = minimum operations to turn the first i of word1 into the first j of word2.
// init: dp[i][0]=i, dp[0][j]=jif word1[i-1] == word2[j-1] { dp[i][j] = dp[i-1][j-1] } // same, no operationelse { dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 } // delete/insert/replace- Key insight: The dp dimension is (m+1)×(n+1); the 0th row/column represents the empty string.
- Pitfall: Don’t use an m×n dp array, boundary handling will go wrong.
DP state table (word1=“horse”, word2=“ros”):
| ∅ | r | o | s | |
| ∅ | 0 | 1 | 2 | 3 |
| h | 1 | 1 (replace h→r) | 2 | 3 |
| o | 2 | 2 | 1 (o==o, inherit) | 2 |
| r | 3 | 2 (r==r, inherit) | 2 | 3 |
| s | 4 | 3 | 3 | 2 (s==s, inherit) |
| e | 5 | 4 | 4 | 3 (delete e) |
Green = characters same, inherit directly; yellow = replace operation; red = final answer. Result = 3 (horse → rorse → rose → ros).
graph TD
A["word1[i-1] == word2[j-1]"] -->|Yes| B["dp[i][j] = dp[i-1][j-1]"]
A -->|No| C["take min of three operations +1"]
C --> D["delete: dp[i-1][j] + 1"]
C --> E["insert: dp[i][j-1] + 1"]
C --> F["replace: dp[i-1][j-1] + 1"]- ⚠️ Common failure: Initializing the first row/column with a
markaccumulator that stops incrementing once a matching character is found, ignoring later delete/insert costs. You should directly setdp[i][0]=i,dp[0][j]=j.
0097. Interleaving String Medium
Problem: Determine whether s3 is formed by interleaving s1 and s2 (keeping each string’s internal relative order).
Core idea: dp[i][j] = whether the first i of s1 and first j of s2 can interleave to form the first i+j of s3.
if s1[i-1] == s3[i+j-1] { dp[i][j] = dp[i-1][j] }if s2[j-1] == s3[i+j-1] { dp[i][j] = dp[i][j] || dp[i][j-1] } // note ||- Key insight: The last step comes from s1 or s2, the two cases are an “or” relationship.
0118. Pascal’s Triangle Easy
ans[i][0], ans[i][i] = 1, 1for j := 1; j < i; j++ { ans[i][j] = ans[i-1][j] + ans[i-1][j-1] }0121. Best Time to Buy and Sell Stock Easy
Problem: Can buy and sell only once; find the maximum profit.
minPrice := math.MaxInt32; ans := 0for _, p := range prices { minPrice = min(minPrice, p) ans = max(ans, p - minPrice)}- Key insight: Maintain the prefix minimum, try selling every day.
0139. Word Break Medium
Problem: Determine whether string s can be concatenated from words in the dictionary wordDict.
dp := make([]bool, n+1); dp[0] = truefor i := 1; i <= n; i++ { for _, word := range wordDict { l := len(word) if l <= i && dp[i-l] && s[i-l:i] == word { dp[i] = true; break } }}- Key insight:
dp[0] = trueis the starting point of the recurrence.
0152. Maximum Product Subarray Medium
Core idea: Maintain both the maximum and minimum (negative × negative = positive).
mem[i] = max(mem[i-1]*nums[i], nums[i], mim[i-1]*nums[i]) // maxmim[i] = min(mem[i-1]*nums[i], nums[i], mim[i-1]*nums[i]) // minans = max(ans, mem[i])- Key insight: A negative turns the minimum into the maximum, so you must maintain both max and min.
- Pitfall:
ansinitializes tonums[0]. - ⚠️ Common failure:
ansinitialized to-10instead ofnums[0]; when the array length is 1 the loop doesn’t run and returns -10. Even for longer arrays, ifnums[0]is the max product it returns the wrong value.
0198. House Robber Medium
Problem: Adjacent houses can’t both be robbed; find the maximum amount that can be robbed.
prev2 := nums[0]; prev1 := max(nums[0], nums[1])for i := 2; i < n; i++ { current := max(prev2+nums[i], prev1) // rob or not prev2 = prev1; prev1 = current}- Key insight: Rob current house + earnings from i-2, or don’t rob and take i-1’s earnings.
- Pitfall:
prev1initializes tomax(nums[0], nums[1])notnums[1].
Execution flow (nums = [2,7,9,3,1]):
| i | nums[i] | rob(i): prev2+nums[i] | not rob(i): prev1 | current | Explanation |
|---|---|---|---|---|---|
| 0 | 2 | - | - | 2 | initial prev2 |
| 1 | 7 | - | - | 7 | prev1=max(2,7) |
| 2 | 9 | 2+9=11 | 7 | 11 | rob 2+9 > not rob 7 |
| 3 | 3 | 7+3=10 | 11 | 11 | not rob(11) > rob(10) |
| 4 | 1 | 11+1=12 | 11 | 12 | rob 11+1 > not rob 11 |
Result = 12 (rob houses 0,2,4: 2+9+1=12)
- ⚠️ Common failure:
prev1initialized tonums[1]instead ofmax(nums[0], nums[1]); whennums[0] > nums[1](e.g.[2,1,1,2]) it takes the smaller value and everything after goes wrong.
0279. Perfect Squares Medium
Problem: Given a positive integer n, what is the minimum number of perfect squares (1, 4, 9, 16, …) that sum to n. (e.g. n=12 → 4+4+4 uses 3; n=13 → 4+9 uses 2)
How to turn it into an unbounded knapsack: Treat each perfect square j*j as a “coin” with face value j*j; treat n as the “amount” to make. Since the same square can be used any number of times (e.g. 12 uses 4 three times), this is exactly an unbounded knapsack (coins can be taken infinitely). The goal is to “make amount n with the fewest coins”.
dp[i]= minimum number of perfect squares to make amounti- Transition: for each usable square
j*j ≤ i,dp[i] = min(dp[i], dp[i - j*j] + 1) - Why unbounded knapsack: Outer
igoes from small to large, inner loops over squares; when computingdp[i],dp[i - j*j]already includes “the square was used” cases, so the same square can be repeatedly added → equivalent to infinite coins. If the innerigoes in reverse, it becomes a 0-1 knapsack (each usable once).
dp := make([]int, n+1); dp[0] = 0for i := 1; i <= n; i++ { dp[i] = n // init to max value (worst case all 1s) for j := 1; j*j <= i; j++ { dp[i] = min(dp[i], dp[i-j*j]+1) }}Example n = 12:
dp[4] = 1(use one 4);dp[8] = 2(4+4);dp[12] = min(..., dp[12-4]+1=dp[8]+1=3)→ 3 (4+4+4).dp[12]=3.Key insight: Perfect squares = coins usable infinitely, goal is “fewest count” → unbounded knapsack for the minimum.
Pitfall:
dp[0]=0is the foundation (amount 0 needs 0 coins); initializedp[i]with a large enough value (e.g.n) not 0, otherwiseminalways takes 0.
0300. Longest Increasing Subsequence Medium
Problem: Find the length of the longest strictly increasing subsequence.
dp[i] = 1 // initfor j := 0; j < i; j++ { if nums[i] > nums[j] { dp[i] = max(dp[i], dp[j]+1) }}ans = max(dp...) // answer is the max of the dp array, not dp[n-1]- Key insight:
dp[i]is defined as “ending atnums[i]” LIS, not “first i”. - Pitfall: The answer is the max of the dp array; advanced: binary search + greedy achieves O(n log n).
Execution flow (nums = [10,9,2,5,3,7,101,18]):
| i | nums[i] | check all j<i | dp[i] | Explanation |
|---|---|---|---|---|
| 0 | 10 | none | 1 | initial |
| 1 | 9 | 9<10 skip | 1 | no predecessor smaller than 9 |
| 2 | 2 | 2<10, 2<9 skip | 1 | no predecessor smaller than 2 |
| 3 | 5 | 5>2→dp[2]+1=2 | 2 | [2,5] |
| 4 | 3 | 3>2→dp[2]+1=2 | 2 | [2,3] |
| 5 | 7 | 7>2→2, 7>5→3, 7>3→3 | 3 | [2,5,7] or [2,3,7] |
| 6 | 101 | 101>all→take dp[5]+1=4 | 4 | [2,5,7,101] |
| 7 | 18 | 18>2→2, 18>5→3, 18>3→3, 18>7→4 | 4 | [2,5,7,18] |
Result = 4 (LIS = [2,5,7,101] or [2,5,7,18])
0322. Coin Change Medium
Problem: Given coins of different denominations and a total amount, compute the minimum number of coins needed to make that amount. Each coin can be used infinitely. If impossible, return -1.
How to turn it into an unbounded knapsack: Treat coins as “items”, each with weight=denomination, value=1 (one coin counts as 1); amount is the “knapsack capacity”. Since each coin can be used any number of times, this is exactly an unbounded knapsack. The goal is “fill the knapsack of capacity amount with the minimum value (fewest coins)”.
dp[i]= minimum coins to make amounti- Transition:
dp[i] = min(dp[i], dp[i - coin] + 1), for eachcoin ≤ i - Why unbounded knapsack: Outer
igoes from small to large; when computingdp[i],dp[i-coin]already contains the state “used multiple copies of that coin”, so the same denomination can be taken repeatedly → infinite coins. If the innerigoes in reverse, it becomes a 0-1 knapsack (each coin once).
dp := make([]int, amount+1); dp[0] = 0for i := 1; i <= amount; i++ { dp[i] = amount + 1 // unreachable marker (more than the max possible coins) for _, coin := range coins { if coin <= i { dp[i] = min(dp[i], dp[i-coin]+1) } }}if dp[amount] > amount { return -1 } // still unreachable → can't makeExample coins = [1,2,5], amount = 11:
dp[5]=1(one 5);dp[10]=2(two 5s);dp[11] = min(..., dp[11-1]+1=dp[10]+1=3, dp[11-5]+1=dp[6]+1=...)→ min 3 (5+5+1 or 5+2+2+2? that’s 4, so take 5+5+1=3).dp[11]=3.Key insight: Unbounded knapsack, each coin usable infinitely, find fewest count →
dptakes min.Pitfall: Return -1 when unreachable (use
amount+1as sentinel); don’t initialize too large or+1overflows; useminfor “fewest”, usemaxfor “most”.
0416. Partition Equal Subset Sum Medium
Problem: Can an array nums be split into two subsets so that the sums of the elements in both subsets are equal.
How to turn it into a 0-1 knapsack:
- First check the total
sum: ifsumis odd, it can’t be split evenly → directlyfalse; otherwise targettarget = sum/2. - The problem becomes: can we pick some numbers from the array so their sum exactly equals
target. This is exactly the 0-1 knapsack — each number is “an item”, weight=value, value=value; knapsack capacity=target; ask if it can be exactly filled. Each number can be used only once (core of 0-1 knapsack). dp[j]means “can we make sumj”. For the current numbernum, either don’t pick (dp[j] unchanged) or pick (ifdp[j-num]is true thendp[j]becomes true):dp[j] = dp[j] || dp[j-num].
Why the inner loop must go in reverse (from target down to num):
- If you go forward, when computing
dp[j], thedp[j-num]used is already the value updated this round (already counted the currentnum), so the samenumgets repeatedly added → degrades into “unbounded knapsack” (each number usable infinitely), wrong. - Reverse traversal guarantees
dp[j-num]is still the previous round state (hasn’t considered the currentnum), so eachnumparticipates at most once this round → correct 0-1 knapsack.
target := sum / 2dp := make([]bool, target+1); dp[0] = true // sum 0 is always reachablefor _, num := range nums { for j := target; j >= num; j-- { // reverse! ensure each number used once dp[j] = dp[j] || dp[j-num] } if dp[target] { return true } // early hit prunes}Example nums = [1,5,11,5], sum=22, target=11:
Process 1:
dp[1]=trueProcess 5:
dp[5]=true, dp[6]=true(1+5)Process 11:
dp[11]=true→ found subset summing to 11 (single 11) → returntrue(the other half [1,5,5] also sums to 11)Key insight: Equal-sum partition = can we “exactly make sum/2” with a 0-1 knapsack.
Pitfall: Odd sum directly
false; inner loop must go in reverse to prevent picking the same element twice;dp[0]=trueis the initialization foundation.
0746. Min Cost Climbing Stairs Easy
dp := make([]int, n+1) // dp[0]=dp[1]=0for i := 2; i <= n; i++ { dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]) }- Key insight: dp length is n+1; the top floor is one position past the array.
1137. N-th Tribonacci Number Easy
t0, t1, t2 := 0, 1, 1for i := 3; i <= n; i++ { t0, t1, t2 = t1, t2, t0+t1+t2 }return t2- ⚠️ Common failure: When
n=0/1/2, the loop conditioni:=3; i<=nisn’t met, andansstays at its initial value 0 and is returned. ButT(1)=1, T(2)=1, so you need a special case or correct base-case initialization.
1143. Longest Common Subsequence Medium
// dp[i][j] = LCS of first i of text1 and first j of text2if text1[i-1] == text2[j-1] { dp[i][j] = dp[i-1][j-1] + 1 }else { dp[i][j] = max(dp[i-1][j], dp[i][j-1]) }- Key insight: Same character takes diagonal +1, different takes max of up/left.
- ⚠️ Common failure: On match, using
max(dp[i+1][j], dp[i][j+1], dp[i+1][j+1]) + 1instead ofdp[i+1][j+1] + 1, causing double counting — the same character counted twice.
X. Backtracking
Universal backtracking template: Make a choice → recurse → undo the choice.
func backtrack(path, choice list) { if end condition met { result.add(copy of path); return } for choice in choice list { make choice backtrack(path, choice list) undo choice }}0017. Letter Combinations of a Phone Number Medium
maps := map[string]string{"2":"abc","3":"def",...}fun = func(idx int) { if idx == len(digits) { ans = append(ans, tmp); return } for _, ch := range maps[string(digits[idx])] { tmp += string(ch); fun(idx+1); tmp = tmp[:len(tmp)-1] // backtrack }}0022. Generate Parentheses Medium
Core idea: Left count >= right count; when remaining counts are equal, can only add a left parenthesis.
fun = func(left, right int, tmp string) { if right == 0 { ans = append(ans, tmp); return } if left == right { fun(left-1, right, tmp+"(") } // can only add left else { if left > 0 { fun(left-1, right, tmp+"(") } // can add left fun(left, right-1, tmp+")") // can add right }}- Key insight: In a valid parenthesis sequence, any prefix has left count >= right count.
0039. Combination Sum Medium
Core idea: Pick/don’t-pick pattern. After picking, can keep picking the same one (repeatable); not picking moves to the next.
if rest-num >= 0 { tem = append(tem, num) if rest-num == 0 { ans = append(ans, append([]int{}, tem...)) } else { find(cand, rest-num) } // keep picking current tem = tem[:len(tem)-1] // backtrack}find(cand[1:], rest) // don't pick current- Key insight: After picking, recurse still passing
cand(notcand[1:]) to enable repeated selection. - Pitfall: Must deep-copy when saving the result.
Backtracking tree (candidates = [2,3,6,7], target = 7):
graph TD
R["rest=7"] --> A2["pick 2 rest=5"]
R --> A3["pick 3 rest=4"]
R --> A6["pick 6 rest=1"]
R --> A7["pick 7 rest=0 ✅ [7]"]
A2 --> B2["pick 2 rest=3"]
A2 --> B3["pick 3 rest=2"]
A2 --> B6["pick 6 rest=-1 ✂prune"]
B2 --> C2["pick 2 rest=1"]
B2 --> C3["pick 3 rest=0 ✅ [2,2,3]"]
C2 --> D2["pick 2 rest=-1 ✂prune"]
C2 --> D3["pick 3 rest=-2 ✂prune"]
A3 --> E3["pick 3 rest=1"]
E3 --> F3["pick 3 rest=-2 ✂prune"]
E3 --> F6["pick 6 rest=-5 ✂prune"]
style A7 fill: #c8e6c9, color: #1a1a1a
style C3 fill: #c8e6c9, color: #1a1a1aGreen = found a valid combination. Result = [[7], [2,2,3]].
0046. Permutations Medium
Problem: Given nums = [1,2,3] with no duplicates, return all permutations.
Core idea: Backtracking — pick an unused number into path, recurse to the bottom, then undo the choice.
for i, b := range onPath { if !b { path = append(path, nums[i]); onPath[i] = true t() // recurse onPath[i] = false; path = path[:len(path)-1] // backtrack }}- Key insight:
onPathboolean array marks selected elements, undone on backtrack. - Pitfall:
path = path[:len(path)-1]pops on backtrack; don’t omit it.
Backtracking decision tree (nums = [1,2,3]):
graph TD
R["start path=[]"] --> A1["pick 1 path=[1]"]
R --> A2["pick 2 path=[2]"]
R --> A3["pick 3 path=[3]"]
A1 --> B1["pick 2 path=[1,2]"]
A1 --> B2["pick 3 path=[1,3]"]
A2 --> B3["pick 1 path=[2,1]"]
A2 --> B4["pick 3 path=[2,3]"]
A3 --> B5["pick 1 path=[3,1]"]
A3 --> B6["pick 2 path=[3,2]"]
B1 --> C1["pick 3 path=[1,2,3] ✅"]
B2 --> C2["pick 2 path=[1,3,2] ✅"]
B3 --> C3["pick 3 path=[2,1,3] ✅"]
B4 --> C4["pick 1 path=[2,3,1] ✅"]
B5 --> C5["pick 2 path=[3,1,2] ✅"]
B6 --> C6["pick 1 path=[3,2,1] ✅"]
style C1 fill: #c8e6c9, color: #1a1a1a
style C2 fill: #c8e6c9, color: #1a1a1a
style C3 fill: #c8e6c9, color: #1a1a1a
style C4 fill: #c8e6c9, color: #1a1a1a
style C5 fill: #c8e6c9, color: #1a1a1a
style C6 fill: #c8e6c9, color: #1a1a1aEach level picks an unused number; reaching a leaf is one complete permutation. Total 3! = 6.
0051. N-Queens Hard
Core idea: Place row by row, use three arrays to mark columns and diagonals.
if !col[i] && !diag1[idx-i+n-1] && !diag2[idx+i] { col[i], diag1[idx-i+n-1], diag2[idx+i] = true, true, true find(idx + 1) // recurse to next row col[i], diag1[idx-i+n-1], diag2[idx+i] = false, false, false // backtrack}- Key insight: Use
row-colandrow+colto identify the two diagonals. - Pitfall: Main-diagonal index
+n-1offset avoids negative values.
Diagonal marking diagram (N=4, placed at row 0 column 1):
row col: 0 1 2 3 0: . Q . . ← place (0,1) 1: x . . x ← diag1 mark / diag2 mark 2: . x . . 3: . . x .| Mark type | Formula | Value at (0,1) | Marked positions | |---|------------|-------------|---------|----------------------| | Column col | col=1 | 1 | (0,1)(1,1)(2,1)(3,1) | | Main diagonal diag1 | row-col+n-1 | 0-1+3=2 | (0,1)(1,2)(2,3) | | Anti-diagonal diag2 | row+col | 0+1=1 | (0,1)(1,0) |
On the same main diagonal
row-colis identical; on the same anti-diagonalrow+colis identical.+n-1avoids negative indices.
0078. Subsets Medium
Problem: Return all subsets (power set) of an integer array with no duplicate elements.
Core idea: Each element pick/don’t-pick; save the result at the end.
dfs = func(cur int) { if cur == len(nums) { ans = append(ans, append([]int(nil), set...)); return } set = append(set, nums[cur]); dfs(cur+1) // pick set = set[:len(set)-1]; dfs(cur+1) // don't pick}0079. Word Search Medium
Problem: Search whether a word exists in an m×n character grid; adjacent cells connect up/down/left/right, and the same cell can’t be reused.
Core idea: DFS + in-place marking. Change the current cell to ’#’ to prevent reuse, restore after recursion.
temp := board[m][n]; board[m][n] = '#' // markres := dfs(m+1,n,idx+1) || dfs(m,n+1,idx+1) || dfs(m-1,n,idx+1) || dfs(m,n-1,idx+1)board[m][n] = temp // restore- Key insight: In-place marking instead of a visited array.
0131. Palindrome Partitioning Medium
Problem: Split a string into substrings so each is a palindrome; return all partition schemes.
Core idea: Enumerate split points; recurse only on palindrome substrings.
for i := idx+1; i <= n; i++ { if isPalindrome(s[idx:i]) { res = append(res, s[idx:i]) sub(i) // recurse res = res[:len(res)-1] // backtrack }}0216. Combination Sum III Medium
Problem: Find all combinations that sum to n and consist of exactly k distinct numbers, where numbers are chosen only from 1..9 and each used at most once. Return all valid combinations. E.g. k=3, n=7 → [[1,2,4]] (1+2+4=7).
Backtracking idea (standard “pick / don’t pick” two branches):
start: the number currently being considered (fromstartto 9, to avoid duplicate combinations and ensure increasing order).target: how much sum is still needed to reachn.path: chosen numbers;k - len(path)is “how many more to pick”.- Two recursive branches:
- Don’t pick
start:dfs(start+1, target)(look at larger numbers ahead). - Pick
start: addstarttopath,dfs(start+1, target-start)(subtract start from sum), after returning undo the choicepath = path[:len(path)-1].
- Don’t pick
- Pruning:
start > 9(numbers exhausted) ortarget < 0(sum exceeded) return directly; when the number still needed is 0, iftarget == 0it’s complete, record the answer.
dfs = func(start, target int) { if k-len(path) == 0 { if target == 0 { ans = append(ans, copy(path)) }; return } if start > 9 || target < 0 { return } // prune dfs(start+1, target) // don't pick start path = append(path, start); dfs(start+1, target-start); path = path[:len(path)-1] // pick start + backtrack}Example k=3, n=7 partial search tree (path | target):
start=1: don't pick → (1,7) pick 1 → path=[1] target=6 start=2: pick 2 → [1,2] target=4 start=3: pick 3 → [1,2,3] target=1 → still need 0 but target≠0, discard start=4: pick 4 → [1,2,4] target=0 → still need 0 and target=0 ✅ record- Key insight: Each number has “pick/don’t pick” two branches backtracking;
startincreases to avoid duplicate combinations. - Pitfall: Record the answer with
copy(path)notpath(otherwise later backtracking changes the stored result); don’t omit thetarget<0prune.
XI. Greedy
0031. Next Permutation Medium
Problem: Find the next lexicographic permutation of the array (the smallest permutation larger than current), modify in place; if already the largest, sort ascending.
Core idea: From back to front, find the first descending position cur; then from back to front find the first element larger than nums[cur] and swap; finally reverse the part after cur.
// 1. from back to front, find the first non-descending positioncur := n-1; for cur > 0 && nums[cur] <= nums[cur-1] { cur-- }; cur--// 2. find the smallest value larger than nums[cur] to swapif cur >= 0 { m := n-1; for nums[cur] >= nums[m] { m-- }; nums[cur], nums[m] = nums[m], nums[cur] }// 3. reverse the part after curreverse(nums[cur+1:])- Key insight: From back to front, find the first position that can be made larger.
- ⚠️ Common failure: After finding the position, directly swap
nums[cur]andnums[pre], but the correct approach is to find the smallest element larger thannums[pre]in the descending suffix afterpre, swap, then reverse the suffix. E.g.[1,3,2]expects[2,1,3], wrong code outputs[3,1,2].
0055. Jump Game Medium
Problem: Each element of the array is the maximum jump length from that position; determine whether you can reach the last position.
m := 0for i := 0; i <= m; i++ { // only traverse within reachable range m = max(m, i+nums[i]) if m >= n-1 { return true }}return false- Key insight: Loop condition
i <= m, only walk within reachable range.
0135. Candy Hard
Problem: Each child gets at least 1 candy; a child with a higher rating gets more candy than adjacent children; find the minimum total candies.
Core idea: Two passes — left to right ensures the right-higher gets more, right to left ensures the left-higher gets more, take the max.
// left to rightif ratings[i] > ratings[i-1] { left[i] = left[i-1] + 1 } else { left[i] = 1 }// right to leftif ratings[i] > ratings[i+1] { right[i] = right[i+1] + 1 } else { right[i] = 1 }// take maxans = sum(max(left[i], right[i]))- Key insight: One pass can’t satisfy both left and right constraints; split into two one-way passes then take max.
- Pitfall: In the right-to-left pass,
igoes from n-2 down to 0, don’t reverse the direction.
Execution flow (ratings = [1,0,2]):
| Index | ratings | Left→right left[] | Right→left right[] | max(left,right) |
|---|---|---|---|---|
| 0 | 1 | 1 (initial) | 2 (0<1→right[0]=right[1]+1=2) | 2 |
| 1 | 0 | 1 (0<1→left[1]=1) | 1 (initial) | 1 |
| 2 | 2 | 2 (2>0→left[2]=left[1]+1=2) | 1 (initial) | 2 |
Total candies = 2+1+2 = 5 (each child at least 1, the higher-rated neighbor gets more).
0334. Increasing Triplet Subsequence Medium
Core idea: Maintain the minimum and second-minimum; when something larger than the second-minimum appears, it’s found.
i, j := math.MaxInt, math.MaxIntfor _, v := range nums { if v < i { i = v } else if v > i && v < j { j = v } else if v > j { return true }}- Key insight: Only need to maintain two values; updating
idoesn’t affect correctness (there was already a largerjpredecessor before).
0605. Can Place Flowers Easy
Problem: Flower bed flowerbed uses 0 (empty) / 1 (planted); rule is no two flowers can be planted in adjacent plots. Ask whether n more flowers can be planted without violating the rule.
if flowerbed[i] == 0 { leftEmpty := i == 0 || flowerbed[i-1] == 0 // left boundary or left is empty rightEmpty := i == l-1 || flowerbed[i+1] == 0 // right boundary or right is empty if leftEmpty && rightEmpty { flowerbed[i] = 1; n-- } // plant if possible, mark in place}- Key insight: Plant whenever you can; greedy is never worse than planting later.
XII. Heaps and Priority Queues
0215. Kth Largest Element in an Array Medium
Core idea: Quickselect — quicksort’s partition recurses only on one side each time.
pivot := rand.Intn(n)arr[0], arr[pivot] = arr[pivot], arr[0]pivotVal := arr[0]; l, r := 1, n-1for l <= r { for l <= r && arr[l] >= pivotVal { l++ } for l <= r && arr[r] <= pivotVal { r-- } if l < r { arr[l], arr[r] = arr[r], arr[l] }}arr[0], arr[r] = arr[r], arr[0]if r == target { return arr[r] }if r > target { return find(arr[:r], target) }return find(arr[r+1:], target-r-1)- Key insight: Quickselect recurses only on one side each time, O(n) expected.
- Pitfall: When recursing into the right half, update the target offset; for a simple problem,
sort.Slicedirectly is faster.
0295. Find Median from Data Stream Hard
Core idea: Two heaps — left heap (max-heap) stores the smaller half, right heap (min-heap) stores the larger half.
// Go has no max-heap, simulate with negative valuesif left.Len() == right.Len() { heap.Push(&left, -right.pushPop(num)) // first into right heap to filter the min, put into left heap} else { heap.Push(&right, -left.pushPop(-num)) // first into left heap to filter the max, put into right heap}// median: left heap top (odd) or average of both tops (even)- Key insight: Median = max of the smaller half and min of the larger half.
- Pitfall: Go’s max-heap is simulated with negative values; pushPop optimizes to avoid two heap operations.
- ⚠️ Common failure: The balancing logic only handles
m > n(right heap too long), completely ignoringn > m+1(left heap longer by more than 1); when inserting smaller numbers consecutively, the left heap grows without bound, causing a wrong median.
graph LR
subgraph Two heaps maintain median
L["Max-heap (left)
stores smaller half
3,1,2"] --- R["Min-heap (right)
stores larger half
5,7,6"]
end
M["Median = (left top + right top) / 2"]
L --> M
R --> M0347. Top K Frequent Elements Medium
Core idea: Hash table counts frequencies + quickselect finds the top k.
mp := map[int]int{}for _, num := range nums { mp[num]++ }// convert to [number, frequency] array, quickselect finds top k- Key insight: Reduce to “k-th largest of the frequency array”, use quickselect to avoid full sorting.
XIII. Design Problems
0146. LRU Cache Medium
Problem: Implement an LRU cache with O(1) get and put, evicting the least recently used when over capacity.
(See Linked List chapter)
Core idea: Hash table + doubly linked list, virtual head/tail nodes simplify boundaries.
- Pitfall: After evicting the tail node, you must
deleteit from the map.
0155. Min Stack Medium
Problem: Implement a stack with push/pop/top/getMin all O(1).
(See Stack chapter)
Core idea: An auxiliary stack maintains the minimum synchronously.
0208. Implement Trie (Prefix Tree) Medium
Core idea: Each node has 26 child pointers and an isEnd marker.
type Trie struct { children [26]*Trie isEnd bool}// SearchPrefix shared: traverse the character path// Search additionally checks isEnd// StartsWith only checks the path exists- Key insight: The only difference between search and startsWith is whether isEnd is checked.
0295. Find Median from Data Stream Hard
(See Heap chapter)
0933. Number of Recent Calls Easy
Core idea: Queue; on each ping, pop expired requests from the front.
func Ping(t int) int { q = append(q, t) for q[0] < t-3000 { q = q[1:] } // pop expired return len(q)}XIV. Matrix
0048. Rotate Image Medium
Core idea: Two flips — horizontal flip top-bottom + transpose along the main diagonal.
// 1. flip top-bottomfor i := 0; i < n/2; i++ { matrix[i], matrix[n-1-i] = matrix[n-1-i], matrix[i] }// 2. transpose along main diagonalfor i := 0; i < n; i++ { for j := 0; j < i; j++ { matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] } }- Key insight: Rotate 90° = horizontal flip + diagonal transpose.
- Pitfall: The diagonal flip only iterates the lower triangle
j < i.
Rotation process visualization (3×3 matrix):
0054. Spiral Matrix Medium
Problem: Return all elements of an m×n matrix in clockwise spiral order.
Core idea: Four-boundary shrinking, traverse right→down→left→up.
for left <= right && up <= bottom { // right: left→right // down: up+1→bottom if left < right && up < bottom { // prevent single row/column duplicate // left: right-1→left // up: bottom-1→up+1 } left++; right--; up++; bottom--}- Key insight:
if left < right && up < bottomprevents re-traversing when only one row/column remains. - Pitfall: After walking all four edges each round, shrink boundaries (left++, right—, up++, bottom—).
Spiral traversal diagram (3×3 matrix):
graph LR
subgraph "Round 1"
A1["(0,0)=1 →right"] --> A2["(0,1)=2 →right"] --> A3["(0,2)=3 ↓turn"]
A3 --> A4["(1,2)=6 ↓"] --> A5["(2,2)=9 ←turn"]
A5 --> A6["(2,1)=8 ←"] --> A7["(2,0)=7 ↑turn"]
A7 --> A8["(1,0)=4 ↑"]
end
subgraph "Round 2 (single element)"
B1["(1,1)=5"]
end
style A3 fill: #ffcdd2, color: #1a1a1a
style A5 fill: #fff9c4, color: #1a1a1a
style A7 fill: #c8e6c9, color: #1a1a1a
style B1 fill: #e3f2fd, color: #1a1a1aOutput order: 1→2→3→6→9→8→7→4→5. Red = right-row end, yellow = down-column end, green = left-row end, blue = center.
Boundary shrink flow:
graph TD
S["left=0, right=n-1, up=0, bottom=m-1"] --> R["→ right: left to right"]
R --> D["↓ down: up+1 to bottom"]
D --> C{"left < right and up < bottom?"}
C -->|Yes| L["← left: right-1 to left"]
C -->|No| E["skip (only one row/column left)"]
L --> U["↑ up: bottom-1 to up+1"]
E --> F["shrink boundaries"]
U --> F
F --> N{"left ≤ right and up ≤ bottom?"}
N -->|Yes| R
N -->|No| END["end"]- ⚠️ Common failure: When only one row/column remains, the “right-to-left” and “bottom-to-up” still execute, causing elements to be added twice. E.g.
[[1,2,3]]gives[1,2,3,2]instead of[1,2,3]. You should addif up < bottomandif left < rightchecks.
0073. Set Matrix Zeroes Medium
Problem: For any element that is 0, set its entire row and column to 0, in O(1) extra space.
Core idea: Use the first row and first column as marking arrays, O(1) space.
// 1. record whether the first row/column has a 0// 2. use the first row/column to mark positions of internal 0s// 3. set internal zeroes according to marks// 4. finally handle the first row/column- Key insight: The matrix’s own first row and first column act as marking arrays.
- Pitfall: Record the original state of the first row/column before writing marks.
- ⚠️ Common failure: When finally zeroing, the row and column logic is reversed —
zerolinedetects whether the first row has a 0, but zeroing sets the first column; same forzerorow. The two for-loop assignment targets should be swapped.
XV. Strings
0151. Reverse Words in a String Medium
Core idea: Scan from right to left, split words on spaces.
t := nfor i := n-1; i >= 0; i-- { if s[i] == ' ' { if t-i > 1 { sb.WriteString(s[i+1:t]); sb.WriteByte(' ') } t = i }}if t > 0 { sb.WriteString(s[:t]) }- ⚠️ Common failure: Appending a trailing space after each word, and the first word may have a leading space, causing extra spaces in the result. E.g.
" hello world "returns"world hello "instead of"world hello".
0345. Reverse Vowels of a String Easy
Problem: Reverse all vowels in the string (aeiou, case-insensitive).
(See Two Pointers chapter)
0443. String Compression Medium
Core idea: Read/write two pointers; write digits in reverse order then reverse.
if read == n-1 || ch != chars[read+1] { chars[write] = ch; write++ if num > 1 { // write digits in reverse order by place, then reverse }}1071. Greatest Common Divisor of Strings Easy
Core idea: str1+str2 == str2+str1 is the necessary and sufficient condition for a common divisor to exist; the length is the GCD.
if str1+str2 != str2+str1 { return "" }return str1[:gcd(len(str1), len(str2))]1657. Determine if Two Strings Are Close Medium
Core idea: Two necessary and sufficient conditions — same character set + same frequency multiset.
if m != n { return false }// character sets must be identicalfor k := range mp1 { if _, ok := mp2[k]; !ok { return false } }// compare frequency multisets after sortingslices.Sort(cnt1); slices.Sort(cnt2)1768. Merge Strings Alternately Easy
Problem: Merge word1 and word2 alternately, appending the remainder to the end.
(See Two Pointers chapter)
Final words: The essence of algorithms is not memorizing code, but building the conditioned reflex of “feature → algorithm”. See a sorted array and think binary search; see a contiguous subarray and think sliding window; see “all solutions” and think backtracking. Do these problems several times, each time focusing on “why think this way” rather than “how to write the code”, and you’ll naturally find the approach quickly.