Hot100 Algorithm Notes - MuxiaoWFSkip to main content
This page was machine-translated and may contain errors or omissions. / 本页面为机器翻译,可能存在错漏。

Hot100 Algorithm Notes

LeetCode Hot100 algorithm notes

Sun Aug 09 2026
20806 words · 132 minutes

LeetCode Algorithm Notes

General Problem-Solving Strategies Overview

What features to look for → which algorithm to use

Problem FeatureGo-to AlgorithmTypical Problems
Sorted array + searchBinary Search33, 34, 35, 74, 153
Max/min of a contiguous subarraySliding Window / Prefix Sum3, 53, 209, 239
Merge two sorted structuresTwo Pointers11, 88, 167
O(n) lookup / deduplication / groupingHash Table1, 49, 128, 560
Linked list: find middle / cycle / k-th from endFast & Slow Pointers19, 141, 142, 876
Tree traversal / level orderDFS recursion / BFS queue94, 102, 104, 199
Parenthesis matching / expression evaluationStack20, 32, 394, 735
Next greater/smaller elementMonotonic Stack84, 739, 239
Find all solutions / combinations / permutationsBacktracking17, 22, 39, 46, 51, 78
Optimal solution + overlapping subproblemsDynamic Programming53, 62, 70, 72, 198, 300
Local optimum at each step → global optimumGreedy11, 45, 55, 121, 135
Top K / MedianHeap23, 215, 295, 347
2D grid search / connected componentsBFS / DFS200, 207, 547, 994
String prefix matchingTrie208
Interval merging / insertionSort + traversal56, 763

How to Recognize DP

KeywordDP TypeState Definition
Longest / shortest / fewest / mostOptimization DPdp[i] = optimal value of the first i elements
Whether possible / yes-or-noFeasibility DPdp[i] = whether the first i elements are feasible
Number of waysCounting DPdp[i] = number of ways for the first i elements
Knapsack / coin changeKnapsack DPdp[j] = optimal value at capacity j
Two sequencesTwo-sequence DPdp[i][j] = result of first i of s1 and first j of s2
Matrix pathMatrix DPdp[i][j] = optimal value to reach (i,j)

Universal Linked List Playbook

ScenarioTechnique
ReverseThree pointers (pre, cur, next)
Find middleFast & slow pointers (fast steps 2, slow steps 1)
Find k-th from endFast pointer moves N steps first, then move together
Detect cycleFast & slow pointers, meet means a cycle exists
Find cycle entryAfter meeting, one returns to head, move together to the next meet
IntersectionTwo pointers traverse each other’s list, meet at the intersection
Delete nodeDummy 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

Stepinums[i]Look up 9-nums[i]Hash tableOperation
102look up 7 → not found{}store {2:0}
217look up 2 → found (index 0){2:0}return [0,1]
  • ⚠️ Common failure: Writing to the map before looking up, causing val == target-val to match itself (e.g. nums=[3,2,4], target=6 returns [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 place
for 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 v maps to index v-1.
  • Pitfall: The swap condition must be nums[i] != nums[nums[i]-1] (to avoid infinite loops on duplicates), not nums[i] != i+1.

Execution example: nums = [3,4,-1,1], the goal is to place value v at index v-1.

Stepinums[i]OperationArray state
1033∈[1,4], swap nums[0]↔nums[2][-1,4,3,1]
20-1-1 not in [1,4], skip[-1,4,3,1]
3144∈[1,4], swap nums[1]↔nums[3][-1,1,3,4]
4111∈[1,4], swap nums[1]↔nums[0][1,-1,3,4]
51-1skip[1,-1,3,4]
6233 already at position 2, skip[1,-1,3,4]
7344 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: #1a1a1a

0049. 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]int to 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-1 is 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 checkednum-1 in set?Is start?Count forwardSequence length
10099 not in → yes✅ start100→(101 not in)1
43 in → no❌ skip--
200199 not in → yes✅ start200→(201 not in)1
10 not in → yes✅ start1→2→3→4→(5 not in)4 ✓max
32 in → no❌ skip--
21 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 := 0
for _, 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] = 1
for i := 1; i < n; i++ { answer[i] = answer[i-1] * nums[i-1] } // left cumulative
temp := 1
for 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, temp starts at 1; update answer before updating temp.

0283. Move Zeroes Easy

Problem: Move all 0s in the array to the end, keeping the relative order of non-zero elements.

l := 0
for 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 := 0
for _, 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):

StepnumpreLook up pre-k=pre-2count addedHash table m
111look up -1 → 00{0:1, 1:1}
212look up 0 → 11{0:1, 1:1, 2:1}
313look up 1 → 12{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 equal pre - k.
  • Pitfall: Initialize m[0]=1 to handle subarrays that start from the beginning (when pre is exactly k, pre-k=0 must 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 frequencies
mp2 := 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 := 0
for _, 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]

Stepnums[i]nums[i+1]Missing rangeans
112none[]
2243[3]
345none[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)-1
for 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 lnums[l]mrsumAction
0-415-4+(-1)+2=-3<0m++
0-425-4+(-1)+2=-3<0m++
0-435-4+0+2=-2<0m++
0-445-4+1+2=-1<0m++, m≥r exit
1-125-1+(-1)+2=0 ✅found [-1,-1,2], m++,r—
1-134-1+0+1=0 ✅found [-1,0,1], m++,r—
1-143m≥r exit-
2-1(dup)skipl=2, nums[2]==nums[1]
30450+1+2=3>0r—, r<m exit

Result = [[-1,-1,2], [-1,0,1]]

  • ⚠️ Common failure: In the sum == 0 branch, mistakenly executing l++ (outer loop variable) instead of m++ (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)-1
for 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 val on the left, find non-val on the right, swap them.
  • Why cur does not advance after swapping: The swap is nums[cur] = nums[lst], and the element coming from the right might also be val (e.g. nums=[3,2,2,3], val=3). If cur++ is done here, this swapped val stays at the front and is not removed. So in the swap branch we only do lst--, keeping cur in place to re-check the newly arrived element next round; only the “else branch” where nums[cur] != val does cur++.
  • ⚠️ Common failure: When lst <= 0 (array length 1) directly returning 0, but if nums[0] != val it should return 1; or wrongly doing cur++ after a swap, leaving the swapped val behind.

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)-1
maxLeft, maxRight := 0, 0
for 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]):

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):

StepleftrightmaxLmaxRh[l]<h[r]?Rain this stepTotal
101101yes0-0=00
211111no1-1=00
311012yes1-1=00
421012yes1-0=11
531022no2-2=01
63921no1-1=01
73822no2-2=01
83723yes2-2=01
94723yes2-1=12
105723yes2-0=24
116723yes2-1=15
127733met3-3=06
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: #1a1a1a

0088. 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 position
for 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%n
reverse(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 order
reverse(nums[k:]) // ③ reverse the last n-k back into order

Example (nums = [1,2,3,4,5,6,7], k = 3):

StepOperationArray 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)-1
b := []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, 0
for 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 letter
start, end := 0, 0
for 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 idx catches up to end, 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, 0
for 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 --> A

0003. 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, 0
for 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 of r-l+1 when 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 needed
for _, ch := range t { tmap[ch]++ }
count := len(tmap) // number of character types still to satisfy
smap := 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 count variable (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+1 can check whether it slid out of the window.

Execution flow (nums = [1,3,-1,-3,5,3,6,7], k = 3):

Stepinums[i]Deque (indices)Deque (values)Expiry checkOutput
101[0][1]i<2, no output-
213[1][3]i<2, no output-
32-1[1,2][3,-1]q[0]=1≥0, OKnums[1]=3
43-3[1,2,3][3,-1,-3]q[0]=1≥1, OKnums[1]=3
545[4][5]q[0]=4≥2, OKnums[4]=5
653[4,5][5,3]q[0]=4≥3, OKnums[4]=5
766[6][6]q[0]=6≥4, OKnums[6]=6
877[7][7]q[0]=7≥5, OKnums[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 p
for 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 diff
for 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 diff update logic — diff should only change when cnt crosses 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 := count
for 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)-k instead of i <= len(s)-k, missing the last window. E.g. s="aeiou", k=5 would 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, not right - left + 1.

General template: left + (right-left)/2 to avoid overflow, loop condition left <= 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 with k), 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, mid may itself be the answer, return on hit; when the loop exits left > 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 when left == right, at which point left (i.e. right) is the answer, no extra check needed. Commonly used for “find the first position satisfying a property” — e.g. the left boundary leftBound in 0034, the insertion position in 0035. Note that in this style mid takes left + (right-left)/2 (floor division), and updates usually right = mid (keep mid as a candidate), otherwise left = 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-1
for 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 target is in the sorted half.
  • Pitfall: Use >= in nums[mid] >= nums[left] because mid may equal left.

Execution flow (nums = [4,5,6,7,0,1,2], target = 0):

Roundleftrightmidnums[mid]JudgmentAction
106377≥4, left sortedtarget=0 not in [4,7), left=4
246511<4, right sortedtarget=0 not in (1,2], right=4
344400==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| I

0034. 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 >= target
leftBound := 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 - 1
rightBound := 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 uses nums[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, left points 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 whole
m, n := len(matrix), len(matrix[0])
l, r := 0, m*n-1
for 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 false

Why 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) and mid%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]mid is in the left (sorted) segment, so the minimum must be to the right of mid (inclusive of mid+1), so left = mid + 1, and treat nums[left] as a candidate minimum.
  • nums[mid] < nums[left]mid is in the right (unsorted) segment, so the minimum must be to the left of mid (possibly mid itself), so right = mid - 1, and treat nums[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):

leftrightmidnums[mid] vs nums[left]JudgmentAction
0425 >= 3left segment sortedans=min(3,3)=3, left=3
3431 >= 1left segment sortedans=min(3,1)=1, left=4
4442 >= 2left segment sortedans=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 with nums[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])-1
for 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] and target > matrix[r][0]; when target == 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 except 0 maps to a valid index, so the path can’t diverge forever and must loop.
  • The repeating number dupe is pointed to by at least two different indices (because two positions hold dupe), so the node dupe has two incoming edges → it’s the cycle entrance.

Cycle detection has two phases:

  1. 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.
  2. 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, 0
for { slow = nums[slow]; fast = nums[nums[fast]]; if slow == fast { break } }
head := 0
for slow != head { slow = nums[slow]; head = nums[head] }
return slow

Example 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: when k ≥ K, total(k) ≤ h (can finish); when k < 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 K instead 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 kTime per pile ⌈p/k⌉Total timeCan finish in 8h?
4⌈3/4⌉+⌈6/4⌉+⌈7/4⌉+⌈11/4⌉ = 1+2+2+38just enough
31+2+3+410no

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, not p/mid directly, 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.

StepabkmidAmidBa[midA]b[midB]Operation
1[1,3][2]200121 < 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):

  • dummy sentinel node: the “fake head” of the result list, so we don’t special-case assigning the “first node”; tail always points to the end of what’s been built, and new nodes attach to tail.Next. Finally return dummy.Next as the real head.
  • carry carries the carry: each digit addition may produce a carry (e.g. 7+8=15, keep 5 in the ones place, carry 1), which carry brings 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 := 0
for 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 digit
return dummy.Next

Digit-by-digit walkthrough l1 = 2→4→3 (342), l2 = 5→6→4 (465), correct sum 807:

Roundl1.Vall2.Valcarry inRaw sumCurrent digit (sum%10)New carry (sum/10)Node attached
12507707
246010010
33418808
4--0done---

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 whether carry is 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, head
for i := 0; i < n; i++ { right = right.Next }
for right != nil { left = left.Next; right = right.Next }
left.Next = left.Next.Next // delete
return 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 := dummy
for 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.Next

0023. 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: #1a1a1a

Number 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 := dummy
for 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.Next

0025. 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, dummy
for {
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; pre and tail are 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.Next is the copy node corresponding to random.

0141. Linked List Cycle Easy

Problem: Determine whether the linked list has a cycle.

fast, slow := head, head
for 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 speed
for 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: #1a1a1a

Execution flow:

PhasefastslowExplanation
Initial33both start from head
Step 12→02fast steps 2, slow steps 1
Step 2-4→2→00fast steps 2, slow steps 1
Step 3-4-4✅ meet at -4
Phase 2head=3slow=-4one back to head, same speed
Step 122✅ 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 delete it 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: #1a1a1a

Green = 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.Next
for fast != nil && fast.Next != nil { slow = slow.Next; fast = fast.Next.Next }
rightHead := slow.Next; slow.Next = nil // detach
return 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: fast must start at head.Next not head, 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, headB
for 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 + lenB steps.

0206. Reverse Linked List Easy

var pre *ListNode; cur := head
for 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 := even
for 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 list

2095. Delete the Middle Node of a Linked List Medium

slow, fast := head, head; var pre *ListNode
for fast != nil && fast.Next != nil {
fast = fast.Next.Next; pre = slow; slow = slow.Next
}
pre.Next = pre.Next.Next

2130. 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) == 0

0032. Longest Valid Parentheses Hard

Core idea: Stack stores indices; the bottom always holds the last unmatched position.

stack := []int{-1} // initial -1 as baseline
for 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 ans instead 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]=0 to simplify.

Histogram visualization (heights = [2,1,5,6,2,3]):

2
1
5
6
2
3
↑ The red bar (height 6) uses itself as height, extends left to height 5, width=2, area=5×2=10 (largest rectangle)

Execution flow (heights = [2,1,5,6,2,3], sentinel heights[6]=0):

StepiCurrent heightStack (indices)ActionArea computed
102[]push-
211[0]1<2, pop 02×(1-(-1)-1)=2
311[]push-
425[1]5>1, push-
536[1,2]6>5, push-
642[1,2,3]2<6, pop 36×(4-2-1)=6
742[1,2]2<5, pop 25×(4-1-1)=10 ✓max
842[1]2>1, push-
953[1,4]3>2, push-
1060(sentinel)[1,4,5]0<3, pop 53×(6-4-1)=3
1160[1,4]0<2, pop 42×(6-1-1)=8
1260[1]0<1, pop 11×(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]]”):

StepCharActioncurrStrcurrNumstrStacknumStack
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: #1a1a1a

2390. 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”

StepCharOperationStack state
1lpushl
2epushle
3epushlee
4tpushleet
5*pop tlee
6*pop ele
7cpushlec
8opushleco
9dpushlecod
10*pop dleco
11epushlecoe
  • 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<<31 and 1<<31-1 (int32 range), but node values range exactly over [-2^31, 2^31-1], causing boundary values to be misjudged. You should use math.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-pair Left/Right.

0102. Binary Tree Level Order Traversal Medium

Core idea: BFS with a queue, or DFS passing a level parameter.

// DFS version
func 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
\
6

How to do it: For the current node cur, if it has a left subtree:

  1. Find the rightmost node pre in the left subtree (it’s the last node of the left subtree’s preorder).
  2. Attach cur’s original right subtree to pre.Right (preserve the right side).
  3. Change cur.Right to cur.Left (move left subtree to the right), and set cur.Left to nil.
  4. Move cur one step right, repeat until all left subtrees are “moved” onto the right subtree chain.
cur := root
for 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 != nil not cur.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: #1a1a1a

Blue = 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 view
res = append(res, values[len(values)-1])

0226. Invert Binary Tree Easy

root.Left, root.Right = root.Right, root.Left
invertTree(root.Left)
invertTree(root.Right)
return root

0230. 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 LCA
if 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
NodeLeft returnsRight returnsJudgment
6nilnilreturn nil
2nilnilreturn nil
5nilnil5==p → return 5
0nilnilreturn nil
8nilnilreturn nil
1nilnil1==q → return 1
35(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 node X (prefix sum).
  • If a path goes from an ancestor A’s child all the way to X, 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 equals s(X)-targetSum, tracked in real time with a hash table cnt.

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 sum s=18): look in cnt for 18-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 for 21-8=13, which appeared as 13 (root 10 + left 5) → +1 (path 5→3→3, sum 8).

  • At 11 (prefix sum 10-3+11=18): look for 18-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: cnt initially 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, size controls “take exactly one whole level at a time” then sum.
  • Pitfall: Compare with strict > not >=, so ties return the smallest level; initialize bestSum to 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 loop
if 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):

1 1 0 0 0
0 1 1 0 0
0 0 1 0 1
0 0 0 0 1
1=land 0=water

Execution flow:

  1. Traverse to (0,0)=‘1’ → ans=1, DFS sinks islands → (0,0)(0,1)(1,1)(1,2)(2,2) all changed to ‘0’.
  2. Traverse to (2,4)=‘1’ → ans=2, DFS sinks islands → (2,4)(3,4) all changed to ‘0’.
  3. 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 entryMeaningDirected edge
[1, 0]take 1 requires taking 0 first0 → 1
[2, 1]take 2 requires taking 1 first1 → 2
[3, 2]take 3 requires taking 2 first2 → 3
[1, 3]take 1 requires taking 3 first3 → 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)

StepDFS nodestatus changeNeighborsResult
100→1[1]recurse 1
210→1[2,3]recurse 2
320→1[3]recurse 3
430→1[1]recurse 1
51status==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: #1a1a1a

0547. 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 visited when 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→4
Suspicious set (reachable from 0): {0, 1, 2}
Check external calls: 3→4 (3 non-suspicious, 4 non-suspicious) → no external→suspicious edge
Can 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 left
leftMax := 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 right
rightMax := 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 position
res := 0
for 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]):

iheightleftMaxrightMaxmin(l,r)rain = min-height
000300
111310
201311
322320
412321
502322
612321
733330
823220
913221
1023220
1113110

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==end immediately triggers ans++ returning 1, but the correct answer is 0 (already at the end, no jump needed). Should add if 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: res initializes to nums[0] not 0 (array may be all negative).

Execution flow (nums = [-2,1,-3,4,-1,2,1,-5,4]):

inums[i] originalprefix sum > 0?nums[i] updatedres
0-2--2-2
11-2≤0, don’t add11
2-31>0, add1+(-3)=-21
34-2≤0, don’t add44
4-14>0, add4+(-1)=34
523>0, add3+2=55
615>0, add5+1=66 ✓max
7-56>0, add6+(-5)=16
841>0, add1+4=56

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 place
for 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)=2
for 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]=j
if word1[i-1] == word2[j-1] { dp[i][j] = dp[i-1][j-1] } // same, no operation
else { 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”):

ros
0123
h11 (replace h→r)23
o221 (o==o, inherit)2
r32 (r==r, inherit)23
s4332 (s==s, inherit)
e5443 (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 mark accumulator that stops incrementing once a matching character is found, ignoring later delete/insert costs. You should directly set dp[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, 1
for 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 := 0
for _, 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] = true
for 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] = true is 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]) // max
mim[i] = min(mem[i-1]*nums[i], nums[i], mim[i-1]*nums[i]) // min
ans = max(ans, mem[i])
  • Key insight: A negative turns the minimum into the maximum, so you must maintain both max and min.
  • Pitfall: ans initializes to nums[0].
  • ⚠️ Common failure: ans initialized to -10 instead of nums[0]; when the array length is 1 the loop doesn’t run and returns -10. Even for longer arrays, if nums[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: prev1 initializes to max(nums[0], nums[1]) not nums[1].

Execution flow (nums = [2,7,9,3,1]):

inums[i]rob(i): prev2+nums[i]not rob(i): prev1currentExplanation
02--2initial prev2
17--7prev1=max(2,7)
292+9=11711rob 2+9 > not rob 7
337+3=101111not rob(11) > rob(10)
4111+1=121112rob 11+1 > not rob 11

Result = 12 (rob houses 0,2,4: 2+9+1=12)

  • ⚠️ Common failure: prev1 initialized to nums[1] instead of max(nums[0], nums[1]); when nums[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=124+4+4 uses 3; n=134+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 amount i
  • Transition: for each usable square j*j ≤ i, dp[i] = min(dp[i], dp[i - j*j] + 1)
  • Why unbounded knapsack: Outer i goes from small to large, inner loops over squares; when computing dp[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 inner i goes in reverse, it becomes a 0-1 knapsack (each usable once).
dp := make([]int, n+1); dp[0] = 0
for 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]=0 is the foundation (amount 0 needs 0 coins); initialize dp[i] with a large enough value (e.g. n) not 0, otherwise min always takes 0.


0300. Longest Increasing Subsequence Medium

Problem: Find the length of the longest strictly increasing subsequence.

dp[i] = 1 // init
for 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 at nums[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]):

inums[i]check all j<idp[i]Explanation
010none1initial
199<10 skip1no predecessor smaller than 9
222<10, 2<9 skip1no predecessor smaller than 2
355>2→dp[2]+1=22[2,5]
433>2→dp[2]+1=22[2,3]
577>2→2, 7>5→3, 7>3→33[2,5,7] or [2,3,7]
6101101>all→take dp[5]+1=44[2,5,7,101]
71818>2→2, 18>5→3, 18>3→3, 18>7→44[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 amount i
  • Transition: dp[i] = min(dp[i], dp[i - coin] + 1), for each coin ≤ i
  • Why unbounded knapsack: Outer i goes from small to large; when computing dp[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 inner i goes in reverse, it becomes a 0-1 knapsack (each coin once).
dp := make([]int, amount+1); dp[0] = 0
for 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 make

Example 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 → dp takes min.

  • Pitfall: Return -1 when unreachable (use amount+1 as sentinel); don’t initialize too large or +1 overflows; use min for “fewest”, use max for “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:

  1. First check the total sum: if sum is odd, it can’t be split evenly → directly false; otherwise target target = sum/2.
  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).
  3. dp[j] means “can we make sum j”. For the current number num, either don’t pick (dp[j] unchanged) or pick (if dp[j-num] is true then dp[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], the dp[j-num] used is already the value updated this round (already counted the current num), so the same num gets 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 current num), so each num participates at most once this round → correct 0-1 knapsack.
target := sum / 2
dp := make([]bool, target+1); dp[0] = true // sum 0 is always reachable
for _, 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]=true

  • Process 5: dp[5]=true, dp[6]=true (1+5)

  • Process 11: dp[11]=true → found subset summing to 11 (single 11) → return true (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]=true is the initialization foundation.


0746. Min Cost Climbing Stairs Easy

dp := make([]int, n+1) // dp[0]=dp[1]=0
for 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, 1
for i := 3; i <= n; i++ { t0, t1, t2 = t1, t2, t0+t1+t2 }
return t2
  • ⚠️ Common failure: When n=0/1/2, the loop condition i:=3; i<=n isn’t met, and ans stays at its initial value 0 and is returned. But T(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 text2
if 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]) + 1 instead of dp[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 (not cand[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: #1a1a1a

Green = 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: onPath boolean 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: #1a1a1a

Each 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-col and row+col to identify the two diagonals.
  • Pitfall: Main-diagonal index +n-1 offset 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-col is identical; on the same anti-diagonal row+col is identical. +n-1 avoids 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] = '#' // mark
res := 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 (from start to 9, to avoid duplicate combinations and ensure increasing order).
  • target: how much sum is still needed to reach n.
  • 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: add start to path, dfs(start+1, target-start) (subtract start from sum), after returning undo the choice path = path[:len(path)-1].
  • Pruning: start > 9 (numbers exhausted) or target < 0 (sum exceeded) return directly; when the number still needed is 0, if target == 0 it’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; start increases to avoid duplicate combinations.
  • Pitfall: Record the answer with copy(path) not path (otherwise later backtracking changes the stored result); don’t omit the target<0 prune.

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 position
cur := n-1; for cur > 0 && nums[cur] <= nums[cur-1] { cur-- }; cur--
// 2. find the smallest value larger than nums[cur] to swap
if cur >= 0 { m := n-1; for nums[cur] >= nums[m] { m-- }; nums[cur], nums[m] = nums[m], nums[cur] }
// 3. reverse the part after cur
reverse(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] and nums[pre], but the correct approach is to find the smallest element larger than nums[pre] in the descending suffix after pre, 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 := 0
for 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 right
if ratings[i] > ratings[i-1] { left[i] = left[i-1] + 1 } else { left[i] = 1 }
// right to left
if ratings[i] > ratings[i+1] { right[i] = right[i+1] + 1 } else { right[i] = 1 }
// take max
ans = 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, i goes from n-2 down to 0, don’t reverse the direction.

Execution flow (ratings = [1,0,2]):

IndexratingsLeft→right left[]Right→left right[]max(left,right)
011 (initial)2 (0<1→right[0]=right[1]+1=2)2
101 (0<1→left[1]=1)1 (initial)1
222 (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.MaxInt
for _, 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 i doesn’t affect correctness (there was already a larger j predecessor 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-1
for 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.Slice directly 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 values
if 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 ignoring n > 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 --> M

0347. 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 delete it 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-bottom
for i := 0; i < n/2; i++ { matrix[i], matrix[n-1-i] = matrix[n-1-i], matrix[i] }
// 2. transpose along main diagonal
for 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):

Original matrix
123
456
789
① flip top-bottom
789
456
123
② diagonal transpose
741
852
963
blue=original orange=after top-bottom flip green=after diagonal transpose (final result: rotated 90° clockwise)

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 < bottom prevents 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: #1a1a1a

Output 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 add if up < bottom and if left < right checks.

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 — zeroline detects whether the first row has a 0, but zeroing sets the first column; same for zerorow. 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 := n
for 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 identical
for k := range mp1 { if _, ok := mp2[k]; !ok { return false } }
// compare frequency multisets after sorting
slices.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.


Thanks for reading! Follow me if you'd like~

Hot100 Algorithm Notes

Sun Aug 09 2026
20806 words · 132 minutes
Cover
Sample track
Sample artist
Cover
Sample track
Sample artist
0:00 / 0:00