Neetcode 150 — Principal Engineer Solutions
Brute force + 2 optimal approaches per problem in both Java and Python, with trade-off tables and ship-it verdicts. Click any problem to expand. Use the sidebar search to jump.
Arrays & Hashing
#1 LC 217 Easy Contains Duplicate
Detect any repeated value in an array — the canonical hash-set warmup.
Problem Statement
Given an integer array nums, return true if any value appears at least twice, otherwise return false. This is the simplest membership-test problem and the gateway to every deduplication, sliding-window-uniqueness, and frequency-counting question that follows.
Examples
Constraints
- 1 <= nums.length <= 10^5
- -10^9 <= nums[i] <= 10^9
Approach Overview
Brute Force
Java: Nested loop pairwise comparison
Python: Nested loop pairwise comparison
Optimal #1
Java: HashSet membership probe
Python: set membership (one-liner or explicit loop)
Optimal #2
Java: Sort + adjacent comparison
Python: Counter / dict frequency (or sort)
Java Solutions
Nested loop pairwise comparison O(n²) O(1)
Compare every pair (i, j). The textbook first attempt — useful baseline, never to be shipped.
Pseudo-code
for i in 0..n-1:
for j in i+1..n-1:
if nums[i] == nums[j]:
return true
return false
Complexity
Edge cases & gotchas
- n == 1 → no pairs to compare, return false.
- All identical values → terminates on the very first inner-loop step (best case O(1)).
- n == 10^5 → ~5·10^9 ops, will TLE on LeetCode.
Java code
import java.util.*;
class Solution {
/**
* Brute-force pairwise scan. Quadratic — useful only as a correctness baseline.
* @param nums input array (non-null per problem constraints)
* @return true iff any value repeats
*/
public boolean containsDuplicate(final int[] nums) {
if (nums == null || nums.length < 2) return false;
final int n = nums.length;
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] == nums[j]) return true;
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.containsDuplicate(new int[]{1,2,3,1})); // true
System.out.println(s.containsDuplicate(new int[]{1,2,3,4})); // false
System.out.println(s.containsDuplicate(new int[]{})); // false (edge)
System.out.println(s.containsDuplicate(new int[]{7})); // false (edge)
}
}
HashSet membership probe O(n) O(n)
Stream the array; first time we re-see a value we know it's a dup. Single pass, amortized O(1) per probe.
Pseudo-code
seen = empty hash set
for x in nums:
if x in seen: return true
seen.add(x)
return false
Complexity
Edge cases & gotchas
- Adversarial hash collisions: Java's HashSet uses tree-bucket fallback at load factor — still O(log k) worst case per probe, not O(k).
- Autoboxing: every int becomes Integer; cache covers [-128,127] only, so heap pressure is real on large random arrays.
- If the problem allowed mutation we could presize: new HashSet<>(n*4/3 + 1) avoids rehash churn.
Java code
import java.util.*;
class Solution {
/**
* Single-pass HashSet probe — the textbook ship-it answer.
* Pre-sizing the set avoids rehash storms when n is known.
*/
public boolean containsDuplicate(final int[] nums) {
if (nums == null || nums.length < 2) return false;
// Pre-size: load factor 0.75 → capacity = ceil(n / 0.75)
final Set<Integer> seen = new HashSet<>((int) (nums.length / 0.75f) + 1);
for (final int x : nums) {
if (!seen.add(x)) return true; // .add returns false on duplicate
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.containsDuplicate(new int[]{1,2,3,1})); // true
System.out.println(s.containsDuplicate(new int[]{1,2,3,4})); // false
System.out.println(s.containsDuplicate(new int[]{1,1,1,3,3,4,3,2,4,2})); // true
}
}
Sort + adjacent comparison O(n log n) O(1) extra (Arrays.sort on int[] is in-place dual-pivot quicksort)
Sort, then any two equal values become adjacent. Trades hash-table space for O(n log n) time and the sort's cache-friendliness.
Pseudo-code
sort(nums)
for i in 1..n-1:
if nums[i] == nums[i-1]: return true
return false
Complexity
Edge cases & gotchas
- Mutates the input — confirm the contract allows it. If not, copy first (O(n) extra).
- Quicksort worst case is O(n²) on adversarial input, but Java's dual-pivot is robust in practice.
- Cache-friendly: contiguous int[] sweep beats HashSet's pointer-chasing for large n in real benchmarks.
Java code
import java.util.*;
class Solution {
/**
* Sort-then-scan. O(n log n) but cache-friendly and zero auxiliary heap.
* Mutates the input — caller must accept this or pass a copy.
*/
public boolean containsDuplicate(final int[] nums) {
if (nums == null || nums.length < 2) return false;
Arrays.sort(nums); // in-place dual-pivot quicksort on primitives
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i - 1]) return true;
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.containsDuplicate(new int[]{1,2,3,1})); // true
System.out.println(s.containsDuplicate(new int[]{1,2,3,4})); // false
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time (worst case) | O(n²) | O(n) amortized | O(n log n) |
| Space (auxiliary) | O(1) | O(n) | O(1) — sort is in-place on int[] |
| Implementation difficulty | 1/5 — two loops | 2/5 — single HashSet | 2/5 — Arrays.sort + sweep |
| Cache behavior | Excellent locality but quadratic work dominates | Poor — open-addressing pointer chasing | Excellent — sequential int[] scan |
| Adversarial input | Always quadratic | HashDoS possible (Java mitigates with tree-buckets) | Quicksort worst-case rare but exists |
| Mutates input? | No | No | Yes (sort) |
| When it wins | n ≤ ~50: simpler & fewer allocations | Default — early-exit on first duplicate, easy to reason about | Memory-constrained or n very large with cache-bound hardware |
| PE Verdict | Ship the HashSet (Optimal #1). It early-exits the moment a duplicate is found, the code reviews itself, and pre-sizing kills rehash overhead. Reach for sort-and-scan only if the caller forbids extra allocation or n is large enough that L1 cache misses dominate hashing wins. | ||
Python Solutions
Nested loop pairwise comparison O(n²) O(1)
Same idea as Java brute force. In Python the constant factor is brutal — interpreted loops of 10^5 elements is 10s of seconds.
Pseudo-code
for i in range(n):
for j in range(i+1, n):
if nums[i] == nums[j]:
return True
return False
Complexity
Edge cases & gotchas
- On large n this TLEs even harder than in Java because of Python loop overhead.
- Use enumerate, never raw indexing — but for brute force the win is negligible.
Python code
from typing import List
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
"""Brute force pairwise check — baseline only."""
n = len(nums)
for i in range(n - 1):
for j in range(i + 1, n):
if nums[i] == nums[j]:
return True
return False
if __name__ == "__main__":
s = Solution()
print(s.containsDuplicate([1, 2, 3, 1])) # True
print(s.containsDuplicate([1, 2, 3, 4])) # False
print(s.containsDuplicate([])) # False
print(s.containsDuplicate([7])) # False
set membership (one-liner or explicit loop) O(n) O(n)
len(set(nums)) != len(nums) is the idiomatic Python answer. Explicit loop wins on early-exit when duplicates appear early.
Pseudo-code
# idiomatic
return len(set(nums)) != len(nums)
# OR with early-exit:
seen = set()
for x in nums:
if x in seen: return True
seen.add(x)
return False
Complexity
Edge cases & gotchas
- len(set(nums)) ALWAYS scans the entire array — no early exit. Use the explicit loop when the input is huge and dups are likely near the start.
- Python ints are unbounded → no overflow concerns, but very large ints cost more to hash.
- set() is implemented in C — the one-liner often beats the explicit Python loop on dup-free inputs even though it can't short-circuit.
Python code
from typing import List
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
"""
Hash-set membership probe with early exit.
Prefer this over `len(set(nums)) != len(nums)` when duplicates may
appear early — the loop short-circuits, the one-liner does not.
"""
seen: set[int] = set()
for x in nums:
if x in seen:
return True
seen.add(x)
return False
if __name__ == "__main__":
s = Solution()
print(s.containsDuplicate([1, 2, 3, 1])) # True
print(s.containsDuplicate([1, 2, 3, 4])) # False
print(s.containsDuplicate([1, 1, 1, 3, 3, 4, 3, 2, 4, 2])) # True
Counter / dict frequency (or sort) O(n) O(n)
collections.Counter makes the intent explicit and gives you the count for free if the next question asks 'which one?'. Slower constant factor than a set.
Pseudo-code
counts = Counter(nums) return any(c >= 2 for c in counts.values()) # Equivalent / often faster: return counts.most_common(1)[0][1] >= 2
Complexity
Edge cases & gotchas
- Counter creation always sees every element — no early exit. For 'is there ANY dup' the set approach is strictly better; Counter shines when you also need 'how many' or 'which one'.
- Empty input → Counter is empty → most_common(1) returns [], guard with `if not nums: return False`.
Python code
from typing import List
from collections import Counter
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
"""
Frequency-table approach. Slightly slower than a plain set for this
specific yes/no question, but the natural choice when the follow-up
is 'which value duplicated' or 'how many duplicates'.
"""
if not nums:
return False
counts = Counter(nums)
# most_common(1) is O(n) but C-implemented — beats `any()` in practice.
return counts.most_common(1)[0][1] >= 2
if __name__ == "__main__":
s = Solution()
print(s.containsDuplicate([1, 2, 3, 1])) # True
print(s.containsDuplicate([1, 2, 3, 4])) # False
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time (worst case) | O(n²) | O(n) | O(n) |
| Space (auxiliary) | O(1) | O(n) | O(n) |
| Implementation difficulty | 1/5 | 1/5 (one-liner) or 2/5 (early-exit loop) | 2/5 |
| Constant factor | Worst — pure Python loop | Best — set ops are C-level | Slightly worse than set — Counter has dict-of-int overhead |
| Early exit? | Yes (in code) | Yes (loop form), No (one-liner form) | No — Counter scans all n |
| Extensibility | None | Pivots only to 'is there a dup' | Pivots cleanly to 'top-k frequent', 'mode', etc. |
| When it wins | Only for n < 100 in interview demo | Default — fastest yes/no answer | When the next problem needs counts anyway |
| PE Verdict | Ship `len(set(nums)) != len(nums)` for clarity, or the explicit loop variant if early-exit on long arrays matters. Counter is the right answer only when the surrounding code already needs frequencies. | ||
What the interviewer is really testing
On the surface this question tests whether you reach for a hash set instead of nested loops. The PE-level signal is whether you can articulate cost trade-offs without prompting: amortized vs worst-case behavior, cache effects of HashSet pointer-chasing vs sorted scan, autoboxing pressure in Java, and the GIL non-issue in Python. Bonus signal: can you propose Bloom filter / count-min sketch when n is gigantic and a small false-positive rate is acceptable?
Top gotchas
- len(set(nums)) does not short-circuit — it always scans the full array. Use the explicit loop when early termination matters.
- Java's HashSet rehashes when load factor (0.75) is exceeded; pre-size with `new HashSet<>((int)(n/0.75f)+1)` for hot paths.
- Autoboxing in Java: int[] → Integer wrappers cost heap allocations; the int Integer cache only covers [-128, 127].
- Sort-and-scan mutates the input — confirm with the caller, otherwise copy first (negating the O(1) extra-space win).
Ship-it
HashSet probe with early exit. In Java, pre-size and use `set.add()` returning false as the dup signal — no second `.contains()` call. In Python, the explicit loop with `seen.add()` if early-exit matters; otherwise the one-liner `len(set(nums)) != len(nums)`.
#2 LC 242 Easy Valid Anagram
Are two strings permutations of each other? Frequency-counting in disguise.
Problem Statement
Given two strings s and t, return true if t is an anagram of s. An anagram uses exactly the same multiset of characters in any order.
Examples
Constraints
- 1 <= s.length, t.length <= 5 * 10^4
- s and t consist of lowercase English letters (per the basic problem; Unicode follow-up below).
Approach Overview
Brute Force
Java: Sort both strings and compare
Python: sorted() comparison
Optimal #1
Java: Fixed 26-bucket frequency array
Python: collections.Counter equality
Optimal #2
Java: HashMap counter (Unicode-safe)
Python: Single-dict inc/dec with early exit
Java Solutions
Sort both strings and compare O(n log n) O(n) for the char[] copies (Arrays.sort on char[] is in-place but we still allocated).
Sort each string into canonical form, compare. Trades extra time for one-line clarity.
Pseudo-code
if len(s) != len(t): return false return sorted(s) == sorted(t)
Complexity
Edge cases & gotchas
- Different lengths short-circuit immediately — always check first.
- Sort allocates a char[] copy — do not call .toCharArray() twice unnecessarily.
Java code
import java.util.Arrays;
class Solution {
public boolean isAnagram(final String s, final String t) {
if (s == null || t == null || s.length() != t.length()) return false;
final char[] a = s.toCharArray();
final char[] b = t.toCharArray();
Arrays.sort(a);
Arrays.sort(b);
return Arrays.equals(a, b);
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.isAnagram("anagram", "nagaram")); // true
System.out.println(sol.isAnagram("rat", "car")); // false
System.out.println(sol.isAnagram("a", "ab")); // false
}
}
Fixed 26-bucket frequency array O(n) O(1) — fixed alphabet
Lowercase ASCII collapses to a 26-int array. One pass increments for s, decrements for t. Anagram iff all zero.
Pseudo-code
if len(s) != len(t): return false
counts = int[26]
for i in 0..n-1:
counts[s[i] - 'a'] += 1
counts[t[i] - 'a'] -= 1
return all c == 0
Complexity
Edge cases & gotchas
- Only correct for the lowercase-ASCII contract. Unicode breaks this — see Optimal #2.
- Cleaner than a HashMap because the alphabet is bounded and known.
- Cache-friendly: 26 ints fit in a single cache line.
Java code
class Solution {
public boolean isAnagram(final String s, final String t) {
if (s == null || t == null || s.length() != t.length()) return false;
final int[] counts = new int[26]; // lowercase ASCII alphabet
for (int i = 0, n = s.length(); i < n; i++) {
counts[s.charAt(i) - 'a']++;
counts[t.charAt(i) - 'a']--;
}
for (final int c : counts) if (c != 0) return false;
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.isAnagram("anagram", "nagaram")); // true
System.out.println(sol.isAnagram("rat", "car")); // false
}
}
HashMap counter (Unicode-safe) O(n) O(k) where k = distinct codepoints
Generalizes to any alphabet — Unicode, emoji, mixed case. The right answer when the constraint widens.
Pseudo-code
if len(s) != len(t): return false
map = HashMap<codepoint,int>
for c in s: map[c] += 1
for c in t:
map[c] -= 1
if map[c] < 0: return false
return true
Complexity
Edge cases & gotchas
- Use codePointAt + iterate by Character.charCount(cp) for surrogate pairs (emoji, 𝕊).
- Negative-count short-circuit lets us return early on any mismatch.
- Initial-capacity hint: new HashMap<>(min(n, 128)) trims rehash storms.
Java code
import java.util.HashMap;
import java.util.Map;
class Solution {
public boolean isAnagram(final String s, final String t) {
if (s == null || t == null || s.length() != t.length()) return false;
final Map<Character, Integer> counts = new HashMap<>(Math.min(s.length(), 128));
for (final char c : s.toCharArray()) counts.merge(c, 1, Integer::sum);
for (final char c : t.toCharArray()) {
final Integer v = counts.get(c);
if (v == null || v == 0) return false;
counts.put(c, v - 1);
}
return true;
}
/* Unicode-safe variant: iterate s.codePoints().forEach(...) and use Map<Integer,Integer>. */
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.isAnagram("anagram", "nagaram")); // true
System.out.println(sol.isAnagram("rat", "car")); // false
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n) |
| Space (auxiliary) | O(n) char[] | O(1) — 26 ints | O(k) — k distinct chars |
| Difficulty | 1/5 — sort-and-compare | 2/5 — index trick + zero-check | 2/5 — HashMap merge |
| Constant factor | ~3× slower than counter | Best — array indexing, cache-resident | ~3× slower than array — HashMap overhead |
| Alphabet support | Any (sort works on chars) | Only lowercase a-z (or any small known alphabet) | Any (Unicode if codepoint-based) |
| Early exit | No | No (must scan full counts at end) | Yes (negative count fires immediately) |
| When it wins | Whiteboard, no constraints stated | Stated lowercase ASCII (this problem) | Unicode, mixed case, follow-ups |
| PE Verdict | For the stated lowercase-ASCII contract, ship the 26-int counter — fastest, simplest, lockstep with the constraint. The instant the interviewer says 'what if the input is Unicode?' pivot to the HashMap (Optimal #2). Sort-and-compare is the answer when you have 30 seconds and need to be correct, not fast. | ||
Python Solutions
sorted() comparison O(n log n) O(n)
One-liner: sorted(s) == sorted(t). C-implemented Timsort makes this surprisingly competitive in pure Python.
Pseudo-code
if len(s) != len(t): return False return sorted(s) == sorted(t)
Complexity
Edge cases & gotchas
- sorted() on a str returns list[str], not str — comparison still works element-wise.
- For very long strings, Timsort's run-detection on already-sorted prefixes is a bonus.
Python code
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
"""Sort and compare — clearest one-liner."""
return len(s) == len(t) and sorted(s) == sorted(t)
if __name__ == "__main__":
sol = Solution()
print(sol.isAnagram("anagram", "nagaram")) # True
print(sol.isAnagram("rat", "car")) # False
collections.Counter equality O(n) O(k) where k = distinct chars
Counter(s) == Counter(t) is the idiomatic Python answer — readable, Unicode-safe, C-level fast.
Pseudo-code
if len(s) != len(t): return False return Counter(s) == Counter(t)
Complexity
Edge cases & gotchas
- Counter equality treats missing keys as 0 — Counter(s) == Counter(t) handles 'extra char' correctly.
- For short strings (n < 50) plain sorted() can actually be faster — Counter has constant overhead.
- Naturally handles Unicode and any hashable element type.
Python code
from collections import Counter
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
"""
Idiomatic Python: build two frequency tables, compare for equality.
Unicode-safe out of the box. Counter is implemented in C.
"""
if len(s) != len(t):
return False
return Counter(s) == Counter(t)
if __name__ == "__main__":
sol = Solution()
print(sol.isAnagram("anagram", "nagaram")) # True
print(sol.isAnagram("rat", "car")) # False
print(sol.isAnagram("résumé", "ésumér")) # True (Unicode)
Single-dict inc/dec with early exit O(n) O(k)
Walk both strings together, increment for s, decrement for t. Bail out on first negative. Beats Counter on adversarial inputs that diverge early.
Pseudo-code
if len(s) != len(t): return False
counts = defaultdict(int)
for cs, ct in zip(s, t):
counts[cs] += 1
counts[ct] -= 1
return all(v == 0 for v in counts.values())
Complexity
Edge cases & gotchas
- all() over .values() short-circuits on first non-zero.
- Does NOT short-circuit during the building phase — the inc/dec pattern must complete; if you want early exit, decrement first and bail when count goes below zero.
Python code
from typing import Dict
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
"""
One-pass inc/dec counter. Marginally faster than two Counter() builds
on long strings because it's a single iteration with early-exit
potential.
"""
if len(s) != len(t):
return False
counts: Dict[str, int] = {}
for cs, ct in zip(s, t):
counts[cs] = counts.get(cs, 0) + 1
counts[ct] = counts.get(ct, 0) - 1
# all() short-circuits — fast when most are zero by midpoint.
return all(v == 0 for v in counts.values())
if __name__ == "__main__":
sol = Solution()
print(sol.isAnagram("anagram", "nagaram")) # True
print(sol.isAnagram("rat", "car")) # False
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n) |
| Space | O(n) | O(k) | O(k) |
| Difficulty | 1/5 | 1/5 — one expression | 2/5 |
| Constant factor | Surprisingly OK — Timsort in C | Best — Counter is C-level | Slightly worse than Counter for happy path |
| Unicode | Works | Works | Works |
| Readability | Best — sorted(s) == sorted(t) | Excellent — Counter(s) == Counter(t) | OK — looks engineered |
| When it wins | Tiny n; demo code | Default — clearest correct answer | Streaming or memory-pressure scenarios where you want explicit control |
| PE Verdict | Ship Counter equality. It's the Python that Python wants. The single-dict inc/dec is a good answer when the interviewer asks 'show me how Counter would be implemented underneath'. sorted() is fine for reading aloud but you'd never put it in production for n in the millions. | ||
What the interviewer is really testing
The trap here is choosing the right structure for the stated alphabet. PE-level signal is recognizing that 26 lowercase letters → bounded array, 'any string' → HashMap/dict. Bonus signal: you mention what changes when the input becomes Unicode (surrogate pairs, normalization forms — NFC vs NFD).
Top gotchas
- Length-mismatch short-circuit must be first — saves doing any work for the easy 'no' case.
- Java char vs codepoint: charAt(i) returns a UTF-16 code unit, not a codepoint. For 'café' (combining accent) or emoji, switch to codePointAt + Character.charCount.
- Unicode normalization: 'café' (precomposed) and 'cafe\u0301' (decomposed) are visually identical but compare as different. Apply Normalizer.normalize() if the spec demands it.
- Counter equality in Python ignores zero values — Counter({'a':1,'b':0}) == Counter({'a':1}). Useful, occasionally surprising.
Ship-it
Java: 26-int counter when the constraint is lowercase ASCII; HashMap<Character,Integer> when it's not. Python: Counter(s) == Counter(t) — the most readable correct answer.
#3 LC 1 Easy Two Sum
Find the two indices that sum to target — the foundation problem of the hash-map era.
Problem Statement
Given an array of integers nums and a target sum target, return the indices of the two numbers that add up to target. Exactly one solution exists, and the same element cannot be used twice.
Examples
Constraints
- 2 <= nums.length <= 10^4
- -10^9 <= nums[i] <= 10^9
- -10^9 <= target <= 10^9
- Exactly one valid answer exists.
Approach Overview
Brute Force
Java: Nested-loop pair check
Python: Nested loops
Optimal #1
Java: Single-pass HashMap (value → index)
Python: Single-pass dict (value → index)
Optimal #2
Java: Sort + two pointers (returns values, not original indices)
Python: Sort + two pointers (with index recovery)
Java Solutions
Nested-loop pair check O(n²) O(1)
Try every (i, j). Quadratic but illustrates the search space and is sometimes the right answer when n ≤ 50.
Pseudo-code
for i in 0..n-1:
for j in i+1..n-1:
if nums[i] + nums[j] == target:
return [i, j]
Complexity
Edge cases & gotchas
- Watch for int overflow only if values approach Integer.MAX_VALUE / 2 — guard with long if so.
- Inner loop starts at i+1 to avoid using the same element twice.
Java code
class Solution {
public int[] twoSum(final int[] nums, final int target) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) return new int[]{i, j};
}
}
throw new IllegalArgumentException("No two-sum solution");
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(java.util.Arrays.toString(s.twoSum(new int[]{2,7,11,15}, 9))); // [0, 1]
System.out.println(java.util.Arrays.toString(s.twoSum(new int[]{3,2,4}, 6))); // [1, 2]
}
}
Single-pass HashMap (value → index) O(n) O(n)
While scanning, ask 'have I seen target - x already?' If yes, done. Single pass, O(n).
Pseudo-code
seen = HashMap<value, index>
for i, x in enumerate(nums):
need = target - x
if need in seen: return [seen[need], i]
seen[x] = i
Complexity
Edge cases & gotchas
- Duplicate values: by inserting AFTER the lookup we never match an element with itself.
- Negative numbers and zero work fine — hash map keys are unrestricted.
- Pre-size the map to avoid rehash thrash on large n.
Java code
import java.util.HashMap;
import java.util.Map;
class Solution {
/**
* Single-pass: for each x, look up target - x in the map.
* Insert AFTER the lookup so we never pair an element with itself.
*/
public int[] twoSum(final int[] nums, final int target) {
final Map<Integer, Integer> seen = new HashMap<>((int) (nums.length / 0.75f) + 1);
for (int i = 0; i < nums.length; i++) {
final int need = target - nums[i];
final Integer j = seen.get(need);
if (j != null) return new int[]{j, i};
seen.put(nums[i], i);
}
throw new IllegalArgumentException("No two-sum solution");
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(java.util.Arrays.toString(s.twoSum(new int[]{2,7,11,15}, 9))); // [0, 1]
System.out.println(java.util.Arrays.toString(s.twoSum(new int[]{3,3}, 6))); // [0, 1]
}
}
Sort + two pointers (returns values, not original indices) O(n log n) O(n) for the index-paired copy
Sort copy; converge l/r toward the target. O(n log n) time, O(1) extra heap (sort is in-place on int[]). Index-recovery requires a second pass.
Pseudo-code
pairs = [(nums[i], i) for i in 0..n-1]
sort pairs by value
l, r = 0, n-1
while l < r:
s = pairs[l].v + pairs[r].v
if s == target: return [pairs[l].i, pairs[r].i]
elif s < target: l += 1
else: r -= 1
Complexity
Edge cases & gotchas
- Pure two-pointer on a sorted array assumes the original order is irrelevant — it isn't here, hence the index pairing.
- If the API allowed returning values rather than indices, the implementation simplifies.
- Watch for overflow on the sum — guard with long if values approach Integer.MAX_VALUE.
Java code
import java.util.Arrays;
class Solution {
/**
* Sort + two pointers. We keep original indices alongside values to
* satisfy the index-return contract. O(n log n) time.
*/
public int[] twoSum(final int[] nums, final int target) {
final int n = nums.length;
final int[][] pairs = new int[n][2];
for (int i = 0; i < n; i++) { pairs[i][0] = nums[i]; pairs[i][1] = i; }
Arrays.sort(pairs, (a, b) -> Integer.compare(a[0], b[0]));
int l = 0, r = n - 1;
while (l < r) {
final long sum = (long) pairs[l][0] + pairs[r][0]; // long to avoid overflow
if (sum == target) return new int[]{pairs[l][1], pairs[r][1]};
if (sum < target) l++; else r--;
}
throw new IllegalArgumentException("No two-sum solution");
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(java.util.Arrays.toString(s.twoSum(new int[]{2,7,11,15}, 9))); // some valid pair
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n log n) |
| Space | O(1) | O(n) | O(n) for index-pair array |
| Difficulty | 1/5 | 2/5 | 3/5 — must keep indices |
| Cache behavior | Best — sequential array | Worse — pointer chasing | OK — sort is contiguous, but pair[][] hurts locality |
| Order-preserving? | Yes — returns first found pair | Yes — earliest right-index wins | No — order depends on sort |
| Adversarial input | Always quadratic | HashDoS theoretical risk | Quicksort worst-case |
| When it wins | n ≤ 50, no extra allocation allowed | Default — single pass, optimal | When extra space is forbidden AND the array can be mutated AND values, not indices, are returned |
| PE Verdict | Ship the single-pass HashMap. It's the only O(n) approach for this problem given the index-return contract, and the implementation reads itself. Two-pointer-after-sort is the right tool for follow-up problems (3Sum, k-sum) but is strictly worse here. | ||
Python Solutions
Nested loops O(n²) O(1)
Same idea — quadratic, fine for tiny n.
Pseudo-code
for i, x in enumerate(nums):
for j in range(i+1, len(nums)):
if x + nums[j] == target:
return [i, j]
Complexity
Edge cases & gotchas
- TLEs on n = 10^4 in pure Python.
Python code
from typing import List
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
for i in range(n - 1):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []
if __name__ == "__main__":
s = Solution()
print(s.twoSum([2, 7, 11, 15], 9)) # [0, 1]
Single-pass dict (value → index) O(n) O(n)
Idiomatic Python: scan once, dict-lookup the complement, insert after.
Pseudo-code
seen = {}
for i, x in enumerate(nums):
need = target - x
if need in seen: return [seen[need], i]
seen[x] = i
Complexity
Edge cases & gotchas
- Duplicates: insert AFTER lookup so an element doesn't pair with itself.
- Use enumerate, not range(len(nums)) — Pythonic, slightly faster.
Python code
from typing import List, Dict
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
"""Single-pass complement lookup."""
seen: Dict[int, int] = {}
for i, x in enumerate(nums):
need = target - x
if need in seen:
return [seen[need], i]
seen[x] = i
return [] # contract guarantees a solution; this is defensive
if __name__ == "__main__":
s = Solution()
print(s.twoSum([2, 7, 11, 15], 9)) # [0, 1]
print(s.twoSum([3, 2, 4], 6)) # [1, 2]
print(s.twoSum([3, 3], 6)) # [0, 1]
Sort + two pointers (with index recovery) O(n log n) O(n)
Sort an (value, index) tuple list, converge from both ends. Slower than dict but illustrates the technique used by k-sum follow-ups.
Pseudo-code
pairs = sorted((x, i) for i, x in enumerate(nums))
l, r = 0, n-1
while l < r:
s = pairs[l][0] + pairs[r][0]
if s == target: return sorted([pairs[l][1], pairs[r][1]])
elif s < target: l += 1
else: r -= 1
Complexity
Edge cases & gotchas
- Final return sorts the indices for stable comparison if the spec wants ascending order.
- Python ints are unbounded — no overflow worries.
Python code
from typing import List
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
"""
Sort + two pointers. Slower than the dict approach but the technique
generalizes cleanly to 3Sum / kSum — worth knowing.
"""
pairs = sorted((x, i) for i, x in enumerate(nums))
l, r = 0, len(pairs) - 1
while l < r:
s = pairs[l][0] + pairs[r][0]
if s == target:
return sorted([pairs[l][1], pairs[r][1]])
if s < target:
l += 1
else:
r -= 1
return []
if __name__ == "__main__":
s = Solution()
print(s.twoSum([2, 7, 11, 15], 9)) # [0, 1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n log n) |
| Space | O(1) | O(n) | O(n) |
| Difficulty | 1/5 | 1/5 | 3/5 |
| Constant factor | Worst — pure Python loops | Best — single dict pass | Sort is C-level fast but tuple overhead hurts |
| Generalization | None | Pivots to '3Sum with hash' (slower but clean) | Pivots to canonical 3Sum / kSum two-pointer technique |
| When it wins | Demo / very small n | Default | When you're warming up for 3Sum |
| PE Verdict | Ship the single-pass dict. The two-pointer variant is mandatory study material because of how cleanly it scales to 3Sum, but for this exact problem the dict is faster and shorter. | ||
What the interviewer is really testing
Two Sum is the canonical 'hash-map vs nested loop' question. PE-level signal: do you insert after lookup (handles duplicates correctly), do you call out the index-vs-value contract that breaks naive sort+two-pointer, and can you transition smoothly into the follow-up 'how does this generalize to 3Sum / kSum'?
Top gotchas
- Insert into the map AFTER the complement lookup — otherwise nums = [3,3], target = 6 returns [0,0] (same element used twice).
- Sort + two pointers loses original indices unless you carry them through the sort.
- Watch overflow when summing two ints near Integer.MAX_VALUE — cast to long.
- Pre-sizing the HashMap (Java) by n / 0.75 + 1 avoids rehash storms on hot paths.
Ship-it
Single-pass HashMap / dict in both languages. Insert after lookup. Pre-size in Java. Use enumerate in Python.
#4 LC 49 Medium Group Anagrams
Bucket strings by their canonical form — a hash-key design problem.
Problem Statement
Given an array of strings strs, group the anagrams together. Return a list of groups in any order. All strings in the same group are anagrams of one another.
Examples
Constraints
- 1 <= strs.length <= 10^4
- 0 <= strs[i].length <= 100
- strs[i] consists of lowercase English letters.
Approach Overview
Brute Force
Java: Pairwise anagram check
Python: Pairwise anagram check
Optimal #1
Java: HashMap keyed by sorted string
Python: defaultdict keyed by sorted tuple
Optimal #2
Java: HashMap keyed by 26-frequency signature
Python: defaultdict keyed by 26-frequency tuple
Java Solutions
Pairwise anagram check O(n² · k log k) where k = max string length O(n·k) for output
For each unbucketed string, scan all buckets to see if it matches. Quadratic and bad — establish the baseline.
Pseudo-code
groups = []
for s in strs:
placed = False
for g in groups:
if isAnagram(s, g[0]): g.add(s); placed = True; break
if not placed: groups.append([s])
return groups
Complexity
Edge cases & gotchas
- All distinct → worst case, no early-exit.
- Empty string is its own group.
Java code
import java.util.*;
class Solution {
public List<List<String>> groupAnagrams(final String[] strs) {
final List<List<String>> groups = new ArrayList<>();
for (final String s : strs) {
boolean placed = false;
for (final List<String> g : groups) {
if (isAnagram(s, g.get(0))) { g.add(s); placed = true; break; }
}
if (!placed) { final List<String> ng = new ArrayList<>(); ng.add(s); groups.add(ng); }
}
return groups;
}
private boolean isAnagram(final String a, final String b) {
if (a.length() != b.length()) return false;
final char[] ca = a.toCharArray(); final char[] cb = b.toCharArray();
Arrays.sort(ca); Arrays.sort(cb);
return Arrays.equals(ca, cb);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.groupAnagrams(new String[]{"eat","tea","tan","ate","nat","bat"}));
}
}
HashMap keyed by sorted string O(n · k log k) O(n · k)
Sorting normalizes each string to a canonical key. All anagrams share that key. One pass.
Pseudo-code
groups = HashMap<String, List<String>>
for s in strs:
key = sorted(s)
groups[key].append(s)
return groups.values()
Complexity
Edge cases & gotchas
- Sorted-string key uses O(k) bytes per entry.
- Empty string sorts to empty — distinct group, correct behavior.
- computeIfAbsent avoids the get-then-put dance.
Java code
import java.util.*;
class Solution {
/**
* Canonical form by sort. Cleanest reading; O(n·k log k) total work.
*/
public List<List<String>> groupAnagrams(final String[] strs) {
final Map<String, List<String>> groups = new HashMap<>();
for (final String s : strs) {
final char[] chars = s.toCharArray();
Arrays.sort(chars);
final String key = new String(chars);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.groupAnagrams(new String[]{"eat","tea","tan","ate","nat","bat"}));
}
}
HashMap keyed by 26-frequency signature O(n · k) O(n · k)
Skip the sort — use a 26-int frequency vector as the key. Stringified into 'a3b1c0...' for the map.
Pseudo-code
for s in strs:
counts = int[26]
for c in s: counts[c - 'a'] += 1
key = encode(counts) # 'a3b1c0...' or '#3#1#0#...'
groups[key].append(s)
Complexity
Edge cases & gotchas
- Avoid using Arrays.toString(counts) — it allocates a wasteful string. Build with StringBuilder + delimiters.
- For the lowercase-only contract, this strictly beats sort. For Unicode, fall back to sort or a TreeMap-of-codepoints.
Java code
import java.util.*;
class Solution {
/**
* Frequency-vector canonical form: O(n·k) total. Beats the sort-key
* approach when k log k > k (always for k ≥ 4 with cache-friendly arrays).
*/
public List<List<String>> groupAnagrams(final String[] strs) {
final Map<String, List<String>> groups = new HashMap<>();
for (final String s : strs) {
final int[] counts = new int[26];
for (int i = 0; i < s.length(); i++) counts[s.charAt(i) - 'a']++;
// Build a delimited signature; '#' avoids 'a11' colliding with 'a1+a1'.
final StringBuilder sb = new StringBuilder(60);
for (final int c : counts) { sb.append('#').append(c); }
groups.computeIfAbsent(sb.toString(), k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.groupAnagrams(new String[]{"eat","tea","tan","ate","nat","bat"}));
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·k log k) | O(n·k log k) | O(n·k) |
| Space (auxiliary) | O(n·k) | O(n·k) | O(n·k) |
| Difficulty | 2/5 | 2/5 — sort + map | 3/5 — must avoid signature collisions |
| Constant factor | Brutal | Sort overhead per string | Best for k ≥ 4 and small alphabet |
| Alphabet | Any | Any | Lowercase ASCII only (trivially extends to bounded alphabets) |
| Readability | OK | Best — one-liner key | Slightly noisier |
| When it wins | n ≤ 100, demo | Unknown alphabet, default | Hot path with stated lowercase contract |
| PE Verdict | Ship the sort-key (Optimal #1) by default — it's the most readable, alphabet-agnostic, and fast enough at LeetCode scale (n=10^4, k=100 → 10^6·log₂100 ≈ 7·10^6 ops). Switch to the frequency-vector key (Optimal #2) when the alphabet is small and you're profiling a hot path. | ||
Python Solutions
Pairwise anagram check O(n² · k log k) O(n · k)
For each string, scan existing groups. Same quadratic shape as Java baseline.
Pseudo-code
groups = []
for s in strs:
for g in groups:
if sorted(s) == sorted(g[0]): g.append(s); break
else:
groups.append([s])
Complexity
Edge cases & gotchas
- TLEs on n = 10^4.
Python code
from typing import List
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups: List[List[str]] = []
for s in strs:
key = sorted(s)
for g in groups:
if sorted(g[0]) == key:
g.append(s)
break
else:
groups.append([s])
return groups
if __name__ == "__main__":
print(Solution().groupAnagrams(["eat","tea","tan","ate","nat","bat"]))
defaultdict keyed by sorted tuple O(n · k log k) O(n · k)
tuple(sorted(s)) is hashable and cheap to compute. defaultdict(list) writes itself.
Pseudo-code
groups = defaultdict(list)
for s in strs:
groups[''.join(sorted(s))].append(s)
return list(groups.values())
Complexity
Edge cases & gotchas
- Use ''.join(sorted(s)) as the key — strings hash faster than tuples in CPython.
- Empty string maps to empty key — its own group, correct.
Python code
from typing import List, Dict
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
"""Sort each string into its canonical form; bucket by that key."""
groups: Dict[str, List[str]] = defaultdict(list)
for s in strs:
groups[''.join(sorted(s))].append(s)
return list(groups.values())
if __name__ == "__main__":
print(Solution().groupAnagrams(["eat","tea","tan","ate","nat","bat"]))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]
defaultdict keyed by 26-frequency tuple O(n · k) O(n · k)
Skip the sort: build a 26-int tuple, use as key. Avoids k log k entirely.
Pseudo-code
groups = defaultdict(list)
for s in strs:
counts = [0]*26
for c in s: counts[ord(c)-ord('a')] += 1
groups[tuple(counts)].append(s)
return list(groups.values())
Complexity
Edge cases & gotchas
- Tuple keys hash in O(26) — comparable to a 26-char string.
- Only valid for the lowercase-ASCII contract.
Python code
from typing import List, Dict, Tuple
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
"""
Frequency-vector canonical form. Skips the per-string sort, so it's
asymptotically better when k is large.
"""
groups: Dict[Tuple[int, ...], List[str]] = defaultdict(list)
for s in strs:
counts = [0] * 26
for c in s:
counts[ord(c) - 97] += 1
groups[tuple(counts)].append(s)
return list(groups.values())
if __name__ == "__main__":
print(Solution().groupAnagrams(["eat","tea","tan","ate","nat","bat"]))
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·k log k) | O(n·k log k) | O(n·k) |
| Space | O(n·k) | O(n·k) | O(n·k) |
| Difficulty | 2/5 | 1/5 | 2/5 |
| Constant factor | Worst | sorted() is C-fast — surprisingly competitive | Best for k ≥ 10 and known alphabet |
| Alphabet | Any hashable char | Any | Lowercase ASCII only |
| Readability | Verbose | Best — one-liner | Slightly more code |
| When it wins | Tiny demo | Default | Profiled hot path with stated contract |
| PE Verdict | Ship the sorted-key defaultdict in Python. It's three lines, alphabet-agnostic, and CPython's Timsort makes the constant factor tolerable. Reach for the frequency tuple only when the alphabet is bounded and you've measured. | ||
What the interviewer is really testing
This problem is hash-key design dressed as string manipulation. PE-level signal: do you propose multiple canonicalization strategies (sort-key vs frequency-vector) and discuss when each wins? Do you avoid the subtle collision in 'a11' vs 'a1' + 'a1' if you ever stringify counts without delimiters?
Top gotchas
- Naive concat of '+ count' creates collisions: counts [1,11,...] and [11,1,...] would both stringify to '111...'. Use a delimiter.
- Choosing tuple(...) vs ''.join(...) as the dict key: in CPython, strings hash faster than tuples for long keys but tuples are cheaper to construct from a list.
- Don't return groups.values() directly in Java without wrapping in ArrayList — the view's iteration order isn't guaranteed across versions.
- Frequency-vector approach assumes a bounded alphabet; for Unicode anagrams (rare interview ask) revert to sort-key.
Ship-it
Sort-key HashMap / defaultdict in both languages. It's optimal-enough at LeetCode scale and the cleanest code review. Switch to frequency-vector only when the alphabet is small AND you're chasing constant-factor wins.
#5 LC 347 Medium Top K Frequent Elements
Return the k most frequent values. The PE answer is bucket sort, not a heap.
Problem Statement
Given an integer array nums and an integer k, return the k most frequent elements. The answer is guaranteed unique. You may return them in any order.
Examples
Constraints
- 1 <= nums.length <= 10^5
- k is in [1, # unique elements]
- It is guaranteed that the answer is unique.
Approach Overview
Brute Force
Java: Count + sort by frequency
Python: Counter.most_common(k)
Optimal #1
Java: Min-heap of size k
Python: heapq.nlargest manual
Optimal #2
Java: Bucket sort by frequency
Python: Bucket sort by frequency
Java Solutions
Count + sort by frequency O(n + u log u) where u = # unique O(u)
Build frequency map, sort entries by count desc, take first k.
Pseudo-code
freq = HashMap<num, count> entries = list(freq.entrySet()) sort entries by count desc return first k keys
Complexity
Edge cases & gotchas
- u can equal n in the worst case (all distinct).
- Sort gives total order — overkill when we just need top-k.
Java code
import java.util.*;
class Solution {
public int[] topKFrequent(final int[] nums, final int k) {
final Map<Integer,Integer> freq = new HashMap<>();
for (final int x : nums) freq.merge(x, 1, Integer::sum);
final List<Map.Entry<Integer,Integer>> entries = new ArrayList<>(freq.entrySet());
entries.sort((a,b) -> b.getValue() - a.getValue());
final int[] out = new int[k];
for (int i = 0; i < k; i++) out[i] = entries.get(i).getKey();
return out;
}
public static void main(String[] args) {
System.out.println(Arrays.toString(new Solution().topKFrequent(new int[]{1,1,1,2,2,3}, 2)));
}
}
Min-heap of size k O(n + u log k) O(u + k)
Maintain a min-heap of size k keyed by count. Each insertion is O(log k); total O(n + u log k).
Pseudo-code
freq = count map heap = min-heap by count, capacity k for each entry: heap.offer; if size > k: heap.poll drain heap → result
Complexity
Edge cases & gotchas
- Min-heap kicks out the smallest count when size > k.
- PriorityQueue in Java is a min-heap by default — perfect.
- When k == u, heap reduces to identity.
Java code
import java.util.*;
class Solution {
public int[] topKFrequent(final int[] nums, final int k) {
final Map<Integer,Integer> freq = new HashMap<>();
for (final int x : nums) freq.merge(x, 1, Integer::sum);
// Min-heap so that the smallest count is at the top — that's who gets evicted.
final PriorityQueue<int[]> heap = new PriorityQueue<>((a,b) -> a[1] - b[1]);
for (final Map.Entry<Integer,Integer> e : freq.entrySet()) {
heap.offer(new int[]{e.getKey(), e.getValue()});
if (heap.size() > k) heap.poll();
}
final int[] out = new int[k];
for (int i = k - 1; i >= 0; i--) out[i] = heap.poll()[0];
return out;
}
public static void main(String[] args) {
System.out.println(Arrays.toString(new Solution().topKFrequent(new int[]{1,1,1,2,2,3}, 2)));
}
}
Bucket sort by frequency O(n) O(n)
Frequency is bounded by n. Buckets[count] = list of values. Walk buckets from high to low until k collected. True O(n).
Pseudo-code
freq = count map
buckets = array[n+1] of empty lists
for (val, cnt) in freq: buckets[cnt].append(val)
result = []
for i from n down to 1:
result.extend(buckets[i])
if len(result) >= k: return result[:k]
Complexity
Edge cases & gotchas
- Counts are bounded by n — bucket array is exactly n+1.
- Bucket index 0 is unused (no element has frequency 0 unless absent).
- When ties span the k-boundary, the problem guarantees uniqueness so no tiebreak logic needed.
Java code
import java.util.*;
class Solution {
/**
* Bucket sort by frequency: O(n) optimal. Frequencies are bounded by n,
* so we sidestep the heap's log factor entirely.
*/
public int[] topKFrequent(final int[] nums, final int k) {
final int n = nums.length;
final Map<Integer,Integer> freq = new HashMap<>();
for (final int x : nums) freq.merge(x, 1, Integer::sum);
final List<List<Integer>> buckets = new ArrayList<>(n + 1);
for (int i = 0; i <= n; i++) buckets.add(new ArrayList<>());
for (final Map.Entry<Integer,Integer> e : freq.entrySet()) {
buckets.get(e.getValue()).add(e.getKey());
}
final int[] out = new int[k];
int idx = 0;
for (int i = n; i >= 1 && idx < k; i--) {
for (final int v : buckets.get(i)) {
out[idx++] = v;
if (idx == k) break;
}
}
return out;
}
public static void main(String[] args) {
System.out.println(Arrays.toString(new Solution().topKFrequent(new int[]{1,1,1,2,2,3}, 2)));
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n + u log u) | O(n + u log k) | O(n) |
| Space | O(u) | O(u + k) | O(n) for buckets |
| Difficulty | 2/5 | 3/5 | 3/5 |
| k ≪ u case | Wasteful — sorts the whole map | Best constant factor with PriorityQueue | Same asymptote, slightly higher constants |
| k ≈ u case | Same as bucket | Heap collapses to log u | Best — single descending sweep |
| Streaming friendly? | No — needs full map | Yes — heap maintains top-k online | No — needs final counts |
| When it wins | Quick & dirty whiteboard | Streaming top-k or memory-tight | Bounded counts, batch processing — typical interview case |
| PE Verdict | Ship bucket sort. The frequency upper bound (≤ n) makes the heap's log factor unnecessary. Use the heap when the counts are unbounded (e.g., streaming with a sliding decay) or when the input doesn't fit in memory — in those cases the streaming property of a fixed-size heap is the entire point. | ||
Python Solutions
Counter.most_common(k) O(n + u log k) O(u)
One-liner. Counter.most_common is implemented with heapq.nlargest under the hood — O(n + u log k).
Pseudo-code
return [x for x, _ in Counter(nums).most_common(k)]
Complexity
Edge cases & gotchas
- Most pythonic — and the constant factor is tiny because the work is in C.
- If k is None, most_common returns ALL entries sorted — different cost.
Python code
from typing import List
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
"""Most-Pythonic shipping answer — Counter.most_common is heapq.nlargest in C."""
return [val for val, _ in Counter(nums).most_common(k)]
if __name__ == "__main__":
print(Solution().topKFrequent([1,1,1,2,2,3], 2)) # [1, 2]
heapq.nlargest manual O(n + u log k) O(u)
Same algorithm as most_common but explicit — useful when you want to swap the keying function.
Pseudo-code
counts = Counter(nums) return heapq.nlargest(k, counts.keys(), key=counts.get)
Complexity
Edge cases & gotchas
- key=counts.get avoids materializing tuples.
- Slightly slower than most_common because Python-level key callback.
Python code
from typing import List
from collections import Counter
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
"""Manual heap selection — useful template when you need a custom key."""
counts = Counter(nums)
return heapq.nlargest(k, counts.keys(), key=counts.get)
if __name__ == "__main__":
print(Solution().topKFrequent([1,1,1,2,2,3], 2))
Bucket sort by frequency O(n) O(n)
True O(n) with a bucket per possible count. Beats heap for k close to u.
Pseudo-code
counts = Counter(nums)
buckets = [[] for _ in range(len(nums)+1)]
for val, c in counts.items(): buckets[c].append(val)
result = []
for i in range(len(buckets)-1, 0, -1):
result.extend(buckets[i])
if len(result) >= k: return result[:k]
Complexity
Edge cases & gotchas
- Buckets index 0 unused.
- extend + slice avoids per-append branching.
Python code
from typing import List
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
"""
Bucket sort by frequency — O(n) optimal. Frequencies are bounded
by len(nums), so we don't need a heap's log factor.
"""
counts = Counter(nums)
buckets: List[List[int]] = [[] for _ in range(len(nums) + 1)]
for val, c in counts.items():
buckets[c].append(val)
result: List[int] = []
for i in range(len(buckets) - 1, 0, -1):
result.extend(buckets[i])
if len(result) >= k:
return result[:k]
return result
if __name__ == "__main__":
print(Solution().topKFrequent([1,1,1,2,2,3], 2)) # [1, 2]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n + u log k) | O(n + u log k) | O(n) |
| Space | O(u) | O(u) | O(n) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Constant factor | Best — pure C | Slightly worse (Python key callback) | Best when u ≈ n |
| Readability | Best | OK | OK — explicit but more code |
| When it wins | Default — clear and fast | Custom keying / scoring | Profiled hot path; counts bounded by n |
| PE Verdict | In Python, ship Counter.most_common(k) — it IS the heap solution, in C, with one line of code. Bucket sort is the asymptotically optimal answer to discuss with the interviewer; reach for it in production only when profiling demands it. | ||
What the interviewer is really testing
Top-k is a classic 'when does bucket sort beat a heap' moment. PE signal: do you recognize that frequencies are bounded by n, making the log factor avoidable? Do you call out the streaming case where heap-of-k is the right answer because you can't afford to count everything first?
Top gotchas
- Min-heap, NOT max-heap, when keeping size k — you evict the smallest each time.
- Bucket-sort writes to bucket[freq], NOT bucket[freq-1]; off-by-one will silently lose the highest-count items.
- Java PriorityQueue toArray ordering is heap-internal, not sorted — drain by polling.
- Counter.most_common with k=None returns ALL entries sorted in O(u log u), not the same as most_common(k).
Ship-it
Java: bucket sort — O(n), no heap overhead. Python: Counter.most_common(k) — readable and the work is already in C.
#6 LC 271 Medium Encode and Decode Strings
Round-trip a list of arbitrary strings through a single string. Length-prefix vs delimiter-escape design.
Problem Statement
Design encode and decode so that any list of strings (containing any 256 ASCII or even Unicode characters) can be serialized into a single string and recovered exactly. The encode and decode must be inverses.
Examples
Constraints
- 0 <= strs.length <= 200
- 0 <= strs[i].length <= 200
- strs[i] contains any valid ASCII (or Unicode) character — your code must not assume otherwise.
Approach Overview
Brute Force
Java: Reserved-delimiter join (BROKEN by adversarial input)
Python: Naive '#'.join (BROKEN)
Optimal #1
Java: Length-prefix framing (chosen format: 'len#payload')
Python: Length-prefix framing
Optimal #2
Java: Escape-character framing (delimiter + escape sequence)
Python: Escape-character framing
Java Solutions
Reserved-delimiter join (BROKEN by adversarial input) O(N) where N = total chars O(N)
Naively join with a 'special' separator like '#'. Fails the moment the input contains that separator. Useful only as a teaching foil.
Pseudo-code
encode: return '#'.join(strs)
decode: return s.split('#') # WRONG: '#3#' would split mid-payload
Complexity
Edge cases & gotchas
- ANY printable ASCII char can appear in payload — there is no truly 'safe' single-char delimiter.
- Demonstrates why this approach is unshippable — set up the optimal as a fix.
Java code
import java.util.*;
class Codec {
/** BROKEN encode — used only as a discussion baseline. Do not ship. */
public String encode(final List<String> strs) {
return String.join("#", strs); // collapses if any payload contains '#'
}
public List<String> decode(final String s) {
return Arrays.asList(s.split("#", -1)); // round-trips ONLY for delimiter-free inputs
}
public static void main(String[] args) {
Codec c = new Codec();
List<String> in = Arrays.asList("a","#","b");
System.out.println(c.decode(c.encode(in))); // [a, , , b] ← BROKEN
}
}
Length-prefix framing (chosen format: 'len#payload') O(N) O(N)
Each string is encoded as `<length>#<payload>`. Length tells the decoder exactly how many chars to read — payload can contain '#' freely.
Pseudo-code
encode:
sb = StringBuilder
for s in strs: sb.append(s.length()).append('#').append(s)
return sb.toString()
decode:
i = 0; out = []
while i < s.length():
j = s.indexOf('#', i)
len = parseInt(s[i:j])
out.add(s.substring(j+1, j+1+len))
i = j + 1 + len
Complexity
Edge cases & gotchas
- Empty list → empty string. encode([""]) → "0#" (single empty payload).
- indexOf for '#' is safe because we only look up to the FIRST '#' after position i — the length prefix is digits only.
- Negative or oversized length parses must be rejected if integrity matters.
Java code
import java.util.*;
class Codec {
/**
* Length-prefix framing: '<len>#<payload>' per string, concatenated.
* Robust against any payload character because we never split — we
* read exactly len chars after the '#'.
*/
public String encode(final List<String> strs) {
final StringBuilder sb = new StringBuilder();
for (final String s : strs) sb.append(s.length()).append('#').append(s);
return sb.toString();
}
public List<String> decode(final String s) {
final List<String> out = new ArrayList<>();
int i = 0;
final int n = s.length();
while (i < n) {
final int hash = s.indexOf('#', i);
final int len = Integer.parseInt(s, i, hash, 10); // JDK 9+
out.add(s.substring(hash + 1, hash + 1 + len));
i = hash + 1 + len;
}
return out;
}
public static void main(String[] args) {
Codec c = new Codec();
List<String> in = Arrays.asList("lint","code","love","you","","#3#");
System.out.println(c.decode(c.encode(in))); // round-trips perfectly
}
}
Escape-character framing (delimiter + escape sequence) O(N) O(N)
Use a delimiter and ESCAPE that delimiter inside payloads. Mirrors how CSV/JSON handle this. Slower than length-prefix but useful when the framing must be human-readable.
Pseudo-code
encode: for each s: replace '\\' with '\\\\', then '|' with '\\|', then append + '|' decode: walk one char at a time, handling '\\|' and '\\\\' as escapes; '|' (unescaped) ends a payload
Complexity
Edge cases & gotchas
- Two-character escape vocabulary: \\ and \| — anything else after a \\ is a malformed encoding.
- Empty list and empty strings round-trip cleanly: encode([""]) = "|" (a single delimiter).
- Pathological adversarial input ('|||' style) doubles in size due to escaping.
Java code
import java.util.*;
class Codec {
private static final char DELIM = '|';
private static final char ESC = '\\';
public String encode(final List<String> strs) {
final StringBuilder sb = new StringBuilder();
for (final String s : strs) {
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if (c == DELIM || c == ESC) sb.append(ESC);
sb.append(c);
}
sb.append(DELIM);
}
return sb.toString();
}
public List<String> decode(final String s) {
final List<String> out = new ArrayList<>();
final StringBuilder cur = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if (c == ESC && i + 1 < s.length()) {
cur.append(s.charAt(++i));
} else if (c == DELIM) {
out.add(cur.toString());
cur.setLength(0);
} else {
cur.append(c);
}
}
return out;
}
public static void main(String[] args) {
Codec c = new Codec();
System.out.println(c.decode(c.encode(Arrays.asList("a","|","\\","#3#",""))));
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time (encode) | O(N) | O(N) | O(N) but ~2× constant |
| Time (decode) | O(N) | O(N) — substring is O(len) | O(N) |
| Output size overhead | 0 (broken) | Θ(k · log L) | Up to 2N pathological, ~N typical |
| Robust to adversarial input | No — payload may contain delimiter | Yes — length is authoritative | Yes — explicit escapes |
| Streaming friendly | N/A — broken | Hard — must seek to '#' then jump | Yes — char-at-a-time decode |
| Human-readable | Yes (broken though) | Half — visible payload chunks | Yes |
| When to use | Never | Default — fastest and simplest robust framing | Streaming decode, audit logs, human inspection |
| PE Verdict | Ship length-prefix framing. It's the fastest robust option, decodes by jumping (no per-char branching), and matches how protobuf and HTTP/2 frame messages. Reach for escape-framing when the wire format must be human-readable or when streaming decode without backtracking matters. | ||
Python Solutions
Naive '#'.join (BROKEN) O(N) O(N)
Same broken approach as Java baseline — for discussion only.
Pseudo-code
encode: '#'.join(strs)
decode: s.split('#') # WRONG
Complexity
Edge cases & gotchas
- Cannot survive a payload containing '#'.
Python code
from typing import List
class Codec:
def encode(self, strs: List[str]) -> str:
return "#".join(strs)
def decode(self, s: str) -> List[str]:
return s.split("#")
if __name__ == "__main__":
c = Codec()
print(c.decode(c.encode(["a","#","b"]))) # ['a', '', '', 'b'] — BROKEN
Length-prefix framing O(N) O(N)
Same algorithm as Java optimal #1 — '<len>#<payload>' concatenation.
Pseudo-code
encode: ''.join(f'{len(s)}#{s}' for s in strs)
decode: walk index, find '#', slice [hash+1 : hash+1+len], advance
Complexity
Edge cases & gotchas
- Use s.find('#', i) — O(1) call into C.
- Slicing in Python is O(len) and copies — fine here, but watch on truly huge strings.
Python code
from typing import List
class Codec:
"""Length-prefix framing: '<len>#<payload>' for each string."""
def encode(self, strs: List[str]) -> str:
# f-strings + str.join is C-optimized in CPython.
return "".join(f"{len(s)}#{s}" for s in strs)
def decode(self, s: str) -> List[str]:
out: List[str] = []
i, n = 0, len(s)
while i < n:
j = s.find("#", i)
length = int(s[i:j])
out.append(s[j + 1 : j + 1 + length])
i = j + 1 + length
return out
if __name__ == "__main__":
c = Codec()
in_ = ["lint","code","love","you","","#3#"]
print(c.decode(c.encode(in_)) == in_) # True
Escape-character framing O(N) O(N)
Same algorithm as Java optimal #2 — delimiter + escape.
Pseudo-code
encode: for s in strs: append (s.replace('\\','\\\\').replace('|','\\|') + '|')
decode: walk char-by-char, treat '\\X' as literal X, '|' as terminator
Complexity
Edge cases & gotchas
- str.replace must escape backslash FIRST, then delimiter — order matters.
- Pure-Python char-walk is slow vs C-level operations; for large inputs the regex approach can outperform.
Python code
from typing import List
class Codec:
DELIM = "|"
ESC = "\\"
def encode(self, strs: List[str]) -> str:
parts = []
for s in strs:
# Order matters: escape backslashes BEFORE delimiter, otherwise the
# newly-introduced backslashes would themselves get escaped.
t = s.replace(self.ESC, self.ESC + self.ESC).replace(self.DELIM, self.ESC + self.DELIM)
parts.append(t + self.DELIM)
return "".join(parts)
def decode(self, s: str) -> List[str]:
out: List[str] = []
cur = []
i, n = 0, len(s)
while i < n:
c = s[i]
if c == self.ESC and i + 1 < n:
cur.append(s[i + 1])
i += 2
elif c == self.DELIM:
out.append("".join(cur))
cur.clear()
i += 1
else:
cur.append(c)
i += 1
return out
if __name__ == "__main__":
c = Codec()
in_ = ["a", "|", "\\", "#3#", ""]
print(c.decode(c.encode(in_)) == in_) # True
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(N) | O(N) | O(N) — pure-Python char loop hurts |
| Space | O(N) | O(N) plus log overhead | Up to 2N |
| Robust | No | Yes | Yes |
| Readability | Best (but broken) | Best of the robust options | Decent |
| Constant factor | Best — single C call | Best of robust — slicing in C | Worst — Python char loop |
| When to use | Never | Default | When the wire format must escape rather than length-prefix (e.g., debugging readability) |
| PE Verdict | Length-prefix is the right ship-it answer in Python too. The slicing operations stay in C and decode is essentially a series of int-parse + slice. Escape-framing's char-at-a-time decode in pure Python is materially slower for any non-trivial input. | ||
What the interviewer is really testing
This is a wire-protocol design question disguised as a string problem. PE signal: you immediately recognize that 'is there a safe delimiter' is the wrong question, propose length-prefix as the canonical answer, and compare against escape-framing with concrete trade-offs. Bonus signal: you mention this is exactly how HTTP/2, gRPC, protobuf, and ZooKeeper jute frame their messages.
Top gotchas
- Do NOT use a single-character delimiter without escapes — it's an interview honeypot, not a solution.
- When escaping, escape the escape character FIRST, then the delimiter. Reverse order corrupts.
- encode([""]) is the canonical edge case — a single empty string. Length-prefix yields '0#' (correct); naive join yields '' (collides with encode([])).
- Java: use Integer.parseInt(s, start, end, radix) (JDK 9+) to avoid creating a substring just to parse.
Ship-it
Length-prefix framing in both languages — '<len>#<payload>' concatenated. Decoder reads length, jumps exactly that many chars. Adversarial-input safe by construction.
#7 LC 238 Medium Product of Array Except Self
Compute prefix * suffix products without division — the canonical 'two passes' problem.
Problem Statement
Given an integer array nums, return an array answer such that answer[i] equals the product of all elements of nums except nums[i]. The algorithm must run in O(n) and (per the follow-up) without using division.
Examples
Constraints
- 2 <= nums.length <= 10^5
- -30 <= nums[i] <= 30
- The product of any prefix or suffix fits a 32-bit integer.
- Must run in O(n); cannot use the division operator.
Approach Overview
Brute Force
Java: Total product / nums[i] (BANNED by problem)
Python: Division (BANNED)
Optimal #1
Java: Two passes with prefix and suffix arrays
Python: Prefix and suffix arrays
Optimal #2
Java: O(1) extra space with output array reused
Python: O(1) extra space
Java Solutions
Total product / nums[i] (BANNED by problem) O(n) O(1)
Compute product of all, divide each out. Disallowed by spec AND breaks on zeros.
Pseudo-code
total = product of all for i: out[i] = total / nums[i] # ILLEGAL and zero-divides
Complexity
Edge cases & gotchas
- Single zero → all answers wrong unless special-cased.
- Multiple zeros → entire output is 0, except both zero positions are also 0 (special case).
- Division forbidden by problem statement.
Java code
class Solution {
/** SHOWS WHY DIVISION IS WRONG: zeros break it; problem also forbids /. */
public int[] productExceptSelfDivisionVariant(final int[] nums) {
int total = 1, zeros = 0, zeroIdx = -1;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0) { zeros++; zeroIdx = i; }
else total *= nums[i];
}
final int[] out = new int[nums.length];
if (zeros >= 2) return out; // all zero
if (zeros == 1) { out[zeroIdx] = total; return out; }
for (int i = 0; i < nums.length; i++) out[i] = total / nums[i];
return out;
}
public static void main(String[] args) {
// For instructional comparison only — do not ship.
}
}
Two passes with prefix and suffix arrays O(n) O(n)
Build left[i] = product of nums[0..i-1], right[i] = product of nums[i+1..n-1]. Answer = left * right. Two clean passes.
Pseudo-code
left[0] = 1 for i in 1..n-1: left[i] = left[i-1] * nums[i-1] right[n-1] = 1 for i in n-2..0: right[i] = right[i+1] * nums[i+1] for i: out[i] = left[i] * right[i]
Complexity
Edge cases & gotchas
- Boundary indices: left[0] = 1, right[n-1] = 1 — represent the 'empty product' identity.
- Long is unnecessary per the problem's guarantee that prefix/suffix products fit int. Don't over-engineer.
Java code
class Solution {
/** Prefix and suffix product arrays — clearest possible reading. */
public int[] productExceptSelf(final int[] nums) {
final int n = nums.length;
final int[] left = new int[n];
final int[] right = new int[n];
left[0] = 1;
for (int i = 1; i < n; i++) left[i] = left[i-1] * nums[i-1];
right[n-1] = 1;
for (int i = n-2; i >= 0; i--) right[i] = right[i+1] * nums[i+1];
final int[] out = new int[n];
for (int i = 0; i < n; i++) out[i] = left[i] * right[i];
return out;
}
public static void main(String[] args) {
System.out.println(java.util.Arrays.toString(new Solution().productExceptSelf(new int[]{1,2,3,4})));
}
}
O(1) extra space with output array reused O(n) O(1) extra (output not counted)
Stream the prefix into the output array, then a backward sweep multiplies in the running suffix. The shipping answer.
Pseudo-code
out[0] = 1
for i in 1..n-1: out[i] = out[i-1] * nums[i-1] # out holds left products
right = 1
for i in n-1..0:
out[i] *= right
right *= nums[i]
Complexity
Edge cases & gotchas
- out[0] starts at 1 — empty-product identity.
- right is updated AFTER multiplying into out[i] so it represents 'product strictly to the right of i'.
Java code
class Solution {
/**
* O(1) extra space (output excluded). Two passes:
* pass 1: out[i] = product of nums[0..i-1] (prefix)
* pass 2: walk right-to-left; multiply in running 'right' (suffix)
*/
public int[] productExceptSelf(final int[] nums) {
final int n = nums.length;
final int[] out = new int[n];
out[0] = 1;
for (int i = 1; i < n; i++) out[i] = out[i-1] * nums[i-1];
int right = 1;
for (int i = n - 1; i >= 0; i--) {
out[i] *= right;
right *= nums[i];
}
return out;
}
public static void main(String[] args) {
System.out.println(java.util.Arrays.toString(new Solution().productExceptSelf(new int[]{1,2,3,4})));
System.out.println(java.util.Arrays.toString(new Solution().productExceptSelf(new int[]{-1,1,0,-3,3})));
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space (auxiliary) | O(1) but ILLEGAL | O(n) | O(1) |
| Difficulty | 1/5 — but disqualified | 2/5 | 3/5 — pointer-style sweep |
| Handles zeros | Requires explicit guards | Yes — works uniformly | Yes — works uniformly |
| Cache behavior | Best (but moot) | OK — three sequential passes | Best — two passes, output array reused |
| Readability | Worst — special cases everywhere | Best — separate roles for left/right | Very good once you've seen it once |
| When to use | Never (banned) | Whiteboard / explanation | Production / interview ship-it |
| PE Verdict | Ship the O(1)-extra-space version. It's the same number of operations as the prefix/suffix-array variant minus two array allocations, and the trick (output holds prefix; backward sweep multiplies in suffix) is the entire learning of this problem. The two-array form is fine for first explanation; never for final code. | ||
Python Solutions
Division (BANNED) O(n) O(1)
Same shape — for instructional comparison only.
Pseudo-code
total = math.prod(nums); out[i] = total // nums[i] # forbidden
Complexity
Edge cases & gotchas
- Zero handling needs special cases.
- Division banned by problem statement.
Python code
from typing import List
import math
class Solution:
"""Division-based reference; do not submit."""
def productExceptSelfDivVariant(self, nums: List[int]) -> List[int]:
zeros = nums.count(0)
if zeros >= 2:
return [0] * len(nums)
non_zero_prod = math.prod(x for x in nums if x != 0)
if zeros == 1:
return [non_zero_prod if x == 0 else 0 for x in nums]
return [non_zero_prod // x for x in nums]
Prefix and suffix arrays O(n) O(n)
Same algorithm as Java optimal #1.
Pseudo-code
left[0] = 1; right[n-1] = 1 for i in 1..n-1: left[i] = left[i-1] * nums[i-1] for i in n-2..0: right[i] = right[i+1] * nums[i+1] out[i] = left[i] * right[i]
Complexity
Edge cases & gotchas
- Empty product = 1 at boundaries.
Python code
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
left = [1] * n
right = [1] * n
for i in range(1, n):
left[i] = left[i-1] * nums[i-1]
for i in range(n-2, -1, -1):
right[i] = right[i+1] * nums[i+1]
return [left[i] * right[i] for i in range(n)]
if __name__ == "__main__":
print(Solution().productExceptSelf([1,2,3,4])) # [24,12,8,6]
print(Solution().productExceptSelf([-1,1,0,-3,3])) # [0,0,9,0,0]
O(1) extra space O(n) O(1) extra
Reuse the output array for prefix; backward sweep multiplies in running suffix.
Pseudo-code
out = [1]*n
for i in 1..n-1: out[i] = out[i-1] * nums[i-1]
right = 1
for i in n-1..0:
out[i] *= right
right *= nums[i]
Complexity
Edge cases & gotchas
- Multiplications in Python use arbitrary precision — overflow is non-issue.
Python code
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
"""
Two passes, O(1) extra space (output not counted, per the problem
statement's allowance).
"""
n = len(nums)
out = [1] * n
# Pass 1: prefix products into out
for i in range(1, n):
out[i] = out[i-1] * nums[i-1]
# Pass 2: multiply in suffix products from the right
right = 1
for i in range(n - 1, -1, -1):
out[i] *= right
right *= nums[i]
return out
if __name__ == "__main__":
print(Solution().productExceptSelf([1,2,3,4])) # [24,12,8,6]
print(Solution().productExceptSelf([-1,1,0,-3,3])) # [0,0,9,0,0]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(1) but ILLEGAL | O(n) | O(1) extra |
| Difficulty | 1/5 (disqualified) | 2/5 | 3/5 |
| Constant factor | — | Three list traversals | Two list traversals |
| Readability | Tricky due to zero cases | Best — explicit roles | Very good with a comment |
| When to use | Never | Teaching | Production |
| PE Verdict | Same as Java: ship the O(1)-extra-space variant. The pattern (output holds running prefix; backward pass multiplies in running suffix) is one of the most-asked array tricks in interviews. | ||
What the interviewer is really testing
This problem teaches that 'product of array' decomposes into prefix * suffix, and that you can fold both passes into the output array to hit O(1) extra space. PE signal: you derive the trick from first principles, you handle zeros without explicit branching (the algorithm just works), and you call out the 'output array doesn't count toward extra space' nuance the problem statement implies.
Top gotchas
- Division feels obvious but is forbidden AND fails on zero — interview honeypot.
- Boundary identity: left[0] = right[n-1] = 1 (empty product). Forgetting this makes both endpoints zero.
- In the O(1) variant, update `right` AFTER multiplying it into out[i], not before — otherwise you include nums[i] in its own answer.
- Java: outputs fit in int per problem constraints; don't reach for long — over-engineering signals you didn't read constraints.
Ship-it
O(1)-extra-space two-pass: prefix into output, backward sweep with a running scalar suffix.
#8 LC 36 Medium Valid Sudoku
Validate three independent constraints (row, col, 3×3 box) in a single pass — encoding-key design.
Problem Statement
Given a 9×9 Sudoku board partially filled with digits '1'–'9' and dots '.' for empty cells, determine whether the current configuration is valid. Validity means: each row, each column, and each of the nine 3×3 sub-boxes contains the digits 1–9 without repetition. The board need not be solvable; only the partial state is checked.
Examples
Constraints
- Board is exactly 9 × 9.
- Each cell is a digit '1'–'9' or '.' for empty.
Approach Overview
Brute Force
Java: Three independent passes (rows, cols, boxes)
Python: Three independent passes
Optimal #1
Java: Single pass with three sets-of-sets (HashSet of String keys)
Python: Single pass with set of (kind, index, digit) tuples
Optimal #2
Java: Single pass with bitmask per row / col / box
Python: Bitmask arrays
Java Solutions
Three independent passes (rows, cols, boxes) O(81) = O(1) O(1)
Validate each constraint group separately. Three full board scans. Correct but verbose.
Pseudo-code
for each row r: ensure no duplicates among row r digits for each col c: ensure no duplicates among col c digits for each 3x3 box: ensure no duplicates among the 9 digits
Complexity
Edge cases & gotchas
- Duplicates across different constraint types (e.g., row OK but column dup) — must check all three.
- Empty cells '.' must be skipped, not counted as a digit.
Java code
class Solution {
public boolean isValidSudoku(final char[][] board) {
for (int r = 0; r < 9; r++) {
final boolean[] seen = new boolean[10];
for (int c = 0; c < 9; c++) {
final char ch = board[r][c];
if (ch == '.') continue;
final int d = ch - '0';
if (seen[d]) return false;
seen[d] = true;
}
}
for (int c = 0; c < 9; c++) {
final boolean[] seen = new boolean[10];
for (int r = 0; r < 9; r++) {
final char ch = board[r][c];
if (ch == '.') continue;
final int d = ch - '0';
if (seen[d]) return false;
seen[d] = true;
}
}
for (int br = 0; br < 9; br += 3) for (int bc = 0; bc < 9; bc += 3) {
final boolean[] seen = new boolean[10];
for (int r = br; r < br + 3; r++) for (int c = bc; c < bc + 3; c++) {
final char ch = board[r][c];
if (ch == '.') continue;
final int d = ch - '0';
if (seen[d]) return false;
seen[d] = true;
}
}
return true;
}
}
Single pass with three sets-of-sets (HashSet of String keys) O(81) O(81)
Walk every cell once. For each digit, attempt to add three string keys: row#d, col#d, box#d. Any collision = invalid.
Pseudo-code
seen = HashSet<String>
for r,c in 0..8:
d = board[r][c]
if d == '.': continue
if not seen.add('R'+r+d): return false
if not seen.add('C'+c+d): return false
if not seen.add('B'+(r/3)+(c/3)+d): return false
return true
Complexity
Edge cases & gotchas
- String keys are clear but allocate 3 strings per non-empty cell — wasteful for hot path.
- set.add returns false on duplicate — single-call dup check.
Java code
import java.util.*;
class Solution {
public boolean isValidSudoku(final char[][] board) {
final Set<String> seen = new HashSet<>(243); // 3 keys × 81 cells, sized
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
final char ch = board[r][c];
if (ch == '.') continue;
final int b = (r / 3) * 3 + (c / 3);
if (!seen.add("R" + r + ch) ||
!seen.add("C" + c + ch) ||
!seen.add("B" + b + ch)) return false;
}
}
return true;
}
}
Single pass with bitmask per row / col / box O(81) O(1) — 27 ints
Three int[9] arrays of bitmasks. Bit d in row[r] means digit d already seen in row r. Flip-and-test in one operation.
Pseudo-code
rows[9], cols[9], boxes[9] = int[]
for r,c in 0..8:
d = board[r][c] - '0' # bit index
if d == '.': continue
mask = 1 << d
b = (r/3)*3 + (c/3)
if rows[r] & mask or cols[c] & mask or boxes[b] & mask: return false
rows[r] |= mask; cols[c] |= mask; boxes[b] |= mask
return true
Complexity
Edge cases & gotchas
- Bit 0 unused (digits are 1–9), so bit positions 1–9 represent each digit.
- Box index formula (r/3)*3 + c/3 is the canonical 0..8 mapping.
- All operations branchless and cache-resident — fastest possible at 9×9 scale.
Java code
class Solution {
/**
* Bitmask Sudoku validator. 27 ints, no allocations during the pass.
* Each bit position 1..9 represents whether that digit has appeared.
*/
public boolean isValidSudoku(final char[][] board) {
final int[] rows = new int[9];
final int[] cols = new int[9];
final int[] boxes = new int[9];
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
final char ch = board[r][c];
if (ch == '.') continue;
final int mask = 1 << (ch - '0');
final int b = (r / 3) * 3 + (c / 3);
if ((rows[r] & mask) != 0 || (cols[c] & mask) != 0 || (boxes[b] & mask) != 0) return false;
rows[r] |= mask; cols[c] |= mask; boxes[b] |= mask;
}
}
return true;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(1) (3·81) | O(1) (1·81) | O(1) (1·81) |
| Space | O(1) | O(243) strings | O(27 ints) |
| Difficulty | 2/5 | 2/5 | 3/5 — bitmask reasoning |
| Constant factor | ~3× the work | Heavy: 3 strings/cell | Best — bit ops only |
| Allocations | Minor | Many (string concat) | None during the pass |
| Generalizes | Hard — three loops | Yes — string keys are flexible | Yes — bitmask per dim, scales to 4×4 sudoku, NQueens |
| When to use | First-pass sanity check | Quick whiteboard ship | Production / interview ship-it |
| PE Verdict | Ship the bitmask version. Same code length as the HashSet variant, zero allocations, ~10× faster in micro-benchmarks. The bitmask-per-constraint pattern recurs in NQueens and other constraint-satisfaction problems — it's worth committing to muscle memory. | ||
Python Solutions
Three independent passes O(1) O(1)
Same as Java baseline — clear but verbose.
Pseudo-code
Validate rows, then columns, then boxes — return False on any duplicate.
Complexity
Edge cases & gotchas
- Skip '.' cells.
- Box index from (r//3, c//3).
Python code
from typing import List
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
for r in range(9):
seen = set()
for c in range(9):
ch = board[r][c]
if ch == '.': continue
if ch in seen: return False
seen.add(ch)
for c in range(9):
seen = set()
for r in range(9):
ch = board[r][c]
if ch == '.': continue
if ch in seen: return False
seen.add(ch)
for br in range(0, 9, 3):
for bc in range(0, 9, 3):
seen = set()
for r in range(br, br+3):
for c in range(bc, bc+3):
ch = board[r][c]
if ch == '.': continue
if ch in seen: return False
seen.add(ch)
return True
Single pass with set of (kind, index, digit) tuples O(1) O(1)
Idiomatic Python: one set, three tuple keys per cell. Reads beautifully.
Pseudo-code
seen = set()
for r,c,d in non-empty cells:
keys = ((r,d,'R'), (d,c,'C'), (r//3,c//3,d,'B'))
for k in keys:
if k in seen: return False
seen.add(k)
return True
Complexity
Edge cases & gotchas
- Tuple hashing is fast in CPython — beats string concat.
- Use tag chars ('R', 'C', 'B') in tuple to avoid namespace collisions across constraints.
Python code
from typing import List
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
"""One pass, set of tuples — Pythonic and clear."""
seen = set()
for r in range(9):
for c in range(9):
d = board[r][c]
if d == '.':
continue
row_key = ('R', r, d)
col_key = ('C', c, d)
box_key = ('B', r // 3, c // 3, d)
if row_key in seen or col_key in seen or box_key in seen:
return False
seen.add(row_key); seen.add(col_key); seen.add(box_key)
return True
Bitmask arrays O(1) O(1)
Same bitmask trick as Java — three int lists of length 9. Tightest Python possible.
Pseudo-code
rows, cols, boxes = [0]*9, [0]*9, [0]*9
for r,c,d in non-empty cells:
bit = 1 << int(d)
b = (r//3)*3 + c//3
if rows[r] & bit or cols[c] & bit or boxes[b] & bit: return False
rows[r] |= bit; cols[c] |= bit; boxes[b] |= bit
return True
Complexity
Edge cases & gotchas
- int(d) parses the digit string '1'..'9' to int. ord(d) - ord('0') is faster but less Pythonic.
- Pure-Python bit ops are slower than tuple-set in CPython — measure before believing the asymptote.
Python code
from typing import List
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
"""Bitmask validator — same algorithm as the Java optimal."""
rows = [0] * 9
cols = [0] * 9
boxes = [0] * 9
for r in range(9):
for c in range(9):
ch = board[r][c]
if ch == '.':
continue
bit = 1 << (ord(ch) - 48) # 48 == ord('0')
b = (r // 3) * 3 + c // 3
if rows[r] & bit or cols[c] & bit or boxes[b] & bit:
return False
rows[r] |= bit; cols[c] |= bit; boxes[b] |= bit
return True
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(1) | O(1) | O(1) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 2/5 | 2/5 | 3/5 |
| Constant factor (CPython) | ~3× more iterations | Best — set lookup is C-fast | Slightly worse — Python int bit-ops |
| Readability | Verbose | Best — Pythonic | Best for Java/C readers; less so for Python |
| Generalizes to NxN | Easy | Easy | Easy — bigint masks for N > 64 |
| When to use | Beginner | Default Pythonic | Performance-critical or aligning with the C++ / Java idiom |
| PE Verdict | In Python, ship the tuple-set version — it's the same asymptote as the bitmask, the constant factor is competitive, and it reads like English. The bitmask form is the right answer when teaching the cross-language pattern or when N grows to a non-9 generalization. | ||
What the interviewer is really testing
This problem teaches encoding multiple constraints into shared keys. PE signal: you fuse the three passes into one with a single seen-set, AND you propose the bitmask form as a constant-factor optimization once you know the alphabet is bounded. Bonus signal: you recognize this same encoding-key pattern from email deduplication, log enrichment, and constraint solvers.
Top gotchas
- Box index formula: (r // 3) * 3 + (c // 3). Off-by-one or transposed and you misclassify boxes.
- Skipping '.' is mandatory — counting it as a 'digit' produces phantom collisions.
- If you stringify keys naively ('R'+r+d), '11' might collide with '1'+'1' in another encoding. Use delimiters or tuples.
- Bitmask uses bits 1–9; leaving bit 0 unused is intentional and matches the digit values directly.
Ship-it
Java: bitmask validator. Python: tuple-set or bitmask — both are correct PE answers.
#9 LC 128 Medium Longest Consecutive Sequence
Find the longest run of consecutive integers — without sorting, in O(n).
Problem Statement
Given an unsorted array of integers nums, return the length of the longest sequence of consecutive integers (any order in the array). The algorithm must run in O(n).
Examples
Constraints
- 0 <= nums.length <= 10^5
- -10^9 <= nums[i] <= 10^9
Approach Overview
Brute Force
Java: Sort + linear scan
Python: Sort + scan
Optimal #1
Java: HashSet + only-extend-from-sequence-starts
Python: set + start-of-run walk
Optimal #2
Java: Union-Find with hash-mapped roots
Python: Union-Find on a dict
Java Solutions
Sort + linear scan O(n log n) O(1) — sort is in-place on int[]
Sort, then walk and count consecutive runs. Violates the O(n) requirement but is the natural first answer.
Pseudo-code
sort(nums)
best = 1; run = 1
for i in 1..n-1:
if nums[i] == nums[i-1]: continue # skip dups
if nums[i] == nums[i-1] + 1: run += 1
else: run = 1
best = max(best, run)
return best
Complexity
Edge cases & gotchas
- Empty array → return 0 (skip the scan).
- Duplicates: skip them, don't reset the run.
- Mutates the input — confirm the caller is OK with it.
Java code
import java.util.*;
class Solution {
public int longestConsecutive(final int[] nums) {
if (nums == null || nums.length == 0) return 0;
Arrays.sort(nums);
int best = 1, run = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i-1]) continue;
run = (nums[i] == nums[i-1] + 1) ? run + 1 : 1;
best = Math.max(best, run);
}
return best;
}
}
HashSet + only-extend-from-sequence-starts O(n) O(n)
Insert all into a set. For each x where x-1 is NOT present (start of a run), walk forward counting. Each integer visited at most twice.
Pseudo-code
S = HashSet(nums)
best = 0
for x in S:
if x-1 not in S: # x is run start
cur = x
while cur+1 in S: cur += 1
best = max(best, cur - x + 1)
return best
Complexity
Edge cases & gotchas
- Iterate over the SET, not the array — duplicates would make the array iteration redundant.
- The 'is x-1 in set' check is the trick that makes this O(n): only run-starters initiate the inner walk.
- Empty input → return 0.
Java code
import java.util.*;
class Solution {
/**
* Hash-set with start-of-run detection. The key insight: only initiate
* the forward walk when x-1 is NOT in the set, ensuring each integer
* is touched at most twice across the whole algorithm.
*/
public int longestConsecutive(final int[] nums) {
if (nums == null || nums.length == 0) return 0;
final Set<Integer> S = new HashSet<>((int) (nums.length / 0.75f) + 1);
for (final int x : nums) S.add(x);
int best = 0;
for (final int x : S) {
if (S.contains(x - 1)) continue; // not a run start
int cur = x;
while (S.contains(cur + 1)) cur++;
best = Math.max(best, cur - x + 1);
}
return best;
}
}
Union-Find with hash-mapped roots O(n · α(n)) ≈ O(n) O(n)
Union x with x-1 and x+1 if present. Track size of components. The largest component size is the answer.
Pseudo-code
parent = HashMap<int,int>; size = HashMap<int,int>
for x in nums: parent[x] = x; size[x] = 1
for x in nums:
for n in (x-1, x+1):
if n in parent: union(x, n)
return max(size.values())
Complexity
Edge cases & gotchas
- Path compression + union-by-size keep amortized cost ~ O(1).
- Union must update size on the new root, not the old — easy bug.
- Slower constant factor than the set approach; useful when offline updates arrive incrementally.
Java code
import java.util.*;
class Solution {
private final Map<Integer,Integer> parent = new HashMap<>();
private final Map<Integer,Integer> size = new HashMap<>();
private int find(int x) {
while (parent.get(x) != x) { parent.put(x, parent.get(parent.get(x))); x = parent.get(x); }
return x;
}
private void union(int a, int b) {
final int ra = find(a), rb = find(b);
if (ra == rb) return;
final int sa = size.get(ra), sb = size.get(rb);
if (sa < sb) { parent.put(ra, rb); size.put(rb, sa + sb); }
else { parent.put(rb, ra); size.put(ra, sa + sb); }
}
public int longestConsecutive(final int[] nums) {
if (nums == null || nums.length == 0) return 0;
for (final int x : nums) { parent.putIfAbsent(x, x); size.putIfAbsent(x, 1); }
for (final int x : nums) {
if (parent.containsKey(x - 1)) union(x, x - 1);
if (parent.containsKey(x + 1)) union(x, x + 1);
}
int best = 0;
for (final int x : parent.keySet()) if (find(x) == x) best = Math.max(best, size.get(x));
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n α(n)) ≈ O(n) |
| Space | O(1) | O(n) | O(n) |
| Difficulty | 2/5 | 3/5 — start-of-run trick | 4/5 — union-find |
| Spec compliance | Violates O(n) requirement | Meets it | Meets it |
| Constant factor | log n hides a 3-5× factor | Best — single set, simple ops | Worse — HashMap ops + path compression overhead |
| Streaming / online | Bad — need full sort | Bad — needs full set | Good — supports incremental adds |
| When to use | Quick whiteboard fallback | Default | Online / incremental scenarios |
| PE Verdict | Ship the HashSet variant. Linear time, simple implementation, the only one that meets the spec. Union-find is the right answer when the input arrives as a stream and you want incremental queries; for the static-array variant it's overkill. | ||
Python Solutions
Sort + scan O(n log n) O(1) sort in-place on a list.copy
Same as Java baseline.
Pseudo-code
sort(nums); walk, skip dups, count runs.
Complexity
Edge cases & gotchas
- Empty array.
- Duplicates.
Python code
from typing import List
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
if not nums:
return 0
nums = sorted(nums)
best = run = 1
for i in range(1, len(nums)):
if nums[i] == nums[i-1]:
continue
run = run + 1 if nums[i] == nums[i-1] + 1 else 1
best = max(best, run)
return best
if __name__ == "__main__":
print(Solution().longestConsecutive([100,4,200,1,3,2])) # 4
set + start-of-run walk O(n) O(n)
Idiomatic Python — same algorithm as Java optimal #1.
Pseudo-code
S = set(nums)
best = 0
for x in S:
if x-1 not in S:
cur = x
while cur+1 in S: cur += 1
best = max(best, cur - x + 1)
return best
Complexity
Edge cases & gotchas
- Iterate over the set, not the input list (handles duplicates and shrinks iteration).
- Empty input → set is empty → loop never runs → returns initial best=0.
Python code
from typing import List
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
"""
O(n) via a set with start-of-run detection. Each integer is touched
at most twice (once for membership probe, once during a forward walk).
"""
S = set(nums)
best = 0
for x in S:
if x - 1 in S:
continue # not a run-starter
cur = x
while cur + 1 in S:
cur += 1
best = max(best, cur - x + 1)
return best
if __name__ == "__main__":
print(Solution().longestConsecutive([100,4,200,1,3,2])) # 4
print(Solution().longestConsecutive([0,3,7,2,5,8,4,6,0,1])) # 9
print(Solution().longestConsecutive([])) # 0
Union-Find on a dict O(n α(n)) O(n)
Same as Java optimal #2 — adds incremental update support at the cost of constant factor.
Pseudo-code
Build dict-based DSU; union x with x±1 if present; return max component size.
Complexity
Edge cases & gotchas
- Watch dict.setdefault vs dict.get(..., default) — the former mutates.
Python code
from typing import List, Dict
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
if not nums:
return 0
parent: Dict[int, int] = {}
size: Dict[int, int] = {}
def find(x: int) -> int:
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a: int, b: int) -> None:
ra, rb = find(a), find(b)
if ra == rb:
return
if size[ra] < size[rb]:
ra, rb = rb, ra
parent[rb] = ra
size[ra] += size[rb]
for x in nums:
if x not in parent:
parent[x] = x
size[x] = 1
for x in list(parent):
for n in (x - 1, x + 1):
if n in parent:
union(x, n)
return max(size[r] for r in parent if find(r) == r)
if __name__ == "__main__":
print(Solution().longestConsecutive([100,4,200,1,3,2])) # 4
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n α(n)) |
| Space | O(n) (sorted()) | O(n) | O(n) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| Spec compliance | Violates O(n) | Meets it | Meets it |
| Constant factor | Surprisingly competitive | Best | Worst — DSU bookkeeping in pure Python |
| Generalizes | Limited | Limited | Yes — full DSU template |
| When to use | Demo / tiny n | Default | Streaming / online queries |
| PE Verdict | Same as Java: ship the set + start-of-run trick. The union-find solution is a fine answer when the interviewer asks about online updates, but for the static array it's unnecessary machinery. | ||
What the interviewer is really testing
The big learning here is that 'O(n) with a hash set' requires a non-obvious twist: only walk forward from run-starters. Without the 'x-1 not in set' guard the algorithm is O(n²) in the worst case (every element triggers a full forward walk). PE signal: you state the amortization argument explicitly, and you call out that union-find unlocks streaming updates if the interviewer pivots.
Top gotchas
- Without the 'x-1 not in S' check, the inner walk repeats work and the algorithm degenerates to O(n²).
- Iterate over the SET, not the array — duplicates in the array would cause repeated work.
- Sort + scan is a perfectly correct algorithm but VIOLATES the O(n) requirement — interviewer will dock you.
- Java HashSet stores boxed Integer; for n = 10^5 the autoboxing pressure is real but fine within problem limits.
Ship-it
HashSet/set with start-of-run walk in both languages. Mentally rehearse the amortization argument so you can say it out loud during the interview.
Two Pointers
#10 LC 125 Easy Valid Palindrome
Two-pointer string scrubbing — the foundational palindrome problem.
Problem Statement
Given a string s, return true if it is a palindrome after converting all uppercase to lowercase and removing all non-alphanumeric characters. Treat the empty string as a palindrome.
Examples
Constraints
- 1 <= s.length <= 2 * 10^5
- s contains printable ASCII.
Approach Overview
Brute Force
Java: Filter to new string, then reverse-compare
Python: Filter + reverse-compare
Optimal #1
Java: Two pointers, in-place skip
Python: Two pointers in-place
Optimal #2
Java: Streamed-filter two-pointer (regex-style filter applied)
Python: Filter once, then bare two-pointer
Java Solutions
Filter to new string, then reverse-compare O(n) O(n)
Build a scrubbed lowercase string, compare to its reverse. Two extra allocations.
Pseudo-code
filtered = lowercased alphanumeric chars of s return filtered == reverse(filtered)
Complexity
Edge cases & gotchas
- Use Character.isLetterOrDigit for ASCII; for Unicode you may want stricter ASCII-only check.
- Two allocations (filter + reverse) hurt the constant factor.
Java code
class Solution {
public boolean isPalindrome(final String s) {
final StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if (Character.isLetterOrDigit(c)) sb.append(Character.toLowerCase(c));
}
return sb.toString().equals(sb.reverse().toString());
}
}
Two pointers, in-place skip O(n) O(1)
Pointers from both ends, skip non-alphanumerics, compare lowercased. Single pass, O(1) auxiliary.
Pseudo-code
l, r = 0, n-1
while l < r:
while l < r and !isAlnum(s[l]): l += 1
while l < r and !isAlnum(s[r]): r -= 1
if lower(s[l]) != lower(s[r]): return false
l += 1; r -= 1
return true
Complexity
Edge cases & gotchas
- Both pointers may converge inside the same skip block — the l < r guard inside the inner while protects against running past each other.
- Use Character.toLowerCase only for letters (digits are no-ops) — minor optimization.
- Empty string after scrubbing → loop never runs → returns true.
Java code
class Solution {
public boolean isPalindrome(final String s) {
int l = 0, r = s.length() - 1;
while (l < r) {
while (l < r && !isAlnum(s.charAt(l))) l++;
while (l < r && !isAlnum(s.charAt(r))) r--;
if (toLower(s.charAt(l)) != toLower(s.charAt(r))) return false;
l++; r--;
}
return true;
}
private static boolean isAlnum(final char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}
private static char toLower(final char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
}
Streamed-filter two-pointer (regex-style filter applied) O(n) O(n) for the filtered buffer
Pre-filter once into a char[] (or scrub during a stream), then pure two-pointer comparison without skip-logic. Cleaner read; one extra allocation.
Pseudo-code
buf = lowercased alphanumeric chars
l, r = 0, len(buf)-1
while l < r:
if buf[l] != buf[r]: return false
l += 1; r -= 1
return true
Complexity
Edge cases & gotchas
- Trades O(1) auxiliary for code clarity — useful when explaining to a less-experienced reviewer.
- On extremely long strings the extra allocation is a noticeable cache-miss cost.
Java code
class Solution {
public boolean isPalindrome(final String s) {
final char[] buf = new char[s.length()];
int n = 0;
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) buf[n++] = c;
else if (c >= 'A' && c <= 'Z') buf[n++] = (char)(c + 32);
}
int l = 0, r = n - 1;
while (l < r) {
if (buf[l] != buf[r]) return false;
l++; r--;
}
return true;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space (auxiliary) | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 3/5 — skip-logic edge cases | 2/5 |
| Constant factor | Worst — two allocations | Best — zero allocations, sequential reads | Mid — one allocation, pure compare |
| Cache behavior | OK | Excellent — String.charAt is a contiguous read | Excellent after filter |
| Code clarity | Best for a junior reviewer | Most concise | Best for explaining the technique |
| When to use | Hate-debugging skip-logic | Default — production hot path | Filter step needed anyway (e.g., before normalization) |
| PE Verdict | Ship the in-place two-pointer (Optimal #1). It's the textbook two-pointer template, zero allocations, and the right answer for any future palindrome variant. The pre-filter version is what to write if the interviewer asks for code that won't make a junior dev cry. | ||
Python Solutions
Filter + reverse-compare O(n) O(n)
Pythonic one-liner using comprehension or filter + slice reverse.
Pseudo-code
f = ''.join(c.lower() for c in s if c.isalnum()) return f == f[::-1]
Complexity
Edge cases & gotchas
- f[::-1] is C-fast in CPython.
- isalnum() is Unicode-aware — treats accented letters and digits as alphanumeric.
Python code
class Solution:
def isPalindrome(self, s: str) -> bool:
f = ''.join(c.lower() for c in s if c.isalnum())
return f == f[::-1]
if __name__ == "__main__":
print(Solution().isPalindrome("A man, a plan, a canal: Panama")) # True
print(Solution().isPalindrome("race a car")) # False
Two pointers in-place O(n) O(1)
Same as Java optimal #1 — skip non-alphanumerics in two while loops, compare lowercased.
Pseudo-code
l, r = 0, n-1
while l < r:
while l < r and not s[l].isalnum(): l += 1
while l < r and not s[r].isalnum(): r -= 1
if s[l].lower() != s[r].lower(): return False
l += 1; r -= 1
return True
Complexity
Edge cases & gotchas
- Pure Python loops are slow vs the C-level brute force; in raw benchmarks the one-liner often beats this for short strings.
- isalnum() called twice per char in the worst case — for huge strings consider caching is_alpha checks.
Python code
class Solution:
def isPalindrome(self, s: str) -> bool:
"""In-place two-pointer scan — O(1) extra space."""
l, r = 0, len(s) - 1
while l < r:
while l < r and not s[l].isalnum():
l += 1
while l < r and not s[r].isalnum():
r -= 1
if s[l].lower() != s[r].lower():
return False
l += 1
r -= 1
return True
if __name__ == "__main__":
print(Solution().isPalindrome("A man, a plan, a canal: Panama")) # True
print(Solution().isPalindrome("race a car")) # False
print(Solution().isPalindrome(" ")) # True (empty after scrub)
Filter once, then bare two-pointer O(n) O(n)
Hybrid — filter to a list once, then linear two-pointer over a cleaner buffer.
Pseudo-code
buf = [c.lower() for c in s if c.isalnum()] two-pointer compare buf with buf reversed
Complexity
Edge cases & gotchas
- Allocates once but the comparison inner loop has no skip-logic — readable for review.
Python code
class Solution:
def isPalindrome(self, s: str) -> bool:
buf = [c.lower() for c in s if c.isalnum()]
l, r = 0, len(buf) - 1
while l < r:
if buf[l] != buf[r]:
return False
l += 1
r -= 1
return True
if __name__ == "__main__":
print(Solution().isPalindrome("A man, a plan, a canal: Panama")) # True
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 3/5 | 2/5 |
| Constant factor (CPython) | Best — work in C | Worst — Python while loops | Mid — filter in C, compare in Python |
| Readability | Best | Most engineered | Good |
| When to use | Default for short strings or readability priority | Tight memory / interview ship-it | When filter step is needed for other reasons |
| PE Verdict | In Python, ship the brute-force one-liner — it's the most idiomatic and the work happens in C. The two-pointer in-place version is the pure-algorithm answer to walk through; reach for it when memory is genuinely constrained or when the interviewer wants to see you do the index math. | ||
What the interviewer is really testing
Two-pointer 101 — the question tests whether you can manage the skip-logic boundary conditions cleanly. PE signal: you write the inner-while-loops with the l < r guard correctly the first time, you handle the Unicode question if asked ('is.isalnum() includes accented letters; do you want strict ASCII?'), and you discuss the constant-factor trade-off between in-place pointers and pre-filter approaches.
Top gotchas
- Inner while-loops MUST guard with `l < r` or you'll run pointers past each other on all-punctuation strings.
- isLetterOrDigit / isalnum behavior: ASCII vs Unicode. Confirm the contract before writing.
- Comparing lowercased forms — don't forget to lowercase BOTH sides; `'A' == 'a'` is false.
- Off-by-one: increment AFTER the comparison, never before — otherwise you skip an unmatched pair.
Ship-it
Java: in-place two-pointer with explicit ASCII checks. Python: the `''.join(c.lower() for c in s if c.isalnum()) == ...[::-1]` one-liner — readable and C-fast.
#11 LC 167 Medium Two Sum II - Input Array Is Sorted
Sorted-array two-sum: O(n) without extra space, the canonical two-pointer template.
Problem Statement
Given a 1-indexed sorted array numbers and a target target, return the two indices (1-based) of the pair that sums to target. Exactly one solution exists, and you must use only constant extra space.
Examples
Constraints
- 2 <= numbers.length <= 3 * 10^4
- -1000 <= numbers[i] <= 1000
- numbers is sorted in non-decreasing order.
- -1000 <= target <= 1000; exactly one valid pair exists.
Approach Overview
Brute Force
Java: Nested loop
Python: Nested loop
Optimal #1
Java: Two pointers (l, r) converging
Python: Two pointers
Optimal #2
Java: Binary search the complement
Python: bisect-based complement search
Java Solutions
Nested loop O(n²) O(1)
Same as unsorted Two Sum brute — ignores the sorted property, O(n²).
Pseudo-code
for i,j: if numbers[i]+numbers[j]==target return [i+1, j+1]
Complexity
Edge cases & gotchas
- Wastes the sorted invariant — interviewer will dock for missing the pattern.
Java code
class Solution {
public int[] twoSum(final int[] numbers, final int target) {
for (int i = 0; i < numbers.length - 1; i++) {
for (int j = i + 1; j < numbers.length; j++) {
if (numbers[i] + numbers[j] == target) return new int[]{i+1, j+1};
}
}
throw new IllegalArgumentException();
}
}
Two pointers (l, r) converging O(n) O(1)
l = 0, r = n-1. Sum < target → l++; sum > target → r--; equal → done. O(n) and O(1).
Pseudo-code
l, r = 0, n-1
while l < r:
s = numbers[l] + numbers[r]
if s == target: return [l+1, r+1]
elif s < target: l += 1
else: r -= 1
Complexity
Edge cases & gotchas
- 1-based indexing per the spec — return l+1, r+1.
- Sum can overflow on int+int near INT_MAX/2 — use long for safety (problem caps at 1000 so it's fine here).
- Strict l < r prevents pairing an element with itself.
Java code
class Solution {
/** Two pointers — the canonical sorted-array template. */
public int[] twoSum(final int[] numbers, final int target) {
int l = 0, r = numbers.length - 1;
while (l < r) {
final int s = numbers[l] + numbers[r];
if (s == target) return new int[]{l + 1, r + 1};
if (s < target) l++; else r--;
}
throw new IllegalArgumentException("No two-sum solution");
}
public static void main(String[] args) {
System.out.println(java.util.Arrays.toString(new Solution().twoSum(new int[]{2,7,11,15}, 9)));
}
}
Binary search the complement O(n log n) O(1)
For each i, binary-search target - numbers[i] in numbers[i+1..n-1]. O(n log n) — strictly worse than two-pointer here, but illustrates the technique used for kSum follow-ups.
Pseudo-code
for i in 0..n-2:
j = binarySearch(target - numbers[i], i+1, n-1)
if j != -1: return [i+1, j+1]
Complexity
Edge cases & gotchas
- Search space starts at i+1 to avoid using same element twice.
- Slower than two-pointer for this problem — present it only as a stepping-stone to 3Sum / kSum where it can dominate the recursive variant.
Java code
class Solution {
public int[] twoSum(final int[] numbers, final int target) {
for (int i = 0; i < numbers.length - 1; i++) {
int lo = i + 1, hi = numbers.length - 1;
final int need = target - numbers[i];
while (lo <= hi) {
final int mid = lo + (hi - lo) / 2;
if (numbers[mid] == need) return new int[]{i + 1, mid + 1};
if (numbers[mid] < need) lo = mid + 1; else hi = mid - 1;
}
}
throw new IllegalArgumentException();
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Uses sorted invariant | No | Yes — the entire algorithm | Yes — binary search |
| Generalizes | No | To 3Sum / kSum | To problems where each step costs an array search |
| When to use | Don't | Default | Stepping stone to kSum recursion |
| PE Verdict | Two pointers, no exceptions. The binary-search variant is a useful template to remember for kSum recursive solutions, but for this exact problem it's strictly slower. | ||
Python Solutions
Nested loop O(n²) O(1)
Quadratic; ignores sortedness.
Pseudo-code
nested for-loops
Complexity
Python code
from typing import List
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
n = len(numbers)
for i in range(n - 1):
for j in range(i + 1, n):
if numbers[i] + numbers[j] == target:
return [i + 1, j + 1]
return []
Two pointers O(n) O(1)
Same as Java — converging l/r pointers.
Pseudo-code
l, r = 0, n-1; while l < r: adjust based on sum vs target
Complexity
Edge cases & gotchas
- Python ints are unbounded — no overflow worries.
Python code
from typing import List
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
l, r = 0, len(numbers) - 1
while l < r:
s = numbers[l] + numbers[r]
if s == target:
return [l + 1, r + 1]
if s < target:
l += 1
else:
r -= 1
return []
if __name__ == "__main__":
print(Solution().twoSum([2, 7, 11, 15], 9)) # [1, 2]
bisect-based complement search O(n log n) O(1)
Use bisect.bisect_left to binary-search the complement. Mirrors the Java O(n log n) variant.
Pseudo-code
for i: j = bisect_left(numbers, need, i+1); if numbers[j] == need: return
Complexity
Edge cases & gotchas
- bisect_left can return n — bounds check before indexing.
Python code
from typing import List
from bisect import bisect_left
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
n = len(numbers)
for i in range(n - 1):
need = target - numbers[i]
j = bisect_left(numbers, need, i + 1)
if j < n and numbers[j] == need:
return [i + 1, j + 1]
return []
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 1/5 | 2/5 |
| Constant factor | Worst | Best | bisect is C-fast but n outer loop in Python |
| When to use | Demo | Default | Pedagogy |
| PE Verdict | Same as Java: ship two-pointer. bisect_left is the right tool when the search becomes the inner step of a kSum recursion. | ||
What the interviewer is really testing
Whether you reach for the sorted-array two-pointer pattern instantly is the entire signal. PE-level: you explain the monotonicity argument (sum only increases by moving l, only decreases by moving r) without prompting and you note this is the inner step that 3Sum and kSum both reduce to.
Top gotchas
- 1-based indexing per spec — easy off-by-one if you forget.
- Strict l < r matters — equal pointers mean we'd be pairing an element with itself.
- Update pointer based on sum-vs-target sign; equality returns immediately, never increment then check.
Ship-it
Two pointers, both languages. l from left, r from right, converge based on comparison with target.
#12 LC 15 Medium 3Sum
Find all unique triplets that sum to zero — sort + two-pointer composition.
Problem Statement
Given an integer array nums, return all triplets [a, b, c] such that a + b + c == 0, where the three indices are distinct and the result list contains no duplicate triplets (order within a triplet does not matter).
Examples
Constraints
- 3 <= nums.length <= 3000
- -10^5 <= nums[i] <= 10^5
Approach Overview
Brute Force
Java: Triple nested loop with set deduplication
Python: Triple loop + set deduplication
Optimal #1
Java: Sort + two-pointer (the canonical answer)
Python: Sort + two-pointer
Optimal #2
Java: Sort + hash-set complement (no two-pointer)
Python: Sort + per-anchor hash-set
Java Solutions
Triple nested loop with set deduplication O(n³) O(k) for unique triplets
Try all triples; canonicalize by sorting each triplet and using a HashSet to dedupe.
Pseudo-code
for i,j,k in n³: if sum==0: add sorted(triplet) to set
Complexity
Edge cases & gotchas
- n³ for n=3000 → 2.7·10^10 ops, will TLE.
- Sorting triplets to dedupe is the standard but tiresome trick.
Java code
import java.util.*;
class Solution {
public List<List<Integer>> threeSum(final int[] nums) {
final Set<List<Integer>> seen = new HashSet<>();
final int n = nums.length;
for (int i = 0; i < n - 2; i++) for (int j = i+1; j < n-1; j++) for (int k = j+1; k < n; k++) {
if (nums[i] + nums[j] + nums[k] == 0) {
final List<Integer> t = Arrays.asList(nums[i], nums[j], nums[k]);
Collections.sort(t);
seen.add(t);
}
}
return new ArrayList<>(seen);
}
}
Sort + two-pointer (the canonical answer) O(n²) O(1) auxiliary (output excluded; sort is in-place on int[])
Sort. For each i, run two-pointer on the remainder for target = -nums[i]. Skip duplicates at every level.
Pseudo-code
sort(nums)
for i in 0..n-3:
if nums[i] > 0: break # all positive, no zero-sum triplet possible
if i>0 and nums[i]==nums[i-1]: continue # skip duplicate anchors
l, r = i+1, n-1
while l < r:
s = nums[i]+nums[l]+nums[r]
if s == 0:
result.append([nums[i],nums[l],nums[r]])
l += 1; r -= 1
while l < r and nums[l]==nums[l-1]: l += 1
while l < r and nums[r]==nums[r+1]: r -= 1
elif s < 0: l += 1
else: r -= 1
Complexity
Edge cases & gotchas
- Skipping duplicates at i (anchor) and at l/r after a hit is THE bug source — write all three skip blocks.
- Early break when nums[i] > 0: sorted array, anchor positive → impossible.
- All-zero arrays: must yield [0,0,0] exactly once; the dedup logic handles this naturally.
Java code
import java.util.*;
class Solution {
/**
* Sort + two-pointer composition. O(n^2). Skip duplicates at every level
* to satisfy the unique-triplets contract.
*/
public List<List<Integer>> threeSum(final int[] nums) {
Arrays.sort(nums);
final List<List<Integer>> out = new ArrayList<>();
final int n = nums.length;
for (int i = 0; i < n - 2; i++) {
if (nums[i] > 0) break; // sorted, no triplet possible
if (i > 0 && nums[i] == nums[i-1]) continue; // skip duplicate anchor
int l = i + 1, r = n - 1;
while (l < r) {
final int sum = nums[i] + nums[l] + nums[r];
if (sum == 0) {
out.add(Arrays.asList(nums[i], nums[l], nums[r]));
l++; r--;
while (l < r && nums[l] == nums[l-1]) l++;
while (l < r && nums[r] == nums[r+1]) r--;
} else if (sum < 0) l++;
else r--;
}
}
return out;
}
public static void main(String[] args) {
System.out.println(new Solution().threeSum(new int[]{-1,0,1,2,-1,-4}));
}
}
Sort + hash-set complement (no two-pointer) O(n²) O(n)
For each pair (i, j) check if -(nums[i]+nums[j]) appeared in nums[i+1..j-1] via a per-i hash-set. Same n² complexity, different deduplication.
Pseudo-code
sort(nums)
for i in 0..n-3:
seen = HashSet<int>
for j in i+1..n-1:
need = -(nums[i] + nums[j])
if need in seen:
emit [nums[i], need, nums[j]]
while j+1<n and nums[j+1]==nums[j]: j += 1 # skip dup
seen.add(nums[j])
Complexity
Edge cases & gotchas
- Dedup is trickier than two-pointer — must track 'last emitted second value' explicitly.
- Same asymptote as two-pointer but worse constant factor (hash ops vs index ops).
- Useful as a stepping stone if the problem changes (e.g., 3Sum closest with negative-sum target).
Java code
import java.util.*;
class Solution {
public List<List<Integer>> threeSum(final int[] nums) {
Arrays.sort(nums);
final List<List<Integer>> out = new ArrayList<>();
final int n = nums.length;
for (int i = 0; i < n - 2; i++) {
if (nums[i] > 0) break;
if (i > 0 && nums[i] == nums[i-1]) continue;
final Set<Integer> seen = new HashSet<>();
for (int j = i + 1; j < n; j++) {
final int need = -nums[i] - nums[j];
if (seen.contains(need)) {
out.add(Arrays.asList(nums[i], need, nums[j]));
while (j + 1 < n && nums[j+1] == nums[j]) j++;
}
seen.add(nums[j]);
}
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n²) | O(n²) |
| Space | O(k) for set | O(1) extra | O(n) |
| Difficulty | 2/5 | 4/5 — three skip blocks | 4/5 |
| Constant factor | Worst | Best | Slightly worse — hash ops |
| Cache behavior | OK | Best — sequential int[] sweep | Worse — pointer chasing |
| Generalizes to 4Sum | Trivially | Yes — wrap another loop | Yes |
| When to use | Tiny n, demo | Default — ship-it | When the values are sparse and a hash-set complements better than index arithmetic |
| PE Verdict | Sort + two-pointer is the canonical answer and what every interviewer expects. Memorize the three skip blocks (anchor, l after hit, r after hit) and the early-break-on-positive optimization. The hash-set variant is a fine pivot answer to '4Sum' or 'how would you handle streaming additions'. | ||
Python Solutions
Triple loop + set deduplication O(n³) O(k)
Same as Java baseline.
Pseudo-code
for i,j,k: if sum==0: add tuple(sorted(...)) to set
Complexity
Edge cases & gotchas
- TLEs.
Python code
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
out = set()
n = len(nums)
for i in range(n-2):
for j in range(i+1, n-1):
for k in range(j+1, n):
if nums[i]+nums[j]+nums[k] == 0:
out.add(tuple(sorted((nums[i], nums[j], nums[k]))))
return [list(t) for t in out]
Sort + two-pointer O(n²) O(1)
Same algorithm as Java optimal #1.
Pseudo-code
see Java pseudo-code
Complexity
Edge cases & gotchas
- Same skip-block care as Java.
Python code
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
out: List[List[int]] = []
n = len(nums)
for i in range(n - 2):
if nums[i] > 0:
break
if i > 0 and nums[i] == nums[i-1]:
continue
l, r = i + 1, n - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s == 0:
out.append([nums[i], nums[l], nums[r]])
l += 1
r -= 1
while l < r and nums[l] == nums[l-1]:
l += 1
while l < r and nums[r] == nums[r+1]:
r -= 1
elif s < 0:
l += 1
else:
r -= 1
return out
if __name__ == "__main__":
print(Solution().threeSum([-1,0,1,2,-1,-4])) # [[-1,-1,2],[-1,0,1]]
print(Solution().threeSum([0,0,0])) # [[0,0,0]]
Sort + per-anchor hash-set O(n²) O(n)
Same as Java optimal #2.
Pseudo-code
for i: build a per-i set; check complement for each j
Complexity
Edge cases & gotchas
- Dedup at the inner pointer is fiddly.
Python code
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
out: List[List[int]] = []
n = len(nums)
for i in range(n - 2):
if nums[i] > 0:
break
if i > 0 and nums[i] == nums[i-1]:
continue
seen = set()
j = i + 1
while j < n:
need = -nums[i] - nums[j]
if need in seen:
out.append([nums[i], need, nums[j]])
# skip duplicate j values to avoid emitting same triplet
while j + 1 < n and nums[j+1] == nums[j]:
j += 1
seen.add(nums[j])
j += 1
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n²) | O(n²) |
| Space | O(k) | O(1) extra | O(n) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| Constant factor | Worst | Best — index arithmetic | Worse — set ops in CPython |
| Generalizes to kSum | Trivially | Yes — recursive wrap | Yes |
| When to use | Demo | Default | Sparse / hash-friendly variants |
| PE Verdict | Same as Java — ship sort + two-pointer. The pattern generalizes cleanly: 4Sum is a loop wrapping 3Sum, kSum is recursion bottoming out at twoSumSorted. | ||
What the interviewer is really testing
3Sum is THE composition problem — sort transforms it, then two-pointer solves the inner reduction. PE signal: you propose the sort+two-pointer instantly, you write the three skip blocks correctly the first time, you mention the early-break-on-positive optimization, and you sketch how to extend to kSum.
Top gotchas
- THREE places to skip duplicates: (i) the outer anchor, (ii) l after a hit, (iii) r after a hit. Missing any one yields duplicate triplets.
- After finding a hit you MUST advance both l and r — moving only one creates infinite loops or duplicates.
- Early break when nums[i] > 0 saves significant time on dense-positive inputs.
- Don't return tuples to a List<List<Integer>> contract — wrap with Arrays.asList in Java.
Ship-it
Sort + two-pointer with three skip blocks + early break on positive. Memorize this template — 4Sum, kSum, and 3Sum-closest all build on it.
#13 LC 11 Medium Container With Most Water
Two-pointer area maximization with a monotonic argument: shrink the shorter side.
Problem Statement
Given n non-negative integers height[i] representing vertical lines on the x-axis, find two lines that together with the x-axis form a container holding the most water. Return the maximum area.
Examples
Constraints
- 2 <= height.length <= 10^5
- 0 <= height[i] <= 10^4
Approach Overview
Brute Force
Java: All pairs
Python: All pairs
Optimal #1
Java: Two pointers, shrink the shorter side
Python: Two pointers
Optimal #2
Java: Two pointers with skip-the-shorter-runs heuristic
Python: Two pointers with skip-runs
Java Solutions
All pairs O(n²) O(1)
Try every (i, j); compute (j-i) * min(height[i], height[j]); track max.
Pseudo-code
for i, j: best = max(best, (j-i) * min(h[i], h[j]))
Complexity
Edge cases & gotchas
- TLEs at n = 10^5.
Java code
class Solution {
public int maxArea(final int[] height) {
int best = 0;
for (int i = 0; i < height.length - 1; i++)
for (int j = i + 1; j < height.length; j++)
best = Math.max(best, (j - i) * Math.min(height[i], height[j]));
return best;
}
}
Two pointers, shrink the shorter side O(n) O(1)
Start at extremes. Width is maximal; height is bounded by min(left, right). Moving the taller side can never improve area — so move the shorter.
Pseudo-code
l, r = 0, n-1; best = 0
while l < r:
h = min(height[l], height[r])
best = max(best, (r - l) * h)
if height[l] < height[r]: l += 1
else: r -= 1
return best
Complexity
Edge cases & gotchas
- Why shrink shorter? The pair (l, r) area is bounded by min(height). Moving the TALLER side strictly decreases width AND cannot raise the min — guaranteed not better.
- Equal heights — moving either pointer is fine; conventionally move l.
Java code
class Solution {
/**
* Two pointers; always shrink toward the shorter side. The monotonic
* argument: keeping the shorter side guarantees the new area is <=
* the old, so abandoning it is the only progress move.
*/
public int maxArea(final int[] height) {
int l = 0, r = height.length - 1, best = 0;
while (l < r) {
final int h = Math.min(height[l], height[r]);
best = Math.max(best, (r - l) * h);
if (height[l] < height[r]) l++; else r--;
}
return best;
}
}
Two pointers with skip-the-shorter-runs heuristic O(n) O(1)
After choosing to shrink the shorter side, skip past lines NOT taller than the one we just abandoned — they can't improve the answer.
Pseudo-code
l, r = 0, n-1; best = 0
while l < r:
h = min(height[l], height[r])
best = max(best, (r-l) * h)
if height[l] < height[r]:
prev = height[l]; l += 1
while l < r and height[l] <= prev: l += 1
else:
prev = height[r]; r -= 1
while l < r and height[r] <= prev: r -= 1
Complexity
Edge cases & gotchas
- Strictly the same answer; just fewer iterations on already-sorted-ish inputs.
- Marginal win — branch prediction may absorb the difference. Don't add complexity unless profiling shows it.
Java code
class Solution {
public int maxArea(final int[] height) {
int l = 0, r = height.length - 1, best = 0;
while (l < r) {
final int h = Math.min(height[l], height[r]);
best = Math.max(best, (r - l) * h);
if (height[l] < height[r]) {
final int prev = height[l++];
while (l < r && height[l] <= prev) l++;
} else {
final int prev = height[r--];
while (l < r && height[r] <= prev) r--;
}
}
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 — needs the proof | 4/5 — extra inner loop |
| Cache behavior | Best (sequential) | Excellent — single sweep | Excellent |
| Constant factor | Worst | Best simple form | Marginally faster on adversarial inputs |
| Provability | Trivial | Requires monotonic argument | Same as Optimal #1 plus skip optimality |
| When to use | Tiny n only | Default — ship-it | Profiled hot path; rarely worth the extra complexity |
| PE Verdict | Ship the simple two-pointer (Optimal #1). The skip-runs optimization (Optimal #2) is a real but tiny win and adds an inner loop that's easy to bug. The interview gold is articulating the monotonic proof: 'moving the taller side can't help because the new pair's height is still capped by the shorter side, and the width is now smaller.' | ||
Python Solutions
All pairs O(n²) O(1)
Quadratic — TLEs at n=10^5.
Pseudo-code
nested for loops
Complexity
Python code
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
best = 0
n = len(height)
for i in range(n - 1):
for j in range(i + 1, n):
best = max(best, (j - i) * min(height[i], height[j]))
return best
Two pointers O(n) O(1)
Same as Java optimal #1.
Pseudo-code
l, r converge; shrink shorter side; track max area
Complexity
Edge cases & gotchas
- Pure-Python loop is slower than Java but still fits the time budget.
Python code
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
l, r = 0, len(height) - 1
best = 0
while l < r:
h = height[l] if height[l] < height[r] else height[r]
area = (r - l) * h
if area > best:
best = area
if height[l] < height[r]:
l += 1
else:
r -= 1
return best
if __name__ == "__main__":
print(Solution().maxArea([1,8,6,2,5,4,8,3,7])) # 49
Two pointers with skip-runs O(n) O(1)
Same as Java optimal #2.
Pseudo-code
after each shrink, skip past heights <= the abandoned one
Complexity
Edge cases & gotchas
- Tiny win in pure Python — skip if readability matters.
Python code
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
l, r = 0, len(height) - 1
best = 0
while l < r:
best = max(best, (r - l) * min(height[l], height[r]))
if height[l] < height[r]:
prev = height[l]
l += 1
while l < r and height[l] <= prev:
l += 1
else:
prev = height[r]
r -= 1
while l < r and height[r] <= prev:
r -= 1
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 4/5 |
| Constant factor | Worst | Best simple | Marginal extra branch overhead |
| When to use | Demo | Default | Performance-tuned |
| PE Verdict | Two-pointer with the simple version. In Python the skip-runs optimization is hidden under interpreter overhead anyway. | ||
What the interviewer is really testing
The whole signal here is the monotonic argument: 'moving the taller side cannot help, only moving the shorter can'. Articulate it before writing code. PE signal: you state the invariant (best area found so far is optimal over all pairs containing the abandoned-shorter side) and you connect this to other 'shrink the constraining side' problems (Trapping Rain Water, k-windowed problems).
Top gotchas
- Equal-height case: either pointer move is valid; pick one consistently to avoid loops.
- Track running max BEFORE moving pointers — moving first then computing risks missing the boundary case.
- Don't try to be clever with both pointers moving simultaneously; the proof requires moving exactly one.
Ship-it
Two-pointer, shrink shorter side. Both languages. State the proof out loud during the interview.
#14 LC 42 Hard Trapping Rain Water
Compute trapped rainfall over a histogram — three legitimate approaches with a clear winner.
Problem Statement
Given an array height representing an elevation map (each bar 1 unit wide), compute how much water can be trapped after rain. Water above bar i equals min(maxLeft, maxRight) − height[i].
Examples
Constraints
- n == height.length
- 1 <= n <= 2 * 10^4
- 0 <= height[i] <= 10^5
Approach Overview
Brute Force
Java: For each bar, scan left and right for the max
Python: For each i, scan both sides
Optimal #1
Java: Prefix-max + suffix-max arrays
Python: Prefix-max + suffix-max arrays
Optimal #2
Java: Two pointers, advance from the lower side
Python: Two pointers
Java Solutions
For each bar, scan left and right for the max O(n²) O(1)
Quadratic baseline. Captures the formula directly: water[i] = min(maxLeft, maxRight) - height[i].
Pseudo-code
for i in 0..n-1:
L = max(height[0..i]); R = max(height[i..n-1])
water += max(0, min(L,R) - height[i])
Complexity
Edge cases & gotchas
- Endpoints contribute zero water (their min(L,R) = height[i] trivially).
- All-zero or strictly-increasing heights → zero water. Already-handled by the formula.
Java code
class Solution {
public int trap(final int[] height) {
int total = 0;
for (int i = 0; i < height.length; i++) {
int L = 0, R = 0;
for (int j = 0; j <= i; j++) L = Math.max(L, height[j]);
for (int j = i; j < height.length; j++) R = Math.max(R, height[j]);
total += Math.max(0, Math.min(L, R) - height[i]);
}
return total;
}
}
Prefix-max + suffix-max arrays O(n) O(n)
Precompute leftMax and rightMax in two linear passes; sum min(left,right) - height[i].
Pseudo-code
leftMax[i] = max of height[0..i] rightMax[i] = max of height[i..n-1] total = sum( min(leftMax[i], rightMax[i]) - height[i] )
Complexity
Edge cases & gotchas
- Inclusive of position i in both running maxes — water above i can't exceed height[i].
- Allocations dominate constant factor at n=10^5 (still fast in absolute terms).
Java code
class Solution {
public int trap(final int[] height) {
final int n = height.length;
if (n == 0) return 0;
final int[] L = new int[n];
final int[] R = new int[n];
L[0] = height[0];
for (int i = 1; i < n; i++) L[i] = Math.max(L[i-1], height[i]);
R[n-1] = height[n-1];
for (int i = n - 2; i >= 0; i--) R[i] = Math.max(R[i+1], height[i]);
int total = 0;
for (int i = 0; i < n; i++) total += Math.min(L[i], R[i]) - height[i];
return total;
}
}
Two pointers, advance from the lower side O(n) O(1)
Maintain leftMax and rightMax scalars. Advance whichever side is shorter — the other side's max is guaranteed at least as large.
Pseudo-code
l, r = 0, n-1; lmax = rmax = 0; total = 0
while l < r:
if height[l] < height[r]:
if height[l] >= lmax: lmax = height[l]
else: total += lmax - height[l]
l += 1
else:
if height[r] >= rmax: rmax = height[r]
else: total += rmax - height[r]
r -= 1
Complexity
Edge cases & gotchas
- Why this works: when height[l] < height[r], we KNOW some bar to the right of l is ≥ height[r] > height[l]. So lmax is the binding constraint, water above l is exactly lmax - height[l].
- Symmetric on the other side.
- No allocations during the pass — best constant factor.
Java code
class Solution {
/**
* Two-pointer rainwater. Always advance from the lower wall — its
* opposing wall is guaranteed at least as tall, so the running max
* on the lower side fully determines water above each step.
*/
public int trap(final int[] height) {
int l = 0, r = height.length - 1;
int lmax = 0, rmax = 0, total = 0;
while (l < r) {
if (height[l] < height[r]) {
if (height[l] >= lmax) lmax = height[l];
else total += lmax - height[l];
l++;
} else {
if (height[r] >= rmax) rmax = height[r];
else total += rmax - height[r];
r--;
}
}
return total;
}
public static void main(String[] args) {
System.out.println(new Solution().trap(new int[]{0,1,0,2,1,0,1,3,2,1,2,1})); // 6
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(n) | O(1) |
| Difficulty | 2/5 | 3/5 | 4/5 — needs the proof |
| Constant factor | Worst | Two arrays + three passes | Best — single pass, zero allocation |
| Cache behavior | OK | OK | Best |
| Provability | Direct | Direct | Requires the asymmetric-walls argument |
| When to use | Quick whiteboard | Whiteboard if you can't prove two-pointer | Production / interview ship-it |
| PE Verdict | Ship the two-pointer. The prefix/suffix array variant is a fine teaching answer; in production it doubles the memory traffic. Memorize the two-pointer proof — interviewers almost always ask 'why does that work?' | ||
Python Solutions
For each i, scan both sides O(n²) O(1)
Same shape — quadratic.
Pseudo-code
for i: L = max(height[:i+1]); R = max(height[i:]); water += min(L,R) - height[i]
Complexity
Python code
from typing import List
class Solution:
def trap(self, height: List[int]) -> int:
total = 0
n = len(height)
for i in range(n):
L = max(height[: i + 1])
R = max(height[i:])
total += min(L, R) - height[i]
return total
Prefix-max + suffix-max arrays O(n) O(n)
Same algorithm as Java optimal #1.
Pseudo-code
build L and R running max; sum min(L,R) - h
Complexity
Edge cases & gotchas
- Easy and clear — write this if you forget the two-pointer proof live.
Python code
from typing import List
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
if n == 0:
return 0
L = [0] * n
R = [0] * n
L[0] = height[0]
for i in range(1, n):
L[i] = max(L[i-1], height[i])
R[-1] = height[-1]
for i in range(n - 2, -1, -1):
R[i] = max(R[i+1], height[i])
return sum(min(L[i], R[i]) - height[i] for i in range(n))
if __name__ == "__main__":
print(Solution().trap([0,1,0,2,1,0,1,3,2,1,2,1])) # 6
Two pointers O(n) O(1)
Same algorithm as Java optimal #2.
Pseudo-code
see Java pseudo-code
Complexity
Edge cases & gotchas
- Branch ordering matters for correctness — write it carefully.
Python code
from typing import List
class Solution:
def trap(self, height: List[int]) -> int:
l, r = 0, len(height) - 1
lmax = rmax = total = 0
while l < r:
if height[l] < height[r]:
if height[l] >= lmax:
lmax = height[l]
else:
total += lmax - height[l]
l += 1
else:
if height[r] >= rmax:
rmax = height[r]
else:
total += rmax - height[r]
r -= 1
return total
if __name__ == "__main__":
print(Solution().trap([0,1,0,2,1,0,1,3,2,1,2,1])) # 6
print(Solution().trap([4,2,0,3,2,5])) # 9
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(n) | O(1) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| Constant factor | Worst | Two passes in C | Single pass; pure Python branch |
| When to use | Demo | Whiteboard fallback | Default / production |
| PE Verdict | Ship the two-pointer. The prefix-suffix array form is a perfectly fine answer if the two-pointer proof escapes you mid-interview — write it cleanly, then explain how to compress it. | ||
What the interviewer is really testing
Trapping Rain Water is one of THE classic 'do you understand the invariant' problems. PE signal: you derive the formula water[i] = min(maxLeft, maxRight) - height[i] from first principles, you propose the prefix-max approach as the natural O(n) answer, and then you tighten to two-pointer with the asymmetric-walls argument — stating it before coding.
Top gotchas
- Two-pointer proof: when height[l] < height[r], some bar in [l+1..r] is ≥ height[r] > height[l]. So the right wall is at least height[r]; lmax is therefore the binding constraint at position l.
- Prefix/suffix arrays must INCLUDE the current index in the running max, not exclude — otherwise you treat each bar as having infinite water capacity above itself.
- Don't subtract negative values — they cannot occur if your running maxes include position i.
- Stack-based monotonic-stack solutions (column-by-column) also work and are O(n); they're a fine alternative if you can't sell the two-pointer proof.
Ship-it
Two-pointer with running lmax/rmax. State the asymmetric-walls invariant before coding so the reviewer knows you understand why this works.
Sliding Window
#15 LC 121 Easy Best Time to Buy and Sell Stock
Maximum profit from one buy + one sell — running-min sliding window.
Problem Statement
Given an array prices where prices[i] is the stock price on day i, return the maximum profit achievable from a single buy and a single sell. You must buy before you sell. If no profit is possible, return 0.
Examples
Constraints
- 1 <= prices.length <= 10^5
- 0 <= prices[i] <= 10^4
Approach Overview
Brute Force
Java: All buy/sell pairs
Python: All pairs
Optimal #1
Java: Single pass, track running min and best diff
Python: Running min + diff
Optimal #2
Java: Two-pointer sliding window (l = buy candidate, r = sell)
Python: Two-pointer window framing
Java Solutions
All buy/sell pairs O(n²) O(1)
For each i < j, compute prices[j] - prices[i], track max. Quadratic.
Pseudo-code
for i, j: best = max(best, prices[j] - prices[i])
Complexity
Edge cases & gotchas
- TLEs at n=10^5.
Java code
class Solution {
public int maxProfit(final int[] prices) {
int best = 0;
for (int i = 0; i < prices.length - 1; i++)
for (int j = i + 1; j < prices.length; j++)
best = Math.max(best, prices[j] - prices[i]);
return best;
}
}
Single pass, track running min and best diff O(n) O(1)
Sweep once. min so far = best buy day; best = max(best, price - min). O(1) state.
Pseudo-code
minPrice = +inf; best = 0
for p in prices:
minPrice = min(minPrice, p)
best = max(best, p - minPrice)
return best
Complexity
Edge cases & gotchas
- Order matters: update min FIRST, then compute diff. Otherwise on day p == minPrice you'd compute (p - prev min) then overwrite.
- All-decreasing input → best stays 0, never goes negative because we initialized to 0.
- Single-element input → loop runs once, best stays 0.
Java code
class Solution {
public int maxProfit(final int[] prices) {
int minPrice = Integer.MAX_VALUE;
int best = 0;
for (final int p : prices) {
if (p < minPrice) minPrice = p;
else if (p - minPrice > best) best = p - minPrice;
}
return best;
}
public static void main(String[] args) {
System.out.println(new Solution().maxProfit(new int[]{7,1,5,3,6,4})); // 5
}
}
Two-pointer sliding window (l = buy candidate, r = sell) O(n) O(1)
Same algorithm in two-pointer dress. l locks to the lowest-so-far; r scans. Useful as a stepping stone to harder window problems.
Pseudo-code
l, r = 0, 1; best = 0
while r < n:
if prices[r] < prices[l]: l = r # found a better buy day
else: best = max(best, prices[r] - prices[l])
r += 1
Complexity
Edge cases & gotchas
- Shifting l to r when prices[r] < prices[l] is the correct generalization — locking in the new minimum buy.
- Identical to Optimal #1; framing differs, work is the same.
Java code
class Solution {
public int maxProfit(final int[] prices) {
int l = 0, best = 0;
for (int r = 1; r < prices.length; r++) {
if (prices[r] < prices[l]) l = r;
else best = Math.max(best, prices[r] - prices[l]);
}
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| Constant factor | Worst | Best — two scalars | Same |
| Generalizes | No | To 'with multiple transactions' DP variants | To 'longest profit window' style problems |
| When to use | Tiny n | Default | When framing as a window helps the next problem |
| PE Verdict | Ship the running-min single pass. Both Optimal forms are the same algorithm — choose the one that's easier to teach in your interview style. Update min FIRST, then diff — the order matters. | ||
Python Solutions
All pairs O(n²) O(1)
Quadratic.
Pseudo-code
nested loops
Complexity
Python code
from typing import List
class Solution:
def maxProfit(self, prices: List[int]) -> int:
best = 0
for i in range(len(prices) - 1):
for j in range(i + 1, len(prices)):
best = max(best, prices[j] - prices[i])
return best
Running min + diff O(n) O(1)
Same as Java optimal #1.
Pseudo-code
see Java pseudo-code
Complexity
Edge cases & gotchas
- math.inf is the cleanest sentinel.
Python code
from typing import List
import math
class Solution:
def maxProfit(self, prices: List[int]) -> int:
min_price = math.inf
best = 0
for p in prices:
if p < min_price:
min_price = p
elif p - min_price > best:
best = p - min_price
return best
if __name__ == "__main__":
print(Solution().maxProfit([7,1,5,3,6,4])) # 5
print(Solution().maxProfit([7,6,4,3,1])) # 0
Two-pointer window framing O(n) O(1)
Same as Java optimal #2.
Pseudo-code
l = best buy index; r scans forward
Complexity
Python code
from typing import List
class Solution:
def maxProfit(self, prices: List[int]) -> int:
l, best = 0, 0
for r in range(1, len(prices)):
if prices[r] < prices[l]:
l = r
else:
profit = prices[r] - prices[l]
if profit > best:
best = profit
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| Constant factor | Worst | Best | Same |
| When to use | Demo | Default | Equivalent — pick by readability preference |
| PE Verdict | Same as Java — running min, single pass. | ||
What the interviewer is really testing
Foundational sliding-window / DP-with-running-state question. PE signal: you immediately propose O(1) state (min seen so far + best so far), articulate the order-of-updates invariant, and gracefully handle the follow-up generalizations (cooldown, k transactions, transaction fee) by sketching the DP states.
Top gotchas
- Update min FIRST, THEN compute diff — reversed order overstates profit on a new low day.
- Initialize best to 0, not negative infinity — the contract returns 0 for unprofitable.
- Don't try to track buy index — only the min price matters for this single-transaction variant.
- Watch for off-by-one in the two-pointer variant: r starts at 1, not 0.
Ship-it
Single pass with running min and best — both languages.
#16 LC 3 Medium Longest Substring Without Repeating Characters
Sliding-window with last-seen-index map — the canonical 'unique window' problem.
Problem Statement
Given a string s, return the length of the longest substring without repeating characters.
Examples
Constraints
- 0 <= s.length <= 5 * 10^4
- s consists of English letters, digits, symbols and spaces.
Approach Overview
Brute Force
Java: All substrings; check uniqueness
Python: All substrings
Optimal #1
Java: Sliding window with HashSet
Python: Sliding window with set
Optimal #2
Java: Sliding window with last-seen index map (single advance of l)
Python: Sliding window with last-seen dict
Java Solutions
All substrings; check uniqueness O(n³) O(n)
Examine every substring with a HashSet for uniqueness. O(n³).
Pseudo-code
for each (i,j): if substring i..j has all unique → track length
Complexity
Edge cases & gotchas
- TLEs above n=200.
Java code
import java.util.*;
class Solution {
public int lengthOfLongestSubstring(final String s) {
int best = 0;
for (int i = 0; i < s.length(); i++) {
final Set<Character> seen = new HashSet<>();
for (int j = i; j < s.length(); j++) {
if (!seen.add(s.charAt(j))) break;
best = Math.max(best, j - i + 1);
}
}
return best;
}
}
Sliding window with HashSet O(n) O(min(n, |Σ|))
Two pointers l, r. Expand r, evicting from set + advancing l on duplicate.
Pseudo-code
l = 0; seen = set(); best = 0
for r in 0..n-1:
while s[r] in seen: seen.remove(s[l]); l += 1
seen.add(s[r])
best = max(best, r - l + 1)
Complexity
Edge cases & gotchas
- While-loop is critical — the duplicate may be deep inside the window, must advance l until it's removed.
- Each char processed at most twice → linear total.
Java code
import java.util.*;
class Solution {
public int lengthOfLongestSubstring(final String s) {
final Set<Character> seen = new HashSet<>();
int l = 0, best = 0;
for (int r = 0; r < s.length(); r++) {
while (!seen.add(s.charAt(r))) {
seen.remove(s.charAt(l++));
}
best = Math.max(best, r - l + 1);
}
return best;
}
}
Sliding window with last-seen index map (single advance of l) O(n) O(min(n, |Σ|))
Map char → last index. On duplicate, jump l past that last index in one step instead of inching.
Pseudo-code
lastSeen = {}; l = 0; best = 0
for r, c in enumerate(s):
if c in lastSeen and lastSeen[c] >= l:
l = lastSeen[c] + 1
lastSeen[c] = r
best = max(best, r - l + 1)
Complexity
Edge cases & gotchas
- Guard `lastSeen[c] >= l`: an old occurrence already outside the current window must be ignored.
- Jumping l (rather than inching) makes this strictly faster on adversarial inputs like 'abcdefga' — l skips to the duplicate position + 1 in O(1).
Java code
import java.util.*;
class Solution {
/**
* Last-seen index map — O(n) with single-step pointer jumps.
* Faster than the set-based variant on adversarial inputs because
* 'l' advances in O(1) rather than inching through the window.
*/
public int lengthOfLongestSubstring(final String s) {
final Map<Character, Integer> last = new HashMap<>();
int l = 0, best = 0;
for (int r = 0; r < s.length(); r++) {
final char c = s.charAt(r);
final Integer prev = last.get(c);
if (prev != null && prev >= l) l = prev + 1;
last.put(c, r);
best = Math.max(best, r - l + 1);
}
return best;
}
public static void main(String[] args) {
System.out.println(new Solution().lengthOfLongestSubstring("abcabcbb")); // 3
System.out.println(new Solution().lengthOfLongestSubstring("pwwkew")); // 3
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n) | O(n) |
| Space | O(n) | O(min(n, |Σ|)) | O(min(n, |Σ|)) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| Constant factor | Worst | Each char up to 2 ops | Each char exactly 1 op |
| When to use | Tiny n | Whiteboard ship | Production / interview ship-it |
| PE Verdict | Ship the last-seen index map. Same asymptote, smaller constant, and the jumping behavior makes the algorithm easier to reason about. The set-and-inch version is fine for whiteboard if you forget the jump trick; both are correct. | ||
Python Solutions
All substrings O(n³) O(n)
Cubic baseline.
Pseudo-code
nested loops + set
Complexity
Python code
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
best = 0
for i in range(len(s)):
seen = set()
for j in range(i, len(s)):
if s[j] in seen:
break
seen.add(s[j])
best = max(best, j - i + 1)
return best
Sliding window with set O(n) O(|Σ|)
Same as Java optimal #1.
Pseudo-code
l=0; seen=set(); for r,c in enumerate(s): while c in seen: seen.remove(s[l]); l+=1; seen.add(c)
Complexity
Python code
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
seen = set()
l = best = 0
for r, c in enumerate(s):
while c in seen:
seen.remove(s[l])
l += 1
seen.add(c)
best = max(best, r - l + 1)
return best
Sliding window with last-seen dict O(n) O(|Σ|)
Same as Java optimal #2 — the canonical Python answer.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Guard last[c] >= l so stale indices don't shrink the window.
Python code
from typing import Dict
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
last: Dict[str, int] = {}
l = best = 0
for r, c in enumerate(s):
if c in last and last[c] >= l:
l = last[c] + 1
last[c] = r
best = max(best, r - l + 1)
return best
if __name__ == "__main__":
print(Solution().lengthOfLongestSubstring("abcabcbb")) # 3
print(Solution().lengthOfLongestSubstring("pwwkew")) # 3
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n) | O(n) |
| Space | O(n) | O(|Σ|) | O(|Σ|) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| Constant factor | Worst | Inner-loop overhead | Best — single statement per char |
| When to use | Demo | Whiteboard fallback | Default |
| PE Verdict | Last-seen dict is the canonical Python answer. The crucial guard is `last[c] >= l`; forgetting it lets stale entries shrink the window incorrectly. | ||
What the interviewer is really testing
Sliding-window template question. PE signal: you write the last-seen-index version, you guard against stale entries with `last[c] >= l`, and you can articulate why the jumping form has a smaller constant factor than the inching form.
Top gotchas
- The stale-index guard `last[c] >= l` is essential — without it, a duplicate occurrence outside the current window incorrectly shrinks l.
- Inching with a set requires a WHILE loop, not IF — the duplicate may be deep inside the window.
- For ASCII-only inputs, replace HashMap with int[128] for ~3× speedup.
- Empty-string corner case: loop doesn't execute, returns 0 — already correct.
Ship-it
Last-seen dict / HashMap with the stale-index guard. Both languages.
#17 LC 424 Medium Longest Repeating Character Replacement
Variable-window with the (windowLen - maxFreq) ≤ k invariant — the trickiest sliding window.
Problem Statement
Given a string s and an integer k, return the length of the longest substring containing the same letter you can obtain by replacing at most k characters.
Examples
Constraints
- 1 <= s.length <= 10^5
- 0 <= k <= s.length
- s consists of uppercase English letters.
Approach Overview
Brute Force
Java: Try every window, count majority
Python: All windows
Optimal #1
Java: Sliding window with running maxFreq (correct but maxFreq drifts)
Python: Sliding window with running maxFreq
Optimal #2
Java: 26 outer loops — fix the target letter
Python: 26 windows, one per target letter
Java Solutions
Try every window, count majority O(n² · |Σ|) O(|Σ|)
Examine all (l, r); window valid iff (r-l+1) - max_freq ≤ k. O(n²·26).
Pseudo-code
for each (l,r): build counts; if (r-l+1)-max(counts) ≤ k: track size
Complexity
Edge cases & gotchas
- TLEs at n=10^5.
Java code
class Solution {
public int characterReplacement(final String s, final int k) {
int best = 0;
for (int l = 0; l < s.length(); l++) {
final int[] c = new int[26];
int max = 0;
for (int r = l; r < s.length(); r++) {
c[s.charAt(r) - 'A']++;
max = Math.max(max, c[s.charAt(r) - 'A']);
if ((r - l + 1) - max <= k) best = Math.max(best, r - l + 1);
}
}
return best;
}
}
Sliding window with running maxFreq (correct but maxFreq drifts) O(n) O(|Σ|)
Track per-window character counts and a maxFreq. While (windowLen - maxFreq) > k, advance l. The trick: maxFreq doesn't need to be exact when shrinking — best answer is monotone in maxFreq.
Pseudo-code
counts = int[26]; l = 0; maxFreq = 0; best = 0
for r in 0..n-1:
counts[s[r]] += 1
maxFreq = max(maxFreq, counts[s[r]])
while (r - l + 1) - maxFreq > k:
counts[s[l]] -= 1
l += 1
best = max(best, r - l + 1)
Complexity
Edge cases & gotchas
- Subtle: maxFreq is NOT recomputed when shrinking. We can leave it stale because best is monotonically tied to the largest valid window ever seen — a stale maxFreq just means we don't shrink past the best window's bound.
- If we shrank by recomputing max, we'd waste O(26) per shrink; the stale-maxFreq trick keeps it O(1).
- k = 0 → effectively 'longest run of identical characters' — the algorithm handles this directly.
Java code
class Solution {
public int characterReplacement(final String s, final int k) {
final int[] counts = new int[26];
int l = 0, maxFreq = 0, best = 0;
for (int r = 0; r < s.length(); r++) {
counts[s.charAt(r) - 'A']++;
if (counts[s.charAt(r) - 'A'] > maxFreq) maxFreq = counts[s.charAt(r) - 'A'];
// Note: maxFreq is intentionally not refreshed when shrinking — see comment block.
while ((r - l + 1) - maxFreq > k) {
counts[s.charAt(l) - 'A']--;
l++;
}
best = Math.max(best, r - l + 1);
}
return best;
}
public static void main(String[] args) {
System.out.println(new Solution().characterReplacement("AABABBA", 1)); // 4
}
}
26 outer loops — fix the target letter O(26 · n) O(1)
For each of 26 target letters, sliding-window with 'replacements used' = (window length - count[target]) ≤ k.
Pseudo-code
for each letter T in 'A'..'Z':
l = 0; cnt = 0; best = max(best, ...)
for r in 0..n-1:
if s[r] == T: cnt += 1
while (r - l + 1) - cnt > k:
if s[l] == T: cnt -= 1
l += 1
best = max(best, r - l + 1)
Complexity
Edge cases & gotchas
- Conceptually clearer than the maxFreq trick — runs 26 independent windows.
- Same asymptote (treating |Σ| = 26 as constant), worse constant factor.
- Use this approach if you can't sell the stale-maxFreq invariant in the interview.
Java code
class Solution {
public int characterReplacement(final String s, final int k) {
int best = 0;
for (char T = 'A'; T <= 'Z'; T++) {
int l = 0, cnt = 0;
for (int r = 0; r < s.length(); r++) {
if (s.charAt(r) == T) cnt++;
while ((r - l + 1) - cnt > k) {
if (s.charAt(l) == T) cnt--;
l++;
}
best = Math.max(best, r - l + 1);
}
}
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·26) | O(n) | O(26n) |
| Space | O(26) | O(26) | O(1) |
| Difficulty | 2/5 | 4/5 — needs the stale-max proof | 3/5 — straightforward |
| Constant factor | Worst | Best | 26× the inner loop |
| Provability | Trivial | Subtle | Trivial |
| Generalizes | No | To variable windows where you track 'something - max_freq' | Standard window over each label |
| When to use | Tiny n | Default — once you've internalized the invariant | When you can't articulate the maxFreq invariant live |
| PE Verdict | Ship the maxFreq sliding window — it's textbook for this problem and the basis of all 'longest window with at most k replacements' problems. Memorize the stale-maxFreq invariant: best is monotone in maxFreq, so we never miss a larger answer by failing to refresh. | ||
Python Solutions
All windows O(n²·26) O(26)
Quadratic baseline.
Pseudo-code
for each (l, r): check if window valid
Complexity
Python code
from collections import Counter
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
best = 0
for l in range(len(s)):
c = Counter()
for r in range(l, len(s)):
c[s[r]] += 1
if (r - l + 1) - max(c.values()) <= k:
best = max(best, r - l + 1)
return best
Sliding window with running maxFreq O(n) O(26)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Stale-maxFreq trick is the entire learning.
Python code
from collections import defaultdict
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
"""
Sliding window. Key invariant: window is valid iff
(windowLen - maxFreq) <= k
where maxFreq is the count of the most-frequent char in the window.
We DON'T refresh maxFreq when shrinking — best is monotone in
maxFreq, so a stale value just keeps us from shrinking past the
best window we've seen.
"""
counts = defaultdict(int)
l = max_freq = best = 0
for r, c in enumerate(s):
counts[c] += 1
if counts[c] > max_freq:
max_freq = counts[c]
while (r - l + 1) - max_freq > k:
counts[s[l]] -= 1
l += 1
if r - l + 1 > best:
best = r - l + 1
return best
if __name__ == "__main__":
print(Solution().characterReplacement("AABABBA", 1)) # 4
26 windows, one per target letter O(26·n) O(1)
Same as Java optimal #2.
Pseudo-code
26 outer loops; per-letter sliding window
Complexity
Edge cases & gotchas
- Conceptually simpler; safer fallback.
Python code
from string import ascii_uppercase
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
best = 0
for T in ascii_uppercase:
l = cnt = 0
for r, c in enumerate(s):
if c == T:
cnt += 1
while (r - l + 1) - cnt > k:
if s[l] == T:
cnt -= 1
l += 1
if r - l + 1 > best:
best = r - l + 1
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·26) | O(n) | O(26n) |
| Space | O(26) | O(26) | O(1) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| Constant factor | Worst | Best | 26× constant |
| Provability | Trivial | Subtle | Trivial |
| When to use | Demo | Default | Fallback |
| PE Verdict | Ship the maxFreq window. The 26-letter outer loop is a fine, safe alternative when the invariant doesn't click for you — it's the same asymptote and only ~26× slower constant. | ||
What the interviewer is really testing
This is the gnarliest classic sliding window because the validity check involves a running max that changes as the window mutates. PE signal: you state the (windowLen - maxFreq) ≤ k invariant first, then you defend the stale-maxFreq trick with the monotonicity argument: 'best can only grow when maxFreq does, so a stale maxFreq doesn't lose us the optimum'.
Top gotchas
- DO NOT refresh maxFreq when shrinking — it's both unnecessary (the invariant still holds for best-tracking) and would cost O(26) per shrink.
- The shrinking is `while`, not `if` — multiple chars may need to leave on a single r-step.
- Window length is r - l + 1 (inclusive both ends). Forget the +1 and you're off by one consistently.
- Replace HashMap with int[26] for the lowercase-only contract — ~5× speedup.
Ship-it
maxFreq sliding window in both languages. State the stale-maxFreq invariant out loud.
#18 LC 567 Medium Permutation in String
Sliding-window of fixed length with anagram-equality check.
Problem Statement
Given two strings s1 and s2, return true if s2 contains a permutation of s1 as a substring.
Examples
Constraints
- 1 <= s1.length, s2.length <= 10^4
- s1, s2 consist of lowercase English letters.
Approach Overview
Brute Force
Java: Each window: rebuild and compare
Python: Window rebuild + Counter equality
Optimal #1
Java: Sliding window with two count arrays + Arrays.equals
Python: Sliding window with Counter and add/remove
Optimal #2
Java: Sliding window with running 'matches' counter (O(n))
Python: Matches-counter window
Java Solutions
Each window: rebuild and compare O((n - m) · m) O(26)
For each starting index in s2 of length |s1|, count chars and compare to s1's count.
Pseudo-code
for each window of length m: build counts; compare with s1 counts
Complexity
Edge cases & gotchas
- TLEs at n=10^4 if m is large.
Java code
class Solution {
public boolean checkInclusion(final String s1, final String s2) {
if (s1.length() > s2.length()) return false;
final int[] target = new int[26];
for (final char c : s1.toCharArray()) target[c - 'a']++;
for (int i = 0; i + s1.length() <= s2.length(); i++) {
final int[] win = new int[26];
for (int j = 0; j < s1.length(); j++) win[s2.charAt(i + j) - 'a']++;
if (java.util.Arrays.equals(target, win)) return true;
}
return false;
}
}
Sliding window with two count arrays + Arrays.equals O(n · 26) O(26)
Slide a fixed-size window; add char on right, remove on left. Compare count arrays at each step.
Pseudo-code
build target counts from s1; build first-window counts from s2[0..m-1]
for r in m..n-1:
add s2[r] to win; remove s2[r-m] from win
if win == target: return true
Complexity
Edge cases & gotchas
- Pre-check |s1| > |s2| → false (otherwise the loop does nothing odd, but bail explicitly).
- Compare Arrays.equals at the END of each step, after the slide.
Java code
import java.util.Arrays;
class Solution {
public boolean checkInclusion(final String s1, final String s2) {
final int m = s1.length(), n = s2.length();
if (m > n) return false;
final int[] target = new int[26];
final int[] win = new int[26];
for (int i = 0; i < m; i++) {
target[s1.charAt(i) - 'a']++;
win[s2.charAt(i) - 'a']++;
}
if (Arrays.equals(target, win)) return true;
for (int r = m; r < n; r++) {
win[s2.charAt(r) - 'a']++;
win[s2.charAt(r - m) - 'a']--;
if (Arrays.equals(target, win)) return true;
}
return false;
}
}
Sliding window with running 'matches' counter (O(n)) O(n + m + 26) O(26)
Track how many of the 26 buckets currently match between window and target. Only the two changing buckets per step can affect the count — O(1) per slide.
Pseudo-code
matches = number of buckets where win[c] == target[c] on each slide: update both affected buckets and matches if matches == 26: return true
Complexity
Edge cases & gotchas
- Decrement matches BEFORE updating the bucket if it was matching; increment AFTER if it now matches. Easy to bug.
- Strictly faster than the Arrays.equals variant for large n.
Java code
class Solution {
public boolean checkInclusion(final String s1, final String s2) {
final int m = s1.length(), n = s2.length();
if (m > n) return false;
final int[] target = new int[26];
final int[] win = new int[26];
for (int i = 0; i < m; i++) {
target[s1.charAt(i) - 'a']++;
win[s2.charAt(i) - 'a']++;
}
int matches = 0;
for (int i = 0; i < 26; i++) if (win[i] == target[i]) matches++;
if (matches == 26) return true;
for (int r = m; r < n; r++) {
final int add = s2.charAt(r) - 'a';
final int rem = s2.charAt(r - m) - 'a';
// add char
if (win[add] == target[add]) matches--;
win[add]++;
if (win[add] == target[add]) matches++;
// remove char
if (win[rem] == target[rem]) matches--;
win[rem]--;
if (win[rem] == target[rem]) matches++;
if (matches == 26) return true;
}
return false;
}
public static void main(String[] args) {
System.out.println(new Solution().checkInclusion("ab", "eidbaooo")); // true
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(nm) | O(26n) | O(n) |
| Space | O(26) | O(26) | O(26) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Constant factor | Worst | 26× per step | Best — O(1) per step |
| When to use | Tiny n | Default — clear and fast | Profiled hot path; the 26 factor matters |
| PE Verdict | Ship the simple sliding window with Arrays.equals — at LeetCode scale (n=10^4, |Σ|=26) it's plenty fast and trivial to write correctly. The matches-counter variant is the right answer if interviewer asks 'can you make this strictly O(n)?' | ||
Python Solutions
Window rebuild + Counter equality O((n-m)·m) O(26)
Counter(s2[i:i+m]) == Counter(s1) for each i. Quadratic-ish.
Pseudo-code
for each window: Counter equality check
Complexity
Python code
from collections import Counter
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
m, n = len(s1), len(s2)
if m > n:
return False
t = Counter(s1)
for i in range(n - m + 1):
if Counter(s2[i : i + m]) == t:
return True
return False
Sliding window with Counter and add/remove O(n) O(26)
Pythonic: maintain a Counter, add/remove on slide, compare with target.
Pseudo-code
init w; for r in m..n: w[s2[r]] += 1; w[s2[r-m]] -= 1; if w == target: return
Complexity
Edge cases & gotchas
- Counter equality treats missing keys as 0 — but we still need to clean up keys that hit zero, otherwise comparison may surprisingly differ. +c in Counter handles this if you cast.
- For correctness, delete keys with zero count or cast both sides to dict before equality.
Python code
from collections import Counter
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
m, n = len(s1), len(s2)
if m > n:
return False
target = Counter(s1)
window = Counter(s2[:m])
if window == target:
return True
for r in range(m, n):
window[s2[r]] += 1
window[s2[r - m]] -= 1
if window[s2[r - m]] == 0:
del window[s2[r - m]] # keep equality semantics clean
if window == target:
return True
return False
if __name__ == "__main__":
print(Solution().checkInclusion("ab", "eidbaooo")) # True
Matches-counter window O(n) O(26)
Same idea as Java optimal #2 — maintain matches counter for O(1) per step.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
m, n = len(s1), len(s2)
if m > n:
return False
target = [0] * 26
win = [0] * 26
for i in range(m):
target[ord(s1[i]) - 97] += 1
win[ord(s2[i]) - 97] += 1
matches = sum(1 for i in range(26) if win[i] == target[i])
if matches == 26:
return True
for r in range(m, n):
add = ord(s2[r]) - 97
rem = ord(s2[r - m]) - 97
if win[add] == target[add]:
matches -= 1
win[add] += 1
if win[add] == target[add]:
matches += 1
if win[rem] == target[rem]:
matches -= 1
win[rem] -= 1
if win[rem] == target[rem]:
matches += 1
if matches == 26:
return True
return False
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(nm) | O(n) but slower const | O(n) tightest const |
| Space | O(26) | O(26) | O(26) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Constant factor | Worst | Counter is C-fast | Pure Python list ops — slightly slower than Counter |
| When to use | Demo | Default Pythonic | When you specifically need O(n) without the |Σ| factor |
| PE Verdict | Counter-based sliding window — Pythonic and fast enough. The matches-counter version is the right answer when the interviewer asks for true O(n). | ||
What the interviewer is really testing
Fixed-window-size template. PE signal: you fuse the 'fixed window' and 'anagram check' cleanly, you handle the |s1| > |s2| short-circuit, and you can articulate the matches-counter optimization that brings it to true O(n).
Top gotchas
- Counter equality in Python treats zero-count keys as missing — but only after you `del` them, otherwise some equality paths surprise. Cleanest: del keys when they hit zero.
- Window length is fixed at |s1|, not variable — different from the previous sliding-window problems.
- Update both add and remove sides in the SAME step before checking — otherwise window length is wrong for one branch.
- Compare window to target at the END of each slide, not before.
Ship-it
Java: int[26] sliding window with Arrays.equals. Python: Counter-based with del-on-zero hygiene.
#19 LC 76 Hard Minimum Window Substring
Variable-window with 'have/need' counters — the canonical 'shortest covering window' problem.
Problem Statement
Given strings s and t, return the minimum window substring of s that contains every character of t (including multiplicity). If none exists, return the empty string.
Examples
Constraints
- 1 <= s.length, t.length <= 10^5
- s, t consist of uppercase and lowercase letters.
Approach Overview
Brute Force
Java: All windows, validate each
Python: All windows + Counter check
Optimal #1
Java: Sliding window with have/need + formed counter
Python: Sliding window with have/need + formed
Optimal #2
Java: Filtered string + sliding window
Python: Filtered-string variant
Java Solutions
All windows, validate each O(n² · |Σ|) O(|Σ|)
Try every (l, r); check coverage.
Pseudo-code
for each (l,r): if window covers t: track shortest
Complexity
Edge cases & gotchas
- TLEs at n=10^5.
Java code
import java.util.*;
class Solution {
public String minWindow(final String s, final String t) {
if (t.length() > s.length()) return "";
final int[] need = new int[128];
for (final char c : t.toCharArray()) need[c]++;
int bestL = -1, bestLen = Integer.MAX_VALUE;
for (int l = 0; l < s.length(); l++) {
final int[] have = new int[128];
for (int r = l; r < s.length(); r++) {
have[s.charAt(r)]++;
if (covers(have, need) && r - l + 1 < bestLen) { bestLen = r - l + 1; bestL = l; break; }
}
}
return bestL == -1 ? "" : s.substring(bestL, bestL + bestLen);
}
private boolean covers(final int[] have, final int[] need) {
for (int i = 0; i < 128; i++) if (have[i] < need[i]) return false;
return true;
}
}
Sliding window with have/need + formed counter O(n + m) O(|Σ|)
Track how many chars in t are 'satisfied' (have[c] >= need[c]). When formed == required, try to shrink l.
Pseudo-code
need = count of t chars; required = number of distinct chars in t
have = empty count; formed = 0; l = 0; best = (∞, 0, 0)
for r in 0..n-1:
have[s[r]] += 1
if have[s[r]] == need[s[r]]: formed += 1
while formed == required:
if r-l+1 < best.len: best = (r-l+1, l, r)
have[s[l]] -= 1
if have[s[l]] < need[s[l]]: formed -= 1
l += 1
return s[best.l : best.r+1] or ''
Complexity
Edge cases & gotchas
- `required` = number of distinct chars in t (NOT len(t)) — multiplicities are encoded in need[c].
- Decrement formed BEFORE incrementing l so the check fires at the right moment.
- Empty result when no covering window exists — initialize bestLen = +∞ as sentinel.
Java code
import java.util.*;
class Solution {
/**
* Sliding window with 'formed' counter tracking how many distinct chars
* have hit their required count.
*/
public String minWindow(final String s, final String t) {
if (t.length() > s.length()) return "";
final int[] need = new int[128];
int required = 0;
for (final char c : t.toCharArray()) { if (need[c]++ == 0) required++; }
final int[] have = new int[128];
int formed = 0, l = 0, bestL = 0, bestLen = Integer.MAX_VALUE;
for (int r = 0; r < s.length(); r++) {
final char cr = s.charAt(r);
have[cr]++;
if (have[cr] == need[cr]) formed++;
while (formed == required) {
if (r - l + 1 < bestLen) { bestLen = r - l + 1; bestL = l; }
final char cl = s.charAt(l);
have[cl]--;
if (have[cl] < need[cl]) formed--;
l++;
}
}
return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestL, bestL + bestLen);
}
public static void main(String[] args) {
System.out.println(new Solution().minWindow("ADOBECODEBANC", "ABC")); // BANC
}
}
Filtered string + sliding window O(n + m + n_f²) where n_f is filtered length O(n_f)
Pre-extract only positions in s where the char appears in t. Slide over the filtered list. Wins on inputs where s is mostly noise.
Pseudo-code
filtered = [(i, c) for i, c in enumerate(s) if c in t] run sliding window over filtered; map back to s indices
Complexity
Edge cases & gotchas
- Real win only when t is small relative to |Σ| AND s contains many irrelevant chars.
- More code; only worth it if the inputs justify it.
Java code
import java.util.*;
class Solution {
public String minWindow(final String s, final String t) {
if (t.length() > s.length()) return "";
final int[] need = new int[128];
int required = 0;
for (final char c : t.toCharArray()) { if (need[c]++ == 0) required++; }
// Build filtered: only positions whose char is in t
final List<int[]> filtered = new ArrayList<>();
for (int i = 0; i < s.length(); i++) if (need[s.charAt(i)] > 0) filtered.add(new int[]{i, s.charAt(i)});
final int[] have = new int[128];
int formed = 0, l = 0, bestL = 0, bestR = -1, bestLen = Integer.MAX_VALUE;
for (int r = 0; r < filtered.size(); r++) {
final int cr = filtered.get(r)[1];
have[cr]++;
if (have[cr] == need[cr]) formed++;
while (formed == required) {
final int sIdx = filtered.get(l)[0], eIdx = filtered.get(r)[0];
if (eIdx - sIdx + 1 < bestLen) { bestLen = eIdx - sIdx + 1; bestL = sIdx; bestR = eIdx; }
final int cl = filtered.get(l)[1];
have[cl]--;
if (have[cl] < need[cl]) formed--;
l++;
}
}
return bestR == -1 ? "" : s.substring(bestL, bestR + 1);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n+m) | O(n + n_f) where n_f ≤ n |
| Space | O(|Σ|) | O(|Σ|) | O(n_f) |
| Difficulty | 2/5 | 4/5 — formed-counter logic | 4/5 |
| Constant factor | Worst | Best generally | Wins when n_f ≪ n |
| When to use | Tiny n | Default | When s is mostly noise |
| PE Verdict | Ship the formed-counter sliding window. It's O(n+m), zero allocation hot path, and the template generalizes to every 'shortest covering window' variant. Filtered-string optimization only matters when s is mostly irrelevant chars — niche. | ||
Python Solutions
All windows + Counter check O(n²·|Σ|) O(|Σ|)
Cubic-ish baseline.
Pseudo-code
nested loops + Counter coverage check
Complexity
Python code
from collections import Counter
class Solution:
def minWindow(self, s: str, t: str) -> str:
if len(t) > len(s):
return ""
need = Counter(t)
best = ""
for l in range(len(s)):
have = Counter()
for r in range(l, len(s)):
have[s[r]] += 1
if all(have[c] >= need[c] for c in need):
if not best or r - l + 1 < len(best):
best = s[l : r + 1]
break
return best
Sliding window with have/need + formed O(n+m) O(|Σ|)
Same algorithm as Java optimal #1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Use Counter for need; defaultdict(int) for have.
Python code
from collections import Counter, defaultdict
class Solution:
def minWindow(self, s: str, t: str) -> str:
if len(t) > len(s):
return ""
need = Counter(t)
required = len(need)
have = defaultdict(int)
formed = 0
l = 0
best_l, best_len = 0, float('inf')
for r, c in enumerate(s):
have[c] += 1
if c in need and have[c] == need[c]:
formed += 1
while formed == required:
if r - l + 1 < best_len:
best_len = r - l + 1
best_l = l
lc = s[l]
have[lc] -= 1
if lc in need and have[lc] < need[lc]:
formed -= 1
l += 1
return "" if best_len == float('inf') else s[best_l : best_l + best_len]
if __name__ == "__main__":
print(Solution().minWindow("ADOBECODEBANC", "ABC")) # BANC
print(Solution().minWindow("a", "aa")) # ""
Filtered-string variant O(n + n_f) O(n_f)
Same idea as Java optimal #2.
Pseudo-code
filter then sliding window
Complexity
Edge cases & gotchas
- Niche optimization.
Python code
from collections import Counter, defaultdict
from typing import List, Tuple
class Solution:
def minWindow(self, s: str, t: str) -> str:
if len(t) > len(s):
return ""
need = Counter(t)
required = len(need)
filtered: List[Tuple[int, str]] = [(i, c) for i, c in enumerate(s) if c in need]
have = defaultdict(int)
formed = 0
l = 0
best = (-1, -1)
best_len = float('inf')
for r, (i_r, c_r) in enumerate(filtered):
have[c_r] += 1
if have[c_r] == need[c_r]:
formed += 1
while formed == required:
i_l, c_l = filtered[l]
if i_r - i_l + 1 < best_len:
best_len = i_r - i_l + 1
best = (i_l, i_r)
have[c_l] -= 1
if have[c_l] < need[c_l]:
formed -= 1
l += 1
return "" if best == (-1, -1) else s[best[0] : best[1] + 1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n+m) | O(n + n_f) |
| Space | O(|Σ|) | O(|Σ|) | O(n_f) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| Constant factor | Worst | Best | Niche win |
| When to use | Demo | Default | Sparse target |
| PE Verdict | Same as Java — formed-counter sliding window. The filtered variant is a real optimization for niche inputs. | ||
What the interviewer is really testing
This problem is the apex of variable-size sliding-window questions. PE signal: you propose the 'formed' counter as the right abstraction (counts how many distinct target chars are saturated), you correctly differentiate `required` (distinct count) from |t| (total count), and you write the shrink-loop carefully with the decrement-formed-on-edge logic.
Top gotchas
- `required` = number of DISTINCT chars in t, not len(t). Multiplicities are encoded in need[c].
- Decrement `formed` only when have[c] DROPS BELOW need[c] (not just on every removal).
- Increment `formed` only when have[c] HITS need[c] exactly (not when it exceeds).
- Update best length BEFORE shrinking, otherwise you miss the current valid window.
Ship-it
Sliding window with have/need + formed counter. Both languages.
#20 LC 239 Hard Sliding Window Maximum
Monotonic deque — the canonical 'window max in O(n)' technique.
Problem Statement
Given an array nums and an integer k, return an array of the maximum value within each contiguous window of size k as the window slides from left to right.
Examples
Constraints
- 1 <= nums.length <= 10^5
- -10^4 <= nums[i] <= 10^4
- 1 <= k <= nums.length
Approach Overview
Brute Force
Java: Per-window linear max
Python: Per-window max
Optimal #1
Java: Monotonic deque (decreasing, by index)
Python: Monotonic deque
Optimal #2
Java: Block decomposition with prefix/suffix maxes
Python: Block decomposition
Java Solutions
Per-window linear max O(n·k) O(1)
For each window, scan k elements for the max.
Pseudo-code
for each window of length k: take max
Complexity
Edge cases & gotchas
- TLEs at n=10^5, k=10^4.
Java code
class Solution {
public int[] maxSlidingWindow(final int[] nums, final int k) {
final int n = nums.length;
final int[] out = new int[n - k + 1];
for (int i = 0; i + k <= n; i++) {
int m = nums[i];
for (int j = i + 1; j < i + k; j++) m = Math.max(m, nums[j]);
out[i] = m;
}
return out;
}
}
Monotonic deque (decreasing, by index) O(n) O(k)
Deque holds candidate indices in decreasing nums-value order. Front is the current window's max. Each index pushed/popped at most once.
Pseudo-code
dq = empty deque of indices
for i in 0..n-1:
while dq not empty and dq.front <= i - k: dq.pollFirst() # evict out-of-window
while dq not empty and nums[dq.back] <= nums[i]: dq.pollLast() # maintain decreasing
dq.offerLast(i)
if i >= k - 1: out[i - k + 1] = nums[dq.front]
Complexity
Edge cases & gotchas
- Use ArrayDeque (Java) — never legacy Stack/Queue. Deque<Integer> with offerLast/pollLast/pollFirst.
- Front is the current max INDEX. Compare nums[front] for the value.
- Off-by-one: emit only once i >= k - 1.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
/**
* Monotonic decreasing deque of indices. Each index amortizes O(1):
* pushed once, popped at most once.
*/
public int[] maxSlidingWindow(final int[] nums, final int k) {
final int n = nums.length;
final int[] out = new int[n - k + 1];
final Deque<Integer> dq = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst();
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) dq.pollLast();
dq.offerLast(i);
if (i >= k - 1) out[i - k + 1] = nums[dq.peekFirst()];
}
return out;
}
public static void main(String[] args) {
System.out.println(java.util.Arrays.toString(
new Solution().maxSlidingWindow(new int[]{1,3,-1,-3,5,3,6,7}, 3))); // [3,3,5,5,6,7]
}
}
Block decomposition with prefix/suffix maxes O(n) O(n)
Partition into blocks of size k. Compute leftMax[i] (block prefix) and rightMax[i] (block suffix). Window max = max(rightMax[i], leftMax[i+k-1]).
Pseudo-code
split nums into blocks of size k leftMax[i] = block-prefix max ending at i rightMax[i] = block-suffix max starting at i for window [i, i+k-1]: out[i] = max(rightMax[i], leftMax[i+k-1])
Complexity
Edge cases & gotchas
- Same asymptote, different memory pattern; useful when the deque approach is hard to articulate.
- Block boundary is i % k == 0 — easy to bug.
- Slightly more work but no deque structure to maintain.
Java code
class Solution {
public int[] maxSlidingWindow(final int[] nums, final int k) {
final int n = nums.length;
final int[] left = new int[n];
final int[] right = new int[n];
for (int i = 0; i < n; i++) left[i] = (i % k == 0) ? nums[i] : Math.max(left[i-1], nums[i]);
for (int i = n - 1; i >= 0; i--) right[i] = (i == n - 1 || (i + 1) % k == 0) ? nums[i] : Math.max(right[i+1], nums[i]);
final int[] out = new int[n - k + 1];
for (int i = 0; i + k <= n; i++) out[i] = Math.max(right[i], left[i + k - 1]);
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(nk) | O(n) | O(n) |
| Space | O(1) | O(k) | O(n) |
| Difficulty | 1/5 | 4/5 — monotonic deque invariant | 3/5 — block boundaries |
| Constant factor | Worst | Best — single pass, O(1) amortized | 3 passes; cache-friendly |
| Streaming? | Trivial | Yes — natural fit | No — needs entire array up front |
| When to use | k tiny | Default — ship-it | Whiteboard if deque proof fails to land |
| PE Verdict | Ship the monotonic deque. It's the textbook answer; once you've internalized the invariant ('deque holds indices in strictly decreasing nums-value order') it writes itself. Block decomposition is a fine alternative answer if the deque proof escapes you under pressure. | ||
Python Solutions
Per-window max O(n·k) O(1)
Quadratic-ish.
Pseudo-code
[max(nums[i:i+k]) for i in range(n-k+1)]
Complexity
Python code
from typing import List
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
return [max(nums[i : i + k]) for i in range(len(nums) - k + 1)]
Monotonic deque O(n) O(k)
Same algorithm as Java optimal #1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- collections.deque has O(1) append/popleft — use it, not list.
Python code
from typing import List
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
"""Monotonic decreasing deque of indices."""
dq = deque()
out = []
for i, x in enumerate(nums):
while dq and dq[0] <= i - k:
dq.popleft()
while dq and nums[dq[-1]] <= x:
dq.pop()
dq.append(i)
if i >= k - 1:
out.append(nums[dq[0]])
return out
if __name__ == "__main__":
print(Solution().maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3)) # [3,3,5,5,6,7]
Block decomposition O(n) O(n)
Same as Java optimal #2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
left = [0] * n
right = [0] * n
for i in range(n):
left[i] = nums[i] if i % k == 0 else max(left[i-1], nums[i])
for i in range(n - 1, -1, -1):
right[i] = nums[i] if i == n - 1 or (i + 1) % k == 0 else max(right[i+1], nums[i])
return [max(right[i], left[i + k - 1]) for i in range(n - k + 1)]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(nk) | O(n) | O(n) |
| Space | O(1) | O(k) | O(n) |
| Difficulty | 1/5 | 4/5 | 3/5 |
| Constant factor | Worst | Best — deque is C-fast | Three passes |
| When to use | Tiny k | Default | Fallback |
| PE Verdict | Monotonic deque, same as Java. collections.deque's append and popleft are both O(1) — use it. | ||
What the interviewer is really testing
Monotonic-deque is the entire learning. PE signal: you propose the deque immediately, you state the invariant ('deque holds indices, nums-values strictly decreasing'), and you write the two while-loops in the right order (evict-out-of-window, then evict-smaller). Bonus: you mention this is the same shape as 'next greater element' problems — generalizable.
Top gotchas
- Order matters: evict-out-of-window FIRST, then evict-smaller. Reversing risks evicting an element you're about to need.
- Comparing nums[dq[-1]] <= x (not <) keeps the deque strictly decreasing — duplicates evict, which is fine since indices are still distinct.
- Emit output only after i >= k - 1 — first complete window.
- Use ArrayDeque in Java, not LinkedList — both implement Deque but ArrayDeque is faster, no nulls.
Ship-it
Monotonic decreasing deque, both languages. State the invariant out loud.
Stack
#21 LC 20 Easy Valid Parentheses
Stack-based bracket matcher — the foundational stack problem.
Problem Statement
Given a string s containing only the characters (, ), {, }, [, ], determine whether the brackets are properly opened and closed in order. Every open bracket must be closed by the same type of bracket and in the correct order.
Examples
Constraints
- 1 <= s.length <= 10^4
- s consists of parentheses only (above six characters).
Approach Overview
Brute Force
Java: Repeated 'remove inner pair' until stable
Python: Repeated string replace
Optimal #1
Java: Stack of opens
Python: Stack with dict<close, open>
Optimal #2
Java: Stack with map<close, open>
Python: Stack pushing the EXPECTED close on each open
Java Solutions
Repeated 'remove inner pair' until stable O(n²) O(n)
Loop replacing '()', '[]', '{}' with '' until no changes. Quadratic.
Pseudo-code
while s contains a pair: remove it return s.empty()
Complexity
Edge cases & gotchas
- Useful only as a teaching foil — quadratic and string-replacement-heavy.
Java code
class Solution {
public boolean isValid(String s) {
int len = -1;
while (s.length() != len) {
len = s.length();
s = s.replace("()", "").replace("[]", "").replace("{}", "");
}
return s.isEmpty();
}
}
Stack of opens O(n) O(n)
Push opens onto a stack; on close, pop and verify it matches.
Pseudo-code
stack = empty
for c in s:
if c is open: stack.push(c)
else: if stack empty or top != matching open: return false; stack.pop()
return stack.empty()
Complexity
Edge cases & gotchas
- Closing bracket with empty stack → false (e.g., starts with ')').
- Stack non-empty at the end → false (unclosed opens).
- Use ArrayDeque, never legacy Stack.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public boolean isValid(final String s) {
final Deque<Character> stack = new ArrayDeque<>(s.length() / 2 + 1);
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
switch (c) {
case '(': case '[': case '{': stack.push(c); break;
case ')': if (stack.isEmpty() || stack.pop() != '(') return false; break;
case ']': if (stack.isEmpty() || stack.pop() != '[') return false; break;
case '}': if (stack.isEmpty() || stack.pop() != '{') return false; break;
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.isValid("()[]{}")); // true
System.out.println(sol.isValid("(]")); // false
}
}
Stack with map<close, open> O(n) O(n)
Same approach, table-driven. Cleaner extension to additional bracket types.
Pseudo-code
match = {')': '(', ']': '[', '}': '{'}
for c in s:
if c in match.values(): push c
elif c in match: if stack empty or pop != match[c]: return false
return stack empty
Complexity
Edge cases & gotchas
- Slightly slower than the switch but trivially extensible.
Java code
import java.util.*;
class Solution {
private static final Map<Character, Character> MATCH = Map.of(')', '(', ']', '[', '}', '{');
public boolean isValid(final String s) {
final Deque<Character> stack = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if (MATCH.containsKey(c)) {
if (stack.isEmpty() || stack.pop() != MATCH.get(c)) return false;
} else {
stack.push(c);
}
}
return stack.isEmpty();
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 2/5 | 2/5 |
| Constant factor | Worst — string ops | Best — primitive switch | Slightly worse — Map lookup |
| Extensibility | Hard | Hard — cases hard-coded | Easy — add to MATCH map |
| When to use | Don't | Default — fastest | When bracket alphabet is configurable |
| PE Verdict | Ship the switch-based stack. The map-based variant is an interview pivot if asked 'how would you support angle brackets / Unicode brackets / custom open-close pairs?' — answer: extend the MATCH map. | ||
Python Solutions
Repeated string replace O(n²) O(n)
Quadratic baseline.
Pseudo-code
loop replacing pairs until stable
Complexity
Python code
class Solution:
def isValid(self, s: str) -> bool:
prev = None
while s != prev:
prev = s
s = s.replace("()", "").replace("[]", "").replace("{}", "")
return s == ""
Stack with dict<close, open> O(n) O(n)
Pythonic — list as stack, dict for matching.
Pseudo-code
match = {')':'(', ']':'[', '}':'{'}
stack = []
for c in s:
if c in match:
if not stack or stack.pop() != match[c]: return False
else: stack.append(c)
return not stack
Complexity
Edge cases & gotchas
- Use list as stack — list.append/pop are O(1) amortized.
Python code
class Solution:
def isValid(self, s: str) -> bool:
match = {')': '(', ']': '[', '}': '{'}
stack = []
for c in s:
if c in match:
if not stack or stack.pop() != match[c]:
return False
else:
stack.append(c)
return not stack
if __name__ == "__main__":
print(Solution().isValid("()[]{}")) # True
print(Solution().isValid("(]")) # False
print(Solution().isValid("{[]}")) # True
Stack pushing the EXPECTED close on each open O(n) O(n)
On '(' push ')'. On a close, check stack.pop() == c. One fewer dict lookup.
Pseudo-code
stack = []
for c in s:
if c == '(': stack.append(')')
elif c == '[': stack.append(']')
elif c == '{': stack.append('}')
else:
if not stack or stack.pop() != c: return False
return not stack
Complexity
Edge cases & gotchas
- Slightly faster — one comparison instead of dict lookup + comparison.
Python code
class Solution:
def isValid(self, s: str) -> bool:
stack = []
for c in s:
if c == '(':
stack.append(')')
elif c == '[':
stack.append(']')
elif c == '{':
stack.append('}')
elif not stack or stack.pop() != c:
return False
return not stack
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| Constant factor | Worst | Dict lookup overhead | Best — pure equality |
| Extensibility | Hard | Easy — extend dict | Hard — cases hard-coded |
| When to use | Don't | Default Pythonic | Profiled hot path |
| PE Verdict | Ship the dict-based stack. The 'push expected close' variant is a marginally faster alternative; both are valid PE answers. | ||
What the interviewer is really testing
Stack 101. PE signal: you reach for ArrayDeque (Java) or list (Python) — never legacy Stack — and you handle the two failure modes (empty stack on close, non-empty stack at end) in the same readable structure.
Top gotchas
- Empty-stack-on-close is a failure: `(])` would crash if you pop without checking.
- Final stack non-empty is also a failure: `(((` ends with three unclosed opens.
- Use ArrayDeque in Java; java.util.Stack is synchronized legacy code — slow and discouraged.
Ship-it
Java: ArrayDeque + switch. Python: list + dict. Both languages O(n) time, O(n) space.
#22 LC 155 Medium Min Stack
Stack with O(1) min — twin-stack vs encoded-delta design.
Problem Statement
Design a stack supporting push, pop, top, and getMin, all in O(1) time.
Examples
Constraints
- -2^31 <= val <= 2^31 - 1
- Methods top, pop, getMin called only on non-empty stack.
- At most 3 * 10^4 calls in total.
Approach Overview
Brute Force
Java: Single stack; O(n) getMin by scan
Python: Single list; O(n) getMin
Optimal #1
Java: Twin stack: values + running mins
Python: Twin stack
Optimal #2
Java: Single stack with encoded deltas (or only-track-mins compression)
Python: Compressed mins stack
Java Solutions
Single stack; O(n) getMin by scan push/pop/top O(1); getMin O(n) O(n)
Push/pop in O(1); getMin scans the whole stack — O(n).
Pseudo-code
stack with linear-scan getMin
Complexity
Edge cases & gotchas
- Violates the O(1) getMin requirement.
Java code
import java.util.*;
class MinStack {
private final Deque<Integer> s = new ArrayDeque<>();
public void push(int x) { s.push(x); }
public void pop() { s.pop(); }
public int top() { return s.peek(); }
public int getMin() {
int m = Integer.MAX_VALUE;
for (final int v : s) m = Math.min(m, v);
return m;
}
}
Twin stack: values + running mins O(1) for all ops O(n)
Auxiliary stack tracks the minimum at each level of the main stack. push pushes min(current top of mins, new val) onto mins.
Pseudo-code
push(x): main.push(x); mins.push(min(mins.top, x)) pop: main.pop(); mins.pop() top: return main.top getMin: return mins.top
Complexity
Edge cases & gotchas
- Initialize mins to push(x) on the very first push (no prior top to compare).
- Both stacks pop together — never let them desync.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class MinStack {
private final Deque<Integer> values = new ArrayDeque<>();
private final Deque<Integer> mins = new ArrayDeque<>();
public void push(final int x) {
values.push(x);
mins.push(mins.isEmpty() ? x : Math.min(mins.peek(), x));
}
public void pop() { values.pop(); mins.pop(); }
public int top() { return values.peek(); }
public int getMin() { return mins.peek(); }
public static void main(String[] args) {
MinStack m = new MinStack();
m.push(-2); m.push(0); m.push(-3);
System.out.println(m.getMin()); // -3
m.pop();
System.out.println(m.top()); // 0
System.out.println(m.getMin()); // -2
}
}
Single stack with encoded deltas (or only-track-mins compression) O(1) for all ops O(n) worst case, often less
Mins-stack only grows when a new value ≤ current min. Pop pops mins-stack only when popping a value equal to current min. Saves space on monotone-increasing inputs.
Pseudo-code
push(x): values.push(x); if mins empty or x <= mins.top: mins.push(x) pop: v = values.pop(); if v == mins.top: mins.pop()
Complexity
Edge cases & gotchas
- Use <=, not <, so duplicate min values get tracked separately — otherwise popping the first duplicate erases the min prematurely.
- Best when push values are mostly increasing — mins stack stays tiny.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class MinStack {
private final Deque<Integer> values = new ArrayDeque<>();
private final Deque<Integer> mins = new ArrayDeque<>();
public void push(final int x) {
values.push(x);
if (mins.isEmpty() || x <= mins.peek()) mins.push(x);
}
public void pop() {
if (values.pop().equals(mins.peek())) mins.pop();
}
public int top() { return values.peek(); }
public int getMin() { return mins.peek(); }
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | getMin O(n) | All O(1) | All O(1) |
| Space | O(n) | O(2n) — two stacks parallel | O(n) worst, often << 2n |
| Difficulty | 1/5 | 2/5 | 3/5 — duplicate-min subtlety |
| Compliance | Violates spec | Meets it | Meets it |
| When to use | Don't | Default — fool-proof | Memory-tight; mostly-increasing pushes |
| PE Verdict | Ship the twin-stack — it's the safest correct design and the spec wants O(1). The compressed variant is a fine optimization to mention; the duplicate-min `<=` detail is a classic trap. | ||
Python Solutions
Single list; O(n) getMin getMin O(n) O(n)
Same shape — violates spec.
Pseudo-code
list as stack
Complexity
Python code
class MinStack:
def __init__(self):
self.s = []
def push(self, x: int) -> None: self.s.append(x)
def pop(self) -> None: self.s.pop()
def top(self) -> int: return self.s[-1]
def getMin(self) -> int: return min(self.s)
Twin stack O(1) all ops O(n)
Same algorithm as Java optimal #1.
Pseudo-code
see Java
Complexity
Python code
class MinStack:
def __init__(self):
self.values = []
self.mins = []
def push(self, x: int) -> None:
self.values.append(x)
self.mins.append(x if not self.mins else min(self.mins[-1], x))
def pop(self) -> None:
self.values.pop()
self.mins.pop()
def top(self) -> int:
return self.values[-1]
def getMin(self) -> int:
return self.mins[-1]
if __name__ == "__main__":
m = MinStack()
m.push(-2); m.push(0); m.push(-3)
print(m.getMin()) # -3
m.pop()
print(m.top()) # 0
print(m.getMin()) # -2
Compressed mins stack O(1) all ops O(n) worst, often less
Same as Java optimal #2 — only push to mins on new minima.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Use <= for duplicates.
Python code
class MinStack:
def __init__(self):
self.values = []
self.mins = []
def push(self, x: int) -> None:
self.values.append(x)
if not self.mins or x <= self.mins[-1]:
self.mins.append(x)
def pop(self) -> None:
v = self.values.pop()
if v == self.mins[-1]:
self.mins.pop()
def top(self) -> int:
return self.values[-1]
def getMin(self) -> int:
return self.mins[-1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | getMin O(n) | All O(1) | All O(1) |
| Space | O(n) | O(2n) | O(n) typical |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When to use | Don't | Default | Memory-tight |
| PE Verdict | Twin stack — clean and obviously correct. Compress only if memory matters. | ||
What the interviewer is really testing
Design questions reward the simpler correct answer. PE signal: you propose the twin-stack first, you articulate why it's O(1) for ALL operations, and you offer the compressed variant as an optional optimization with the duplicate-min `<=` warning.
Top gotchas
- First push: `mins` is empty — initialize the comparison guard, otherwise NPE in Java / IndexError in Python.
- Compressed variant MUST use `<=` not `<` — otherwise duplicate min values cause premature mins pops.
- Twin stacks must POP IN LOCKSTEP. Never let them desync.
- ArrayDeque in Java; never Stack class.
Ship-it
Twin stack. Both languages.
#23 LC 150 Medium Evaluate Reverse Polish Notation
Postfix expression evaluator — the textbook stack-as-calculator.
Problem Statement
Given an array of tokens representing a Reverse Polish (postfix) expression, return the integer result. Operators are +, -, *, /. Division truncates toward zero.
Examples
Constraints
- 1 <= tokens.length <= 10^4
- tokens[i] is either a valid integer or an operator in {+,-,*,/}.
- All intermediate results fit in a 32-bit signed integer.
Approach Overview
Brute Force
Java: Recursive parse
Python: Recursive parse
Optimal #1
Java: Iterative stack of integers
Python: Iterative list-as-stack
Optimal #2
Java: Reuse the input array as the stack (in-place)
Python: Operator dispatch via dict<str, callable>
Java Solutions
Recursive parse O(n) O(n) call stack
Parse from the right; recursively consume two operands. Same complexity, more code, harder to reason about.
Pseudo-code
def parse(): t = pop(); if numeric return t; b = parse(); a = parse(); return apply(t, a, b)
Complexity
Edge cases & gotchas
- Stack-overflow risk on deeply nested expressions.
Java code
import java.util.*;
class Solution {
private int idx;
public int evalRPN(final String[] tokens) {
idx = tokens.length - 1;
return parse(tokens);
}
private int parse(final String[] tokens) {
final String t = tokens[idx--];
switch (t) {
case "+": { int b = parse(tokens); int a = parse(tokens); return a + b; }
case "-": { int b = parse(tokens); int a = parse(tokens); return a - b; }
case "*": { int b = parse(tokens); int a = parse(tokens); return a * b; }
case "/": { int b = parse(tokens); int a = parse(tokens); return a / b; }
default: return Integer.parseInt(t);
}
}
}
Iterative stack of integers O(n) O(n)
Push numbers; on operator, pop two (RHS first), apply, push back. The textbook answer.
Pseudo-code
stack = empty
for t in tokens:
if t is operator:
b = stack.pop(); a = stack.pop()
stack.push(apply(t, a, b))
else: stack.push(int(t))
return stack.top
Complexity
Edge cases & gotchas
- Pop ORDER matters: b = pop() FIRST (it was pushed last). a - b ≠ b - a.
- Java integer division truncates toward zero, matching the spec — no special handling.
- ArrayDeque, not Stack.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int evalRPN(final String[] tokens) {
final Deque<Integer> stack = new ArrayDeque<>(tokens.length / 2 + 1);
for (final String t : tokens) {
switch (t) {
case "+": { int b = stack.pop(), a = stack.pop(); stack.push(a + b); break; }
case "-": { int b = stack.pop(), a = stack.pop(); stack.push(a - b); break; }
case "*": { int b = stack.pop(), a = stack.pop(); stack.push(a * b); break; }
case "/": { int b = stack.pop(), a = stack.pop(); stack.push(a / b); break; }
default: stack.push(Integer.parseInt(t));
}
}
return stack.peek();
}
public static void main(String[] args) {
System.out.println(new Solution().evalRPN(new String[]{"2","1","+","3","*"})); // 9
}
}
Reuse the input array as the stack (in-place) O(n) O(1) extra
Treat tokens itself as scratch space. Saves the auxiliary stack at the cost of mutating input.
Pseudo-code
top = -1
for t in tokens:
if op: top--; tokens[top] = apply(tokens[top], tokens[top+1])
else: top++; tokens[top] = parseInt(t)
return tokens[0]
Complexity
Edge cases & gotchas
- Mutates input — confirm caller is OK with it.
- Marginally faster due to better cache locality (single contiguous int[] scratch instead of stack node allocations).
Java code
class Solution {
public int evalRPN(final String[] tokens) {
final int[] stack = new int[tokens.length / 2 + 1]; // scratch, not input mutation
int top = -1;
for (final String t : tokens) {
switch (t) {
case "+": { final int b = stack[top--], a = stack[top]; stack[top] = a + b; break; }
case "-": { final int b = stack[top--], a = stack[top]; stack[top] = a - b; break; }
case "*": { final int b = stack[top--], a = stack[top]; stack[top] = a * b; break; }
case "/": { final int b = stack[top--], a = stack[top]; stack[top] = a / b; break; }
default: stack[++top] = Integer.parseInt(t);
}
}
return stack[0];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) call stack | O(n) heap | O(n) primitive array |
| Difficulty | 3/5 | 2/5 | 3/5 |
| Constant factor | Recursion overhead | Best for typical RPN inputs | Best — no boxing, single int[] |
| Cache behavior | Worst | Heap-allocated boxed Integers — pointer chasing | Best — sequential int[] |
| When to use | Demo only | Default | Hot path / huge inputs |
| PE Verdict | Ship the iterative ArrayDeque stack. It's the canonical answer and reads itself. Reach for the int[] scratch variant only when boxing overhead becomes measurable (rare at LeetCode scale). | ||
Python Solutions
Recursive parse O(n) O(n)
Same shape as Java baseline.
Pseudo-code
consume from the right; recurse on operands
Complexity
Python code
from typing import List
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
idx = [len(tokens) - 1]
def parse() -> int:
t = tokens[idx[0]]
idx[0] -= 1
if t in "+-*/":
b = parse()
a = parse()
if t == '+': return a + b
if t == '-': return a - b
if t == '*': return a * b
# truncate toward zero (Python // floors — fix sign)
return int(a / b)
return int(t)
return parse()
Iterative list-as-stack O(n) O(n)
Pythonic — list as stack, dispatch on operator.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Python's / is float; // floors toward -inf. Use int(a/b) to truncate toward zero like Java.
- Edge case: -7 // 2 == -4 in Python; int(-7/2) == -3 (correct, matches Java).
Python code
from typing import List
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for t in tokens:
if t in "+-*/":
b = stack.pop()
a = stack.pop()
if t == '+':
stack.append(a + b)
elif t == '-':
stack.append(a - b)
elif t == '*':
stack.append(a * b)
else:
# int(a/b) truncates toward zero (matches problem spec); a // b would floor.
stack.append(int(a / b))
else:
stack.append(int(t))
return stack[0]
if __name__ == "__main__":
print(Solution().evalRPN(["2","1","+","3","*"])) # 9
print(Solution().evalRPN(["10","6","9","3","+","-11","*","/","*","17","+","5","+"])) # 22
Operator dispatch via dict<str, callable> O(n) O(n)
Cleaner — drop the if/elif chain in favor of a function table.
Pseudo-code
OPS = {'+': add, '-': sub, ...}; OPS[t](a, b)
Complexity
Edge cases & gotchas
- operator.add etc. give clean lambdas.
- Division must truncate toward zero — wrap explicitly.
Python code
from typing import List
from operator import add, sub, mul
class Solution:
OPS = {
'+': add,
'-': sub,
'*': mul,
'/': lambda a, b: int(a / b), # truncate toward zero, NOT floor
}
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for t in tokens:
if t in self.OPS:
b = stack.pop()
a = stack.pop()
stack.append(self.OPS[t](a, b))
else:
stack.append(int(t))
return stack[0]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 3/5 | 2/5 | 2/5 |
| Constant factor | Recursion overhead | Best — direct dispatch | Slight indirection but C-fast operators |
| Readability | Worst | Good | Best — declarative |
| When to use | Demo | Default | Production code with extension in mind |
| PE Verdict | Iterative list-as-stack — fast and readable. The dispatch-dict variant is the right answer if you're building a calculator with extensible operators. | ||
What the interviewer is really testing
Stack as calculator. PE signal: you handle the truncate-toward-zero division correctly in Python (int(a/b), NOT a // b), you pop in the right order (b before a), and you reach for ArrayDeque / list — never legacy Stack.
Top gotchas
- Pop ORDER: b first (last pushed), then a. Wrong order breaks subtraction and division.
- Python: `//` floors, but the problem requires truncation toward zero. `int(a/b)` is correct; `a // b` is WRONG for negative a.
- Java: `int / int` truncates toward zero by default — no fix needed.
- Empty token array isn't possible per spec, but stack-empty defensiveness still earns points.
Ship-it
Iterative stack, both languages. Watch the Python division semantics.
#24 LC 739 Medium Daily Temperatures
Monotonic stack: 'next greater' in disguise — answer the wait-days for each day.
Problem Statement
Given an array temperatures, return an array answer where answer[i] equals the number of days you have to wait after day i to get a warmer temperature. If no such day exists, set answer[i] = 0.
Examples
Constraints
- 1 <= temperatures.length <= 10^5
- 30 <= temperatures[i] <= 100
Approach Overview
Brute Force
Java: For each i, scan forward
Python: Forward scan
Optimal #1
Java: Monotonic decreasing stack of indices
Python: Monotonic stack
Optimal #2
Java: Backward sweep with jump pointers
Python: Backward sweep with jump
Java Solutions
For each i, scan forward O(n²) O(1)
Quadratic — works but TLEs on large inputs.
Pseudo-code
for i: for j in i+1..n-1: if temps[j] > temps[i]: ans[i] = j - i; break
Complexity
Edge cases & gotchas
- TLEs at n=10^5.
Java code
class Solution {
public int[] dailyTemperatures(final int[] t) {
final int n = t.length;
final int[] out = new int[n];
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (t[j] > t[i]) { out[i] = j - i; break; }
return out;
}
}
Monotonic decreasing stack of indices O(n) O(n)
Stack stores indices whose temperature has not yet been beaten. On day i with temp[i] > temp[stack.top]: pop, record gap.
Pseudo-code
stack = empty; out = [0]*n
for i, t in enumerate(temps):
while stack and t > temps[stack.top]:
j = stack.pop(); out[j] = i - j
stack.push(i)
return out
Complexity
Edge cases & gotchas
- Strict > not >= — equal temperatures don't qualify as 'warmer'.
- Indices left on the stack at the end have answer 0 — already initialized.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int[] dailyTemperatures(final int[] t) {
final int n = t.length;
final int[] out = new int[n];
final Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && t[i] > t[stack.peek()]) {
final int j = stack.pop();
out[j] = i - j;
}
stack.push(i);
}
return out;
}
public static void main(String[] args) {
System.out.println(java.util.Arrays.toString(
new Solution().dailyTemperatures(new int[]{73,74,75,71,69,72,76,73}))); // [1,1,4,2,1,1,0,0]
}
}
Backward sweep with jump pointers O(n) amortized O(1) extra
Walk right-to-left. For each i, scan forward but JUMP using already-computed answers — skips already-processed runs.
Pseudo-code
for i from n-1 down to 0:
j = i + 1
while j < n and temps[j] <= temps[i]:
if out[j] == 0: j = n; break # no warmer day exists
j += out[j] # skip ahead by the warmer-day gap
if j < n: out[i] = j - i
Complexity
Edge cases & gotchas
- Slightly trickier to verify O(n); rests on the path-compression-like skip.
- No allocation beyond the output — useful in space-tight contexts.
Java code
class Solution {
public int[] dailyTemperatures(final int[] t) {
final int n = t.length;
final int[] out = new int[n];
for (int i = n - 1; i >= 0; i--) {
int j = i + 1;
while (j < n && t[j] <= t[i]) {
if (out[j] == 0) { j = n; break; }
j += out[j];
}
if (j < n) out[i] = j - i;
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) amortized |
| Space | O(1) | O(n) | O(1) extra |
| Difficulty | 1/5 | 3/5 — monotonic stack invariant | 4/5 — amortization argument |
| Constant factor | Worst | Best — single push/pop per index | Slightly worse — but no allocation |
| Readability | Best for juniors | Best engineered | Trickiest |
| When to use | Tiny n | Default — ship-it | Memory-tight; advanced answer |
| PE Verdict | Monotonic stack is the canonical answer to every 'next greater element' problem. Memorize the template — it's exactly the same pattern as 'Next Greater Element', 'Largest Rectangle in Histogram', and 'Sliding Window Maximum' (with deque). | ||
Python Solutions
Forward scan O(n²) O(1)
Quadratic baseline.
Pseudo-code
nested loops
Complexity
Python code
from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
out = [0] * n
for i in range(n):
for j in range(i + 1, n):
if temperatures[j] > temperatures[i]:
out[i] = j - i
break
return out
Monotonic stack O(n) O(n)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
"""Monotonic decreasing stack of indices."""
n = len(temperatures)
out = [0] * n
stack: List[int] = []
for i, t in enumerate(temperatures):
while stack and t > temperatures[stack[-1]]:
j = stack.pop()
out[j] = i - j
stack.append(i)
return out
if __name__ == "__main__":
print(Solution().dailyTemperatures([73,74,75,71,69,72,76,73])) # [1,1,4,2,1,1,0,0]
Backward sweep with jump O(n) amortized O(1) extra
Same as Java optimal #2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
out = [0] * n
for i in range(n - 1, -1, -1):
j = i + 1
while j < n and temperatures[j] <= temperatures[i]:
if out[j] == 0:
j = n
break
j += out[j]
if j < n:
out[i] = j - i
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) amortized |
| Space | O(1) | O(n) | O(1) extra |
| Difficulty | 1/5 | 3/5 | 4/5 |
| When to use | Demo | Default | Memory-tight |
| PE Verdict | Monotonic stack — same as Java. Template generalizes to all next-greater-element style problems. | ||
What the interviewer is really testing
Monotonic stack is one of THE templates worth memorizing. PE signal: you recognize 'next greater' instantly, you state the invariant ('stack indices are in strictly decreasing temperature order'), and you generalize to the family (NGE, NSE, histogram, etc.).
Top gotchas
- Strict `>` not `>=` — same temperature doesn't qualify as warmer.
- Push every index, even if you don't pop anything that step.
- Indices remaining at the end correspond to days with no warmer follow-up — answer stays 0.
- Stack stores INDICES, not temperatures — you need indices to compute the gap.
Ship-it
Monotonic decreasing-by-temperature stack of indices, both languages.
#25 LC 853 Medium Car Fleet
Sort by start, then a backward sweep over arrival times — fleets form when a slower car blocks a faster one.
Problem Statement
There are n cars on a one-lane road heading to target. Each car has position position[i] and constant speed speed[i]. A faster car catching up to a slower one joins it and they move together as a fleet at the slower speed. Return the number of car fleets that arrive at the target.
Examples
Constraints
- n == position.length == speed.length
- 1 <= n <= 10^5
- 0 < target <= 10^6, 0 <= position[i] < target, all positions distinct
- 0 < speed[i] <= 10^6
Approach Overview
Brute Force
Java: Pairwise simulation
Python: Pairwise simulation
Optimal #1
Java: Sort by position desc, monotonic stack of arrival times
Python: Sort + arrival-time monotonic check
Optimal #2
Java: Same algorithm, fixed-point times to avoid float comparison
Python: Cross-multiplication variant
Java Solutions
Pairwise simulation O(n² log n) O(n)
Track each car; advance time, merge on collision. Quadratic and fragile.
Pseudo-code
simulate each car arrival; count surviving distinct-arrival groups
Complexity
Java code
class Solution {
public int carFleet(int target, int[] position, int[] speed) {
// Conceptual baseline only — see optimal for the right approach.
return 0;
}
}
Sort by position desc, monotonic stack of arrival times O(n log n) O(n)
Sort cars by position descending (closest to target first). For each, compute arrival time t = (target - pos) / speed. Push onto stack only if its arrival time is STRICTLY GREATER than the stack top — otherwise it joins that fleet.
Pseudo-code
pairs = list((position[i], speed[i])) sorted by position desc
stack = []
for (p, s) in pairs:
t = (target - p) / s
if stack empty or t > stack.top: stack.push(t)
return stack.size
Complexity
Edge cases & gotchas
- Compare arrival times with strict > — equal times mean the trailing car catches up exactly at the target, joining the fleet.
- Use double for arrival time; the divisions don't repeat-compute.
- Sort descending so cars closer to target are processed first — they form the leading fleet.
Java code
import java.util.*;
class Solution {
public int carFleet(final int target, final int[] position, final int[] speed) {
final int n = position.length;
final double[][] cars = new double[n][2];
for (int i = 0; i < n; i++) { cars[i][0] = position[i]; cars[i][1] = speed[i]; }
// Sort by position descending — closest to target first
Arrays.sort(cars, (a, b) -> Double.compare(b[0], a[0]));
int fleets = 0;
double leadTime = 0.0;
for (final double[] c : cars) {
final double arrival = (target - c[0]) / c[1];
if (arrival > leadTime) {
leadTime = arrival;
fleets++;
}
}
return fleets;
}
public static void main(String[] args) {
System.out.println(new Solution().carFleet(12, new int[]{10,8,0,5,3}, new int[]{2,4,1,1,3})); // 3
}
}
Same algorithm, fixed-point times to avoid float comparison O(n log n) O(n)
Compare (target - p1) * s2 vs (target - p2) * s1 using cross-multiplication — no floats.
Pseudo-code
for sorted-by-position-desc:
if leadDist * speed > leadSpeed * dist: new fleet
Complexity
Edge cases & gotchas
- Avoids floating-point precision issues — useful in competitive contexts.
- Cross-multiplication can overflow if target * speed approaches INT_MAX — use long.
Java code
import java.util.*;
class Solution {
public int carFleet(final int target, final int[] position, final int[] speed) {
final int n = position.length;
final Integer[] idx = new Integer[n];
for (int i = 0; i < n; i++) idx[i] = i;
Arrays.sort(idx, (a, b) -> Integer.compare(position[b], position[a]));
int fleets = 0;
long leadDist = 0, leadSpeed = 1; // virtual lead: arrival time = leadDist / leadSpeed
for (final int i : idx) {
final long dist = (long)(target - position[i]);
final long sp = speed[i];
// car's arrival = dist/sp; lead's arrival = leadDist/leadSpeed
// car arrives strictly later iff dist * leadSpeed > leadDist * sp
if (dist * leadSpeed > leadDist * sp) {
leadDist = dist; leadSpeed = sp;
fleets++;
}
}
return fleets;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·log n) | O(n log n) | O(n log n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | — | 3/5 | 4/5 |
| Float precision | — | Acceptable at problem scale | Bullet-proof |
| Constant factor | Worst | Best for typical inputs | Slightly more arithmetic per step |
| When to use | Don't | Default | Adversarial precision concerns / huge target |
| PE Verdict | Sort-by-position-desc + arrival-time monotonic check. The float precision is fine at LeetCode scale; reach for cross-multiplication only if the spec hints at adversarial inputs. | ||
Python Solutions
Pairwise simulation — —
Conceptual baseline.
Pseudo-code
—
Complexity
Python code
class Solution:
def carFleet(self, target: int, position, speed):
# See optimal — pairwise simulation is the wrong shape.
return 0
Sort + arrival-time monotonic check O(n log n) O(n)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Use sorted with reverse=True; zip position and speed.
Python code
from typing import List
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
# Sort by position desc — closest car to target leads
cars = sorted(zip(position, speed), key=lambda c: -c[0])
fleets = 0
lead_time = 0.0
for p, s in cars:
arrival = (target - p) / s
if arrival > lead_time:
lead_time = arrival
fleets += 1
return fleets
if __name__ == "__main__":
print(Solution().carFleet(12, [10,8,0,5,3], [2,4,1,1,3])) # 3
Cross-multiplication variant O(n log n) O(n)
Same as Java optimal #2.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Python ints unbounded — no overflow worries (rare CPython advantage).
Python code
from typing import List
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
cars = sorted(zip(position, speed), key=lambda c: -c[0])
fleets = 0
lead_dist, lead_speed = 0, 1
for p, s in cars:
dist = target - p
# lead's arrival time = lead_dist/lead_speed; car's = dist/s.
# Car arrives strictly later iff dist*lead_speed > lead_dist*s.
if dist * lead_speed > lead_dist * s:
lead_dist, lead_speed = dist, s
fleets += 1
return fleets
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | — | O(n log n) | O(n log n) |
| Space | — | O(n) | O(n) |
| Difficulty | — | 3/5 | 3/5 (Python ints simplify) |
| Float precision | — | Acceptable | Exact — Python ints unbounded |
| When to use | — | Default | When precision matters |
| PE Verdict | Sort by position desc + arrival-time check. Python's exact integer arithmetic makes the cross-multiplication variant trivially safe. | ||
What the interviewer is really testing
Coordinate compression + monotonic logic. PE signal: you recognize 'fleet boundary = arrival-time strictly greater than the lead', you sort descending by position so the first item is always the lead, and you discuss the float-precision-vs-cross-multiplication trade-off.
Top gotchas
- Sort DESCENDING by position — the closest car to target is the natural fleet lead.
- Use STRICT `>` when comparing arrival times — equal times means catching up exactly at target, which counts as the same fleet.
- Float precision is acceptable at LeetCode scale; for adversarial cases use cross-multiplication.
- Watch for zero-distance: a car already AT target has arrival 0; the strict-> handles this naturally.
Ship-it
Sort by position desc, monotonic max of arrival time. Both languages.
#26 LC 84 Hard Largest Rectangle in Histogram
Monotonic stack to find the largest rectangle — the apex of stack-based optimization.
Problem Statement
Given an array heights representing the heights of histogram bars (each 1 unit wide), return the area of the largest rectangle that can be formed within the histogram.
Examples
Constraints
- 1 <= heights.length <= 10^5
- 0 <= heights[i] <= 10^4
Approach Overview
Brute Force
Java: For each bar, expand left and right
Python: Expand around each bar
Optimal #1
Java: Monotonic increasing stack of indices, single pass with sentinel
Python: Single-pass monotonic stack with sentinel
Optimal #2
Java: Two passes: precompute leftSmaller and rightSmaller arrays
Python: Two passes: nearest smaller left/right
Java Solutions
For each bar, expand left and right O(n²) O(1)
Quadratic — for each bar i, find the maximal contiguous block where heights[j] >= heights[i].
Pseudo-code
for i: extend l, r while heights[l-1], heights[r+1] >= heights[i]
area = heights[i] * (r - l + 1)
Complexity
Edge cases & gotchas
- TLEs at n=10^5.
Java code
class Solution {
public int largestRectangleArea(final int[] h) {
int best = 0;
for (int i = 0; i < h.length; i++) {
int l = i, r = i;
while (l > 0 && h[l-1] >= h[i]) l--;
while (r < h.length - 1 && h[r+1] >= h[i]) r++;
best = Math.max(best, h[i] * (r - l + 1));
}
return best;
}
}
Monotonic increasing stack of indices, single pass with sentinel O(n) O(n)
Stack maintains indices in non-decreasing height order. On a smaller incoming bar, pop and compute area for each popped bar (it can extend right to current index, left to the new top of stack).
Pseudo-code
stack = empty; best = 0
for i in 0..n (treat n as sentinel height = 0):
h = heights[i] if i < n else 0
while stack and heights[stack.top] > h:
idx = stack.pop()
width = stack empty ? i : i - stack.top - 1
best = max(best, heights[idx] * width)
stack.push(i)
Complexity
Edge cases & gotchas
- Sentinel iteration at i = n with virtual height 0 forces the stack to drain at the end.
- Width when stack becomes empty after pop is `i` (extends from index 0).
- Equal heights: use strict >, so equal heights don't pop — we compute area for the rightmost in the run.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
/**
* Monotonic increasing stack of indices. The +1 sentinel iteration at
* i == n flushes the remaining stack with height 0.
*/
public int largestRectangleArea(final int[] heights) {
final int n = heights.length;
final Deque<Integer> stack = new ArrayDeque<>();
int best = 0;
for (int i = 0; i <= n; i++) {
final int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && heights[stack.peek()] > h) {
final int idx = stack.pop();
final int width = stack.isEmpty() ? i : i - stack.peek() - 1;
best = Math.max(best, heights[idx] * width);
}
stack.push(i);
}
return best;
}
public static void main(String[] args) {
System.out.println(new Solution().largestRectangleArea(new int[]{2,1,5,6,2,3})); // 10
}
}
Two passes: precompute leftSmaller and rightSmaller arrays O(n) O(n)
leftSmaller[i] = index of nearest smaller bar to the left; rightSmaller[i] symmetric. Width = right - left - 1.
Pseudo-code
leftSmaller[i] = nearest j < i with heights[j] < heights[i] (or -1) rightSmaller[i] = nearest j > i with heights[j] < heights[i] (or n) best = max(heights[i] * (rightSmaller[i] - leftSmaller[i] - 1))
Complexity
Edge cases & gotchas
- Boundary sentinels: -1 and n.
- Strict-less comparisons in both directions.
- Reads more like 'find next smaller' templates — useful for teaching, slightly more code.
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int largestRectangleArea(final int[] heights) {
final int n = heights.length;
final int[] left = new int[n], right = new int[n];
final Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) stack.pop();
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
for (int i = n - 1; i >= 0; i--) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) stack.pop();
right[i] = stack.isEmpty() ? n : stack.peek();
stack.push(i);
}
int best = 0;
for (int i = 0; i < n; i++) best = Math.max(best, heights[i] * (right[i] - left[i] - 1));
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(n) | O(n) |
| Difficulty | 2/5 | 5/5 — sentinel + width formula | 4/5 — two passes more familiar |
| Constant factor | Worst | Best — single pass | Slightly worse — three passes |
| Readability | Best for juniors | Hardest | More didactic — clearer intent per pass |
| Generalizes | Limited | To 'rectangle in 2D matrix' (LC 85) | Same |
| When to use | Tiny n | Production / interview ship-it | Whiteboard if you can't fit the single-pass logic |
| PE Verdict | Single-pass monotonic stack with sentinel — the canonical answer. Memorize the width formula `i - stack.top - 1` (or `i` when stack empty). The two-pass variant is the right answer if you need to also output 'next smaller' indices for downstream code. | ||
Python Solutions
Expand around each bar O(n²) O(1)
Quadratic.
Pseudo-code
expand l, r for each bar
Complexity
Python code
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
best = 0
for i in range(n):
l = r = i
while l > 0 and heights[l-1] >= heights[i]: l -= 1
while r < n - 1 and heights[r+1] >= heights[i]: r += 1
best = max(best, heights[i] * (r - l + 1))
return best
Single-pass monotonic stack with sentinel O(n) O(n)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
"""
Monotonic increasing stack of indices.
Sentinel iteration at i == n with virtual height 0 flushes the stack.
"""
n = len(heights)
stack: List[int] = []
best = 0
for i in range(n + 1):
h = 0 if i == n else heights[i]
while stack and heights[stack[-1]] > h:
idx = stack.pop()
width = i if not stack else i - stack[-1] - 1
best = max(best, heights[idx] * width)
stack.append(i)
return best
if __name__ == "__main__":
print(Solution().largestRectangleArea([2,1,5,6,2,3])) # 10
print(Solution().largestRectangleArea([2,4])) # 4
Two passes: nearest smaller left/right O(n) O(n)
Same as Java optimal #2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
left = [-1] * n
right = [n] * n
stack: List[int] = []
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
left[i] = stack[-1] if stack else -1
stack.append(i)
stack.clear()
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
right[i] = stack[-1] if stack else n
stack.append(i)
return max(heights[i] * (right[i] - left[i] - 1) for i in range(n))
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(n) | O(n) |
| Difficulty | 2/5 | 5/5 | 4/5 |
| When to use | Demo | Default | Whiteboard fallback |
| PE Verdict | Single-pass with sentinel. Same template as the Java answer; the n+1 sentinel iteration is the cleanest way to flush the stack. | ||
What the interviewer is really testing
Largest Rectangle is the canonical 'monotonic stack with width tracking' problem. PE signal: you derive the width formula from scratch ('after pop, the remaining stack top is the left boundary; current i is the right boundary'), you handle the empty-stack case correctly (left boundary becomes -1), and you mention the sentinel trick (i = n with height 0) as the cleanest end-of-array flush.
Top gotchas
- Width when stack becomes empty after pop is `i` (extends all the way to index 0), NOT `i - 1`.
- Width when stack non-empty is `i - stack.top - 1` (exclusive on both sides because both bars are smaller).
- Sentinel iteration at i == n with virtual height 0 is the cleanest way to flush the stack.
- Use strict > in the pop condition; equal heights don't trigger a pop, leaving the rightmost in a run to compute area for the whole equal-height block.
Ship-it
Single-pass monotonic stack with end-of-array sentinel. Both languages.
Binary Search
#27 LC 704 Easy Binary Search
The textbook binary search — get the bounds and the loop invariant exactly right.
Problem Statement
Given a sorted array nums of distinct integers and a target value target, return the index of target if found, otherwise return -1. Must run in O(log n).
Examples
Constraints
- 1 <= nums.length <= 10^4
- -10^4 <= nums[i], target <= 10^4
- All values in nums are distinct, sorted in ascending order.
Approach Overview
Brute Force
Java: Linear scan
Python: Linear scan
Optimal #1
Java: Iterative binary search (closed interval [lo, hi])
Python: Iterative closed-interval binary search
Optimal #2
Java: Half-open interval [lo, hi)
Python: bisect_left + match check
Java Solutions
Linear scan O(n) O(1)
O(n) — violates the spec but useful baseline.
Pseudo-code
for i: if nums[i] == target return i
Complexity
Edge cases & gotchas
- Violates O(log n) requirement.
Java code
class Solution {
public int search(final int[] nums, final int target) {
for (int i = 0; i < nums.length; i++) if (nums[i] == target) return i;
return -1;
}
}
Iterative binary search (closed interval [lo, hi]) O(log n) O(1)
Standard form. Loop while lo <= hi; mid = lo + (hi - lo) / 2 to avoid overflow.
Pseudo-code
lo, hi = 0, n-1
while lo <= hi:
mid = lo + (hi - lo) / 2
if nums[mid] == target: return mid
elif nums[mid] < target: lo = mid + 1
else: hi = mid - 1
return -1
Complexity
Edge cases & gotchas
- lo + (hi - lo)/2 not (lo + hi)/2 — the latter overflows for large indices.
- Loop bounds matter: <= for closed interval, < for half-open.
- Update by mid + 1 / mid - 1 to make progress and avoid infinite loops.
Java code
class Solution {
public int search(final int[] nums, final int target) {
int lo = 0, hi = nums.length - 1;
while (lo <= hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
public static void main(String[] args) {
System.out.println(new Solution().search(new int[]{-1,0,3,5,9,12}, 9)); // 4
}
}
Half-open interval [lo, hi) O(log n) O(1)
Often easier to compose with other algorithms (lower_bound / upper_bound style). hi starts at n; loop while lo < hi.
Pseudo-code
lo, hi = 0, n
while lo < hi:
mid = lo + (hi - lo) / 2
if nums[mid] < target: lo = mid + 1
else: hi = mid # NOT mid - 1
if lo < n and nums[lo] == target: return lo
return -1
Complexity
Edge cases & gotchas
- Half-open form returns the LOWER BOUND of target — the smallest index where nums[i] >= target.
- After loop, must check that lo is in range AND equals target.
- Generalizes cleanly to 'first occurrence' / 'insertion point'.
Java code
class Solution {
public int search(final int[] nums, final int target) {
int lo = 0, hi = nums.length;
while (lo < hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] < target) lo = mid + 1;
else hi = mid;
}
return (lo < nums.length && nums[lo] == target) ? lo : -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 — bounds care | 3/5 — invariant care |
| Spec compliance | Violates | Meets | Meets |
| Generalizes | No | Direct equality match | Lower bound / insertion point / upper bound |
| When to use | Trivial demo | Default | Composable variants (kth element, count, etc.) |
| PE Verdict | Closed-interval form for direct search. Master both — half-open is the right answer the moment the question asks for 'insertion point' or 'first occurrence'. | ||
Python Solutions
Linear scan O(n) O(1)
O(n).
Pseudo-code
list scan
Complexity
Python code
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
for i, x in enumerate(nums):
if x == target:
return i
return -1
Iterative closed-interval binary search O(log n) O(1)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Python ints are unbounded — overflow is non-issue, but use the same idiom for portability.
Python code
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
if __name__ == "__main__":
print(Solution().search([-1,0,3,5,9,12], 9)) # 4
bisect_left + match check O(log n) O(1)
Use bisect.bisect_left from stdlib; check the returned index for equality.
Pseudo-code
i = bisect_left(nums, target); return i if i<n and nums[i]==target else -1
Complexity
Edge cases & gotchas
- bisect_left returns n when target > all elements — bounds-check before indexing.
Python code
from typing import List
from bisect import bisect_left
class Solution:
def search(self, nums: List[int], target: int) -> int:
i = bisect_left(nums, target)
return i if i < len(nums) and nums[i] == target else -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 1/5 |
| Constant factor | Worst | Pure Python loop overhead | Best — bisect is C |
| When to use | Demo | Whiteboard / show the technique | Production / hot path |
| PE Verdict | Use bisect_left in production Python — it's C-fast and idiomatic. Write the iterative form on a whiteboard to demonstrate that you understand the algorithm. | ||
What the interviewer is really testing
The classic 'is your loop invariant solid' interview probe. PE signal: you write `lo + (hi - lo) / 2` without prompting (overflow safe), you can recite the difference between closed and half-open intervals, and you can pivot to lower_bound / upper_bound variants for follow-ups.
Top gotchas
- Use `lo + (hi - lo) / 2` — not `(lo + hi) / 2` — to avoid integer overflow in Java.
- Closed [lo, hi]: while lo <= hi; update lo = mid+1, hi = mid-1.
- Half-open [lo, hi): while lo < hi; update lo = mid+1, hi = mid.
- Forgetting the +1 / -1 adjustments creates infinite loops on a single-element array.
Ship-it
Java: closed-interval iterative form. Python: bisect_left + equality check.
#28 LC 74 Medium Search a 2D Matrix
2D matrix as flattened sorted array — single binary search vs staircase walk.
Problem Statement
You are given an m × n integer matrix where each row is sorted ascending and each row's first element is greater than the previous row's last element. Given a target, return whether it exists in the matrix. Must run in O(log(mn)).
Examples
Constraints
- 1 <= m, n <= 100
- -10^4 <= matrix[i][j], target <= 10^4
Approach Overview
Brute Force
Java: Linear scan
Python: Linear scan
Optimal #1
Java: Single binary search treating matrix as 1D
Python: Single binary search 1D
Optimal #2
Java: Two-step: find row, then binary search within
Python: Two-step: bisect on first column, bisect within row
Java Solutions
Linear scan O(mn) O(1)
Walk every cell. O(mn).
Pseudo-code
for each cell: if equals target return true
Complexity
Edge cases & gotchas
- Violates spec.
Java code
class Solution {
public boolean searchMatrix(final int[][] m, final int t) {
for (int[] row : m) for (int x : row) if (x == t) return true;
return false;
}
}
Single binary search treating matrix as 1D O(log(mn)) O(1)
Indices [0, m·n). map idx → (idx/n, idx%n). True O(log mn).
Pseudo-code
lo, hi = 0, m*n - 1
while lo <= hi:
mid = lo + (hi-lo)/2
val = matrix[mid/n][mid%n]
if val == t: return true
elif val < t: lo = mid+1
else: hi = mid-1
Complexity
Edge cases & gotchas
- Use long for mid only if mn could exceed INT_MAX (rare, but mention).
- Empty matrix → return false; guard early.
Java code
class Solution {
public boolean searchMatrix(final int[][] matrix, final int target) {
if (matrix.length == 0 || matrix[0].length == 0) return false;
final int m = matrix.length, n = matrix[0].length;
int lo = 0, hi = m * n - 1;
while (lo <= hi) {
final int mid = lo + (hi - lo) / 2;
final int v = matrix[mid / n][mid % n];
if (v == target) return true;
if (v < target) lo = mid + 1; else hi = mid - 1;
}
return false;
}
}
Two-step: find row, then binary search within O(log m + log n) O(1)
Binary-search on first column to find the row, then binary-search the row. Same O(log mn) total.
Pseudo-code
find row r where matrix[r][0] <= t < matrix[r+1][0]; binary search row
Complexity
Edge cases & gotchas
- Same asymptote as Optimal #1; subtly more boundary care.
- Two-step generalizes to weaker invariants (only rows sorted, not strict cross-row ordering).
Java code
class Solution {
public boolean searchMatrix(final int[][] matrix, final int target) {
final int m = matrix.length;
if (m == 0) return false;
final int n = matrix[0].length;
if (n == 0) return false;
// Step 1: find candidate row
int lo = 0, hi = m - 1;
while (lo < hi) {
final int mid = lo + (hi - lo + 1) / 2;
if (matrix[mid][0] <= target) lo = mid; else hi = mid - 1;
}
if (matrix[lo][0] > target) return false;
// Step 2: search the row
int l = 0, r = n - 1;
while (l <= r) {
final int mid = l + (r - l) / 2;
if (matrix[lo][mid] == target) return true;
if (matrix[lo][mid] < target) l = mid + 1; else r = mid - 1;
}
return false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(mn) | O(log mn) | O(log m + log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 4/5 — two-stage boundary care |
| Spec compliance | Violates | Meets | Meets |
| Cleanliness | Verbose | Cleanest | More boundary cases |
| Generalizes | No | Requires strict cross-row order | Loosens to per-row sorted (LC 240 variant) |
| When to use | Demo | Default — strict cross-row invariant | Looser invariant, follow-ups |
| PE Verdict | Single binary search treating the matrix as a flattened sorted array. Cleanest code, optimal asymptote, easy to verify. Two-step is the right answer if the invariant is relaxed (rows independently sorted but cross-row order weaker). | ||
Python Solutions
Linear scan O(mn) O(1)
O(mn).
Pseudo-code
any(t in row for row in matrix)
Complexity
Python code
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
return any(target in row for row in matrix)
Single binary search 1D O(log mn) O(1)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if not matrix or not matrix[0]:
return False
m, n = len(matrix), len(matrix[0])
lo, hi = 0, m * n - 1
while lo <= hi:
mid = (lo + hi) // 2
v = matrix[mid // n][mid % n]
if v == target:
return True
elif v < target:
lo = mid + 1
else:
hi = mid - 1
return False
if __name__ == "__main__":
M = [[1,3,5,7],[10,11,16,20],[23,30,34,60]]
print(Solution().searchMatrix(M, 3)) # True
print(Solution().searchMatrix(M, 13)) # False
Two-step: bisect on first column, bisect within row O(log m + log n) O(1)
Same idea as Java optimal #2 using bisect.
Pseudo-code
row = bisect_right(first_col, target) - 1; bisect inside that row
Complexity
Python code
from typing import List
from bisect import bisect_right, bisect_left
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if not matrix or not matrix[0]:
return False
first_col = [row[0] for row in matrix]
r = bisect_right(first_col, target) - 1
if r < 0:
return False
row = matrix[r]
i = bisect_left(row, target)
return i < len(row) and row[i] == target
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(mn) | O(log mn) | O(log m + log n) |
| Space | O(1) | O(1) | O(m) first-col copy |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Constant factor | Worst | Pure Python loop | Best — bisect is C |
| When to use | Demo | Default | Production, large matrix |
| PE Verdict | Single 1D binary search — same idiom as Java. Bisect-based two-step is faster constant-factor for large matrices but requires the first-column copy. | ||
What the interviewer is really testing
Tests whether you abstract a 2D problem into 1D using the row-major mapping. PE signal: you propose the flatten-to-1D binary search instantly and you discuss when the two-step approach matters (relaxed invariant).
Top gotchas
- Index mapping: row = idx / n, col = idx % n. Don't transpose accidentally.
- Empty matrix or empty row guards — first thing to write.
- Strict cross-row invariant (each row's first > previous row's last) is what makes the 1D treatment correct.
Ship-it
Single binary search over the flattened index space. Both languages.
#29 LC 875 Medium Koko Eating Bananas
Binary search on the answer — the canonical template for 'minimum k such that f(k) ≤ H'.
Problem Statement
Koko has piles of bananas. The i-th pile has piles[i] bananas. The guards return in h hours. Each hour, Koko picks a pile and eats up to k bananas (if pile has fewer, she finishes and moves on next hour). Return the MINIMUM integer k such that she can finish all piles within h hours.
Examples
Constraints
- 1 <= piles.length <= 10^4
- piles.length <= h <= 10^9
- 1 <= piles[i] <= 10^9
Approach Overview
Brute Force
Java: Linear scan k = 1, 2, ...
Python: Linear scan k
Optimal #1
Java: Binary search on k in [1, max(piles)]
Python: Binary search on k
Optimal #2
Java: Binary search with tighter lower bound
Python: Tighter lower bound
Java Solutions
Linear scan k = 1, 2, ... O(max(piles) · n) O(1)
Try every k from 1 upward; first k where total hours ≤ h wins.
Pseudo-code
for k = 1..max(piles): if hours(k) <= h return k
Complexity
Edge cases & gotchas
- TLEs at piles[i] = 10^9.
Java code
class Solution {
public int minEatingSpeed(final int[] piles, final int h) {
int max = 0;
for (int p : piles) if (p > max) max = p;
for (int k = 1; k <= max; k++) {
long hours = 0;
for (int p : piles) hours += (p + k - 1) / k;
if (hours <= h) return k;
}
return max;
}
}
Binary search on k in [1, max(piles)] O(n log max(piles)) O(1)
hours(k) is monotonic non-increasing in k. Search for the smallest k with hours(k) ≤ h.
Pseudo-code
lo, hi = 1, max(piles)
while lo < hi:
mid = lo + (hi-lo)/2
if hours(mid) <= h: hi = mid
else: lo = mid + 1
return lo
Complexity
Edge cases & gotchas
- Use ceiling division: (p + k - 1) / k. NOT (p / k) + 1.
- Hours can overflow int if n=10^4 and piles[i]=10^9 — use long for accumulator.
- lo = 1 because k = 0 would mean infinite hours.
Java code
class Solution {
/**
* Binary search on the answer. hours(k) is non-increasing in k, so we
* search for the smallest k with hours(k) <= h.
*/
public int minEatingSpeed(final int[] piles, final int h) {
int lo = 1, hi = 0;
for (final int p : piles) if (p > hi) hi = p;
while (lo < hi) {
final int mid = lo + (hi - lo) / 2;
if (canFinish(piles, mid, h)) hi = mid;
else lo = mid + 1;
}
return lo;
}
private boolean canFinish(final int[] piles, final int k, final int h) {
long hours = 0;
for (final int p : piles) {
hours += (p + k - 1) / k; // ceiling div
if (hours > h) return false; // early exit
}
return true;
}
public static void main(String[] args) {
System.out.println(new Solution().minEatingSpeed(new int[]{3,6,7,11}, 8)); // 4
}
}
Binary search with tighter lower bound O(n log max(piles)) O(1)
Lower bound = ceil(sum(piles) / h). She must average at least this fast. Tightens the search range.
Pseudo-code
lo = ceil(sum(piles) / h) hi = max(piles) binary search as Optimal #1
Complexity
Edge cases & gotchas
- sum(piles) can be ~10^13 — use long.
- Tighter bound saves some iterations but doesn't change the asymptote.
Java code
class Solution {
public int minEatingSpeed(final int[] piles, final int h) {
long sum = 0; int max = 0;
for (final int p : piles) { sum += p; if (p > max) max = p; }
int lo = (int) ((sum + h - 1) / h); // tighter lower bound
int hi = max;
while (lo < hi) {
final int mid = lo + (hi - lo) / 2;
long hours = 0;
for (final int p : piles) hours += (p + mid - 1) / mid;
if (hours <= h) hi = mid; else lo = mid + 1;
}
return lo;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(max·n) | O(n log max) | O(n log max) tighter constant |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 — monotonicity proof | 3/5 |
| When to use | Tiny inputs | Default | Production / hot path |
| PE Verdict | Binary search on the answer with [1, max(piles)] bounds. The tighter sum/h lower bound is a nice optimization to mention but not necessary for correctness. | ||
Python Solutions
Linear scan k O(max·n) O(1)
Same shape — quadratic-ish.
Pseudo-code
for k from 1: if hours(k) <= h: return k
Complexity
Python code
from typing import List
from math import ceil
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
for k in range(1, max(piles) + 1):
if sum(ceil(p / k) for p in piles) <= h:
return k
return max(piles)
Binary search on k O(n log max) O(1)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Use -(-p // k) for ceil division — avoids float.
Python code
from typing import List
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
def hours(k: int) -> int:
return sum(-(-p // k) for p in piles) # ceil div, no float
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
if hours(mid) <= h:
hi = mid
else:
lo = mid + 1
return lo
if __name__ == "__main__":
print(Solution().minEatingSpeed([3,6,7,11], 8)) # 4
print(Solution().minEatingSpeed([30,11,23,4,20], 5)) # 30
Tighter lower bound O(n log max) O(1)
Same as Java optimal #2.
Pseudo-code
lo = ceil(sum(piles)/h)
Complexity
Python code
from typing import List
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
total = sum(piles)
lo = -(-total // h)
hi = max(piles)
while lo < hi:
mid = (lo + hi) // 2
if sum(-(-p // mid) for p in piles) <= h:
hi = mid
else:
lo = mid + 1
return lo
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(max·n) | O(n log max) | O(n log max) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When to use | Demo | Default | Production |
| PE Verdict | Binary search on the answer with [1, max(piles)] bounds. The tighter lower bound is a small constant-factor win. | ||
What the interviewer is really testing
'Binary search on the answer' is one of the most-tested templates. PE signal: you recognize the monotonicity instantly, you state the predicate ('hours(k) ≤ h'), and you handle ceiling division correctly without floats. Bonus: you mention the tighter-lower-bound optimization.
Top gotchas
- Ceiling division: (p + k - 1) / k in Java, -(-p // k) in Python. Plain p / k floors, missing the leftovers.
- Use long in Java for the hours accumulator — n = 10^4 × 10^9 overflows int.
- lo = 1 not 0 — k = 0 would mean infinite hours.
- Half-open binary search returning lo (smallest valid k) is the cleanest invariant.
Ship-it
Binary search on k in [1, max(piles)] with monotonicity-based shrink. Both languages.
#30 LC 153 Medium Find Minimum in Rotated Sorted Array
Modified binary search to find the rotation pivot — compare mid to right.
Problem Statement
Given a sorted array of distinct values rotated between 1 and n times, return the minimum element. Must run in O(log n).
Examples
Constraints
- n == nums.length
- 1 <= n <= 5000
- All values distinct.
Approach Overview
Brute Force
Java: Linear scan
Python: min(nums)
Optimal #1
Java: Binary search comparing mid to right
Python: Binary search vs right
Optimal #2
Java: Compare mid to first; handle non-rotated case explicitly
Python: Pre-check + binary search
Java Solutions
Linear scan O(n) O(1)
min over all elements.
Pseudo-code
min(nums)
Complexity
Edge cases & gotchas
- Violates spec.
Java code
class Solution {
public int findMin(final int[] nums) {
int m = Integer.MAX_VALUE;
for (int x : nums) if (x < m) m = x;
return m;
}
}
Binary search comparing mid to right O(log n) O(1)
If nums[mid] > nums[hi], min is in [mid+1, hi]. Else min is in [lo, mid]. Converges to the pivot.
Pseudo-code
lo, hi = 0, n-1
while lo < hi:
mid = lo + (hi-lo)/2
if nums[mid] > nums[hi]: lo = mid + 1
else: hi = mid
return nums[lo]
Complexity
Edge cases & gotchas
- Compare to RIGHT not LEFT — comparing to nums[lo] fails on non-rotated arrays (e.g., [1,2,3]).
- lo < hi (half-open) — exactly one element survives at convergence: the minimum.
- Distinct values are required; duplicates need a different approach (LC 154).
Java code
class Solution {
/**
* Binary search comparing mid to RIGHT (not left).
* Reason: a non-rotated array has nums[mid] < nums[hi] always — without
* the right comparison we'd incorrectly walk into the wrong half.
*/
public int findMin(final int[] nums) {
int lo = 0, hi = nums.length - 1;
while (lo < hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] > nums[hi]) lo = mid + 1;
else hi = mid;
}
return nums[lo];
}
public static void main(String[] args) {
System.out.println(new Solution().findMin(new int[]{3,4,5,1,2})); // 1
System.out.println(new Solution().findMin(new int[]{11,13,15,17})); // 11
}
}
Compare mid to first; handle non-rotated case explicitly O(log n) O(1)
Same idea but pre-checks if nums is already non-rotated.
Pseudo-code
if nums[0] <= nums[n-1]: return nums[0] // otherwise rotated — binary search for the inflection
Complexity
Edge cases & gotchas
- Pre-check is a small optimization — same asymptote.
- Marginally easier to reason about for less-experienced readers.
Java code
class Solution {
public int findMin(final int[] nums) {
if (nums[0] <= nums[nums.length - 1]) return nums[0];
int lo = 0, hi = nums.length - 1;
while (lo < hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] >= nums[0]) lo = mid + 1;
else hi = mid;
}
return nums[lo];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| Spec compliance | Violates | Meets | Meets |
| When to use | Demo | Default | Slightly clearer to juniors |
| PE Verdict | Compare mid to RIGHT, half-open binary search. Memorize this — comparing to LEFT or to nums[0] adds edge-case branches without simplifying anything. | ||
Python Solutions
min(nums) O(n) O(1)
C-fast but O(n).
Pseudo-code
min(nums)
Complexity
Python code
from typing import List
class Solution:
def findMin(self, nums: List[int]) -> int:
return min(nums)
Binary search vs right O(log n) O(1)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findMin(self, nums: List[int]) -> int:
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] > nums[hi]:
lo = mid + 1
else:
hi = mid
return nums[lo]
if __name__ == "__main__":
print(Solution().findMin([3,4,5,1,2])) # 1
print(Solution().findMin([4,5,6,7,0,1,2])) # 0
Pre-check + binary search O(log n) O(1)
Same as Java optimal #2.
Pseudo-code
early return on non-rotated
Complexity
Python code
from typing import List
class Solution:
def findMin(self, nums: List[int]) -> int:
if nums[0] <= nums[-1]:
return nums[0]
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] >= nums[0]:
lo = mid + 1
else:
hi = mid
return nums[lo]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When to use | min() demo | Default | Equivalent |
| PE Verdict | Compare mid to right. Same idiom as Java. | ||
What the interviewer is really testing
Modified binary search where the comparator is the trick. PE signal: you compare mid to RIGHT (not left), you justify the choice ('comparing to right handles the non-rotated case naturally'), and you pivot to the duplicate-allowed variant (LC 154) if asked.
Top gotchas
- Compare to RIGHT (nums[hi]), not LEFT — left fails on non-rotated arrays.
- Strict > on the comparison — equal would need a different rule (skipping right) for the duplicate variant.
- Half-open lo < hi; converge to a single index without ever testing a specific value (no `nums[mid] == ...`).
- Distinct-values constraint is essential here. Duplicates require LC 154's conservative shrink.
Ship-it
Half-open binary search comparing nums[mid] to nums[hi]. Both languages.
#31 LC 33 Medium Search in Rotated Sorted Array
Binary search in a rotated array — exactly one half is sorted at every step.
Problem Statement
Given a rotated sorted array of distinct values nums and a target target, return the index of target or -1. Must run in O(log n).
Examples
Constraints
- 1 <= nums.length <= 5000
- All values distinct.
Approach Overview
Brute Force
Java: Linear scan
Python: Linear scan
Optimal #1
Java: Single-pass binary search with sorted-half detection
Python: Single-pass binary search
Optimal #2
Java: Two-step: find pivot, then standard binary search
Python: Two-step pivot + binary search
Java Solutions
Linear scan O(n) O(1)
O(n).
Pseudo-code
scan
Complexity
Edge cases & gotchas
- Violates spec.
Java code
class Solution {
public int search(final int[] nums, final int target) {
for (int i = 0; i < nums.length; i++) if (nums[i] == target) return i;
return -1;
}
}
Single-pass binary search with sorted-half detection O(log n) O(1)
At each step, exactly one of [lo..mid] and [mid..hi] is sorted. Determine which, then check whether target falls in that half.
Pseudo-code
while lo <= hi:
mid = lo + (hi-lo)/2
if nums[mid] == target: return mid
if nums[lo] <= nums[mid]: # left half sorted
if nums[lo] <= target < nums[mid]: hi = mid-1
else: lo = mid+1
else: # right half sorted
if nums[mid] < target <= nums[hi]: lo = mid+1
else: hi = mid-1
Complexity
Edge cases & gotchas
- Use <= when comparing nums[lo] to nums[mid] — the equality case (single element on left) means left is trivially sorted.
- Bounds in the inclusion check matter: < on the open side, <= on the sorted-end side.
- Distinct values are critical; duplicates need the LC 81 variant which can degrade to O(n).
Java code
class Solution {
/**
* Single-pass binary search. At each step exactly one half is sorted;
* inclusion test on that half decides which way to go.
*/
public int search(final int[] nums, final int target) {
int lo = 0, hi = nums.length - 1;
while (lo <= hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) { // left sorted
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
else lo = mid + 1;
} else { // right sorted
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
System.out.println(new Solution().search(new int[]{4,5,6,7,0,1,2}, 0)); // 4
}
}
Two-step: find pivot, then standard binary search O(log n) O(1)
Use the find-min-in-rotated approach to locate the pivot in O(log n); then run standard binary search on whichever half contains target.
Pseudo-code
pivot = findMinIndex(nums) if target >= nums[0]: search [0, pivot-1] else: search [pivot, n-1]
Complexity
Edge cases & gotchas
- Cleaner mental model — two well-known phases.
- Slightly higher constant factor (two passes).
Java code
class Solution {
public int search(final int[] nums, final int target) {
final int n = nums.length;
final int pivot = findMinIndex(nums);
// Search [0, pivot - 1] or [pivot, n - 1]
if (pivot == 0) return binSearch(nums, 0, n - 1, target);
if (target >= nums[0]) return binSearch(nums, 0, pivot - 1, target);
return binSearch(nums, pivot, n - 1, target);
}
private int findMinIndex(final int[] nums) {
int lo = 0, hi = nums.length - 1;
while (lo < hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] > nums[hi]) lo = mid + 1; else hi = mid;
}
return lo;
}
private int binSearch(final int[] nums, int lo, int hi, final int target) {
while (lo <= hi) {
final int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[mid] < target) lo = mid + 1; else hi = mid - 1;
}
return -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) (~2× constant) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 4/5 | 3/5 — composes two simpler routines |
| Code length | Shortest | Mid | Most lines but each line is simpler |
| Generalizes | No | Specific to rotated | Find-pivot reused; standard binary search reused |
| When to use | Demo | Default — single-pass elegance | Whiteboard if the merged version's branches feel risky |
| PE Verdict | Single-pass with sorted-half detection. The two-step approach is clearer to write under pressure but slightly slower; both are correct. | ||
Python Solutions
Linear scan O(n) O(1)
O(n).
Pseudo-code
scan
Complexity
Python code
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
try:
return nums.index(target)
except ValueError:
return -1
Single-pass binary search O(log n) O(1)
Same as Java optimal #1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]: # left sorted
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else: # right sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
if __name__ == "__main__":
print(Solution().search([4,5,6,7,0,1,2], 0)) # 4
print(Solution().search([4,5,6,7,0,1,2], 3)) # -1
Two-step pivot + binary search O(log n) O(1)
Same as Java optimal #2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from bisect import bisect_left
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
# Find pivot (index of min)
lo, hi = 0, n - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] > nums[hi]: lo = mid + 1
else: hi = mid
pivot = lo
# Decide which half target lives in
if pivot == 0 or target >= nums[0]:
l, r = 0, pivot - 1 if pivot > 0 else n - 1
else:
l, r = pivot, n - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target: return mid
if nums[mid] < target: l = mid + 1
else: r = mid - 1
return -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 4/5 | 3/5 |
| When to use | Demo | Default | Whiteboard fallback |
| PE Verdict | Single-pass with sorted-half detection. Same as Java. | ||
What the interviewer is really testing
Half-sorted-search is the canonical 'modified binary search' problem. PE signal: you state the invariant ('exactly one half is sorted'), you correctly handle the inclusion test (which boundary uses < vs <=), and you mention duplicates degrading to O(n) for LC 81.
Top gotchas
- Use `nums[lo] <= nums[mid]` not `<` — single-element left half is trivially sorted.
- Inclusion test boundary: nums[lo] <= target < nums[mid] for left, nums[mid] < target <= nums[hi] for right.
- Distinct-value constraint matters; duplicates allow nums[lo] == nums[mid] == nums[hi], which forces a conservative O(n) shrink.
- Closed interval lo <= hi to handle the single-element case.
Ship-it
Single-pass binary search with sorted-half detection. Both languages.
#32 LC 981 Medium Time Based Key-Value Store
Per-key sorted timestamp array — binary search to find the latest set <= t.
Problem Statement
Design a class TimeMap with: set(key, value, timestamp) stores the value at the given timestamp; get(key, timestamp) returns the value associated with the largest stored timestamp_prev ≤ timestamp, or empty string if none exists. Timestamps for a given key are guaranteed strictly increasing across calls.
Examples
Constraints
- 1 <= key.length, value.length <= 100
- 0 <= timestamp <= 10^7
- All timestamps for a given key are strictly increasing.
- At most 2*10^5 calls total.
Approach Overview
Brute Force
Java: Per-key list, linear scan from the end
Python: Per-key list, reverse linear scan
Optimal #1
Java: Per-key parallel arrays + binary search (rightmost ≤ t)
Python: Per-key (timestamps, values) + bisect
Optimal #2
Java: TreeMap.floorEntry per key
Python: SortedList from sortedcontainers (3rd-party)
Java Solutions
Per-key list, linear scan from the end set O(1), get O(n_key) O(total set calls)
Walk backwards from end; first timestamp <= t wins.
Pseudo-code
for entry in reversed(history[key]): if entry.t <= t: return entry.v
Complexity
Edge cases & gotchas
- TLEs at 2*10^5 sets on the same key.
Java code
import java.util.*;
class TimeMap {
private final Map<String, List<String[]>> map = new HashMap<>();
public void set(String k, String v, int t) {
map.computeIfAbsent(k, x -> new ArrayList<>()).add(new String[]{String.valueOf(t), v});
}
public String get(String k, int t) {
final List<String[]> hist = map.getOrDefault(k, Collections.emptyList());
for (int i = hist.size() - 1; i >= 0; i--) {
if (Integer.parseInt(hist.get(i)[0]) <= t) return hist.get(i)[1];
}
return "";
}
}
Per-key parallel arrays + binary search (rightmost ≤ t) set O(1) amortized; get O(log n_key) O(total)
Map<key, (timestamps, values)>. Set appends; get binary-searches the timestamps for the rightmost ≤ t. Strictly-increasing timestamps means no sort needed.
Pseudo-code
set(k, v, t): map[k].timestamps.append(t); map[k].values.append(v)
get(k, t):
list = map[k]
use binary search to find largest i with timestamps[i] <= t
return values[i] or '' if none
Complexity
Edge cases & gotchas
- Strictly-increasing timestamps per key means we can append without sorting.
- Binary search for 'largest ≤ t' = upper_bound(t) - 1 (exclusive).
- Empty result when t < first timestamp — return empty string.
Java code
import java.util.*;
class TimeMap {
private final Map<String, List<int[]>> stamps = new HashMap<>();
private final Map<String, List<String>> vals = new HashMap<>();
public void set(final String key, final String value, final int timestamp) {
stamps.computeIfAbsent(key, k -> new ArrayList<>()).add(new int[]{timestamp});
vals.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
public String get(final String key, final int timestamp) {
final List<int[]> ts = stamps.get(key);
if (ts == null || ts.isEmpty() || ts.get(0)[0] > timestamp) return "";
// Binary search: largest index i with ts[i] <= timestamp
int lo = 0, hi = ts.size() - 1, ans = -1;
while (lo <= hi) {
final int mid = lo + (hi - lo) / 2;
if (ts.get(mid)[0] <= timestamp) { ans = mid; lo = mid + 1; }
else hi = mid - 1;
}
return ans == -1 ? "" : vals.get(key).get(ans);
}
}
TreeMap.floorEntry per key set O(log n_key); get O(log n_key) O(total)
Use Java's TreeMap, which gives O(log n) floorEntry — exact 'rightmost ≤ t' for free. Most concise.
Pseudo-code
stamps[key] = TreeMap<int, String>(); get: stamps[key].floorEntry(t)
Complexity
Edge cases & gotchas
- TreeMap is a red-black tree — insertion is O(log n) vs append+constant for the parallel-array approach.
- Cleaner code; slightly slower constant factor for set.
- Python equivalent uses sortedcontainers.SortedList — not stdlib.
Java code
import java.util.*;
class TimeMap {
private final Map<String, TreeMap<Integer, String>> map = new HashMap<>();
public void set(final String key, final String value, final int t) {
map.computeIfAbsent(key, k -> new TreeMap<>()).put(t, value);
}
public String get(final String key, final int t) {
final TreeMap<Integer, String> tm = map.get(key);
if (tm == null) return "";
final Map.Entry<Integer, String> e = tm.floorEntry(t);
return e == null ? "" : e.getValue();
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| set | O(1) | O(1) amortized | O(log n) |
| get | O(n) | O(log n) | O(log n) |
| Space | O(total) | O(total) | O(total) + tree pointers |
| Difficulty | 1/5 | 3/5 | 2/5 — TreeMap does the work |
| Constant factor (set) | Best | Best | Worse — tree balancing |
| Constant factor (get) | Worst | Best — array access cache-friendly | Slower — pointer chasing |
| When to use | Tiny n | Default — fastest get | Concise code priority |
| PE Verdict | Per-key parallel arrays + binary search. Set is O(1), get is O(log n), zero tree-pointer overhead. Reach for TreeMap when code conciseness or generic ordered operations matter (lower bound, range queries, etc.). | ||
Python Solutions
Per-key list, reverse linear scan set O(1), get O(n) O(total)
Same shape — TLEs.
Pseudo-code
scan history[key] backwards
Complexity
Python code
from collections import defaultdict
class TimeMap:
def __init__(self):
self.data = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.data[key].append((timestamp, value))
def get(self, key: str, timestamp: int) -> str:
for t, v in reversed(self.data[key]):
if t <= timestamp:
return v
return ""
Per-key (timestamps, values) + bisect set O(1); get O(log n) O(total)
Same as Java optimal #1. Use bisect_right and step back by one.
Pseudo-code
i = bisect_right(stamps, t) - 1; return values[i] or ''
Complexity
Edge cases & gotchas
- bisect_right(t) - 1 = -1 if t < first timestamp; guard with `if i >= 0`.
- Strictly-increasing-per-key timestamps mean no sort is required.
Python code
from collections import defaultdict
from bisect import bisect_right
from typing import Dict, List, Tuple
class TimeMap:
def __init__(self) -> None:
self.stamps: Dict[str, List[int]] = defaultdict(list)
self.values: Dict[str, List[str]] = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.stamps[key].append(timestamp)
self.values[key].append(value)
def get(self, key: str, timestamp: int) -> str:
ts = self.stamps.get(key)
if not ts:
return ""
i = bisect_right(ts, timestamp) - 1
return self.values[key][i] if i >= 0 else ""
if __name__ == "__main__":
tm = TimeMap()
tm.set("foo", "bar", 1)
print(tm.get("foo", 1)) # bar
print(tm.get("foo", 3)) # bar
tm.set("foo", "bar2", 4)
print(tm.get("foo", 4)) # bar2
print(tm.get("foo", 5)) # bar2
SortedList from sortedcontainers (3rd-party) set O(log n); get O(log n) O(total)
If sortedcontainers is allowed, SortedList provides O(log n) insertion in arbitrary order. Useful when timestamps aren't guaranteed monotone.
Pseudo-code
use SortedList of (t, v) tuples; bisect for lookup
Complexity
Edge cases & gotchas
- Not stdlib — only use if interviewer allows.
- Overkill if timestamps are guaranteed strictly increasing.
Python code
# Requires `pip install sortedcontainers`
from sortedcontainers import SortedList
from collections import defaultdict
from typing import Dict
class TimeMap:
def __init__(self) -> None:
self.data: Dict[str, SortedList] = defaultdict(SortedList)
def set(self, key: str, value: str, timestamp: int) -> None:
self.data[key].add((timestamp, value))
def get(self, key: str, timestamp: int) -> str:
sl = self.data.get(key)
if not sl:
return ""
i = sl.bisect_right((timestamp, chr(127))) - 1 # rightmost <= timestamp
return sl[i][1] if i >= 0 else ""
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| set | O(1) | O(1) | O(log n) |
| get | O(n) | O(log n) | O(log n) |
| Space | O(total) | O(total) | O(total) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Stdlib only? | Yes | Yes | No — needs sortedcontainers |
| When to use | Demo | Default | Non-monotone timestamps |
| PE Verdict | bisect on parallel lists. Stdlib, O(log n) get, exploits the monotone-timestamps invariant. SortedList is the right answer if that invariant is dropped. | ||
What the interviewer is really testing
Design + binary search composition. PE signal: you exploit the strictly-increasing-timestamps invariant to skip the sort, you use bisect / lower_bound correctly to find 'largest ≤ t', and you compare the parallel-array approach to TreeMap / SortedList with crisp constant-factor reasoning.
Top gotchas
- 'Largest ≤ t' = bisect_right(t) - 1, NOT bisect_left(t). bisect_left finds the insertion point at or BEFORE matching elements.
- When bisect_right returns 0, the target is smaller than all entries — return empty string.
- Strictly-increasing-per-key timestamps means we can append without sorting; without that guarantee, switch to SortedList / TreeMap.
- Java: Integer parsing inside the brute-force baseline allocates — avoid in hot paths.
Ship-it
Java: parallel ArrayLists + binary search. Python: bisect_right - 1 over parallel lists.
#33 LC 4 Hard Median of Two Sorted Arrays
Binary search on partition point — find the median of two sorted arrays in O(log min(m,n)).
Problem Statement
Given two sorted arrays nums1 and nums2, return the median of the combined array. Must run in O(log(m+n)).
Examples
Constraints
- 0 <= m, n <= 1000
- 1 <= m+n <= 2000
Approach Overview
Brute Force
Java: Merge and pick middle
Python: Merge and pick
Optimal #1
Java: Binary search on partition (smaller array)
Python: Partition binary search
Optimal #2
Java: K-th element via binary search (recursive)
Python: K-th element elimination
Java Solutions
Merge and pick middle O(m+n) O(m+n)
Merge into one sorted array; return middle element(s).
Pseudo-code
merge sorted; return middle
Complexity
Edge cases & gotchas
- Violates O(log) requirement.
Java code
class Solution {
public double findMedianSortedArrays(int[] a, int[] b) {
int m = a.length, n = b.length, total = m + n;
int[] merged = new int[total];
int i=0, j=0, k=0;
while (i<m && j<n) merged[k++] = a[i]<=b[j] ? a[i++] : b[j++];
while (i<m) merged[k++] = a[i++];
while (j<n) merged[k++] = b[j++];
if (total%2==1) return merged[total/2];
return (merged[total/2-1] + merged[total/2]) / 2.0;
}
}
Binary search on partition (smaller array) O(log min(m,n)) O(1)
Partition both arrays so left side has (m+n+1)/2 elements; binary search the cut in the smaller array.
Pseudo-code
ensure m <= n (swap if needed)
lo=0, hi=m
while lo <= hi:
i = (lo+hi)/2; j = (m+n+1)/2 - i
L1 = i==0 ? -inf : a[i-1]; R1 = i==m ? +inf : a[i]
L2 = j==0 ? -inf : b[j-1]; R2 = j==n ? +inf : b[j]
if L1 <= R2 and L2 <= R1:
if (m+n) even: return (max(L1,L2)+min(R1,R2))/2
else: return max(L1,L2)
elif L1 > R2: hi = i-1
else: lo = i+1
Complexity
Edge cases & gotchas
- Swap to make `a` smaller — bounds search complexity.
- Use INT_MIN/MAX as sentinels for edge partitions.
Java code
class Solution {
public double findMedianSortedArrays(int[] a, int[] b) {
if (a.length > b.length) { int[] t = a; a = b; b = t; }
int m = a.length, n = b.length, half = (m+n+1)/2;
int lo = 0, hi = m;
while (lo <= hi) {
int i = (lo+hi)/2, j = half - i;
int L1 = i==0 ? Integer.MIN_VALUE : a[i-1];
int R1 = i==m ? Integer.MAX_VALUE : a[i];
int L2 = j==0 ? Integer.MIN_VALUE : b[j-1];
int R2 = j==n ? Integer.MAX_VALUE : b[j];
if (L1 <= R2 && L2 <= R1) {
if ((m+n)%2 == 1) return Math.max(L1, L2);
return (Math.max(L1,L2) + Math.min(R1,R2)) / 2.0;
} else if (L1 > R2) hi = i - 1;
else lo = i + 1;
}
return 0.0;
}
}
K-th element via binary search (recursive) O(log(m+n)) O(log(m+n)) recursion
Reframe as 'find k-th smallest'; eliminate k/2 elements per step.
Pseudo-code
findKth(a, b, k): compare a[k/2-1] vs b[k/2-1]; discard k/2
Complexity
Edge cases & gotchas
- Recursion can be flattened to iteration.
Java code
class Solution {
public double findMedianSortedArrays(int[] a, int[] b) {
int t = a.length + b.length;
if (t%2==1) return findKth(a,0,b,0,t/2+1);
return (findKth(a,0,b,0,t/2) + findKth(a,0,b,0,t/2+1)) / 2.0;
}
private int findKth(int[] a, int ai, int[] b, int bi, int k) {
if (ai >= a.length) return b[bi+k-1];
if (bi >= b.length) return a[ai+k-1];
if (k == 1) return Math.min(a[ai], b[bi]);
int half = k/2;
int aVal = ai+half-1 < a.length ? a[ai+half-1] : Integer.MAX_VALUE;
int bVal = bi+half-1 < b.length ? b[bi+half-1] : Integer.MAX_VALUE;
if (aVal < bVal) return findKth(a, ai+half, b, bi, k-half);
return findKth(a, ai, b, bi+half, k-half);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m+n) | O(log min(m,n)) | O(log(m+n)) |
| Space | O(m+n) | O(1) | O(log(m+n)) |
| Difficulty | 1/5 | 5/5 — partition reasoning | 4/5 — k-th elim |
| Spec compliance | Violates | Meets | Meets |
| When to use | Demo | Default ship-it | Pedagogy |
| PE Verdict | Partition-based binary search on the smaller array. Hard but the canonical answer. | ||
Python Solutions
Merge and pick O(m+n) O(m+n)
Linear merge.
Pseudo-code
merge then index
Complexity
Python code
from typing import List
class Solution:
def findMedianSortedArrays(self, a: List[int], b: List[int]) -> float:
m = sorted(a + b); n = len(m)
return m[n//2] if n%2 else (m[n//2-1] + m[n//2]) / 2
Partition binary search O(log min(m,n)) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Use float('-inf') / float('inf') as sentinels.
Python code
from typing import List
class Solution:
def findMedianSortedArrays(self, a: List[int], b: List[int]) -> float:
if len(a) > len(b): a, b = b, a
m, n = len(a), len(b)
half = (m+n+1) // 2
lo, hi = 0, m
while lo <= hi:
i = (lo+hi)//2; j = half - i
L1 = a[i-1] if i>0 else float('-inf')
R1 = a[i] if i<m else float('inf')
L2 = b[j-1] if j>0 else float('-inf')
R2 = b[j] if j<n else float('inf')
if L1 <= R2 and L2 <= R1:
if (m+n)%2: return max(L1, L2)
return (max(L1,L2) + min(R1,R2)) / 2
elif L1 > R2: hi = i-1
else: lo = i+1
return 0.0
K-th element elimination O(log(m+n)) O(log(m+n))
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findMedianSortedArrays(self, a: List[int], b: List[int]) -> float:
def kth(ai, bi, k):
if ai >= len(a): return b[bi+k-1]
if bi >= len(b): return a[ai+k-1]
if k == 1: return min(a[ai], b[bi])
half = k//2
av = a[ai+half-1] if ai+half-1 < len(a) else float('inf')
bv = b[bi+half-1] if bi+half-1 < len(b) else float('inf')
if av < bv: return kth(ai+half, bi, k-half)
return kth(ai, bi+half, k-half)
t = len(a) + len(b)
if t%2: return kth(0,0,t//2+1)
return (kth(0,0,t//2) + kth(0,0,t//2+1)) / 2
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m+n) | O(log min(m,n)) | O(log(m+n)) |
| Space | O(m+n) | O(1) | O(log) |
| Difficulty | 1/5 | 5/5 | 4/5 |
| When to use | Demo | Default | Pedagogy |
| PE Verdict | Partition binary search — same as Java. | ||
What the interviewer is really testing
Hardest LC interview problem on partition reasoning. PE signal: search the SMALLER array, use ±∞ sentinels, derive `half = (m+n+1)/2` for even/odd unification.
Top gotchas
- Swap arrays so binary search runs on the smaller one — bounds the work.
- half = (m+n+1)/2 unifies even and odd cases via the +1.
- Use INT_MIN/INT_MAX (or ±inf) as sentinels for edge partitions.
Ship-it
Partition-based binary search on the smaller array.
Linked List
#34 LC 206 Easy Reverse Linked List
Reverse a singly-linked list — iterative pointer swap vs recursion.
Problem Statement
Given the head of a singly linked list, reverse the list and return the new head.
Examples
Constraints
- 0 <= n <= 5000
- -5000 <= Node.val <= 5000
Approach Overview
Brute Force
Java: Push to stack then rebuild
Python: List to array, reverse, rebuild
Optimal #1
Java: Iterative pointer reversal
Python: Iterative
Optimal #2
Java: Recursive reversal
Python: Recursive
Java Solutions
Push to stack then rebuild O(n) O(n)
Push all values, then build new list. Wasteful.
Pseudo-code
stack each node; pop to build new list
Complexity
Edge cases & gotchas
- Allocates n new nodes.
Java code
import java.util.*;
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public ListNode reverseList(ListNode head) {
Deque<Integer> s = new ArrayDeque<>();
for (ListNode c = head; c != null; c = c.next) s.push(c.val);
ListNode dummy = new ListNode(0), tail = dummy;
while (!s.isEmpty()) { tail.next = new ListNode(s.pop()); tail = tail.next; }
return dummy.next;
}
}
Iterative pointer reversal O(n) O(1)
Walk through, redirecting next pointers as you go. O(1) extra.
Pseudo-code
prev=null; cur=head
while cur:
next=cur.next; cur.next=prev; prev=cur; cur=next
return prev
Complexity
Edge cases & gotchas
- Empty list → returns null naturally.
- Save next BEFORE rewiring cur.next.
Java code
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null, cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
}
Recursive reversal O(n) O(n) recursion
Recurse to end, then rewire on the way back.
Pseudo-code
if not head or not head.next: return head newHead = reverse(head.next) head.next.next = head head.next = null return newHead
Complexity
Edge cases & gotchas
- Stack overflow for n > ~10^4.
Java code
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) stack |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When to use | Demo only | Default | Pedagogy / small lists |
| PE Verdict | Iterative — O(1) space, no stack overflow risk. | ||
Python Solutions
List to array, reverse, rebuild O(n) O(n)
Wasteful.
Pseudo-code
collect; rebuild reversed
Complexity
Python code
class ListNode:
def __init__(self, val=0, nxt=None): self.val=val; self.next=nxt
class Solution:
def reverseList(self, head):
vals = []
c = head
while c: vals.append(c.val); c = c.next
dummy = ListNode(); tail = dummy
for v in reversed(vals): tail.next = ListNode(v); tail = tail.next
return dummy.next
Iterative O(n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseList(self, head):
prev, cur = None, head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
Recursive O(n) O(n)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- sys.setrecursionlimit may be needed.
Python code
class Solution:
def reverseList(self, head):
if not head or not head.next: return head
new_head = self.reverseList(head.next)
head.next.next = head
head.next = None
return new_head
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When to use | Demo | Default | Pedagogy |
| PE Verdict | Iterative — same as Java. | ||
What the interviewer is really testing
Foundational LL pointer manipulation. PE signal: save next BEFORE rewiring; iterative beats recursive on space.
Top gotchas
- Save cur.next BEFORE overwriting cur.next = prev — otherwise you lose the rest of the list.
- Initialize prev = null; the new tail's next must be null.
- Recursive risks stack overflow for n > ~10^4 in Java (default 512KB stack).
Ship-it
Iterative pointer reversal, both languages.
#35 LC 21 Easy Merge Two Sorted Lists
Two-pointer merge into a single sorted linked list — dummy head pattern.
Problem Statement
Merge two sorted singly-linked lists into one sorted list by splicing nodes (no new allocations of values).
Examples
Constraints
- 0 <= total <= 100
- Each list is non-decreasing.
Approach Overview
Brute Force
Java: Collect values, sort, rebuild
Python: Collect + sort
Optimal #1
Java: Iterative splice with dummy head
Python: Iterative splice
Optimal #2
Java: Recursive merge
Python: Recursive
Java Solutions
Collect values, sort, rebuild O((m+n) log(m+n)) O(m+n)
Throws away the in-place advantage.
Pseudo-code
collect into list, sort, build new
Complexity
Edge cases & gotchas
- Wasteful — doesn't use sorted-ness.
Java code
import java.util.*;
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public ListNode mergeTwoLists(ListNode a, ListNode b) {
List<Integer> vals = new ArrayList<>();
while (a != null) { vals.add(a.val); a = a.next; }
while (b != null) { vals.add(b.val); b = b.next; }
Collections.sort(vals);
ListNode dummy = new ListNode(0), t = dummy;
for (int v : vals) { t.next = new ListNode(v); t = t.next; }
return dummy.next;
}
}
Iterative splice with dummy head O(m+n) O(1)
Walk both lists; splice the smaller into a growing tail. Dummy head removes special-case handling.
Pseudo-code
dummy = new node; tail = dummy
while l1 and l2:
if l1.val <= l2.val: tail.next = l1; l1 = l1.next
else: tail.next = l2; l2 = l2.next
tail = tail.next
tail.next = l1 != null ? l1 : l2
return dummy.next
Complexity
Edge cases & gotchas
- Dummy head obviates 'is this the first node?' branching.
- Append remaining tail directly — both lists are sorted, no further comparisons needed.
Java code
class Solution {
public ListNode mergeTwoLists(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), tail = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next;
}
tail.next = (a != null) ? a : b;
return dummy.next;
}
}
Recursive merge O(m+n) O(m+n) stack
Elegant but O(n) stack.
Pseudo-code
if !l1: return l2; if !l2: return l1 if l1.val <= l2.val: l1.next = merge(l1.next, l2); return l1 else: l2.next = merge(l1, l2.next); return l2
Complexity
Edge cases & gotchas
- Stack overflow risk on long lists.
Java code
class Solution {
public ListNode mergeTwoLists(ListNode a, ListNode b) {
if (a == null) return b;
if (b == null) return a;
if (a.val <= b.val) { a.next = mergeTwoLists(a.next, b); return a; }
b.next = mergeTwoLists(a, b.next); return b;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((m+n) log) | O(m+n) | O(m+n) |
| Space | O(m+n) | O(1) | O(m+n) stack |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When to use | Don't | Default | Code-golf |
| PE Verdict | Iterative with dummy head — O(1) extra, no recursion limit. | ||
Python Solutions
Collect + sort O((m+n) log) O(m+n)
Wasteful.
Pseudo-code
collect, sort, rebuild
Complexity
Python code
class ListNode:
def __init__(self, val=0, nxt=None): self.val=val; self.next=nxt
class Solution:
def mergeTwoLists(self, a, b):
vals = []
while a: vals.append(a.val); a = a.next
while b: vals.append(b.val); b = b.next
vals.sort()
dummy = ListNode(); t = dummy
for v in vals: t.next = ListNode(v); t = t.next
return dummy.next
Iterative splice O(m+n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def mergeTwoLists(self, a, b):
dummy = ListNode(); tail = dummy
while a and b:
if a.val <= b.val: tail.next = a; a = a.next
else: tail.next = b; b = b.next
tail = tail.next
tail.next = a or b
return dummy.next
Recursive O(m+n) O(m+n)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def mergeTwoLists(self, a, b):
if not a: return b
if not b: return a
if a.val <= b.val:
a.next = self.mergeTwoLists(a.next, b); return a
b.next = self.mergeTwoLists(a, b.next); return b
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((m+n) log) | O(m+n) | O(m+n) |
| Space | O(m+n) | O(1) | O(m+n) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When to use | Don't | Default | Pedagogy |
| PE Verdict | Iterative + dummy head. | ||
What the interviewer is really testing
Linked-list mechanics + dummy-head pattern. PE signal: dummy head, append remaining tail in one assignment.
Top gotchas
- Dummy head removes the 'first node?' branch — fold both languages around it.
- After the main loop, tail.next = a or b (whichever is non-null) appends the rest.
- DO NOT reset the unused list's next — splice means we reuse those nodes.
Ship-it
Iterative splice with dummy head.
#36 LC 141 Easy Linked List Cycle
Floyd's tortoise-and-hare cycle detection — O(1) space.
Problem Statement
Return true if a linked list contains a cycle.
Examples
Constraints
- 0 <= n <= 10^4
Approach Overview
Brute Force
Java: HashSet of visited
Python: set of visited
Optimal #1
Java: Floyd's tortoise and hare
Python: Floyd
Optimal #2
Java: Destructive: rewrite next pointers as you go
Python: Destructive sentinel
Java Solutions
HashSet of visited O(n) O(n)
Record each node; revisit → cycle.
Pseudo-code
for each node: if in set return true; add to set
Complexity
Java code
import java.util.*;
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<>();
for (ListNode c = head; c != null; c = c.next) {
if (!seen.add(c)) return true;
}
return false;
}
}
Floyd's tortoise and hare O(n) O(1)
Slow moves 1, fast moves 2. If they meet, cycle exists.
Pseudo-code
slow = fast = head
while fast and fast.next:
slow = slow.next; fast = fast.next.next
if slow == fast: return true
return false
Complexity
Edge cases & gotchas
- Check fast and fast.next BOTH non-null.
- Empty/single-node list → no cycle.
Java code
class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
}
Destructive: rewrite next pointers as you go O(n) O(1)
Set visited node.next to a sentinel/self. Detects cycle on hit.
Pseudo-code
for each node: if next == sentinel return true; else next = sentinel
Complexity
Edge cases & gotchas
- DESTRUCTIVE — mutates the list. Only acceptable if caller permits.
Java code
class Solution {
private static final ListNode VISITED = new ListNode(0);
public boolean hasCycle(ListNode head) {
while (head != null) {
if (head.next == VISITED) return true;
ListNode next = head.next;
head.next = VISITED;
head = next;
}
return false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| Mutates input | No | No | Yes |
| When to use | Read-only & memory-OK | Default | When mutation is allowed and memory is tight |
| PE Verdict | Floyd's algorithm — O(1) space and non-destructive. | ||
Python Solutions
set of visited O(n) O(n)
Same shape.
Pseudo-code
set membership
Complexity
Python code
class ListNode:
def __init__(self, val=0, nxt=None): self.val=val; self.next=nxt
class Solution:
def hasCycle(self, head) -> bool:
seen = set()
c = head
while c:
if id(c) in seen: return True
seen.add(id(c)); c = c.next
return False
Floyd O(n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def hasCycle(self, head) -> bool:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: return True
return False
Destructive sentinel O(n) O(1)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
SENTINEL = ListNode(0)
def hasCycle(self, head) -> bool:
while head:
if head.next is self.SENTINEL: return True
nxt = head.next; head.next = self.SENTINEL; head = nxt
return False
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When to use | Demo | Default | Memory-tight + mutable OK |
| PE Verdict | Floyd's algorithm. | ||
What the interviewer is really testing
Classic Floyd's algorithm. PE signal: O(1) space, articulate why they MUST meet inside a cycle.
Top gotchas
- Check fast AND fast.next non-null before fast.next.next.
- Use `is` (identity) in Python, not `==` (would call __eq__).
- If cycle length k, fast laps slow within k iterations after slow enters the cycle.
Ship-it
Floyd's tortoise and hare.
#37 LC 143 Medium Reorder List
Reorder L0→Ln→L1→Ln-1... — find middle, reverse second half, interleave.
Problem Statement
Reorder a linked list in place so that L0 → Ln-1 → L1 → Ln-2 → ...
Examples
Constraints
- 1 <= n <= 5*10^4
Approach Overview
Brute Force
Java: Array of nodes, two-pointer rebuild
Python: Node array + index rebuild
Optimal #1
Java: Find middle (slow/fast), reverse second half, merge
Python: Mid + reverse + merge
Optimal #2
Java: Deque-based interleave
Python: Deque interleave
Java Solutions
Array of nodes, two-pointer rebuild O(n) O(n)
Collect all nodes into an array, then re-link by indices.
Pseudo-code
arr = nodes; l=0, r=n-1; interleave; last.next = null
Complexity
Edge cases & gotchas
- Allocates array — opt1 is O(1) extra.
Java code
import java.util.*;
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public void reorderList(ListNode head) {
List<ListNode> a = new ArrayList<>();
for (ListNode c = head; c != null; c = c.next) a.add(c);
int l = 0, r = a.size()-1;
while (l < r) {
a.get(l).next = a.get(r);
l++;
if (l == r) break;
a.get(r).next = a.get(l);
r--;
}
a.get(l).next = null;
}
}
Find middle (slow/fast), reverse second half, merge O(n) O(1)
Three classic LL ops composed: midpoint, reverse, interleave.
Pseudo-code
1) slow/fast → slow ends at middle 2) reverse from slow.next; slow.next = null 3) merge first half with reversed second half
Complexity
Edge cases & gotchas
- Sever the first half at the midpoint BEFORE reversing.
- Odd n: first half is longer; merge handles it naturally.
Java code
class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// 1) find middle
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next; fast = fast.next.next;
}
// 2) reverse second half
ListNode second = slow.next;
slow.next = null;
ListNode prev = null, cur = second;
while (cur != null) {
ListNode nxt = cur.next; cur.next = prev; prev = cur; cur = nxt;
}
// 3) merge
ListNode a = head, b = prev;
while (b != null) {
ListNode an = a.next, bn = b.next;
a.next = b; b.next = an;
a = an; b = bn;
}
}
}
Deque-based interleave O(n) O(n)
Push all nodes to deque, alternate pollFirst/pollLast.
Pseudo-code
deque all nodes; alternate front/back, splice
Complexity
Edge cases & gotchas
- More memory; clearer code.
Java code
import java.util.*;
class Solution {
public void reorderList(ListNode head) {
Deque<ListNode> dq = new ArrayDeque<>();
for (ListNode c = head; c != null; c = c.next) dq.offer(c);
ListNode dummy = new ListNode(0), tail = dummy;
boolean front = true;
while (!dq.isEmpty()) {
tail.next = front ? dq.pollFirst() : dq.pollLast();
tail = tail.next;
front = !front;
}
tail.next = null;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 — three composed ops | 2/5 |
| When to use | Demo | Default | Code clarity |
| PE Verdict | Compose mid+reverse+merge for O(1) space. | ||
Python Solutions
Node array + index rebuild O(n) O(n)
Same.
Pseudo-code
indices rebuild
Complexity
Python code
class Solution:
def reorderList(self, head):
arr = []
c = head
while c: arr.append(c); c = c.next
l, r = 0, len(arr)-1
while l < r:
arr[l].next = arr[r]; l += 1
if l == r: break
arr[r].next = arr[l]; r -= 1
arr[l].next = None
Mid + reverse + merge O(n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reorderList(self, head) -> None:
if not head or not head.next: return
slow = fast = head
while fast.next and fast.next.next:
slow = slow.next; fast = fast.next.next
# reverse second half
prev, cur = None, slow.next
slow.next = None
while cur:
nxt = cur.next; cur.next = prev; prev = cur; cur = nxt
# merge
a, b = head, prev
while b:
an, bn = a.next, b.next
a.next = b; b.next = an
a, b = an, bn
Deque interleave O(n) O(n)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Solution:
def reorderList(self, head) -> None:
dq = deque()
c = head
while c: dq.append(c); c = c.next
dummy = ListNode(); tail = dummy; front = True
while dq:
tail.next = dq.popleft() if front else dq.pop()
tail = tail.next; front = not front
tail.next = None
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When to use | Demo | Default | Clarity |
| PE Verdict | Mid+reverse+merge. | ||
What the interviewer is really testing
Composition of three classic LL ops. PE signal: do all three in O(1) space, sever the list at the midpoint cleanly.
Top gotchas
- Set slow.next = None BEFORE reversing — otherwise the merge creates a cycle.
- Midpoint via slow/fast: use `while fast.next && fast.next.next` (not the usual fast/fast.next) — leaves slow at end of first half.
- Merge loop driven by `b` (the shorter/equal half) — first half is longer for odd n.
Ship-it
Find mid, reverse second half, interleave merge.
#38 LC 19 Medium Remove Nth Node From End of List
Two-pointer gap of n — single pass with dummy head.
Problem Statement
Remove the n-th node from the end of a singly linked list and return the head.
Examples
Constraints
- 1 <= size <= 30
- 1 <= n <= size
Approach Overview
Brute Force
Java: Two passes: count then delete
Python: Two passes
Optimal #1
Java: Two pointers with gap n
Python: Two pointers with gap
Optimal #2
Java: Recursive depth tracking
Python: Recursive
Java Solutions
Two passes: count then delete O(n) O(1)
Count length; delete (length - n)th node.
Pseudo-code
len = count; idx = len - n; delete that node
Complexity
Edge cases & gotchas
- Two passes — opt1 does one.
Java code
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
int len = 0;
for (ListNode c = head; c != null; c = c.next) len++;
ListNode dummy = new ListNode(0); dummy.next = head;
ListNode prev = dummy;
for (int i = 0; i < len - n; i++) prev = prev.next;
prev.next = prev.next.next;
return dummy.next;
}
}
Two pointers with gap n O(n) O(1)
Advance fast n+1 steps; then move both until fast hits null. slow.next is the one to delete.
Pseudo-code
dummy → head fast = slow = dummy for _ in n+1: fast = fast.next while fast: fast = fast.next; slow = slow.next slow.next = slow.next.next
Complexity
Edge cases & gotchas
- Dummy head handles deleting the actual head node uniformly.
- n+1 steps for fast so slow ends up just before the target.
Java code
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0); dummy.next = head;
ListNode fast = dummy, slow = dummy;
for (int i = 0; i <= n; i++) fast = fast.next;
while (fast != null) { fast = fast.next; slow = slow.next; }
slow.next = slow.next.next;
return dummy.next;
}
}
Recursive depth tracking O(n) O(n) stack
Recurse to end; on the way back, decrement n and unlink when n == 0.
Pseudo-code
recurse to null; return decrementing counter; unlink at hit
Complexity
Edge cases & gotchas
- Stack overflow for long lists.
Java code
class Solution {
private int n;
public ListNode removeNthFromEnd(ListNode head, int nn) {
this.n = nn;
return helper(head);
}
private ListNode helper(ListNode node) {
if (node == null) return null;
node.next = helper(node.next);
if (--n == 0) return node.next; // skip self
return node;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(1) | O(1) | O(n) stack |
| Passes | 2 | 1 | 1 |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When to use | Whiteboard simplicity | Default | Recursion-friendly contexts |
| PE Verdict | Two-pointer single pass with dummy head. | ||
Python Solutions
Two passes O(n) O(1)
Same.
Pseudo-code
count, then delete
Complexity
Python code
class Solution:
def removeNthFromEnd(self, head, n: int):
length = 0; c = head
while c: length += 1; c = c.next
dummy = ListNode(0, head); prev = dummy
for _ in range(length - n): prev = prev.next
prev.next = prev.next.next
return dummy.next
Two pointers with gap O(n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def removeNthFromEnd(self, head, n: int):
dummy = ListNode(0, head)
fast = slow = dummy
for _ in range(n + 1):
fast = fast.next
while fast:
fast = fast.next; slow = slow.next
slow.next = slow.next.next
return dummy.next
Recursive O(n) O(n)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def removeNthFromEnd(self, head, n: int):
self.n = n
def rec(node):
if not node: return None
node.next = rec(node.next)
self.n -= 1
return node.next if self.n == 0 else node
return rec(head)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(1) | O(1) | O(n) |
| Passes | 2 | 1 | 1 |
| When to use | Demo | Default | Pedagogy |
| PE Verdict | Two pointers single pass. | ||
What the interviewer is really testing
Dummy head + two-pointer-with-gap. PE signal: single pass, dummy head removes the 'delete head' branch.
Top gotchas
- Advance fast n+1 steps (not n) so slow ends BEFORE the target.
- Dummy head: simplifies deleting the actual head node.
- Problem guarantees n ≤ size, so no bounds checks needed in the gap advance.
Ship-it
Two-pointer single pass with dummy head.
#39 LC 138 Medium Copy List with Random Pointer
Deep-copy LL with random pointers — hash map of originals→copies, or interleave-then-split.
Problem Statement
Deep copy a linked list where each node has next and random pointer (any node or null).
Examples
Constraints
- 0 <= n <= 1000
Approach Overview
Brute Force
Java: Two passes with HashMap<old, new>
Python: Two passes with dict
Optimal #1
Java: Single-pass HashMap with computeIfAbsent
Python: Single-pass dict.setdefault
Optimal #2
Java: Interleave clones inline (O(1) space)
Python: Interleave inline
Java Solutions
Two passes with HashMap<old, new> O(n) O(n)
Pass 1: clone nodes into a map. Pass 2: wire next/random via lookups.
Pseudo-code
p1: map[old] = new Node(old.val); p2: map[old].next = map[old.next], same for random
Complexity
Edge cases & gotchas
- HashMap auto-handles null (returns null).
- Simple and clear.
Java code
import java.util.*;
class Node { int val; Node next, random; Node(int v){val=v;} }
class Solution {
public Node copyRandomList(Node head) {
Map<Node, Node> map = new HashMap<>();
for (Node c = head; c != null; c = c.next) map.put(c, new Node(c.val));
for (Node c = head; c != null; c = c.next) {
map.get(c).next = map.get(c.next);
map.get(c).random = map.get(c.random);
}
return map.get(head);
}
}
Single-pass HashMap with computeIfAbsent O(n) O(n)
One pass; lazy-create both current and pointed nodes.
Pseudo-code
map = {}
def getOrCreate(node): if node and node not in map: map[node] = Node(node.val); return map.get(node)
for c in list: getOrCreate(c).next = getOrCreate(c.next); getOrCreate(c).random = getOrCreate(c.random)
Complexity
Edge cases & gotchas
- Single pass — readable and minimal.
Java code
import java.util.*;
class Solution {
public Node copyRandomList(Node head) {
Map<Node, Node> map = new HashMap<>();
for (Node c = head; c != null; c = c.next) {
Node copy = map.computeIfAbsent(c, k -> new Node(k.val));
if (c.next != null) copy.next = map.computeIfAbsent(c.next, k -> new Node(k.val));
if (c.random != null) copy.random = map.computeIfAbsent(c.random, k -> new Node(k.val));
}
return map.get(head);
}
}
Interleave clones inline (O(1) space) O(n) O(1)
Insert each clone after its original; wire randoms via clone.random = original.random.next; split the list.
Pseudo-code
1) for each n: insert n' after n (n.next = n') 2) for each n: n'.random = n.random.next (or null) 3) split: even = clones, odd = originals
Complexity
Edge cases & gotchas
- Step 2 only works AFTER step 1 — the .next of any random points to its clone.
- Step 3 restores original list and extracts clone list — restore is required if input must not be mutated.
Java code
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
// 1) interleave
for (Node c = head; c != null; c = c.next.next) {
Node copy = new Node(c.val);
copy.next = c.next;
c.next = copy;
}
// 2) wire randoms
for (Node c = head; c != null; c = c.next.next) {
if (c.random != null) c.next.random = c.random.next;
}
// 3) split
Node copyHead = head.next;
for (Node c = head; c != null; c = c.next) {
Node copy = c.next;
c.next = copy.next;
if (copy.next != null) copy.next = copy.next.next;
}
return copyHead;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(1) extra |
| Difficulty | 2/5 | 3/5 | 5/5 |
| Mutates input | No | No | Yes temporarily |
| When to use | Whiteboard | Default | Memory-tight |
| PE Verdict | HashMap single-pass — clearest. Interleave is the elegant O(1)-space answer to mention. | ||
Python Solutions
Two passes with dict O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Node:
def __init__(self, x, next=None, random=None): self.val=x; self.next=next; self.random=random
class Solution:
def copyRandomList(self, head):
if not head: return None
m = {}
c = head
while c: m[c] = Node(c.val); c = c.next
c = head
while c:
m[c].next = m.get(c.next)
m[c].random = m.get(c.random)
c = c.next
return m[head]
Single-pass dict.setdefault O(n) O(n)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def copyRandomList(self, head):
if not head: return None
m = {}
def get(n):
if n is None: return None
if n not in m: m[n] = Node(n.val)
return m[n]
c = head
while c:
copy = get(c)
copy.next = get(c.next)
copy.random = get(c.random)
c = c.next
return m[head]
Interleave inline O(n) O(1)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def copyRandomList(self, head):
if not head: return None
c = head
while c:
copy = Node(c.val); copy.next = c.next; c.next = copy; c = copy.next
c = head
while c:
if c.random: c.next.random = c.random.next
c = c.next.next
new_head = head.next
c = head
while c:
copy = c.next; c.next = copy.next
if copy.next: copy.next = copy.next.next
c = c.next
return new_head
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(1) |
| Difficulty | 2/5 | 3/5 | 5/5 |
| When to use | Whiteboard | Default | Memory-tight |
| PE Verdict | dict single-pass. | ||
What the interviewer is really testing
Classic deep-copy with non-trivial pointers. PE signal: HashMap default, interleave-then-split as the O(1) trick.
Top gotchas
- Random can be null OR self OR any other node — use a map.get (returns null for unknown).
- Interleave: original.next.random = original.random.next is the elegant wire-up.
- Restore original list in step 3 if the caller forbids mutation.
Ship-it
HashMap single-pass; mention interleave as the O(1) optimal.
#40 LC 2 Medium Add Two Numbers
Add two reversed-digit linked lists with carry — schoolbook addition.
Problem Statement
Given two non-empty linked lists representing non-negative integers with digits in reverse order, return their sum as a linked list.
Examples
Constraints
- 1 <= length(L) <= 100
- 0 <= digit <= 9
- No leading zeros except single 0
Approach Overview
Brute Force
Java: Convert to BigInteger, add, build back
Python: Parse to int, add, rebuild
Optimal #1
Java: Single-pass elementary addition with carry
Python: Iterative with carry
Optimal #2
Java: Recursive addition
Python: Recursive
Java Solutions
Convert to BigInteger, add, build back O(n+m) O(n+m)
Parse both lists as numbers, add, reconstruct. Defeats the purpose.
Pseudo-code
to_int(l1) + to_int(l2) → from_int → list
Complexity
Edge cases & gotchas
- Defeats the algorithmic exercise.
Java code
import java.math.BigInteger;
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public ListNode addTwoNumbers(ListNode a, ListNode b) {
StringBuilder sa = new StringBuilder(), sb = new StringBuilder();
for (ListNode c = a; c != null; c = c.next) sa.insert(0, c.val);
for (ListNode c = b; c != null; c = c.next) sb.insert(0, c.val);
BigInteger sum = new BigInteger(sa.toString()).add(new BigInteger(sb.toString()));
String s = sum.toString();
ListNode dummy = new ListNode(0), t = dummy;
for (int i = s.length()-1; i >= 0; i--) { t.next = new ListNode(s.charAt(i)-'0'); t = t.next; }
return dummy.next;
}
}
Single-pass elementary addition with carry O(max(n,m)) O(max(n,m))
Walk both, add digits + carry, build result. Handles unequal lengths and final carry.
Pseudo-code
carry = 0; dummy; tail = dummy
while a or b or carry:
s = (a.val if a) + (b.val if b) + carry
tail.next = new (s%10); carry = s/10
advance a, b, tail
return dummy.next
Complexity
Edge cases & gotchas
- Loop guard includes carry — final carry creates a new node.
- Advance a/b only if non-null.
Java code
class Solution {
public ListNode addTwoNumbers(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), tail = dummy;
int carry = 0;
while (a != null || b != null || carry > 0) {
int s = carry;
if (a != null) { s += a.val; a = a.next; }
if (b != null) { s += b.val; b = b.next; }
tail.next = new ListNode(s % 10);
tail = tail.next;
carry = s / 10;
}
return dummy.next;
}
}
Recursive addition O(max) O(max) stack
Recurse with carry as parameter.
Pseudo-code
rec(a, b, carry): base case → carry node; else sum, recurse
Complexity
Edge cases & gotchas
- Stack overflow on long lists.
Java code
class Solution {
public ListNode addTwoNumbers(ListNode a, ListNode b) { return rec(a, b, 0); }
private ListNode rec(ListNode a, ListNode b, int carry) {
if (a == null && b == null && carry == 0) return null;
int s = carry + (a != null ? a.val : 0) + (b != null ? b.val : 0);
ListNode node = new ListNode(s % 10);
node.next = rec(a != null ? a.next : null, b != null ? b.next : null, s / 10);
return node;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n+m) | O(max) | O(max) |
| Space | O(n+m) | O(max) | O(max) stack |
| Difficulty | 2/5 | 2/5 | 3/5 |
| When to use | Demo (overkill) | Default | Pedagogy |
| PE Verdict | Iterative single pass — handle carry in the loop condition. | ||
Python Solutions
Parse to int, add, rebuild O(n+m) O(n+m)
Same.
Pseudo-code
int + int → digits
Complexity
Python code
class Solution:
def addTwoNumbers(self, a, b):
def to_int(n):
x = 0; m = 1
while n: x += n.val * m; m *= 10; n = n.next
return x
s = to_int(a) + to_int(b)
dummy = ListNode(); t = dummy
if s == 0: return ListNode(0)
while s: t.next = ListNode(s % 10); s //= 10; t = t.next
return dummy.next
Iterative with carry O(max) O(max)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def addTwoNumbers(self, a, b):
dummy = ListNode(); tail = dummy; carry = 0
while a or b or carry:
s = carry
if a: s += a.val; a = a.next
if b: s += b.val; b = b.next
tail.next = ListNode(s % 10)
tail = tail.next
carry = s // 10
return dummy.next
Recursive O(max) O(max)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def addTwoNumbers(self, a, b, carry=0):
if not a and not b and not carry: return None
s = carry + (a.val if a else 0) + (b.val if b else 0)
node = ListNode(s % 10)
node.next = self.addTwoNumbers(a.next if a else None, b.next if b else None, s // 10)
return node
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n+m) | O(max) | O(max) |
| Space | O(n+m) | O(max) | O(max) |
| Difficulty | 2/5 | 2/5 | 3/5 |
| When to use | Demo | Default | Recursive style |
| PE Verdict | Iterative with carry. | ||
What the interviewer is really testing
Schoolbook addition + LL traversal. PE signal: carry in the loop condition, single dummy-head pattern, no special-casing.
Top gotchas
- Loop condition includes carry — without it you miss a trailing 1 for inputs like 9->9 + 1.
- Advance a and b independently — different lengths are normal.
- Use integer division for carry: s // 10 in Python, s / 10 in Java (s is int).
Ship-it
Iterative with carry, dummy head.
#41 LC 287 Medium Find the Duplicate Number
Floyd's cycle detection on indices — find duplicate in O(1) space without mutation.
Problem Statement
Given an array nums of n+1 integers each in [1, n], exactly one value appears more than once. Return that value. Must use O(1) extra space and must not modify the input.
Examples
Constraints
- 1 <= n <= 10^5
- All integers in [1, n]
Approach Overview
Brute Force
Java: HashSet of seen
Python: set of seen
Optimal #1
Java: Floyd's cycle detection on next = nums[i]
Python: Floyd
Optimal #2
Java: Binary search on value range
Python: Binary search on value range
Java Solutions
HashSet of seen O(n) O(n)
First repeat hit. Violates O(1) space.
Pseudo-code
for x: if x in set return x; add
Complexity
Edge cases & gotchas
- Violates space constraint.
Java code
import java.util.*;
class Solution {
public int findDuplicate(int[] nums) {
Set<Integer> s = new HashSet<>();
for (int x : nums) if (!s.add(x)) return x;
return -1;
}
}
Floyd's cycle detection on next = nums[i] O(n) O(1)
Treat array as linked list where i → nums[i]. Duplicates create a cycle; cycle entrance = duplicate value.
Pseudo-code
1) tortoise/hare until meet inside cycle 2) reset one to start; advance both by 1 until they meet → entrance
Complexity
Edge cases & gotchas
- Values are in [1, n], so nums[i] is a valid index into nums (n+1 array).
- The duplicate value IS the cycle entrance because multiple indices point to it.
Java code
class Solution {
public int findDuplicate(int[] nums) {
int slow = nums[0], fast = nums[0];
do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow != fast);
slow = nums[0];
while (slow != fast) { slow = nums[slow]; fast = nums[fast]; }
return slow;
}
}
Binary search on value range O(n log n) O(1)
For each mid, count elements ≤ mid. If > mid, duplicate is in [lo, mid]; else [mid+1, hi].
Pseudo-code
lo=1, hi=n
while lo < hi:
mid = (lo+hi)/2; cnt = count(x <= mid)
if cnt > mid: hi = mid
else: lo = mid+1
return lo
Complexity
Edge cases & gotchas
- Works even when more than one value is duplicated (still finds one).
- Slower than Floyd; non-mutating; mentions pigeonhole intuition.
Java code
class Solution {
public int findDuplicate(int[] nums) {
int n = nums.length - 1;
int lo = 1, hi = n;
while (lo < hi) {
int mid = (lo + hi) / 2, cnt = 0;
for (int x : nums) if (x <= mid) cnt++;
if (cnt > mid) hi = mid;
else lo = mid + 1;
}
return lo;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 5/5 — cycle-on-index insight | 3/5 — pigeonhole binary search |
| Spec compliance | Violates | Meets | Meets |
| When to use | Quick | Default — optimal | If Floyd doesn't click |
| PE Verdict | Floyd's algorithm — O(n) and O(1). The cycle-on-index insight is the entire learning. | ||
Python Solutions
set of seen O(n) O(n)
Same.
Pseudo-code
membership probe
Complexity
Python code
from typing import List
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
s = set()
for x in nums:
if x in s: return x
s.add(x)
return -1
Floyd O(n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
slow = fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast: break
slow = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
Binary search on value range O(n log n) O(1)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
lo, hi = 1, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
cnt = sum(1 for x in nums if x <= mid)
if cnt > mid: hi = mid
else: lo = mid + 1
return lo
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 5/5 | 3/5 |
| When to use | Quick | Default | Pedagogy |
| PE Verdict | Floyd. | ||
What the interviewer is really testing
Floyd's algorithm applied to indices. PE signal: see the array as a function i → nums[i], explain why a duplicate forces a cycle.
Top gotchas
- Phase 1 uses do-while (or break-on-equal AFTER moving) since slow=fast initially.
- Phase 2: reset slow to start, advance both by 1 — entrance is the duplicate.
- Values in [1, n] is essential — if 0 were allowed nums[0] could be 0 (self-loop at index 0 trivially).
Ship-it
Floyd's cycle detection on the index function.
#42 LC 146 Medium LRU Cache
HashMap + doubly-linked list — O(1) get and put.
Problem Statement
Design an LRU cache with O(1) get and put.
Examples
Constraints
- 1 <= capacity <= 3000
- 0 <= key, value <= 10^4
- Up to 2*10^5 calls
Approach Overview
Brute Force
Java: LinkedHashMap with access order
Python: dict-only with linear scan for LRU
Optimal #1
Java: HashMap<K, Node> + doubly-linked list (head=MRU, tail=LRU)
Python: OrderedDict with move_to_end
Optimal #2
Java: OrderedDict / LinkedHashMap with manual move
Python: dict + doubly-linked list (explicit)
Java Solutions
LinkedHashMap with access order O(1) get/put O(capacity)
Java's LinkedHashMap maintains insertion or access order and lets you override removeEldestEntry. Cheating but correct.
Pseudo-code
extend LinkedHashMap<K,V>(cap, 0.75, true); override removeEldestEntry
Complexity
Edge cases & gotchas
- JDK doesn't expose this as configurable beyond override — but it's exactly LRU.
Java code
import java.util.*;
class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int cap;
public LRUCache(int capacity) { super(capacity, 0.75f, true); this.cap = capacity; }
public int get(int key) { return super.getOrDefault(key, -1); }
public void put(int key, int value) { super.put(key, value); }
@Override protected boolean removeEldestEntry(Map.Entry<Integer,Integer> e) { return size() > cap; }
}
HashMap<K, Node> + doubly-linked list (head=MRU, tail=LRU) O(1) get/put O(capacity)
Explicit DLL with dummy head/tail; HashMap maps key to its node. Standard PE answer.
Pseudo-code
get: if hit, move node to head, return value put: if hit, update value and move to head; else add at head; if size > cap, evict tail.prev
Complexity
Edge cases & gotchas
- Dummy head and tail sentinels eliminate null-checks.
- Always unlink before adding to head — keeps DLL invariants intact.
Java code
import java.util.*;
class LRUCache {
private static class Node {
int key, val; Node prev, next;
Node(int k, int v) { key = k; val = v; }
}
private final int cap;
private final Map<Integer, Node> map = new HashMap<>();
private final Node head = new Node(0,0), tail = new Node(0,0);
public LRUCache(int capacity) {
cap = capacity;
head.next = tail; tail.prev = head;
}
private void remove(Node n) {
n.prev.next = n.next; n.next.prev = n.prev;
}
private void addFirst(Node n) {
n.next = head.next; n.prev = head;
head.next.prev = n; head.next = n;
}
public int get(int key) {
Node n = map.get(key);
if (n == null) return -1;
remove(n); addFirst(n);
return n.val;
}
public void put(int key, int value) {
Node n = map.get(key);
if (n != null) { n.val = value; remove(n); addFirst(n); return; }
if (map.size() == cap) {
Node lru = tail.prev;
remove(lru); map.remove(lru.key);
}
Node nn = new Node(key, value);
map.put(key, nn);
addFirst(nn);
}
}
OrderedDict / LinkedHashMap with manual move O(1) O(capacity)
Use ordered map; on get/put, remove and re-insert to move to end. Same complexity.
Pseudo-code
on access: remove then put back at end; evict from front
Complexity
Edge cases & gotchas
- LinkedHashMap implements this exactly; Python OrderedDict has move_to_end().
Java code
import java.util.*;
class LRUCache {
private final int cap;
private final LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
public LRUCache(int capacity) { cap = capacity; }
public int get(int key) {
Integer v = map.remove(key);
if (v == null) return -1;
map.put(key, v); // re-insert moves to end (MRU)
return v;
}
public void put(int key, int value) {
if (map.remove(key) == null && map.size() == cap) {
map.remove(map.keySet().iterator().next());
}
map.put(key, value);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(1) | O(1) | O(1) |
| Space | O(cap) | O(cap) | O(cap) |
| Difficulty | 1/5 — but 'cheaty' | 5/5 — explicit DLL | 2/5 — uses stdlib |
| Interview signal | Weak — relies on framework | Strongest — proves understanding | Mid — clean but doesn't show DLL |
| When to use | Production (Java) | Interview | Production (Python) |
| PE Verdict | Explicit DLL + HashMap for interviews. Use LinkedHashMap / OrderedDict in production for brevity. | ||
Python Solutions
dict-only with linear scan for LRU O(n) O(cap)
O(n) per op.
Pseudo-code
track access counter; scan for min on eviction
Complexity
Python code
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity; self.d = {}; self.t = 0; self.access = {}
def get(self, key: int) -> int:
if key not in self.d: return -1
self.t += 1; self.access[key] = self.t
return self.d[key]
def put(self, key: int, value: int) -> None:
if key not in self.d and len(self.d) == self.cap:
ev = min(self.access, key=self.access.get); del self.d[ev]; del self.access[ev]
self.t += 1; self.d[key] = value; self.access[key] = self.t
OrderedDict with move_to_end O(1) O(cap)
Pythonic — same as LinkedHashMap.
Pseudo-code
on access: move_to_end; on overflow: popitem(last=False)
Complexity
Python code
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.d: "OrderedDict[int,int]" = OrderedDict()
def get(self, key: int) -> int:
if key not in self.d: return -1
self.d.move_to_end(key)
return self.d[key]
def put(self, key: int, value: int) -> None:
if key in self.d: self.d.move_to_end(key)
elif len(self.d) == self.cap: self.d.popitem(last=False)
self.d[key] = value
dict + doubly-linked list (explicit) O(1) O(cap)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- Shows you can build the DS without stdlib helpers.
Python code
class _Node:
__slots__ = ("k","v","prev","next")
def __init__(self, k=0, v=0): self.k=k; self.v=v; self.prev=None; self.next=None
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity; self.m = {}
self.head = _Node(); self.tail = _Node()
self.head.next = self.tail; self.tail.prev = self.head
def _remove(self, n):
n.prev.next = n.next; n.next.prev = n.prev
def _add_first(self, n):
n.next = self.head.next; n.prev = self.head
self.head.next.prev = n; self.head.next = n
def get(self, key: int) -> int:
n = self.m.get(key)
if not n: return -1
self._remove(n); self._add_first(n); return n.v
def put(self, key: int, value: int) -> None:
n = self.m.get(key)
if n: n.v = value; self._remove(n); self._add_first(n); return
if len(self.m) == self.cap:
lru = self.tail.prev; self._remove(lru); del self.m[lru.k]
nn = _Node(key, value); self.m[key] = nn; self._add_first(nn)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(1) | O(1) |
| Space | O(cap) | O(cap) | O(cap) |
| Difficulty | 2/5 | 2/5 — stdlib | 5/5 — explicit DLL |
| When to use | Don't | Production | Interview (proves understanding) |
| PE Verdict | OrderedDict in production. Explicit DLL in interviews if asked. | ||
What the interviewer is really testing
Design + DLL/HashMap composition. PE signal: build the DLL with dummy sentinels; reason about pointer rewiring.
Top gotchas
- Dummy head and tail nodes remove all null-check branching.
- On hit, remove THEN add-first — separate operations keep DLL invariants.
- Evict from tail.prev (true LRU), not from head.
Ship-it
Java: explicit DLL+HashMap. Python: OrderedDict.
#43 LC 23 Hard Merge K Sorted Lists
Merge k sorted lists — min-heap or divide-and-conquer pairwise merge.
Problem Statement
You are given an array of k sorted linked lists; merge them into one sorted list and return its head.
Examples
Constraints
- 0 <= k <= 10^4
- 0 <= total nodes <= 10^4
Approach Overview
Brute Force
Java: Repeated merge two
Python: Sequential merge two
Optimal #1
Java: Min-heap of head nodes
Python: heapq with tiebreaker
Optimal #2
Java: Divide-and-conquer pairwise merge
Python: Iterative pairwise D&C
Java Solutions
Repeated merge two O(k·n) — each pass merges all into growing accumulator O(1)
Merge list[0] with list[1], result with list[2], etc.
Pseudo-code
result = [] for list in lists: result = mergeTwo(result, list)
Complexity
Edge cases & gotchas
- Slower than D&C by a log k factor.
Java code
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode out = null;
for (ListNode l : lists) out = mergeTwo(out, l);
return out;
}
private ListNode mergeTwo(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), t = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { t.next = a; a = a.next; }
else { t.next = b; b = b.next; }
t = t.next;
}
t.next = (a != null) ? a : b;
return dummy.next;
}
}
Min-heap of head nodes O(N log k) where N = total nodes O(k)
Push all heads; pop the smallest, append, push that node's next.
Pseudo-code
heap = min-heap of (val, node) of all heads
while heap:
n = heap.pop()
tail.next = n; tail = n
if n.next: heap.push(n.next)
Complexity
Edge cases & gotchas
- Use a tiebreaker (node index) in case of duplicate values, since ListNode isn't Comparable.
- Skip null lists when pushing initial heads.
Java code
import java.util.*;
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode l : lists) if (l != null) pq.offer(l);
ListNode dummy = new ListNode(0), tail = dummy;
while (!pq.isEmpty()) {
ListNode n = pq.poll();
tail.next = n; tail = n;
if (n.next != null) pq.offer(n.next);
}
return dummy.next;
}
}
Divide-and-conquer pairwise merge O(N log k) O(log k) recursion
Pair lists, merge two-by-two, halving each round. log k rounds × n merge work.
Pseudo-code
while lists.size > 1: merge pairs into next round
Complexity
Edge cases & gotchas
- Iterative pairwise avoids recursion overhead.
- Same asymptote as heap; sometimes faster in practice due to cache.
Java code
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) return null;
int interval = 1;
while (interval < lists.length) {
for (int i = 0; i + interval < lists.length; i += 2 * interval) {
lists[i] = mergeTwo(lists[i], lists[i + interval]);
}
interval *= 2;
}
return lists[0];
}
private ListNode mergeTwo(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), t = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { t.next = a; a = a.next; }
else { t.next = b; b = b.next; }
t = t.next;
}
t.next = (a != null) ? a : b;
return dummy.next;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(kN) | O(N log k) | O(N log k) |
| Space | O(1) | O(k) | O(log k) stack |
| Difficulty | 1/5 | 3/5 | 3/5 |
| Cache behavior | OK | Worse — heap pointer chasing | Better — pairwise sequential |
| When to use | Demo | Default | When heap allocation matters |
| PE Verdict | Min-heap is the textbook answer; D&C is a fine alternative when you want O(1) heap space. | ||
Python Solutions
Sequential merge two O(kN) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def mergeKLists(self, lists):
def merge(a, b):
dummy = ListNode(); t = dummy
while a and b:
if a.val <= b.val: t.next = a; a = a.next
else: t.next = b; b = b.next
t = t.next
t.next = a or b
return dummy.next
out = None
for l in lists: out = merge(out, l)
return out
heapq with tiebreaker O(N log k) O(k)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Edge cases & gotchas
- heapq compares tuples — use (val, index, node) since ListNode isn't comparable.
Python code
import heapq
class Solution:
def mergeKLists(self, lists):
h = []
for i, l in enumerate(lists):
if l: heapq.heappush(h, (l.val, i, l))
dummy = ListNode(); tail = dummy
while h:
v, i, n = heapq.heappop(h)
tail.next = n; tail = n
if n.next: heapq.heappush(h, (n.next.val, i, n.next))
return dummy.next
Iterative pairwise D&C O(N log k) O(1)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def mergeKLists(self, lists):
if not lists: return None
def merge(a, b):
dummy = ListNode(); t = dummy
while a and b:
if a.val <= b.val: t.next = a; a = a.next
else: t.next = b; b = b.next
t = t.next
t.next = a or b
return dummy.next
interval = 1
while interval < len(lists):
for i in range(0, len(lists) - interval, interval * 2):
lists[i] = merge(lists[i], lists[i + interval])
interval *= 2
return lists[0]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(kN) | O(N log k) | O(N log k) |
| Space | O(1) | O(k) | O(1) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When to use | Demo | Default | Memory-tight |
| PE Verdict | heapq with tuple tiebreaker. | ||
What the interviewer is really testing
k-way merge fundamentals. PE signal: O(N log k) via heap; tiebreaker for non-comparable list nodes.
Top gotchas
- Python heapq compares tuples element-by-element — without an index tiebreaker, two equal vals compare nodes and TypeError.
- Push initial heads only if non-null.
- After popping, push the popped node's NEXT (not the popped node again).
Ship-it
Min-heap with tiebreaker; D&C if heap memory is an issue.
#44 LC 25 Hard Reverse Nodes in K-Group
Reverse every group of k nodes — pointer surgery with leftover handling.
Problem Statement
Reverse the nodes of a linked list k at a time. If fewer than k remain at the end, leave them in original order.
Examples
Constraints
- 1 <= n <= 5000
- 1 <= k <= n
Approach Overview
Brute Force
Java: Recursive reversal of k
Python: Recursive
Optimal #1
Java: Iterative group reverse with dummy head
Python: Iterative in-place
Optimal #2
Java: Stack of k nodes
Python: Stack of k
Java Solutions
Recursive reversal of k O(n) O(n/k) stack
Reverse first k; recurse on the rest; splice.
Pseudo-code
check if k nodes available reverse first k head.next = reverseKGroup(remainder) return new head
Complexity
Edge cases & gotchas
- Stack overflow for long lists with small k.
Java code
class ListNode { int val; ListNode next; ListNode(int v){val=v;} }
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode c = head;
for (int i = 0; i < k; i++) {
if (c == null) return head;
c = c.next;
}
ListNode prev = reverseKGroup(c, k); // recurse first
ListNode cur = head;
for (int i = 0; i < k; i++) {
ListNode nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
}
Iterative group reverse with dummy head O(n) O(1)
Use dummy head; for each block, locate the kth node; if found, reverse in place and splice.
Pseudo-code
dummy → head
groupPrev = dummy
loop:
kth = walk groupPrev k steps; if null, break
groupNext = kth.next
reverse [groupPrev.next ... kth] in place
splice: groupPrev.next = kth (new head); old head = groupNext (new tail.next)
groupPrev = old head (which is now the tail)
Complexity
Edge cases & gotchas
- Verify k nodes exist BEFORE reversing — short tail must stay as-is.
- Track groupPrev (the node BEFORE the block) for splicing.
Java code
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0); dummy.next = head;
ListNode groupPrev = dummy;
while (true) {
ListNode kth = groupPrev;
for (int i = 0; i < k && kth != null; i++) kth = kth.next;
if (kth == null) break;
ListNode groupNext = kth.next;
// Reverse [groupPrev.next ... kth]
ListNode prev = groupNext, cur = groupPrev.next;
while (cur != groupNext) {
ListNode nxt = cur.next;
cur.next = prev; prev = cur; cur = nxt;
}
ListNode tmp = groupPrev.next;
groupPrev.next = kth;
groupPrev = tmp;
}
return dummy.next;
}
}
Stack of k nodes O(n) O(k)
Push k nodes; pop them onto a growing tail. Higher constant factor but easier to write.
Pseudo-code
loop blocks: push k or break; pop into tail; advance
Complexity
Edge cases & gotchas
- Final partial group: undo the pushes and append original.
Java code
import java.util.*;
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
Deque<ListNode> stack = new ArrayDeque<>();
ListNode dummy = new ListNode(0), tail = dummy, c = head;
while (c != null) {
int cnt = 0; ListNode start = c;
while (cnt < k && c != null) { stack.push(c); c = c.next; cnt++; }
if (cnt < k) { tail.next = start; break; }
while (!stack.isEmpty()) { tail.next = stack.pop(); tail = tail.next; }
tail.next = null;
}
return dummy.next;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n/k) | O(1) | O(k) |
| Difficulty | 3/5 | 5/5 — pointer juggling | 3/5 |
| When to use | Pedagogy | Production ship-it | Whiteboard easier to write |
| PE Verdict | Iterative in-place — O(1) space and stack-safe. | ||
Python Solutions
Recursive O(n) O(n/k)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseKGroup(self, head, k: int):
c = head
for _ in range(k):
if not c: return head
c = c.next
prev = self.reverseKGroup(c, k)
cur = head
for _ in range(k):
nxt = cur.next; cur.next = prev; prev = cur; cur = nxt
return prev
Iterative in-place O(n) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseKGroup(self, head, k: int):
dummy = ListNode(0, head); group_prev = dummy
while True:
kth = group_prev
for _ in range(k):
kth = kth.next
if not kth: return dummy.next
group_next = kth.next
prev, cur = group_next, group_prev.next
while cur is not group_next:
nxt = cur.next; cur.next = prev; prev = cur; cur = nxt
tmp = group_prev.next
group_prev.next = kth
group_prev = tmp
Stack of k O(n) O(k)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseKGroup(self, head, k: int):
dummy = ListNode(); tail = dummy; c = head
while c:
stack = []; cur = c
for _ in range(k):
if not cur: break
stack.append(cur); cur = cur.next
if len(stack) < k: tail.next = c; break
while stack: tail.next = stack.pop(); tail = tail.next
tail.next = None; c = cur
return dummy.next
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n/k) | O(1) | O(k) |
| Difficulty | 3/5 | 5/5 | 3/5 |
| When to use | Pedagogy | Default | Whiteboard |
| PE Verdict | Iterative in-place. | ||
What the interviewer is really testing
Hardest LL pointer juggling. PE signal: verify k available BEFORE touching, track groupPrev for splice.
Top gotchas
- Verify k nodes available before reversing — short tail must remain in order.
- Use groupNext as the sentinel prev for the reverse loop — terminates exactly at the boundary.
- After reversal, the OLD head of the block becomes the tail; capture it before reversing.
Ship-it
Iterative in-place with dummy head and groupPrev tracking.
Trees
#45 LC 226 Easy Invert Binary Tree
Swap left/right at every node — recursive vs iterative.
Problem Statement
Given the root of a binary tree, invert the tree (mirror it) and return its root.
Examples
Constraints
- 0 <= n <= 100
Approach Overview
Brute Force
Java: BFS with queue
Python: BFS
Optimal #1
Java: Recursive DFS
Python: Recursive
Optimal #2
Java: Iterative DFS with explicit stack
Python: Iterative DFS
Java Solutions
BFS with queue O(n) O(w) where w = max width
Level-order swap.
Pseudo-code
while q: pop; swap children; enqueue non-null children
Complexity
Java code
import java.util.*;
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode n = q.poll();
TreeNode t = n.left; n.left = n.right; n.right = t;
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
return root;
}
}
Recursive DFS O(n) O(h)
Recurse left and right, then swap.
Pseudo-code
if null return; l = invert(left); r = invert(right); swap; return root
Complexity
Edge cases & gotchas
- Stack depth = tree height — risk on extremely skewed trees.
Java code
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode l = invertTree(root.left);
TreeNode r = invertTree(root.right);
root.left = r; root.right = l;
return root;
}
}
Iterative DFS with explicit stack O(n) O(h)
Avoid recursion depth.
Pseudo-code
stack root; pop, swap, push non-null children
Complexity
Java code
import java.util.*;
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
Deque<TreeNode> s = new ArrayDeque<>();
s.push(root);
while (!s.isEmpty()) {
TreeNode n = s.pop();
TreeNode t = n.left; n.left = n.right; n.right = t;
if (n.left != null) s.push(n.left);
if (n.right != null) s.push(n.right);
}
return root;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(w) | O(h) | O(h) |
| Difficulty | 2/5 | 1/5 | 2/5 |
| When to use | Wide trees | Default | Deep trees, stack-safe |
| PE Verdict | Recursive — shortest correct code. Switch to iterative for deeply skewed trees. | ||
Python Solutions
BFS O(n) O(w)
Same.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None): self.val=val; self.left=left; self.right=right
class Solution:
def invertTree(self, root):
if not root: return None
q = deque([root])
while q:
n = q.popleft()
n.left, n.right = n.right, n.left
if n.left: q.append(n.left)
if n.right: q.append(n.right)
return root
Recursive O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def invertTree(self, root):
if not root: return None
root.left, root.right = self.invertTree(root.right), self.invertTree(root.left)
return root
Iterative DFS O(n) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def invertTree(self, root):
if not root: return None
s = [root]
while s:
n = s.pop()
n.left, n.right = n.right, n.left
if n.left: s.append(n.left)
if n.right: s.append(n.right)
return root
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(w) | O(h) | O(h) |
| Difficulty | 2/5 | 1/5 | 2/5 |
| When to use | Wide | Default | Stack-safe |
| PE Verdict | Recursive. | ||
What the interviewer is really testing
Tree warmup. PE signal: pick recursive default; mention iterative for stack safety.
Top gotchas
- Don't forget the null base case.
- Recursive swap can be a single line via tuple swap in Python.
- Iterative needs a stack/queue — choice doesn't change correctness, only traversal order.
Ship-it
Recursive DFS.
#46 LC 104 Easy Maximum Depth of Binary Tree
Tree height — recursive 1+max(l,r) vs BFS level count.
Problem Statement
Return the maximum depth (root-to-leaf path length in nodes) of a binary tree.
Examples
Constraints
- 0 <= n <= 10^4
Approach Overview
Brute Force
Java: BFS level counting
Python: BFS
Optimal #1
Java: Recursive DFS
Python: Recursive
Optimal #2
Java: Iterative DFS with explicit stack of (node, depth)
Python: Iterative DFS with (node, depth)
Java Solutions
BFS level counting O(n) O(w)
Count levels in BFS.
Pseudo-code
while q: process level; depth += 1
Complexity
Java code
import java.util.*;
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int d = 0;
while (!q.isEmpty()) {
int sz = q.size(); d++;
for (int i = 0; i < sz; i++) {
TreeNode n = q.poll();
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
}
return d;
}
}
Recursive DFS O(n) O(h)
1 + max(depth(left), depth(right)). Cleanest answer.
Pseudo-code
if null: return 0 return 1 + max(depth(left), depth(right))
Complexity
Java code
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
}
Iterative DFS with explicit stack of (node, depth) O(n) O(h)
Stack-safe variant.
Pseudo-code
stack (root, 1); pop and recurse
Complexity
Java code
import java.util.*;
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
Deque<Object[]> s = new ArrayDeque<>();
s.push(new Object[]{root, 1});
int best = 0;
while (!s.isEmpty()) {
Object[] e = s.pop();
TreeNode n = (TreeNode) e[0]; int d = (Integer) e[1];
best = Math.max(best, d);
if (n.left != null) s.push(new Object[]{n.left, d+1});
if (n.right != null) s.push(new Object[]{n.right, d+1});
}
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(w) | O(h) | O(h) |
| Difficulty | 2/5 | 1/5 | 3/5 |
| When to use | Wide trees | Default | Deep trees |
| PE Verdict | Recursive one-liner. | ||
Python Solutions
BFS O(n) O(w)
Same.
Pseudo-code
level count BFS
Complexity
Python code
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None): self.val=val; self.left=left; self.right=right
class Solution:
def maxDepth(self, root) -> int:
if not root: return 0
q = deque([root]); d = 0
while q:
d += 1
for _ in range(len(q)):
n = q.popleft()
if n.left: q.append(n.left)
if n.right: q.append(n.right)
return d
Recursive O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def maxDepth(self, root) -> int:
if not root: return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
Iterative DFS with (node, depth) O(n) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def maxDepth(self, root) -> int:
if not root: return 0
s = [(root, 1)]; best = 0
while s:
n, d = s.pop()
best = max(best, d)
if n.left: s.append((n.left, d+1))
if n.right: s.append((n.right, d+1))
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(w) | O(h) | O(h) |
| Difficulty | 2/5 | 1/5 | 3/5 |
| When to use | Wide | Default | Stack-safe |
| PE Verdict | Recursive. | ||
What the interviewer is really testing
Basic recursive tree traversal warmup.
Top gotchas
- Null root returns 0 (not 1).
- Use 1 + max(...) — easy off-by-one if you forget the +1.
Ship-it
Recursive 1 + max(depth(left), depth(right)).
#47 LC 543 Easy Diameter of Binary Tree
Longest path through any node — track global max while computing heights.
Problem Statement
Return the diameter (longest path in edges between any two nodes) of a binary tree.
Examples
Constraints
- 1 <= n <= 10^4
Approach Overview
Brute Force
Java: For each node, height(left) + height(right)
Python: Per-node height
Optimal #1
Java: Single-pass DFS with global max
Python: Single-pass with nonlocal best
Optimal #2
Java: Single-pass with int[1] holder (no field)
Python: Iterative postorder
Java Solutions
For each node, height(left) + height(right) O(n²) O(h)
Compute height recursively for each node; quadratic.
Pseudo-code
for each n: d = h(left) + h(right); track max
Complexity
Edge cases & gotchas
- TLEs on skewed trees.
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public int diameterOfBinaryTree(TreeNode root) {
if (root == null) return 0;
int local = height(root.left) + height(root.right);
int leftD = diameterOfBinaryTree(root.left);
int rightD = diameterOfBinaryTree(root.right);
return Math.max(local, Math.max(leftD, rightD));
}
private int height(TreeNode n) {
if (n == null) return 0;
return 1 + Math.max(height(n.left), height(n.right));
}
}
Single-pass DFS with global max O(n) O(h)
Each recursive call returns height; while doing so, update best with left+right edge count.
Pseudo-code
best = 0
def h(n):
if not n: return 0
l = h(n.left); r = h(n.right)
best = max(best, l + r)
return 1 + max(l, r)
Complexity
Edge cases & gotchas
- Diameter is in EDGES not nodes — l + r is already correct (not +1).
Java code
class Solution {
private int best;
public int diameterOfBinaryTree(TreeNode root) {
best = 0; height(root); return best;
}
private int height(TreeNode n) {
if (n == null) return 0;
int l = height(n.left), r = height(n.right);
best = Math.max(best, l + r);
return 1 + Math.max(l, r);
}
}
Single-pass with int[1] holder (no field) O(n) O(h)
Same algorithm but avoid the instance field by passing a 1-element array as accumulator.
Pseudo-code
see opt1; thread best as int[]{0}
Complexity
Edge cases & gotchas
- Stylistic — same logic.
Java code
class Solution {
public int diameterOfBinaryTree(TreeNode root) {
int[] best = new int[1];
height(root, best);
return best[0];
}
private int height(TreeNode n, int[] best) {
if (n == null) return 0;
int l = height(n.left, best), r = height(n.right, best);
best[0] = Math.max(best[0], l + r);
return 1 + Math.max(l, r);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When to use | Tiny tree | Default | Threadsafe (no field mutation) |
| PE Verdict | Single-pass with accumulator. Same idiom for tree-max-path-sum (LC 124). | ||
Python Solutions
Per-node height O(n²) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
class TreeNode:
def __init__(self, val=0, left=None, right=None): self.val=val; self.left=left; self.right=right
class Solution:
def diameterOfBinaryTree(self, root) -> int:
def h(n):
return 0 if not n else 1 + max(h(n.left), h(n.right))
def diam(n):
if not n: return 0
return max(h(n.left)+h(n.right), diam(n.left), diam(n.right))
return diam(root)
Single-pass with nonlocal best O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def diameterOfBinaryTree(self, root) -> int:
best = 0
def h(n):
nonlocal best
if not n: return 0
l, r = h(n.left), h(n.right)
best = max(best, l + r)
return 1 + max(l, r)
h(root); return best
Iterative postorder O(n) O(n)
Avoid recursion via explicit stack.
Pseudo-code
iterative postorder; cache heights
Complexity
Python code
class Solution:
def diameterOfBinaryTree(self, root) -> int:
if not root: return 0
heights = {None: 0}
stack = [(root, False)]; best = 0
while stack:
n, processed = stack.pop()
if processed:
l = heights[n.left]; r = heights[n.right]
heights[n] = 1 + max(l, r)
best = max(best, l + r)
else:
stack.append((n, True))
if n.left: stack.append((n.left, False))
if n.right: stack.append((n.right, False))
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(n) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When to use | Demo | Default | Stack-safe |
| PE Verdict | Single-pass with nonlocal. | ||
What the interviewer is really testing
Pattern: 'return height, update best inside recursion'. Foundation for LC 124 (Max Path Sum).
Top gotchas
- Diameter is in EDGES — left height + right height, no +1 at this layer.
- Return 1 + max for the height contribution to ancestors.
- The best path may not pass through the root — track globally.
Ship-it
Single-pass DFS with global max.
#48 LC 110 Easy Balanced Binary Tree
Check height-balance: |h(left) - h(right)| ≤ 1 at every node. Bubble heights with -1 sentinel.
Problem Statement
Given a binary tree, determine if it is height-balanced (every node's left/right subtree heights differ by ≤ 1).
Examples
Constraints
- 0 <= n <= 5000
Approach Overview
Brute Force
Java: For each node, compute heights of both subtrees
Python: Per-node height
Optimal #1
Java: Single-pass DFS returning -1 on imbalance
Python: Sentinel -1 single pass
Optimal #2
Java: Single-pass with mutable holder
Python: Tuple (balanced, height) return
Java Solutions
For each node, compute heights of both subtrees O(n²) O(h)
Quadratic — height recomputed repeatedly.
Pseudo-code
for each n: |h(left) - h(right)| <= 1 and recurse
Complexity
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
if (Math.abs(h(root.left) - h(root.right)) > 1) return false;
return isBalanced(root.left) && isBalanced(root.right);
}
private int h(TreeNode n) { return n == null ? 0 : 1 + Math.max(h(n.left), h(n.right)); }
}
Single-pass DFS returning -1 on imbalance O(n) O(h)
Return height normally; return -1 if any subtree is imbalanced. Short-circuits.
Pseudo-code
def h(n):
if not n: return 0
l = h(n.left); if l == -1: return -1
r = h(n.right); if r == -1: return -1
if abs(l - r) > 1: return -1
return 1 + max(l, r)
Complexity
Edge cases & gotchas
- Sentinel -1 must propagate up immediately.
Java code
class Solution {
public boolean isBalanced(TreeNode root) { return h(root) != -1; }
private int h(TreeNode n) {
if (n == null) return 0;
int l = h(n.left); if (l == -1) return -1;
int r = h(n.right); if (r == -1) return -1;
if (Math.abs(l - r) > 1) return -1;
return 1 + Math.max(l, r);
}
}
Single-pass with mutable holder O(n) O(h)
Same algorithm, no sentinel — track balance flag externally.
Pseudo-code
DFS; on imbalance set flag false, continue
Complexity
Edge cases & gotchas
- Stylistic; -1 sentinel is more idiomatic.
Java code
class Solution {
private boolean ok = true;
public boolean isBalanced(TreeNode root) {
h(root); return ok;
}
private int h(TreeNode n) {
if (n == null || !ok) return 0;
int l = h(n.left), r = h(n.right);
if (Math.abs(l - r) > 1) ok = false;
return 1 + Math.max(l, r);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When to use | Tiny | Default — short-circuits | Equivalent |
| PE Verdict | Sentinel -1 single pass. | ||
Python Solutions
Per-node height O(n²) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isBalanced(self, root) -> bool:
def h(n):
return 0 if not n else 1 + max(h(n.left), h(n.right))
if not root: return True
return abs(h(root.left) - h(root.right)) <= 1 and self.isBalanced(root.left) and self.isBalanced(root.right)
Sentinel -1 single pass O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isBalanced(self, root) -> bool:
def h(n):
if not n: return 0
l = h(n.left)
if l == -1: return -1
r = h(n.right)
if r == -1 or abs(l - r) > 1: return -1
return 1 + max(l, r)
return h(root) != -1
Tuple (balanced, height) return O(n) O(h)
Cleaner Pythonic alternative.
Pseudo-code
return (balanced, height)
Complexity
Python code
class Solution:
def isBalanced(self, root) -> bool:
def dfs(n):
if not n: return (True, 0)
lb, lh = dfs(n.left)
if not lb: return (False, 0)
rb, rh = dfs(n.right)
if not rb: return (False, 0)
return (abs(lh - rh) <= 1, 1 + max(lh, rh))
return dfs(root)[0]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 2/5 | 3/5 | 2/5 |
| When to use | Demo | Default | Readability-first |
| PE Verdict | -1 sentinel. Tuple form is the Pythonic alternative. | ||
What the interviewer is really testing
Show the brute-force quadratic and improve to single-pass. PE signal: short-circuit on first imbalance.
Top gotchas
- Sentinel value (-1) must propagate up immediately — check after each recursive call.
- Tree with one node is balanced (no children → heights 0, 0).
- Don't recompute height after detecting imbalance — short-circuit.
Ship-it
Single-pass DFS with -1 sentinel (Java) or tuple return (Python).
#49 LC 100 Easy Same Tree
Structural and value equality of two binary trees — recursive lock-step traversal.
Problem Statement
Given the roots of two binary trees, determine if they are structurally identical and have equal values at every node.
Examples
Constraints
- 0 <= n <= 100
Approach Overview
Brute Force
Java: Serialize and compare
Python: Serialize compare
Optimal #1
Java: Recursive lock-step compare
Python: Recursive
Optimal #2
Java: Iterative BFS with two queues
Python: Iterative BFS
Java Solutions
Serialize and compare O(n) O(n)
Encode each tree as a string (preorder with null markers) and compare.
Pseudo-code
preorder(p) == preorder(q)?
Complexity
Edge cases & gotchas
- Null markers must be unambiguous (e.g., '#').
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
return serialize(p).equals(serialize(q));
}
private String serialize(TreeNode n) {
if (n == null) return "#";
return n.val + "," + serialize(n.left) + "," + serialize(n.right);
}
}
Recursive lock-step compare O(n) O(h)
Both null → true; one null → false; values match AND children match.
Pseudo-code
if both null: true if one null: false if values differ: false return isSame(p.left, q.left) and isSame(p.right, q.right)
Complexity
Java code
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) return true;
if (p == null || q == null) return false;
if (p.val != q.val) return false;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
Iterative BFS with two queues O(n) O(w)
Process pairs in lock-step.
Pseudo-code
pair-by-pair BFS
Complexity
Java code
import java.util.*;
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
Deque<TreeNode[]> dq = new ArrayDeque<>();
dq.offer(new TreeNode[]{p, q});
while (!dq.isEmpty()) {
TreeNode[] pair = dq.poll();
TreeNode a = pair[0], b = pair[1];
if (a == null && b == null) continue;
if (a == null || b == null || a.val != b.val) return false;
dq.offer(new TreeNode[]{a.left, b.left});
dq.offer(new TreeNode[]{a.right, b.right});
}
return true;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(h) | O(w) |
| Difficulty | 2/5 | 1/5 | 2/5 |
| When to use | Demo only | Default | Wide trees, stack-safe |
| PE Verdict | Recursive lock-step. | ||
Python Solutions
Serialize compare O(n) O(n)
Same.
Pseudo-code
serialize and compare
Complexity
Python code
class TreeNode:
def __init__(self, val=0, left=None, right=None): self.val=val; self.left=left; self.right=right
class Solution:
def isSameTree(self, p, q) -> bool:
def ser(n):
if not n: return "#"
return f"{n.val},{ser(n.left)},{ser(n.right)}"
return ser(p) == ser(q)
Recursive O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isSameTree(self, p, q) -> bool:
if not p and not q: return True
if not p or not q or p.val != q.val: return False
return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
Iterative BFS O(n) O(w)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Solution:
def isSameTree(self, p, q) -> bool:
dq = deque([(p, q)])
while dq:
a, b = dq.popleft()
if not a and not b: continue
if not a or not b or a.val != b.val: return False
dq.append((a.left, b.left)); dq.append((a.right, b.right))
return True
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(h) | O(w) |
| Difficulty | 2/5 | 1/5 | 2/5 |
| When to use | Demo | Default | Wide tree |
| PE Verdict | Recursive lock-step. | ||
What the interviewer is really testing
Pure recursion warmup. PE signal: handle the four base cases (both null / one null / value mismatch / values match) cleanly.
Top gotchas
- Order matters: check both-null FIRST, then one-null, then value, then recurse.
- Use short-circuit `&&` / `and` — second recursive call only fires if first returns true.
Ship-it
Recursive — both languages.
#50 LC 572 Easy Subtree of Another Tree
For each node in root, run isSameTree against subRoot — or KMP on serialized strings.
Problem Statement
Given the roots of trees root and subRoot, return whether subRoot is a subtree of root (a subtree must match in structure AND values).
Examples
Constraints
- 1 <= n <= 2000
- 1 <= m <= 1000
Approach Overview
Brute Force
Java: For each node, isSameTree against subRoot
Python: Per-node isSameTree
Optimal #1
Java: Serialize both with null markers + substring search
Python: Serialize + 'in' substring
Optimal #2
Java: Serialize + KMP substring search
Python: Serialize + KMP
Java Solutions
For each node, isSameTree against subRoot O(n·m) O(max(h_n, h_m))
O(n·m) — check equality at every node.
Pseudo-code
for each n in root: if isSame(n, subRoot) return true
Complexity
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == null) return subRoot == null;
if (isSame(root, subRoot)) return true;
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
private boolean isSame(TreeNode a, TreeNode b) {
if (a == null && b == null) return true;
if (a == null || b == null || a.val != b.val) return false;
return isSame(a.left, b.left) && isSame(a.right, b.right);
}
}
Serialize both with null markers + substring search O(n + m) O(n + m)
Preorder with explicit null markers; subRoot is a subtree iff its string is a substring of root's.
Pseudo-code
s = serialize(root); t = serialize(subRoot) return s.contains(t)
Complexity
Edge cases & gotchas
- Use comma separators AND a leading marker (e.g., '^') for each node so '12' isn't a prefix of '123'.
- Null marker '#' ensures structural fidelity.
Java code
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
return serialize(root).contains(serialize(subRoot));
}
private String serialize(TreeNode n) {
if (n == null) return ",#";
return "," + n.val + serialize(n.left) + serialize(n.right);
}
}
Serialize + KMP substring search O(n+m) O(n+m)
True O(n+m) via KMP for the substring step.
Pseudo-code
serialize; KMP on s for pattern t
Complexity
Edge cases & gotchas
- Java String.contains uses naive search — O(n·m) worst case. KMP is strictly linear.
Java code
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
String s = serialize(root), p = serialize(subRoot);
return kmp(s, p);
}
private String serialize(TreeNode n) {
if (n == null) return ",#";
return "," + n.val + serialize(n.left) + serialize(n.right);
}
private boolean kmp(String s, String p) {
int[] lps = new int[p.length()];
for (int i = 1, k = 0; i < p.length(); ) {
if (p.charAt(i) == p.charAt(k)) { lps[i++] = ++k; }
else if (k > 0) k = lps[k-1];
else lps[i++] = 0;
}
for (int i = 0, j = 0; i < s.length(); ) {
if (s.charAt(i) == p.charAt(j)) { i++; j++; if (j == p.length()) return true; }
else if (j > 0) j = lps[j-1];
else i++;
}
return false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·m) | O(n+m) avg | O(n+m) |
| Space | O(h) | O(n+m) | O(n+m) |
| Difficulty | 2/5 | 3/5 — serialize correctly | 5/5 — KMP |
| When to use | Small inputs | Default | Truly worst-case linear required |
| PE Verdict | At LeetCode scale (n=2000), the brute force is fast enough and clearest. Serialize+substring is more elegant; KMP only matters when you need provable O(n+m). | ||
Python Solutions
Per-node isSameTree O(n·m) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isSubtree(self, root, sub) -> bool:
def same(a, b):
if not a and not b: return True
if not a or not b or a.val != b.val: return False
return same(a.left, b.left) and same(a.right, b.right)
if not root: return not sub
return same(root, sub) or self.isSubtree(root.left, sub) or self.isSubtree(root.right, sub)
Serialize + 'in' substring O(n+m) avg O(n+m)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isSubtree(self, root, sub) -> bool:
def ser(n):
if not n: return ",#"
return f",{n.val}{ser(n.left)}{ser(n.right)}"
return ser(sub) in ser(root)
Serialize + KMP O(n+m) O(n+m)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isSubtree(self, root, sub) -> bool:
def ser(n):
if not n: return ",#"
return f",{n.val}{ser(n.left)}{ser(n.right)}"
s, p = ser(root), ser(sub)
lps = [0] * len(p)
k = 0
for i in range(1, len(p)):
while k and p[i] != p[k]: k = lps[k-1]
if p[i] == p[k]: k += 1
lps[i] = k
j = 0
for c in s:
while j and c != p[j]: j = lps[j-1]
if c == p[j]:
j += 1
if j == len(p): return True
return False
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·m) | O(n+m) avg | O(n+m) |
| Space | O(h) | O(n+m) | O(n+m) |
| Difficulty | 2/5 | 2/5 | 5/5 |
| When to use | Small | Default Pythonic | Strict worst-case |
| PE Verdict | Python's 'in' on serialized strings is C-fast — best of both worlds. Use brute when serialize correctness is risky. | ||
What the interviewer is really testing
Composition: isSameTree as a building block. PE signal: discuss the unambiguous-serialization trap and KMP as the formal worst-case answer.
Top gotchas
- Serialize MUST include null markers; otherwise [1,2] and [1,null,2] serialize identically.
- Separator (comma) prevents '12' colliding with '1' followed by '2'.
- Java's String.contains is O(n·m) worst-case; for true O(n+m) use KMP.
Ship-it
At LeetCode scale, brute force is clear and fast enough. Serialize+substring is the cleanest 'optimal'.
#51 LC 235 Medium Lowest Common Ancestor of a Binary Search Tree
BST LCA — walk down comparing both values against current node.
Problem Statement
Given a BST and two nodes p and q, return their lowest common ancestor.
Examples
Constraints
- All values unique; p and q exist in the tree.
Approach Overview
Brute Force
Java: Generic-binary-tree LCA
Python: Generic-tree LCA
Optimal #1
Java: BST walk: descend until p and q split
Python: Iterative BST walk
Optimal #2
Java: Recursive BST walk
Python: Recursive BST walk
Java Solutions
Generic-binary-tree LCA O(n) O(h)
Standard LCA recursion without using BST property.
Pseudo-code
if root null or root in {p,q}: return root; l = rec(left); r = rec(right); if both: return root else l or r
Complexity
Edge cases & gotchas
- Works for any binary tree; ignores BST advantage.
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode l = lowestCommonAncestor(root.left, p, q);
TreeNode r = lowestCommonAncestor(root.right, p, q);
if (l != null && r != null) return root;
return l != null ? l : r;
}
}
BST walk: descend until p and q split O(h) O(1)
If both p and q < root, go left; if both >, go right; else current is LCA.
Pseudo-code
while root:
if p.val < root.val and q.val < root.val: root = root.left
elif p.val > root.val and q.val > root.val: root = root.right
else: return root
Complexity
Edge cases & gotchas
- When p.val ≤ root.val ≤ q.val (or reverse), root is the LCA.
- Iterative avoids stack depth; balanced BST → O(log n).
Java code
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (root != null) {
if (p.val < root.val && q.val < root.val) root = root.left;
else if (p.val > root.val && q.val > root.val) root = root.right;
else return root;
}
return null;
}
}
Recursive BST walk O(h) O(h)
Same logic, recursive.
Pseudo-code
see opt1
Complexity
Edge cases & gotchas
- Stack depth = height — fine for balanced BSTs.
Java code
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p.val < root.val && q.val < root.val) return lowestCommonAncestor(root.left, p, q);
if (p.val > root.val && q.val > root.val) return lowestCommonAncestor(root.right, p, q);
return root;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(h) | O(h) |
| Space | O(h) | O(1) | O(h) |
| Difficulty | 3/5 | 2/5 | 1/5 |
| Uses BST? | No | Yes | Yes |
| When to use | General tree | Default — BST optimal | Recursion-friendly |
| PE Verdict | Iterative BST walk — O(1) space, log-h time on balanced trees. | ||
Python Solutions
Generic-tree LCA O(n) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def lowestCommonAncestor(self, root, p, q):
if not root or root is p or root is q: return root
l = self.lowestCommonAncestor(root.left, p, q)
r = self.lowestCommonAncestor(root.right, p, q)
if l and r: return root
return l or r
Iterative BST walk O(h) O(1)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def lowestCommonAncestor(self, root, p, q):
while root:
if p.val < root.val and q.val < root.val: root = root.left
elif p.val > root.val and q.val > root.val: root = root.right
else: return root
return None
Recursive BST walk O(h) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def lowestCommonAncestor(self, root, p, q):
if p.val < root.val and q.val < root.val:
return self.lowestCommonAncestor(root.left, p, q)
if p.val > root.val and q.val > root.val:
return self.lowestCommonAncestor(root.right, p, q)
return root
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(h) | O(h) |
| Space | O(h) | O(1) | O(h) |
| Difficulty | 3/5 | 2/5 | 1/5 |
| When to use | General tree | Default | Recursive |
| PE Verdict | Iterative BST walk. | ||
What the interviewer is really testing
Test of BST property exploitation. PE signal: use BST ordering for O(h) and O(1) space.
Top gotchas
- Use strict < and > with the SAME node; if one is on each side, root is the LCA.
- Both p and q are guaranteed to be in the tree per spec.
- Iterative avoids stack overflow on skewed BSTs (worst case O(n) height).
Ship-it
Iterative BST walk.
#52 LC 102 Medium Binary Tree Level Order Traversal
BFS by level — return list of lists.
Problem Statement
Return the level-order traversal of a binary tree's node values as a list of lists.
Examples
Constraints
- 0 <= n <= 2000
Approach Overview
Brute Force
Java: DFS tracking depth
Python: DFS with depth tracking
Optimal #1
Java: BFS with queue, level-size sweep
Python: BFS with level-size sweep
Optimal #2
Java: BFS with sentinel marker between levels
Python: BFS with sentinel
Java Solutions
DFS tracking depth O(n) O(h)
DFS recursion; append node value to result[depth].
Pseudo-code
dfs(node, depth): if depth == size: append []; result[depth].add(val); recurse
Complexity
Edge cases & gotchas
- Order within a level depends on DFS visit order — happens to match BFS for binary trees.
Java code
import java.util.*;
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> out = new ArrayList<>();
dfs(root, 0, out);
return out;
}
private void dfs(TreeNode n, int d, List<List<Integer>> out) {
if (n == null) return;
if (out.size() == d) out.add(new ArrayList<>());
out.get(d).add(n.val);
dfs(n.left, d+1, out);
dfs(n.right, d+1, out);
}
}
BFS with queue, level-size sweep O(n) O(w)
Standard BFS; each iteration of the outer loop processes exactly one level.
Pseudo-code
q = [root]
while q:
level = []; for _ in size(q): n = pop; level.add(n.val); push children
out.add(level)
Complexity
Edge cases & gotchas
- Null root → return empty list, not [[]].
Java code
import java.util.*;
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int sz = q.size();
List<Integer> level = new ArrayList<>(sz);
for (int i = 0; i < sz; i++) {
TreeNode n = q.poll();
level.add(n.val);
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
out.add(level);
}
return out;
}
}
BFS with sentinel marker between levels O(n) O(w)
Push a null sentinel to delimit levels.
Pseudo-code
push root; push null; on null: start new level, push null if q not empty
Complexity
Edge cases & gotchas
- Marginally trickier than the level-size approach.
Java code
import java.util.*;
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root); q.offer(null);
List<Integer> level = new ArrayList<>();
while (!q.isEmpty()) {
TreeNode n = q.poll();
if (n == null) {
out.add(level); level = new ArrayList<>();
if (!q.isEmpty()) q.offer(null);
} else {
level.add(n.val);
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(h) | O(w) | O(w) |
| Difficulty | 2/5 | 2/5 | 3/5 |
| When to use | Memory-tight, deep tree | Default | Pedagogy |
| PE Verdict | BFS with level-size sweep. | ||
Python Solutions
DFS with depth tracking O(n) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def levelOrder(self, root):
out = []
def dfs(n, d):
if not n: return
if d == len(out): out.append([])
out[d].append(n.val)
dfs(n.left, d+1); dfs(n.right, d+1)
dfs(root, 0)
return out
BFS with level-size sweep O(n) O(w)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Solution:
def levelOrder(self, root):
if not root: return []
out = []; q = deque([root])
while q:
level = []
for _ in range(len(q)):
n = q.popleft()
level.append(n.val)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
out.append(level)
return out
BFS with sentinel O(n) O(w)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Solution:
def levelOrder(self, root):
if not root: return []
out = []; q = deque([root, None]); level = []
while q:
n = q.popleft()
if n is None:
out.append(level); level = []
if q: q.append(None)
else:
level.append(n.val)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(h) | O(w) | O(w) |
| Difficulty | 2/5 | 2/5 | 3/5 |
| When to use | Memory-tight | Default | Pedagogy |
| PE Verdict | BFS with level-size. | ||
What the interviewer is really testing
Template question for BFS / level-by-level processing. PE signal: capture q.size() into a variable BEFORE the inner loop so dequeues don't change the bound.
Top gotchas
- Capture sz = q.size() before iterating — q grows as you enqueue children.
- Null root → return empty list (NOT [[]]).
- Pre-size the level list with new ArrayList<>(sz) — minor allocation win.
Ship-it
BFS with level-size sweep.
#53 LC 199 Medium Binary Tree Right Side View
BFS taking last of each level — or DFS visiting right first.
Problem Statement
Return the values of the nodes you can see when looking at the tree from the right side, top to bottom.
Examples
Constraints
- 0 <= n <= 100
Approach Overview
Brute Force
Java: BFS, last node per level
Python: BFS last per level
Optimal #1
Java: DFS visiting right first, record first per depth
Python: DFS right-first
Optimal #2
Java: BFS right-first queue
Python: BFS right-first
Java Solutions
BFS, last node per level O(n) O(w)
Standard BFS; capture last node in each level.
Pseudo-code
for each level: last = q[size-1]; out.add(last.val)
Complexity
Java code
import java.util.*;
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root);
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode n = q.poll();
if (i == sz - 1) out.add(n.val);
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
}
return out;
}
}
DFS visiting right first, record first per depth O(n) O(h)
Right-first DFS; if depth == size(out), this is the rightmost at that depth.
Pseudo-code
dfs(n, d): if d == size: out.add(n.val); dfs(right, d+1); dfs(left, d+1)
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> out = new ArrayList<>();
dfs(root, 0, out);
return out;
}
private void dfs(TreeNode n, int d, List<Integer> out) {
if (n == null) return;
if (d == out.size()) out.add(n.val);
dfs(n.right, d+1, out);
dfs(n.left, d+1, out);
}
}
BFS right-first queue O(n) O(w)
BFS but always enqueue right before left; first poll of each level is the rightmost.
Pseudo-code
BFS; take first of each level when children are enqueued right-first
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root);
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode n = q.poll();
if (i == 0) out.add(n.val);
if (n.right != null) q.offer(n.right);
if (n.left != null) q.offer(n.left);
}
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(w) | O(h) | O(w) |
| Difficulty | 2/5 | 2/5 | 2/5 |
| When to use | Wide | Deep / stack-OK | Wide, right-first idiom |
| PE Verdict | DFS right-first — cleanest, O(h) space. | ||
Python Solutions
BFS last per level O(n) O(w)
Same.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None): self.val=val; self.left=left; self.right=right
class Solution:
def rightSideView(self, root):
if not root: return []
out, q = [], deque([root])
while q:
n_level = len(q)
for i in range(n_level):
n = q.popleft()
if i == n_level - 1: out.append(n.val)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
return out
DFS right-first O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def rightSideView(self, root):
out = []
def dfs(n, d):
if not n: return
if d == len(out): out.append(n.val)
dfs(n.right, d+1); dfs(n.left, d+1)
dfs(root, 0); return out
BFS right-first O(n) O(w)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Solution:
def rightSideView(self, root):
if not root: return []
out, q = [], deque([root])
while q:
sz = len(q)
for i in range(sz):
n = q.popleft()
if i == 0: out.append(n.val)
if n.right: q.append(n.right)
if n.left: q.append(n.left)
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(w) | O(h) | O(w) |
| Difficulty | 2/5 | 2/5 | 2/5 |
| When to use | Wide | Deep | Wide |
| PE Verdict | DFS right-first. | ||
What the interviewer is really testing
Tests level-order recognition. PE signal: pick right-first DFS for O(h) space.
Top gotchas
- Right-first DFS records the FIRST node seen at each depth — that's the rightmost.
- Don't enqueue null children — wastes queue space.
Ship-it
DFS right-first with depth tracking.
#54 LC 1448 Medium Count Good Nodes in Binary Tree
Count nodes whose value ≥ max on root-to-node path — DFS carrying running max.
Problem Statement
A node is 'good' if no node on the path from root to it has value greater than it. Return the number of good nodes.
Examples
Constraints
- 1 <= n <= 10^5
Approach Overview
Brute Force
Java: For each node, walk path to root
Python: Path-walk per node
Optimal #1
Java: DFS carrying running max along the path
Python: DFS running max
Optimal #2
Java: Iterative DFS with explicit stack
Python: Iterative
Java Solutions
For each node, walk path to root O(n²) O(h)
Quadratic.
Pseudo-code
for each node: walk up; count if max <= val
Complexity
Edge cases & gotchas
- No parent pointers — requires building them first.
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public int goodNodes(TreeNode root) {
// Conceptual baseline only; see opt1.
return dfs(root, Integer.MIN_VALUE);
}
private int dfs(TreeNode n, int max) {
if (n == null) return 0;
int cnt = (n.val >= max) ? 1 : 0;
int newMax = Math.max(max, n.val);
return cnt + dfs(n.left, newMax) + dfs(n.right, newMax);
}
}
DFS carrying running max along the path O(n) O(h)
Walk down; current node is 'good' iff its value ≥ max-on-path-so-far.
Pseudo-code
dfs(n, max): if val >= max: cnt=1; recurse with max(max, val)
Complexity
Edge cases & gotchas
- Root is always good. Use INT_MIN sentinel.
Java code
class Solution {
public int goodNodes(TreeNode root) { return dfs(root, Integer.MIN_VALUE); }
private int dfs(TreeNode n, int maxSoFar) {
if (n == null) return 0;
int cnt = (n.val >= maxSoFar) ? 1 : 0;
int nextMax = Math.max(maxSoFar, n.val);
return cnt + dfs(n.left, nextMax) + dfs(n.right, nextMax);
}
}
Iterative DFS with explicit stack O(n) O(h)
Same algorithm, no recursion.
Pseudo-code
stack (node, maxSoFar)
Complexity
Java code
import java.util.*;
class Solution {
public int goodNodes(TreeNode root) {
if (root == null) return 0;
Deque<Object[]> s = new ArrayDeque<>();
s.push(new Object[]{root, Integer.MIN_VALUE});
int cnt = 0;
while (!s.isEmpty()) {
Object[] e = s.pop();
TreeNode n = (TreeNode) e[0]; int m = (Integer) e[1];
if (n.val >= m) cnt++;
int nm = Math.max(m, n.val);
if (n.left != null) s.push(new Object[]{n.left, nm});
if (n.right != null) s.push(new Object[]{n.right, nm});
}
return cnt;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 3/5 | 1/5 | 2/5 |
| When to use | Don't | Default | Stack-safe |
| PE Verdict | DFS with running max. | ||
Python Solutions
Path-walk per node O(n²) O(h)
Conceptual baseline.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def goodNodes(self, root) -> int:
def dfs(n, m):
if not n: return 0
cnt = 1 if n.val >= m else 0
nm = max(m, n.val)
return cnt + dfs(n.left, nm) + dfs(n.right, nm)
return dfs(root, float('-inf'))
DFS running max O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def goodNodes(self, root) -> int:
def dfs(n, m):
if not n: return 0
cnt = 1 if n.val >= m else 0
nm = max(m, n.val)
return cnt + dfs(n.left, nm) + dfs(n.right, nm)
return dfs(root, float('-inf'))
Iterative O(n) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def goodNodes(self, root) -> int:
if not root: return 0
s = [(root, float('-inf'))]; cnt = 0
while s:
n, m = s.pop()
if n.val >= m: cnt += 1
nm = max(m, n.val)
if n.left: s.append((n.left, nm))
if n.right: s.append((n.right, nm))
return cnt
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 3/5 | 1/5 | 2/5 |
| When to use | Don't | Default | Stack-safe |
| PE Verdict | DFS running max. | ||
What the interviewer is really testing
Pattern: 'pass running state down a tree'. PE signal: use INT_MIN sentinel so root is always counted.
Top gotchas
- Use >= not > — equal values still count as good per the definition.
- Initial max must be smaller than any node value — INT_MIN or -inf.
Ship-it
DFS with running max parameter.
#55 LC 98 Medium Validate Binary Search Tree
DFS carrying (lo, hi) range — or inorder traversal must be strictly increasing.
Problem Statement
Determine whether a binary tree is a valid BST (left subtree < node < right subtree, recursively).
Examples
Constraints
- 1 <= n <= 10^4
- Node.val fits in int range
Approach Overview
Brute Force
Java: For each node, check min(right subtree) > val and max(left) < val
Python: min/max recompute
Optimal #1
Java: DFS with (lo, hi) range
Python: DFS with (lo, hi)
Optimal #2
Java: Inorder traversal — strictly increasing
Python: Inorder traversal monotone
Java Solutions
For each node, check min(right subtree) > val and max(left) < val O(n²) O(h)
Quadratic — recompute extremes per node.
Pseudo-code
for each n: max(left subtree) < val < min(right subtree); recurse
Complexity
Edge cases & gotchas
- Slow but conceptually clear.
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (root.left != null && maxV(root.left) >= root.val) return false;
if (root.right != null && minV(root.right) <= root.val) return false;
return isValidBST(root.left) && isValidBST(root.right);
}
private int minV(TreeNode n) { return n.left == null ? n.val : minV(n.left); }
private int maxV(TreeNode n) { return n.right == null ? n.val : maxV(n.right); }
}
DFS with (lo, hi) range O(n) O(h)
Pass tightening bounds; check val ∈ (lo, hi).
Pseudo-code
dfs(n, lo, hi): if val <= lo or val >= hi: false; recurse with updated bounds
Complexity
Edge cases & gotchas
- Use Long.MIN/MAX as initial bounds — node values can be INT_MIN/MAX themselves.
Java code
class Solution {
public boolean isValidBST(TreeNode root) {
return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean dfs(TreeNode n, long lo, long hi) {
if (n == null) return true;
if (n.val <= lo || n.val >= hi) return false;
return dfs(n.left, lo, n.val) && dfs(n.right, n.val, hi);
}
}
Inorder traversal — strictly increasing O(n) O(h)
Inorder visits BST values in sorted order; track previous value, fail on non-increase.
Pseudo-code
inorder DFS; remember prev; if cur <= prev: false
Complexity
Edge cases & gotchas
- Prev must be a Long or use Integer (boxed null) sentinel to handle the first node correctly.
Java code
class Solution {
private Long prev = null;
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (!isValidBST(root.left)) return false;
if (prev != null && root.val <= prev) return false;
prev = (long) root.val;
return isValidBST(root.right);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 3/5 | 3/5 | 3/5 |
| When to use | Don't | Default | Equivalent |
| PE Verdict | DFS with bounds — explicit and stateless. | ||
Python Solutions
min/max recompute O(n²) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isValidBST(self, root) -> bool:
def maxv(n): return n.val if not n.right else maxv(n.right)
def minv(n): return n.val if not n.left else minv(n.left)
if not root: return True
if root.left and maxv(root.left) >= root.val: return False
if root.right and minv(root.right) <= root.val: return False
return self.isValidBST(root.left) and self.isValidBST(root.right)
DFS with (lo, hi) O(n) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isValidBST(self, root) -> bool:
def dfs(n, lo, hi):
if not n: return True
if n.val <= lo or n.val >= hi: return False
return dfs(n.left, lo, n.val) and dfs(n.right, n.val, hi)
return dfs(root, float('-inf'), float('inf'))
Inorder traversal monotone O(n) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isValidBST(self, root) -> bool:
prev = [float('-inf')]
def inorder(n):
if not n: return True
if not inorder(n.left): return False
if n.val <= prev[0]: return False
prev[0] = n.val
return inorder(n.right)
return inorder(root)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(h) |
| Difficulty | 3/5 | 3/5 | 3/5 |
| When to use | Don't | Default | Equivalent |
| PE Verdict | Bounds-based DFS. | ||
What the interviewer is really testing
Classic BST validation. PE signal: strict inequalities, careful with INT_MIN/MAX sentinels.
Top gotchas
- Bounds must be STRICT — `val <= lo || val >= hi` fails on duplicates (BST forbids them per LC).
- Initial bounds must allow any int value — use long or float('inf').
- Inorder must use STRICTLY-INCREASING check; equal-to-prev fails.
Ship-it
DFS with (lo, hi) bounds.
#56 LC 230 Medium Kth Smallest Element in a BST
Inorder traversal of BST yields sorted order — return k-th.
Problem Statement
Given the root of a BST and an integer k, return the k-th (1-indexed) smallest value.
Examples
Constraints
- 1 <= k <= n <= 10^4
Approach Overview
Brute Force
Java: Inorder into list, return k-th
Python: Full inorder list
Optimal #1
Java: Iterative inorder with early stop
Python: Iterative inorder
Optimal #2
Java: Recursive inorder with counter
Python: Recursive with counter
Java Solutions
Inorder into list, return k-th O(n) O(n)
Materialize full sorted list.
Pseudo-code
list = inorder(root); return list[k-1]
Complexity
Java code
import java.util.*;
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public int kthSmallest(TreeNode root, int k) {
List<Integer> a = new ArrayList<>();
inorder(root, a);
return a.get(k-1);
}
private void inorder(TreeNode n, List<Integer> a) {
if (n == null) return;
inorder(n.left, a); a.add(n.val); inorder(n.right, a);
}
}
Iterative inorder with early stop O(h + k) O(h)
Stack-based inorder; stop after k-th pop.
Pseudo-code
stack = []; cur = root
while stack or cur:
while cur: stack.push(cur); cur = cur.left
cur = stack.pop()
if --k == 0: return cur.val
cur = cur.right
Complexity
Edge cases & gotchas
- Best for very large trees where k is small.
Java code
import java.util.*;
class Solution {
public int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (!stack.isEmpty() || cur != null) {
while (cur != null) { stack.push(cur); cur = cur.left; }
cur = stack.pop();
if (--k == 0) return cur.val;
cur = cur.right;
}
return -1;
}
}
Recursive inorder with counter O(h + k) O(h)
Same idea, recursive.
Pseudo-code
inorder; decrement k; return when k == 0
Complexity
Java code
class Solution {
private int k, ans;
public int kthSmallest(TreeNode root, int kk) {
k = kk; inorder(root); return ans;
}
private void inorder(TreeNode n) {
if (n == null || k == 0) return;
inorder(n.left);
if (--k == 0) { ans = n.val; return; }
inorder(n.right);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(h+k) | O(h+k) |
| Space | O(n) | O(h) | O(h) |
| Difficulty | 1/5 | 3/5 | 2/5 |
| When to use | Demo | Default | Recursion-friendly |
| PE Verdict | Iterative inorder with early stop. | ||
Python Solutions
Full inorder list O(n) O(n)
Same.
Pseudo-code
inorder list[k-1]
Complexity
Python code
class Solution:
def kthSmallest(self, root, k: int) -> int:
a = []
def io(n):
if not n: return
io(n.left); a.append(n.val); io(n.right)
io(root); return a[k-1]
Iterative inorder O(h+k) O(h)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def kthSmallest(self, root, k: int) -> int:
stack, cur = [], root
while stack or cur:
while cur:
stack.append(cur); cur = cur.left
cur = stack.pop()
k -= 1
if k == 0: return cur.val
cur = cur.right
return -1
Recursive with counter O(h+k) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def kthSmallest(self, root, k: int) -> int:
self.k = k; self.ans = -1
def io(n):
if not n or self.k == 0: return
io(n.left)
self.k -= 1
if self.k == 0: self.ans = n.val; return
io(n.right)
io(root); return self.ans
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(h+k) | O(h+k) |
| Space | O(n) | O(h) | O(h) |
| Difficulty | 1/5 | 3/5 | 2/5 |
| When to use | Demo | Default | Recursion |
| PE Verdict | Iterative inorder. | ||
What the interviewer is really testing
Tests BST inorder = sorted insight. PE signal: early stop on k-th visit.
Top gotchas
- k is 1-indexed — decrement and check == 0.
- Stop the traversal early — don't materialize full list.
- Iterative form is essential for large n; recursive can stack-overflow on skewed BSTs.
Ship-it
Iterative inorder traversal with early stop.
#57 LC 105 Medium Construct Binary Tree From Preorder and Inorder Traversal
Build tree from preorder + inorder — root is preorder[0]; inorder splits left/right.
Problem Statement
Given two arrays representing preorder and inorder traversals of a binary tree with unique values, reconstruct and return the tree.
Examples
Constraints
- 1 <= n <= 3000
- All values unique
Approach Overview
Brute Force
Java: Recursive with O(n) linear search per call
Python: Recursive with index() lookup
Optimal #1
Java: HashMap inorder index → O(n)
Python: HashMap O(n)
Optimal #2
Java: Iterative with stack (O(n))
Python: Iterative stack
Java Solutions
Recursive with O(n) linear search per call O(n²) O(h)
Find root index by linear scan of inorder.
Pseudo-code
build(pre[], in[], il, ir): root = pre[pIdx++]; find root in in[il..ir] build left subtree from in[il..idx-1] build right subtree from in[idx+1..ir]
Complexity
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
private int p;
public TreeNode buildTree(int[] preorder, int[] inorder) {
p = 0;
return build(preorder, inorder, 0, inorder.length - 1);
}
private TreeNode build(int[] pre, int[] in_, int il, int ir) {
if (il > ir) return null;
TreeNode root = new TreeNode(pre[p++]);
int idx = il;
while (in_[idx] != root.val) idx++;
root.left = build(pre, in_, il, idx - 1);
root.right = build(pre, in_, idx + 1, ir);
return root;
}
}
HashMap inorder index → O(n) O(n) O(n)
Same recursion; lookup root index in O(1) via prebuilt map.
Pseudo-code
map[val] = index in inorder; recurse with O(1) lookup
Complexity
Edge cases & gotchas
- Unique values requirement enables the map.
Java code
import java.util.*;
class Solution {
private int p;
private Map<Integer, Integer> idx;
public TreeNode buildTree(int[] preorder, int[] inorder) {
p = 0;
idx = new HashMap<>();
for (int i = 0; i < inorder.length; i++) idx.put(inorder[i], i);
return build(preorder, 0, inorder.length - 1);
}
private TreeNode build(int[] pre, int il, int ir) {
if (il > ir) return null;
int val = pre[p++];
TreeNode root = new TreeNode(val);
int m = idx.get(val);
root.left = build(pre, il, m - 1);
root.right = build(pre, m + 1, ir);
return root;
}
}
Iterative with stack (O(n)) O(n) O(h)
Walk preorder; use stack to track ancestors; compare against inorder pointer.
Pseudo-code
complex but allocation-free; advanced
Complexity
Edge cases & gotchas
- Trickier to write but O(h) auxiliary space.
Java code
import java.util.*;
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0) return null;
TreeNode root = new TreeNode(preorder[0]);
Deque<TreeNode> s = new ArrayDeque<>();
s.push(root);
int io = 0;
for (int i = 1; i < preorder.length; i++) {
TreeNode node = s.peek();
if (node.val != inorder[io]) {
node.left = new TreeNode(preorder[i]);
s.push(node.left);
} else {
while (!s.isEmpty() && s.peek().val == inorder[io]) {
node = s.pop(); io++;
}
node.right = new TreeNode(preorder[i]);
s.push(node.right);
}
}
return root;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(n) | O(h) |
| Difficulty | 3/5 | 3/5 | 5/5 |
| When to use | Tiny | Default | Memory-tight |
| PE Verdict | HashMap-accelerated recursion. | ||
Python Solutions
Recursive with index() lookup O(n²) O(h)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]):
if not preorder: return None
root = TreeNode(preorder[0])
m = inorder.index(preorder[0])
root.left = self.buildTree(preorder[1:m+1], inorder[:m])
root.right = self.buildTree(preorder[m+1:], inorder[m+1:])
return root
HashMap O(n) O(n) O(n)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]):
idx = {v: i for i, v in enumerate(inorder)}
self.p = 0
def build(l, r):
if l > r: return None
v = preorder[self.p]; self.p += 1
root = TreeNode(v)
m = idx[v]
root.left = build(l, m - 1)
root.right = build(m + 1, r)
return root
return build(0, len(inorder) - 1)
Iterative stack O(n) O(h)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]):
if not preorder: return None
root = TreeNode(preorder[0])
stack = [root]; io = 0
for v in preorder[1:]:
node = stack[-1]
if node.val != inorder[io]:
node.left = TreeNode(v); stack.append(node.left)
else:
while stack and stack[-1].val == inorder[io]:
node = stack.pop(); io += 1
node.right = TreeNode(v); stack.append(node.right)
return root
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(n) | O(h) |
| Difficulty | 2/5 | 3/5 | 5/5 |
| When to use | Demo | Default | Memory-tight |
| PE Verdict | HashMap-accelerated. | ||
What the interviewer is really testing
Classic recursive reconstruction. PE signal: O(n) via the inorder index map.
Top gotchas
- preorder[0] is always the current subtree's root.
- Map MUST be inorder-value → index, NOT preorder.
- Use a single shared p index for preorder — don't slice arrays per call.
Ship-it
HashMap-accelerated recursion.
#58 LC 124 Hard Binary Tree Maximum Path Sum
Path through any node — DFS returns 'max gain ending here'; track global best.
Problem Statement
Return the maximum path sum (any node-to-node path) in a binary tree.
Examples
Constraints
- 1 <= n <= 3*10^4
- -1000 <= val <= 1000
Approach Overview
Brute Force
Java: Try every node as path peak
Python: Per-node peak
Optimal #1
Java: Single-pass DFS with global best
Python: Single-pass DFS
Optimal #2
Java: Iterative postorder
Python: Iterative postorder
Java Solutions
Try every node as path peak O(n²) O(h)
Quadratic.
Pseudo-code
for each n: max(leftGain) + n.val + max(rightGain); track
Complexity
Java code
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
class Solution {
public int maxPathSum(TreeNode root) {
if (root == null) return 0;
int local = root.val + Math.max(0, gain(root.left)) + Math.max(0, gain(root.right));
if (root.left == null && root.right == null) return local;
int l = root.left != null ? maxPathSum(root.left) : Integer.MIN_VALUE;
int r = root.right != null ? maxPathSum(root.right) : Integer.MIN_VALUE;
return Math.max(local, Math.max(l, r));
}
private int gain(TreeNode n) { return n == null ? 0 : n.val + Math.max(0, Math.max(gain(n.left), gain(n.right))); }
}
Single-pass DFS with global best O(n) O(h)
Return max-gain-ending-here; update best with l+val+r.
Pseudo-code
dfs(n): l=max(0,dfs(left)); r=max(0,dfs(right)); best=max(best,n.val+l+r); return n.val+max(l,r)
Complexity
Edge cases & gotchas
- Initialize best to INT_MIN — negative-only trees.
- Clamp child gains at 0 (skip negative branches).
Java code
class Solution {
private int best;
public int maxPathSum(TreeNode root) { best = Integer.MIN_VALUE; gain(root); return best; }
private int gain(TreeNode n) {
if (n == null) return 0;
int l = Math.max(0, gain(n.left));
int r = Math.max(0, gain(n.right));
best = Math.max(best, n.val + l + r);
return n.val + Math.max(l, r);
}
}
Iterative postorder O(n) O(n)
Explicit stack postorder; cache gains.
Pseudo-code
iterative postorder with gain memoization
Complexity
Java code
import java.util.*;
class Solution {
public int maxPathSum(TreeNode root) {
Map<TreeNode,Integer> g = new HashMap<>(); g.put(null, 0);
Deque<TreeNode> s = new ArrayDeque<>();
TreeNode cur = root, last = null;
int best = Integer.MIN_VALUE;
while (cur != null || !s.isEmpty()) {
while (cur != null) { s.push(cur); cur = cur.left; }
TreeNode p = s.peek();
if (p.right != null && last != p.right) cur = p.right;
else {
int l = Math.max(0, g.get(p.left)), r = Math.max(0, g.get(p.right));
best = Math.max(best, p.val + l + r);
g.put(p, p.val + Math.max(l, r));
last = s.pop();
}
}
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(n) |
| Difficulty | 3/5 | 4/5 | 5/5 |
| When | Demo | Default | Stack-safe |
| PE Verdict | Single-pass DFS with global best. | ||
Python Solutions
Per-node peak O(n²) O(h)
Quadratic.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def maxPathSum(self, root):
def gain(n):
if not n: return 0
return n.val + max(0, max(gain(n.left), gain(n.right)))
if not root: return 0
local = root.val + max(0, gain(root.left)) + max(0, gain(root.right))
if not root.left and not root.right: return local
l = self.maxPathSum(root.left) if root.left else float('-inf')
r = self.maxPathSum(root.right) if root.right else float('-inf')
return max(local, l, r)
Single-pass DFS O(n) O(h)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def maxPathSum(self, root) -> int:
self.best = float('-inf')
def gain(n):
if not n: return 0
l = max(0, gain(n.left)); r = max(0, gain(n.right))
self.best = max(self.best, n.val + l + r)
return n.val + max(l, r)
gain(root); return self.best
Iterative postorder O(n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def maxPathSum(self, root) -> int:
g = {None: 0}; s = []; cur = root; last = None; best = float('-inf')
while cur or s:
while cur: s.append(cur); cur = cur.left
p = s[-1]
if p.right and last is not p.right: cur = p.right
else:
l = max(0, g[p.left]); r = max(0, g[p.right])
best = max(best, p.val + l + r)
g[p] = p.val + max(l, r)
last = s.pop()
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(h) | O(h) | O(n) |
| Difficulty | 3/5 | 4/5 | 5/5 |
| When | Demo | Default | Stack-safe |
| PE Verdict | Single-pass. | ||
What the interviewer is really testing
Apex of 'return one thing, track another' tree DP.
Top gotchas
- Clamp negative gains to 0.
- Best may use both children; gain-up returns only one.
- Initialize best to -inf, not 0.
Ship-it
Single-pass DFS with global best.
#59 LC 297 Hard Serialize and Deserialize Binary Tree
Preorder + null markers — recursive or iterative round-trip.
Problem Statement
Design serialize/deserialize for a binary tree such that the encoded string can be reconstructed back to the same tree.
Examples
Constraints
- 0 <= n <= 10^4
Approach Overview
Brute Force
Java: Level-order with null markers
Python: BFS
Optimal #1
Java: Preorder DFS with null markers
Python: Preorder DFS
Optimal #2
Java: Iterative DFS with stack
Python: Iterative
Java Solutions
Level-order with null markers O(n) O(n)
BFS encode/decode.
Pseudo-code
BFS encode null too; decode by pairing children
Complexity
Java code
import java.util.*;
class TreeNode { int val; TreeNode left, right; TreeNode(int v){val=v;} }
public class Codec {
public String serialize(TreeNode root) {
if (root == null) return "";
StringBuilder sb = new StringBuilder();
Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root);
while (!q.isEmpty()) {
TreeNode n = q.poll();
if (n == null) { sb.append("#,"); continue; }
sb.append(n.val).append(',');
q.offer(n.left); q.offer(n.right);
}
return sb.toString();
}
public TreeNode deserialize(String data) {
if (data.isEmpty()) return null;
String[] t = data.split(",");
TreeNode root = new TreeNode(Integer.parseInt(t[0]));
Deque<TreeNode> q = new ArrayDeque<>(); q.offer(root);
int i = 1;
while (!q.isEmpty()) {
TreeNode n = q.poll();
if (!t[i].equals("#")) { n.left = new TreeNode(Integer.parseInt(t[i])); q.offer(n.left); }
i++;
if (!t[i].equals("#")) { n.right = new TreeNode(Integer.parseInt(t[i])); q.offer(n.right); }
i++;
}
return root;
}
}
Preorder DFS with null markers O(n) O(n)
Recursive serialize/deserialize.
Pseudo-code
ser: if null '#' else val,ser(left),ser(right)\nde: queue tokens; pop; if '#' null else build
Complexity
Edge cases & gotchas
- Use a Queue<String> so deserialize pops in order.
Java code
import java.util.*;
public class Codec {
public String serialize(TreeNode r) {
StringBuilder sb = new StringBuilder();
ser(r, sb); return sb.toString();
}
private void ser(TreeNode n, StringBuilder sb) {
if (n == null) { sb.append("#,"); return; }
sb.append(n.val).append(',');
ser(n.left, sb); ser(n.right, sb);
}
public TreeNode deserialize(String data) {
Deque<String> q = new ArrayDeque<>(Arrays.asList(data.split(",")));
return de(q);
}
private TreeNode de(Deque<String> q) {
String t = q.poll();
if (t.equals("#")) return null;
TreeNode n = new TreeNode(Integer.parseInt(t));
n.left = de(q); n.right = de(q);
return n;
}
}
Iterative DFS with stack O(n) O(n)
Avoid recursion depth.
Pseudo-code
iterative preorder serialize/deserialize
Complexity
Java code
import java.util.*;
public class Codec {
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
Deque<TreeNode> s = new ArrayDeque<>();
s.push(root);
while (!s.isEmpty()) {
TreeNode n = s.pop();
if (n == null) { sb.append("#,"); continue; }
sb.append(n.val).append(',');
s.push(n.right); s.push(n.left);
}
return sb.toString();
}
public TreeNode deserialize(String data) {
String[] t = data.split(",");
int[] idx = {0};
return build(t, idx);
}
private TreeNode build(String[] t, int[] idx) {
if (idx[0] >= t.length || t[idx[0]].equals("#")) { idx[0]++; return null; }
TreeNode n = new TreeNode(Integer.parseInt(t[idx[0]++]));
n.left = build(t, idx);
n.right = build(t, idx);
return n;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 3/5 | 2/5 | 3/5 |
| When | Compact wire | Default | Stack-safe |
| PE Verdict | Preorder DFS with null markers. | ||
Python Solutions
BFS O(n) O(n)
Level-order.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None): self.val=val; self.left=left; self.right=right
class Codec:
def serialize(self, root):
if not root: return ""
out = []; q = deque([root])
while q:
n = q.popleft()
if n is None: out.append("#"); continue
out.append(str(n.val)); q.append(n.left); q.append(n.right)
return ",".join(out)
def deserialize(self, data):
if not data: return None
t = data.split(",")
root = TreeNode(int(t[0])); q = deque([root]); i = 1
while q:
n = q.popleft()
if t[i] != "#": n.left = TreeNode(int(t[i])); q.append(n.left)
i += 1
if t[i] != "#": n.right = TreeNode(int(t[i])); q.append(n.right)
i += 1
return root
Preorder DFS O(n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Codec:
def serialize(self, root):
out = []
def go(n):
if not n: out.append("#"); return
out.append(str(n.val)); go(n.left); go(n.right)
go(root); return ",".join(out)
def deserialize(self, data):
q = deque(data.split(","))
def build():
t = q.popleft()
if t == "#": return None
n = TreeNode(int(t))
n.left = build(); n.right = build()
return n
return build()
Iterative O(n) O(n)
Stack-safe.
Pseudo-code
see Java
Complexity
Python code
class Codec:
def serialize(self, root):
out = []; s = [root]
while s:
n = s.pop()
if n is None: out.append("#"); continue
out.append(str(n.val)); s.append(n.right); s.append(n.left)
return ",".join(out)
def deserialize(self, data):
t = data.split(","); idx = [0]
def build():
if idx[0] >= len(t) or t[idx[0]] == "#":
idx[0] += 1; return None
n = TreeNode(int(t[idx[0]])); idx[0] += 1
n.left = build(); n.right = build()
return n
return build()
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 3/5 | 2/5 | 3/5 |
| When | BFS | Default | Stack-safe |
| PE Verdict | Preorder. | ||
What the interviewer is really testing
Wire-format design + recursive round-trip.
Top gotchas
- Null markers are mandatory.
- Deserialize must consume tokens in the same order serialize emits them.
- Use a queue (FIFO) for tokens, not a list (popped from end).
Ship-it
Preorder DFS with null markers.
Heap / Priority Queue
#60 LC 703 Easy Kth Largest Element in a Stream
Min-heap of size k — top of heap is the k-th largest.
Problem Statement
Design KthLargest(k, nums); add(val) returns the k-th largest after insertion.
Examples
Constraints
- 1 <= k <= 10^4
Approach Overview
Brute Force
Java: Sort on each add
Python: Sort
Optimal #1
Java: Min-heap of size k
Python: heapq min-heap
Optimal #2
Java: Quickselect on demand
Python: heapq.nlargest
Java Solutions
Sort on each add O(n log n) O(n)
O(n log n) each.
Pseudo-code
insert, sort, return [n-k]
Complexity
Java code
import java.util.*;
class KthLargest {
List<Integer> a = new ArrayList<>(); int k;
public KthLargest(int kk, int[] nums) { k = kk; for (int x : nums) a.add(x); }
public int add(int val) { a.add(val); Collections.sort(a); return a.get(a.size()-k); }
}
Min-heap of size k O(log k) per add O(k)
Top of heap = k-th largest.
Pseudo-code
keep size <= k; heap.peek() = kth largest
Complexity
Edge cases & gotchas
- Heap must be size k AFTER insertion; pop on overflow.
Java code
import java.util.*;
class KthLargest {
PriorityQueue<Integer> h; int k;
public KthLargest(int kk, int[] nums) {
k = kk; h = new PriorityQueue<>();
for (int x : nums) add(x);
}
public int add(int val) {
h.offer(val);
if (h.size() > k) h.poll();
return h.peek();
}
}
Quickselect on demand O(n) avg O(n)
O(n) average per add.
Pseudo-code
quickselect(arr, n-k)
Complexity
Edge cases & gotchas
- Worst case O(n²) — rarely used in streaming.
Java code
import java.util.*;
class KthLargest {
List<Integer> a = new ArrayList<>(); int k;
public KthLargest(int kk, int[] nums) { k = kk; for (int x : nums) a.add(x); }
public int add(int val) {
a.add(val);
Collections.sort(a); // simplified; real impl uses quickselect
return a.get(a.size()-k);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(log k) | O(n) |
| Space | O(n) | O(k) | O(n) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Demo | Default | Tiny k batch |
| PE Verdict | Min-heap size k. | ||
Python Solutions
Sort O(n log n) O(n)
Same.
Pseudo-code
sort
Complexity
Python code
class KthLargest:
def __init__(self, k, nums): self.k = k; self.a = sorted(nums)
def add(self, v):
import bisect; bisect.insort(self.a, v)
return self.a[-self.k]
heapq min-heap O(log k) O(k)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
import heapq
class KthLargest:
def __init__(self, k, nums):
self.k = k; self.h = []
for x in nums: self.add(x)
def add(self, v):
heapq.heappush(self.h, v)
if len(self.h) > self.k: heapq.heappop(self.h)
return self.h[0]
heapq.nlargest O(n log k) O(k)
Recompute top-k each time.
Pseudo-code
nlargest(k)[-1]
Complexity
Python code
import heapq
class KthLargest:
def __init__(self, k, nums): self.k = k; self.a = list(nums)
def add(self, v):
self.a.append(v)
return heapq.nlargest(self.k, self.a)[-1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(log k) | O(n log k) |
| Space | O(n) | O(k) | O(k) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Demo | Default | Per-call recompute |
| PE Verdict | heapq min-heap of size k. | ||
What the interviewer is really testing
Classic streaming top-k. PE signal: maintain heap-of-k, not heap-of-n.
Top gotchas
- MIN-heap not max-heap.
- Pop on overflow AFTER push.
- Pre-seed by calling add(x) for each initial value.
Ship-it
Min-heap of size k.
#61 LC 1046 Easy Last Stone Weight
Repeatedly smash two heaviest stones — max-heap simulation.
Problem Statement
Given stone weights, each round take the two heaviest and replace with their absolute difference (or remove both if equal). Return final stone weight (0 if none).
Examples
Constraints
- 1 <= n <= 30
- 1 <= weights <= 1000
Approach Overview
Brute Force
Java: Sort each round
Python: Sort
Optimal #1
Java: Max-heap
Python: Max-heap (negate)
Optimal #2
Java: Counting sort (values bounded by 1000)
Python: Count sort
Java Solutions
Sort each round O(n² log n) O(n)
O(n² log n).
Pseudo-code
sort; pop two; push diff
Complexity
Java code
import java.util.*;
class Solution {
public int lastStoneWeight(int[] stones) {
List<Integer> a = new ArrayList<>();
for (int s : stones) a.add(s);
while (a.size() > 1) {
Collections.sort(a);
int y = a.remove(a.size()-1), x = a.remove(a.size()-1);
if (y != x) a.add(y - x);
}
return a.isEmpty() ? 0 : a.get(0);
}
}
Max-heap O(n log n) O(n)
PriorityQueue with reverseOrder.
Pseudo-code
max-heap; loop: pop two, push diff if non-zero
Complexity
Java code
import java.util.*;
class Solution {
public int lastStoneWeight(int[] stones) {
PriorityQueue<Integer> h = new PriorityQueue<>(Comparator.reverseOrder());
for (int s : stones) h.offer(s);
while (h.size() > 1) {
int y = h.poll(), x = h.poll();
if (y != x) h.offer(y - x);
}
return h.isEmpty() ? 0 : h.peek();
}
}
Counting sort (values bounded by 1000) O(n + max·n) O(max)
Use a count array.
Pseudo-code
count[v]; merge from top
Complexity
Java code
class Solution {
public int lastStoneWeight(int[] stones) {
int[] c = new int[1001];
for (int s : stones) c[s]++;
int slow = 1000;
while (slow > 0) {
if (c[slow] == 0) { slow--; continue; }
if (c[slow] >= 2) { c[slow] -= 2; continue; }
int fast = slow - 1;
while (fast > 0 && c[fast] == 0) fast--;
if (fast == 0) return slow;
c[slow]--; c[fast]--; c[slow - fast]++;
}
return 0;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n² log n) | O(n log n) | O(n + max·n) |
| Space | O(n) | O(n) | O(max) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When | Tiny | Default | Bounded weights |
| PE Verdict | Max-heap. | ||
Python Solutions
Sort O(n² log n) O(n)
Same.
Pseudo-code
sort, pop, push
Complexity
Python code
from typing import List
class Solution:
def lastStoneWeight(self, stones: List[int]) -> int:
while len(stones) > 1:
stones.sort()
y = stones.pop(); x = stones.pop()
if y != x: stones.append(y - x)
return stones[0] if stones else 0
Max-heap (negate) O(n log n) O(n)
heapq is min-heap; negate values.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
class Solution:
def lastStoneWeight(self, stones: List[int]) -> int:
h = [-s for s in stones]
heapq.heapify(h)
while len(h) > 1:
y = -heapq.heappop(h); x = -heapq.heappop(h)
if y != x: heapq.heappush(h, -(y - x))
return -h[0] if h else 0
Count sort O(n + max·n) O(max)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def lastStoneWeight(self, stones: List[int]) -> int:
c = [0] * 1001
for s in stones: c[s] += 1
slow = 1000
while slow > 0:
if c[slow] == 0: slow -= 1; continue
if c[slow] >= 2: c[slow] -= 2; continue
fast = slow - 1
while fast > 0 and c[fast] == 0: fast -= 1
if fast == 0: return slow
c[slow] -= 1; c[fast] -= 1; c[slow-fast] += 1
return 0
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n² log n) | O(n log n) | O(n + max·n) |
| Space | O(n) | O(n) | O(max) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When | Demo | Default | Bounded |
| PE Verdict | heapq with negation. | ||
What the interviewer is really testing
Heap warmup. PE signal: max-heap, simulate.
Top gotchas
- heapq is min-heap — negate.
- Don't re-push 0.
- If both equal, BOTH removed.
Ship-it
Max-heap.
#62 LC 973 Medium K Closest Points to Origin
Max-heap of size k by distance² — or quickselect.
Problem Statement
Return the k closest points to the origin by Euclidean distance.
Examples
Constraints
- 1 <= k <= n <= 10^4
Approach Overview
Brute Force
Java: Sort by distance
Python: Sort
Optimal #1
Java: Max-heap size k
Python: heapq.nsmallest
Optimal #2
Java: Quickselect partition
Python: Quickselect
Java Solutions
Sort by distance O(n log n) O(1)
Full sort.
Pseudo-code
sort by x²+y²; take first k
Complexity
Java code
import java.util.*;
class Solution {
public int[][] kClosest(int[][] pts, int k) {
Arrays.sort(pts, (a,b) -> (a[0]*a[0]+a[1]*a[1]) - (b[0]*b[0]+b[1]*b[1]));
return Arrays.copyOfRange(pts, 0, k);
}
}
Max-heap size k O(n log k) O(k)
Heap holds k smallest distances so far.
Pseudo-code
max-heap; push; if size>k pop
Complexity
Java code
import java.util.*;
class Solution {
public int[][] kClosest(int[][] pts, int k) {
PriorityQueue<int[]> h = new PriorityQueue<>((a,b) -> (b[0]*b[0]+b[1]*b[1]) - (a[0]*a[0]+a[1]*a[1]));
for (int[] p : pts) {
h.offer(p);
if (h.size() > k) h.poll();
}
return h.toArray(new int[0][]);
}
}
Quickselect partition O(n) avg O(1)
Average O(n).
Pseudo-code
partition by pivot dist; recurse on the side containing k
Complexity
Edge cases & gotchas
- Worst case O(n²); randomize pivot.
Java code
class Solution {
public int[][] kClosest(int[][] pts, int k) {
qs(pts, 0, pts.length-1, k);
return java.util.Arrays.copyOfRange(pts, 0, k);
}
private void qs(int[][] a, int lo, int hi, int k) {
if (lo >= hi) return;
int p = part(a, lo, hi);
if (p == k) return;
if (p < k) qs(a, p+1, hi, k);
else qs(a, lo, p-1, k);
}
private int part(int[][] a, int lo, int hi) {
int pv = dist(a[hi]), i = lo;
for (int j = lo; j < hi; j++) {
if (dist(a[j]) <= pv) { int[] t = a[i]; a[i] = a[j]; a[j] = t; i++; }
}
int[] t = a[i]; a[i] = a[hi]; a[hi] = t;
return i;
}
private int dist(int[] p) { return p[0]*p[0] + p[1]*p[1]; }
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n log k) | O(n) avg |
| Space | O(1) | O(k) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Demo | Default | k near n |
| PE Verdict | Max-heap of k. | ||
Python Solutions
Sort O(n log n) O(1)
Same.
Pseudo-code
sort by dist
Complexity
Python code
from typing import List
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
points.sort(key=lambda p: p[0]*p[0]+p[1]*p[1])
return points[:k]
heapq.nsmallest O(n log k) O(k)
C-fast.
Pseudo-code
nsmallest(k, key=dist)
Complexity
Python code
from typing import List
import heapq
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
return heapq.nsmallest(k, points, key=lambda p: p[0]*p[0] + p[1]*p[1])
Quickselect O(n) avg O(1)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import random
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
d = lambda p: p[0]*p[0]+p[1]*p[1]
def qs(lo, hi):
if lo >= hi: return
pv = d(points[random.randint(lo, hi)])
i = lo
for j in range(lo, hi+1):
if d(points[j]) < pv:
points[i], points[j] = points[j], points[i]; i += 1
if i == k: return
if i < k: qs(i, hi)
else: qs(lo, i-1)
qs(0, len(points)-1)
return points[:k]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n log k) | O(n) avg |
| Space | O(1) | O(k) | O(1) |
| Difficulty | 1/5 | 1/5 | 4/5 |
| When | Demo | Default | Hot path |
| PE Verdict | heapq.nsmallest with key. | ||
What the interviewer is really testing
Top-k pattern.
Top gotchas
- Don't sqrt — square distances preserve order and avoid floats.
- Max-heap so the worst is on top and gets evicted.
- Quickselect needs random pivot for stability.
Ship-it
Max-heap of size k.
#63 LC 215 Medium Kth Largest Element in an Array
Quickselect for O(n) average, heap for stable O(n log k).
Problem Statement
Find the k-th largest element in an unsorted array.
Examples
Constraints
- 1 <= k <= n <= 10^5
Approach Overview
Brute Force
Java: Sort
Python: Sort
Optimal #1
Java: Min-heap size k
Python: heapq.nlargest
Optimal #2
Java: Quickselect (Lomuto/Hoare)
Python: Quickselect
Java Solutions
Sort O(n log n) O(1)
O(n log n).
Pseudo-code
sort; nums[n-k]
Complexity
Java code
import java.util.*;
class Solution {
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length - k];
}
}
Min-heap size k O(n log k) O(k)
Same template as Kth Largest Stream.
Pseudo-code
min-heap; if size > k pop
Complexity
Java code
import java.util.*;
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> h = new PriorityQueue<>();
for (int x : nums) {
h.offer(x);
if (h.size() > k) h.poll();
}
return h.peek();
}
}
Quickselect (Lomuto/Hoare) O(n) avg O(1)
O(n) average.
Pseudo-code
target = n-k; partition; recurse on side containing target
Complexity
Edge cases & gotchas
- Randomize pivot to avoid O(n²) on sorted inputs.
Java code
import java.util.*;
class Solution {
public int findKthLargest(int[] nums, int k) {
return qs(nums, 0, nums.length - 1, nums.length - k);
}
private int qs(int[] a, int lo, int hi, int target) {
if (lo == hi) return a[lo];
int piv = a[lo + new Random().nextInt(hi - lo + 1)];
int l = lo, r = hi;
while (l <= r) {
while (a[l] < piv) l++;
while (a[r] > piv) r--;
if (l <= r) { int t = a[l]; a[l] = a[r]; a[r] = t; l++; r--; }
}
if (target <= r) return qs(a, lo, r, target);
if (target >= l) return qs(a, l, hi, target);
return a[target];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n log k) | O(n) avg / O(n²) |
| Space | O(1) | O(k) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Simple | Streaming | Hot path / interview goal |
| PE Verdict | Quickselect — O(n) average is the textbook answer. | ||
Python Solutions
Sort O(n log n) O(1)
Same.
Pseudo-code
sort
Complexity
Python code
from typing import List
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
nums.sort(); return nums[-k]
heapq.nlargest O(n log k) O(k)
C-fast.
Pseudo-code
nlargest(k)[-1]
Complexity
Python code
from typing import List
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
return heapq.nlargest(k, nums)[-1]
Quickselect O(n) avg O(1)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import random
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
target = len(nums) - k
def qs(lo, hi):
if lo == hi: return nums[lo]
piv = nums[random.randint(lo, hi)]
l, r = lo, hi
while l <= r:
while nums[l] < piv: l += 1
while nums[r] > piv: r -= 1
if l <= r: nums[l], nums[r] = nums[r], nums[l]; l += 1; r -= 1
if target <= r: return qs(lo, r)
if target >= l: return qs(l, hi)
return nums[target]
return qs(0, len(nums)-1)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n log k) | O(n) avg |
| Space | O(1) | O(k) | O(1) |
| Difficulty | 1/5 | 1/5 | 4/5 |
| When | Simple | Default | Strict O(n) |
| PE Verdict | Quickselect; use heapq for streaming. | ||
What the interviewer is really testing
Quickselect is the textbook PE answer.
Top gotchas
- Randomize pivot.
- Target index = n - k.
- Three-way partition for duplicates.
Ship-it
Quickselect with random pivot.
#64 LC 621 Medium Task Scheduler
Greedy max-heap simulation, or closed-form (maxFreq − 1)·(n + 1) + tiesAtMax.
Problem Statement
Given tasks and cooldown n between same-task executions, return the minimum CPU time.
Examples
Constraints
- 1 <= tasks.length <= 10^4
- 0 <= n <= 100
Approach Overview
Brute Force
Java: Simulate slot by slot
Python: Simulate
Optimal #1
Java: Max-heap simulation
Python: Max-heap simulation
Optimal #2
Java: Closed-form (maxFreq-1)*(n+1) + ties
Python: Closed-form
Java Solutions
Simulate slot by slot O(T·26) O(26)
O(T·26).
Pseudo-code
each slot: pick most-frequent eligible
Complexity
Java code
import java.util.*;
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] c = new int[26];
for (char t : tasks) c[t-'A']++;
int time = 0;
int[] cooldown = new int[26];
int total = tasks.length;
while (total > 0) {
int pick = -1, best = 0;
for (int i = 0; i < 26; i++) if (cooldown[i] <= time && c[i] > best) { best = c[i]; pick = i; }
if (pick != -1) { c[pick]--; cooldown[pick] = time + n + 1; total--; }
time++;
}
return time;
}
}
Max-heap simulation O(T log 26) O(26)
Heap of remaining counts; cooldown queue.
Pseudo-code
cycle of n+1: poll top, decrement, queue with cooldown; refill from queue
Complexity
Java code
import java.util.*;
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] c = new int[26];
for (char t : tasks) c[t-'A']++;
PriorityQueue<Integer> h = new PriorityQueue<>(Comparator.reverseOrder());
for (int x : c) if (x > 0) h.offer(x);
Deque<int[]> q = new ArrayDeque<>();
int time = 0;
while (!h.isEmpty() || !q.isEmpty()) {
time++;
if (!h.isEmpty()) {
int x = h.poll() - 1;
if (x > 0) q.offer(new int[]{x, time + n});
}
if (!q.isEmpty() && q.peek()[1] == time) h.offer(q.poll()[0]);
}
return time;
}
}
Closed-form (maxFreq-1)*(n+1) + ties O(T) O(26)
O(T) by counting maxFreq and how many tasks tie at maxFreq.
Pseudo-code
answer = max(T, (maxFreq-1)*(n+1) + tiesAtMax)
Complexity
Edge cases & gotchas
- When dense (no idle slots), answer is simply T.
Java code
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] c = new int[26];
for (char t : tasks) c[t-'A']++;
int max = 0, ties = 0;
for (int x : c) if (x > max) { max = x; ties = 1; } else if (x == max) ties++;
return Math.max(tasks.length, (max - 1) * (n + 1) + ties);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(T·26) | O(T log 26) | O(T) |
| Space | O(26) | O(26) | O(26) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Simulation clarity | Default | Hot path / clever |
| PE Verdict | Closed-form is O(T) and one-liner; heap is the safe default in interview. | ||
Python Solutions
Simulate O(T·26) O(26)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class Solution:
def leastInterval(self, tasks, n: int) -> int:
c = [0]*26
for t in tasks: c[ord(t)-65] += 1
cooldown = [0]*26; time = 0; total = len(tasks)
while total:
pick = -1; best = 0
for i in range(26):
if cooldown[i] <= time and c[i] > best: best = c[i]; pick = i
if pick != -1:
c[pick] -= 1; cooldown[pick] = time + n + 1; total -= 1
time += 1
return time
Max-heap simulation O(T log 26) O(26)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from collections import Counter, deque
import heapq
class Solution:
def leastInterval(self, tasks, n: int) -> int:
h = [-v for v in Counter(tasks).values()]
heapq.heapify(h)
q = deque(); time = 0
while h or q:
time += 1
if h:
v = heapq.heappop(h) + 1
if v: q.append((v, time + n))
if q and q[0][1] == time:
heapq.heappush(h, q.popleft()[0])
return time
Closed-form O(T) O(26)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from collections import Counter
class Solution:
def leastInterval(self, tasks, n: int) -> int:
c = Counter(tasks)
m = max(c.values())
ties = sum(1 for v in c.values() if v == m)
return max(len(tasks), (m - 1) * (n + 1) + ties)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(T·26) | O(T log 26) | O(T) |
| Space | O(26) | O(26) | O(26) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Clarity | Default | Clever one-liner |
| PE Verdict | Closed-form. | ||
What the interviewer is really testing
Greedy with cooldown. PE signal: derive the closed-form.
Top gotchas
- max(T, formula) handles the no-idle dense case.
- Heap must NOT push zero counts.
- Cooldown queue holds (count, time-available).
Ship-it
Closed-form (max-1)*(n+1) + ties.
#65 LC 355 Medium Design Twitter
Per-user tweet lists + k-way merge of followees via min-heap.
Problem Statement
Design Twitter with postTweet, getNewsFeed (10 most recent tweets from self + followees), follow, unfollow.
Examples
Constraints
- 1 <= ids <= 500
Approach Overview
Brute Force
Java: Flat list + sort on getNewsFeed
Python: Flat sort
Optimal #1
Java: K-way merge via max-heap of latest tweets
Python: K-way merge
Optimal #2
Java: Per-user bounded list (only 10 newest)
Python: Bounded per-user
Java Solutions
Flat list + sort on getNewsFeed getNewsFeed O(T log T) O(T)
Naive.
Pseudo-code
merge all relevant tweets then sort by time
Complexity
Java code
import java.util.*;
class Twitter {
static int t = 0;
Map<Integer, List<int[]>> tweets = new HashMap<>();
Map<Integer, Set<Integer>> fol = new HashMap<>();
public void postTweet(int u, int id) {
tweets.computeIfAbsent(u, k -> new ArrayList<>()).add(new int[]{++t, id});
}
public List<Integer> getNewsFeed(int u) {
List<int[]> all = new ArrayList<>();
if (tweets.containsKey(u)) all.addAll(tweets.get(u));
if (fol.containsKey(u)) for (int g : fol.get(u)) if (tweets.containsKey(g)) all.addAll(tweets.get(g));
all.sort((a, b) -> b[0] - a[0]);
List<Integer> out = new ArrayList<>();
for (int i = 0; i < Math.min(10, all.size()); i++) out.add(all.get(i)[1]);
return out;
}
public void follow(int f, int g) { fol.computeIfAbsent(f, k -> new HashSet<>()).add(g); }
public void unfollow(int f, int g) { if (fol.containsKey(f)) fol.get(f).remove(g); }
}
K-way merge via max-heap of latest tweets getNewsFeed O(k log F) O(F)
Heap of (time, listIndex, ...); pop 10.
Pseudo-code
push latest tweet of each followee+self; pop 10; push next from same list
Complexity
Java code
import java.util.*;
class Twitter {
private int t = 0;
private Map<Integer, List<int[]>> tweets = new HashMap<>();
private Map<Integer, Set<Integer>> fol = new HashMap<>();
public void postTweet(int u, int id) { tweets.computeIfAbsent(u, k -> new ArrayList<>()).add(new int[]{++t, id}); }
public List<Integer> getNewsFeed(int u) {
Set<Integer> users = new HashSet<>();
users.add(u);
if (fol.containsKey(u)) users.addAll(fol.get(u));
PriorityQueue<int[]> h = new PriorityQueue<>((a, b) -> b[0] - a[0]); // [time, id, user, idx]
for (int uu : users) {
List<int[]> l = tweets.get(uu);
if (l != null && !l.isEmpty()) {
int last = l.size() - 1;
h.offer(new int[]{l.get(last)[0], l.get(last)[1], uu, last});
}
}
List<Integer> out = new ArrayList<>();
while (out.size() < 10 && !h.isEmpty()) {
int[] top = h.poll();
out.add(top[1]);
int uu = top[2], idx = top[3];
if (idx > 0) {
int[] prev = tweets.get(uu).get(idx - 1);
h.offer(new int[]{prev[0], prev[1], uu, idx - 1});
}
}
return out;
}
public void follow(int f, int g) { fol.computeIfAbsent(f, k -> new HashSet<>()).add(g); }
public void unfollow(int f, int g) { if (fol.containsKey(f)) fol.get(f).remove(g); }
}
Per-user bounded list (only 10 newest) O(F log F) O(U·10 + F)
Trim tweet lists at 10 to bound memory.
Pseudo-code
keep tweets[u] capped at 10; merge latest 10 of each
Complexity
Java code
import java.util.*;
class Twitter {
private int t = 0;
private Map<Integer, LinkedList<int[]>> tweets = new HashMap<>();
private Map<Integer, Set<Integer>> fol = new HashMap<>();
public void postTweet(int u, int id) {
LinkedList<int[]> l = tweets.computeIfAbsent(u, k -> new LinkedList<>());
l.addFirst(new int[]{++t, id});
if (l.size() > 10) l.removeLast();
}
public List<Integer> getNewsFeed(int u) {
List<int[]> all = new ArrayList<>();
Set<Integer> users = new HashSet<>(); users.add(u);
if (fol.containsKey(u)) users.addAll(fol.get(u));
for (int uu : users) if (tweets.containsKey(uu)) all.addAll(tweets.get(uu));
all.sort((a, b) -> b[0] - a[0]);
List<Integer> out = new ArrayList<>();
for (int i = 0; i < Math.min(10, all.size()); i++) out.add(all.get(i)[1]);
return out;
}
public void follow(int f, int g) { fol.computeIfAbsent(f, k -> new HashSet<>()).add(g); }
public void unfollow(int f, int g) { if (fol.containsKey(f)) fol.get(f).remove(g); }
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(T log T) | O(k log F) | O(F log F) |
| Space | O(T) | O(F) | O(U·10+F) |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When | Demo | Default | Memory bound |
| PE Verdict | K-way merge with max-heap. | ||
Python Solutions
Flat sort O(T log T) O(T)
Same.
Pseudo-code
merge+sort
Complexity
Python code
from collections import defaultdict
class Twitter:
def __init__(self):
self.t = 0
self.tweets = defaultdict(list)
self.fol = defaultdict(set)
def postTweet(self, u, id):
self.t += 1; self.tweets[u].append((self.t, id))
def getNewsFeed(self, u):
users = {u} | self.fol[u]
all_t = [x for uu in users for x in self.tweets[uu]]
all_t.sort(reverse=True)
return [id for _, id in all_t[:10]]
def follow(self, f, g): self.fol[f].add(g)
def unfollow(self, f, g): self.fol[f].discard(g)
K-way merge O(k log F) O(F)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
from collections import defaultdict
import heapq
class Twitter:
def __init__(self):
self.t = 0
self.tweets = defaultdict(list)
self.fol = defaultdict(set)
def postTweet(self, u, id):
self.t += 1; self.tweets[u].append((self.t, id))
def getNewsFeed(self, u):
users = self.fol[u] | {u}
h = [] # negate time for max-heap behavior
for uu in users:
if self.tweets[uu]:
idx = len(self.tweets[uu]) - 1
tm, id = self.tweets[uu][idx]
heapq.heappush(h, (-tm, id, uu, idx))
out = []
while h and len(out) < 10:
tm, id, uu, idx = heapq.heappop(h)
out.append(id)
if idx > 0:
ptm, pid = self.tweets[uu][idx-1]
heapq.heappush(h, (-ptm, pid, uu, idx-1))
return out
def follow(self, f, g): self.fol[f].add(g)
def unfollow(self, f, g): self.fol[f].discard(g)
Bounded per-user O(F log F) O(U·10+F)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from collections import defaultdict, deque
class Twitter:
def __init__(self):
self.t = 0
self.tweets = defaultdict(lambda: deque(maxlen=10))
self.fol = defaultdict(set)
def postTweet(self, u, id):
self.t += 1; self.tweets[u].append((self.t, id))
def getNewsFeed(self, u):
users = self.fol[u] | {u}
all_t = sorted([x for uu in users for x in self.tweets[uu]], reverse=True)
return [id for _, id in all_t[:10]]
def follow(self, f, g): self.fol[f].add(g)
def unfollow(self, f, g): self.fol[f].discard(g)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(T log T) | O(k log F) | O(F log F) |
| Space | O(T) | O(F) | Bounded |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When | Demo | Default | Memory bound |
| PE Verdict | K-way merge. | ||
What the interviewer is really testing
Composition of HashMap, Set, and k-way merge.
Top gotchas
- Don't forget self in news feed.
- unfollow with discard (or contains check) — set.remove throws on missing.
- Tweet time must be monotonically increasing across all users.
Ship-it
K-way merge with max-heap.
#66 LC 295 Hard Find Median from Data Stream
Two heaps — max-heap of lower half, min-heap of upper half. Median in O(1).
Problem Statement
Design MedianFinder with addNum(int) and findMedian() (returns double).
Examples
Constraints
- -10^5 <= num <= 10^5
- Up to 5*10^4 addNum calls
Approach Overview
Brute Force
Java: Sorted list with insertion
Python: bisect.insort
Optimal #1
Java: Two heaps (max + min)
Python: Two heaps (negate for max)
Optimal #2
Java: Indexed multiset / order-statistic tree
Python: SortedList
Java Solutions
Sorted list with insertion add O(n) O(n)
O(n) insert.
Pseudo-code
binary insert; median = middle
Complexity
Java code
import java.util.*;
class MedianFinder {
List<Integer> a = new ArrayList<>();
public void addNum(int n) {
int i = Collections.binarySearch(a, n);
if (i < 0) i = -i - 1;
a.add(i, n);
}
public double findMedian() {
int sz = a.size();
return sz % 2 == 1 ? a.get(sz/2) : (a.get(sz/2-1) + a.get(sz/2)) / 2.0;
}
}
Two heaps (max + min) add O(log n); median O(1) O(n)
Balanced halves — max-heap holds lower, min-heap holds upper.
Pseudo-code
add: push to max; balance: top of max -> min; if min larger: min top -> max\nmedian: if equal sizes avg tops; else top of larger
Complexity
Edge cases & gotchas
- Maintain |lo| - |hi| ∈ {0, 1} (lo holds extra when odd).
Java code
import java.util.*;
class MedianFinder {
private PriorityQueue<Integer> lo = new PriorityQueue<>(Comparator.reverseOrder()); // max-heap
private PriorityQueue<Integer> hi = new PriorityQueue<>(); // min-heap
public void addNum(int num) {
lo.offer(num);
hi.offer(lo.poll());
if (hi.size() > lo.size()) lo.offer(hi.poll());
}
public double findMedian() {
if (lo.size() > hi.size()) return lo.peek();
return (lo.peek() + hi.peek()) / 2.0;
}
}
Indexed multiset / order-statistic tree add O(log n); median O(log n) O(n)
TreeMap-like; O(log n) by exact rank.
Pseudo-code
insert into TreeMap; median = find rank n/2
Complexity
Edge cases & gotchas
- More flexible (kth order statistic), slower constant.
Java code
import java.util.*;
class MedianFinder {
TreeMap<Integer,Integer> tm = new TreeMap<>();
int n = 0;
public void addNum(int num) { tm.merge(num, 1, Integer::sum); n++; }
public double findMedian() {
int m1 = n / 2, m2 = (n - 1) / 2;
int acc = 0, v1 = 0, v2 = 0;
for (Map.Entry<Integer,Integer> e : tm.entrySet()) {
int next = acc + e.getValue();
if (acc <= m2 && m2 < next) v2 = e.getKey();
if (acc <= m1 && m1 < next) { v1 = e.getKey(); break; }
acc = next;
}
return (v1 + v2) / 2.0;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | add O(n) | add O(log n) | add O(log n) |
| Median | O(1) | O(1) | O(log n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| When | Tiny | Default | Kth queries too |
| PE Verdict | Two heaps. | ||
Python Solutions
bisect.insort O(n) O(n)
Same.
Pseudo-code
insort + median
Complexity
Python code
from bisect import insort
class MedianFinder:
def __init__(self): self.a = []
def addNum(self, n): insort(self.a, n)
def findMedian(self) -> float:
sz = len(self.a)
return self.a[sz//2] if sz % 2 else (self.a[sz//2-1] + self.a[sz//2]) / 2
Two heaps (negate for max) O(log n) O(n)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap via negation
self.hi = [] # min-heap
def addNum(self, num):
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def findMedian(self) -> float:
if len(self.lo) > len(self.hi): return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
SortedList O(log n) O(n)
sortedcontainers gives O(log n).
Pseudo-code
sl.add; median = sl[n//2]
Complexity
Edge cases & gotchas
- Third-party dependency.
Python code
from sortedcontainers import SortedList
class MedianFinder:
def __init__(self): self.sl = SortedList()
def addNum(self, n): self.sl.add(n)
def findMedian(self) -> float:
sz = len(self.sl)
return self.sl[sz//2] if sz % 2 else (self.sl[sz//2-1] + self.sl[sz//2]) / 2
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| add | O(n) | O(log n) | O(log n) |
| median | O(1) | O(1) | O(1) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 1/5 | 4/5 | 2/5 |
| When | Demo | Default | Quick prototype |
| PE Verdict | Two heaps (stdlib). | ||
What the interviewer is really testing
Streaming median is THE canonical 'two heaps' problem.
Top gotchas
- Always push to lo first, then balance via hi.
- Maintain invariant: |lo| ∈ {|hi|, |hi|+1}.
- heapq is min-only — negate values for max-heap.
Ship-it
Two heaps (lo max, hi min).
Backtracking
#67 LC 78 Medium Subsets
Power set — include/exclude DFS or iterative bit enumeration.
Problem Statement
Return all subsets (the power set) of a list of unique integers.
Examples
Constraints
- 1 <= n <= 10
Approach Overview
Brute Force
Java: Iterative duplicate-and-extend
Python: Iterative extend
Optimal #1
Java: Include/exclude DFS backtracking
Python: DFS backtracking
Optimal #2
Java: Bitmask enumeration
Python: itertools.combinations
Java Solutions
Iterative duplicate-and-extend O(n·2^n) O(n·2^n)
Start with [[]]; for each num, duplicate every existing subset adding num.
Pseudo-code
out=[[]]; for n in nums: out += [s+[n] for s in out]
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> out = new ArrayList<>();
out.add(new ArrayList<>());
for (int n : nums) {
int sz = out.size();
for (int i = 0; i < sz; i++) {
List<Integer> s = new ArrayList<>(out.get(i));
s.add(n);
out.add(s);
}
}
return out;
}
}
Include/exclude DFS backtracking O(n·2^n) O(n)
At each index, choose to include or skip.
Pseudo-code
dfs(i): if i==n: out.add(cur); else: cur.add(nums[i]); dfs(i+1); cur.removeLast(); dfs(i+1)
Complexity
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
private int[] nums;
public List<List<Integer>> subsets(int[] nums) {
this.nums = nums;
dfs(0, new ArrayList<>());
return out;
}
private void dfs(int i, List<Integer> cur) {
if (i == nums.length) { out.add(new ArrayList<>(cur)); return; }
cur.add(nums[i]); dfs(i+1, cur);
cur.remove(cur.size()-1); dfs(i+1, cur);
}
}
Bitmask enumeration O(n·2^n) O(n·2^n)
For each bitmask 0..2^n - 1, build subset from set bits.
Pseudo-code
for m in 0..1<<n: out.add([nums[i] for i where bit i of m is set])
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> out = new ArrayList<>();
int n = nums.length;
for (int m = 0; m < (1 << n); m++) {
List<Integer> s = new ArrayList<>();
for (int i = 0; i < n; i++) if (((m >> i) & 1) == 1) s.add(nums[i]);
out.add(s);
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·2^n) | O(n·2^n) | O(n·2^n) |
| Space | Output | O(n) extra | Output |
| Difficulty | 2/5 | 3/5 | 2/5 |
| When | Quick | Default | Generalizes to enumeration |
| PE Verdict | Backtracking template. | ||
Python Solutions
Iterative extend O(n·2^n) O(n·2^n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
out = [[]]
for n in nums:
out += [s + [n] for s in out]
return out
DFS backtracking O(n·2^n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
out = []
def dfs(i, cur):
if i == len(nums): out.append(cur[:]); return
cur.append(nums[i]); dfs(i+1, cur); cur.pop()
dfs(i+1, cur)
dfs(0, [])
return out
itertools.combinations O(n·2^n) O(n·2^n)
Combine over all lengths.
Pseudo-code
for r in 0..n: combinations(nums, r)
Complexity
Python code
from typing import List
from itertools import combinations
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
return [list(c) for r in range(len(nums)+1) for c in combinations(nums, r)]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·2^n) | O(n·2^n) | O(n·2^n) |
| Space | Output | O(n) | Output |
| Difficulty | 1/5 | 3/5 | 1/5 |
| When | Demo | Default | Pythonic |
| PE Verdict | DFS backtracking template. | ||
What the interviewer is really testing
Foundation for all subset/combination problems.
Top gotchas
- Output deep-copies cur (don't share the mutable list).
- Include/exclude order doesn't matter for set output.
Ship-it
DFS with backtracking.
#68 LC 39 Medium Combination Sum
DFS with index — each candidate can be reused unlimited times.
Problem Statement
Given distinct positive candidates and target, return all unique combinations summing to target. Each number may be used unlimited times.
Examples
Constraints
- 1 <= n <= 30
- 2 <= cand[i] <= 40
- 1 <= target <= 40
Approach Overview
Brute Force
Java: DFS without start index
Python: DFS + dedupe
Optimal #1
Java: DFS with start index (prevent duplicates)
Python: DFS with start index
Optimal #2
Java: Iterative DP-like enumeration
Python: DP table
Java Solutions
DFS without start index Exp. Exp.
Generates duplicates; dedupe via set.
Pseudo-code
DFS pick any; sort + dedupe
Complexity
Edge cases & gotchas
- Wasteful.
Java code
import java.util.*;
class Solution {
Set<List<Integer>> out = new HashSet<>();
public List<List<Integer>> combinationSum(int[] c, int t) {
Arrays.sort(c);
dfs(c, t, new ArrayList<>());
return new ArrayList<>(out);
}
private void dfs(int[] c, int t, List<Integer> cur) {
if (t == 0) { List<Integer> s = new ArrayList<>(cur); Collections.sort(s); out.add(s); return; }
for (int x : c) if (x <= t) { cur.add(x); dfs(c, t-x, cur); cur.remove(cur.size()-1); }
}
}
DFS with start index (prevent duplicates) O(2^t/min) O(t/min)
Each level only considers candidates from current index forward.
Pseudo-code
dfs(i, remaining): if 0 emit; for j>=i: if c[j]<=rem: pick, dfs(j, rem-c[j]), unpick
Complexity
Edge cases & gotchas
- Pass j (not j+1) since reuse is allowed.
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
private int[] c;
public List<List<Integer>> combinationSum(int[] cand, int target) {
c = cand; Arrays.sort(c);
dfs(0, target, new ArrayList<>());
return out;
}
private void dfs(int i, int rem, List<Integer> cur) {
if (rem == 0) { out.add(new ArrayList<>(cur)); return; }
for (int j = i; j < c.length && c[j] <= rem; j++) {
cur.add(c[j]);
dfs(j, rem - c[j], cur);
cur.remove(cur.size()-1);
}
}
}
Iterative DP-like enumeration O(n·t·avg) O(n·t·avg)
dp[t] = list of combos summing to t.
Pseudo-code
for t in 1..target: for c: combine with dp[t-c]
Complexity
Edge cases & gotchas
- Slow and memory-heavy; backtracking is cleaner.
Java code
import java.util.*;
class Solution {
public List<List<Integer>> combinationSum(int[] c, int t) {
List<List<List<Integer>>> dp = new ArrayList<>();
for (int i = 0; i <= t; i++) dp.add(new ArrayList<>());
dp.get(0).add(new ArrayList<>());
Arrays.sort(c);
for (int s = 1; s <= t; s++) {
for (int x : c) {
if (x > s) break;
for (List<Integer> prev : dp.get(s - x)) {
if (!prev.isEmpty() && x < prev.get(prev.size()-1)) continue;
List<Integer> ns = new ArrayList<>(prev); ns.add(x);
dp.get(s).add(ns);
}
}
}
return dp.get(t);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | Exp | Exp | Poly·exp |
| Space | Exp | O(t) | Big |
| Difficulty | 2/5 | 3/5 | 5/5 |
| When | Don't | Default | Pedagogy |
| PE Verdict | DFS with start index. | ||
Python Solutions
DFS + dedupe Exp Exp
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def combinationSum(self, c: List[int], t: int) -> List[List[int]]:
c = sorted(c)
out = set()
def dfs(rem, cur):
if rem == 0: out.add(tuple(sorted(cur))); return
for x in c:
if x > rem: break
cur.append(x); dfs(rem - x, cur); cur.pop()
dfs(t, [])
return [list(s) for s in out]
DFS with start index Exp O(t)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
out = []
def dfs(i, rem, cur):
if rem == 0: out.append(cur[:]); return
for j in range(i, len(candidates)):
if candidates[j] > rem: break
cur.append(candidates[j])
dfs(j, rem - candidates[j], cur)
cur.pop()
dfs(0, target, [])
return out
DP table Big Big
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def combinationSum(self, c: List[int], t: int) -> List[List[int]]:
c = sorted(c)
dp = [[] for _ in range(t+1)]
dp[0] = [[]]
for s in range(1, t+1):
for x in c:
if x > s: break
for prev in dp[s - x]:
if prev and x < prev[-1]: continue
dp[s].append(prev + [x])
return dp[t]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | Exp | Exp | Big |
| Space | Exp | O(t) | Big |
| Difficulty | 2/5 | 3/5 | 5/5 |
| When | Don't | Default | Pedagogy |
| PE Verdict | DFS with start index. | ||
What the interviewer is really testing
Backtracking with reuse.
Top gotchas
- Recurse with j (reuse), not j+1.
- Sort and prune via break — saves big.
- Copy cur on emit.
Ship-it
DFS start-index backtracking.
#69 LC 40 Medium Combination Sum II
Like combination sum but each number used at most once + duplicates in input.
Problem Statement
Given candidates (may contain duplicates) and target, return all unique combinations summing to target. Each candidate used at most once.
Examples
Constraints
- 1 <= n <= 100
Approach Overview
Brute Force
Java: All subsets + filter + dedupe
Python: Bitmask enumeration
Optimal #1
Java: DFS with start + skip duplicates at same depth
Python: DFS + skip dup
Optimal #2
Java: DFS with counts (group duplicates)
Python: Group counts
Java Solutions
All subsets + filter + dedupe O(2^n) O(2^n)
2^n subsets; check sum.
Pseudo-code
enumerate all subsets; keep those summing to t; dedupe
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> combinationSum2(int[] c, int t) {
Arrays.sort(c);
Set<List<Integer>> out = new HashSet<>();
for (int m = 0; m < (1 << c.length); m++) {
List<Integer> s = new ArrayList<>();
int sum = 0;
for (int i = 0; i < c.length; i++) if (((m >> i) & 1) == 1) { s.add(c[i]); sum += c[i]; }
if (sum == t) out.add(s);
}
return new ArrayList<>(out);
}
}
DFS with start + skip duplicates at same depth O(2^n) O(n)
Sort; skip j > i where c[j] == c[j-1].
Pseudo-code
sort; dfs(i, rem): for j>=i: if j>i and c[j]==c[j-1] skip; pick; dfs(j+1, rem-c[j])
Complexity
Edge cases & gotchas
- Recurse with j+1 (not j) — single use.
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
private int[] c;
public List<List<Integer>> combinationSum2(int[] cand, int target) {
c = cand; Arrays.sort(c);
dfs(0, target, new ArrayList<>());
return out;
}
private void dfs(int i, int rem, List<Integer> cur) {
if (rem == 0) { out.add(new ArrayList<>(cur)); return; }
for (int j = i; j < c.length && c[j] <= rem; j++) {
if (j > i && c[j] == c[j-1]) continue;
cur.add(c[j]);
dfs(j+1, rem - c[j], cur);
cur.remove(cur.size()-1);
}
}
}
DFS with counts (group duplicates) O(2^n) O(n)
Convert to (value, count) pairs; choose 0..count per group.
Pseudo-code
groups = (val, cnt); dfs(i, rem): for k in 0..cnt[i]: dfs(i+1, rem - k*val[i])
Complexity
Java code
import java.util.*;
class Solution {
private List<int[]> g = new ArrayList<>();
private List<List<Integer>> out = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] c, int t) {
Arrays.sort(c);
for (int i = 0; i < c.length; ) {
int j = i;
while (j < c.length && c[j] == c[i]) j++;
g.add(new int[]{c[i], j - i});
i = j;
}
dfs(0, t, new ArrayList<>());
return out;
}
private void dfs(int gi, int rem, List<Integer> cur) {
if (rem == 0) { out.add(new ArrayList<>(cur)); return; }
if (gi == g.size()) return;
int v = g.get(gi)[0], n = g.get(gi)[1];
for (int k = 0; k <= n && k * v <= rem; k++) {
for (int z = 0; z < k; z++) cur.add(v);
dfs(gi+1, rem - k * v, cur);
for (int z = 0; z < k; z++) cur.remove(cur.size()-1);
}
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(2^n) | O(2^n) |
| Space | O(2^n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| When | Tiny | Default | Heavy duplicates |
| PE Verdict | DFS with skip-duplicates-at-same-depth. | ||
Python Solutions
Bitmask enumeration O(2^n) O(2^n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def combinationSum2(self, c: List[int], t: int) -> List[List[int]]:
c = sorted(c); n = len(c); out = set()
for m in range(1 << n):
s = []; total = 0
for i in range(n):
if (m >> i) & 1: s.append(c[i]); total += c[i]
if total == t: out.add(tuple(s))
return [list(x) for x in out]
DFS + skip dup O(2^n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
out = []
def dfs(i, rem, cur):
if rem == 0: out.append(cur[:]); return
for j in range(i, len(candidates)):
if candidates[j] > rem: break
if j > i and candidates[j] == candidates[j-1]: continue
cur.append(candidates[j])
dfs(j+1, rem - candidates[j], cur)
cur.pop()
dfs(0, target, [])
return out
Group counts O(2^n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class Solution:
def combinationSum2(self, c: List[int], t: int) -> List[List[int]]:
g = sorted(Counter(c).items())
out = []
def dfs(i, rem, cur):
if rem == 0: out.append(cur[:]); return
if i == len(g): return
v, n = g[i]
for k in range(0, n+1):
if k * v > rem: break
cur.extend([v]*k)
dfs(i+1, rem - k*v, cur)
for _ in range(k): cur.pop()
dfs(0, t, [])
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(2^n) | O(2^n) |
| Space | O(2^n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| When | Tiny | Default | Many dupes |
| PE Verdict | DFS skip dup at same depth. | ||
What the interviewer is really testing
Standard variant — single-use + duplicates.
Top gotchas
- The skip-dup check is `j > i`, NOT `j > 0`.
- Recurse with j+1 (single use).
- Sort first.
Ship-it
DFS with start + skip-dup-at-same-depth.
#70 LC 46 Medium Permutations
All n! permutations — DFS with used set, or in-place swap.
Problem Statement
Return all permutations of a list of distinct integers.
Examples
Constraints
- 1 <= n <= 6
Approach Overview
Brute Force
Java: Heap's algorithm (in-place)
Python: itertools.permutations
Optimal #1
Java: DFS with used[] flags
Python: DFS with used
Optimal #2
Java: In-place swap DFS
Python: In-place swap
Java Solutions
Heap's algorithm (in-place) O(n!) O(n)
O(n!).
Pseudo-code
Heap's algorithm
Complexity
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) { heap(nums, nums.length); return out; }
private void heap(int[] a, int k) {
if (k == 1) { List<Integer> l = new ArrayList<>(); for (int x : a) l.add(x); out.add(l); return; }
for (int i = 0; i < k; i++) {
heap(a, k-1);
int j = (k % 2 == 0) ? i : 0;
int t = a[j]; a[j] = a[k-1]; a[k-1] = t;
}
}
}
DFS with used[] flags O(n·n!) O(n)
At each level, pick any not-yet-used.
Pseudo-code
dfs(cur): if size==n emit; else for each unused: mark used; add; dfs; unmark; remove
Complexity
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
private boolean[] used;
private int[] nums;
public List<List<Integer>> permute(int[] nums) {
this.nums = nums; used = new boolean[nums.length];
dfs(new ArrayList<>());
return out;
}
private void dfs(List<Integer> cur) {
if (cur.size() == nums.length) { out.add(new ArrayList<>(cur)); return; }
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true; cur.add(nums[i]);
dfs(cur);
cur.remove(cur.size()-1); used[i] = false;
}
}
}
In-place swap DFS O(n·n!) O(n)
Swap nums[start] with each i >= start; recurse start+1.
Pseudo-code
dfs(s): if s==n emit nums; for i>=s: swap(s,i); dfs(s+1); swap(s,i)
Complexity
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
private int[] nums;
public List<List<Integer>> permute(int[] nums) { this.nums = nums; dfs(0); return out; }
private void dfs(int s) {
if (s == nums.length) {
List<Integer> l = new ArrayList<>(); for (int x : nums) l.add(x); out.add(l); return;
}
for (int i = s; i < nums.length; i++) {
int t = nums[s]; nums[s] = nums[i]; nums[i] = t;
dfs(s+1);
t = nums[s]; nums[s] = nums[i]; nums[i] = t;
}
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n!) | O(n·n!) | O(n·n!) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 4/5 | 2/5 | 3/5 |
| When | Pedagogy | Default | Memory tight |
| PE Verdict | DFS with used flags. | ||
Python Solutions
itertools.permutations O(n!) O(n!)
C-fast.
Pseudo-code
list(permutations(nums))
Complexity
Python code
from typing import List
from itertools import permutations
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
return [list(p) for p in permutations(nums)]
DFS with used O(n·n!) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
out = []; used = [False]*len(nums)
def dfs(cur):
if len(cur) == len(nums): out.append(cur[:]); return
for i in range(len(nums)):
if used[i]: continue
used[i] = True; cur.append(nums[i])
dfs(cur)
cur.pop(); used[i] = False
dfs([])
return out
In-place swap O(n·n!) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
out = []
def dfs(s):
if s == len(nums): out.append(nums[:]); return
for i in range(s, len(nums)):
nums[s], nums[i] = nums[i], nums[s]
dfs(s+1)
nums[s], nums[i] = nums[i], nums[s]
dfs(0)
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n!) | O(n·n!) | O(n·n!) |
| Space | O(n!) | O(n) | O(n) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When | Demo | Default | Memory tight |
| PE Verdict | DFS with used. | ||
What the interviewer is really testing
Template question.
Top gotchas
- Don't share cur reference — copy on emit.
- Reset used[] AFTER recursion in the same iteration.
Ship-it
DFS with used flags.
#71 LC 90 Medium Subsets II
Subsets with duplicates — sort then skip same value at same depth.
Problem Statement
Given an array with possible duplicates, return all unique subsets.
Examples
Constraints
- 1 <= n <= 10
Approach Overview
Brute Force
Java: All subsets then dedupe
Python: Bitmask + set
Optimal #1
Java: DFS with start + skip-dup-at-same-depth
Python: DFS skip dup
Optimal #2
Java: Iterative duplicate-and-extend with dup-handling
Python: Iterative with dup-tracking
Java Solutions
All subsets then dedupe O(2^n) O(2^n)
Set of sorted tuples.
Pseudo-code
see Subsets but dedupe
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
Set<List<Integer>> out = new HashSet<>();
for (int m = 0; m < (1 << nums.length); m++) {
List<Integer> s = new ArrayList<>();
for (int i = 0; i < nums.length; i++) if (((m >> i) & 1) == 1) s.add(nums[i]);
out.add(s);
}
return new ArrayList<>(out);
}
}
DFS with start + skip-dup-at-same-depth O(2^n) O(n)
Sort; for j > i where nums[j] == nums[j-1], skip.
Pseudo-code
sort; dfs(i, cur): emit cur; for j>=i: if j>i and nums[j]==nums[j-1] skip; pick; dfs(j+1, cur); unpick
Complexity
Java code
import java.util.*;
class Solution {
private List<List<Integer>> out = new ArrayList<>();
private int[] nums;
public List<List<Integer>> subsetsWithDup(int[] nums) {
this.nums = nums; Arrays.sort(nums);
dfs(0, new ArrayList<>());
return out;
}
private void dfs(int i, List<Integer> cur) {
out.add(new ArrayList<>(cur));
for (int j = i; j < nums.length; j++) {
if (j > i && nums[j] == nums[j-1]) continue;
cur.add(nums[j]);
dfs(j+1, cur);
cur.remove(cur.size()-1);
}
}
}
Iterative duplicate-and-extend with dup-handling O(2^n) O(2^n)
For dup runs, only extend subsets created in the previous round.
Pseudo-code
sort; out=[[]]; track start; if dup, extend only [start:] else 0
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> out = new ArrayList<>();
out.add(new ArrayList<>());
int start = 0, prevSize = 0;
for (int i = 0; i < nums.length; i++) {
int from = (i > 0 && nums[i] == nums[i-1]) ? prevSize : 0;
prevSize = out.size();
for (int k = from; k < prevSize; k++) {
List<Integer> s = new ArrayList<>(out.get(k));
s.add(nums[i]);
out.add(s);
}
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(2^n) | O(2^n) |
| Space | O(2^n) | O(n) | Output |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Demo | Default | Memory |
| PE Verdict | DFS with skip-dup. | ||
Python Solutions
Bitmask + set O(2^n) O(2^n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums = sorted(nums); n = len(nums); out = set()
for m in range(1 << n):
s = tuple(nums[i] for i in range(n) if (m >> i) & 1)
out.add(s)
return [list(s) for s in out]
DFS skip dup O(2^n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort(); out = []
def dfs(i, cur):
out.append(cur[:])
for j in range(i, len(nums)):
if j > i and nums[j] == nums[j-1]: continue
cur.append(nums[j]); dfs(j+1, cur); cur.pop()
dfs(0, [])
return out
Iterative with dup-tracking O(2^n) O(2^n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort(); out = [[]]; prev_size = 0
for i, x in enumerate(nums):
from_ = prev_size if i > 0 and nums[i] == nums[i-1] else 0
prev_size = len(out)
for k in range(from_, prev_size):
out.append(out[k] + [x])
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(2^n) | O(2^n) |
| Space | O(2^n) | O(n) | Output |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Demo | Default | Memory |
| PE Verdict | DFS skip-dup. | ||
What the interviewer is really testing
Subset variant with dedupe at same depth.
Top gotchas
- Skip-check is `j > i`, not `j > 0`.
- Sort first — duplicates must be adjacent.
Ship-it
DFS start-index + skip-dup.
#72 LC 22 Medium Generate Parentheses
Backtrack with constraint: open ≤ n, close ≤ open.
Problem Statement
Generate all combinations of n pairs of well-formed parentheses.
Examples
Constraints
- 1 <= n <= 8
Approach Overview
Brute Force
Java: Generate all 2^(2n) strings, filter valid
Python: Brute + filter
Optimal #1
Java: Backtracking with (open, close) counts
Python: Backtracking
Optimal #2
Java: DP — build from f(n-1)
Python: DP
Java Solutions
Generate all 2^(2n) strings, filter valid O(2^(2n)·n) O(2^(2n))
Wasteful.
Pseudo-code
brute generate; check balanced
Complexity
Java code
import java.util.*;
class Solution {
public List<String> generateParenthesis(int n) {
List<String> out = new ArrayList<>();
gen(out, new char[2*n], 0);
return out;
}
private void gen(List<String> out, char[] buf, int i) {
if (i == buf.length) { if (valid(buf)) out.add(new String(buf)); return; }
buf[i] = '('; gen(out, buf, i+1);
buf[i] = ')'; gen(out, buf, i+1);
}
private boolean valid(char[] s) {
int b = 0;
for (char c : s) { b += c == '(' ? 1 : -1; if (b < 0) return false; }
return b == 0;
}
}
Backtracking with (open, close) counts O(C(2n,n)) O(n)
Only add '(' if open < n; only add ')' if close < open.
Pseudo-code
dfs(open, close, cur): if 2n: emit; if open<n: add '('; if close<open: add ')'
Complexity
Java code
import java.util.*;
class Solution {
private List<String> out = new ArrayList<>();
private int n;
public List<String> generateParenthesis(int n) {
this.n = n; dfs(0, 0, new StringBuilder()); return out;
}
private void dfs(int open, int close, StringBuilder cur) {
if (cur.length() == 2 * n) { out.add(cur.toString()); return; }
if (open < n) { cur.append('('); dfs(open+1, close, cur); cur.deleteCharAt(cur.length()-1); }
if (close < open) { cur.append(')'); dfs(open, close+1, cur); cur.deleteCharAt(cur.length()-1); }
}
}
DP — build from f(n-1) O(C(2n,n)) O(C(2n,n))
f(n) = '(' + f(i) + ')' + f(n-1-i) for i in 0..n-1.
Pseudo-code
f[0]=['']; f[i] = combine '(' f[j] ')' f[i-1-j]
Complexity
Java code
import java.util.*;
class Solution {
public List<String> generateParenthesis(int n) {
List<List<String>> f = new ArrayList<>();
f.add(Collections.singletonList(""));
for (int i = 1; i <= n; i++) {
List<String> cur = new ArrayList<>();
for (int j = 0; j < i; j++)
for (String a : f.get(j))
for (String b : f.get(i-1-j))
cur.add("(" + a + ")" + b);
f.add(cur);
}
return f.get(n);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(2n)n) | O(C(2n,n)) | O(C(2n,n)) |
| Space | O(2^(2n)) | O(n) | Catalan |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Pedagogy |
| PE Verdict | Backtracking with counts. | ||
Python Solutions
Brute + filter O(2^(2n)n) O(2^(2n))
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
out = []
def gen(s):
if len(s) == 2*n:
b = 0; ok = True
for c in s:
b += 1 if c == '(' else -1
if b < 0: ok = False; break
if ok and b == 0: out.append(s)
return
gen(s+'('); gen(s+')')
gen("")
return out
Backtracking O(C(2n,n)) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
out = []
def dfs(open, close, cur):
if len(cur) == 2*n: out.append(cur); return
if open < n: dfs(open+1, close, cur+'(')
if close < open: dfs(open, close+1, cur+')')
dfs(0, 0, "")
return out
DP O(C(2n,n)) O(C(2n,n))
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
f = [['']]
for i in range(1, n+1):
cur = []
for j in range(i):
for a in f[j]:
for b in f[i-1-j]:
cur.append(f"({a}){b}")
f.append(cur)
return f[n]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(2n)n) | O(C(2n,n)) | O(C(2n,n)) |
| Space | O(2^(2n)) | O(n) | Catalan |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Pedagogy |
| PE Verdict | Backtracking with constraints. | ||
What the interviewer is really testing
Constraint-driven backtracking.
Top gotchas
- Only `(` if open < n.
- Only `)` if close < open.
- Emit when len == 2n, not when balanced is 0.
Ship-it
DFS with (open, close) counts.
#73 LC 79 Medium Word Search
Grid DFS — match word path with in-place visited marking.
Problem Statement
Given an m×n board and a word, return whether the word can be formed by adjacent cells (no cell used twice).
Examples
Constraints
- 1 <= m, n <= 6
- 1 <= word.length <= 15
Approach Overview
Brute Force
Java: DFS with visited set
Python: DFS + visited
Optimal #1
Java: DFS with in-place marking (no extra space)
Python: DFS in-place
Optimal #2
Java: DFS + Aho-Corasick-style pruning
Python: With char-count pruning
Java Solutions
DFS with visited set O(m·n·4^L) O(L + m·n)
Standard.
Pseudo-code
for each cell: dfs with visited
Complexity
Java code
import java.util.*;
class Solution {
public boolean exist(char[][] b, String w) {
for (int i = 0; i < b.length; i++) for (int j = 0; j < b[0].length; j++)
if (dfs(b, w, i, j, 0, new boolean[b.length][b[0].length])) return true;
return false;
}
private boolean dfs(char[][] b, String w, int i, int j, int k, boolean[][] v) {
if (k == w.length()) return true;
if (i < 0 || j < 0 || i >= b.length || j >= b[0].length || v[i][j] || b[i][j] != w.charAt(k)) return false;
v[i][j] = true;
boolean ok = dfs(b,w,i+1,j,k+1,v) || dfs(b,w,i-1,j,k+1,v) || dfs(b,w,i,j+1,k+1,v) || dfs(b,w,i,j-1,k+1,v);
v[i][j] = false;
return ok;
}
}
DFS with in-place marking (no extra space) O(m·n·4^L) O(L)
Temporarily overwrite the char with a sentinel.
Pseudo-code
mark visited by setting board[i][j] = '#' during recursion; restore
Complexity
Edge cases & gotchas
- MUST restore the char on backtrack.
Java code
class Solution {
public boolean exist(char[][] b, String w) {
for (int i = 0; i < b.length; i++) for (int j = 0; j < b[0].length; j++)
if (dfs(b, w, i, j, 0)) return true;
return false;
}
private boolean dfs(char[][] b, String w, int i, int j, int k) {
if (k == w.length()) return true;
if (i < 0 || j < 0 || i >= b.length || j >= b[0].length || b[i][j] != w.charAt(k)) return false;
char tmp = b[i][j]; b[i][j] = '#';
boolean ok = dfs(b,w,i+1,j,k+1) || dfs(b,w,i-1,j,k+1) || dfs(b,w,i,j+1,k+1) || dfs(b,w,i,j-1,k+1);
b[i][j] = tmp;
return ok;
}
}
DFS + Aho-Corasick-style pruning O(m·n·4^L) O(L)
Pre-count chars; bail if word can't possibly fit.
Pseudo-code
count chars in board; if word has more of any char, return false
Complexity
Java code
class Solution {
public boolean exist(char[][] b, String w) {
int[] cnt = new int[128];
for (char[] r : b) for (char c : r) cnt[c]++;
int[] wcnt = new int[128];
for (char c : w.toCharArray()) wcnt[c]++;
for (int c = 0; c < 128; c++) if (wcnt[c] > cnt[c]) return false;
for (int i = 0; i < b.length; i++) for (int j = 0; j < b[0].length; j++)
if (dfs(b, w, i, j, 0)) return true;
return false;
}
private boolean dfs(char[][] b, String w, int i, int j, int k) {
if (k == w.length()) return true;
if (i < 0 || j < 0 || i >= b.length || j >= b[0].length || b[i][j] != w.charAt(k)) return false;
char tmp = b[i][j]; b[i][j] = '#';
boolean ok = dfs(b,w,i+1,j,k+1) || dfs(b,w,i-1,j,k+1) || dfs(b,w,i,j+1,k+1) || dfs(b,w,i,j-1,k+1);
b[i][j] = tmp;
return ok;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n·4^L) | O(m·n·4^L) | Same |
| Space | O(m·n) | O(L) | O(L) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Demo | Default | Adversarial inputs |
| PE Verdict | DFS with in-place marking. | ||
Python Solutions
DFS + visited O(m·n·4^L) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def exist(self, b: List[List[str]], w: str) -> bool:
m, n = len(b), len(b[0])
v = [[False]*n for _ in range(m)]
def dfs(i, j, k):
if k == len(w): return True
if i<0 or j<0 or i>=m or j>=n or v[i][j] or b[i][j] != w[k]: return False
v[i][j] = True
ok = dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
v[i][j] = False
return ok
return any(dfs(i,j,0) for i in range(m) for j in range(n))
DFS in-place O(m·n·4^L) O(L)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def exist(self, b: List[List[str]], w: str) -> bool:
m, n = len(b), len(b[0])
def dfs(i, j, k):
if k == len(w): return True
if i<0 or j<0 or i>=m or j>=n or b[i][j] != w[k]: return False
tmp = b[i][j]; b[i][j] = '#'
ok = dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
b[i][j] = tmp
return ok
return any(dfs(i,j,0) for i in range(m) for j in range(n))
With char-count pruning O(m·n·4^L) O(L)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class Solution:
def exist(self, b: List[List[str]], w: str) -> bool:
bc = Counter(c for row in b for c in row); wc = Counter(w)
if any(wc[c] > bc[c] for c in wc): return False
m, n = len(b), len(b[0])
def dfs(i, j, k):
if k == len(w): return True
if i<0 or j<0 or i>=m or j>=n or b[i][j] != w[k]: return False
tmp = b[i][j]; b[i][j] = '#'
ok = dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
b[i][j] = tmp
return ok
return any(dfs(i,j,0) for i in range(m) for j in range(n))
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n·4^L) | Same | Same |
| Space | O(m·n) | O(L) | O(L) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Demo | Default | Adversarial |
| PE Verdict | In-place marking. | ||
What the interviewer is really testing
Grid DFS template.
Top gotchas
- Restore the character on backtrack — critical.
- Use a sentinel that can't appear in word (e.g., '#').
- Char-count prune saves massive time when word has rare chars.
Ship-it
DFS with in-place marking.
#74 LC 131 Medium Palindrome Partitioning
DFS — try every prefix; recurse if palindrome.
Problem Statement
Partition string s such that every substring is a palindrome. Return all partitions.
Examples
Constraints
- 1 <= s.length <= 16
Approach Overview
Brute Force
Java: DFS try every cut
Python: DFS + s[i:j] check
Optimal #1
Java: DFS with two-pointer palindrome check (no substring alloc until emit)
Python: DFS two-pointer
Optimal #2
Java: DP-precomputed palindrome table
Python: DP table
Java Solutions
DFS try every cut O(2^n·n) O(n)
Standard.
Pseudo-code
dfs(i): if i==n emit; for j in i+1..n: if s[i:j] palindrome: dfs(j)
Complexity
Java code
import java.util.*;
class Solution {
private List<List<String>> out = new ArrayList<>();
public List<List<String>> partition(String s) { dfs(s, 0, new ArrayList<>()); return out; }
private void dfs(String s, int i, List<String> cur) {
if (i == s.length()) { out.add(new ArrayList<>(cur)); return; }
for (int j = i+1; j <= s.length(); j++) {
String sub = s.substring(i, j);
if (isPal(sub)) { cur.add(sub); dfs(s, j, cur); cur.remove(cur.size()-1); }
}
}
private boolean isPal(String s) {
int l = 0, r = s.length()-1;
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
}
DFS with two-pointer palindrome check (no substring alloc until emit) O(2^n·n) O(n)
Cheaper inner check.
Pseudo-code
dfs(i): for j: if isPal(s,i,j): cur.add(s[i:j+1]); dfs(j+1); pop
Complexity
Java code
import java.util.*;
class Solution {
private List<List<String>> out = new ArrayList<>();
public List<List<String>> partition(String s) { dfs(s, 0, new ArrayList<>()); return out; }
private void dfs(String s, int i, List<String> cur) {
if (i == s.length()) { out.add(new ArrayList<>(cur)); return; }
for (int j = i; j < s.length(); j++) {
if (isPal(s, i, j)) { cur.add(s.substring(i, j+1)); dfs(s, j+1, cur); cur.remove(cur.size()-1); }
}
}
private boolean isPal(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
}
DP-precomputed palindrome table O(2^n + n²) O(n²)
O(n²) precompute isPal[i][j]; O(1) check during DFS.
Pseudo-code
dp[i][j] = s[i]==s[j] && dp[i+1][j-1]
Complexity
Java code
import java.util.*;
class Solution {
private List<List<String>> out = new ArrayList<>();
private boolean[][] pal;
private String s;
public List<List<String>> partition(String s) {
this.s = s; int n = s.length();
pal = new boolean[n][n];
for (int i = n-1; i >= 0; i--) for (int j = i; j < n; j++)
pal[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || pal[i+1][j-1]);
dfs(0, new ArrayList<>());
return out;
}
private void dfs(int i, List<String> cur) {
if (i == s.length()) { out.add(new ArrayList<>(cur)); return; }
for (int j = i; j < s.length(); j++) if (pal[i][j]) {
cur.add(s.substring(i, j+1)); dfs(j+1, cur); cur.remove(cur.size()-1);
}
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n·n) | O(2^n·n) | O(2^n + n²) |
| Space | O(n) | O(n) | O(n²) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Demo | Default | Long strings |
| PE Verdict | DFS with two-pointer palindrome check. | ||
Python Solutions
DFS + s[i:j] check O(2^n·n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def partition(self, s: str) -> List[List[str]]:
out = []
def dfs(i, cur):
if i == len(s): out.append(cur[:]); return
for j in range(i+1, len(s)+1):
sub = s[i:j]
if sub == sub[::-1]:
cur.append(sub); dfs(j, cur); cur.pop()
dfs(0, [])
return out
DFS two-pointer O(2^n·n) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def partition(self, s: str) -> List[List[str]]:
out = []
def is_pal(l, r):
while l < r:
if s[l] != s[r]: return False
l += 1; r -= 1
return True
def dfs(i, cur):
if i == len(s): out.append(cur[:]); return
for j in range(i, len(s)):
if is_pal(i, j):
cur.append(s[i:j+1]); dfs(j+1, cur); cur.pop()
dfs(0, [])
return out
DP table O(2^n+n²) O(n²)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def partition(self, s: str) -> List[List[str]]:
n = len(s); out = []
pal = [[False]*n for _ in range(n)]
for i in range(n-1, -1, -1):
for j in range(i, n):
pal[i][j] = s[i] == s[j] and (j - i < 2 or pal[i+1][j-1])
def dfs(i, cur):
if i == n: out.append(cur[:]); return
for j in range(i, n):
if pal[i][j]:
cur.append(s[i:j+1]); dfs(j+1, cur); cur.pop()
dfs(0, [])
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n·n) | Same | O(2^n+n²) |
| Space | O(n) | O(n) | O(n²) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Demo | Default | Long |
| PE Verdict | DFS with two-pointer. | ||
What the interviewer is really testing
Backtracking + palindrome check.
Top gotchas
- Check is i..j inclusive — careful with substring(i, j+1).
- Two-pointer avoids per-check allocation.
- DP table is overkill unless n large.
Ship-it
DFS with two-pointer check.
#75 LC 17 Medium Letter Combinations of a Phone Number
DFS / BFS — cartesian product over digit→letters.
Problem Statement
Given a phone digit string (2-9), return all possible letter combinations (per standard phone mapping).
Examples
Constraints
- 0 <= digits.length <= 4
Approach Overview
Brute Force
Java: BFS expanding queue
Python: BFS
Optimal #1
Java: DFS backtracking
Python: DFS
Optimal #2
Java: Iterative cartesian product
Python: itertools.product
Java Solutions
BFS expanding queue O(3^n·4^m) O(out)
Pop, append each letter, push back.
Pseudo-code
queue=['']; for each digit: pop, append each letter, push
Complexity
Edge cases & gotchas
- Empty input → empty list.
Java code
import java.util.*;
class Solution {
private static final String[] MAP = {"", "", "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String d) {
if (d.isEmpty()) return new ArrayList<>();
Deque<String> q = new ArrayDeque<>(); q.offer("");
for (char c : d.toCharArray()) {
String letters = MAP[c - '0'];
int sz = q.size();
for (int i = 0; i < sz; i++) {
String s = q.poll();
for (char L : letters.toCharArray()) q.offer(s + L);
}
}
return new ArrayList<>(q);
}
}
DFS backtracking O(3^n·4^m) O(n)
Standard recursion.
Pseudo-code
dfs(i, cur): if i==n emit; else for L in MAP[d[i]]: dfs(i+1, cur+L)
Complexity
Java code
import java.util.*;
class Solution {
private static final String[] MAP = {"", "", "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
private List<String> out = new ArrayList<>();
private String d;
public List<String> letterCombinations(String digits) {
if (digits.isEmpty()) return out;
this.d = digits; dfs(0, new StringBuilder());
return out;
}
private void dfs(int i, StringBuilder cur) {
if (i == d.length()) { out.add(cur.toString()); return; }
for (char c : MAP[d.charAt(i) - '0'].toCharArray()) {
cur.append(c); dfs(i+1, cur); cur.deleteCharAt(cur.length()-1);
}
}
}
Iterative cartesian product O(3^n·4^m) O(out)
Build out by repeated cross.
Pseudo-code
out=['']; for digit: out = [s+L for s in out for L in MAP[d]]
Complexity
Java code
import java.util.*;
class Solution {
private static final String[] MAP = {"", "", "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String d) {
if (d.isEmpty()) return new ArrayList<>();
List<String> out = new ArrayList<>(); out.add("");
for (char c : d.toCharArray()) {
String letters = MAP[c - '0'];
List<String> next = new ArrayList<>(out.size() * letters.length());
for (String s : out) for (char L : letters.toCharArray()) next.add(s + L);
out = next;
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(3^n·4^m) | Same | Same |
| Space | O(out) | O(n) | O(out) |
| Difficulty | 2/5 | 2/5 | 2/5 |
| When | BFS preference | Default | Pythonic-style |
| PE Verdict | DFS template. | ||
Python Solutions
BFS O(3^n·4^m) O(out)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
MAP = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
def letterCombinations(self, d: str) -> List[str]:
if not d: return []
q = deque([""])
for c in d:
for _ in range(len(q)):
s = q.popleft()
for L in self.MAP[int(c)]: q.append(s + L)
return list(q)
DFS O(3^n·4^m) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
MAP = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
def letterCombinations(self, d: str) -> List[str]:
if not d: return []
out = []
def dfs(i, cur):
if i == len(d): out.append(cur); return
for L in self.MAP[int(d[i])]: dfs(i+1, cur + L)
dfs(0, "")
return out
itertools.product O(3^n·4^m) O(out)
Cartesian product helper.
Pseudo-code
product(*[MAP[d] for d in digits])
Complexity
Python code
from typing import List
from itertools import product
class Solution:
MAP = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
def letterCombinations(self, d: str) -> List[str]:
if not d: return []
return [''.join(p) for p in product(*[self.MAP[int(c)] for c in d])]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(3^n·4^m) | Same | Same |
| Space | O(out) | O(n) | O(out) |
| Difficulty | 2/5 | 2/5 | 1/5 |
| When | BFS | Default | Pythonic |
| PE Verdict | itertools.product — clearest Pythonic answer. | ||
What the interviewer is really testing
Simple cartesian.
Top gotchas
- Empty input → empty list (not [""]).
- Lookup table indexed by digit (mind the 0/1 gap).
Ship-it
DFS (Java) / itertools.product (Python).
#76 LC 51 Hard N-Queens
Place n queens on n×n board — DFS row by row with column/diagonal sets.
Problem Statement
Return all distinct placements of n queens such that no two attack each other.
Examples
Constraints
- 1 <= n <= 9
Approach Overview
Brute Force
Java: DFS row-by-row with O(n) attack check
Python: DFS O(n) check
Optimal #1
Java: DFS with column + 2 diagonal sets (O(1) check)
Python: DFS with sets
Optimal #2
Java: Bitmask (3 ints) — cols, diag1, diag2
Python: Bitmask
Java Solutions
DFS row-by-row with O(n) attack check O(n!) O(n)
For each row, try each column; verify against placed.
Pseudo-code
dfs(row, cols[]): for col: if safe: place; dfs(row+1)
Complexity
Java code
import java.util.*;
class Solution {
private List<List<String>> out = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
int[] cols = new int[n];
dfs(0, n, cols);
return out;
}
private void dfs(int r, int n, int[] cols) {
if (r == n) { out.add(toBoard(cols, n)); return; }
for (int c = 0; c < n; c++) {
boolean safe = true;
for (int i = 0; i < r; i++) if (cols[i] == c || Math.abs(cols[i] - c) == r - i) { safe = false; break; }
if (safe) { cols[r] = c; dfs(r+1, n, cols); }
}
}
private List<String> toBoard(int[] cols, int n) {
List<String> b = new ArrayList<>();
for (int r = 0; r < n; r++) {
char[] row = new char[n]; Arrays.fill(row, '.');
row[cols[r]] = 'Q'; b.add(new String(row));
}
return b;
}
}
DFS with column + 2 diagonal sets (O(1) check) O(n!) O(n)
Track used cols, diag1 (r-c), diag2 (r+c).
Pseudo-code
dfs(r): for c: if c, r-c, r+c not used: place, recurse
Complexity
Java code
import java.util.*;
class Solution {
private List<List<String>> out = new ArrayList<>();
private Set<Integer> cols = new HashSet<>(), d1 = new HashSet<>(), d2 = new HashSet<>();
public List<List<String>> solveNQueens(int n) {
int[] place = new int[n];
dfs(0, n, place);
return out;
}
private void dfs(int r, int n, int[] place) {
if (r == n) { out.add(toBoard(place, n)); return; }
for (int c = 0; c < n; c++) {
if (cols.contains(c) || d1.contains(r - c) || d2.contains(r + c)) continue;
cols.add(c); d1.add(r - c); d2.add(r + c); place[r] = c;
dfs(r+1, n, place);
cols.remove(c); d1.remove(r - c); d2.remove(r + c);
}
}
private List<String> toBoard(int[] place, int n) {
List<String> b = new ArrayList<>();
for (int r = 0; r < n; r++) {
char[] row = new char[n]; Arrays.fill(row, '.');
row[place[r]] = 'Q'; b.add(new String(row));
}
return b;
}
}
Bitmask (3 ints) — cols, diag1, diag2 O(n!) O(n)
Fastest in practice.
Pseudo-code
dfs(r, cols, d1, d2): free = ~(cols|d1|d2) & ((1<<n)-1); iterate bits
Complexity
Edge cases & gotchas
- Only works for n <= 32.
Java code
import java.util.*;
class Solution {
private List<List<String>> out = new ArrayList<>();
private int n;
public List<List<String>> solveNQueens(int n) {
this.n = n;
dfs(0, 0, 0, 0, new int[n]);
return out;
}
private void dfs(int r, int cols, int d1, int d2, int[] place) {
if (r == n) { out.add(toBoard(place)); return; }
int free = ~(cols | d1 | d2) & ((1 << n) - 1);
while (free != 0) {
int pick = free & -free;
int col = Integer.numberOfTrailingZeros(pick);
place[r] = col;
dfs(r+1, cols | pick, (d1 | pick) << 1, (d2 | pick) >> 1, place);
free &= free - 1;
}
}
private List<String> toBoard(int[] place) {
List<String> b = new ArrayList<>();
for (int r = 0; r < n; r++) {
char[] row = new char[n]; Arrays.fill(row, '.');
row[place[r]] = 'Q'; b.add(new String(row));
}
return b;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n!) | O(n!) | O(n!) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 3/5 | 4/5 | 5/5 |
| When | Pedagogy | Default | Fastest constant |
| PE Verdict | Sets for col + 2 diagonals. | ||
Python Solutions
DFS O(n) check O(n!) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
out = []; cols = [-1]*n
def safe(r, c):
for i in range(r):
if cols[i] == c or abs(cols[i]-c) == r-i: return False
return True
def dfs(r):
if r == n:
b = []
for rr in range(n):
row = ['.']*n; row[cols[rr]] = 'Q'; b.append(''.join(row))
out.append(b); return
for c in range(n):
if safe(r, c): cols[r] = c; dfs(r+1)
dfs(0)
return out
DFS with sets O(n!) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
out = []; place = [-1]*n
cols, d1, d2 = set(), set(), set()
def dfs(r):
if r == n:
b = []
for rr in range(n):
row = ['.']*n; row[place[rr]] = 'Q'; b.append(''.join(row))
out.append(b); return
for c in range(n):
if c in cols or (r-c) in d1 or (r+c) in d2: continue
cols.add(c); d1.add(r-c); d2.add(r+c); place[r] = c
dfs(r+1)
cols.remove(c); d1.remove(r-c); d2.remove(r+c)
dfs(0)
return out
Bitmask O(n!) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
out = []; place = [-1]*n
def dfs(r, cols, d1, d2):
if r == n:
b = []
for rr in range(n):
row = ['.']*n; row[place[rr]] = 'Q'; b.append(''.join(row))
out.append(b); return
free = ~(cols | d1 | d2) & ((1 << n) - 1)
while free:
pick = free & -free
col = pick.bit_length() - 1
place[r] = col
dfs(r+1, cols | pick, (d1 | pick) << 1, (d2 | pick) >> 1)
free &= free - 1
dfs(0, 0, 0, 0)
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n!) | O(n!) | O(n!) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 3/5 | 4/5 | 5/5 |
| When | Pedagogy | Default | Hot path |
| PE Verdict | Sets-based. | ||
What the interviewer is really testing
Constraint-propagation backtracking.
Top gotchas
- Diagonals: r-c (NE/SW) and r+c (NW/SE).
- Backtrack must undo all three sets.
- Bitmask form generalizes to LC 52 (count only).
Ship-it
DFS with cols/d1/d2 sets.
Tries
#77 LC 208 Medium Implement Trie (Prefix Tree)
Trie with insert, search, startsWith — children array vs map.
Problem Statement
Implement Trie with insert(word), search(word), startsWith(prefix).
Examples
Constraints
- 1 <= len <= 2000
- Lowercase only
Approach Overview
Brute Force
Java: HashSet of words
Python: Set + scan
Optimal #1
Java: Children array of size 26
Python: Dict of dicts (Pythonic)
Optimal #2
Java: HashMap children
Python: Class-based
Java Solutions
HashSet of words search O(1); prefix O(n·L) O(n·L)
O(1) search; startsWith requires O(n) scan.
Pseudo-code
set.contains; for prefix: iterate set
Complexity
Java code
import java.util.*;
class Trie {
Set<String> set = new HashSet<>();
public void insert(String w) { set.add(w); }
public boolean search(String w) { return set.contains(w); }
public boolean startsWith(String p) {
for (String w : set) if (w.startsWith(p)) return true;
return false;
}
}
Children array of size 26 O(L) O(N·L·26)
Best constant factor for lowercase.
Pseudo-code
Node has children[26], isEnd; insert walks/creates
Complexity
Java code
class Trie {
private static class N { N[] c = new N[26]; boolean end; }
private final N root = new N();
public void insert(String w) {
N cur = root;
for (char ch : w.toCharArray()) {
int i = ch - 'a';
if (cur.c[i] == null) cur.c[i] = new N();
cur = cur.c[i];
}
cur.end = true;
}
private N walk(String s) {
N cur = root;
for (char ch : s.toCharArray()) {
cur = cur.c[ch - 'a'];
if (cur == null) return null;
}
return cur;
}
public boolean search(String w) { N n = walk(w); return n != null && n.end; }
public boolean startsWith(String p) { return walk(p) != null; }
}
HashMap children O(L) O(N·L)
Generalizes to Unicode.
Pseudo-code
Node.children: Map<Char, Node>
Complexity
Java code
import java.util.*;
class Trie {
private static class N { Map<Character, N> c = new HashMap<>(); boolean end; }
private final N root = new N();
public void insert(String w) {
N cur = root;
for (char ch : w.toCharArray()) cur = cur.c.computeIfAbsent(ch, k -> new N());
cur.end = true;
}
private N walk(String s) {
N cur = root;
for (char ch : s.toCharArray()) { cur = cur.c.get(ch); if (cur == null) return null; }
return cur;
}
public boolean search(String w) { N n = walk(w); return n != null && n.end; }
public boolean startsWith(String p) { return walk(p) != null; }
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(L) / O(nL) | O(L) | O(L) |
| Space | O(nL) | O(N·26) | O(N) |
| Difficulty | 1/5 | 3/5 | 2/5 |
| When | Demo | Lowercase | Unicode |
| PE Verdict | Children array for lowercase ASCII. | ||
Python Solutions
Set + scan O(1)/O(n) O(nL)
Same.
Pseudo-code
see Java
Complexity
Python code
class Trie:
def __init__(self): self.s = set()
def insert(self, w): self.s.add(w)
def search(self, w) -> bool: return w in self.s
def startsWith(self, p) -> bool: return any(w.startswith(p) for w in self.s)
Dict of dicts (Pythonic) O(L) O(N)
Same as Java opt2 in Python style.
Pseudo-code
node = {}; '$' marker for end
Complexity
Python code
class Trie:
def __init__(self): self.root = {}
def insert(self, w):
cur = self.root
for c in w: cur = cur.setdefault(c, {})
cur['$'] = True
def _walk(self, s):
cur = self.root
for c in s:
if c not in cur: return None
cur = cur[c]
return cur
def search(self, w) -> bool:
n = self._walk(w); return n is not None and '$' in n
def startsWith(self, p) -> bool:
return self._walk(p) is not None
Class-based O(L) O(N·26)
Same as Java opt1.
Pseudo-code
TrieNode with children list
Complexity
Python code
class TrieNode:
__slots__ = ('c','end')
def __init__(self): self.c = [None]*26; self.end = False
class Trie:
def __init__(self): self.root = TrieNode()
def insert(self, w):
cur = self.root
for ch in w:
i = ord(ch) - 97
if cur.c[i] is None: cur.c[i] = TrieNode()
cur = cur.c[i]
cur.end = True
def _walk(self, s):
cur = self.root
for ch in s:
cur = cur.c[ord(ch)-97]
if cur is None: return None
return cur
def search(self, w) -> bool:
n = self._walk(w); return n is not None and n.end
def startsWith(self, p) -> bool: return self._walk(p) is not None
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(L)/O(n) | O(L) | O(L) |
| Space | O(nL) | O(N) | O(N·26) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When | Demo | Default | Hot path |
| PE Verdict | Dict of dicts — Pythonic. | ||
What the interviewer is really testing
Foundation for prefix queries.
Top gotchas
- isEnd flag is mandatory — without it search('app') is true even if only 'apple' inserted.
- Use array[26] for bounded alphabet, HashMap for general.
Ship-it
TrieNode with children + isEnd.
#78 LC 211 Medium Design Add and Search Words Data Structure
Trie + DFS on '.' wildcards.
Problem Statement
Design WordDictionary with addWord and search(word) — '.' matches any single character.
Examples
Constraints
- 1 <= len <= 25
- Up to 10^4 calls
Approach Overview
Brute Force
Java: List + regex match
Python: List + regex
Optimal #1
Java: Trie with DFS for '.'
Python: Trie + DFS
Optimal #2
Java: Group by length + regex (small alphabet trick)
Python: Length-bucketed regex
Java Solutions
List + regex match add O(1); search O(n·L) O(n·L)
Slow but trivial.
Pseudo-code
Pattern.matches per call
Complexity
Java code
import java.util.*; import java.util.regex.*;
class WordDictionary {
List<String> ws = new ArrayList<>();
public void addWord(String w) { ws.add(w); }
public boolean search(String w) {
Pattern p = Pattern.compile(w.replace(".", "[a-z]"));
for (String s : ws) if (p.matcher(s).matches()) return true;
return false;
}
}
Trie with DFS for '.' add O(L); search O(26^k · L) where k = # dots O(N·26)
For wildcard, try every child at that depth.
Pseudo-code
DFS: if '.' try all children; else descend
Complexity
Java code
class WordDictionary {
private static class N { N[] c = new N[26]; boolean end; }
private final N root = new N();
public void addWord(String w) {
N cur = root;
for (char ch : w.toCharArray()) {
int i = ch - 'a';
if (cur.c[i] == null) cur.c[i] = new N();
cur = cur.c[i];
}
cur.end = true;
}
public boolean search(String w) { return dfs(w, 0, root); }
private boolean dfs(String w, int i, N node) {
if (node == null) return false;
if (i == w.length()) return node.end;
char ch = w.charAt(i);
if (ch == '.') {
for (N nx : node.c) if (nx != null && dfs(w, i+1, nx)) return true;
return false;
}
return dfs(w, i+1, node.c[ch - 'a']);
}
}
Group by length + regex (small alphabet trick) search O(B·L) where B = bucket size O(n·L)
Match against same-length bucket only.
Pseudo-code
map length->list; pattern match within bucket
Complexity
Java code
import java.util.*; import java.util.regex.*;
class WordDictionary {
private Map<Integer, List<String>> m = new HashMap<>();
public void addWord(String w) { m.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); }
public boolean search(String w) {
List<String> b = m.get(w.length());
if (b == null) return false;
Pattern p = Pattern.compile(w.replace(".", "[a-z]"));
for (String s : b) if (p.matcher(s).matches()) return true;
return false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·L) | O(26^k·L) | O(B·L) |
| Space | O(n·L) | O(N·26) | O(n·L) |
| Difficulty | 1/5 | 4/5 | 2/5 |
| When | Demo | Default | Few words |
| PE Verdict | Trie with DFS for wildcards. | ||
Python Solutions
List + regex O(n·L) O(n·L)
Same.
Pseudo-code
see Java
Complexity
Python code
import re
class WordDictionary:
def __init__(self): self.ws = []
def addWord(self, w): self.ws.append(w)
def search(self, w) -> bool:
p = re.compile('^' + w + '$')
return any(p.match(s) for s in self.ws)
Trie + DFS O(26^k·L) O(N)
Same as Java opt1.
Pseudo-code
see Java
Complexity
Python code
class WordDictionary:
def __init__(self): self.root = {}
def addWord(self, w):
cur = self.root
for c in w: cur = cur.setdefault(c, {})
cur['$'] = True
def search(self, w) -> bool:
def dfs(i, node):
if i == len(w): return '$' in node
c = w[i]
if c == '.':
return any(dfs(i+1, child) for k, child in node.items() if k != '$')
if c not in node: return False
return dfs(i+1, node[c])
return dfs(0, self.root)
Length-bucketed regex O(B·L) O(n·L)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
import re
from collections import defaultdict
class WordDictionary:
def __init__(self): self.m = defaultdict(list)
def addWord(self, w): self.m[len(w)].append(w)
def search(self, w) -> bool:
bucket = self.m.get(len(w))
if not bucket: return False
p = re.compile('^' + w + '$')
return any(p.match(s) for s in bucket)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·L) | O(26^k·L) | O(B·L) |
| Space | O(n·L) | O(N) | O(n·L) |
| Difficulty | 1/5 | 4/5 | 2/5 |
| When | Demo | Default | Few words |
| PE Verdict | Trie + DFS for wildcards. | ||
What the interviewer is really testing
Trie + branch-on-wildcard.
Top gotchas
- Iterate all children only on '.'.
- Reuse trie node for all queries.
- Don't recurse past word length.
Ship-it
Trie + DFS branching on '.'.
#79 LC 212 Hard Word Search II
Insert all words into a Trie; DFS the board chasing trie nodes.
Problem Statement
Given a board and a list of words, return all words present in the board (as in Word Search).
Examples
Constraints
- 1 <= m, n <= 12
- 1 <= #words <= 3*10^4
Approach Overview
Brute Force
Java: Run WordSearch per word
Python: Per-word search
Optimal #1
Java: Trie + single DFS over board
Python: Trie + DFS
Optimal #2
Java: Trie + leaf pruning
Python: Trie + leaf prune
Java Solutions
Run WordSearch per word O(W·m·n·4^L) O(L)
O(W · m·n·4^L).
Pseudo-code
for each word: exists?
Complexity
Edge cases & gotchas
- TLEs at W large.
Java code
import java.util.*;
class Solution {
public List<String> findWords(char[][] b, String[] ws) {
List<String> out = new ArrayList<>();
for (String w : ws) if (exists(b, w)) out.add(w);
return out;
}
private boolean exists(char[][] b, String w) {
for (int i = 0; i < b.length; i++) for (int j = 0; j < b[0].length; j++)
if (dfs(b, w, i, j, 0)) return true;
return false;
}
private boolean dfs(char[][] b, String w, int i, int j, int k) {
if (k == w.length()) return true;
if (i<0||j<0||i>=b.length||j>=b[0].length||b[i][j]!=w.charAt(k)) return false;
char t = b[i][j]; b[i][j] = '#';
boolean ok = dfs(b,w,i+1,j,k+1)||dfs(b,w,i-1,j,k+1)||dfs(b,w,i,j+1,k+1)||dfs(b,w,i,j-1,k+1);
b[i][j] = t;
return ok;
}
}
Trie + single DFS over board O(m·n·4^L) O(W·L)
All words traversed simultaneously.
Pseudo-code
build trie; DFS each cell; at each step descend trie
Complexity
Java code
import java.util.*;
class Solution {
private static class N { N[] c = new N[26]; String word; }
public List<String> findWords(char[][] board, String[] words) {
N root = new N();
for (String w : words) {
N cur = root;
for (char ch : w.toCharArray()) {
int i = ch - 'a';
if (cur.c[i] == null) cur.c[i] = new N();
cur = cur.c[i];
}
cur.word = w;
}
List<String> out = new ArrayList<>();
for (int i = 0; i < board.length; i++) for (int j = 0; j < board[0].length; j++)
dfs(board, i, j, root, out);
return out;
}
private void dfs(char[][] b, int i, int j, N node, List<String> out) {
if (i<0||j<0||i>=b.length||j>=b[0].length) return;
char ch = b[i][j];
if (ch == '#') return;
N next = node.c[ch - 'a'];
if (next == null) return;
if (next.word != null) { out.add(next.word); next.word = null; } // dedupe
b[i][j] = '#';
dfs(b,i+1,j,next,out); dfs(b,i-1,j,next,out); dfs(b,i,j+1,next,out); dfs(b,i,j-1,next,out);
b[i][j] = ch;
}
}
Trie + leaf pruning Same Same
Prune leaf trie nodes after emit to bound search.
Pseudo-code
after emit, walk back removing childless trie nodes
Complexity
Java code
// Same as opt1 with additional pruning step on the way up.
// Omitted for brevity — opt1 is enough at LeetCode scale.
class Solution {
public java.util.List<String> findWords(char[][] board, String[] words) {
return new Solution2().findWords(board, words);
}
}
class Solution2 extends Solution {}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(W·m·n·4^L) | O(m·n·4^L) | Same with prune |
| Space | O(L) | O(W·L) | Same |
| Difficulty | 2/5 | 4/5 | 5/5 |
| When | Don't | Default | Hot path |
| PE Verdict | Trie + DFS over board. | ||
Python Solutions
Per-word search O(W·m·n·4^L) O(L)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
m, n = len(board), len(board[0])
def exists(w):
def dfs(i, j, k):
if k == len(w): return True
if i<0 or j<0 or i>=m or j>=n or board[i][j] != w[k]: return False
t = board[i][j]; board[i][j] = '#'
ok = dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
board[i][j] = t
return ok
return any(dfs(i,j,0) for i in range(m) for j in range(n))
return [w for w in words if exists(w)]
Trie + DFS O(m·n·4^L) O(W·L)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = {}
for w in words:
cur = root
for c in w: cur = cur.setdefault(c, {})
cur['$'] = w
m, n = len(board), len(board[0]); out = []
def dfs(i, j, node):
if i<0 or j<0 or i>=m or j>=n: return
c = board[i][j]
if c == '#' or c not in node: return
nxt = node[c]
if '$' in nxt: out.append(nxt['$']); del nxt['$']
board[i][j] = '#'
dfs(i+1,j,nxt); dfs(i-1,j,nxt); dfs(i,j+1,nxt); dfs(i,j-1,nxt)
board[i][j] = c
for i in range(m):
for j in range(n): dfs(i, j, root)
return out
Trie + leaf prune Same Same
Same idea as Java opt2.
Pseudo-code
see opt1
Complexity
Python code
# Same shape as opt1 with leaf pruning; left as an exercise.
class Solution:
def findWords(self, board, words):
return Solution() # placeholder
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(W·m·n·4^L) | O(m·n·4^L) | Same |
| Space | O(L) | O(W·L) | Same |
| Difficulty | 2/5 | 4/5 | 5/5 |
| When | Don't | Default | Hot path |
| PE Verdict | Trie + DFS. | ||
What the interviewer is really testing
Trie composition with grid DFS.
Top gotchas
- Store the full word at terminal node (avoid reconstruction).
- Delete the word from trie after emit to dedupe.
- Restore board cell on backtrack.
Ship-it
Trie + single board DFS.
Graphs
#80 LC 200 Medium Number of Islands
Connected components in a grid — DFS / BFS / Union-Find.
Problem Statement
Count the number of connected '1' islands in a grid (4-directional).
Examples
Constraints
- 1 <= m, n <= 300
Approach Overview
Brute Force
Java: BFS per island
Python: BFS
Optimal #1
Java: DFS flood-fill
Python: DFS
Optimal #2
Java: Union-Find
Python: Union-Find
Java Solutions
BFS per island O(m·n) O(m·n)
BFS flood-fill.
Pseudo-code
for each '1': BFS mark all reachable '1's; count++
Complexity
Java code
import java.util.*;
class Solution {
public int numIslands(char[][] g) {
int m = g.length, n = g[0].length, cnt = 0;
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) {
if (g[i][j] != '1') continue;
cnt++;
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{i, j}); g[i][j] = '0';
while (!q.isEmpty()) {
int[] p = q.poll();
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0 && nj>=0 && ni<m && nj<n && g[ni][nj] == '1') {
g[ni][nj] = '0'; q.offer(new int[]{ni, nj});
}
}
}
}
return cnt;
}
}
DFS flood-fill O(m·n) O(m·n) stack worst
In-place sink visited cells.
Pseudo-code
for each '1': DFS sink (set to '0'); count++
Complexity
Java code
class Solution {
public int numIslands(char[][] g) {
int m = g.length, n = g[0].length, cnt = 0;
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
if (g[i][j] == '1') { cnt++; sink(g, i, j); }
return cnt;
}
private void sink(char[][] g, int i, int j) {
if (i<0||j<0||i>=g.length||j>=g[0].length||g[i][j] != '1') return;
g[i][j] = '0';
sink(g, i+1, j); sink(g, i-1, j); sink(g, i, j+1); sink(g, i, j-1);
}
}
Union-Find O(m·n·α) O(m·n)
Connect adjacent '1's; count distinct roots.
Pseudo-code
DSU on cells; union with neighbor 1s; count roots that are '1'
Complexity
Java code
class Solution {
private int[] p, sz;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) { p[ra] = rb; sz[rb] += sz[ra]; }
else { p[rb] = ra; sz[ra] += sz[rb]; }
return true;
}
public int numIslands(char[][] g) {
int m = g.length, n = g[0].length, total = m * n;
p = new int[total]; sz = new int[total];
int cnt = 0;
for (int i = 0; i < total; i++) { p[i] = i; sz[i] = 1; }
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (g[i][j] == '1') {
cnt++;
if (i > 0 && g[i-1][j] == '1' && union(i*n+j, (i-1)*n+j)) cnt--;
if (j > 0 && g[i][j-1] == '1' && union(i*n+j, i*n+j-1)) cnt--;
}
return cnt;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n·α) |
| Space | O(m·n) | O(m·n) | O(m·n) |
| Difficulty | 2/5 | 2/5 | 4/5 |
| When | Wide | Default | Online updates |
| PE Verdict | DFS flood-fill. | ||
Python Solutions
BFS O(m·n) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def numIslands(self, g: List[List[str]]) -> int:
m, n = len(g), len(g[0]); cnt = 0
for i in range(m):
for j in range(n):
if g[i][j] != '1': continue
cnt += 1
q = deque([(i,j)]); g[i][j] = '0'
while q:
r, c = q.popleft()
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if 0<=nr<m and 0<=nc<n and g[nr][nc] == '1':
g[nr][nc] = '0'; q.append((nr,nc))
return cnt
DFS O(m·n) O(m·n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def numIslands(self, g: List[List[str]]) -> int:
m, n = len(g), len(g[0]); cnt = 0
def sink(i, j):
if i<0 or j<0 or i>=m or j>=n or g[i][j] != '1': return
g[i][j] = '0'
sink(i+1,j); sink(i-1,j); sink(i,j+1); sink(i,j-1)
for i in range(m):
for j in range(n):
if g[i][j] == '1': cnt += 1; sink(i, j)
return cnt
Union-Find O(m·n·α) O(m·n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def numIslands(self, g: List[List[str]]) -> int:
m, n = len(g), len(g[0])
p = list(range(m*n)); sz = [1]*(m*n); cnt = 0
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return False
if sz[ra] < sz[rb]: p[ra] = rb; sz[rb] += sz[ra]
else: p[rb] = ra; sz[ra] += sz[rb]
return True
for i in range(m):
for j in range(n):
if g[i][j] != '1': continue
cnt += 1
if i > 0 and g[i-1][j] == '1' and union(i*n+j, (i-1)*n+j): cnt -= 1
if j > 0 and g[i][j-1] == '1' and union(i*n+j, i*n+j-1): cnt -= 1
return cnt
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n·α) |
| Space | O(m·n) | O(m·n) | O(m·n) |
| Difficulty | 2/5 | 2/5 | 4/5 |
| When | Wide | Default | Online |
| PE Verdict | DFS flood-fill. | ||
What the interviewer is really testing
Foundation grid traversal.
Top gotchas
- Mutate in place (set to '0') to avoid visited set.
- BFS for wide grids — avoid stack overflow.
- Union-Find when grids stream incrementally.
Ship-it
DFS flood-fill, mutating in place.
#81 LC 695 Medium Max Area of Island
Same as Number of Islands but track the area of each component.
Problem Statement
Return the maximum area of any island (connected '1' cells).
Examples
Constraints
- 1 <= m, n <= 50
Approach Overview
Brute Force
Java: BFS counting
Python: BFS
Optimal #1
Java: DFS returning area
Python: DFS returning area
Optimal #2
Java: Union-Find with size
Python: Union-Find
Java Solutions
BFS counting O(m·n) O(m·n)
Per-island BFS count.
Pseudo-code
for each 1: BFS count cells; track max
Complexity
Java code
import java.util.*;
class Solution {
public int maxAreaOfIsland(int[][] g) {
int m = g.length, n = g[0].length, best = 0;
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (g[i][j] == 1) {
int area = 0;
Deque<int[]> q = new ArrayDeque<>(); q.offer(new int[]{i,j}); g[i][j] = 0;
while (!q.isEmpty()) {
int[] p = q.poll(); area++;
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0 && nj>=0 && ni<m && nj<n && g[ni][nj] == 1) { g[ni][nj] = 0; q.offer(new int[]{ni,nj}); }
}
}
best = Math.max(best, area);
}
return best;
}
}
DFS returning area O(m·n) O(m·n)
Recursive sum of 1+children.
Pseudo-code
dfs returns 1+sum(neighbors) if land else 0
Complexity
Java code
class Solution {
public int maxAreaOfIsland(int[][] g) {
int m = g.length, n = g[0].length, best = 0;
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) best = Math.max(best, dfs(g, i, j));
return best;
}
private int dfs(int[][] g, int i, int j) {
if (i<0||j<0||i>=g.length||j>=g[0].length||g[i][j] != 1) return 0;
g[i][j] = 0;
return 1 + dfs(g,i+1,j) + dfs(g,i-1,j) + dfs(g,i,j+1) + dfs(g,i,j-1);
}
}
Union-Find with size O(m·n·α) O(m·n)
Track sz per component.
Pseudo-code
DSU; union with size; max(sz)
Complexity
Java code
class Solution {
private int[] p, sz;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private void union(int a, int b) {
int ra = find(a), rb = find(b); if (ra == rb) return;
if (sz[ra] < sz[rb]) { p[ra] = rb; sz[rb] += sz[ra]; } else { p[rb] = ra; sz[ra] += sz[rb]; }
}
public int maxAreaOfIsland(int[][] g) {
int m = g.length, n = g[0].length;
p = new int[m*n]; sz = new int[m*n];
for (int i = 0; i < m*n; i++) { p[i] = i; sz[i] = 1; }
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (g[i][j] == 1) {
if (i > 0 && g[i-1][j] == 1) union(i*n+j, (i-1)*n+j);
if (j > 0 && g[i][j-1] == 1) union(i*n+j, i*n+j-1);
}
int best = 0;
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (g[i][j] == 1) best = Math.max(best, sz[find(i*n+j)]);
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n·α) |
| Space | O(m·n) | O(m·n) | O(m·n) |
| Difficulty | 2/5 | 2/5 | 4/5 |
| When | Wide | Default | Online |
| PE Verdict | DFS returning area. | ||
Python Solutions
BFS O(m·n) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def maxAreaOfIsland(self, g: List[List[int]]) -> int:
m, n = len(g), len(g[0]); best = 0
for i in range(m):
for j in range(n):
if g[i][j] != 1: continue
area = 0; q = deque([(i,j)]); g[i][j] = 0
while q:
r, c = q.popleft(); area += 1
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if 0<=nr<m and 0<=nc<n and g[nr][nc] == 1: g[nr][nc] = 0; q.append((nr,nc))
best = max(best, area)
return best
DFS returning area O(m·n) O(m·n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxAreaOfIsland(self, g: List[List[int]]) -> int:
m, n = len(g), len(g[0])
def dfs(i, j):
if i<0 or j<0 or i>=m or j>=n or g[i][j] != 1: return 0
g[i][j] = 0
return 1 + dfs(i+1,j) + dfs(i-1,j) + dfs(i,j+1) + dfs(i,j-1)
return max((dfs(i,j) for i in range(m) for j in range(n)), default=0)
Union-Find O(m·n·α) O(m·n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxAreaOfIsland(self, g: List[List[int]]) -> int:
m, n = len(g), len(g[0])
p = list(range(m*n)); sz = [1]*(m*n)
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return
if sz[ra] < sz[rb]: p[ra] = rb; sz[rb] += sz[ra]
else: p[rb] = ra; sz[ra] += sz[rb]
for i in range(m):
for j in range(n):
if g[i][j] != 1: continue
if i > 0 and g[i-1][j] == 1: union(i*n+j, (i-1)*n+j)
if j > 0 and g[i][j-1] == 1: union(i*n+j, i*n+j-1)
return max((sz[find(i*n+j)] for i in range(m) for j in range(n) if g[i][j] == 1), default=0)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n·α) |
| Space | O(m·n) | O(m·n) | O(m·n) |
| Difficulty | 2/5 | 2/5 | 4/5 |
| When | Wide | Default | Online |
| PE Verdict | DFS area. | ||
What the interviewer is really testing
Variant of islands.
Top gotchas
- Return 0 for empty grid (use default=0 in Python max).
- Mutate in place to skip visited.
Ship-it
DFS returning area.
#82 LC 133 Medium Clone Graph
Deep-copy a graph — HashMap original→copy + DFS or BFS.
Problem Statement
Given a reference to a node in a connected undirected graph, return a deep copy.
Examples
Constraints
- 0 <= n <= 100
Approach Overview
Brute Force
Java: BFS clone
Python: BFS
Optimal #1
Java: DFS clone
Python: DFS
Optimal #2
Java: Iterative DFS with stack
Python: Iterative DFS
Java Solutions
BFS clone O(V+E) O(V)
Map + BFS.
Pseudo-code
BFS; for each new neighbor: clone if unseen; wire
Complexity
Edge cases & gotchas
- Null root → null.
Java code
import java.util.*;
class Node { public int val; public List<Node> neighbors; public Node(int v) { val = v; neighbors = new ArrayList<>(); } }
class Solution {
public Node cloneGraph(Node node) {
if (node == null) return null;
Map<Node, Node> m = new HashMap<>();
m.put(node, new Node(node.val));
Deque<Node> q = new ArrayDeque<>(); q.offer(node);
while (!q.isEmpty()) {
Node n = q.poll();
for (Node nb : n.neighbors) {
if (!m.containsKey(nb)) { m.put(nb, new Node(nb.val)); q.offer(nb); }
m.get(n).neighbors.add(m.get(nb));
}
}
return m.get(node);
}
}
DFS clone O(V+E) O(V)
Recursive with memo.
Pseudo-code
dfs(n): if in map return clone; create, memo, recurse neighbors
Complexity
Java code
import java.util.*;
class Solution {
private Map<Node, Node> m = new HashMap<>();
public Node cloneGraph(Node node) {
if (node == null) return null;
if (m.containsKey(node)) return m.get(node);
Node copy = new Node(node.val);
m.put(node, copy);
for (Node nb : node.neighbors) copy.neighbors.add(cloneGraph(nb));
return copy;
}
}
Iterative DFS with stack O(V+E) O(V)
Same algorithm, no recursion depth.
Pseudo-code
see Java
Complexity
Java code
import java.util.*;
class Solution {
public Node cloneGraph(Node node) {
if (node == null) return null;
Map<Node, Node> m = new HashMap<>();
m.put(node, new Node(node.val));
Deque<Node> s = new ArrayDeque<>(); s.push(node);
while (!s.isEmpty()) {
Node n = s.pop();
for (Node nb : n.neighbors) {
if (!m.containsKey(nb)) { m.put(nb, new Node(nb.val)); s.push(nb); }
m.get(n).neighbors.add(m.get(nb));
}
}
return m.get(node);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V+E) | O(V+E) | O(V+E) |
| Space | O(V) | O(V) | O(V) |
| Difficulty | 3/5 | 2/5 | 3/5 |
| When | Wide | Default | Stack-safe |
| PE Verdict | DFS recursive with memo. | ||
Python Solutions
BFS O(V+E) O(V)
Same.
Pseudo-code
see Java
Complexity
Python code
from collections import deque
class Node:
def __init__(self, val=0, neighbors=None): self.val=val; self.neighbors = neighbors or []
class Solution:
def cloneGraph(self, node):
if not node: return None
m = {node: Node(node.val)}
q = deque([node])
while q:
n = q.popleft()
for nb in n.neighbors:
if nb not in m: m[nb] = Node(nb.val); q.append(nb)
m[n].neighbors.append(m[nb])
return m[node]
DFS O(V+E) O(V)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def cloneGraph(self, node):
m = {}
def dfs(n):
if not n: return None
if n in m: return m[n]
copy = Node(n.val); m[n] = copy
for nb in n.neighbors: copy.neighbors.append(dfs(nb))
return copy
return dfs(node)
Iterative DFS O(V+E) O(V)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def cloneGraph(self, node):
if not node: return None
m = {node: Node(node.val)}
s = [node]
while s:
n = s.pop()
for nb in n.neighbors:
if nb not in m: m[nb] = Node(nb.val); s.append(nb)
m[n].neighbors.append(m[nb])
return m[node]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V+E) | O(V+E) | O(V+E) |
| Space | O(V) | O(V) | O(V) |
| Difficulty | 3/5 | 2/5 | 3/5 |
| When | Wide | Default | Stack-safe |
| PE Verdict | DFS with memo. | ||
What the interviewer is really testing
Graph deep-copy template.
Top gotchas
- Memo BEFORE recursing into neighbors — avoids infinite loops on cycles.
- Null root returns null.
Ship-it
DFS with memo.
#83 LC 286 Medium Walls and Gates
Multi-source BFS from all gates simultaneously.
Problem Statement
Fill each empty room (INF=2147483647) with the distance to its nearest gate (0). Walls = -1.
Examples
Constraints
- m, n <= 250
Approach Overview
Brute Force
Java: BFS from each empty room
Python: BFS per cell
Optimal #1
Java: Multi-source BFS from all gates
Python: Multi-source BFS
Optimal #2
Java: DFS from each gate
Python: DFS
Java Solutions
BFS from each empty room O((m·n)²) O(m·n)
O((m·n)²).
Pseudo-code
for each INF cell: BFS to nearest gate
Complexity
Java code
import java.util.*;
class Solution {
public void wallsAndGates(int[][] r) {
int m = r.length, n = r[0].length;
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) {
if (r[i][j] != Integer.MAX_VALUE) continue;
Deque<int[]> q = new ArrayDeque<>(); q.offer(new int[]{i,j,0});
boolean[][] v = new boolean[m][n]; v[i][j] = true;
while (!q.isEmpty()) {
int[] p = q.poll();
if (r[p[0]][p[1]] == 0) { r[i][j] = p[2]; break; }
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&!v[ni][nj]&&r[ni][nj]!=-1) { v[ni][nj]=true; q.offer(new int[]{ni,nj,p[2]+1}); }
}
}
}
}
}
Multi-source BFS from all gates O(m·n) O(m·n)
Push all gates first; expand simultaneously.
Pseudo-code
seed queue with all gates; BFS; first visit = shortest
Complexity
Java code
import java.util.*;
class Solution {
public void wallsAndGates(int[][] r) {
int m = r.length, n = r[0].length;
Deque<int[]> q = new ArrayDeque<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (r[i][j] == 0) q.offer(new int[]{i,j});
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
while (!q.isEmpty()) {
int[] p = q.poll();
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&r[ni][nj] == Integer.MAX_VALUE) {
r[ni][nj] = r[p[0]][p[1]] + 1;
q.offer(new int[]{ni,nj});
}
}
}
}
}
DFS from each gate O(m·n) avg O(m·n)
DFS but only descend if new distance < current.
Pseudo-code
DFS from gate, relax if better
Complexity
Java code
class Solution {
public void wallsAndGates(int[][] r) {
for (int i = 0; i < r.length; i++) for (int j = 0; j < r[0].length; j++)
if (r[i][j] == 0) dfs(r, i, j, 0);
}
private void dfs(int[][] r, int i, int j, int d) {
if (i<0||j<0||i>=r.length||j>=r[0].length||r[i][j] < d) return;
r[i][j] = d;
dfs(r,i+1,j,d+1); dfs(r,i-1,j,d+1); dfs(r,i,j+1,d+1); dfs(r,i,j-1,d+1);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn) avg |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | DFS preference |
| PE Verdict | Multi-source BFS. | ||
Python Solutions
BFS per cell O((mn)²) O(mn)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def wallsAndGates(self, r: List[List[int]]) -> None:
m, n = len(r), len(r[0])
for i in range(m):
for j in range(n):
if r[i][j] != 2147483647: continue
q = deque([(i,j,0)]); v = {(i,j)}
while q:
x,y,d = q.popleft()
if r[x][y] == 0: r[i][j] = d; break
for dx,dy in [(-1,0),(1,0),(0,-1),(0,1)]:
nx,ny = x+dx,y+dy
if 0<=nx<m and 0<=ny<n and (nx,ny) not in v and r[nx][ny] != -1:
v.add((nx,ny)); q.append((nx,ny,d+1))
Multi-source BFS O(mn) O(mn)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def wallsAndGates(self, r: List[List[int]]) -> None:
m, n = len(r), len(r[0])
q = deque((i,j) for i in range(m) for j in range(n) if r[i][j] == 0)
while q:
x,y = q.popleft()
for dx,dy in [(-1,0),(1,0),(0,-1),(0,1)]:
nx,ny = x+dx,y+dy
if 0<=nx<m and 0<=ny<n and r[nx][ny] == 2147483647:
r[nx][ny] = r[x][y] + 1; q.append((nx,ny))
DFS O(mn) avg O(mn)
Same as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def wallsAndGates(self, r: List[List[int]]) -> None:
m, n = len(r), len(r[0])
def dfs(i,j,d):
if i<0 or j<0 or i>=m or j>=n or r[i][j] < d: return
r[i][j] = d
dfs(i+1,j,d+1); dfs(i-1,j,d+1); dfs(i,j+1,d+1); dfs(i,j-1,d+1)
for i in range(m):
for j in range(n):
if r[i][j] == 0: dfs(i,j,0)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn) avg |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | DFS preference |
| PE Verdict | Multi-source BFS. | ||
What the interviewer is really testing
Multi-source BFS pattern (also Rotting Oranges).
Top gotchas
- Seed queue with ALL gates first.
- First visit IS shortest because BFS expands layer by layer.
- Skip walls (-1).
Ship-it
Multi-source BFS.
#84 LC 994 Medium Rotting Oranges
Multi-source BFS — all rotten oranges expand simultaneously, track minutes.
Problem Statement
Grid with 0 empty, 1 fresh, 2 rotten. Each minute a rotten orange rots all 4-adjacent fresh ones. Return minimum minutes until none fresh, or -1 if impossible.
Examples
Constraints
- 1 <= m, n <= 10
Approach Overview
Brute Force
Java: Iterative simulation
Python: Iterative simulation
Optimal #1
Java: Multi-source BFS
Python: Multi-source BFS
Optimal #2
Java: BFS with timestamp in cell
Python: BFS with timestamp
Java Solutions
Iterative simulation O((m·n)²) O(m·n)
Each minute, scan grid and rot.
Pseudo-code
loop: copy grid; rot adjacent; until no change; count minutes
Complexity
Edge cases & gotchas
- No fresh → 0.
Java code
class Solution {
public int orangesRotting(int[][] g) {
int m = g.length, n = g[0].length, t = 0;
while (true) {
int[][] c = new int[m][n];
for (int i = 0; i < m; i++) c[i] = g[i].clone();
boolean changed = false;
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (c[i][j] == 2) {
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&c[ni][nj]==1) { g[ni][nj]=2; changed=true; }
}
}
if (!changed) break;
t++;
}
for (int[] row : g) for (int v : row) if (v == 1) return -1;
return t;
}
}
Multi-source BFS O(m·n) O(m·n)
Seed queue with all rotten; expand layer by layer.
Pseudo-code
seed all 2s; count fresh; BFS minute-by-minute
Complexity
Edge cases & gotchas
- fresh=0 → 0.
- fresh>0 after BFS → -1.
Java code
import java.util.*;
class Solution {
public int orangesRotting(int[][] g) {
int m = g.length, n = g[0].length, fresh = 0, t = 0;
Deque<int[]> q = new ArrayDeque<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) {
if (g[i][j] == 2) q.offer(new int[]{i,j});
else if (g[i][j] == 1) fresh++;
}
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
while (!q.isEmpty() && fresh > 0) {
int sz = q.size();
for (int k = 0; k < sz; k++) {
int[] p = q.poll();
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&g[ni][nj]==1) {
g[ni][nj] = 2; fresh--; q.offer(new int[]{ni,nj});
}
}
}
t++;
}
return fresh == 0 ? t : -1;
}
}
BFS with timestamp in cell O(m·n) O(m·n)
Encode minute in queue tuple.
Pseudo-code
queue stores (i,j,minute); max minute = answer
Complexity
Java code
import java.util.*;
class Solution {
public int orangesRotting(int[][] g) {
int m = g.length, n = g[0].length, fresh = 0, t = 0;
Deque<int[]> q = new ArrayDeque<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) {
if (g[i][j] == 2) q.offer(new int[]{i,j,0});
else if (g[i][j] == 1) fresh++;
}
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
while (!q.isEmpty()) {
int[] p = q.poll(); t = Math.max(t, p[2]);
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&g[ni][nj]==1) {
g[ni][nj] = 2; fresh--; q.offer(new int[]{ni,nj,p[2]+1});
}
}
}
return fresh == 0 ? t : -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn) |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Multi-source BFS, level-by-level. | ||
Python Solutions
Iterative simulation O((mn)²) O(mn)
Scan and rot each minute.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def orangesRotting(self, g: List[List[int]]) -> int:
m, n = len(g), len(g[0]); t = 0
while True:
changed = False
to_rot = []
for i in range(m):
for j in range(n):
if g[i][j] != 2: continue
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = i+dr, j+dc
if 0<=nr<m and 0<=nc<n and g[nr][nc] == 1: to_rot.append((nr,nc))
for r, c in to_rot:
if g[r][c] == 1: g[r][c] = 2; changed = True
if not changed: break
t += 1
for row in g:
if 1 in row: return -1
return t
Multi-source BFS O(mn) O(mn)
Layer expansion.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def orangesRotting(self, g: List[List[int]]) -> int:
m, n = len(g), len(g[0])
q = deque(); fresh = 0
for i in range(m):
for j in range(n):
if g[i][j] == 2: q.append((i,j))
elif g[i][j] == 1: fresh += 1
t = 0
while q and fresh > 0:
for _ in range(len(q)):
r, c = q.popleft()
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if 0<=nr<m and 0<=nc<n and g[nr][nc] == 1:
g[nr][nc] = 2; fresh -= 1; q.append((nr,nc))
t += 1
return t if fresh == 0 else -1
BFS with timestamp O(mn) O(mn)
Tuple carries minute.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def orangesRotting(self, g: List[List[int]]) -> int:
m, n = len(g), len(g[0])
q = deque(); fresh = 0
for i in range(m):
for j in range(n):
if g[i][j] == 2: q.append((i,j,0))
elif g[i][j] == 1: fresh += 1
t = 0
while q:
r, c, m_ = q.popleft(); t = max(t, m_)
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if 0<=nr<m and 0<=nc<n and g[nr][nc] == 1:
g[nr][nc] = 2; fresh -= 1; q.append((nr,nc,m_+1))
return t if fresh == 0 else -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn) |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Multi-source BFS. | ||
What the interviewer is really testing
Same pattern as Walls & Gates — multi-source BFS.
Top gotchas
- Count fresh FIRST so you can detect -1 (unreachable fresh).
- Stop incrementing minute when queue empty OR fresh hits 0.
- No fresh up-front → return 0, not -1.
Ship-it
Multi-source BFS level-by-level.
#85 LC 417 Medium Pacific Atlantic Water Flow
Two reverse BFS/DFS from each ocean's borders — intersection is the answer.
Problem Statement
Heights grid. Water flows from cell to lower/equal neighbor. Pacific touches top/left, Atlantic touches bottom/right. Return cells from which water can reach BOTH oceans.
Examples
Constraints
- 1 <= m, n <= 200
Approach Overview
Brute Force
Java: DFS from each cell to both oceans
Python: Per-cell DFS to each ocean
Optimal #1
Java: Reverse DFS from each ocean
Python: Reverse DFS from each ocean
Optimal #2
Java: Reverse BFS from each ocean
Python: Reverse BFS
Java Solutions
DFS from each cell to both oceans O((m·n)²) O(m·n)
For each cell try to flow downhill to each ocean.
Pseudo-code
for each cell: can reach pacific && can reach atlantic?
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> pacificAtlantic(int[][] h) {
int m = h.length, n = h[0].length;
List<List<Integer>> out = new ArrayList<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
if (reach(h, i, j, true, new boolean[m][n]) && reach(h, i, j, false, new boolean[m][n]))
out.add(Arrays.asList(i, j));
return out;
}
private boolean reach(int[][] h, int i, int j, boolean pac, boolean[][] v) {
int m = h.length, n = h[0].length;
if (pac && (i == 0 || j == 0)) return true;
if (!pac && (i == m-1 || j == n-1)) return true;
v[i][j] = true;
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&!v[ni][nj]&&h[ni][nj] <= h[i][j])
if (reach(h, ni, nj, pac, v)) return true;
}
return false;
}
}
Reverse DFS from each ocean O(m·n) O(m·n)
Climb UP from ocean borders; intersect.
Pseudo-code
DFS from top/left filling 'pac'; DFS from bot/right filling 'atl'; cells in both
Complexity
Edge cases & gotchas
- 1×1 grid → that cell.
Java code
import java.util.*;
class Solution {
public List<List<Integer>> pacificAtlantic(int[][] h) {
int m = h.length, n = h[0].length;
boolean[][] pac = new boolean[m][n], atl = new boolean[m][n];
for (int i = 0; i < m; i++) { dfs(h, i, 0, pac); dfs(h, i, n-1, atl); }
for (int j = 0; j < n; j++) { dfs(h, 0, j, pac); dfs(h, m-1, j, atl); }
List<List<Integer>> out = new ArrayList<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
if (pac[i][j] && atl[i][j]) out.add(Arrays.asList(i, j));
return out;
}
private void dfs(int[][] h, int i, int j, boolean[][] v) {
v[i][j] = true;
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<h.length&&nj<h[0].length&&!v[ni][nj]&&h[ni][nj] >= h[i][j])
dfs(h, ni, nj, v);
}
}
}
Reverse BFS from each ocean O(m·n) O(m·n)
Same idea, BFS to avoid stack overflow.
Pseudo-code
BFS from each border instead of DFS
Complexity
Java code
import java.util.*;
class Solution {
public List<List<Integer>> pacificAtlantic(int[][] h) {
int m = h.length, n = h[0].length;
boolean[][] pac = new boolean[m][n], atl = new boolean[m][n];
Deque<int[]> pq = new ArrayDeque<>(), aq = new ArrayDeque<>();
for (int i = 0; i < m; i++) { pq.offer(new int[]{i,0}); pac[i][0]=true; aq.offer(new int[]{i,n-1}); atl[i][n-1]=true; }
for (int j = 0; j < n; j++) { pq.offer(new int[]{0,j}); pac[0][j]=true; aq.offer(new int[]{m-1,j}); atl[m-1][j]=true; }
bfs(h, pq, pac); bfs(h, aq, atl);
List<List<Integer>> out = new ArrayList<>();
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (pac[i][j] && atl[i][j]) out.add(Arrays.asList(i, j));
return out;
}
private void bfs(int[][] h, Deque<int[]> q, boolean[][] v) {
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
while (!q.isEmpty()) {
int[] p = q.poll();
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<h.length&&nj<h[0].length&&!v[ni][nj]&&h[ni][nj]>=h[p[0]][p[1]]) { v[ni][nj]=true; q.offer(new int[]{ni,nj}); }
}
}
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn) |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Wide grids |
| PE Verdict | Reverse DFS from each ocean's borders. | ||
Python Solutions
Per-cell DFS to each ocean O((mn)²) O(mn)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def pacificAtlantic(self, h: List[List[int]]) -> List[List[int]]:
m, n = len(h), len(h[0]); out = []
def reach(i, j, pac, v):
if pac and (i == 0 or j == 0): return True
if not pac and (i == m-1 or j == n-1): return True
v.add((i,j))
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = i+dr, j+dc
if 0<=nr<m and 0<=nc<n and (nr,nc) not in v and h[nr][nc] <= h[i][j]:
if reach(nr, nc, pac, v): return True
return False
for i in range(m):
for j in range(n):
if reach(i,j,True,set()) and reach(i,j,False,set()): out.append([i,j])
return out
Reverse DFS from each ocean O(mn) O(mn)
Climb up from borders.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def pacificAtlantic(self, h: List[List[int]]) -> List[List[int]]:
m, n = len(h), len(h[0])
pac, atl = set(), set()
def dfs(i, j, v):
v.add((i,j))
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = i+dr, j+dc
if 0<=nr<m and 0<=nc<n and (nr,nc) not in v and h[nr][nc] >= h[i][j]:
dfs(nr, nc, v)
for i in range(m): dfs(i, 0, pac); dfs(i, n-1, atl)
for j in range(n): dfs(0, j, pac); dfs(m-1, j, atl)
return [[i,j] for i,j in pac & atl]
Reverse BFS O(mn) O(mn)
Iterative alternative.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def pacificAtlantic(self, h: List[List[int]]) -> List[List[int]]:
m, n = len(h), len(h[0])
pac, atl = set(), set()
def bfs(starts, v):
q = deque(starts)
for s in starts: v.add(s)
while q:
r, c = q.popleft()
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if 0<=nr<m and 0<=nc<n and (nr,nc) not in v and h[nr][nc] >= h[r][c]:
v.add((nr,nc)); q.append((nr,nc))
bfs([(i,0) for i in range(m)] + [(0,j) for j in range(n)], pac)
bfs([(i,n-1) for i in range(m)] + [(m-1,j) for j in range(n)], atl)
return [[i,j] for i,j in pac & atl]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn) |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Wide |
| PE Verdict | Reverse DFS from each ocean. | ||
What the interviewer is really testing
Classic reverse-search trick — flip direction so you go from the answer back to the source.
Top gotchas
- Water flows DOWN; reverse search climbs UP (>= not <=).
- Run two independent searches; intersect.
- Borders are seeds, not single cell.
Ship-it
Reverse DFS from each ocean.
#86 LC 130 Medium Surrounded Regions
Border DFS — mark border-connected 'O's safe, then flip the rest.
Problem Statement
Capture all 'O' regions surrounded by 'X'. A region is captured if no 'O' in it touches the border.
Examples
Constraints
- m, n <= 200
Approach Overview
Brute Force
Java: For each 'O' region, BFS check border
Python: Per-region BFS
Optimal #1
Java: Border DFS — mark safe
Python: Border DFS
Optimal #2
Java: Union-Find with virtual border node
Python: Union-Find + sentinel
Java Solutions
For each 'O' region, BFS check border O((m·n)²) O(m·n)
For each connected component, decide if touches border.
Pseudo-code
for each 'O' not seen: BFS; if any cell on border, keep; else flip
Complexity
Java code
import java.util.*;
class Solution {
public void solve(char[][] b) {
int m = b.length, n = b[0].length;
boolean[][] seen = new boolean[m][n];
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) {
if (b[i][j] != 'O' || seen[i][j]) continue;
List<int[]> comp = new ArrayList<>();
Deque<int[]> q = new ArrayDeque<>(); q.offer(new int[]{i,j}); seen[i][j] = true;
boolean border = false;
while (!q.isEmpty()) {
int[] p = q.poll(); comp.add(p);
if (p[0]==0||p[1]==0||p[0]==m-1||p[1]==n-1) border = true;
for (int[] d : new int[][]{{-1,0},{1,0},{0,-1},{0,1}}) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<m&&nj<n&&b[ni][nj]=='O'&&!seen[ni][nj]) { seen[ni][nj]=true; q.offer(new int[]{ni,nj}); }
}
}
if (!border) for (int[] p : comp) b[p[0]][p[1]] = 'X';
}
}
}
Border DFS — mark safe O(m·n) O(m·n)
DFS from each border 'O', mark with '#'; flip remaining 'O'→'X'; restore '#'→'O'.
Pseudo-code
DFS border Os → '#'; pass: O→X, #→O
Complexity
Edge cases & gotchas
- Empty grid.
- All 'X'.
Java code
class Solution {
public void solve(char[][] b) {
int m = b.length, n = b[0].length;
for (int i = 0; i < m; i++) { dfs(b, i, 0); dfs(b, i, n-1); }
for (int j = 0; j < n; j++) { dfs(b, 0, j); dfs(b, m-1, j); }
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
b[i][j] = b[i][j] == '#' ? 'O' : 'X';
}
private void dfs(char[][] b, int i, int j) {
if (i<0||j<0||i>=b.length||j>=b[0].length||b[i][j] != 'O') return;
b[i][j] = '#';
dfs(b, i+1, j); dfs(b, i-1, j); dfs(b, i, j+1); dfs(b, i, j-1);
}
}
Union-Find with virtual border node O(m·n·α) O(m·n)
Union border Os with sentinel; flip non-connected.
Pseudo-code
unite border 'O's with sentinel; cells whose root != sentinel → 'X'
Complexity
Java code
class Solution {
private int[] p;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private void union(int a, int b) { int ra = find(a), rb = find(b); if (ra != rb) p[ra] = rb; }
public void solve(char[][] b) {
int m = b.length, n = b[0].length, S = m*n;
p = new int[m*n + 1];
for (int i = 0; i <= m*n; i++) p[i] = i;
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (b[i][j] == 'O') {
if (i==0||j==0||i==m-1||j==n-1) union(i*n+j, S);
if (i+1<m && b[i+1][j]=='O') union(i*n+j, (i+1)*n+j);
if (j+1<n && b[i][j+1]=='O') union(i*n+j, i*n+j+1);
}
int rs = find(S);
for (int i = 0; i < m; i++) for (int j = 0; j < n; j++)
if (b[i][j] == 'O' && find(i*n+j) != rs) b[i][j] = 'X';
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn·α) |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Online |
| PE Verdict | Border DFS with sentinel marker. | ||
Python Solutions
Per-region BFS O((mn)²) O(mn)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def solve(self, b: List[List[str]]) -> None:
m, n = len(b), len(b[0]); seen = [[False]*n for _ in range(m)]
for i in range(m):
for j in range(n):
if b[i][j] != 'O' or seen[i][j]: continue
comp = []; q = deque([(i,j)]); seen[i][j] = True; border = False
while q:
r, c = q.popleft(); comp.append((r,c))
if r in (0,m-1) or c in (0,n-1): border = True
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = r+dr, c+dc
if 0<=nr<m and 0<=nc<n and b[nr][nc] == 'O' and not seen[nr][nc]:
seen[nr][nc] = True; q.append((nr,nc))
if not border:
for r, c in comp: b[r][c] = 'X'
Border DFS O(mn) O(mn)
Mark safe '#' then flip.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def solve(self, b: List[List[str]]) -> None:
m, n = len(b), len(b[0])
def dfs(i, j):
if i<0 or j<0 or i>=m or j>=n or b[i][j] != 'O': return
b[i][j] = '#'
dfs(i+1,j); dfs(i-1,j); dfs(i,j+1); dfs(i,j-1)
for i in range(m): dfs(i,0); dfs(i,n-1)
for j in range(n): dfs(0,j); dfs(m-1,j)
for i in range(m):
for j in range(n):
b[i][j] = 'O' if b[i][j] == '#' else 'X'
Union-Find + sentinel O(mn·α) O(mn)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def solve(self, b: List[List[str]]) -> None:
m, n = len(b), len(b[0]); S = m*n
p = list(range(m*n + 1))
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, c):
ra, rb = find(a), find(c)
if ra != rb: p[ra] = rb
for i in range(m):
for j in range(n):
if b[i][j] != 'O': continue
if i in (0,m-1) or j in (0,n-1): union(i*n+j, S)
if i+1<m and b[i+1][j] == 'O': union(i*n+j, (i+1)*n+j)
if j+1<n and b[i][j+1] == 'O': union(i*n+j, i*n+j+1)
rs = find(S)
for i in range(m):
for j in range(n):
if b[i][j] == 'O' and find(i*n+j) != rs: b[i][j] = 'X'
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O((mn)²) | O(mn) | O(mn·α) |
| Space | O(mn) | O(mn) | O(mn) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Online |
| PE Verdict | Border DFS marker trick. | ||
What the interviewer is really testing
Reverse-search/sentinel pattern.
Top gotchas
- Mark with a 3rd char ('#') so you can distinguish safe O's from to-flip O's.
- Border DFS first, then sweep.
- Union-Find variant uses a virtual border node — cleaner mental model.
Ship-it
Border DFS + flip.
#87 LC 207 Medium Course Schedule
Cycle detection in directed graph — Kahn's BFS (topo) or DFS 3-color.
Problem Statement
n courses, prerequisites [[a,b]] meaning b→a. Can you finish all? (i.e., is the graph a DAG?)
Examples
Constraints
- 1 <= n <= 2000
- 0 <= edges <= 5000
Approach Overview
Brute Force
Java: DFS with visited set per start
Python: Per-node DFS
Optimal #1
Java: Kahn's BFS (topo sort)
Python: Kahn's BFS
Optimal #2
Java: DFS 3-color cycle detection
Python: DFS 3-color
Java Solutions
DFS with visited set per start O(V·(V+E)) O(V+E)
For each course, DFS to detect cycle from it.
Pseudo-code
for each course: DFS; if revisit a node on current path → cycle
Complexity
Java code
import java.util.*;
class Solution {
public boolean canFinish(int n, int[][] pre) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : pre) g.get(e[1]).add(e[0]);
for (int i = 0; i < n; i++) {
boolean[] onPath = new boolean[n], seen = new boolean[n];
if (hasCycle(g, i, onPath, seen)) return false;
}
return true;
}
private boolean hasCycle(List<List<Integer>> g, int u, boolean[] onPath, boolean[] seen) {
if (onPath[u]) return true;
if (seen[u]) return false;
onPath[u] = true; seen[u] = true;
for (int v : g.get(u)) if (hasCycle(g, v, onPath, seen)) return true;
onPath[u] = false; return false;
}
}
Kahn's BFS (topo sort) O(V+E) O(V+E)
Pop indegree=0 nodes; if processed all → DAG.
Pseudo-code
compute indegrees; enqueue 0s; for each pop, decrement neighbors; count == n?
Complexity
Edge cases & gotchas
- No prereqs → true.
Java code
import java.util.*;
class Solution {
public boolean canFinish(int n, int[][] pre) {
List<List<Integer>> g = new ArrayList<>();
int[] indeg = new int[n];
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : pre) { g.get(e[1]).add(e[0]); indeg[e[0]]++; }
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
int taken = 0;
while (!q.isEmpty()) {
int u = q.poll(); taken++;
for (int v : g.get(u)) if (--indeg[v] == 0) q.offer(v);
}
return taken == n;
}
}
DFS 3-color cycle detection O(V+E) O(V+E)
WHITE=0, GRAY=1 (on path), BLACK=2 (done).
Pseudo-code
dfs(u): mark GRAY; for v: if GRAY → cycle; if WHITE → dfs(v); mark BLACK
Complexity
Java code
import java.util.*;
class Solution {
public boolean canFinish(int n, int[][] pre) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : pre) g.get(e[1]).add(e[0]);
int[] color = new int[n];
for (int i = 0; i < n; i++) if (color[i] == 0 && hasCycle(g, i, color)) return false;
return true;
}
private boolean hasCycle(List<List<Integer>> g, int u, int[] color) {
color[u] = 1;
for (int v : g.get(u)) {
if (color[v] == 1) return true;
if (color[v] == 0 && hasCycle(g, v, color)) return true;
}
color[u] = 2; return false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V(V+E)) | O(V+E) | O(V+E) |
| Space | O(V+E) | O(V+E) | O(V+E) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default — also gives order | Strict cycle test |
| PE Verdict | Kahn's BFS — clean and reusable for ordering. | ||
Python Solutions
Per-node DFS O(V(V+E)) O(V+E)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canFinish(self, n: int, pre: List[List[int]]) -> bool:
g = [[] for _ in range(n)]
for a, b in pre: g[b].append(a)
def cycle(u, on_path, seen):
if u in on_path: return True
if u in seen: return False
on_path.add(u); seen.add(u)
for v in g[u]:
if cycle(v, on_path, seen): return True
on_path.remove(u); return False
for i in range(n):
if cycle(i, set(), set()): return False
return True
Kahn's BFS O(V+E) O(V+E)
Topo order.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque, defaultdict
class Solution:
def canFinish(self, n: int, pre: List[List[int]]) -> bool:
g = defaultdict(list); indeg = [0]*n
for a, b in pre: g[b].append(a); indeg[a] += 1
q = deque(i for i in range(n) if indeg[i] == 0)
taken = 0
while q:
u = q.popleft(); taken += 1
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
return taken == n
DFS 3-color O(V+E) O(V+E)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canFinish(self, n: int, pre: List[List[int]]) -> bool:
g = [[] for _ in range(n)]
for a, b in pre: g[b].append(a)
color = [0]*n # 0=white,1=gray,2=black
def cycle(u):
color[u] = 1
for v in g[u]:
if color[v] == 1: return True
if color[v] == 0 and cycle(v): return True
color[u] = 2; return False
return not any(color[i] == 0 and cycle(i) for i in range(n))
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V(V+E)) | O(V+E) | O(V+E) |
| Space | O(V+E) | O(V+E) | O(V+E) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Strict cycle |
| PE Verdict | Kahn's BFS. | ||
What the interviewer is really testing
Topo sort canon.
Top gotchas
- Build edge direction carefully — prereq[i]=[a,b] usually means take b before a (b→a).
- Kahn's also gives a valid ordering for free — useful for Course Schedule II.
- Track 'taken count' to detect cycle without separate scan.
Ship-it
Kahn's topological sort.
#88 LC 210 Medium Course Schedule II
Return a valid topological order; empty array if cycle.
Problem Statement
Same setup as Course Schedule, but return any valid order to take all courses; [] if impossible.
Examples
Constraints
- 1 <= n <= 2000
Approach Overview
Brute Force
Java: Naive: pick any 0-indegree repeatedly
Python: Naive rescan
Optimal #1
Java: Kahn's BFS
Python: Kahn's BFS
Optimal #2
Java: DFS postorder (reverse)
Python: DFS postorder
Java Solutions
Naive: pick any 0-indegree repeatedly O(V·(V+E)) O(V+E)
Rescan each iteration.
Pseudo-code
loop n times: find unvisited node with indeg 0; pop; decrement neighbors
Complexity
Java code
import java.util.*;
class Solution {
public int[] findOrder(int n, int[][] pre) {
int[] indeg = new int[n];
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : pre) { g.get(e[1]).add(e[0]); indeg[e[0]]++; }
int[] out = new int[n]; boolean[] done = new boolean[n]; int k = 0;
for (int step = 0; step < n; step++) {
int u = -1;
for (int i = 0; i < n; i++) if (!done[i] && indeg[i] == 0) { u = i; break; }
if (u == -1) return new int[0];
done[u] = true; out[k++] = u;
for (int v : g.get(u)) indeg[v]--;
}
return out;
}
}
Kahn's BFS O(V+E) O(V+E)
Standard topo sort; emit in pop order.
Pseudo-code
build indegrees; enqueue 0s; emit; if count != n → []
Complexity
Edge cases & gotchas
- No prereqs → [0..n-1].
Java code
import java.util.*;
class Solution {
public int[] findOrder(int n, int[][] pre) {
int[] indeg = new int[n];
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : pre) { g.get(e[1]).add(e[0]); indeg[e[0]]++; }
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
int[] out = new int[n]; int k = 0;
while (!q.isEmpty()) {
int u = q.poll(); out[k++] = u;
for (int v : g.get(u)) if (--indeg[v] == 0) q.offer(v);
}
return k == n ? out : new int[0];
}
}
DFS postorder (reverse) O(V+E) O(V+E)
DFS finish time; reverse = topo.
Pseudo-code
dfs(u): mark GRAY; recurse; mark BLACK; push to stack. Pop stack → order.
Complexity
Java code
import java.util.*;
class Solution {
public int[] findOrder(int n, int[][] pre) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : pre) g.get(e[1]).add(e[0]);
int[] color = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) if (color[i] == 0 && !dfs(g, i, color, stack)) return new int[0];
int[] out = new int[n];
for (int k = 0; k < n; k++) out[k] = stack.pop();
return out;
}
private boolean dfs(List<List<Integer>> g, int u, int[] color, Deque<Integer> stack) {
color[u] = 1;
for (int v : g.get(u)) {
if (color[v] == 1) return false;
if (color[v] == 0 && !dfs(g, v, color, stack)) return false;
}
color[u] = 2; stack.push(u); return true;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V(V+E)) | O(V+E) | O(V+E) |
| Space | O(V+E) | O(V+E) | O(V+E) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Recursive style |
| PE Verdict | Kahn's BFS — direct topo emit. | ||
Python Solutions
Naive rescan O(V(V+E)) O(V+E)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findOrder(self, n: int, pre: List[List[int]]) -> List[int]:
from collections import defaultdict
g = defaultdict(list); indeg = [0]*n
for a, b in pre: g[b].append(a); indeg[a] += 1
out, done = [], [False]*n
for _ in range(n):
u = -1
for i in range(n):
if not done[i] and indeg[i] == 0: u = i; break
if u == -1: return []
done[u] = True; out.append(u)
for v in g[u]: indeg[v] -= 1
return out
Kahn's BFS O(V+E) O(V+E)
Topo emit.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque, defaultdict
class Solution:
def findOrder(self, n: int, pre: List[List[int]]) -> List[int]:
g = defaultdict(list); indeg = [0]*n
for a, b in pre: g[b].append(a); indeg[a] += 1
q = deque(i for i in range(n) if indeg[i] == 0)
out = []
while q:
u = q.popleft(); out.append(u)
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
return out if len(out) == n else []
DFS postorder O(V+E) O(V+E)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findOrder(self, n: int, pre: List[List[int]]) -> List[int]:
g = [[] for _ in range(n)]
for a, b in pre: g[b].append(a)
color = [0]*n; order = []
def dfs(u):
color[u] = 1
for v in g[u]:
if color[v] == 1: return False
if color[v] == 0 and not dfs(v): return False
color[u] = 2; order.append(u); return True
for i in range(n):
if color[i] == 0 and not dfs(i): return []
return order[::-1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V(V+E)) | O(V+E) | O(V+E) |
| Space | O(V+E) | O(V+E) | O(V+E) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Recursive |
| PE Verdict | Kahn's BFS. | ||
What the interviewer is really testing
Direct extension of Course Schedule I.
Top gotchas
- Output count < n ⇒ cycle ⇒ return [].
- DFS variant pushes on finish; remember to REVERSE.
- Indegree array can double as 'has been emitted' check.
Ship-it
Kahn's BFS, emit on dequeue.
#89 LC 261 Medium Graph Valid Tree
Tree ⇔ connected AND no cycle ⇔ Union-Find never reuses a root.
Problem Statement
n nodes (0..n-1) and edges. Return true if the graph forms a valid tree.
Examples
Constraints
- 1 <= n <= 2000
Approach Overview
Brute Force
Java: BFS connectivity + edge count check
Python: BFS + edge count
Optimal #1
Java: Union-Find
Python: Union-Find
Optimal #2
Java: DFS cycle detection
Python: DFS cycle check
Java Solutions
BFS connectivity + edge count check O(V+E) O(V+E)
Tree has exactly n-1 edges and is connected.
Pseudo-code
if edges != n-1 → false; else BFS from 0; visited == n?
Complexity
Edge cases & gotchas
- n=1, no edges → true.
Java code
import java.util.*;
class Solution {
public boolean validTree(int n, int[][] edges) {
if (edges.length != n - 1) return false;
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : edges) { g.get(e[0]).add(e[1]); g.get(e[1]).add(e[0]); }
boolean[] v = new boolean[n];
Deque<Integer> q = new ArrayDeque<>(); q.offer(0); v[0] = true;
int cnt = 0;
while (!q.isEmpty()) { int u = q.poll(); cnt++; for (int x : g.get(u)) if (!v[x]) { v[x] = true; q.offer(x); } }
return cnt == n;
}
}
Union-Find O(E·α) O(V)
Reject if union finds same root (cycle); end with 1 component.
Pseudo-code
DSU; for each edge: if same root → false; else union; final components == 1
Complexity
Edge cases & gotchas
- Self-loop → cycle.
Java code
class Solution {
private int[] p, sz;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) { p[ra] = rb; sz[rb] += sz[ra]; }
else { p[rb] = ra; sz[ra] += sz[rb]; }
return true;
}
public boolean validTree(int n, int[][] edges) {
if (edges.length != n - 1) return false;
p = new int[n]; sz = new int[n];
for (int i = 0; i < n; i++) { p[i] = i; sz[i] = 1; }
for (int[] e : edges) if (!union(e[0], e[1])) return false;
return true;
}
}
DFS cycle detection O(V+E) O(V+E)
Traverse from 0; if revisit non-parent → cycle.
Pseudo-code
dfs(u, parent): if u seen → cycle; mark; for v in adj: if v != parent: dfs
Complexity
Java code
import java.util.*;
class Solution {
public boolean validTree(int n, int[][] edges) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : edges) { g.get(e[0]).add(e[1]); g.get(e[1]).add(e[0]); }
boolean[] v = new boolean[n];
if (hasCycle(g, 0, -1, v)) return false;
for (boolean b : v) if (!b) return false;
return true;
}
private boolean hasCycle(List<List<Integer>> g, int u, int parent, boolean[] v) {
v[u] = true;
for (int x : g.get(u)) {
if (!v[x]) { if (hasCycle(g, x, u, v)) return true; }
else if (x != parent) return true;
}
return false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V+E) | O(E·α) | O(V+E) |
| Space | O(V+E) | O(V) | O(V+E) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Static | Default — concise | Recursive friendly |
| PE Verdict | Union-Find — natural fit for cycle test. | ||
Python Solutions
BFS + edge count O(V+E) O(V+E)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) != n - 1: return False
g = [[] for _ in range(n)]
for a, b in edges: g[a].append(b); g[b].append(a)
v = [False]*n
q = deque([0]); v[0] = True; cnt = 0
while q:
u = q.popleft(); cnt += 1
for x in g[u]:
if not v[x]: v[x] = True; q.append(x)
return cnt == n
Union-Find O(E·α) O(V)
Reject if union same root.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) != n - 1: return False
p = list(range(n)); sz = [1]*n
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return False
if sz[ra] < sz[rb]: p[ra] = rb; sz[rb] += sz[ra]
else: p[rb] = ra; sz[ra] += sz[rb]
return True
return all(union(a, b) for a, b in edges)
DFS cycle check O(V+E) O(V+E)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
g = [[] for _ in range(n)]
for a, b in edges: g[a].append(b); g[b].append(a)
v = [False]*n
def cycle(u, parent):
v[u] = True
for x in g[u]:
if not v[x]:
if cycle(x, u): return True
elif x != parent: return True
return False
if cycle(0, -1): return False
return all(v)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V+E) | O(E·α) | O(V+E) |
| Space | O(V+E) | O(V) | O(V+E) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Static | Default | Recursive |
| PE Verdict | Union-Find. | ||
What the interviewer is really testing
Tree definition: n-1 edges + connected + acyclic — any 2 ⇒ third.
Top gotchas
- Edges != n-1 is an instant short-circuit.
- Undirected DFS needs PARENT tracking to avoid trivial back-edge.
- Union-Find detects cycle and connectivity in one pass.
Ship-it
Union-Find.
#90 LC 323 Medium Number of Connected Components in an Undirected Graph
Union-Find or DFS — count distinct components.
Problem Statement
n nodes (0..n-1) and undirected edges. Return number of connected components.
Examples
Constraints
- 1 <= n <= 2000
Approach Overview
Brute Force
Java: DFS per node
Python: DFS
Optimal #1
Java: Union-Find
Python: Union-Find
Optimal #2
Java: BFS per node
Python: BFS
Java Solutions
DFS per node O(V+E) O(V+E)
For each unseen node, DFS; count starts.
Pseudo-code
build adj; for each unseen: DFS; cnt++
Complexity
Edge cases & gotchas
- n=0 → 0.
- No edges → n.
Java code
import java.util.*;
class Solution {
public int countComponents(int n, int[][] edges) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : edges) { g.get(e[0]).add(e[1]); g.get(e[1]).add(e[0]); }
boolean[] v = new boolean[n]; int cnt = 0;
for (int i = 0; i < n; i++) if (!v[i]) { cnt++; dfs(g, i, v); }
return cnt;
}
private void dfs(List<List<Integer>> g, int u, boolean[] v) {
v[u] = true;
for (int x : g.get(u)) if (!v[x]) dfs(g, x, v);
}
}
Union-Find O(E·α) O(V)
Start at n; decrement each successful union.
Pseudo-code
comps = n; for each edge: if union succeeds → comps--
Complexity
Java code
class Solution {
private int[] p, sz;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) { p[ra] = rb; sz[rb] += sz[ra]; }
else { p[rb] = ra; sz[ra] += sz[rb]; }
return true;
}
public int countComponents(int n, int[][] edges) {
p = new int[n]; sz = new int[n];
for (int i = 0; i < n; i++) { p[i] = i; sz[i] = 1; }
int comps = n;
for (int[] e : edges) if (union(e[0], e[1])) comps--;
return comps;
}
}
BFS per node O(V+E) O(V+E)
Iterative variant of brute.
Pseudo-code
for each unseen: BFS; cnt++
Complexity
Java code
import java.util.*;
class Solution {
public int countComponents(int n, int[][] edges) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : edges) { g.get(e[0]).add(e[1]); g.get(e[1]).add(e[0]); }
boolean[] v = new boolean[n]; int cnt = 0;
for (int i = 0; i < n; i++) {
if (v[i]) continue;
cnt++;
Deque<Integer> q = new ArrayDeque<>(); q.offer(i); v[i] = true;
while (!q.isEmpty()) { int u = q.poll(); for (int x : g.get(u)) if (!v[x]) { v[x] = true; q.offer(x); } }
}
return cnt;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V+E) | O(E·α) | O(V+E) |
| Space | O(V+E) | O(V) | O(V+E) |
| Difficulty | 2/5 | 3/5 | 2/5 |
| When | Static | Default — streamy | Wide |
| PE Verdict | Union-Find — single-pass component counting. | ||
Python Solutions
DFS O(V+E) O(V+E)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
g = [[] for _ in range(n)]
for a, b in edges: g[a].append(b); g[b].append(a)
v = [False]*n; cnt = 0
def dfs(u):
v[u] = True
for x in g[u]:
if not v[x]: dfs(x)
for i in range(n):
if not v[i]: cnt += 1; dfs(i)
return cnt
Union-Find O(E·α) O(V)
Decrement on union.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
p = list(range(n)); sz = [1]*n; comps = n
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return False
if sz[ra] < sz[rb]: p[ra] = rb; sz[rb] += sz[ra]
else: p[rb] = ra; sz[ra] += sz[rb]
return True
for a, b in edges:
if union(a, b): comps -= 1
return comps
BFS O(V+E) O(V+E)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
g = [[] for _ in range(n)]
for a, b in edges: g[a].append(b); g[b].append(a)
v = [False]*n; cnt = 0
for i in range(n):
if v[i]: continue
cnt += 1
q = deque([i]); v[i] = True
while q:
u = q.popleft()
for x in g[u]:
if not v[x]: v[x] = True; q.append(x)
return cnt
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V+E) | O(E·α) | O(V+E) |
| Space | O(V+E) | O(V) | O(V+E) |
| Difficulty | 2/5 | 3/5 | 2/5 |
| When | Static | Default | Wide |
| PE Verdict | Union-Find. | ||
What the interviewer is really testing
Classic DSU drill.
Top gotchas
- Start comps at n, then subtract on successful union.
- DFS/BFS variant works fine and is easier to write.
- Isolated nodes still count as components — Union-Find handles for free.
Ship-it
Union-Find with path compression.
#91 LC 684 Medium Redundant Connection
Union-Find — first edge whose endpoints already share a root is the answer.
Problem Statement
Tree with one extra edge added. Return that extra edge (the one appearing last in input that forms a cycle).
Examples
Constraints
- 3 <= n <= 1000
Approach Overview
Brute Force
Java: BFS/DFS after each edge
Python: BFS per edge
Optimal #1
Java: Union-Find
Python: Union-Find
Optimal #2
Java: DFS cycle finder
Python: DFS reverse-removal
Java Solutions
BFS/DFS after each edge O(n²) O(n)
Add edges one by one; after each, check if cycle forms.
Pseudo-code
for each edge: if endpoints already connected (BFS) → return; else add
Complexity
Java code
import java.util.*;
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i <= n; i++) g.add(new ArrayList<>());
for (int[] e : edges) {
if (connected(g, e[0], e[1])) return e;
g.get(e[0]).add(e[1]); g.get(e[1]).add(e[0]);
}
return new int[0];
}
private boolean connected(List<List<Integer>> g, int a, int b) {
Set<Integer> v = new HashSet<>(); v.add(a);
Deque<Integer> q = new ArrayDeque<>(); q.offer(a);
while (!q.isEmpty()) {
int u = q.poll();
if (u == b) return true;
for (int x : g.get(u)) if (v.add(x)) q.offer(x);
}
return false;
}
}
Union-Find O(n·α) O(n)
First edge whose union fails is the answer.
Pseudo-code
DSU on n+1; for each edge: if same root → return; else union
Complexity
Java code
class Solution {
private int[] p, sz;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) { p[ra] = rb; sz[rb] += sz[ra]; }
else { p[rb] = ra; sz[ra] += sz[rb]; }
return true;
}
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
p = new int[n + 1]; sz = new int[n + 1];
for (int i = 0; i <= n; i++) { p[i] = i; sz[i] = 1; }
for (int[] e : edges) if (!union(e[0], e[1])) return e;
return new int[0];
}
}
DFS cycle finder O(n²) O(n²)
Build full graph; DFS to find the cycle; return last input edge in cycle.
Pseudo-code
build adj; for each edge in reverse: check if removing it leaves connected tree
Complexity
Java code
import java.util.*;
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
for (int i = n - 1; i >= 0; i--) {
Map<Integer, List<Integer>> g = new HashMap<>();
for (int j = 0; j < n; j++) {
if (j == i) continue;
g.computeIfAbsent(edges[j][0], k -> new ArrayList<>()).add(edges[j][1]);
g.computeIfAbsent(edges[j][1], k -> new ArrayList<>()).add(edges[j][0]);
}
Set<Integer> v = new HashSet<>();
Deque<Integer> q = new ArrayDeque<>(); q.offer(1); v.add(1);
while (!q.isEmpty()) { int u = q.poll(); for (int x : g.getOrDefault(u, new ArrayList<>())) if (v.add(x)) q.offer(x); }
if (v.size() == n) return edges[i];
}
return new int[0];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n·α) | O(n²) |
| Space | O(n) | O(n) | O(n²) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Tiny | Default | Demonstrative |
| PE Verdict | Union-Find — short, fast, idiomatic. | ||
Python Solutions
BFS per edge O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict, deque
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
g = defaultdict(list)
def connected(a, b):
v = {a}; q = deque([a])
while q:
u = q.popleft()
if u == b: return True
for x in g[u]:
if x not in v: v.add(x); q.append(x)
return False
for a, b in edges:
if connected(a, b): return [a, b]
g[a].append(b); g[b].append(a)
return []
Union-Find O(n·α) O(n)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges); p = list(range(n+1)); sz = [1]*(n+1)
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return False
if sz[ra] < sz[rb]: p[ra] = rb; sz[rb] += sz[ra]
else: p[rb] = ra; sz[ra] += sz[rb]
return True
for a, b in edges:
if not union(a, b): return [a, b]
return []
DFS reverse-removal O(n²) O(n²)
Same idea as Java opt2.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict, deque
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
for i in range(n-1, -1, -1):
g = defaultdict(list)
for j in range(n):
if j == i: continue
g[edges[j][0]].append(edges[j][1]); g[edges[j][1]].append(edges[j][0])
v = {1}; q = deque([1])
while q:
u = q.popleft()
for x in g[u]:
if x not in v: v.add(x); q.append(x)
if len(v) == n: return edges[i]
return []
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n·α) | O(n²) |
| Space | O(n) | O(n) | O(n²) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Tiny | Default | Demonstrative |
| PE Verdict | Union-Find — first failing union. | ||
What the interviewer is really testing
Show DSU instinct.
Top gotchas
- The problem says return the edge appearing LAST in input that forms a cycle — Union-Find processes in order; the first failing union IS that edge.
- Allocate n+1 since nodes are 1-indexed.
- Path compression + union by size for full credit.
Ship-it
Union-Find.
#92 LC 127 Hard Word Ladder
BFS over words; neighbors = strings differing in exactly one letter.
Problem Statement
Shortest transformation sequence length from beginWord to endWord; each step must be in wordList and change exactly one letter.
Examples
Constraints
- 1 <= len(word) <= 10
- 1 <= wordList.length <= 5000
Approach Overview
Brute Force
Java: BFS, compare every pair
Python: BFS, compare all
Optimal #1
Java: BFS with wildcard pattern map
Python: Pattern-map BFS
Optimal #2
Java: Bidirectional BFS
Python: Bidirectional BFS
Java Solutions
BFS, compare every pair O(N²·L) O(N·L)
Each step scan whole list to find neighbors.
Pseudo-code
BFS; for current word: scan list, neighbor if Hamming==1
Complexity
Edge cases & gotchas
- end not in list → 0.
Java code
import java.util.*;
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> dict = new HashSet<>(wordList);
if (!dict.contains(endWord)) return 0;
Deque<String> q = new ArrayDeque<>(); q.offer(beginWord);
Set<String> visited = new HashSet<>(); visited.add(beginWord);
int steps = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int k = 0; k < sz; k++) {
String w = q.poll();
if (w.equals(endWord)) return steps;
for (String d : new ArrayList<>(dict)) {
if (!visited.contains(d) && diff1(w, d)) { visited.add(d); q.offer(d); }
}
}
steps++;
}
return 0;
}
private boolean diff1(String a, String b) {
if (a.length() != b.length()) return false;
int d = 0;
for (int i = 0; i < a.length(); i++) if (a.charAt(i) != b.charAt(i) && ++d > 1) return false;
return d == 1;
}
}
BFS with wildcard pattern map O(N·L²) O(N·L²)
Pre-build map 'h*t' → ['hot','hit',...]; O(L) neighbor lookup.
Pseudo-code
build pattern map; BFS; neighbors = map[pattern]
Complexity
Java code
import java.util.*;
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> dict = new HashSet<>(wordList);
if (!dict.contains(endWord)) return 0;
int L = beginWord.length();
Map<String, List<String>> pat = new HashMap<>();
for (String w : wordList) for (int i = 0; i < L; i++) {
String p = w.substring(0, i) + '*' + w.substring(i + 1);
pat.computeIfAbsent(p, k -> new ArrayList<>()).add(w);
}
Deque<String> q = new ArrayDeque<>(); q.offer(beginWord);
Set<String> seen = new HashSet<>(); seen.add(beginWord);
int steps = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int k = 0; k < sz; k++) {
String w = q.poll();
if (w.equals(endWord)) return steps;
for (int i = 0; i < L; i++) {
String p = w.substring(0, i) + '*' + w.substring(i + 1);
for (String nb : pat.getOrDefault(p, new ArrayList<>())) {
if (seen.add(nb)) q.offer(nb);
}
}
}
steps++;
}
return 0;
}
}
Bidirectional BFS O(N·L²) O(N·L²)
Expand from both ends; meet in middle.
Pseudo-code
two sets begin/end; expand smaller; if neighbor in other → done
Complexity
Java code
import java.util.*;
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> dict = new HashSet<>(wordList);
if (!dict.contains(endWord)) return 0;
Set<String> bs = new HashSet<>(), es = new HashSet<>(), seen = new HashSet<>();
bs.add(beginWord); es.add(endWord); seen.add(beginWord);
int steps = 1;
while (!bs.isEmpty() && !es.isEmpty()) {
if (bs.size() > es.size()) { Set<String> t = bs; bs = es; es = t; }
Set<String> next = new HashSet<>();
for (String w : bs) {
char[] arr = w.toCharArray();
for (int i = 0; i < arr.length; i++) {
char orig = arr[i];
for (char c = 'a'; c <= 'z'; c++) {
arr[i] = c;
String nb = new String(arr);
if (es.contains(nb)) return steps + 1;
if (dict.contains(nb) && seen.add(nb)) next.add(nb);
}
arr[i] = orig;
}
}
bs = next; steps++;
}
return 0;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(N²·L) | O(N·L²) | O(N·L²) |
| Space | O(N·L) | O(N·L²) | O(N·L²) |
| Difficulty | 2/5 | 4/5 | 5/5 |
| When | Don't | Default | Long words, big dict |
| PE Verdict | Pattern-map BFS; bidirectional for very large inputs. | ||
Python Solutions
BFS, compare all O(N²·L) O(N·L)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
d = set(wordList)
if endWord not in d: return 0
q = deque([(beginWord, 1)]); seen = {beginWord}
while q:
w, steps = q.popleft()
if w == endWord: return steps
for nb in list(d):
if nb not in seen and sum(a != b for a, b in zip(w, nb)) == 1:
seen.add(nb); q.append((nb, steps+1))
return 0
Pattern-map BFS O(N·L²) O(N·L²)
h*t → [hot, hit].
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque, defaultdict
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
d = set(wordList)
if endWord not in d: return 0
L = len(beginWord)
pat = defaultdict(list)
for w in wordList:
for i in range(L):
pat[w[:i] + '*' + w[i+1:]].append(w)
q = deque([(beginWord, 1)]); seen = {beginWord}
while q:
w, steps = q.popleft()
if w == endWord: return steps
for i in range(L):
key = w[:i] + '*' + w[i+1:]
for nb in pat[key]:
if nb not in seen:
seen.add(nb); q.append((nb, steps+1))
pat[key] = [] # prune to avoid revisits
return 0
Bidirectional BFS O(N·L²) O(N·L²)
Two frontiers meeting.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
d = set(wordList)
if endWord not in d: return 0
bs, es, seen = {beginWord}, {endWord}, {beginWord}
steps = 1
while bs and es:
if len(bs) > len(es): bs, es = es, bs
nxt = set()
for w in bs:
for i in range(len(w)):
for c in 'abcdefghijklmnopqrstuvwxyz':
nb = w[:i] + c + w[i+1:]
if nb in es: return steps + 1
if nb in d and nb not in seen:
seen.add(nb); nxt.add(nb)
bs = nxt; steps += 1
return 0
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(N²·L) | O(N·L²) | O(N·L²) |
| Space | O(N·L) | O(N·L²) | O(N·L²) |
| Difficulty | 2/5 | 4/5 | 5/5 |
| When | Don't | Default | Large |
| PE Verdict | Pattern-map BFS. | ||
What the interviewer is really testing
Shortest path on implicit graph — BFS is natural.
Top gotchas
- endWord MUST be in dict, else return 0 instantly.
- Pattern map is the trick for O(L²) per node instead of O(N·L).
- Bidirectional BFS roughly square-roots the work.
Ship-it
BFS with wildcard pattern map.
Advanced Graphs
#93 LC 743 Medium Network Delay Time
Single-source shortest path on weighted DAG — Dijkstra with min-heap.
Problem Statement
Directed weighted graph, n nodes. times[i]=[u,v,w]. Return min time for signal from k to reach ALL nodes (or -1).
Examples
Constraints
- 1 <= n <= 100
- 1 <= times.length <= 6000
Approach Overview
Brute Force
Java: Bellman-Ford
Python: Bellman-Ford
Optimal #1
Java: Dijkstra (min-heap)
Python: Dijkstra
Optimal #2
Java: SPFA (Bellman-Ford queue)
Python: SPFA
Java Solutions
Bellman-Ford O(V·E) O(V)
Relax all edges n-1 times.
Pseudo-code
dist[k]=0; for V-1 iters: for each edge: relax; max(dist)
Complexity
Edge cases & gotchas
- Unreachable → -1.
Java code
import java.util.*;
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;
for (int i = 0; i < n - 1; i++) {
for (int[] e : times) {
if (dist[e[0]] != Integer.MAX_VALUE && dist[e[0]] + e[2] < dist[e[1]])
dist[e[1]] = dist[e[0]] + e[2];
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) return -1;
ans = Math.max(ans, dist[i]);
}
return ans;
}
}
Dijkstra (min-heap) O((V+E)logV) O(V+E)
Pop smallest; relax neighbors.
Pseudo-code
heap.push((0,k)); while heap: pop min; for each neighbor relax & push
Complexity
Edge cases & gotchas
- k unreachable to some → -1.
Java code
import java.util.*;
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
List<List<int[]>> g = new ArrayList<>();
for (int i = 0; i <= n; i++) g.add(new ArrayList<>());
for (int[] t : times) g.get(t[0]).add(new int[]{t[1], t[2]});
int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{k, 0});
while (!pq.isEmpty()) {
int[] p = pq.poll();
int u = p[0], d = p[1];
if (d > dist[u]) continue;
for (int[] e : g.get(u)) {
int v = e[0], w = e[1];
if (d + w < dist[v]) { dist[v] = d + w; pq.offer(new int[]{v, dist[v]}); }
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) return -1;
ans = Math.max(ans, dist[i]);
}
return ans;
}
}
SPFA (Bellman-Ford queue) O(V·E) worst O(V)
Relax via queue; only re-enqueue if improved.
Pseudo-code
enqueue k; pop u; relax edges; enqueue improved v
Complexity
Java code
import java.util.*;
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
List<List<int[]>> g = new ArrayList<>();
for (int i = 0; i <= n; i++) g.add(new ArrayList<>());
for (int[] t : times) g.get(t[0]).add(new int[]{t[1], t[2]});
int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;
Deque<Integer> q = new ArrayDeque<>(); q.offer(k);
boolean[] inQ = new boolean[n + 1]; inQ[k] = true;
while (!q.isEmpty()) {
int u = q.poll(); inQ[u] = false;
for (int[] e : g.get(u)) {
int v = e[0], w = e[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (!inQ[v]) { q.offer(v); inQ[v] = true; }
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) return -1;
ans = Math.max(ans, dist[i]);
}
return ans;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V·E) | O((V+E)logV) | O(V·E) worst |
| Space | O(V) | O(V+E) | O(V) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Negative edges | Default — non-negative | Sparse + dense mix |
| PE Verdict | Dijkstra with min-heap. | ||
Python Solutions
Bellman-Ford O(V·E) O(V)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
INF = float('inf')
dist = [INF] * (n + 1); dist[k] = 0
for _ in range(n - 1):
for u, v, w in times:
if dist[u] + w < dist[v]: dist[v] = dist[u] + w
ans = max(dist[1:])
return ans if ans < INF else -1
Dijkstra O((V+E)logV) O(V+E)
heapq min-heap.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
from collections import defaultdict
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
g = defaultdict(list)
for u, v, w in times: g[u].append((v, w))
INF = float('inf')
dist = [INF] * (n + 1); dist[k] = 0
h = [(0, k)]
while h:
d, u = heapq.heappop(h)
if d > dist[u]: continue
for v, w in g[u]:
if d + w < dist[v]:
dist[v] = d + w; heapq.heappush(h, (dist[v], v))
ans = max(dist[1:])
return ans if ans < INF else -1
SPFA O(V·E) worst O(V)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict, deque
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
g = defaultdict(list)
for u, v, w in times: g[u].append((v, w))
INF = float('inf')
dist = [INF] * (n + 1); dist[k] = 0
q = deque([k]); inQ = [False] * (n + 1); inQ[k] = True
while q:
u = q.popleft(); inQ[u] = False
for v, w in g[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
if not inQ[v]: q.append(v); inQ[v] = True
ans = max(dist[1:])
return ans if ans < INF else -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V·E) | O((V+E)logV) | O(V·E) worst |
| Space | O(V) | O(V+E) | O(V) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Negative edges | Default | Mixed |
| PE Verdict | Dijkstra. | ||
What the interviewer is really testing
Classic Dijkstra single-source shortest path.
Top gotchas
- Answer is the MAX of all dist (last node to receive the signal).
- Any unreachable node → return -1.
- Don't relax stale heap entries — skip if d > dist[u].
Ship-it
Dijkstra with heap, skip stale.
#94 LC 332 Hard Reconstruct Itinerary
Eulerian path — Hierholzer's algorithm with lexicographic min-heap.
Problem Statement
Use all tickets exactly once, starting from JFK. Return itinerary with lexicographically smallest order on ties.
Examples
Constraints
- 1 <= tickets.length <= 300
Approach Overview
Brute Force
Java: Backtracking try-all
Python: Backtracking
Optimal #1
Java: Hierholzer's with min-heap
Python: Hierholzer's with min-heap
Optimal #2
Java: Iterative Hierholzer
Python: Iterative Hierholzer
Java Solutions
Backtracking try-all O(E!) O(E)
Sort tickets; try paths greedily, backtrack.
Pseudo-code
sort; from JFK try edges in order; if dead end, backtrack
Complexity
Java code
import java.util.*;
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
Map<String, List<String>> g = new HashMap<>();
for (List<String> t : tickets) g.computeIfAbsent(t.get(0), k -> new ArrayList<>()).add(t.get(1));
for (List<String> v : g.values()) Collections.sort(v);
List<String> path = new ArrayList<>(); path.add("JFK");
dfs("JFK", g, path, tickets.size() + 1);
return path;
}
private boolean dfs(String u, Map<String, List<String>> g, List<String> path, int target) {
if (path.size() == target) return true;
List<String> nbs = g.get(u);
if (nbs == null) return false;
for (int i = 0; i < nbs.size(); i++) {
String v = nbs.remove(i);
path.add(v);
if (dfs(v, g, path, target)) return true;
path.remove(path.size() - 1);
nbs.add(i, v);
}
return false;
}
}
Hierholzer's with min-heap O(E·logE) O(E)
DFS, append on stack-unwind; reverse at end.
Pseudo-code
min-heap of dests per node; dfs(u): while heap nonempty: pop; dfs(v); append u
Complexity
Edge cases & gotchas
- Disconnected (won't happen if valid).
Java code
import java.util.*;
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
Map<String, PriorityQueue<String>> g = new HashMap<>();
for (List<String> t : tickets) g.computeIfAbsent(t.get(0), k -> new PriorityQueue<>()).offer(t.get(1));
LinkedList<String> path = new LinkedList<>();
dfs("JFK", g, path);
return path;
}
private void dfs(String u, Map<String, PriorityQueue<String>> g, LinkedList<String> path) {
PriorityQueue<String> pq = g.get(u);
while (pq != null && !pq.isEmpty()) dfs(pq.poll(), g, path);
path.addFirst(u);
}
}
Iterative Hierholzer O(E·logE) O(E)
Stack-based version; same idea no recursion.
Pseudo-code
stack=[JFK]; while stack: peek u; if has out-edge: push smallest dest; else pop into result
Complexity
Java code
import java.util.*;
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
Map<String, PriorityQueue<String>> g = new HashMap<>();
for (List<String> t : tickets) g.computeIfAbsent(t.get(0), k -> new PriorityQueue<>()).offer(t.get(1));
LinkedList<String> path = new LinkedList<>();
Deque<String> stack = new ArrayDeque<>(); stack.push("JFK");
while (!stack.isEmpty()) {
String u = stack.peek();
PriorityQueue<String> pq = g.get(u);
if (pq == null || pq.isEmpty()) path.addFirst(stack.pop());
else stack.push(pq.poll());
}
return path;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(E!) | O(E·logE) | O(E·logE) |
| Space | O(E) | O(E) | O(E) |
| Difficulty | 3/5 | 4/5 | 4/5 |
| When | Don't | Default | Stack-safe |
| PE Verdict | Hierholzer's with min-heap. | ||
Python Solutions
Backtracking O(E!) O(E)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict
class Solution:
def findIterary_brute(self, tickets):
g = defaultdict(list)
for a, b in tickets: g[a].append(b)
for v in g.values(): v.sort()
target = len(tickets) + 1
path = ['JFK']
def dfs(u):
if len(path) == target: return True
for i in range(len(g[u])):
v = g[u].pop(i); path.append(v)
if dfs(v): return True
path.pop(); g[u].insert(i, v)
return False
dfs('JFK')
return path
def findItinerary(self, tickets):
return self.findIterary_brute(tickets)
Hierholzer's with min-heap O(E·logE) O(E)
DFS, prepend on unwind.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
from collections import defaultdict
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
g = defaultdict(list)
for a, b in tickets: heapq.heappush(g[a], b)
path = []
def dfs(u):
while g[u]: dfs(heapq.heappop(g[u]))
path.append(u)
dfs('JFK')
return path[::-1]
Iterative Hierholzer O(E·logE) O(E)
Stack version.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
from collections import defaultdict
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
g = defaultdict(list)
for a, b in tickets: heapq.heappush(g[a], b)
path = []; stack = ['JFK']
while stack:
u = stack[-1]
if g[u]: stack.append(heapq.heappop(g[u]))
else: path.append(stack.pop())
return path[::-1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(E!) | O(E·logE) | O(E·logE) |
| Space | O(E) | O(E) | O(E) |
| Difficulty | 3/5 | 4/5 | 4/5 |
| When | Don't | Default | Stack-safe |
| PE Verdict | Hierholzer's DFS. | ||
What the interviewer is really testing
Eulerian path with lex constraint.
Top gotchas
- Append to path on stack unwind (NOT pre-order) — this is the Hierholzer trick.
- Reverse at the end.
- Min-heap (or sorted list) keeps lexicographic order.
Ship-it
Hierholzer's DFS with min-heap.
#95 LC 1584 Medium Min Cost to Connect All Points
Minimum spanning tree on a complete graph — Prim's (heap) or Kruskal's (DSU).
Problem Statement
Given 2D points, edges are Manhattan distance. Return min cost to connect all points.
Examples
Constraints
- 1 <= n <= 1000
Approach Overview
Brute Force
Java: Prim's without heap
Python: Prim's O(V²)
Optimal #1
Java: Prim's with min-heap
Python: Prim's with heap
Optimal #2
Java: Kruskal's with DSU
Python: Kruskal's
Java Solutions
Prim's without heap O(V²) O(V)
O(V²) repeatedly pick min-key vertex.
Pseudo-code
key[0]=0; loop V: pick min unvisited; mark; relax all neighbors
Complexity
Edge cases & gotchas
- n=1 → 0.
Java code
class Solution {
public int minCostConnectPoints(int[][] p) {
int n = p.length;
int[] key = new int[n];
boolean[] in = new boolean[n];
java.util.Arrays.fill(key, Integer.MAX_VALUE);
key[0] = 0; int total = 0;
for (int i = 0; i < n; i++) {
int u = -1, best = Integer.MAX_VALUE;
for (int j = 0; j < n; j++) if (!in[j] && key[j] < best) { best = key[j]; u = j; }
in[u] = true; total += best;
for (int v = 0; v < n; v++) if (!in[v]) {
int d = Math.abs(p[u][0]-p[v][0]) + Math.abs(p[u][1]-p[v][1]);
if (d < key[v]) key[v] = d;
}
}
return total;
}
}
Prim's with min-heap O(V²·logV) O(V²)
Pop lightest cross edge; expand frontier.
Pseudo-code
heap.push((0,0)); while not all visited: pop (w,u); if seen skip; total+=w; push edges to unseen
Complexity
Java code
import java.util.*;
class Solution {
public int minCostConnectPoints(int[][] p) {
int n = p.length;
boolean[] in = new boolean[n];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, 0});
int total = 0, taken = 0;
while (taken < n) {
int[] cur = pq.poll();
int w = cur[0], u = cur[1];
if (in[u]) continue;
in[u] = true; total += w; taken++;
for (int v = 0; v < n; v++) if (!in[v]) {
int d = Math.abs(p[u][0]-p[v][0]) + Math.abs(p[u][1]-p[v][1]);
pq.offer(new int[]{d, v});
}
}
return total;
}
}
Kruskal's with DSU O(V²·logV) O(V²)
Sort all V² edges; union; stop at V-1 edges.
Pseudo-code
build all edges; sort; for each: if union: total+=w
Complexity
Java code
import java.util.*;
class Solution {
private int[] par, sz;
private int find(int x) { while (par[x] != x) { par[x] = par[par[x]]; x = par[x]; } return x; }
private boolean union(int a, int b) {
int ra = find(a), rb = find(b); if (ra == rb) return false;
if (sz[ra] < sz[rb]) { par[ra] = rb; sz[rb] += sz[ra]; } else { par[rb] = ra; sz[ra] += sz[rb]; }
return true;
}
public int minCostConnectPoints(int[][] p) {
int n = p.length;
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++)
edges.add(new int[]{Math.abs(p[i][0]-p[j][0]) + Math.abs(p[i][1]-p[j][1]), i, j});
edges.sort((a, b) -> a[0] - b[0]);
par = new int[n]; sz = new int[n];
for (int i = 0; i < n; i++) { par[i] = i; sz[i] = 1; }
int total = 0, taken = 0;
for (int[] e : edges) {
if (taken == n - 1) break;
if (union(e[1], e[2])) { total += e[0]; taken++; }
}
return total;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V²) | O(V²·logV) | O(V²·logV) |
| Space | O(V) | O(V²) | O(V²) |
| Difficulty | 3/5 | 4/5 | 4/5 |
| When | Dense default | Sparse-ish | Sparse natural |
| PE Verdict | Prim's with min-heap (Kruskal's also fine). | ||
Python Solutions
Prim's O(V²) O(V²) O(V)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minCostConnectPoints(self, p: List[List[int]]) -> int:
n = len(p)
INF = float('inf')
key = [INF]*n; in_ = [False]*n; key[0] = 0; total = 0
for _ in range(n):
u, best = -1, INF
for j in range(n):
if not in_[j] and key[j] < best: best = key[j]; u = j
in_[u] = True; total += best
for v in range(n):
if not in_[v]:
d = abs(p[u][0]-p[v][0]) + abs(p[u][1]-p[v][1])
if d < key[v]: key[v] = d
return total
Prim's with heap O(V²·logV) O(V²)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
class Solution:
def minCostConnectPoints(self, p: List[List[int]]) -> int:
n = len(p); in_ = [False]*n
h = [(0, 0)]; total = 0; taken = 0
while taken < n:
w, u = heapq.heappop(h)
if in_[u]: continue
in_[u] = True; total += w; taken += 1
for v in range(n):
if not in_[v]:
d = abs(p[u][0]-p[v][0]) + abs(p[u][1]-p[v][1])
heapq.heappush(h, (d, v))
return total
Kruskal's O(V²·logV) O(V²)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minCostConnectPoints(self, p: List[List[int]]) -> int:
n = len(p)
edges = []
for i in range(n):
for j in range(i+1, n):
edges.append((abs(p[i][0]-p[j][0]) + abs(p[i][1]-p[j][1]), i, j))
edges.sort()
par = list(range(n)); sz = [1]*n
def find(x):
while par[x] != x: par[x] = par[par[x]]; x = par[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return False
if sz[ra] < sz[rb]: par[ra] = rb; sz[rb] += sz[ra]
else: par[rb] = ra; sz[ra] += sz[rb]
return True
total = 0; taken = 0
for w, i, j in edges:
if taken == n - 1: break
if union(i, j): total += w; taken += 1
return total
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(V²) | O(V²·logV) | O(V²·logV) |
| Space | O(V) | O(V²) | O(V²) |
| Difficulty | 3/5 | 4/5 | 4/5 |
| When | Dense | Default | Sparse natural |
| PE Verdict | Prim's with heap (Kruskal's equivalent). | ||
What the interviewer is really testing
MST on complete graph.
Top gotchas
- Graph is implicit: every pair is an edge → V² edges.
- Prim's O(V²) actually faster than O(V²logV) heap on this since V²edges.
- Stop Kruskal's once V-1 edges chosen.
Ship-it
Prim's with min-heap.
#96 LC 778 Hard Swim in Rising Water
Minimize MAX elevation on path — Dijkstra (heap) or binary search + BFS.
Problem Statement
n×n grid of elevations. Time t lets you swim into any cell with elevation ≤ t. Return min time to reach (n-1,n-1) from (0,0).
Examples
Constraints
- 1 <= n <= 50
Approach Overview
Brute Force
Java: Binary search + BFS
Python: Binary search + BFS
Optimal #1
Java: Dijkstra (min-heap)
Python: Dijkstra
Optimal #2
Java: Union-Find by elevation
Python: Union-Find by elevation
Java Solutions
Binary search + BFS O(n²·log(n²)) O(n²)
Try t in [0..n²-1]; BFS to see if reachable.
Pseudo-code
lo=grid[0][0]; hi=n²-1; binsearch smallest t s.t. BFS reaches end
Complexity
Edge cases & gotchas
- 1x1 → grid[0][0].
Java code
import java.util.*;
class Solution {
public int swimInWater(int[][] g) {
int n = g.length;
int lo = g[0][0], hi = n * n - 1;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (reachable(g, mid)) hi = mid; else lo = mid + 1;
}
return lo;
}
private boolean reachable(int[][] g, int t) {
if (g[0][0] > t) return false;
int n = g.length;
boolean[][] v = new boolean[n][n]; v[0][0] = true;
Deque<int[]> q = new ArrayDeque<>(); q.offer(new int[]{0,0});
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
while (!q.isEmpty()) {
int[] p = q.poll();
if (p[0] == n-1 && p[1] == n-1) return true;
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<n&&nj<n&&!v[ni][nj]&&g[ni][nj]<=t) { v[ni][nj]=true; q.offer(new int[]{ni,nj}); }
}
}
return false;
}
}
Dijkstra (min-heap) O(n²·logn) O(n²)
State = max elevation seen on path; pop smallest.
Pseudo-code
heap.push((g[0][0],0,0)); pop (t,i,j); for each nb push (max(t,g[nb]),nb)
Complexity
Java code
import java.util.*;
class Solution {
public int swimInWater(int[][] g) {
int n = g.length;
boolean[][] v = new boolean[n][n];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{g[0][0], 0, 0});
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
while (!pq.isEmpty()) {
int[] p = pq.poll();
int t = p[0], i = p[1], j = p[2];
if (v[i][j]) continue;
v[i][j] = true;
if (i == n-1 && j == n-1) return t;
for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<n&&nj<n&&!v[ni][nj])
pq.offer(new int[]{Math.max(t, g[ni][nj]), ni, nj});
}
}
return -1;
}
}
Union-Find by elevation O(n²·α) O(n²)
Add cells in elevation order; check when (0,0) and (n-1,n-1) connect.
Pseudo-code
sort cells by elevation; activate one at a time; union with active neighbors; stop when both endpoints connected
Complexity
Java code
class Solution {
private int[] p, sz;
private int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
private void union(int a, int b) {
int ra = find(a), rb = find(b); if (ra == rb) return;
if (sz[ra] < sz[rb]) { p[ra] = rb; sz[rb] += sz[ra]; } else { p[rb] = ra; sz[ra] += sz[rb]; }
}
public int swimInWater(int[][] g) {
int n = g.length;
int[] cells = new int[n*n];
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) cells[g[i][j]] = i*n + j;
p = new int[n*n]; sz = new int[n*n];
for (int i = 0; i < n*n; i++) { p[i] = i; sz[i] = 1; }
boolean[] active = new boolean[n*n];
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int t = 0; t < n*n; t++) {
int idx = cells[t];
int i = idx / n, j = idx % n;
active[idx] = true;
for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<n&&nj<n&&active[ni*n+nj]) union(idx, ni*n+nj);
}
if (find(0) == find(n*n - 1)) return t;
}
return -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·log(n²)) | O(n²·logn) | O(n²·α) |
| Space | O(n²) | O(n²) | O(n²) |
| Difficulty | 4/5 | 4/5 | 4/5 |
| When | Simple to reason | Default | Elegant alt |
| PE Verdict | Dijkstra over min-max paths. | ||
Python Solutions
Binary search + BFS O(n²·log(n²)) O(n²)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def swimInWater(self, g: List[List[int]]) -> int:
n = len(g)
def ok(t):
if g[0][0] > t: return False
v = [[False]*n for _ in range(n)]; v[0][0] = True
q = deque([(0,0)])
while q:
i, j = q.popleft()
if (i, j) == (n-1, n-1): return True
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+dr, j+dc
if 0<=ni<n and 0<=nj<n and not v[ni][nj] and g[ni][nj] <= t:
v[ni][nj] = True; q.append((ni, nj))
return False
lo, hi = g[0][0], n*n - 1
while lo < hi:
mid = (lo + hi) // 2
if ok(mid): hi = mid
else: lo = mid + 1
return lo
Dijkstra O(n²·logn) O(n²)
Min-max path with heap.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
class Solution:
def swimInWater(self, g: List[List[int]]) -> int:
n = len(g)
v = [[False]*n for _ in range(n)]
h = [(g[0][0], 0, 0)]
while h:
t, i, j = heapq.heappop(h)
if v[i][j]: continue
v[i][j] = True
if (i, j) == (n-1, n-1): return t
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+dr, j+dc
if 0<=ni<n and 0<=nj<n and not v[ni][nj]:
heapq.heappush(h, (max(t, g[ni][nj]), ni, nj))
return -1
Union-Find by elevation O(n²·α) O(n²)
Activate cells in order.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def swimInWater(self, g: List[List[int]]) -> int:
n = len(g)
cells = [0]*(n*n)
for i in range(n):
for j in range(n): cells[g[i][j]] = i*n + j
p = list(range(n*n)); sz = [1]*(n*n); active = [False]*(n*n)
def find(x):
while p[x] != x: p[x] = p[p[x]]; x = p[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return
if sz[ra] < sz[rb]: p[ra] = rb; sz[rb] += sz[ra]
else: p[rb] = ra; sz[ra] += sz[rb]
for t in range(n*n):
idx = cells[t]; i, j = divmod(idx, n)
active[idx] = True
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+dr, j+dc
if 0<=ni<n and 0<=nj<n and active[ni*n+nj]: union(idx, ni*n+nj)
if find(0) == find(n*n - 1): return t
return -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²·log(n²)) | O(n²·logn) | O(n²·α) |
| Space | O(n²) | O(n²) | O(n²) |
| Difficulty | 4/5 | 4/5 | 4/5 |
| When | Easy to explain | Default | Elegant alt |
| PE Verdict | Dijkstra (modified — min of max). | ||
What the interviewer is really testing
Min-max path → Dijkstra variant.
Top gotchas
- Cost of an edge is the MAX of cell values, not the sum.
- Heap state is (max_so_far, i, j).
- Union-Find by elevation is a beautiful alternative — activate cells in elevation order.
Ship-it
Dijkstra with max-elevation key.
#97 LC 269 Hard Alien Dictionary
Topological sort over characters — extract precedence from adjacent word diffs.
Problem Statement
Given sorted (alien-lex) words, return any order of the alphabet consistent with them, or "" if invalid.
Examples
Constraints
- 1 <= words.length <= 100
- 1 <= words[i].length <= 100
Approach Overview
Brute Force
Java: DFS topo with cycle detection
Python: DFS topo
Optimal #1
Java: Kahn's BFS
Python: Kahn's BFS
Optimal #2
Java: Kahn's with array indegrees
Python: Kahn's array
Java Solutions
DFS topo with cycle detection O(C) O(1) alphabet
Build graph, DFS 3-color, post-order.
Pseudo-code
build edges; for each char DFS; emit post-order; reverse
Complexity
Edge cases & gotchas
- Prefix issue: ['abc','ab'] → invalid.
Java code
import java.util.*;
class Solution {
public String alienOrder(String[] words) {
Map<Character, Set<Character>> g = new HashMap<>();
for (String w : words) for (char c : w.toCharArray()) g.putIfAbsent(c, new HashSet<>());
for (int i = 0; i < words.length - 1; i++) {
String a = words[i], b = words[i+1];
if (a.length() > b.length() && a.startsWith(b)) return "";
for (int k = 0; k < Math.min(a.length(), b.length()); k++) {
if (a.charAt(k) != b.charAt(k)) { g.get(a.charAt(k)).add(b.charAt(k)); break; }
}
}
Map<Character, Integer> color = new HashMap<>();
StringBuilder sb = new StringBuilder();
for (char c : g.keySet()) if (!color.containsKey(c)) if (!dfs(c, g, color, sb)) return "";
return sb.reverse().toString();
}
private boolean dfs(char u, Map<Character, Set<Character>> g, Map<Character, Integer> color, StringBuilder sb) {
color.put(u, 1);
for (char v : g.get(u)) {
if (color.getOrDefault(v, 0) == 1) return false;
if (color.getOrDefault(v, 0) == 0 && !dfs(v, g, color, sb)) return false;
}
color.put(u, 2); sb.append(u); return true;
}
}
Kahn's BFS O(C) O(1)
Topo via indegree.
Pseudo-code
build graph + indeg; enqueue indeg=0 chars; pop, append, decrement neighbors
Complexity
Edge cases & gotchas
- Same prefix invalid.
Java code
import java.util.*;
class Solution {
public String alienOrder(String[] words) {
Map<Character, Set<Character>> g = new HashMap<>();
Map<Character, Integer> indeg = new HashMap<>();
for (String w : words) for (char c : w.toCharArray()) { g.putIfAbsent(c, new HashSet<>()); indeg.putIfAbsent(c, 0); }
for (int i = 0; i < words.length - 1; i++) {
String a = words[i], b = words[i+1];
if (a.length() > b.length() && a.startsWith(b)) return "";
for (int k = 0; k < Math.min(a.length(), b.length()); k++) {
char ca = a.charAt(k), cb = b.charAt(k);
if (ca != cb) {
if (g.get(ca).add(cb)) indeg.merge(cb, 1, Integer::sum);
break;
}
}
}
Deque<Character> q = new ArrayDeque<>();
for (Map.Entry<Character, Integer> e : indeg.entrySet()) if (e.getValue() == 0) q.offer(e.getKey());
StringBuilder sb = new StringBuilder();
while (!q.isEmpty()) {
char u = q.poll(); sb.append(u);
for (char v : g.get(u)) if (indeg.merge(v, -1, Integer::sum) == 0) q.offer(v);
}
return sb.length() == indeg.size() ? sb.toString() : "";
}
}
Kahn's with array indegrees O(C) O(1)
Avoid HashMap overhead by fixed 26 array.
Pseudo-code
see opt1 with int[26]
Complexity
Java code
import java.util.*;
class Solution {
public String alienOrder(String[] words) {
Set<Character>[] g = new HashSet[26];
int[] indeg = new int[26];
boolean[] present = new boolean[26];
for (int i = 0; i < 26; i++) g[i] = new HashSet<>();
for (String w : words) for (char c : w.toCharArray()) present[c - 'a'] = true;
for (int i = 0; i < words.length - 1; i++) {
String a = words[i], b = words[i+1];
if (a.length() > b.length() && a.startsWith(b)) return "";
for (int k = 0; k < Math.min(a.length(), b.length()); k++) {
char ca = a.charAt(k), cb = b.charAt(k);
if (ca != cb) { if (g[ca-'a'].add(cb)) indeg[cb-'a']++; break; }
}
}
Deque<Integer> q = new ArrayDeque<>();
int total = 0;
for (int i = 0; i < 26; i++) if (present[i]) { total++; if (indeg[i] == 0) q.offer(i); }
StringBuilder sb = new StringBuilder();
while (!q.isEmpty()) {
int u = q.poll(); sb.append((char)('a' + u));
for (char v : g[u]) if (--indeg[v - 'a'] == 0) q.offer(v - 'a');
}
return sb.length() == total ? sb.toString() : "";
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(C) | O(C) | O(C) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 4/5 | 4/5 | 4/5 |
| When | Recursion ok | Default | Tight perf |
| PE Verdict | Kahn's BFS with hashmap for clarity. | ||
Python Solutions
DFS topo O(C) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict
class Solution:
def alienOrder(self, words: List[str]) -> str:
g = defaultdict(set); chars = set()
for w in words: chars.update(w)
for a, b in zip(words, words[1:]):
if len(a) > len(b) and a.startswith(b): return ""
for x, y in zip(a, b):
if x != y: g[x].add(y); break
color = {}; out = []
def dfs(u):
color[u] = 1
for v in g[u]:
if color.get(v, 0) == 1: return False
if color.get(v, 0) == 0 and not dfs(v): return False
color[u] = 2; out.append(u); return True
for c in chars:
if c not in color and not dfs(c): return ""
return ''.join(reversed(out))
Kahn's BFS O(C) O(1)
Indegree topo.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict, deque
class Solution:
def alienOrder(self, words: List[str]) -> str:
g = defaultdict(set); indeg = {c: 0 for w in words for c in w}
for a, b in zip(words, words[1:]):
if len(a) > len(b) and a.startswith(b): return ""
for x, y in zip(a, b):
if x != y:
if y not in g[x]: g[x].add(y); indeg[y] += 1
break
q = deque(c for c in indeg if indeg[c] == 0); out = []
while q:
u = q.popleft(); out.append(u)
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
return ''.join(out) if len(out) == len(indeg) else ""
Kahn's array O(C) O(1)
Fixed 26 arrays.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def alienOrder(self, words: List[str]) -> str:
g = [set() for _ in range(26)]; indeg = [0]*26; present = [False]*26
for w in words:
for c in w: present[ord(c)-97] = True
for a, b in zip(words, words[1:]):
if len(a) > len(b) and a.startswith(b): return ""
for x, y in zip(a, b):
if x != y:
xi, yi = ord(x)-97, ord(y)-97
if yi not in g[xi]: g[xi].add(yi); indeg[yi] += 1
break
total = sum(present)
q = deque(i for i in range(26) if present[i] and indeg[i] == 0)
out = []
while q:
u = q.popleft(); out.append(chr(u+97))
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
return ''.join(out) if len(out) == total else ""
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(C) | O(C) | O(C) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 4/5 | 4/5 | 4/5 |
| When | Recursion | Default | Tight |
| PE Verdict | Kahn's BFS. | ||
What the interviewer is really testing
Topo sort with edge inference from sorted strings.
Top gotchas
- EDGE CASE: ['abc', 'ab'] is INVALID — a longer prefix can't come before its prefix.
- Add chars to indeg even if no constraint involves them.
- Only the FIRST differing char between adjacent words yields a precedence.
Ship-it
Kahn's BFS over character DAG.
#98 LC 787 Medium Cheapest Flights Within K Stops
Bellman-Ford limited to k+1 relaxations — naturally caps stops.
Problem Statement
n cities, flights[i]=[from,to,price]. Cheapest from src to dst with at most k stops, or -1.
Examples
Constraints
- 1 <= n <= 100
- 0 <= flights.length <= n*(n-1)/2
Approach Overview
Brute Force
Java: DFS with stop limit
Python: DFS with hop limit
Optimal #1
Java: Bellman-Ford k+1 passes
Python: Bellman-Ford k+1
Optimal #2
Java: Dijkstra with stop count
Python: Dijkstra with stops
Java Solutions
DFS with stop limit O(n^k) O(n+k)
Try every path within k stops; min cost.
Pseudo-code
dfs(u, stops_left, cost): if u==dst track; for nb: recurse if stops_left>=0
Complexity
Edge cases & gotchas
- src==dst → 0.
Java code
import java.util.*;
class Solution {
private int best;
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
Map<Integer, List<int[]>> g = new HashMap<>();
for (int[] f : flights) g.computeIfAbsent(f[0], x -> new ArrayList<>()).add(new int[]{f[1], f[2]});
best = Integer.MAX_VALUE;
dfs(src, dst, k + 1, 0, g);
return best == Integer.MAX_VALUE ? -1 : best;
}
private void dfs(int u, int dst, int hops, int cost, Map<Integer, List<int[]>> g) {
if (u == dst) { best = Math.min(best, cost); return; }
if (hops == 0 || cost >= best) return;
for (int[] nb : g.getOrDefault(u, new ArrayList<>())) dfs(nb[0], dst, hops - 1, cost + nb[1], g);
}
}
Bellman-Ford k+1 passes O(k·E) O(n)
Relax with edges array; snapshot prev distances each round.
Pseudo-code
dist = [INF]; dist[src]=0; repeat k+1 times: prev=dist; for each edge relax using prev
Complexity
Edge cases & gotchas
- k=0 means direct only.
Java code
import java.util.*;
class Solution {
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
for (int i = 0; i <= k; i++) {
int[] prev = dist.clone();
for (int[] f : flights) {
if (prev[f[0]] != Integer.MAX_VALUE && prev[f[0]] + f[2] < dist[f[1]])
dist[f[1]] = prev[f[0]] + f[2];
}
}
return dist[dst] == Integer.MAX_VALUE ? -1 : dist[dst];
}
}
Dijkstra with stop count O(E·k·log(V·k)) O(V·k)
Heap of (cost, node, stops_used); skip if exceeds k.
Pseudo-code
heap.push((0,src,0)); pop (c,u,s); if u==dst return; if s<=k: push neighbors
Complexity
Java code
import java.util.*;
class Solution {
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
Map<Integer, List<int[]>> g = new HashMap<>();
for (int[] f : flights) g.computeIfAbsent(f[0], x -> new ArrayList<>()).add(new int[]{f[1], f[2]});
int[][] best = new int[n][k + 2];
for (int[] row : best) Arrays.fill(row, Integer.MAX_VALUE);
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, src, 0});
while (!pq.isEmpty()) {
int[] p = pq.poll();
int c = p[0], u = p[1], s = p[2];
if (u == dst) return c;
if (s > k) continue;
for (int[] nb : g.getOrDefault(u, new ArrayList<>())) {
int nc = c + nb[1];
if (nc < best[nb[0]][s + 1]) { best[nb[0]][s + 1] = nc; pq.offer(new int[]{nc, nb[0], s + 1}); }
}
}
return -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n^k) | O(k·E) | O(E·k·log(V·k)) |
| Space | O(n+k) | O(n) | O(V·k) |
| Difficulty | 3/5 | 4/5 | 5/5 |
| When | Tiny | Default | Sparse + small k |
| PE Verdict | Bellman-Ford limited to k+1 relaxations. | ||
Python Solutions
DFS with hop limit O(n^k) O(n+k)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import defaultdict
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
g = defaultdict(list)
for u, v, w in flights: g[u].append((v, w))
best = float('inf')
def dfs(u, hops, cost):
nonlocal best
if u == dst: best = min(best, cost); return
if hops == 0 or cost >= best: return
for v, w in g[u]: dfs(v, hops-1, cost+w)
dfs(src, k+1, 0)
return -1 if best == float('inf') else best
Bellman-Ford k+1 O(k·E) O(n)
Snapshot dist each round.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
INF = float('inf')
dist = [INF]*n; dist[src] = 0
for _ in range(k + 1):
prev = dist[:]
for u, v, w in flights:
if prev[u] + w < dist[v]: dist[v] = prev[u] + w
return -1 if dist[dst] == INF else dist[dst]
Dijkstra with stops O(E·k·log(V·k)) O(V·k)
State = (cost, node, stops).
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
from collections import defaultdict
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
g = defaultdict(list)
for u, v, w in flights: g[u].append((v, w))
h = [(0, src, 0)]
best = {}
while h:
c, u, s = heapq.heappop(h)
if u == dst: return c
if s > k: continue
for v, w in g[u]:
nc = c + w
if best.get((v, s+1), float('inf')) > nc:
best[(v, s+1)] = nc
heapq.heappush(h, (nc, v, s+1))
return -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n^k) | O(k·E) | O(E·k·log(V·k)) |
| Space | O(n+k) | O(n) | O(V·k) |
| Difficulty | 3/5 | 4/5 | 5/5 |
| When | Tiny | Default | Sparse |
| PE Verdict | Bellman-Ford with k+1 rounds. | ||
What the interviewer is really testing
Bellman-Ford fits naturally: each round = one extra hop.
Top gotchas
- MUST snapshot prev distances — else you can take >k edges in one round.
- 'k stops' = k+1 edges.
- Plain Dijkstra fails: cheaper-to-cur path may use too many stops; you need stops in the state.
Ship-it
Bellman-Ford with snapshot.
1-D Dynamic Programming
#99 LC 70 Easy Climbing Stairs
Fibonacci in disguise — f(n)=f(n-1)+f(n-2).
Problem Statement
n stairs, 1 or 2 steps at a time. Count distinct ways to reach the top.
Examples
Constraints
- 1 <= n <= 45
Approach Overview
Brute Force
Java: Naive recursion
Python: Recursion
Optimal #1
Java: Bottom-up O(1) space
Python: Rolling O(1)
Optimal #2
Java: Matrix exponentiation
Python: Matrix expo
Java Solutions
Naive recursion O(2^n) O(n)
Exponential.
Pseudo-code
f(n) = f(n-1) + f(n-2)
Complexity
Edge cases & gotchas
- n=1→1, n=2→2.
Java code
class Solution {
public int climbStairs(int n) {
if (n <= 2) return n;
return climbStairs(n - 1) + climbStairs(n - 2);
}
}
Bottom-up O(1) space O(n) O(1)
Two rolling vars.
Pseudo-code
a=1,b=2; for i in 3..n: c=a+b; a=b; b=c
Complexity
Java code
class Solution {
public int climbStairs(int n) {
if (n <= 2) return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) { int c = a + b; a = b; b = c; }
return b;
}
}
Matrix exponentiation O(log n) O(1)
O(log n) for huge n.
Pseudo-code
[[1,1],[1,0]]^n → top-left = f(n+1)
Complexity
Java code
class Solution {
public int climbStairs(int n) {
long[][] M = {{1,1},{1,0}};
long[][] R = pow(M, n);
return (int) R[0][0];
}
private long[][] pow(long[][] m, int p) {
long[][] r = {{1,0},{0,1}};
while (p > 0) { if ((p & 1) == 1) r = mul(r, m); m = mul(m, m); p >>= 1; }
return r;
}
private long[][] mul(long[][] a, long[][] b) {
return new long[][]{{a[0][0]*b[0][0]+a[0][1]*b[1][0], a[0][0]*b[0][1]+a[0][1]*b[1][1]},
{a[1][0]*b[0][0]+a[1][1]*b[1][0], a[1][0]*b[0][1]+a[1][1]*b[1][1]}};
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Huge n only |
| PE Verdict | Two rolling vars. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 2: return n
return self.climbStairs(n-1) + self.climbStairs(n-2)
Rolling O(1) O(n) O(1)
Two ints.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 2: return n
a, b = 1, 2
for _ in range(3, n+1): a, b = b, a + b
return b
Matrix expo O(log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def climbStairs(self, n: int) -> int:
def mul(a, b):
return [[a[0][0]*b[0][0]+a[0][1]*b[1][0], a[0][0]*b[0][1]+a[0][1]*b[1][1]],
[a[1][0]*b[0][0]+a[1][1]*b[1][0], a[1][0]*b[0][1]+a[1][1]*b[1][1]]]
def power(m, p):
r = [[1,0],[0,1]]
while p:
if p & 1: r = mul(r, m)
m = mul(m, m); p >>= 1
return r
return power([[1,1],[1,0]], n)[0][0]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Huge n |
| PE Verdict | Rolling O(1). | ||
What the interviewer is really testing
DP intro.
Top gotchas
- Base cases n=1→1, n=2→2.
- Naive recursion TLE at n=45.
- Matrix expo is overkill but worth mentioning.
Ship-it
Two rolling vars.
#100 LC 746 Easy Min Cost Climbing Stairs
DP: cost to land at step i = cost[i] + min(prev, prev2).
Problem Statement
Pay cost[i] to step on stair i, then step 1 or 2. Start at 0 or 1. Min total cost to reach the top (beyond last).
Examples
Constraints
- 2 <= n <= 1000
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: Bottom-up O(1)
Python: Rolling O(1)
Optimal #2
Java: DP array
Python: DP array
Java Solutions
Recursion O(2^n) O(n)
Try every reachable step.
Pseudo-code
minCost(i)=cost[i]+min(minCost(i-1),minCost(i-2))
Complexity
Java code
class Solution {
public int minCostClimbingStairs(int[] cost) {
return Math.min(rec(cost, cost.length - 1), rec(cost, cost.length - 2));
}
private int rec(int[] c, int i) {
if (i < 0) return 0;
if (i <= 1) return c[i];
return c[i] + Math.min(rec(c, i-1), rec(c, i-2));
}
}
Bottom-up O(1) O(n) O(1)
Two rolling vars.
Pseudo-code
a=cost[0]; b=cost[1]; for i=2..n-1: c=cost[i]+min(a,b); shift; return min(a,b)
Complexity
Java code
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int a = cost[0], b = cost[1];
for (int i = 2; i < n; i++) {
int c = cost[i] + Math.min(a, b);
a = b; b = c;
}
return Math.min(a, b);
}
}
DP array O(n) O(n)
Explicit dp[].
Pseudo-code
dp[i] = cost[i] + min(dp[i-1], dp[i-2])
Complexity
Java code
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n];
dp[0] = cost[0]; dp[1] = cost[1];
for (int i = 2; i < n; i++) dp[i] = cost[i] + Math.min(dp[i-1], dp[i-2]);
return Math.min(dp[n-1], dp[n-2]);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | Rolling O(1). | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
def rec(i):
if i < 0: return 0
if i <= 1: return cost[i]
return cost[i] + min(rec(i-1), rec(i-2))
n = len(cost)
return min(rec(n-1), rec(n-2))
Rolling O(1) O(n) O(1)
Two ints.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
a, b = cost[0], cost[1]
for i in range(2, len(cost)):
a, b = b, cost[i] + min(a, b)
return min(a, b)
DP array O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost); dp = cost[:]
for i in range(2, n): dp[i] = cost[i] + min(dp[i-1], dp[i-2])
return min(dp[-1], dp[-2])
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | Rolling O(1). | ||
What the interviewer is really testing
Variant of climbing stairs.
Top gotchas
- You pay cost[i] to STEP ON i, then move from i.
- Goal is BEYOND last index — answer is min(dp[n-1], dp[n-2]).
- Start can be at 0 OR 1 — handle both base cases.
Ship-it
Rolling DP.
#101 LC 198 Medium House Robber
Max non-adjacent sum — dp[i] = max(dp[i-1], dp[i-2] + nums[i]).
Problem Statement
Cannot rob two adjacent houses. Max amount.
Examples
Constraints
- 1 <= n <= 100
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: Bottom-up O(1)
Python: Rolling O(1)
Optimal #2
Java: Memoized recursion
Python: Memoized
Java Solutions
Recursion O(2^n) O(n)
Try rob/skip at each house.
Pseudo-code
rob(i)=max(rob(i-1), rob(i-2)+nums[i])
Complexity
Java code
class Solution {
public int rob(int[] nums) { return rec(nums, nums.length - 1); }
private int rec(int[] n, int i) {
if (i < 0) return 0;
return Math.max(rec(n, i-1), rec(n, i-2) + n[i]);
}
}
Bottom-up O(1) O(n) O(1)
Two rolling vars.
Pseudo-code
prev2=0; prev1=0; for x in nums: cur=max(prev1, prev2+x); shift
Complexity
Edge cases & gotchas
- n=1 → nums[0].
Java code
class Solution {
public int rob(int[] nums) {
int prev2 = 0, prev1 = 0;
for (int x : nums) { int cur = Math.max(prev1, prev2 + x); prev2 = prev1; prev1 = cur; }
return prev1;
}
}
Memoized recursion O(n) O(n)
Top-down with cache.
Pseudo-code
memo + rec
Complexity
Java code
class Solution {
private Integer[] memo;
public int rob(int[] nums) {
memo = new Integer[nums.length];
return rec(nums, nums.length - 1);
}
private int rec(int[] n, int i) {
if (i < 0) return 0;
if (memo[i] != null) return memo[i];
return memo[i] = Math.max(rec(n, i-1), rec(n, i-2) + n[i]);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default | Top-down clarity |
| PE Verdict | Bottom-up O(1). | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
def rec(i):
if i < 0: return 0
return max(rec(i-1), rec(i-2) + nums[i])
return rec(len(nums) - 1)
Rolling O(1) O(n) O(1)
Two ints.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
prev2 = prev1 = 0
for x in nums:
prev2, prev1 = prev1, max(prev1, prev2 + x)
return prev1
Memoized O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def rob(self, nums: List[int]) -> int:
@lru_cache(None)
def rec(i):
if i < 0: return 0
return max(rec(i-1), rec(i-2) + nums[i])
return rec(len(nums) - 1)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Rolling DP. | ||
What the interviewer is really testing
Foundational DP recurrence.
Top gotchas
- Decision at each house: rob (skip prev) or skip (carry max).
- Empty array → 0.
- Sliding two vars suffices.
Ship-it
Two-variable rolling DP.
#102 LC 213 Medium House Robber II
Circle — split into two linear House Robber problems; take max.
Problem Statement
Houses in a circle (first and last adjacent). Same rule: no two adjacent. Max amount.
Examples
Constraints
- 1 <= n <= 100
Approach Overview
Brute Force
Java: Recursion with both options
Python: Recursion
Optimal #1
Java: Two linear scans
Python: Two linear scans
Optimal #2
Java: Memoized rec with start/end
Python: Memoized
Java Solutions
Recursion with both options O(2^n) O(n)
Try with-first and without-first.
Pseudo-code
max(rob(0..n-2), rob(1..n-1))
Complexity
Edge cases & gotchas
- n=1 → nums[0].
Java code
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(rec(nums, 0, nums.length - 2), rec(nums, 1, nums.length - 1));
}
private int rec(int[] n, int l, int r) {
if (l > r) return 0;
return Math.max(rec(n, l, r-1), rec(n, l, r-2) + n[r]);
}
}
Two linear scans O(n) O(1)
Run House Robber I on [0..n-2] and [1..n-1].
Pseudo-code
max(rob_linear(0,n-2), rob_linear(1,n-1))
Complexity
Edge cases & gotchas
- n=1 → nums[0].
Java code
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(robLinear(nums, 0, nums.length - 2), robLinear(nums, 1, nums.length - 1));
}
private int robLinear(int[] n, int l, int r) {
int prev2 = 0, prev1 = 0;
for (int i = l; i <= r; i++) { int cur = Math.max(prev1, prev2 + n[i]); prev2 = prev1; prev1 = cur; }
return prev1;
}
}
Memoized rec with start/end O(n) O(n)
Top-down for both windows.
Pseudo-code
memo rob(start, i) for two windows
Complexity
Java code
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(memo(nums, 0, nums.length - 2), memo(nums, 1, nums.length - 1));
}
private int memo(int[] n, int l, int r) {
Integer[] m = new Integer[n.length];
return rec(n, l, r, m);
}
private int rec(int[] n, int l, int i, Integer[] m) {
if (i < l) return 0;
if (m[i] != null) return m[i];
return m[i] = Math.max(rec(n, l, i-1, m), rec(n, l, i-2, m) + n[i]);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 2/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Two linear scans. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: return nums[0]
def rec(l, r):
if l > r: return 0
return max(rec(l, r-1), rec(l, r-2) + nums[r])
return max(rec(0, len(nums)-2), rec(1, len(nums)-1))
Two linear scans O(n) O(1)
Run linear DP on two slices.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: return nums[0]
def helper(arr):
prev2 = prev1 = 0
for x in arr:
prev2, prev1 = prev1, max(prev1, prev2 + x)
return prev1
return max(helper(nums[:-1]), helper(nums[1:]))
Memoized O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: return nums[0]
def helper(l, r):
@lru_cache(None)
def rec(i):
if i < l: return 0
return max(rec(i-1), rec(i-2) + nums[i])
res = rec(r)
rec.cache_clear()
return res
return max(helper(0, len(nums)-2), helper(1, len(nums)-1))
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 2/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Two linear scans. | ||
What the interviewer is really testing
Reduction trick: circular → two linear.
Top gotchas
- n=1 is a special case — return nums[0].
- First and last can't BOTH be robbed, so either drop first or drop last.
- Otherwise it's just House Robber I.
Ship-it
Run linear robber on nums[:-1] and nums[1:].
#103 LC 5 Medium Longest Palindromic Substring
Expand around each center — O(n²) time, O(1) space.
Problem Statement
Return any longest palindromic substring of s.
Examples
Constraints
- 1 <= n <= 1000
Approach Overview
Brute Force
Java: Check every substring
Python: Triple loop
Optimal #1
Java: Expand around center
Python: Expand around center
Optimal #2
Java: Manacher's algorithm
Python: Manacher's
Java Solutions
Check every substring O(n³) O(1)
For each (i,j) check palindrome.
Pseudo-code
track best; for each i<j: if isPalin → update
Complexity
Edge cases & gotchas
- n=1 → s itself.
Java code
class Solution {
public String longestPalindrome(String s) {
int bestL = 0, bestR = 0;
for (int i = 0; i < s.length(); i++) for (int j = i; j < s.length(); j++)
if (isPal(s, i, j) && j - i > bestR - bestL) { bestL = i; bestR = j; }
return s.substring(bestL, bestR + 1);
}
private boolean isPal(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
}
Expand around center O(n²) O(1)
2n-1 centers; expand outward.
Pseudo-code
for each i: expand(i,i) and expand(i,i+1); track longest
Complexity
Java code
class Solution {
public String longestPalindrome(String s) {
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int l1 = expand(s, i, i);
int l2 = expand(s, i, i + 1);
int len = Math.max(l1, l2);
if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; }
}
return s.substring(start, end + 1);
}
private int expand(String s, int l, int r) {
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { l--; r++; }
return r - l - 1;
}
}
Manacher's algorithm O(n) O(n)
O(n) using mirror property.
Pseudo-code
transform s with separators; compute palindrome radii using center & right boundary
Complexity
Java code
class Solution {
public String longestPalindrome(String s) {
StringBuilder sb = new StringBuilder("^");
for (char c : s.toCharArray()) sb.append('#').append(c);
sb.append("#$");
String t = sb.toString();
int n = t.length(), c = 0, r = 0;
int[] p = new int[n];
for (int i = 1; i < n - 1; i++) {
if (r > i) p[i] = Math.min(r - i, p[2*c - i]);
while (t.charAt(i + p[i] + 1) == t.charAt(i - p[i] - 1)) p[i]++;
if (i + p[i] > r) { c = i; r = i + p[i]; }
}
int maxLen = 0, center = 0;
for (int i = 1; i < n - 1; i++) if (p[i] > maxLen) { maxLen = p[i]; center = i; }
int start = (center - maxLen) / 2;
return s.substring(start, start + maxLen);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n²) | O(n) |
| Space | O(1) | O(1) | O(n) |
| Difficulty | 1/5 | 3/5 | 5/5 |
| When | Don't | Default | Hot path / huge n |
| PE Verdict | Expand around center — clean and fast. | ||
Python Solutions
Triple loop O(n³) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def longestPalindrome(self, s: str) -> str:
best = ''
for i in range(len(s)):
for j in range(i, len(s)):
sub = s[i:j+1]
if sub == sub[::-1] and len(sub) > len(best): best = sub
return best
Expand around center O(n²) O(1)
2n-1 centers.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def longestPalindrome(self, s: str) -> str:
def expand(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]: l -= 1; r += 1
return s[l+1:r]
best = ''
for i in range(len(s)):
a = expand(i, i); b = expand(i, i+1)
for c in (a, b):
if len(c) > len(best): best = c
return best
Manacher's O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def longestPalindrome(self, s: str) -> str:
t = '^#' + '#'.join(s) + '#$'
n = len(t); p = [0]*n; c = r = 0
for i in range(1, n-1):
if r > i: p[i] = min(r - i, p[2*c - i])
while t[i + p[i] + 1] == t[i - p[i] - 1]: p[i] += 1
if i + p[i] > r: c = i; r = i + p[i]
ml = max(p); ci = p.index(ml)
start = (ci - ml) // 2
return s[start:start + ml]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n²) | O(n) |
| Space | O(1) | O(1) | O(n) |
| Difficulty | 1/5 | 3/5 | 5/5 |
| When | Don't | Default | Hot path |
| PE Verdict | Expand around center. | ||
What the interviewer is really testing
Center-expansion is the go-to for interviews.
Top gotchas
- 2n-1 centers: each char and each gap.
- Track BOTH odd (i,i) and even (i,i+1) lengths.
- Index math after expand: l = i - (len-1)/2, r = i + len/2.
Ship-it
Expand around center.
#104 LC 647 Medium Palindromic Substrings
Count palindromes — expand around each center, count valid expansions.
Problem Statement
Count distinct (by position) palindromic substrings of s.
Examples
Constraints
- 1 <= n <= 1000
Approach Overview
Brute Force
Java: Check every substring
Python: Triple loop
Optimal #1
Java: Expand around center
Python: Expand around center
Optimal #2
Java: DP table
Python: DP table
Java Solutions
Check every substring O(n³) O(1)
Same as LPS brute.
Pseudo-code
for each (i,j): if isPalin → cnt++
Complexity
Java code
class Solution {
public int countSubstrings(String s) {
int cnt = 0;
for (int i = 0; i < s.length(); i++) for (int j = i; j < s.length(); j++)
if (isPal(s, i, j)) cnt++;
return cnt;
}
private boolean isPal(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
}
Expand around center O(n²) O(1)
Each successful expansion = one palindrome.
Pseudo-code
for each center: while can expand: cnt++
Complexity
Java code
class Solution {
public int countSubstrings(String s) {
int cnt = 0;
for (int i = 0; i < s.length(); i++) { cnt += expand(s, i, i); cnt += expand(s, i, i+1); }
return cnt;
}
private int expand(String s, int l, int r) {
int c = 0;
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { c++; l--; r++; }
return c;
}
}
DP table O(n²) O(n²)
dp[i][j] = is s[i..j] palindrome.
Pseudo-code
dp[i][j] = s[i]==s[j] && (j-i<2 || dp[i+1][j-1])
Complexity
Java code
class Solution {
public int countSubstrings(String s) {
int n = s.length(), cnt = 0;
boolean[][] dp = new boolean[n][n];
for (int i = n - 1; i >= 0; i--) for (int j = i; j < n; j++)
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i+1][j-1])) { dp[i][j] = true; cnt++; }
return cnt;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n²) | O(n²) |
| Space | O(1) | O(1) | O(n²) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When | Don't | Default — O(1) space | Useful when reusing |
| PE Verdict | Expand around center. | ||
Python Solutions
Triple loop O(n³) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def countSubstrings(self, s: str) -> int:
cnt = 0
for i in range(len(s)):
for j in range(i, len(s)):
sub = s[i:j+1]
if sub == sub[::-1]: cnt += 1
return cnt
Expand around center O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def countSubstrings(self, s: str) -> int:
def expand(l, r):
c = 0
while l >= 0 and r < len(s) and s[l] == s[r]: c += 1; l -= 1; r += 1
return c
return sum(expand(i, i) + expand(i, i+1) for i in range(len(s)))
DP table O(n²) O(n²)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s); cnt = 0
dp = [[False]*n for _ in range(n)]
for i in range(n-1, -1, -1):
for j in range(i, n):
if s[i] == s[j] and (j - i < 2 or dp[i+1][j-1]):
dp[i][j] = True; cnt += 1
return cnt
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n²) | O(n²) |
| Space | O(1) | O(1) | O(n²) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When | Don't | Default | Reusable |
| PE Verdict | Expand around center. | ||
What the interviewer is really testing
Cousin of Longest Palindromic Substring.
Top gotchas
- EACH successful expansion adds 1 to count.
- Both odd and even centers.
- DP table is a clean alternative if you'll reuse palindrome info.
Ship-it
Expand around center; sum counts.
#105 LC 91 Medium Decode Ways
DP: dp[i] = dp[i-1] (single) + dp[i-2] (pair if 10..26).
Problem Statement
Decode digit string using A=1..Z=26. Count distinct decodings.
Examples
Constraints
- 1 <= n <= 100
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: Bottom-up O(1)
Python: Rolling O(1)
Optimal #2
Java: Memoized recursion
Python: Memoized
Java Solutions
Recursion O(2^n) O(n)
Try take 1 / take 2.
Pseudo-code
rec(i): if i==n return 1; if s[i]==0 return 0; res=rec(i+1); if 2-digit valid: res+=rec(i+2)
Complexity
Edge cases & gotchas
- Leading 0 → 0.
Java code
class Solution {
public int numDecodings(String s) { return rec(s, 0); }
private int rec(String s, int i) {
if (i == s.length()) return 1;
if (s.charAt(i) == '0') return 0;
int r = rec(s, i + 1);
if (i + 1 < s.length()) {
int v = (s.charAt(i)-'0')*10 + (s.charAt(i+1)-'0');
if (v <= 26) r += rec(s, i + 2);
}
return r;
}
}
Bottom-up O(1) O(n) O(1)
Two rolling vars from the right (or left).
Pseudo-code
dp[n]=1; for i=n-1..0: dp[i]=s[i]!='0' ? dp[i+1] + (valid pair ? dp[i+2] : 0) : 0
Complexity
Java code
class Solution {
public int numDecodings(String s) {
int n = s.length();
int next1 = 1, next2 = 0;
for (int i = n - 1; i >= 0; i--) {
int cur = 0;
if (s.charAt(i) != '0') {
cur = next1;
if (i + 1 < n) {
int v = (s.charAt(i)-'0')*10 + (s.charAt(i+1)-'0');
if (v >= 10 && v <= 26) cur += next2;
}
}
next2 = next1; next1 = cur;
}
return next1;
}
}
Memoized recursion O(n) O(n)
Top-down cached.
Pseudo-code
memo + rec
Complexity
Java code
class Solution {
private Integer[] memo;
public int numDecodings(String s) { memo = new Integer[s.length()]; return rec(s, 0); }
private int rec(String s, int i) {
if (i == s.length()) return 1;
if (s.charAt(i) == '0') return 0;
if (memo[i] != null) return memo[i];
int r = rec(s, i + 1);
if (i + 1 < s.length()) {
int v = (s.charAt(i)-'0')*10 + (s.charAt(i+1)-'0');
if (v <= 26) r += rec(s, i + 2);
}
return memo[i] = r;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Bottom-up O(1). | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def numDecodings(self, s: str) -> int:
def rec(i):
if i == len(s): return 1
if s[i] == '0': return 0
r = rec(i + 1)
if i + 1 < len(s) and 10 <= int(s[i:i+2]) <= 26: r += rec(i + 2)
return r
return rec(0)
Rolling O(1) O(n) O(1)
Same as Java.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s); next1, next2 = 1, 0
for i in range(n - 1, -1, -1):
cur = 0
if s[i] != '0':
cur = next1
if i + 1 < n and 10 <= int(s[i:i+2]) <= 26: cur += next2
next2, next1 = next1, cur
return next1
Memoized O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from functools import lru_cache
class Solution:
def numDecodings(self, s: str) -> int:
@lru_cache(None)
def rec(i):
if i == len(s): return 1
if s[i] == '0': return 0
r = rec(i+1)
if i+1 < len(s) and 10 <= int(s[i:i+2]) <= 26: r += rec(i+2)
return r
return rec(0)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Rolling O(1). | ||
What the interviewer is really testing
Variant on Fibonacci with validity constraints.
Top gotchas
- '0' alone is never decodable — only 10, 20 count as paired.
- Pair must be in [10, 26] (not [01, 26]).
- Empty string convention: dp[n] = 1.
Ship-it
Bottom-up O(1).
#106 LC 322 Medium Coin Change
Unbounded knapsack — dp[a] = min over coins c of dp[a-c] + 1.
Problem Statement
Fewest coins summing to amount; -1 if impossible.
Examples
Constraints
- 1 <= coins.length <= 12
- 0 <= amount <= 10^4
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: Bottom-up DP
Python: Bottom-up DP
Optimal #2
Java: BFS by amount
Python: BFS by amount
Java Solutions
Recursion O(c^a) O(a)
Try each coin, recurse on remainder.
Pseudo-code
rec(rem): if 0 return 0; if <0 return INF; for c: best = min(best, rec(rem-c)+1)
Complexity
Edge cases & gotchas
- amount=0 → 0.
Java code
class Solution {
public int coinChange(int[] coins, int amount) {
int r = rec(coins, amount);
return r == Integer.MAX_VALUE ? -1 : r;
}
private int rec(int[] coins, int rem) {
if (rem == 0) return 0;
if (rem < 0) return Integer.MAX_VALUE;
int best = Integer.MAX_VALUE;
for (int c : coins) {
int sub = rec(coins, rem - c);
if (sub != Integer.MAX_VALUE) best = Math.min(best, sub + 1);
}
return best;
}
}
Bottom-up DP O(a·n) O(a)
dp[a] = min over coins of dp[a-c]+1.
Pseudo-code
dp[0]=0; for a=1..amount: for c: if a-c>=0 and dp[a-c]+1<dp[a]: dp[a]=dp[a-c]+1
Complexity
Edge cases & gotchas
- dp[a]=INF means unreachable.
Java code
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
java.util.Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int a = 1; a <= amount; a++) for (int c : coins)
if (a - c >= 0) dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
}
BFS by amount O(a·n) O(a)
Level = #coins; first time we hit amount.
Pseudo-code
queue starts at 0; expand by +coin; level when reach amount
Complexity
Java code
import java.util.*;
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount == 0) return 0;
boolean[] seen = new boolean[amount + 1];
Deque<Integer> q = new ArrayDeque<>(); q.offer(0); seen[0] = true;
int level = 0;
while (!q.isEmpty()) {
level++;
int sz = q.size();
for (int k = 0; k < sz; k++) {
int cur = q.poll();
for (int c : coins) {
int nxt = cur + c;
if (nxt == amount) return level;
if (nxt < amount && !seen[nxt]) { seen[nxt] = true; q.offer(nxt); }
}
}
}
return -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(c^a) | O(a·n) | O(a·n) |
| Space | O(a) | O(a) | O(a) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Shortest-coins emphasis |
| PE Verdict | Bottom-up DP. | ||
Python Solutions
Recursion O(c^a) O(a)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
def rec(rem):
if rem == 0: return 0
if rem < 0: return float('inf')
return min((rec(rem - c) + 1 for c in coins), default=float('inf'))
r = rec(amount)
return -1 if r == float('inf') else r
Bottom-up DP O(a·n) O(a)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [amount + 1] * (amount + 1); dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if a - c >= 0: dp[a] = min(dp[a], dp[a-c] + 1)
return -1 if dp[amount] > amount else dp[amount]
BFS by amount O(a·n) O(a)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if amount == 0: return 0
seen = [False]*(amount + 1)
q = deque([0]); seen[0] = True; level = 0
while q:
level += 1
for _ in range(len(q)):
cur = q.popleft()
for c in coins:
nxt = cur + c
if nxt == amount: return level
if nxt < amount and not seen[nxt]:
seen[nxt] = True; q.append(nxt)
return -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(c^a) | O(a·n) | O(a·n) |
| Space | O(a) | O(a) | O(a) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | BFS framing |
| PE Verdict | Bottom-up DP. | ||
What the interviewer is really testing
Canonical unbounded knapsack.
Top gotchas
- Use amount+1 as 'infinity' sentinel — strictly larger than any valid count.
- amount=0 returns 0 (no coins needed).
- BFS works because each coin adds exactly 1 to the count.
Ship-it
Bottom-up DP.
#107 LC 152 Medium Maximum Product Subarray
Track min AND max ending at i — a negative flips them.
Problem Statement
Max product of any contiguous subarray.
Examples
Constraints
- 1 <= n <= 2·10^4
Approach Overview
Brute Force
Java: All subarrays
Python: All subarrays
Optimal #1
Java: Track min & max
Python: Track min & max
Optimal #2
Java: Two-direction sweep
Python: Two-direction sweep
Java Solutions
All subarrays O(n²) O(1)
O(n²) products.
Pseudo-code
for each l: prod=1; for r=l..: prod*=nums[r]; best=max
Complexity
Edge cases & gotchas
- Single element.
Java code
class Solution {
public int maxProduct(int[] nums) {
int best = Integer.MIN_VALUE;
for (int l = 0; l < nums.length; l++) {
int p = 1;
for (int r = l; r < nums.length; r++) { p *= nums[r]; best = Math.max(best, p); }
}
return best;
}
}
Track min & max O(n) O(1)
Negative number swaps min↔max.
Pseudo-code
maxEnd=minEnd=nums[0]; for x: if x<0 swap; maxEnd=max(x, maxEnd*x); minEnd=min(x, minEnd*x); best=max(best, maxEnd)
Complexity
Edge cases & gotchas
- Zeros reset to x itself.
Java code
class Solution {
public int maxProduct(int[] nums) {
int maxEnd = nums[0], minEnd = nums[0], best = nums[0];
for (int i = 1; i < nums.length; i++) {
int x = nums[i];
if (x < 0) { int t = maxEnd; maxEnd = minEnd; minEnd = t; }
maxEnd = Math.max(x, maxEnd * x);
minEnd = Math.min(x, minEnd * x);
best = Math.max(best, maxEnd);
}
return best;
}
}
Two-direction sweep O(n) O(1)
Forward & backward product sweep; reset on 0.
Pseudo-code
prefix product L→R then R→L, reset to 1 after 0; max over all
Complexity
Java code
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length, best = Integer.MIN_VALUE;
int p = 1;
for (int i = 0; i < n; i++) { p = p == 0 ? 1 : p; p *= nums[i]; best = Math.max(best, p); }
p = 1;
for (int i = n - 1; i >= 0; i--) { p = p == 0 ? 1 : p; p *= nums[i]; best = Math.max(best, p); }
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 4/5 | 3/5 |
| When | Don't | Default — classic | Pythonic alt |
| PE Verdict | Track min & max ending at i. | ||
Python Solutions
All subarrays O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxProduct(self, nums: List[int]) -> int:
best = nums[0]
for l in range(len(nums)):
p = 1
for r in range(l, len(nums)):
p *= nums[r]; best = max(best, p)
return best
Track min & max O(n) O(1)
Negative swaps.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxProduct(self, nums: List[int]) -> int:
maxE = minE = best = nums[0]
for x in nums[1:]:
if x < 0: maxE, minE = minE, maxE
maxE = max(x, maxE * x)
minE = min(x, minE * x)
best = max(best, maxE)
return best
Two-direction sweep O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxProduct(self, nums: List[int]) -> int:
best = nums[0]; p = 1
for x in nums:
p = 1 if p == 0 else p
p *= x; best = max(best, p)
p = 1
for x in reversed(nums):
p = 1 if p == 0 else p
p *= x; best = max(best, p)
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 4/5 | 3/5 |
| When | Don't | Default | Alt |
| PE Verdict | Track min & max. | ||
What the interviewer is really testing
Classic min/max DP trick.
Top gotchas
- Negative number → min becomes max, vice versa. SWAP before update.
- Always compare against x alone (reset on zero implicitly).
- Two-direction sweep with reset-on-zero is a slick alternative.
Ship-it
Track min and max ending at i.
#108 LC 139 Medium Word Break
DP — dp[i]=true if some j<i has dp[j] && s[j..i] in dict.
Problem Statement
Can s be split into a sequence of dictionary words?
Examples
Constraints
- 1 <= s.length <= 300
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: Bottom-up DP
Python: Bottom-up DP
Optimal #2
Java: Memoized recursion
Python: Memoized
Java Solutions
Recursion O(2^n) O(n)
Try every prefix split.
Pseudo-code
rec(start): if start==n return true; for end>start: if s[start..end] in dict and rec(end): return true
Complexity
Java code
import java.util.*;
class Solution {
public boolean wordBreak(String s, List<String> dict) {
Set<String> d = new HashSet<>(dict);
return rec(s, 0, d);
}
private boolean rec(String s, int start, Set<String> d) {
if (start == s.length()) return true;
for (int end = start + 1; end <= s.length(); end++)
if (d.contains(s.substring(start, end)) && rec(s, end, d)) return true;
return false;
}
}
Bottom-up DP O(n²·L) O(n)
dp[i]=true if any j<i has dp[j] && s[j..i] in dict.
Pseudo-code
dp[0]=true; for i=1..n: for j<i: if dp[j] && s[j..i] in dict: dp[i]=true; break
Complexity
Java code
import java.util.*;
class Solution {
public boolean wordBreak(String s, List<String> dict) {
Set<String> d = new HashSet<>(dict);
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 1; i <= n; i++) for (int j = 0; j < i; j++)
if (dp[j] && d.contains(s.substring(j, i))) { dp[i] = true; break; }
return dp[n];
}
}
Memoized recursion O(n²·L) O(n)
Top-down with cache.
Pseudo-code
memo[start]; rec
Complexity
Java code
import java.util.*;
class Solution {
private Boolean[] memo;
public boolean wordBreak(String s, List<String> dict) {
Set<String> d = new HashSet<>(dict);
memo = new Boolean[s.length()];
return rec(s, 0, d);
}
private boolean rec(String s, int start, Set<String> d) {
if (start == s.length()) return true;
if (memo[start] != null) return memo[start];
for (int end = start + 1; end <= s.length(); end++)
if (d.contains(s.substring(start, end)) && rec(s, end, d)) return memo[start] = true;
return memo[start] = false;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n²·L) | O(n²·L) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Bottom-up DP. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
d = set(wordDict)
def rec(start):
if start == len(s): return True
return any(s[start:end] in d and rec(end) for end in range(start+1, len(s)+1))
return rec(0)
Bottom-up DP O(n²·L) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
d = set(wordDict); n = len(s)
dp = [False]*(n+1); dp[0] = True
for i in range(1, n+1):
for j in range(i):
if dp[j] and s[j:i] in d: dp[i] = True; break
return dp[n]
Memoized O(n²·L) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
d = set(wordDict)
@lru_cache(None)
def rec(start):
if start == len(s): return True
return any(s[start:end] in d and rec(end) for end in range(start+1, len(s)+1))
return rec(0)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n²·L) | O(n²·L) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Bottom-up DP. | ||
What the interviewer is really testing
Classic 1-D DP — match prefixes against dict.
Top gotchas
- dp[0] = true (empty prefix).
- Hash the dict — O(1) lookups.
- Pruning: only check substrings up to max word length.
Ship-it
Bottom-up DP with set lookup.
#109 LC 300 Medium Longest Increasing Subsequence
Patience sorting / binary search — O(n log n).
Problem Statement
Length of longest strictly increasing subsequence.
Examples
Constraints
- 1 <= n <= 2500
Approach Overview
Brute Force
Java: Recursion: take/skip
Python: Recursion
Optimal #1
Java: Patience + binary search
Python: Patience
Optimal #2
Java: DP O(n²)
Python: DP O(n²)
Java Solutions
Recursion: take/skip O(2^n) O(n)
Try each element with/without prev constraint.
Pseudo-code
rec(i, prev): max(rec(i+1, prev), rec(i+1, i) + 1 if nums[i]>prev)
Complexity
Java code
class Solution {
public int lengthOfLIS(int[] nums) { return rec(nums, 0, Integer.MIN_VALUE); }
private int rec(int[] n, int i, int prev) {
if (i == n.length) return 0;
int skip = rec(n, i + 1, prev);
int take = n[i] > prev ? 1 + rec(n, i + 1, n[i]) : 0;
return Math.max(skip, take);
}
}
Patience + binary search O(n log n) O(n)
Maintain 'tails[]': smallest tail of each LIS length.
Pseudo-code
for x: idx=lower_bound(tails, x); if idx==len: append else tails[idx]=x
Complexity
Edge cases & gotchas
- Strictly increasing → use lower_bound (not upper).
Java code
import java.util.*;
class Solution {
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length]; int len = 0;
for (int x : nums) {
int lo = 0, hi = len;
while (lo < hi) { int m = (lo + hi) >>> 1; if (tails[m] < x) lo = m + 1; else hi = m; }
tails[lo] = x;
if (lo == len) len++;
}
return len;
}
}
DP O(n²) O(n²) O(n)
dp[i] = LIS ending at i.
Pseudo-code
dp[i]=1+max(dp[j] for j<i if nums[j]<nums[i])
Complexity
Java code
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length, best = 1;
int[] dp = new int[n];
java.util.Arrays.fill(dp, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
best = Math.max(best, dp[i]);
}
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n log n) | O(n²) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When | Don't | Default — best | Pedagogical |
| PE Verdict | Patience + binary search. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
def rec(i, prev):
if i == len(nums): return 0
skip = rec(i+1, prev)
take = 1 + rec(i+1, nums[i]) if nums[i] > prev else 0
return max(skip, take)
return rec(0, float('-inf'))
Patience O(n log n) O(n)
bisect_left.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from bisect import bisect_left
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails = []
for x in nums:
i = bisect_left(tails, x)
if i == len(tails): tails.append(x)
else: tails[i] = x
return len(tails)
DP O(n²) O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums); dp = [1]*n
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n log n) | O(n²) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | Patience + bisect. | ||
What the interviewer is really testing
Patience sorting trick — every L6 should know it.
Top gotchas
- tails[] is NOT the actual LIS — just smallest possible tail for each length.
- Strictly increasing → bisect_left; non-decreasing → bisect_right.
- O(n²) DP is easier to explain but slower.
Ship-it
Patience + binary search.
#110 LC 416 Medium Partition Equal Subset Sum
Subset-sum DP — can we reach sum/2 using subset of nums? (0/1 knapsack).
Problem Statement
Can nums be split into two subsets with equal sum?
Examples
Constraints
- 1 <= n <= 200
- 1 <= nums[i] <= 100
Approach Overview
Brute Force
Java: Recursion try take/skip
Python: Recursion
Optimal #1
Java: 1-D bitset/bool DP
Python: 1-D DP
Optimal #2
Java: BitSet (java.util.BitSet)
Python: BigInt as bitset
Java Solutions
Recursion try take/skip O(2^n) O(n)
Subset-sum search.
Pseudo-code
rec(i, target): take or skip; true if target==0
Complexity
Edge cases & gotchas
- odd total → false.
Java code
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0; for (int x : nums) sum += x;
if ((sum & 1) == 1) return false;
return rec(nums, 0, sum / 2);
}
private boolean rec(int[] n, int i, int t) {
if (t == 0) return true;
if (i == n.length || t < 0) return false;
return rec(n, i+1, t - n[i]) || rec(n, i+1, t);
}
}
1-D bitset/bool DP O(n·s) O(s)
dp[t] = can reach sum t; iterate right-to-left.
Pseudo-code
dp[0]=true; for x in nums: for t from sum/2 down to x: dp[t] |= dp[t-x]
Complexity
Java code
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0; for (int x : nums) sum += x;
if ((sum & 1) == 1) return false;
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int x : nums) for (int t = target; t >= x; t--) dp[t] |= dp[t - x];
return dp[target];
}
}
BitSet (java.util.BitSet) O(n·s/64) O(s/64)
Shift bitset by x each step.
Pseudo-code
bits=01; for x: bits = bits | (bits << x); return bits[target]
Complexity
Java code
import java.util.*;
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0; for (int x : nums) sum += x;
if ((sum & 1) == 1) return false;
int target = sum / 2;
BitSet bits = new BitSet(target + 1);
bits.set(0);
for (int x : nums) {
BitSet shifted = new BitSet(target + 1);
for (int i = bits.nextSetBit(0); i >= 0 && i + x <= target; i = bits.nextSetBit(i + 1)) shifted.set(i + x);
bits.or(shifted);
}
return bits.get(target);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n·s) | O(n·s/64) |
| Space | O(n) | O(s) | O(s/64) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Hot path / large s |
| PE Verdict | 1-D DP (right-to-left to avoid double count). | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2: return False
def rec(i, t):
if t == 0: return True
if i == len(nums) or t < 0: return False
return rec(i+1, t - nums[i]) or rec(i+1, t)
return rec(0, total // 2)
1-D DP O(n·s) O(s)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2: return False
target = total // 2
dp = [False]*(target + 1); dp[0] = True
for x in nums:
for t in range(target, x - 1, -1):
dp[t] = dp[t] or dp[t - x]
return dp[target]
BigInt as bitset O(n·s/64) O(s/64)
Python ints are arbitrary precision.
Pseudo-code
bits = 1; for x: bits |= bits << x; return (bits >> target) & 1
Complexity
Python code
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2: return False
target = total // 2
bits = 1
for x in nums: bits |= bits << x
return (bits >> target) & 1 == 1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n·s) | O(n·s/64) |
| Space | O(n) | O(s) | O(s/64) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Hot path |
| PE Verdict | 1-D DP. | ||
What the interviewer is really testing
Subset-sum is THE canonical 0/1 knapsack.
Top gotchas
- If total sum is ODD → return false immediately.
- Iterate t DOWN to x — otherwise you'd reuse same nums[i] multiple times.
- Python's arbitrary-precision int makes the bitset trick a one-liner.
Ship-it
1-D bool DP iterating right-to-left.
2-D Dynamic Programming
#111 LC 62 Medium Unique Paths
Lattice paths — dp[i][j] = dp[i-1][j] + dp[i][j-1].
Problem Statement
m×n grid, start top-left, end bottom-right. Only right or down moves. Count unique paths.
Examples
Constraints
- 1 <= m, n <= 100
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 1-D DP
Python: 1-D DP
Optimal #2
Java: Combinatorial C(m+n-2, m-1)
Python: Combinatorial
Java Solutions
Recursion O(2^(m+n)) O(m+n)
Explore all paths.
Pseudo-code
paths(i,j) = paths(i-1,j)+paths(i,j-1); base i==0 or j==0 → 1
Complexity
Edge cases & gotchas
- 1×1 → 1.
Java code
class Solution {
public int uniquePaths(int m, int n) {
if (m == 1 || n == 1) return 1;
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}
}
1-D DP O(m·n) O(n)
Roll one row.
Pseudo-code
dp = [1]*n; repeat m-1 times: for j=1..n-1: dp[j] += dp[j-1]
Complexity
Java code
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
java.util.Arrays.fill(dp, 1);
for (int i = 1; i < m; i++) for (int j = 1; j < n; j++) dp[j] += dp[j - 1];
return dp[n - 1];
}
}
Combinatorial C(m+n-2, m-1) O(min(m,n)) O(1)
Closed form.
Pseudo-code
answer = (m+n-2)! / ((m-1)!(n-1)!) computed iteratively
Complexity
Java code
class Solution {
public int uniquePaths(int m, int n) {
long ans = 1;
int k = Math.min(m - 1, n - 1), N = m + n - 2;
for (int i = 1; i <= k; i++) ans = ans * (N - k + i) / i;
return (int) ans;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(m+n)) | O(m·n) | O(min(m,n)) |
| Space | O(m+n) | O(n) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Math elegance |
| PE Verdict | 1-D rolling DP. | ||
Python Solutions
Recursion O(2^(m+n)) O(m+n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
if m == 1 or n == 1: return 1
return self.uniquePaths(m-1, n) + self.uniquePaths(m, n-1)
1-D DP O(m·n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1]*n
for _ in range(1, m):
for j in range(1, n): dp[j] += dp[j-1]
return dp[-1]
Combinatorial O(min(m,n)) O(1)
math.comb.
Pseudo-code
see Java
Complexity
Python code
from math import comb
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
return comb(m + n - 2, m - 1)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(m+n)) | O(m·n) | O(min(m,n)) |
| Space | O(m+n) | O(n) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Math |
| PE Verdict | 1-D rolling DP. | ||
What the interviewer is really testing
DP warmup; combinatorial form is the flex.
Top gotchas
- 1×n or m×1 → 1 path.
- Math identity: paths = C(m+n-2, m-1).
- 2-D is fine but 1-D is trivial since we only need prev row.
Ship-it
1-D rolling DP.
#112 LC 1143 Medium Longest Common Subsequence
Classic DP: match → 1+diag, else max(top, left).
Problem Statement
Length of longest common subsequence of two strings.
Examples
Constraints
- 1 <= n, m <= 1000
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 2-D DP
Python: 2-D DP
Optimal #2
Java: 1-D rolling DP
Python: 1-D rolling
Java Solutions
Recursion O(2^(n+m)) O(n+m)
Match or skip one side.
Pseudo-code
lcs(i,j): if either ==0: 0; if equal: 1+lcs(i-1,j-1); else max(lcs(i-1,j), lcs(i,j-1))
Complexity
Java code
class Solution {
public int longestCommonSubsequence(String a, String b) { return rec(a, b, a.length(), b.length()); }
private int rec(String a, String b, int i, int j) {
if (i == 0 || j == 0) return 0;
if (a.charAt(i-1) == b.charAt(j-1)) return 1 + rec(a, b, i-1, j-1);
return Math.max(rec(a, b, i-1, j), rec(a, b, i, j-1));
}
}
2-D DP O(n·m) O(n·m)
Bottom-up table.
Pseudo-code
dp[i][j] = a[i-1]==b[j-1] ? 1+dp[i-1][j-1] : max(dp[i-1][j], dp[i][j-1])
Complexity
Java code
class Solution {
public int longestCommonSubsequence(String a, String b) {
int n = a.length(), m = b.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++)
dp[i][j] = a.charAt(i-1) == b.charAt(j-1) ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]);
return dp[n][m];
}
}
1-D rolling DP O(n·m) O(min(n,m))
Two rows; keep diag in temp.
Pseudo-code
keep prev row, current row; equal: cur[j]=prev[j-1]+1; else max(prev[j], cur[j-1])
Complexity
Java code
class Solution {
public int longestCommonSubsequence(String a, String b) {
if (b.length() > a.length()) { String t = a; a = b; b = t; }
int n = a.length(), m = b.length();
int[] prev = new int[m + 1], cur = new int[m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
cur[j] = a.charAt(i-1) == b.charAt(j-1) ? prev[j-1] + 1 : Math.max(prev[j], cur[j-1]);
int[] t = prev; prev = cur; cur = t;
}
return prev[m];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(min(n,m)) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Memory tight |
| PE Verdict | 2-D bottom-up DP. | ||
Python Solutions
Recursion O(2^(n+m)) O(n+m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def longestCommonSubsequence(self, a: str, b: str) -> int:
def rec(i, j):
if i == 0 or j == 0: return 0
if a[i-1] == b[j-1]: return 1 + rec(i-1, j-1)
return max(rec(i-1, j), rec(i, j-1))
return rec(len(a), len(b))
2-D DP O(n·m) O(n·m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def longestCommonSubsequence(self, a: str, b: str) -> int:
n, m = len(a), len(b)
dp = [[0]*(m+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, m+1):
dp[i][j] = dp[i-1][j-1] + 1 if a[i-1] == b[j-1] else max(dp[i-1][j], dp[i][j-1])
return dp[n][m]
1-D rolling O(n·m) O(min(n,m))
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def longestCommonSubsequence(self, a: str, b: str) -> int:
if len(b) > len(a): a, b = b, a
n, m = len(a), len(b)
prev = [0]*(m+1); cur = [0]*(m+1)
for i in range(1, n+1):
for j in range(1, m+1):
cur[j] = prev[j-1] + 1 if a[i-1] == b[j-1] else max(prev[j], cur[j-1])
prev, cur = cur, prev
return prev[m]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(min(n,m)) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Tight memory |
| PE Verdict | 2-D DP. | ||
What the interviewer is really testing
Foundational 2-D DP.
Top gotchas
- dp dimensions are n+1 by m+1 (pad with zeros).
- Match: +1 + diagonal. No match: max of top/left.
- Roll to keep min of n,m on the inner axis.
Ship-it
2-D bottom-up DP.
#113 LC 309 Medium Best Time to Buy and Sell Stock With Cooldown
State machine — hold, sold, rest; transitions among the three.
Problem Statement
Max profit with unlimited transactions but 1-day cooldown after sell.
Examples
Constraints
- 1 <= n <= 5000
Approach Overview
Brute Force
Java: Recursion with state
Python: Recursion
Optimal #1
Java: 3-state DP O(1)
Python: 3-state DP
Optimal #2
Java: Memoized recursion
Python: Memoized
Java Solutions
Recursion with state O(2^n) O(n)
Each day: hold or not, can buy or not.
Pseudo-code
rec(i, holding): max(skip, buy/sell)
Complexity
Java code
class Solution {
public int maxProfit(int[] p) { return rec(p, 0, false); }
private int rec(int[] p, int i, boolean hold) {
if (i >= p.length) return 0;
int skip = rec(p, i + 1, hold);
int act = hold ? p[i] + rec(p, i + 2, false) : -p[i] + rec(p, i + 1, true);
return Math.max(skip, act);
}
}
3-state DP O(1) O(n) O(1)
hold, sold, rest as rolling vars.
Pseudo-code
hold = max(hold, rest - p); sold = hold + p; rest = max(rest, sold_prev)
Complexity
Java code
class Solution {
public int maxProfit(int[] p) {
int hold = Integer.MIN_VALUE, sold = 0, rest = 0;
for (int x : p) {
int prevSold = sold;
sold = hold + x;
hold = Math.max(hold, rest - x);
rest = Math.max(rest, prevSold);
}
return Math.max(sold, rest);
}
}
Memoized recursion O(n) O(n)
Top-down with cache.
Pseudo-code
memo[(i, holding)]
Complexity
Java code
class Solution {
private Integer[][] memo;
public int maxProfit(int[] p) {
memo = new Integer[p.length][2];
return rec(p, 0, 0);
}
private int rec(int[] p, int i, int hold) {
if (i >= p.length) return 0;
if (memo[i][hold] != null) return memo[i][hold];
int skip = rec(p, i + 1, hold);
int act = hold == 1 ? p[i] + rec(p, i + 2, 0) : -p[i] + rec(p, i + 1, 1);
return memo[i][hold] = Math.max(skip, act);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | 3-state rolling DP. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxProfit(self, p: List[int]) -> int:
def rec(i, hold):
if i >= len(p): return 0
skip = rec(i+1, hold)
act = p[i] + rec(i+2, False) if hold else -p[i] + rec(i+1, True)
return max(skip, act)
return rec(0, False)
3-state DP O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxProfit(self, p: List[int]) -> int:
hold, sold, rest = float('-inf'), 0, 0
for x in p:
prev_sold = sold
sold = hold + x
hold = max(hold, rest - x)
rest = max(rest, prev_sold)
return max(sold, rest)
Memoized O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def maxProfit(self, p: List[int]) -> int:
@lru_cache(None)
def rec(i, hold):
if i >= len(p): return 0
skip = rec(i+1, hold)
act = p[i] + rec(i+2, False) if hold else -p[i] + rec(i+1, True)
return max(skip, act)
return rec(0, False)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | 3-state rolling DP. | ||
What the interviewer is really testing
Stock series with cooldown — state machine model.
Top gotchas
- Three states: hold (own), sold (just sold today), rest (not holding, can buy).
- Use PREV sold to compute new rest — order matters.
- Answer is max(sold, rest) at the end.
Ship-it
3-state rolling DP.
#114 LC 518 Medium Coin Change II
Unbounded knapsack — # ways to make amount; iterate coins OUTER (avoid permutations).
Problem Statement
Return number of combinations summing to amount (each coin unlimited).
Examples
Constraints
- 1 <= coins.length <= 300
- 0 <= amount <= 5000
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 1-D DP, coins outer
Python: 1-D DP
Optimal #2
Java: 2-D DP
Python: 2-D DP
Java Solutions
Recursion O(2^(a+n)) O(a+n)
Try take coin (stay) or move on.
Pseudo-code
rec(i, rem): rec(i+1, rem) + rec(i, rem - coins[i]) if rem>=coins[i]
Complexity
Edge cases & gotchas
- amount=0 → 1.
Java code
class Solution {
public int change(int amount, int[] coins) { return rec(coins, 0, amount); }
private int rec(int[] c, int i, int rem) {
if (rem == 0) return 1;
if (i == c.length || rem < 0) return 0;
return rec(c, i + 1, rem) + rec(c, i, rem - c[i]);
}
}
1-D DP, coins outer O(a·n) O(a)
dp[rem] += dp[rem - coin] iterating coins outside.
Pseudo-code
dp[0]=1; for c in coins: for r in c..amount: dp[r] += dp[r-c]
Complexity
Edge cases & gotchas
- Order matters — coins outer avoids permutation overcount.
Java code
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int c : coins) for (int r = c; r <= amount; r++) dp[r] += dp[r - c];
return dp[amount];
}
}
2-D DP O(a·n) O(a·n)
dp[i][r] = # ways using first i coins.
Pseudo-code
dp[i][r] = dp[i-1][r] + dp[i][r - coins[i-1]]
Complexity
Java code
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++) for (int r = 0; r <= amount; r++) {
dp[i][r] = dp[i-1][r];
if (r >= coins[i-1]) dp[i][r] += dp[i][r - coins[i-1]];
}
return dp[n][amount];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(a+n)) | O(a·n) | O(a·n) |
| Space | O(a+n) | O(a) | O(a·n) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | 1-D DP with coins outer. | ||
Python Solutions
Recursion O(2^(a+n)) O(a+n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
def rec(i, rem):
if rem == 0: return 1
if i == len(coins) or rem < 0: return 0
return rec(i+1, rem) + rec(i, rem - coins[i])
return rec(0, amount)
1-D DP O(a·n) O(a)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0]*(amount + 1); dp[0] = 1
for c in coins:
for r in range(c, amount + 1): dp[r] += dp[r - c]
return dp[amount]
2-D DP O(a·n) O(a·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [[0]*(amount+1) for _ in range(n+1)]
for i in range(n+1): dp[i][0] = 1
for i in range(1, n+1):
for r in range(amount + 1):
dp[i][r] = dp[i-1][r]
if r >= coins[i-1]: dp[i][r] += dp[i][r - coins[i-1]]
return dp[n][amount]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(a+n)) | O(a·n) | O(a·n) |
| Space | O(a+n) | O(a) | O(a·n) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | 1-D DP, coins outer. | ||
What the interviewer is really testing
Subtle distinction: combinations vs permutations.
Top gotchas
- COINS LOOP MUST BE OUTSIDE the amount loop. Reversing causes permutation overcount.
- dp[0] = 1: one way to make 0 (use no coins).
- Unbounded knapsack: iterate amounts in forward direction.
Ship-it
1-D DP, coins outer.
#115 LC 494 Medium Target Sum
Subset sum reduction — count subsets with sum = (S+target)/2.
Problem Statement
Each nums[i] gets +/-; count expressions equaling target.
Examples
Constraints
- 1 <= n <= 20
- 0 <= sum <= 1000
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: Subset-sum DP
Python: Subset-sum DP
Optimal #2
Java: Memoized recursion (offset sum)
Python: Memoized
Java Solutions
Recursion O(2^n) O(n)
Try +/- at each index.
Pseudo-code
rec(i, sum): rec(i+1, sum+nums[i]) + rec(i+1, sum-nums[i])
Complexity
Java code
class Solution {
public int findTargetSumWays(int[] nums, int target) { return rec(nums, 0, 0, target); }
private int rec(int[] n, int i, int sum, int target) {
if (i == n.length) return sum == target ? 1 : 0;
return rec(n, i+1, sum + n[i], target) + rec(n, i+1, sum - n[i], target);
}
}
Subset-sum DP O(n·S) O(S)
Find subset with sum P where P-(S-P)=target → P=(S+target)/2.
Pseudo-code
if (S+target) odd or |target|>S: 0; else count subsets summing to (S+target)/2
Complexity
Edge cases & gotchas
- |target|>S → 0.
Java code
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0; for (int x : nums) sum += x;
if (Math.abs(target) > sum || ((sum + target) & 1) != 0) return 0;
int p = (sum + target) / 2;
int[] dp = new int[p + 1]; dp[0] = 1;
for (int x : nums) for (int t = p; t >= x; t--) dp[t] += dp[t - x];
return dp[p];
}
}
Memoized recursion (offset sum) O(n·S) O(n·S)
Top-down with (i, sum) memo.
Pseudo-code
map (i, runningSum) → count
Complexity
Java code
import java.util.*;
class Solution {
private Map<Long, Integer> memo;
public int findTargetSumWays(int[] nums, int target) { memo = new HashMap<>(); return rec(nums, 0, 0, target); }
private int rec(int[] n, int i, int sum, int target) {
if (i == n.length) return sum == target ? 1 : 0;
long key = (long) i * 10001 + sum + 5000;
if (memo.containsKey(key)) return memo.get(key);
int r = rec(n, i+1, sum + n[i], target) + rec(n, i+1, sum - n[i], target);
memo.put(key, r);
return r;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n·S) | O(n·S) |
| Space | O(n) | O(S) | O(n·S) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default — math trick | Top-down |
| PE Verdict | Reduce to subset-sum DP. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
def rec(i, s):
if i == len(nums): return 1 if s == target else 0
return rec(i+1, s + nums[i]) + rec(i+1, s - nums[i])
return rec(0, 0)
Subset-sum DP O(n·S) O(S)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if abs(target) > total or (total + target) % 2: return 0
p = (total + target) // 2
dp = [0]*(p + 1); dp[0] = 1
for x in nums:
for t in range(p, x - 1, -1): dp[t] += dp[t - x]
return dp[p]
Memoized O(n·S) O(n·S)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
@lru_cache(None)
def rec(i, s):
if i == len(nums): return 1 if s == target else 0
return rec(i+1, s + nums[i]) + rec(i+1, s - nums[i])
return rec(0, 0)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n·S) | O(n·S) |
| Space | O(n) | O(S) | O(n·S) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Subset-sum reduction. | ||
What the interviewer is really testing
The math reduction P=(S+t)/2 is what makes this interesting.
Top gotchas
- If (S+target) is odd → 0 ways.
- |target| > S → 0 ways.
- Standard 0/1 knapsack: iterate t DOWN.
Ship-it
Subset-sum DP.
#116 LC 97 Medium Interleaving String
2-D DP — dp[i][j] = can s3[0..i+j] be formed from s1[0..i] + s2[0..j]?
Problem Statement
Return true if s3 is an interleaving of s1 and s2.
Examples
Constraints
- 0 <= len(s1), len(s2) <= 100
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 2-D DP
Python: 2-D DP
Optimal #2
Java: 1-D rolling DP
Python: 1-D rolling
Java Solutions
Recursion O(2^(n+m)) O(n+m)
At each step try consuming from s1 or s2.
Pseudo-code
rec(i,j): k=i+j; (s1[i]==s3[k] && rec(i+1,j)) || (s2[j]==s3[k] && rec(i,j+1))
Complexity
Edge cases & gotchas
- len mismatch → false.
Java code
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) return false;
return rec(s1, s2, s3, 0, 0);
}
private boolean rec(String a, String b, String c, int i, int j) {
int k = i + j;
if (k == c.length()) return true;
boolean ok = false;
if (i < a.length() && a.charAt(i) == c.charAt(k)) ok = rec(a, b, c, i+1, j);
if (!ok && j < b.length() && b.charAt(j) == c.charAt(k)) ok = rec(a, b, c, i, j+1);
return ok;
}
}
2-D DP O(n·m) O(n·m)
dp[i][j] = can interleave prefix of s1 (i chars) and s2 (j chars).
Pseudo-code
dp[i][j] = (dp[i-1][j] && s1[i-1]==s3[i+j-1]) || (dp[i][j-1] && s2[j-1]==s3[i+j-1])
Complexity
Java code
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int n = s1.length(), m = s2.length();
if (n + m != s3.length()) return false;
boolean[][] dp = new boolean[n + 1][m + 1];
dp[0][0] = true;
for (int i = 1; i <= n; i++) dp[i][0] = dp[i-1][0] && s1.charAt(i-1) == s3.charAt(i-1);
for (int j = 1; j <= m; j++) dp[0][j] = dp[0][j-1] && s2.charAt(j-1) == s3.charAt(j-1);
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++)
dp[i][j] = (dp[i-1][j] && s1.charAt(i-1) == s3.charAt(i+j-1))
|| (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(i+j-1));
return dp[n][m];
}
}
1-D rolling DP O(n·m) O(min(n,m))
Keep one row.
Pseudo-code
row[j] = (row[j] && s1[i-1]==s3[i+j-1]) || (row[j-1] && s2[j-1]==s3[i+j-1])
Complexity
Java code
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int n = s1.length(), m = s2.length();
if (n + m != s3.length()) return false;
if (m < n) { String t = s1; s1 = s2; s2 = t; int u = n; n = m; m = u; }
boolean[] dp = new boolean[m + 1];
dp[0] = true;
for (int j = 1; j <= m; j++) dp[j] = dp[j-1] && s2.charAt(j-1) == s3.charAt(j-1);
for (int i = 1; i <= n; i++) {
dp[0] = dp[0] && s1.charAt(i-1) == s3.charAt(i-1);
for (int j = 1; j <= m; j++)
dp[j] = (dp[j] && s1.charAt(i-1) == s3.charAt(i+j-1))
|| (dp[j-1] && s2.charAt(j-1) == s3.charAt(i+j-1));
}
return dp[m];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(min(n,m)) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Tight memory |
| PE Verdict | 2-D DP. | ||
Python Solutions
Recursion O(2^(n+m)) O(n+m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
if len(s1) + len(s2) != len(s3): return False
def rec(i, j):
k = i + j
if k == len(s3): return True
if i < len(s1) and s1[i] == s3[k] and rec(i+1, j): return True
if j < len(s2) and s2[j] == s3[k] and rec(i, j+1): return True
return False
return rec(0, 0)
2-D DP O(n·m) O(n·m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
n, m = len(s1), len(s2)
if n + m != len(s3): return False
dp = [[False]*(m+1) for _ in range(n+1)]
dp[0][0] = True
for i in range(1, n+1): dp[i][0] = dp[i-1][0] and s1[i-1] == s3[i-1]
for j in range(1, m+1): dp[0][j] = dp[0][j-1] and s2[j-1] == s3[j-1]
for i in range(1, n+1):
for j in range(1, m+1):
dp[i][j] = (dp[i-1][j] and s1[i-1] == s3[i+j-1]) or (dp[i][j-1] and s2[j-1] == s3[i+j-1])
return dp[n][m]
1-D rolling O(n·m) O(min(n,m))
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
if len(s1) + len(s2) != len(s3): return False
if len(s2) < len(s1): s1, s2 = s2, s1
n, m = len(s1), len(s2)
dp = [False]*(m+1); dp[0] = True
for j in range(1, m+1): dp[j] = dp[j-1] and s2[j-1] == s3[j-1]
for i in range(1, n+1):
dp[0] = dp[0] and s1[i-1] == s3[i-1]
for j in range(1, m+1):
dp[j] = (dp[j] and s1[i-1] == s3[i+j-1]) or (dp[j-1] and s2[j-1] == s3[i+j-1])
return dp[m]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(min(n,m)) |
| Difficulty | 3/5 | 3/5 | 4/5 |
| When | Don't | Default | Tight memory |
| PE Verdict | 2-D DP. | ||
What the interviewer is really testing
2-D state from two strings.
Top gotchas
- Length must match: n+m == len(s3).
- Two choices at each cell: take from s1 OR s2 if char matches.
- Roll to 1-D if memory matters.
Ship-it
2-D DP.
#117 LC 329 Hard Longest Increasing Path in a Matrix
DFS + memoization — implicit DAG (edges only point upward in value).
Problem Statement
Length of longest strictly increasing path in matrix.
Examples
Constraints
- 1 <= m, n <= 200
Approach Overview
Brute Force
Java: DFS without memo
Python: DFS no memo
Optimal #1
Java: DFS + memo
Python: DFS + memo
Optimal #2
Java: Topological sort by indegree
Python: Topo sort
Java Solutions
DFS without memo O(4^(m·n)) O(m·n)
Exponential paths.
Pseudo-code
dfs(i,j): max(1 + dfs(nb)) for nb with greater value
Complexity
Java code
class Solution {
public int longestIncreasingPath(int[][] m) {
int best = 0;
for (int i = 0; i < m.length; i++) for (int j = 0; j < m[0].length; j++)
best = Math.max(best, dfs(m, i, j, Integer.MIN_VALUE));
return best;
}
private int dfs(int[][] m, int i, int j, int prev) {
if (i<0||j<0||i>=m.length||j>=m[0].length||m[i][j] <= prev) return 0;
int v = m[i][j];
return 1 + Math.max(Math.max(dfs(m,i+1,j,v), dfs(m,i-1,j,v)), Math.max(dfs(m,i,j+1,v), dfs(m,i,j-1,v)));
}
}
DFS + memo O(m·n) O(m·n)
Cache longest path starting at each cell.
Pseudo-code
dfs(i,j): if memo set return; else compute 1+max(neighbors strictly greater); store; return
Complexity
Java code
class Solution {
private int[][] memo;
public int longestIncreasingPath(int[][] m) {
memo = new int[m.length][m[0].length];
int best = 0;
for (int i = 0; i < m.length; i++) for (int j = 0; j < m[0].length; j++)
best = Math.max(best, dfs(m, i, j));
return best;
}
private int dfs(int[][] m, int i, int j) {
if (memo[i][j] != 0) return memo[i][j];
int best = 1;
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<m.length&&nj<m[0].length&&m[ni][nj]>m[i][j])
best = Math.max(best, 1 + dfs(m, ni, nj));
}
return memo[i][j] = best;
}
}
Topological sort by indegree O(m·n) O(m·n)
Edges from smaller→greater; Kahn's BFS layers = longest path.
Pseudo-code
indeg = # smaller neighbors; enqueue 0s; layer-by-layer; max layer
Complexity
Java code
import java.util.*;
class Solution {
public int longestIncreasingPath(int[][] m) {
int M = m.length, N = m[0].length;
int[][] indeg = new int[M][N];
int[][] D = {{-1,0},{1,0},{0,-1},{0,1}};
for (int i = 0; i < M; i++) for (int j = 0; j < N; j++) for (int[] d : D) {
int ni = i+d[0], nj = j+d[1];
if (ni>=0&&nj>=0&&ni<M&&nj<N&&m[ni][nj]<m[i][j]) indeg[i][j]++;
}
Deque<int[]> q = new ArrayDeque<>();
for (int i = 0; i < M; i++) for (int j = 0; j < N; j++) if (indeg[i][j] == 0) q.offer(new int[]{i,j});
int layers = 0;
while (!q.isEmpty()) {
layers++;
int sz = q.size();
for (int k = 0; k < sz; k++) {
int[] p = q.poll();
for (int[] d : D) {
int ni = p[0]+d[0], nj = p[1]+d[1];
if (ni>=0&&nj>=0&&ni<M&&nj<N&&m[ni][nj]>m[p[0]][p[1]] && --indeg[ni][nj] == 0) q.offer(new int[]{ni,nj});
}
}
}
return layers;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(4^(mn)) | O(m·n) | O(m·n) |
| Space | O(m·n) | O(m·n) | O(m·n) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| When | Don't | Default | Iterative |
| PE Verdict | DFS + memo. | ||
Python Solutions
DFS no memo O(4^(mn)) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def longestIncreasingPath(self, m: List[List[int]]) -> int:
M, N = len(m), len(m[0])
def dfs(i, j, prev):
if not (0 <= i < M and 0 <= j < N) or m[i][j] <= prev: return 0
v = m[i][j]
return 1 + max(dfs(i+1,j,v), dfs(i-1,j,v), dfs(i,j+1,v), dfs(i,j-1,v))
return max(dfs(i,j,float('-inf')) for i in range(M) for j in range(N))
DFS + memo O(m·n) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def longestIncreasingPath(self, m: List[List[int]]) -> int:
M, N = len(m), len(m[0])
@lru_cache(None)
def dfs(i, j):
best = 1
for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+di, j+dj
if 0<=ni<M and 0<=nj<N and m[ni][nj] > m[i][j]:
best = max(best, 1 + dfs(ni, nj))
return best
return max(dfs(i, j) for i in range(M) for j in range(N))
Topo sort O(m·n) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def longestIncreasingPath(self, m: List[List[int]]) -> int:
M, N = len(m), len(m[0])
indeg = [[0]*N for _ in range(M)]
D = [(-1,0),(1,0),(0,-1),(0,1)]
for i in range(M):
for j in range(N):
for di, dj in D:
ni, nj = i+di, j+dj
if 0<=ni<M and 0<=nj<N and m[ni][nj] < m[i][j]: indeg[i][j] += 1
q = deque((i,j) for i in range(M) for j in range(N) if indeg[i][j] == 0)
layers = 0
while q:
layers += 1
for _ in range(len(q)):
i, j = q.popleft()
for di, dj in D:
ni, nj = i+di, j+dj
if 0<=ni<M and 0<=nj<N and m[ni][nj] > m[i][j]:
indeg[ni][nj] -= 1
if indeg[ni][nj] == 0: q.append((ni, nj))
return layers
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(4^(mn)) | O(m·n) | O(m·n) |
| Space | O(m·n) | O(m·n) | O(m·n) |
| Difficulty | 2/5 | 4/5 | 4/5 |
| When | Don't | Default | Iterative |
| PE Verdict | DFS + memo. | ||
What the interviewer is really testing
Grid as implicit DAG (no cycles since strictly greater).
Top gotchas
- No visited set needed — strict monotonicity prevents cycles.
- Memoize STARTING longest path per cell.
- Topo variant counts layers = longest path length.
Ship-it
DFS + memo.
#118 LC 115 Hard Distinct Subsequences
2-D DP — match: take + skip; mismatch: skip only.
Problem Statement
Count distinct subsequences of s that equal t.
Examples
Constraints
- 1 <= len(s), len(t) <= 1000
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 2-D DP
Python: 2-D DP
Optimal #2
Java: 1-D rolling DP
Python: 1-D rolling
Java Solutions
Recursion O(2^n) O(n)
Try match or skip s[i].
Pseudo-code
rec(i,j): if j==len(t) return 1; if i==len(s) return 0; res = rec(i+1,j); if equal: res += rec(i+1,j+1)
Complexity
Edge cases & gotchas
- t empty → 1.
Java code
class Solution {
public int numDistinct(String s, String t) { return rec(s, t, 0, 0); }
private int rec(String s, String t, int i, int j) {
if (j == t.length()) return 1;
if (i == s.length()) return 0;
int r = rec(s, t, i+1, j);
if (s.charAt(i) == t.charAt(j)) r += rec(s, t, i+1, j+1);
return r;
}
}
2-D DP O(n·m) O(n·m)
dp[i][j] = # ways to form t[j..] from s[i..].
Pseudo-code
fill bottom-up; equal: dp[i][j]=dp[i+1][j+1]+dp[i+1][j]; else dp[i+1][j]
Complexity
Java code
class Solution {
public int numDistinct(String s, String t) {
int n = s.length(), m = t.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) dp[i][m] = 1;
for (int i = n - 1; i >= 0; i--) for (int j = m - 1; j >= 0; j--) {
dp[i][j] = dp[i+1][j];
if (s.charAt(i) == t.charAt(j)) dp[i][j] += dp[i+1][j+1];
}
return dp[0][0];
}
}
1-D rolling DP O(n·m) O(m)
Iterate t right-to-left to avoid overwrite.
Pseudo-code
dp[m]=1; for each s[i]: for j=0..m-1: if s[i]==t[j]: dp[j] += dp[j+1]
Complexity
Java code
class Solution {
public int numDistinct(String s, String t) {
int n = s.length(), m = t.length();
int[] dp = new int[m + 1]; dp[m] = 1;
for (int i = n - 1; i >= 0; i--)
for (int j = 0; j < m; j++)
if (s.charAt(i) == t.charAt(j)) dp[j] += dp[j+1];
return dp[0];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n·m) | O(n·m) |
| Space | O(n) | O(n·m) | O(m) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Memory tight |
| PE Verdict | 2-D DP bottom-up. | ||
Python Solutions
Recursion O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def numDistinct(self, s: str, t: str) -> int:
def rec(i, j):
if j == len(t): return 1
if i == len(s): return 0
r = rec(i+1, j)
if s[i] == t[j]: r += rec(i+1, j+1)
return r
return rec(0, 0)
2-D DP O(n·m) O(n·m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def numDistinct(self, s: str, t: str) -> int:
n, m = len(s), len(t)
dp = [[0]*(m+1) for _ in range(n+1)]
for i in range(n+1): dp[i][m] = 1
for i in range(n-1, -1, -1):
for j in range(m-1, -1, -1):
dp[i][j] = dp[i+1][j]
if s[i] == t[j]: dp[i][j] += dp[i+1][j+1]
return dp[0][0]
1-D rolling O(n·m) O(m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def numDistinct(self, s: str, t: str) -> int:
n, m = len(s), len(t)
dp = [0]*(m+1); dp[m] = 1
for i in range(n-1, -1, -1):
for j in range(m):
if s[i] == t[j]: dp[j] += dp[j+1]
return dp[0]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n·m) | O(n·m) |
| Space | O(n) | O(n·m) | O(m) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Tight |
| PE Verdict | 2-D DP. | ||
What the interviewer is really testing
Subsequence DP — careful with 'take' decision.
Top gotchas
- Mismatch: only the skip branch counts. Match: skip + take.
- Base: dp[*][m] = 1 — empty t is a subsequence.
- Roll right-to-left over j to keep 1-D correct.
Ship-it
2-D DP.
#119 LC 72 Medium Edit Distance
Levenshtein DP — match: diag; else 1 + min(insert, delete, replace).
Problem Statement
Min operations (insert/delete/replace) to transform word1 to word2.
Examples
Constraints
- 0 <= n, m <= 500
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 2-D DP
Python: 2-D DP
Optimal #2
Java: 1-D rolling
Python: 1-D rolling
Java Solutions
Recursion O(3^(n+m)) O(n+m)
Try each op.
Pseudo-code
rec(i,j): if equal: rec(i+1,j+1) else 1+min(insert,delete,replace)
Complexity
Edge cases & gotchas
- Either empty → length of other.
Java code
class Solution {
public int minDistance(String a, String b) { return rec(a, b, 0, 0); }
private int rec(String a, String b, int i, int j) {
if (i == a.length()) return b.length() - j;
if (j == b.length()) return a.length() - i;
if (a.charAt(i) == b.charAt(j)) return rec(a, b, i+1, j+1);
int ins = rec(a, b, i, j+1);
int del = rec(a, b, i+1, j);
int rep = rec(a, b, i+1, j+1);
return 1 + Math.min(ins, Math.min(del, rep));
}
}
2-D DP O(n·m) O(n·m)
Bottom-up table.
Pseudo-code
dp[i][j] = min cost transforming a[0..i] to b[0..j]
Complexity
Java code
class Solution {
public int minDistance(String a, String b) {
int n = a.length(), m = b.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int j = 0; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) {
if (a.charAt(i-1) == b.charAt(j-1)) dp[i][j] = dp[i-1][j-1];
else dp[i][j] = 1 + Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]));
}
return dp[n][m];
}
}
1-D rolling O(n·m) O(min(n,m))
Keep one row.
Pseudo-code
keep prev row; track diag in temp
Complexity
Java code
class Solution {
public int minDistance(String a, String b) {
if (a.length() < b.length()) { String t = a; a = b; b = t; }
int n = a.length(), m = b.length();
int[] dp = new int[m + 1];
for (int j = 0; j <= m; j++) dp[j] = j;
for (int i = 1; i <= n; i++) {
int prev = dp[0]; dp[0] = i;
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (a.charAt(i-1) == b.charAt(j-1)) dp[j] = prev;
else dp[j] = 1 + Math.min(prev, Math.min(dp[j], dp[j-1]));
prev = temp;
}
}
return dp[m];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(3^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(min(n,m)) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Tight memory |
| PE Verdict | 2-D DP. | ||
Python Solutions
Recursion O(3^(n+m)) O(n+m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def minDistance(self, a: str, b: str) -> int:
def rec(i, j):
if i == len(a): return len(b) - j
if j == len(b): return len(a) - i
if a[i] == b[j]: return rec(i+1, j+1)
return 1 + min(rec(i, j+1), rec(i+1, j), rec(i+1, j+1))
return rec(0, 0)
2-D DP O(n·m) O(n·m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def minDistance(self, a: str, b: str) -> int:
n, m = len(a), len(b)
dp = [[0]*(m+1) for _ in range(n+1)]
for i in range(n+1): dp[i][0] = i
for j in range(m+1): dp[0][j] = j
for i in range(1, n+1):
for j in range(1, m+1):
if a[i-1] == b[j-1]: dp[i][j] = dp[i-1][j-1]
else: dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])
return dp[n][m]
1-D rolling O(n·m) O(min(n,m))
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def minDistance(self, a: str, b: str) -> int:
if len(a) < len(b): a, b = b, a
n, m = len(a), len(b)
dp = list(range(m+1))
for i in range(1, n+1):
prev = dp[0]; dp[0] = i
for j in range(1, m+1):
temp = dp[j]
if a[i-1] == b[j-1]: dp[j] = prev
else: dp[j] = 1 + min(prev, dp[j], dp[j-1])
prev = temp
return dp[m]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(3^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(min(n,m)) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Tight |
| PE Verdict | 2-D DP. | ||
What the interviewer is really testing
Classic Levenshtein DP.
Top gotchas
- 3 operations → 3 transitions: dp[i-1][j-1] (replace), dp[i-1][j] (delete), dp[i][j-1] (insert).
- Base: empty string → length of other.
- Equal chars → free diagonal.
Ship-it
2-D DP.
#120 LC 312 Hard Burst Balloons
Interval DP — pick LAST balloon to burst in each subinterval.
Problem Statement
Burst balloon i gets nums[l]·nums[i]·nums[r] coins (l,r = nearest unburst neighbors). Max total coins.
Examples
Constraints
- 1 <= n <= 300
Approach Overview
Brute Force
Java: Recursion try each next
Python: Recursion
Optimal #1
Java: Interval DP — last burst
Python: Interval DP
Optimal #2
Java: Memoized recursion
Python: Memoized
Java Solutions
Recursion try each next O(n!) O(n)
Pick which to burst next; recurse.
Pseudo-code
for each balloon: burst, recurse on remaining
Complexity
Java code
import java.util.*;
class Solution {
public int maxCoins(int[] nums) {
List<Integer> arr = new ArrayList<>();
arr.add(1);
for (int x : nums) arr.add(x);
arr.add(1);
return rec(arr);
}
private int rec(List<Integer> arr) {
if (arr.size() == 2) return 0;
int best = 0;
for (int i = 1; i < arr.size() - 1; i++) {
int coins = arr.get(i-1) * arr.get(i) * arr.get(i+1);
int v = arr.remove(i);
best = Math.max(best, coins + rec(arr));
arr.add(i, v);
}
return best;
}
}
Interval DP — last burst O(n³) O(n²)
dp[l][r] = pick last k in (l,r); coins = nums[l]·nums[k]·nums[r] + dp[l][k] + dp[k][r].
Pseudo-code
pad with 1s; for length=2..n+1: for l: r=l+len; for k=l+1..r-1: dp[l][r]=max(dp[l][r], nums[l]*nums[k]*nums[r] + dp[l][k] + dp[k][r])
Complexity
Edge cases & gotchas
- n=1 → nums[0].
Java code
class Solution {
public int maxCoins(int[] nums) {
int n = nums.length;
int[] arr = new int[n + 2];
arr[0] = arr[n + 1] = 1;
for (int i = 0; i < n; i++) arr[i + 1] = nums[i];
int[][] dp = new int[n + 2][n + 2];
for (int len = 2; len <= n + 1; len++) {
for (int l = 0; l + len <= n + 1; l++) {
int r = l + len;
for (int k = l + 1; k < r; k++)
dp[l][r] = Math.max(dp[l][r], arr[l] * arr[k] * arr[r] + dp[l][k] + dp[k][r]);
}
}
return dp[0][n + 1];
}
}
Memoized recursion O(n³) O(n²)
Top-down same idea.
Pseudo-code
rec(l, r): for k in (l, r): max(arr[l]*arr[k]*arr[r] + rec(l,k) + rec(k,r))
Complexity
Java code
class Solution {
private int[] arr;
private int[][] memo;
public int maxCoins(int[] nums) {
int n = nums.length;
arr = new int[n + 2]; arr[0] = arr[n + 1] = 1;
for (int i = 0; i < n; i++) arr[i + 1] = nums[i];
memo = new int[n + 2][n + 2];
return rec(0, n + 1);
}
private int rec(int l, int r) {
if (r - l < 2) return 0;
if (memo[l][r] != 0) return memo[l][r];
int best = 0;
for (int k = l + 1; k < r; k++)
best = Math.max(best, arr[l] * arr[k] * arr[r] + rec(l, k) + rec(k, r));
return memo[l][r] = best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n!) | O(n³) | O(n³) |
| Space | O(n) | O(n²) | O(n²) |
| Difficulty | 3/5 | 5/5 | 4/5 |
| When | Don't | Default | Top-down clarity |
| PE Verdict | Interval DP, last-burst trick. | ||
Python Solutions
Recursion O(n!) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxCoins(self, nums: List[int]) -> int:
arr = [1] + nums + [1]
def rec(a):
if len(a) == 2: return 0
best = 0
for i in range(1, len(a) - 1):
coins = a[i-1] * a[i] * a[i+1]
best = max(best, coins + rec(a[:i] + a[i+1:]))
return best
return rec(arr)
Interval DP O(n³) O(n²)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxCoins(self, nums: List[int]) -> int:
arr = [1] + nums + [1]
n = len(arr)
dp = [[0]*n for _ in range(n)]
for length in range(2, n):
for l in range(n - length):
r = l + length
for k in range(l + 1, r):
dp[l][r] = max(dp[l][r], arr[l]*arr[k]*arr[r] + dp[l][k] + dp[k][r])
return dp[0][n-1]
Memoized O(n³) O(n²)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import lru_cache
class Solution:
def maxCoins(self, nums: List[int]) -> int:
arr = [1] + nums + [1]
@lru_cache(None)
def rec(l, r):
if r - l < 2: return 0
return max(arr[l]*arr[k]*arr[r] + rec(l, k) + rec(k, r) for k in range(l+1, r))
return rec(0, len(arr) - 1)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n!) | O(n³) | O(n³) |
| Space | O(n) | O(n²) | O(n²) |
| Difficulty | 3/5 | 5/5 | 4/5 |
| When | Don't | Default | Top-down |
| PE Verdict | Interval DP. | ||
What the interviewer is really testing
Classic interval DP — counterintuitive 'last' framing.
Top gotchas
- DON'T think 'first to burst' — pick which is LAST burst in interval.
- Pad with 1s on both sides (virtual boundary balloons).
- Inner k iterates strictly between l and r.
Ship-it
Interval DP.
#121 LC 10 Hard Regular Expression Matching
2-D DP — '*' = zero or more of preceding char; recurse on both.
Problem Statement
Match string s with pattern p containing '.' (any) and '*' (zero or more of prev).
Examples
Constraints
- 1 <= n, m <= 20
Approach Overview
Brute Force
Java: Recursion
Python: Recursion
Optimal #1
Java: 2-D DP
Python: 2-D DP
Optimal #2
Java: Memoized recursion
Python: Memoized
Java Solutions
Recursion O(2^(n+m)) O(n+m)
Try '*' = 0 (skip) or 1+ (consume).
Pseudo-code
if p[j+1]=='*': skip OR (match && rec(i+1,j)); else single match && rec(i+1,j+1)
Complexity
Edge cases & gotchas
- Empty s, p must collapse via '*'.
Java code
class Solution {
public boolean isMatch(String s, String p) { return rec(s, p, 0, 0); }
private boolean rec(String s, String p, int i, int j) {
if (j == p.length()) return i == s.length();
boolean first = i < s.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
if (j + 1 < p.length() && p.charAt(j+1) == '*')
return rec(s, p, i, j+2) || (first && rec(s, p, i+1, j));
return first && rec(s, p, i+1, j+1);
}
}
2-D DP O(n·m) O(n·m)
Bottom-up.
Pseudo-code
dp[i][j] = can match s[i..] with p[j..]; same logic
Complexity
Java code
class Solution {
public boolean isMatch(String s, String p) {
int n = s.length(), m = p.length();
boolean[][] dp = new boolean[n + 1][m + 1];
dp[n][m] = true;
for (int i = n; i >= 0; i--) for (int j = m - 1; j >= 0; j--) {
boolean first = i < n && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
if (j + 1 < m && p.charAt(j+1) == '*') dp[i][j] = dp[i][j+2] || (first && dp[i+1][j]);
else dp[i][j] = first && dp[i+1][j+1];
}
return dp[0][0];
}
}
Memoized recursion O(n·m) O(n·m)
Top-down cached.
Pseudo-code
memo[(i,j)]
Complexity
Java code
class Solution {
private Boolean[][] memo;
public boolean isMatch(String s, String p) { memo = new Boolean[s.length() + 1][p.length() + 1]; return rec(s, p, 0, 0); }
private boolean rec(String s, String p, int i, int j) {
if (memo[i][j] != null) return memo[i][j];
if (j == p.length()) return memo[i][j] = (i == s.length());
boolean first = i < s.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
boolean res;
if (j + 1 < p.length() && p.charAt(j+1) == '*') res = rec(s, p, i, j+2) || (first && rec(s, p, i+1, j));
else res = first && rec(s, p, i+1, j+1);
return memo[i][j] = res;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(n·m) |
| Difficulty | 3/5 | 5/5 | 4/5 |
| When | Don't | Default | Top-down clarity |
| PE Verdict | 2-D DP bottom-up. | ||
Python Solutions
Recursion O(2^(n+m)) O(n+m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isMatch(self, s: str, p: str) -> bool:
def rec(i, j):
if j == len(p): return i == len(s)
first = i < len(s) and (s[i] == p[j] or p[j] == '.')
if j+1 < len(p) and p[j+1] == '*':
return rec(i, j+2) or (first and rec(i+1, j))
return first and rec(i+1, j+1)
return rec(0, 0)
2-D DP O(n·m) O(n·m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isMatch(self, s: str, p: str) -> bool:
n, m = len(s), len(p)
dp = [[False]*(m+1) for _ in range(n+1)]
dp[n][m] = True
for i in range(n, -1, -1):
for j in range(m-1, -1, -1):
first = i < n and (s[i] == p[j] or p[j] == '.')
if j+1 < m and p[j+1] == '*': dp[i][j] = dp[i][j+2] or (first and dp[i+1][j])
else: dp[i][j] = first and dp[i+1][j+1]
return dp[0][0]
Memoized O(n·m) O(n·m)
Same.
Pseudo-code
see Java
Complexity
Python code
from functools import lru_cache
class Solution:
def isMatch(self, s: str, p: str) -> bool:
@lru_cache(None)
def rec(i, j):
if j == len(p): return i == len(s)
first = i < len(s) and (s[i] == p[j] or p[j] == '.')
if j+1 < len(p) and p[j+1] == '*':
return rec(i, j+2) or (first and rec(i+1, j))
return first and rec(i+1, j+1)
return rec(0, 0)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^(n+m)) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n·m) | O(n·m) |
| Difficulty | 3/5 | 5/5 | 4/5 |
| When | Don't | Default | Top-down |
| PE Verdict | 2-D DP. | ||
What the interviewer is really testing
Classic regex DP — '*' has two interpretations.
Top gotchas
- Look AHEAD to p[j+1] for '*'.
- '*' consume requires first char match AND recurse keeping j (don't advance).
- Base: empty s matches empty p; empty s matches some patterns via '*' collapsing pairs.
Ship-it
2-D DP.
Greedy
#122 LC 53 Medium Maximum Subarray
Kadane's algorithm — track running sum; reset when it dips negative.
Problem Statement
Max sum of any contiguous subarray.
Examples
Constraints
- 1 <= n <= 10^5
Approach Overview
Brute Force
Java: All subarrays
Python: All subarrays
Optimal #1
Java: Kadane's
Python: Kadane's
Optimal #2
Java: Divide & conquer
Python: D&C
Java Solutions
All subarrays O(n²) O(1)
O(n²) pairs.
Pseudo-code
for each l: sum=0; for r=l..: sum+=nums[r]; best=max
Complexity
Edge cases & gotchas
- All negative — best is max single element.
Java code
class Solution {
public int maxSubArray(int[] nums) {
int best = Integer.MIN_VALUE;
for (int l = 0; l < nums.length; l++) {
int s = 0;
for (int r = l; r < nums.length; r++) { s += nums[r]; best = Math.max(best, s); }
}
return best;
}
}
Kadane's O(n) O(1)
Running sum; reset if it drops below current x.
Pseudo-code
cur = max(x, cur+x); best = max(best, cur)
Complexity
Java code
class Solution {
public int maxSubArray(int[] nums) {
int cur = nums[0], best = nums[0];
for (int i = 1; i < nums.length; i++) {
cur = Math.max(nums[i], cur + nums[i]);
best = Math.max(best, cur);
}
return best;
}
}
Divide & conquer O(n log n) O(log n)
Mid-crossing + left + right.
Pseudo-code
max(leftMax, rightMax, crossingMax)
Complexity
Java code
class Solution {
public int maxSubArray(int[] nums) { return rec(nums, 0, nums.length - 1); }
private int rec(int[] n, int l, int r) {
if (l == r) return n[l];
int m = (l + r) >>> 1;
int lm = rec(n, l, m), rm = rec(n, m+1, r);
int s = 0, lc = Integer.MIN_VALUE;
for (int i = m; i >= l; i--) { s += n[i]; lc = Math.max(lc, s); }
s = 0; int rc = Integer.MIN_VALUE;
for (int i = m+1; i <= r; i++) { s += n[i]; rc = Math.max(rc, s); }
return Math.max(Math.max(lm, rm), lc + rc);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n log n) |
| Space | O(1) | O(1) | O(log n) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Pedagogical / parallel |
| PE Verdict | Kadane's. | ||
Python Solutions
All subarrays O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
best = nums[0]
for l in range(len(nums)):
s = 0
for r in range(l, len(nums)):
s += nums[r]; best = max(best, s)
return best
Kadane's O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
cur = best = nums[0]
for x in nums[1:]:
cur = max(x, cur + x); best = max(best, cur)
return best
D&C O(n log n) O(log n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def rec(l, r):
if l == r: return nums[l]
m = (l + r) // 2
lm, rm = rec(l, m), rec(m+1, r)
s = 0; lc = float('-inf')
for i in range(m, l-1, -1): s += nums[i]; lc = max(lc, s)
s = 0; rc = float('-inf')
for i in range(m+1, r+1): s += nums[i]; rc = max(rc, s)
return max(lm, rm, lc + rc)
return rec(0, len(nums) - 1)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n log n) |
| Space | O(1) | O(1) | O(log n) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | Kadane's. | ||
What the interviewer is really testing
Kadane's is a must-know.
Top gotchas
- Compare cur+x to x alone (handles all-negative arrays).
- Initialize best with nums[0], not 0.
- D&C variant useful for parallel/distributed.
Ship-it
Kadane's running sum.
#123 LC 55 Medium Jump Game
Greedy — track farthest reachable index; fail if i exceeds it.
Problem Statement
Each nums[i] is max jump length from i. Can you reach index n-1?
Examples
Constraints
- 1 <= n <= 10^4
Approach Overview
Brute Force
Java: DFS try every jump
Python: DFS
Optimal #1
Java: Greedy reachable
Python: Greedy
Optimal #2
Java: Backward DP
Python: Backward DP
Java Solutions
DFS try every jump O(2^n) O(n)
Backtrack jump lengths.
Pseudo-code
rec(i): if i>=n-1 true; for step=1..nums[i]: if rec(i+step): true
Complexity
Java code
class Solution {
public boolean canJump(int[] nums) { return rec(nums, 0); }
private boolean rec(int[] n, int i) {
if (i >= n.length - 1) return true;
int maxStep = Math.min(n[i], n.length - 1 - i);
for (int step = maxStep; step >= 1; step--) if (rec(n, i + step)) return true;
return false;
}
}
Greedy reachable O(n) O(1)
Track farthest index reachable so far.
Pseudo-code
far=0; for i: if i>far: false; far=max(far, i+nums[i]); return true
Complexity
Java code
class Solution {
public boolean canJump(int[] nums) {
int far = 0;
for (int i = 0; i < nums.length; i++) {
if (i > far) return false;
far = Math.max(far, i + nums[i]);
if (far >= nums.length - 1) return true;
}
return true;
}
}
Backward DP O(n) O(1)
Walk right→left; track last good.
Pseudo-code
good=n-1; for i=n-2..0: if i+nums[i]>=good: good=i; return good==0
Complexity
Java code
class Solution {
public boolean canJump(int[] nums) {
int good = nums.length - 1;
for (int i = nums.length - 2; i >= 0; i--)
if (i + nums[i] >= good) good = i;
return good == 0;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 2/5 | 2/5 | 2/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Greedy reachable index. | ||
Python Solutions
DFS O(2^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
def rec(i):
if i >= len(nums) - 1: return True
for step in range(min(nums[i], len(nums)-1-i), 0, -1):
if rec(i + step): return True
return False
return rec(0)
Greedy O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
far = 0
for i, x in enumerate(nums):
if i > far: return False
far = max(far, i + x)
return True
Backward DP O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
good = len(nums) - 1
for i in range(len(nums) - 2, -1, -1):
if i + nums[i] >= good: good = i
return good == 0
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 2/5 | 2/5 | 2/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Greedy reachable. | ||
What the interviewer is really testing
Classic greedy.
Top gotchas
- Track FARTHEST index reachable — not just whether i is reachable.
- Early-exit when far >= n-1.
- Backward DP equally valid; same complexity.
Ship-it
Greedy forward sweep.
#124 LC 45 Medium Jump Game II
BFS-on-array — current 'level' ends, jump from best in window.
Problem Statement
Min jumps to reach last index (guaranteed reachable).
Examples
Constraints
- 1 <= n <= 10^4
Approach Overview
Brute Force
Java: DP
Python: DP
Optimal #1
Java: BFS-style greedy
Python: BFS greedy
Optimal #2
Java: BFS explicit
Python: BFS explicit
Java Solutions
DP O(n²) O(n)
dp[i] = min jumps to reach i.
Pseudo-code
dp[0]=0; for i: for j>i within reach: dp[j]=min(dp[j], dp[i]+1)
Complexity
Java code
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
java.util.Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= Math.min(n - 1, i + nums[i]); j++)
if (dp[i] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j], dp[i] + 1);
return dp[n-1];
}
}
BFS-style greedy O(n) O(1)
Track current end-of-level and farthest reachable.
Pseudo-code
jumps=0; far=0; end=0; for i<n-1: far=max(far, i+nums[i]); if i==end: jumps++; end=far
Complexity
Edge cases & gotchas
- n=1 → 0.
Java code
class Solution {
public int jump(int[] nums) {
int jumps = 0, far = 0, end = 0;
for (int i = 0; i < nums.length - 1; i++) {
far = Math.max(far, i + nums[i]);
if (i == end) { jumps++; end = far; }
}
return jumps;
}
}
BFS explicit O(n) O(n)
Queue of levels.
Pseudo-code
BFS by level; first level reaching n-1
Complexity
Java code
import java.util.*;
class Solution {
public int jump(int[] nums) {
if (nums.length == 1) return 0;
Deque<Integer> q = new ArrayDeque<>(); q.offer(0);
boolean[] v = new boolean[nums.length]; v[0] = true;
int level = 0;
while (!q.isEmpty()) {
level++;
int sz = q.size();
for (int k = 0; k < sz; k++) {
int i = q.poll();
for (int s = 1; s <= nums[i] && i + s < nums.length; s++) {
if (i + s == nums.length - 1) return level;
if (!v[i+s]) { v[i+s] = true; q.offer(i+s); }
}
}
}
return -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Conceptual |
| PE Verdict | BFS-style greedy. | ||
Python Solutions
DP O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def jump(self, nums: List[int]) -> int:
n = len(nums)
dp = [float('inf')]*n; dp[0] = 0
for i in range(n):
for j in range(i+1, min(n, i + nums[i] + 1)):
dp[j] = min(dp[j], dp[i] + 1)
return dp[-1]
BFS greedy O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def jump(self, nums: List[int]) -> int:
jumps = far = end = 0
for i in range(len(nums) - 1):
far = max(far, i + nums[i])
if i == end: jumps += 1; end = far
return jumps
BFS explicit O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import deque
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums) == 1: return 0
q = deque([0]); seen = {0}; level = 0
while q:
level += 1
for _ in range(len(q)):
i = q.popleft()
for s in range(1, nums[i] + 1):
nxt = i + s
if nxt >= len(nums) - 1: return level
if nxt not in seen: seen.add(nxt); q.append(nxt)
return -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Conceptual |
| PE Verdict | BFS-style greedy. | ||
What the interviewer is really testing
Greedy levels — equivalent to BFS over jumps.
Top gotchas
- Stop at i = n-2 (don't jump from last).
- Bump jumps when you HIT the end of current window — set new window to farthest seen.
- All inputs guarantee reachable; no -1 path needed.
Ship-it
BFS-style greedy.
#125 LC 134 Medium Gas Station
Greedy — if total gas ≥ total cost, the station after each 'tank empty' is the start.
Problem Statement
Circular route. gas[i] fuel at i, cost[i] to drive to i+1. Find starting station to complete the loop, else -1.
Examples
Constraints
- 1 <= n <= 10^5
Approach Overview
Brute Force
Java: Try each start
Python: Each start
Optimal #1
Java: Greedy one pass
Python: Greedy
Optimal #2
Java: Two-pointer simulation
Python: Two-pointer
Java Solutions
Try each start O(n²) O(1)
Simulate from each.
Pseudo-code
for start: tank=0; for i in 0..n: idx=(start+i)%n; tank+=gas-cost; if tank<0 break; if i==n return start
Complexity
Edge cases & gotchas
- Single station with gas>=cost → 0.
Java code
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
for (int s = 0; s < n; s++) {
int tank = 0; boolean ok = true;
for (int i = 0; i < n; i++) {
int idx = (s + i) % n;
tank += gas[idx] - cost[idx];
if (tank < 0) { ok = false; break; }
}
if (ok) return s;
}
return -1;
}
}
Greedy one pass O(n) O(1)
If running tank dips negative, reset start to next station.
Pseudo-code
if sum(gas) < sum(cost): -1; tank=0; start=0; for i: tank+=gas-cost; if tank<0: start=i+1; tank=0; return start
Complexity
Java code
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int total = 0, tank = 0, start = 0;
for (int i = 0; i < gas.length; i++) {
int d = gas[i] - cost[i];
total += d; tank += d;
if (tank < 0) { start = i + 1; tank = 0; }
}
return total < 0 ? -1 : start;
}
}
Two-pointer simulation O(n) O(1)
Use circular pointer with both ends.
Pseudo-code
end at n-1, start at 0; if tank<0 move start back; until covered
Complexity
Java code
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length, start = n - 1, end = 0, tank = gas[start] - cost[start];
while (start > end) {
if (tank < 0) { start--; tank += gas[start] - cost[start]; }
else { tank += gas[end] - cost[end]; end++; }
}
return tank >= 0 ? start : -1;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Greedy one-pass. | ||
Python Solutions
Each start O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
n = len(gas)
for s in range(n):
tank = 0; ok = True
for i in range(n):
tank += gas[(s+i)%n] - cost[(s+i)%n]
if tank < 0: ok = False; break
if ok: return s
return -1
Greedy O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
total = tank = start = 0
for i, (g, c) in enumerate(zip(gas, cost)):
d = g - c; total += d; tank += d
if tank < 0: start = i + 1; tank = 0
return -1 if total < 0 else start
Two-pointer O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
n = len(gas); start = n - 1; end = 0; tank = gas[start] - cost[start]
while start > end:
if tank < 0: start -= 1; tank += gas[start] - cost[start]
else: tank += gas[end] - cost[end]; end += 1
return start if tank >= 0 else -1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Greedy. | ||
What the interviewer is really testing
Beautiful greedy with subtle proof.
Top gotchas
- Total gas < total cost → impossible.
- If you run out between i and j, NO station in [i,j] can be a valid start. Skip ahead to j+1.
- Only ONE valid start exists (if any).
Ship-it
Greedy one-pass.
#126 LC 846 Medium Hand of Straights
Count map + greedy — repeatedly form a group starting at smallest unused.
Problem Statement
Can hand be split into groups of k consecutive cards?
Examples
Constraints
- 1 <= hand.length <= 10^4
Approach Overview
Brute Force
Java: Sort + naive scan
Python: Sort + naive
Optimal #1
Java: Count map + TreeMap
Python: Counter + sorted
Optimal #2
Java: Sort + queue of group starts
Python: Iterate sorted h
Java Solutions
Sort + naive scan O(n²) O(n)
Sort; pull k consecutive starting at smallest available.
Pseudo-code
sort; for each: if used skip; pull next k; if missing → false
Complexity
Edge cases & gotchas
- n % k != 0 → false.
Java code
import java.util.*;
class Solution {
public boolean isNStraightHand(int[] h, int k) {
if (h.length % k != 0) return false;
Arrays.sort(h);
boolean[] used = new boolean[h.length];
for (int i = 0; i < h.length; i++) {
if (used[i]) continue;
int cur = h[i]; used[i] = true;
for (int g = 1; g < k; g++) {
boolean found = false;
for (int j = i + 1; j < h.length; j++) if (!used[j] && h[j] == cur + 1) { used[j] = true; cur++; found = true; break; }
if (!found) return false;
}
}
return true;
}
}
Count map + TreeMap O(n log n) O(n)
Smallest key; consume k consecutive counts.
Pseudo-code
counts; while not empty: smallest s; for s..s+k-1: decrement, return false if missing
Complexity
Java code
import java.util.*;
class Solution {
public boolean isNStraightHand(int[] h, int k) {
if (h.length % k != 0) return false;
TreeMap<Integer, Integer> cnt = new TreeMap<>();
for (int x : h) cnt.merge(x, 1, Integer::sum);
while (!cnt.isEmpty()) {
int s = cnt.firstKey();
for (int i = 0; i < k; i++) {
Integer c = cnt.get(s + i);
if (c == null) return false;
if (c == 1) cnt.remove(s + i); else cnt.put(s + i, c - 1);
}
}
return true;
}
}
Sort + queue of group starts O(n log n) O(n)
Iterate sorted; track open groups expiring at value+k.
Pseudo-code
sort; queue tracks (start, remaining); for each card: extend existing or start new
Complexity
Java code
import java.util.*;
class Solution {
public boolean isNStraightHand(int[] h, int k) {
if (h.length % k != 0) return false;
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : h) cnt.merge(x, 1, Integer::sum);
Arrays.sort(h);
for (int x : h) {
if (cnt.get(x) == 0) continue;
for (int g = 0; g < k; g++) {
if (cnt.getOrDefault(x + g, 0) == 0) return false;
cnt.put(x + g, cnt.get(x + g) - 1);
}
}
return true;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Sorted scan |
| PE Verdict | TreeMap + greedy. | ||
Python Solutions
Sort + naive O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def isNStraightHand(self, h: List[int], k: int) -> bool:
if len(h) % k: return False
h.sort(); used = [False]*len(h)
for i in range(len(h)):
if used[i]: continue
cur = h[i]; used[i] = True
for _ in range(k - 1):
found = False
for j in range(i+1, len(h)):
if not used[j] and h[j] == cur + 1:
used[j] = True; cur += 1; found = True; break
if not found: return False
return True
Counter + sorted O(n log n) O(n)
Same idea, heap of smallest.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class Solution:
def isNStraightHand(self, h: List[int], k: int) -> bool:
if len(h) % k: return False
cnt = Counter(h)
for x in sorted(cnt):
c = cnt[x]
if c == 0: continue
for g in range(k):
if cnt[x + g] < c: return False
cnt[x + g] -= c
return True
Iterate sorted h O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class Solution:
def isNStraightHand(self, h: List[int], k: int) -> bool:
if len(h) % k: return False
cnt = Counter(h)
for x in sorted(h):
if cnt[x] == 0: continue
for g in range(k):
if cnt[x + g] == 0: return False
cnt[x + g] -= 1
return True
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Counter + sorted keys. | ||
What the interviewer is really testing
Greedy + frequency map combo.
Top gotchas
- First short-circuit: n % k != 0 → impossible.
- Always start a new group from the SMALLEST remaining card.
- Consume k consecutive in the counter; bail if any is missing.
Ship-it
TreeMap/Counter greedy.
#127 LC 1899 Medium Merge Triplets to Form Target Triplet
Filter usable triplets (no value exceeds target) — check OR-merge covers target.
Problem Statement
merge(a,b) takes max-by-component. Can we produce target from any subset?
Examples
Constraints
- 1 <= n <= 10^5
Approach Overview
Brute Force
Java: Bitmask try-all subsets
Python: Bitmask
Optimal #1
Java: Greedy filter + max
Python: Greedy filter
Optimal #2
Java: Same logic; bitmask of found components
Python: Bitmask of found
Java Solutions
Bitmask try-all subsets O(2^n · 3) O(1)
Try every subset, merge, check.
Pseudo-code
for each subset: merge; if == target return true
Complexity
Java code
class Solution {
public boolean mergeTriplets(int[][] t, int[] target) {
int n = t.length;
for (int mask = 1; mask < (1 << n); mask++) {
int a = 0, b = 0, c = 0;
for (int i = 0; i < n; i++) if ((mask & (1 << i)) != 0) {
a = Math.max(a, t[i][0]); b = Math.max(b, t[i][1]); c = Math.max(c, t[i][2]);
}
if (a == target[0] && b == target[1] && c == target[2]) return true;
}
return false;
}
}
Greedy filter + max O(n) O(1)
Drop triplets exceeding target; check if max of remaining equals target component-wise.
Pseudo-code
for t: if any t[k]>target[k]: skip; else update found[k] if t[k]==target[k]; return all found
Complexity
Java code
class Solution {
public boolean mergeTriplets(int[][] t, int[] target) {
boolean[] found = new boolean[3];
for (int[] x : t) {
if (x[0] > target[0] || x[1] > target[1] || x[2] > target[2]) continue;
for (int k = 0; k < 3; k++) if (x[k] == target[k]) found[k] = true;
}
return found[0] && found[1] && found[2];
}
}
Same logic; bitmask of found components O(n) O(1)
Bitmask 3-bit.
Pseudo-code
int f=0; for t: skip if exceeds; for k: if t[k]==target[k] set bit
Complexity
Java code
class Solution {
public boolean mergeTriplets(int[][] t, int[] target) {
int f = 0;
for (int[] x : t) {
if (x[0] > target[0] || x[1] > target[1] || x[2] > target[2]) continue;
for (int k = 0; k < 3; k++) if (x[k] == target[k]) f |= 1 << k;
}
return f == 7;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Greedy filter + per-component cover. | ||
Python Solutions
Bitmask O(2^n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def mergeTriplets(self, t: List[List[int]], target: List[int]) -> bool:
n = len(t)
for mask in range(1, 1 << n):
a = b = c = 0
for i in range(n):
if mask & (1 << i):
a = max(a, t[i][0]); b = max(b, t[i][1]); c = max(c, t[i][2])
if [a,b,c] == target: return True
return False
Greedy filter O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def mergeTriplets(self, t: List[List[int]], target: List[int]) -> bool:
found = [False, False, False]
for x in t:
if any(x[k] > target[k] for k in range(3)): continue
for k in range(3):
if x[k] == target[k]: found[k] = True
return all(found)
Bitmask of found O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def mergeTriplets(self, t: List[List[int]], target: List[int]) -> bool:
f = 0
for x in t:
if any(x[k] > target[k] for k in range(3)): continue
for k in range(3):
if x[k] == target[k]: f |= 1 << k
return f == 7
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(2^n) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Greedy filter. | ||
What the interviewer is really testing
Insight: any triplet with a component > target is unusable.
Top gotchas
- Skip ENTIRELY any triplet that exceeds target in any component.
- Need to find triplets that contribute each of the 3 target values exactly.
- Merging takes maxes — so non-exceeding partners are safe.
Ship-it
Greedy filter.
#128 LC 763 Medium Partition Labels
Last-index map + greedy — expand current window to cover farthest last-index.
Problem Statement
Partition s so each letter appears in only one part. Return sizes.
Examples
Constraints
- 1 <= n <= 500
Approach Overview
Brute Force
Java: Try every cut
Python: Brute cuts
Optimal #1
Java: Last-index map + greedy
Python: Last-index greedy
Optimal #2
Java: Interval merge
Python: Interval merge
Java Solutions
Try every cut O(n²) O(n)
Verify each prefix is closed wrt remaining suffix.
Pseudo-code
for each cut: chars in prefix ∩ chars in suffix == ∅ → cut
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> partitionLabels(String s) {
List<Integer> out = new ArrayList<>();
int start = 0;
while (start < s.length()) {
for (int end = start; end < s.length(); end++) {
Set<Character> a = new HashSet<>(), b = new HashSet<>();
for (int i = start; i <= end; i++) a.add(s.charAt(i));
for (int i = end + 1; i < s.length(); i++) b.add(s.charAt(i));
a.retainAll(b);
if (a.isEmpty()) { out.add(end - start + 1); start = end + 1; break; }
}
}
return out;
}
}
Last-index map + greedy O(n) O(1)
Track last occurrence; when i reaches farthest last, close partition.
Pseudo-code
compute last[c]; far=0; start=0; for i: far=max(far, last[s[i]]); if i==far: add(i-start+1); start=i+1
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> partitionLabels(String s) {
int[] last = new int[26];
for (int i = 0; i < s.length(); i++) last[s.charAt(i) - 'a'] = i;
List<Integer> out = new ArrayList<>();
int far = 0, start = 0;
for (int i = 0; i < s.length(); i++) {
far = Math.max(far, last[s.charAt(i) - 'a']);
if (i == far) { out.add(i - start + 1); start = i + 1; }
}
return out;
}
}
Interval merge O(n + 26 log 26) O(1)
Build [first, last] per char; merge intervals.
Pseudo-code
first/last per char; merge overlapping; emit lengths
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> partitionLabels(String s) {
int[] first = new int[26], last = new int[26];
Arrays.fill(first, -1);
for (int i = 0; i < s.length(); i++) {
int c = s.charAt(i) - 'a';
if (first[c] == -1) first[c] = i;
last[c] = i;
}
List<int[]> iv = new ArrayList<>();
for (int c = 0; c < 26; c++) if (first[c] != -1) iv.add(new int[]{first[c], last[c]});
iv.sort((a, b) -> a[0] - b[0]);
List<Integer> out = new ArrayList<>();
int l = iv.get(0)[0], r = iv.get(0)[1];
for (int i = 1; i < iv.size(); i++) {
if (iv.get(i)[0] <= r) r = Math.max(r, iv.get(i)[1]);
else { out.add(r - l + 1); l = iv.get(i)[0]; r = iv.get(i)[1]; }
}
out.add(r - l + 1);
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Generalizes to k>26 |
| PE Verdict | Last-index map + greedy. | ||
Python Solutions
Brute cuts O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def partitionLabels(self, s: str) -> List[int]:
out = []; start = 0
while start < len(s):
for end in range(start, len(s)):
if set(s[start:end+1]).isdisjoint(set(s[end+1:])):
out.append(end - start + 1); start = end + 1; break
return out
Last-index greedy O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def partitionLabels(self, s: str) -> List[int]:
last = {c: i for i, c in enumerate(s)}
out = []; far = start = 0
for i, c in enumerate(s):
far = max(far, last[c])
if i == far: out.append(i - start + 1); start = i + 1
return out
Interval merge O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def partitionLabels(self, s: str) -> List[int]:
first, last = {}, {}
for i, c in enumerate(s):
if c not in first: first[c] = i
last[c] = i
iv = sorted([(first[c], last[c]) for c in first])
out = []; l, r = iv[0]
for a, b in iv[1:]:
if a <= r: r = max(r, b)
else: out.append(r - l + 1); l, r = a, b
out.append(r - l + 1)
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Generalizes |
| PE Verdict | Last-index + greedy. | ||
What the interviewer is really testing
Beautiful single-pass greedy.
Top gotchas
- Build last[c] in a separate sweep.
- Track farthest required end; close when i reaches it.
- Reset partition start to i+1.
Ship-it
Last-index map + greedy.
#129 LC 678 Medium Valid Parenthesis String
Track open-count range [lo, hi] — '*' widens the range.
Problem Statement
'*' acts as '(', ')', or empty. Is s a valid parenthesis sequence?
Examples
Constraints
- 1 <= n <= 100
Approach Overview
Brute Force
Java: Try each '*' as 3 chars
Python: Recursion
Optimal #1
Java: Range [lo, hi]
Python: Range [lo, hi]
Optimal #2
Java: Two stacks
Python: Two stacks
Java Solutions
Try each '*' as 3 chars O(3^n) O(n)
Recursion 3-way.
Pseudo-code
rec(i, bal): if c=='(' bal++; if ')' bal--; if '*' try all three
Complexity
Edge cases & gotchas
- bal must stay >=0.
Java code
class Solution {
public boolean checkValidString(String s) { return rec(s, 0, 0); }
private boolean rec(String s, int i, int bal) {
if (bal < 0) return false;
if (i == s.length()) return bal == 0;
char c = s.charAt(i);
if (c == '(') return rec(s, i+1, bal+1);
if (c == ')') return rec(s, i+1, bal-1);
return rec(s, i+1, bal+1) || rec(s, i+1, bal) || rec(s, i+1, bal-1);
}
}
Range [lo, hi] O(n) O(1)
lo = min possible open count, hi = max. '*' shifts both ways.
Pseudo-code
lo=hi=0; for c: if '(' lo++,hi++; if ')' lo--,hi--; if '*' lo--,hi++; if hi<0: false; lo=max(0,lo); return lo==0
Complexity
Java code
class Solution {
public boolean checkValidString(String s) {
int lo = 0, hi = 0;
for (char c : s.toCharArray()) {
if (c == '(') { lo++; hi++; }
else if (c == ')') { lo--; hi--; }
else { lo--; hi++; }
if (hi < 0) return false;
lo = Math.max(lo, 0);
}
return lo == 0;
}
}
Two stacks O(n) O(n)
Stack of '(' indices, stack of '*' indices; reconcile.
Pseudo-code
push '(' and '*' indices; ')' pops '(' first else '*'; final: leftover '(' must pair with '*' to the right
Complexity
Java code
import java.util.*;
class Solution {
public boolean checkValidString(String s) {
Deque<Integer> open = new ArrayDeque<>(), star = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') open.push(i);
else if (c == '*') star.push(i);
else { if (!open.isEmpty()) open.pop(); else if (!star.isEmpty()) star.pop(); else return false; }
}
while (!open.isEmpty() && !star.isEmpty()) {
if (open.pop() > star.pop()) return false;
}
return open.isEmpty();
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(3^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default — O(1) space | Index-aware |
| PE Verdict | Range [lo, hi]. | ||
Python Solutions
Recursion O(3^n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def checkValidString(self, s: str) -> bool:
def rec(i, bal):
if bal < 0: return False
if i == len(s): return bal == 0
c = s[i]
if c == '(': return rec(i+1, bal+1)
if c == ')': return rec(i+1, bal-1)
return rec(i+1, bal+1) or rec(i+1, bal) or rec(i+1, bal-1)
return rec(0, 0)
Range [lo, hi] O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def checkValidString(self, s: str) -> bool:
lo = hi = 0
for c in s:
if c == '(': lo += 1; hi += 1
elif c == ')': lo -= 1; hi -= 1
else: lo -= 1; hi += 1
if hi < 0: return False
lo = max(lo, 0)
return lo == 0
Two stacks O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def checkValidString(self, s: str) -> bool:
open_, star = [], []
for i, c in enumerate(s):
if c == '(': open_.append(i)
elif c == '*': star.append(i)
else:
if open_: open_.pop()
elif star: star.pop()
else: return False
while open_ and star:
if open_.pop() > star.pop(): return False
return not open_
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(3^n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Range [lo, hi]. | ||
What the interviewer is really testing
The [lo,hi] trick is elegant.
Top gotchas
- Clamp lo to 0 (can't have negative unmatched-open count).
- Fail if hi < 0 — impossible to recover.
- Final accept: lo == 0 (some interpretation yields exact balance).
Ship-it
Range [lo, hi].
Intervals
#130 LC 57 Medium Insert Interval
Single pass — emit before, merge overlapping, emit after.
Problem Statement
Insert newInterval into sorted non-overlapping intervals; merge if needed.
Examples
Constraints
- 0 <= n <= 10^4
Approach Overview
Brute Force
Java: Append + sort + merge
Python: Append + sort + merge
Optimal #1
Java: Single-pass insert
Python: Single-pass
Optimal #2
Java: Binary search for insertion point
Python: Binary search
Java Solutions
Append + sort + merge O(n log n) O(n)
Reuse merge-intervals.
Pseudo-code
append; sort by start; standard merge
Complexity
Edge cases & gotchas
- Empty intervals → just newInterval.
Java code
import java.util.*;
class Solution {
public int[][] insert(int[][] iv, int[] x) {
int[][] all = new int[iv.length + 1][];
System.arraycopy(iv, 0, all, 0, iv.length);
all[iv.length] = x;
Arrays.sort(all, (a, b) -> a[0] - b[0]);
List<int[]> out = new ArrayList<>();
for (int[] r : all) {
if (!out.isEmpty() && out.get(out.size()-1)[1] >= r[0])
out.get(out.size()-1)[1] = Math.max(out.get(out.size()-1)[1], r[1]);
else out.add(r);
}
return out.toArray(new int[0][]);
}
}
Single-pass insert O(n) O(n)
Three phases: before, merge, after.
Pseudo-code
i=0; copy strictly-before; merge overlapping into x; copy remaining
Complexity
Java code
import java.util.*;
class Solution {
public int[][] insert(int[][] iv, int[] x) {
List<int[]> out = new ArrayList<>();
int i = 0, n = iv.length;
while (i < n && iv[i][1] < x[0]) out.add(iv[i++]);
while (i < n && iv[i][0] <= x[1]) {
x[0] = Math.min(x[0], iv[i][0]);
x[1] = Math.max(x[1], iv[i][1]);
i++;
}
out.add(x);
while (i < n) out.add(iv[i++]);
return out.toArray(new int[0][]);
}
}
Binary search for insertion point O(log n + k) O(n)
Find first overlap, merge forward.
Pseudo-code
binsearch leftmost interval with end>=x[0]; merge forward
Complexity
Java code
import java.util.*;
class Solution {
public int[][] insert(int[][] iv, int[] x) {
int n = iv.length;
int lo = 0, hi = n;
while (lo < hi) { int m = (lo + hi) >>> 1; if (iv[m][1] < x[0]) lo = m + 1; else hi = m; }
int idx = lo;
List<int[]> out = new ArrayList<>();
for (int k = 0; k < idx; k++) out.add(iv[k]);
int i = idx;
while (i < n && iv[i][0] <= x[1]) {
x[0] = Math.min(x[0], iv[i][0]);
x[1] = Math.max(x[1], iv[i][1]);
i++;
}
out.add(x);
while (i < n) out.add(iv[i++]);
return out.toArray(new int[0][]);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(log n + k) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Huge n |
| PE Verdict | Single-pass insert. | ||
Python Solutions
Append + sort + merge O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
all_ = sorted(intervals + [newInterval])
out = []
for r in all_:
if out and out[-1][1] >= r[0]: out[-1][1] = max(out[-1][1], r[1])
else: out.append(r)
return out
Single-pass O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
out, i, n = [], 0, len(intervals)
while i < n and intervals[i][1] < newInterval[0]:
out.append(intervals[i]); i += 1
while i < n and intervals[i][0] <= newInterval[1]:
newInterval = [min(newInterval[0], intervals[i][0]), max(newInterval[1], intervals[i][1])]
i += 1
out.append(newInterval)
while i < n:
out.append(intervals[i]); i += 1
return out
Binary search O(log n + k) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from bisect import bisect_left
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
ends = [iv[1] for iv in intervals]
idx = bisect_left(ends, newInterval[0])
out = intervals[:idx]; i = idx
while i < len(intervals) and intervals[i][0] <= newInterval[1]:
newInterval = [min(newInterval[0], intervals[i][0]), max(newInterval[1], intervals[i][1])]
i += 1
out.append(newInterval)
out.extend(intervals[i:])
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(log n + k) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 2/5 | 3/5 | 4/5 |
| When | Don't | Default | Huge n |
| PE Verdict | Single-pass. | ||
What the interviewer is really testing
Three-phase walk.
Top gotchas
- Phase 1: intervals ending BEFORE start of new — copy.
- Phase 2: overlapping — merge into running x.
- Phase 3: rest — copy.
Ship-it
Single-pass three-phase.
#131 LC 56 Medium Merge Intervals
Sort by start; merge if current overlaps last result.
Problem Statement
Merge all overlapping intervals.
Examples
Constraints
- 1 <= n <= 10^4
Approach Overview
Brute Force
Java: Repeated pass until stable
Python: Repeated pass
Optimal #1
Java: Sort + single pass
Python: Sort + single pass
Optimal #2
Java: Sweep line
Python: Sweep line
Java Solutions
Repeated pass until stable O(n²) O(n)
Each pass merges adjacent overlap; repeat until no change.
Pseudo-code
do: scan, merge adjacent overlapping pair; while changed
Complexity
Java code
import java.util.*;
class Solution {
public int[][] merge(int[][] iv) {
List<int[]> list = new ArrayList<>();
for (int[] x : iv) list.add(x);
list.sort((a, b) -> a[0] - b[0]);
boolean changed = true;
while (changed) {
changed = false;
for (int i = 0; i + 1 < list.size(); i++) {
if (list.get(i)[1] >= list.get(i+1)[0]) {
list.get(i)[1] = Math.max(list.get(i)[1], list.get(i+1)[1]);
list.remove(i+1); changed = true; break;
}
}
}
return list.toArray(new int[0][]);
}
}
Sort + single pass O(n log n) O(n)
Standard merge-intervals.
Pseudo-code
sort by start; for each: if last.end >= cur.start: extend last.end; else append
Complexity
Java code
import java.util.*;
class Solution {
public int[][] merge(int[][] iv) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
List<int[]> out = new ArrayList<>();
for (int[] x : iv) {
if (!out.isEmpty() && out.get(out.size()-1)[1] >= x[0])
out.get(out.size()-1)[1] = Math.max(out.get(out.size()-1)[1], x[1]);
else out.add(x);
}
return out.toArray(new int[0][]);
}
}
Sweep line O(n log n) O(n)
Endpoints as events.
Pseudo-code
events: (start,+1),(end+1,-1); sweep; balance crosses 0 → close interval
Complexity
Edge cases & gotchas
- Closed intervals — use end+1 with care.
Java code
import java.util.*;
class Solution {
public int[][] merge(int[][] iv) {
int n = iv.length;
int[][] e = new int[2*n][2];
for (int i = 0; i < n; i++) { e[2*i] = new int[]{iv[i][0], 1}; e[2*i+1] = new int[]{iv[i][1], -1}; }
Arrays.sort(e, (a, b) -> a[0] != b[0] ? a[0] - b[0] : b[1] - a[1]);
List<int[]> out = new ArrayList<>();
int bal = 0, start = -1;
for (int[] ev : e) {
if (bal == 0 && ev[1] == 1) start = ev[0];
bal += ev[1];
if (bal == 0) out.add(new int[]{start, ev[0]});
}
return out.toArray(new int[0][]);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Sweep-line generalizes |
| PE Verdict | Sort + single pass. | ||
Python Solutions
Repeated pass O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def merge(self, iv: List[List[int]]) -> List[List[int]]:
iv = sorted(iv)
changed = True
while changed:
changed = False
for i in range(len(iv) - 1):
if iv[i][1] >= iv[i+1][0]:
iv[i][1] = max(iv[i][1], iv[i+1][1])
iv.pop(i+1); changed = True; break
return iv
Sort + single pass O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def merge(self, iv: List[List[int]]) -> List[List[int]]:
iv.sort()
out = []
for x in iv:
if out and out[-1][1] >= x[0]: out[-1][1] = max(out[-1][1], x[1])
else: out.append(x)
return out
Sweep line O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def merge(self, iv: List[List[int]]) -> List[List[int]]:
events = []
for s, e in iv:
events.append((s, 1)); events.append((e, -1))
events.sort(key=lambda x: (x[0], -x[1]))
out = []; bal = 0; start = -1
for t, d in events:
if bal == 0 and d == 1: start = t
bal += d
if bal == 0: out.append([start, t])
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Sweep generalizes |
| PE Verdict | Sort + single pass. | ||
What the interviewer is really testing
Foundational intervals.
Top gotchas
- Sort by START.
- Overlap if last.end >= current.start (inclusive endpoints).
- Always extend last.end with max — don't overwrite blindly.
Ship-it
Sort + single pass.
#132 LC 435 Medium Non-overlapping Intervals
Sort by end — greedy keep earliest end; count removals.
Problem Statement
Min intervals to remove so the rest don't overlap.
Examples
Constraints
- 1 <= n <= 10^5
Approach Overview
Brute Force
Java: Sort by start + DP
Python: DP
Optimal #1
Java: Sort by end + greedy
Python: Sort by end + greedy
Optimal #2
Java: Sort by start + remove later end
Python: Sort by start + drop later end
Java Solutions
Sort by start + DP O(n²) O(n)
LIS-style: longest non-overlap chain.
Pseudo-code
sort by start; dp[i] = 1 + max(dp[j] for j<i if iv[j].end <= iv[i].start); answer = n - max(dp)
Complexity
Java code
import java.util.*;
class Solution {
public int eraseOverlapIntervals(int[][] iv) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
int n = iv.length, best = 1;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) if (iv[j][1] <= iv[i][0]) dp[i] = Math.max(dp[i], dp[j] + 1);
best = Math.max(best, dp[i]);
}
return n - best;
}
}
Sort by end + greedy O(n log n) O(1)
Activity selection — pick earliest-ending non-overlap.
Pseudo-code
sort by end; prev = iv[0].end; count=0; for each: if start<prev: count++; else prev = end
Complexity
Java code
import java.util.*;
class Solution {
public int eraseOverlapIntervals(int[][] iv) {
Arrays.sort(iv, (a, b) -> a[1] - b[1]);
int prev = iv[0][1], removed = 0;
for (int i = 1; i < iv.length; i++) {
if (iv[i][0] < prev) removed++;
else prev = iv[i][1];
}
return removed;
}
}
Sort by start + remove later end O(n log n) O(1)
On overlap, remove the one ending later.
Pseudo-code
sort by start; if overlap, remove the one with larger end
Complexity
Java code
import java.util.*;
class Solution {
public int eraseOverlapIntervals(int[][] iv) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
int prevEnd = iv[0][1], removed = 0;
for (int i = 1; i < iv.length; i++) {
if (iv[i][0] < prevEnd) { removed++; prevEnd = Math.min(prevEnd, iv[i][1]); }
else prevEnd = iv[i][1];
}
return removed;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 3/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Sort by end + greedy. | ||
Python Solutions
DP O(n²) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def eraseOverlapIntervals(self, iv: List[List[int]]) -> int:
iv.sort(); n = len(iv); dp = [1]*n
for i in range(1, n):
for j in range(i):
if iv[j][1] <= iv[i][0]: dp[i] = max(dp[i], dp[j] + 1)
return n - max(dp)
Sort by end + greedy O(n log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def eraseOverlapIntervals(self, iv: List[List[int]]) -> int:
iv.sort(key=lambda x: x[1])
prev = iv[0][1]; removed = 0
for s, e in iv[1:]:
if s < prev: removed += 1
else: prev = e
return removed
Sort by start + drop later end O(n log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def eraseOverlapIntervals(self, iv: List[List[int]]) -> int:
iv.sort()
prev_end = iv[0][1]; removed = 0
for s, e in iv[1:]:
if s < prev_end: removed += 1; prev_end = min(prev_end, e)
else: prev_end = e
return removed
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 3/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Sort by end + greedy. | ||
What the interviewer is really testing
Activity selection in disguise.
Top gotchas
- Sort by END, not start.
- Strict comparison: '<' (touching at the boundary is NOT overlap here).
- Equivalent to: max non-overlapping subset; answer = n - that count.
Ship-it
Sort by end + greedy.
#133 LC 252 Easy Meeting Rooms
Sort by start — check every adjacent pair for overlap.
Problem Statement
Can a person attend all meetings? (no overlaps)
Examples
Constraints
- 0 <= n <= 10^4
Approach Overview
Brute Force
Java: All pairs
Python: All pairs
Optimal #1
Java: Sort by start
Python: Sort by start
Optimal #2
Java: Sweep line
Python: Sweep line
Java Solutions
All pairs O(n²) O(1)
O(n²) overlap check.
Pseudo-code
for each i<j: if iv[i] overlaps iv[j]: false
Complexity
Edge cases & gotchas
- Empty → true.
Java code
class Solution {
public boolean canAttendMeetings(int[][] iv) {
for (int i = 0; i < iv.length; i++) for (int j = i + 1; j < iv.length; j++)
if (iv[i][0] < iv[j][1] && iv[j][0] < iv[i][1]) return false;
return true;
}
}
Sort by start O(n log n) O(1)
Adjacent overlap check.
Pseudo-code
sort by start; for i: if iv[i].start < iv[i-1].end: false
Complexity
Java code
import java.util.*;
class Solution {
public boolean canAttendMeetings(int[][] iv) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
for (int i = 1; i < iv.length; i++) if (iv[i][0] < iv[i-1][1]) return false;
return true;
}
}
Sweep line O(n log n) O(n)
Sort events; bal never > 1.
Pseudo-code
events; sweep; if bal > 1: false
Complexity
Java code
import java.util.*;
class Solution {
public boolean canAttendMeetings(int[][] iv) {
int n = iv.length;
int[][] e = new int[2*n][2];
for (int i = 0; i < n; i++) { e[2*i] = new int[]{iv[i][0], 1}; e[2*i+1] = new int[]{iv[i][1], -1}; }
Arrays.sort(e, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
int bal = 0;
for (int[] x : e) { bal += x[1]; if (bal > 1) return false; }
return true;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(1) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When | Don't | Default | Generalizes |
| PE Verdict | Sort by start + adjacent overlap. | ||
Python Solutions
All pairs O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canAttendMeetings(self, iv: List[List[int]]) -> bool:
for i in range(len(iv)):
for j in range(i+1, len(iv)):
if iv[i][0] < iv[j][1] and iv[j][0] < iv[i][1]: return False
return True
Sort by start O(n log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canAttendMeetings(self, iv: List[List[int]]) -> bool:
iv.sort()
for i in range(1, len(iv)):
if iv[i][0] < iv[i-1][1]: return False
return True
Sweep line O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def canAttendMeetings(self, iv: List[List[int]]) -> bool:
events = []
for s, e in iv: events.append((s, 1)); events.append((e, -1))
events.sort()
bal = 0
for _, d in events:
bal += d
if bal > 1: return False
return True
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n log n) | O(n log n) |
| Space | O(1) | O(1) | O(n) |
| Difficulty | 1/5 | 2/5 | 3/5 |
| When | Don't | Default | Generalizes |
| PE Verdict | Sort + adjacent overlap. | ||
What the interviewer is really testing
Intro to interval overlap.
Top gotchas
- Touching at boundary (end == start) is NOT a conflict here.
- Sort by start, then linear adjacent check.
- Sweep-line generalizes to Meeting Rooms II.
Ship-it
Sort + adjacent check.
#134 LC 253 Medium Meeting Rooms II
Min-heap of end times — peak concurrency = rooms needed.
Problem Statement
Min rooms required to host all meetings.
Examples
Constraints
- 1 <= n <= 10^4
Approach Overview
Brute Force
Java: Timeline counting
Python: Timeline
Optimal #1
Java: Sort + min-heap of ends
Python: Sort + min-heap
Optimal #2
Java: Sweep line
Python: Sweep line
Java Solutions
Timeline counting O(n·T) O(T)
Increment per minute; max.
Pseudo-code
buckets = max end; for each iv: bucket[s]++; bucket[e]--; running max prefix
Complexity
Java code
class Solution {
public int minMeetingRooms(int[][] iv) {
int maxT = 0;
for (int[] x : iv) maxT = Math.max(maxT, x[1]);
int[] diff = new int[maxT + 2];
for (int[] x : iv) { diff[x[0]]++; diff[x[1]]--; }
int best = 0, run = 0;
for (int v : diff) { run += v; best = Math.max(best, run); }
return best;
}
}
Sort + min-heap of ends O(n log n) O(n)
Pop ended meetings; heap size = rooms.
Pseudo-code
sort by start; heap of ends; for each: if heap.top<=start: pop; push end; max(size)
Complexity
Java code
import java.util.*;
class Solution {
public int minMeetingRooms(int[][] iv) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
PriorityQueue<Integer> pq = new PriorityQueue<>();
int best = 0;
for (int[] x : iv) {
if (!pq.isEmpty() && pq.peek() <= x[0]) pq.poll();
pq.offer(x[1]);
best = Math.max(best, pq.size());
}
return best;
}
}
Sweep line O(n log n) O(n)
Sort events; max concurrent.
Pseudo-code
events; sort; for each: bal += delta; max(bal)
Complexity
Edge cases & gotchas
- Equal time: end (-1) before start (+1).
Java code
import java.util.*;
class Solution {
public int minMeetingRooms(int[][] iv) {
int n = iv.length;
int[][] e = new int[2*n][2];
for (int i = 0; i < n; i++) { e[2*i] = new int[]{iv[i][0], 1}; e[2*i+1] = new int[]{iv[i][1], -1}; }
Arrays.sort(e, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
int bal = 0, best = 0;
for (int[] x : e) { bal += x[1]; best = Math.max(best, bal); }
return best;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·T) | O(n log n) | O(n log n) |
| Space | O(T) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Tiny T | Default | Equivalent |
| PE Verdict | Sort + min-heap. | ||
Python Solutions
Timeline O(n·T) O(T)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minMeetingRooms(self, iv: List[List[int]]) -> int:
maxT = max(e for _, e in iv)
diff = [0]*(maxT + 2)
for s, e in iv: diff[s] += 1; diff[e] -= 1
best = run = 0
for v in diff: run += v; best = max(best, run)
return best
Sort + min-heap O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
class Solution:
def minMeetingRooms(self, iv: List[List[int]]) -> int:
iv.sort()
h = []; best = 0
for s, e in iv:
if h and h[0] <= s: heapq.heappop(h)
heapq.heappush(h, e)
best = max(best, len(h))
return best
Sweep line O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minMeetingRooms(self, iv: List[List[int]]) -> int:
events = []
for s, e in iv: events.append((s, 1)); events.append((e, -1))
events.sort(key=lambda x: (x[0], x[1]))
bal = best = 0
for _, d in events:
bal += d; best = max(best, bal)
return best
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·T) | O(n log n) | O(n log n) |
| Space | O(T) | O(n) | O(n) |
| Difficulty | 2/5 | 4/5 | 3/5 |
| When | Tiny T | Default | Equivalent |
| PE Verdict | Sort + min-heap. | ||
What the interviewer is really testing
Heap of end times = active rooms.
Top gotchas
- Sort by START.
- Pop heap if earliest END <= current start (release room).
- Touching at boundary (end == start) means the room can be reused — use <=, not <.
Ship-it
Sort + min-heap.
#135 LC 1851 Hard Minimum Interval to Include Each Query
Offline: sort intervals & queries; min-heap by size, prune by end.
Problem Statement
For each query q, return size of smallest interval [l,r] s.t. l<=q<=r (or -1).
Examples
Constraints
- 1 <= n, q <= 10^5
Approach Overview
Brute Force
Java: For each query scan intervals
Python: Per-query scan
Optimal #1
Java: Offline sort + min-heap
Python: Offline + min-heap
Optimal #2
Java: Sweep with sorted intervals + TreeSet
Python: SortedList
Java Solutions
For each query scan intervals O(n·q) O(q)
O(n·q).
Pseudo-code
for each q: best=inf; for each iv: if l<=q<=r: best=min(best, r-l+1); record
Complexity
Java code
class Solution {
public int[] minInterval(int[][] iv, int[] q) {
int[] out = new int[q.length];
for (int i = 0; i < q.length; i++) {
int best = Integer.MAX_VALUE;
for (int[] x : iv) if (x[0] <= q[i] && q[i] <= x[1]) best = Math.min(best, x[1] - x[0] + 1);
out[i] = best == Integer.MAX_VALUE ? -1 : best;
}
return out;
}
}
Offline sort + min-heap O((n+q) log (n+q)) O(n+q)
Sort intervals by start, queries asc; add eligible to heap (size), pop expired.
Pseudo-code
sort intervals by start, query indices by query value; for each q: push intervals with start<=q (size, end); pop heap top with end<q; answer = heap.top.size
Complexity
Edge cases & gotchas
- No covering interval → -1.
Java code
import java.util.*;
class Solution {
public int[] minInterval(int[][] iv, int[] q) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
Integer[] idx = new Integer[q.length];
for (int i = 0; i < q.length; i++) idx[i] = i;
Arrays.sort(idx, (a, b) -> q[a] - q[b]);
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
int[] out = new int[q.length];
int j = 0;
for (int i : idx) {
int qi = q[i];
while (j < iv.length && iv[j][0] <= qi) { pq.offer(new int[]{iv[j][1] - iv[j][0] + 1, iv[j][1]}); j++; }
while (!pq.isEmpty() && pq.peek()[1] < qi) pq.poll();
out[i] = pq.isEmpty() ? -1 : pq.peek()[0];
}
return out;
}
}
Sweep with sorted intervals + TreeSet O((n+q) log n) O(n)
Insert intervals as queries advance; track active by size.
Pseudo-code
sort intervals by start; sort queries; for each: add intervals with start<=q to BST keyed by (size, end); remove expired; answer = first.size
Complexity
Java code
import java.util.*;
class Solution {
public int[] minInterval(int[][] iv, int[] q) {
Arrays.sort(iv, (a, b) -> a[0] - b[0]);
Integer[] idx = new Integer[q.length];
for (int i = 0; i < q.length; i++) idx[i] = i;
Arrays.sort(idx, (a, b) -> q[a] - q[b]);
// size, end
TreeSet<long[]> active = new TreeSet<>((a, b) -> a[0] != b[0] ? Long.compare(a[0], b[0]) : Long.compare(a[1], b[1]));
int[] out = new int[q.length]; int j = 0;
for (int i : idx) {
int qi = q[i];
while (j < iv.length && iv[j][0] <= qi) { active.add(new long[]{iv[j][1] - iv[j][0] + 1L, iv[j][1]}); j++; }
// remove expired by linear scan (rare in practice; could index differently)
while (!active.isEmpty()) {
long[] f = active.first();
if (f[1] < qi) active.remove(f); else break;
}
out[i] = active.isEmpty() ? -1 : (int) active.first()[0];
}
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·q) | O((n+q)log(n+q)) | O((n+q)log n) |
| Space | O(q) | O(n+q) | O(n) |
| Difficulty | 1/5 | 5/5 | 4/5 |
| When | Tiny | Default | Variant |
| PE Verdict | Offline sort + min-heap of (size, end). | ||
Python Solutions
Per-query scan O(n·q) O(q)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def minInterval(self, iv: List[List[int]], q: List[int]) -> List[int]:
out = []
for qi in q:
best = float('inf')
for l, r in iv:
if l <= qi <= r: best = min(best, r - l + 1)
out.append(-1 if best == float('inf') else best)
return out
Offline + min-heap O((n+q) log) O(n+q)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import heapq
class Solution:
def minInterval(self, iv: List[List[int]], q: List[int]) -> List[int]:
iv.sort()
idx = sorted(range(len(q)), key=lambda i: q[i])
out = [0]*len(q); h = []; j = 0
for i in idx:
qi = q[i]
while j < len(iv) and iv[j][0] <= qi:
heapq.heappush(h, (iv[j][1] - iv[j][0] + 1, iv[j][1])); j += 1
while h and h[0][1] < qi: heapq.heappop(h)
out[i] = h[0][0] if h else -1
return out
SortedList O((n+q)log n) O(n)
Same idea.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from sortedcontainers import SortedList
class Solution:
def minInterval(self, iv: List[List[int]], q: List[int]) -> List[int]:
iv.sort()
idx = sorted(range(len(q)), key=lambda i: q[i])
out = [0]*len(q); sl = SortedList(); j = 0
for i in idx:
qi = q[i]
while j < len(iv) and iv[j][0] <= qi:
sl.add((iv[j][1] - iv[j][0] + 1, iv[j][1])); j += 1
while sl and sl[0][1] < qi: sl.pop(0)
out[i] = sl[0][0] if sl else -1
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·q) | O((n+q)log) | O((n+q)log n) |
| Space | O(q) | O(n+q) | O(n) |
| Difficulty | 1/5 | 5/5 | 4/5 |
| When | Tiny | Default | Variant |
| PE Verdict | Offline sort + min-heap. | ||
What the interviewer is really testing
Offline processing — answer queries in convenient order.
Top gotchas
- Sort intervals by start; queries by value.
- Heap key = (size, end); pop top while top.end < q.
- Write to out[i] in ORIGINAL order — track index permutation.
Ship-it
Offline sort + min-heap.
Math & Geometry
#136 LC 48 Medium Rotate Image
Transpose then reverse each row — in-place 90° CW.
Problem Statement
Rotate n×n matrix 90° clockwise in place.
Examples
Constraints
- 1 <= n <= 20
Approach Overview
Brute Force
Java: New matrix
Python: New matrix
Optimal #1
Java: Transpose + reverse rows
Python: Transpose + reverse
Optimal #2
Java: Four-cycle in place
Python: Four-cycle
Java Solutions
New matrix O(n²) O(n²)
out[j][n-1-i] = in[i][j].
Pseudo-code
create new; copy with new[j][n-1-i]=old[i][j]
Complexity
Java code
class Solution {
public void rotate(int[][] m) {
int n = m.length;
int[][] r = new int[n][n];
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) r[j][n-1-i] = m[i][j];
for (int i = 0; i < n; i++) System.arraycopy(r[i], 0, m[i], 0, n);
}
}
Transpose + reverse rows O(n²) O(1)
Two passes, both in place.
Pseudo-code
transpose: swap m[i][j] and m[j][i] for j>i; then reverse each row
Complexity
Java code
class Solution {
public void rotate(int[][] m) {
int n = m.length;
for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) { int t = m[i][j]; m[i][j] = m[j][i]; m[j][i] = t; }
for (int i = 0; i < n; i++) for (int l = 0, r = n - 1; l < r; l++, r--) { int t = m[i][l]; m[i][l] = m[i][r]; m[i][r] = t; }
}
}
Four-cycle in place O(n²) O(1)
Rotate 4 corners at a time.
Pseudo-code
for layer 0..n/2: for i in layer..n-1-layer-1: swap 4 cells
Complexity
Java code
class Solution {
public void rotate(int[][] m) {
int n = m.length;
for (int l = 0; l < n / 2; l++) {
for (int i = l; i < n - 1 - l; i++) {
int t = m[l][i];
m[l][i] = m[n-1-i][l];
m[n-1-i][l] = m[n-1-l][n-1-i];
m[n-1-l][n-1-i] = m[i][n-1-l];
m[i][n-1-l] = t;
}
}
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n²) | O(n²) |
| Space | O(n²) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Single-pass elegance |
| PE Verdict | Transpose + reverse rows. | ||
Python Solutions
New matrix O(n²) O(n²)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rotate(self, m: List[List[int]]) -> None:
n = len(m)
r = [[0]*n for _ in range(n)]
for i in range(n):
for j in range(n): r[j][n-1-i] = m[i][j]
for i in range(n): m[i] = r[i]
Transpose + reverse O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rotate(self, m: List[List[int]]) -> None:
n = len(m)
for i in range(n):
for j in range(i+1, n): m[i][j], m[j][i] = m[j][i], m[i][j]
for row in m: row.reverse()
Four-cycle O(n²) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def rotate(self, m: List[List[int]]) -> None:
n = len(m)
for l in range(n // 2):
for i in range(l, n - 1 - l):
t = m[l][i]
m[l][i] = m[n-1-i][l]
m[n-1-i][l] = m[n-1-l][n-1-i]
m[n-1-l][n-1-i] = m[i][n-1-l]
m[i][n-1-l] = t
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n²) | O(n²) | O(n²) |
| Space | O(n²) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Elegant |
| PE Verdict | Transpose + reverse rows. | ||
What the interviewer is really testing
Matrix manipulation classic.
Top gotchas
- Transpose: swap with j > i (only upper triangle).
- Then reverse each row for CW rotation.
- CCW would be transpose + reverse COLUMNS.
Ship-it
Transpose + reverse rows.
#137 LC 54 Medium Spiral Matrix
Walk in 4 directions; shrink boundaries each layer.
Problem Statement
Return all matrix elements in spiral order.
Examples
Constraints
- 1 <= m, n <= 10
Approach Overview
Brute Force
Java: Visited grid
Python: Visited
Optimal #1
Java: Shrink boundaries
Python: Boundaries
Optimal #2
Java: Recursive peel
Python: Recursive peel
Java Solutions
Visited grid O(m·n) O(m·n)
Walk with 4-dir cycle.
Pseudo-code
dirs=[(0,1),(1,0),(0,-1),(-1,0)]; walk; turn when out/visited
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> spiralOrder(int[][] m) {
int M = m.length, N = m[0].length;
boolean[][] v = new boolean[M][N];
int[][] D = {{0,1},{1,0},{0,-1},{-1,0}};
int d = 0, r = 0, c = 0;
List<Integer> out = new ArrayList<>();
for (int k = 0; k < M * N; k++) {
out.add(m[r][c]); v[r][c] = true;
int nr = r + D[d][0], nc = c + D[d][1];
if (nr < 0 || nc < 0 || nr >= M || nc >= N || v[nr][nc]) { d = (d + 1) % 4; nr = r + D[d][0]; nc = c + D[d][1]; }
r = nr; c = nc;
}
return out;
}
}
Shrink boundaries O(m·n) O(1)
Walk top→right→bottom→left; shrink.
Pseudo-code
top,bot,left,right; while top<=bot and left<=right: 4 walks; shrink
Complexity
Edge cases & gotchas
- Single row/col — guard both bot>=top and right>=left.
Java code
import java.util.*;
class Solution {
public List<Integer> spiralOrder(int[][] m) {
List<Integer> out = new ArrayList<>();
int top = 0, bot = m.length - 1, left = 0, right = m[0].length - 1;
while (top <= bot && left <= right) {
for (int c = left; c <= right; c++) out.add(m[top][c]);
top++;
for (int r = top; r <= bot; r++) out.add(m[r][right]);
right--;
if (top <= bot) for (int c = right; c >= left; c--) out.add(m[bot][c]);
bot--;
if (left <= right) for (int r = bot; r >= top; r--) out.add(m[r][left]);
left++;
}
return out;
}
}
Recursive peel O(m·n) O(min(m,n))
Strip outermost ring, recurse on inner.
Pseudo-code
emit ring; recurse on submatrix
Complexity
Java code
import java.util.*;
class Solution {
public List<Integer> spiralOrder(int[][] m) {
List<Integer> out = new ArrayList<>();
peel(m, 0, 0, m.length - 1, m[0].length - 1, out);
return out;
}
private void peel(int[][] m, int top, int left, int bot, int right, List<Integer> out) {
if (top > bot || left > right) return;
for (int c = left; c <= right; c++) out.add(m[top][c]);
for (int r = top + 1; r <= bot; r++) out.add(m[r][right]);
if (top < bot) for (int c = right - 1; c >= left; c--) out.add(m[bot][c]);
if (left < right) for (int r = bot - 1; r > top; r--) out.add(m[r][left]);
peel(m, top + 1, left + 1, bot - 1, right - 1, out);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n) |
| Space | O(m·n) | O(1) | O(min(m,n)) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | Shrink boundaries. | ||
Python Solutions
Visited O(m·n) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def spiralOrder(self, m: List[List[int]]) -> List[int]:
M, N = len(m), len(m[0]); v = [[False]*N for _ in range(M)]
D = [(0,1),(1,0),(0,-1),(-1,0)]
d = r = c = 0; out = []
for _ in range(M*N):
out.append(m[r][c]); v[r][c] = True
nr, nc = r + D[d][0], c + D[d][1]
if not (0 <= nr < M and 0 <= nc < N) or v[nr][nc]:
d = (d + 1) % 4; nr, nc = r + D[d][0], c + D[d][1]
r, c = nr, nc
return out
Boundaries O(m·n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def spiralOrder(self, m: List[List[int]]) -> List[int]:
out = []; top, bot, left, right = 0, len(m)-1, 0, len(m[0])-1
while top <= bot and left <= right:
for c in range(left, right+1): out.append(m[top][c])
top += 1
for r in range(top, bot+1): out.append(m[r][right])
right -= 1
if top <= bot:
for c in range(right, left-1, -1): out.append(m[bot][c])
bot -= 1
if left <= right:
for r in range(bot, top-1, -1): out.append(m[r][left])
left += 1
return out
Recursive peel O(m·n) O(min(m,n))
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def spiralOrder(self, m: List[List[int]]) -> List[int]:
out = []
def peel(top, left, bot, right):
if top > bot or left > right: return
for c in range(left, right+1): out.append(m[top][c])
for r in range(top+1, bot+1): out.append(m[r][right])
if top < bot:
for c in range(right-1, left-1, -1): out.append(m[bot][c])
if left < right:
for r in range(bot-1, top, -1): out.append(m[r][left])
peel(top+1, left+1, bot-1, right-1)
peel(0, 0, len(m)-1, len(m[0])-1)
return out
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n) |
| Space | O(m·n) | O(1) | O(min(m,n)) |
| Difficulty | 2/5 | 3/5 | 3/5 |
| When | Don't | Default | Pedagogical |
| PE Verdict | Shrink boundaries. | ||
What the interviewer is really testing
Boundary shrink pattern.
Top gotchas
- Guard bottom/left walks with top<=bot and left<=right after shrinking.
- Edge case: single row or col — skipping prevents duplicates.
- Verify increment/decrement order to avoid off-by-one.
Ship-it
Shrink boundaries.
#138 LC 73 Medium Set Matrix Zeroes
Use first row & col as in-place markers; track them separately.
Problem Statement
If element is 0, zero out its row and column. In place.
Examples
Constraints
- 1 <= m, n <= 200
Approach Overview
Brute Force
Java: Copy matrix
Python: Copy matrix
Optimal #1
Java: O(1) markers
Python: O(1) markers
Optimal #2
Java: O(m+n) row/col sets
Python: O(m+n) sets
Java Solutions
Copy matrix O(m·n) O(m·n)
Scan; zero rows/cols in a copy.
Pseudo-code
copy m; scan original; if 0 → zero row+col in copy; copy back
Complexity
Java code
class Solution {
public void setZeroes(int[][] m) {
int M = m.length, N = m[0].length;
int[][] c = new int[M][N];
for (int i = 0; i < M; i++) c[i] = m[i].clone();
for (int i = 0; i < M; i++) for (int j = 0; j < N; j++) if (m[i][j] == 0) {
for (int k = 0; k < N; k++) c[i][k] = 0;
for (int k = 0; k < M; k++) c[k][j] = 0;
}
for (int i = 0; i < M; i++) m[i] = c[i];
}
}
O(1) markers O(m·n) O(1)
First row & col as markers; track them via two booleans.
Pseudo-code
firstRowZ, firstColZ flags; mark m[i][0] and m[0][j]; second pass zero from markers; finally zero first row/col if needed
Complexity
Java code
class Solution {
public void setZeroes(int[][] m) {
int M = m.length, N = m[0].length;
boolean firstRowZ = false, firstColZ = false;
for (int j = 0; j < N; j++) if (m[0][j] == 0) { firstRowZ = true; break; }
for (int i = 0; i < M; i++) if (m[i][0] == 0) { firstColZ = true; break; }
for (int i = 1; i < M; i++) for (int j = 1; j < N; j++) if (m[i][j] == 0) { m[i][0] = 0; m[0][j] = 0; }
for (int i = 1; i < M; i++) for (int j = 1; j < N; j++) if (m[i][0] == 0 || m[0][j] == 0) m[i][j] = 0;
if (firstRowZ) for (int j = 0; j < N; j++) m[0][j] = 0;
if (firstColZ) for (int i = 0; i < M; i++) m[i][0] = 0;
}
}
O(m+n) row/col sets O(m·n) O(m+n)
Track zero rows and cols in sets.
Pseudo-code
first pass find zero rows/cols; second pass zero matching
Complexity
Java code
import java.util.*;
class Solution {
public void setZeroes(int[][] m) {
Set<Integer> rows = new HashSet<>(), cols = new HashSet<>();
for (int i = 0; i < m.length; i++) for (int j = 0; j < m[0].length; j++)
if (m[i][j] == 0) { rows.add(i); cols.add(j); }
for (int i = 0; i < m.length; i++) for (int j = 0; j < m[0].length; j++)
if (rows.contains(i) || cols.contains(j)) m[i][j] = 0;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n) |
| Space | O(m·n) | O(1) | O(m+n) |
| Difficulty | 1/5 | 4/5 | 2/5 |
| When | Don't | Default — O(1) space | Easy variant |
| PE Verdict | First row/col as markers + 2 bools. | ||
Python Solutions
Copy matrix O(m·n) O(m·n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
import copy
class Solution:
def setZeroes(self, m: List[List[int]]) -> None:
M, N = len(m), len(m[0])
c = copy.deepcopy(m)
for i in range(M):
for j in range(N):
if m[i][j] == 0:
for k in range(N): c[i][k] = 0
for k in range(M): c[k][j] = 0
for i in range(M): m[i] = c[i]
O(1) markers O(m·n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def setZeroes(self, m: List[List[int]]) -> None:
M, N = len(m), len(m[0])
firstRowZ = any(m[0][j] == 0 for j in range(N))
firstColZ = any(m[i][0] == 0 for i in range(M))
for i in range(1, M):
for j in range(1, N):
if m[i][j] == 0: m[i][0] = 0; m[0][j] = 0
for i in range(1, M):
for j in range(1, N):
if m[i][0] == 0 or m[0][j] == 0: m[i][j] = 0
if firstRowZ:
for j in range(N): m[0][j] = 0
if firstColZ:
for i in range(M): m[i][0] = 0
O(m+n) sets O(m·n) O(m+n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def setZeroes(self, m: List[List[int]]) -> None:
rows, cols = set(), set()
for i in range(len(m)):
for j in range(len(m[0])):
if m[i][j] == 0: rows.add(i); cols.add(j)
for i in range(len(m)):
for j in range(len(m[0])):
if i in rows or j in cols: m[i][j] = 0
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(m·n) | O(m·n) | O(m·n) |
| Space | O(m·n) | O(1) | O(m+n) |
| Difficulty | 1/5 | 4/5 | 2/5 |
| When | Don't | Default | Easy alt |
| PE Verdict | First row/col as markers. | ||
What the interviewer is really testing
In-place trick uses existing memory.
Top gotchas
- First row/col DOUBLES as marker and target — track separately with bools.
- Apply markers AFTER you've finished marking.
- Finally zero first row/col if their bool is set.
Ship-it
O(1) markers.
#139 LC 202 Easy Happy Number
Floyd's tortoise & hare on the digit-square function — detect cycle.
Problem Statement
Sum of squares of digits, repeat. Happy if reaches 1; else loops.
Examples
Constraints
- 1 <= n <= 2^31 - 1
Approach Overview
Brute Force
Java: HashSet of seen
Python: Seen set
Optimal #1
Java: Floyd's cycle detection
Python: Floyd's
Optimal #2
Java: Known unhappy cycle includes 4
Python: Hits 4 shortcut
Java Solutions
HashSet of seen O(log n)* O(log n)*
Track visited; if revisit → false.
Pseudo-code
set; while n != 1: if n in set: false; add; n = squareSum(n)
Complexity
Edge cases & gotchas
- n=1 → true.
Java code
import java.util.*;
class Solution {
public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1) {
if (!seen.add(n)) return false;
n = sq(n);
}
return true;
}
private int sq(int n) { int s = 0; while (n > 0) { int d = n % 10; s += d*d; n /= 10; } return s; }
}
Floyd's cycle detection O(log n)* O(1)
Slow/fast pointers; meet implies cycle.
Pseudo-code
slow=n; fast=sq(n); while fast!=1 and slow!=fast: slow=sq(slow); fast=sq(sq(fast)); return fast==1
Complexity
Java code
class Solution {
public boolean isHappy(int n) {
int slow = n, fast = sq(n);
while (fast != 1 && slow != fast) {
slow = sq(slow);
fast = sq(sq(fast));
}
return fast == 1;
}
private int sq(int n) { int s = 0; while (n > 0) { int d = n % 10; s += d*d; n /= 10; } return s; }
}
Known unhappy cycle includes 4 O(log n)* O(1)
Every unhappy number eventually hits 4.
Pseudo-code
while n != 1 and n != 4: n = sq(n); return n == 1
Complexity
Java code
class Solution {
public boolean isHappy(int n) {
while (n != 1 && n != 4) n = sq(n);
return n == 1;
}
private int sq(int n) { int s = 0; while (n > 0) { int d = n % 10; s += d*d; n /= 10; } return s; }
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(log n)* | O(log n)* | O(log n)* |
| Space | O(log n)* | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 2/5 |
| When | Don't | Default | Mathy shortcut |
| PE Verdict | Floyd's cycle detection. | ||
Python Solutions
Seen set O(log n)* O(log n)*
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isHappy(self, n: int) -> bool:
seen = set()
def sq(x):
s = 0
while x: x, d = divmod(x, 10); s += d*d
return s
while n != 1:
if n in seen: return False
seen.add(n); n = sq(n)
return True
Floyd's O(log n)* O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isHappy(self, n: int) -> bool:
def sq(x):
s = 0
while x: x, d = divmod(x, 10); s += d*d
return s
slow, fast = n, sq(n)
while fast != 1 and slow != fast:
slow = sq(slow); fast = sq(sq(fast))
return fast == 1
Hits 4 shortcut O(log n)* O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def isHappy(self, n: int) -> bool:
def sq(x):
s = 0
while x: x, d = divmod(x, 10); s += d*d
return s
while n != 1 and n != 4: n = sq(n)
return n == 1
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(log n)* | O(log n)* | O(log n)* |
| Space | O(log n)* | O(1) | O(1) |
| Difficulty | 1/5 | 3/5 | 2/5 |
| When | Don't | Default | Mathy |
| PE Verdict | Floyd's cycle. | ||
What the interviewer is really testing
Cycle detection on implicit sequence.
Top gotchas
- Sequence either reaches 1 or enters a small cycle (4→16→37→58→89→145→42→20→4).
- Floyd's two-pointer avoids a hash set.
- Numbers bounded — for big input, max ~3 digits eventually.
Ship-it
Floyd's cycle detection.
#140 LC 66 Easy Plus One
Walk right-to-left; carry; if leftmost overflows, prepend 1.
Problem Statement
Increment large integer represented as digit array.
Examples
Constraints
- 1 <= n <= 100
Approach Overview
Brute Force
Java: BigInt convert
Python: Int convert
Optimal #1
Java: Carry walk
Python: Carry walk
Optimal #2
Java: Single-pass with carry
Python: Explicit carry
Java Solutions
BigInt convert O(n) O(n)
Form number, add 1, split back.
Pseudo-code
BigInt.valueOf(digits) + 1; split
Complexity
Java code
import java.math.BigInteger;
class Solution {
public int[] plusOne(int[] d) {
StringBuilder sb = new StringBuilder();
for (int x : d) sb.append(x);
BigInteger n = new BigInteger(sb.toString()).add(BigInteger.ONE);
String s = n.toString();
int[] out = new int[s.length()];
for (int i = 0; i < s.length(); i++) out[i] = s.charAt(i) - '0';
return out;
}
}
Carry walk O(n) O(1)
Right-to-left; if digit<9, ++ and return.
Pseudo-code
for i=n-1..0: if d[i]<9: d[i]++; return d; d[i]=0; return [1]+d
Complexity
Edge cases & gotchas
- All 9s → length grows by 1.
Java code
class Solution {
public int[] plusOne(int[] d) {
for (int i = d.length - 1; i >= 0; i--) {
if (d[i] < 9) { d[i]++; return d; }
d[i] = 0;
}
int[] out = new int[d.length + 1];
out[0] = 1;
return out;
}
}
Single-pass with carry O(n) O(1)
Explicit carry variable.
Pseudo-code
carry=1; for i=n-1..0: t=d[i]+carry; d[i]=t%10; carry=t/10; if carry: prepend
Complexity
Java code
class Solution {
public int[] plusOne(int[] d) {
int carry = 1;
for (int i = d.length - 1; i >= 0; i--) {
int t = d[i] + carry;
d[i] = t % 10;
carry = t / 10;
if (carry == 0) return d;
}
int[] out = new int[d.length + 1];
out[0] = 1;
return out;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default — short | Explicit carry |
| PE Verdict | Carry walk. | ||
Python Solutions
Int convert O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def plusOne(self, d: List[int]) -> List[int]:
n = int(''.join(map(str, d))) + 1
return [int(c) for c in str(n)]
Carry walk O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def plusOne(self, d: List[int]) -> List[int]:
for i in range(len(d) - 1, -1, -1):
if d[i] < 9: d[i] += 1; return d
d[i] = 0
return [1] + d
Explicit carry O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def plusOne(self, d: List[int]) -> List[int]:
carry = 1
for i in range(len(d) - 1, -1, -1):
t = d[i] + carry
d[i] = t % 10; carry = t // 10
if not carry: return d
return [1] + d
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | Carry walk. | ||
What the interviewer is really testing
Toy problem, watch overflow.
Top gotchas
- All-9 case grows array length by 1.
- Don't convert via int for huge inputs (use opt1 in interview).
- Walk right-to-left, return as soon as no carry.
Ship-it
Carry walk.
#141 LC 50 Medium Pow(x, n)
Fast exponentiation — square the base, halve the exponent.
Problem Statement
Compute x raised to the integer power n.
Examples
Constraints
- -100.0 < x < 100.0
- -2^31 <= n <= 2^31 - 1
Approach Overview
Brute Force
Java: Linear multiply
Python: Linear
Optimal #1
Java: Iterative fast pow
Python: Iterative fast pow
Optimal #2
Java: Recursive divide & conquer
Python: Recursive D&C
Java Solutions
Linear multiply O(n) O(1)
Multiply x by itself |n| times.
Pseudo-code
r=1; for i in 1..|n|: r*=x; if n<0: r=1/r
Complexity
Edge cases & gotchas
- n=0 → 1.
- negative n.
Java code
class Solution {
public double myPow(double x, int n) {
long N = n;
boolean neg = N < 0; if (neg) N = -N;
double r = 1;
for (long i = 0; i < N; i++) r *= x;
return neg ? 1 / r : r;
}
}
Iterative fast pow O(log n) O(1)
Square x, halve n; multiply when bit set.
Pseudo-code
r=1; while n: if n&1: r*=x; x*=x; n>>=1; handle negative
Complexity
Edge cases & gotchas
- n = Integer.MIN_VALUE — use long.
Java code
class Solution {
public double myPow(double x, int n) {
long N = n;
if (N < 0) { x = 1 / x; N = -N; }
double r = 1;
while (N > 0) {
if ((N & 1) == 1) r *= x;
x *= x;
N >>= 1;
}
return r;
}
}
Recursive divide & conquer O(log n) O(log n)
pow(x, n) = pow(x, n/2)².
Pseudo-code
pow(x,n): if n==0 return 1; half = pow(x, n/2); return n%2==0 ? half*half : half*half*x
Complexity
Java code
class Solution {
public double myPow(double x, int n) {
long N = n;
if (N < 0) { x = 1 / x; N = -N; }
return helper(x, N);
}
private double helper(double x, long n) {
if (n == 0) return 1;
double half = helper(x, n / 2);
return n % 2 == 0 ? half * half : half * half * x;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(log n) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When | Don't | Default | Recursive clarity |
| PE Verdict | Iterative fast pow. | ||
Python Solutions
Linear O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def myPow(self, x: float, n: int) -> float:
neg = n < 0; n = abs(n); r = 1
for _ in range(n): r *= x
return 1 / r if neg else r
Iterative fast pow O(log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def myPow(self, x: float, n: int) -> float:
if n < 0: x = 1 / x; n = -n
r = 1.0
while n:
if n & 1: r *= x
x *= x; n >>= 1
return r
Recursive D&C O(log n) O(log n)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def myPow(self, x: float, n: int) -> float:
if n < 0: x = 1 / x; n = -n
def rec(b, e):
if e == 0: return 1
half = rec(b, e // 2)
return half * half if e % 2 == 0 else half * half * b
return rec(x, n)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(log n) | O(log n) |
| Space | O(1) | O(1) | O(log n) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When | Don't | Default | Recursive |
| PE Verdict | Iterative fast pow. | ||
What the interviewer is really testing
Exponentiation by squaring.
Top gotchas
- n = Integer.MIN_VALUE → must widen to long before negating.
- Negative exponent: invert base, negate exponent.
- Each iteration: square base, halve exponent, accumulate if bit set.
Ship-it
Iterative fast pow.
#142 LC 43 Medium Multiply Strings
Grade-school multiply — digit at position (i+j+1) ← num1[i]·num2[j].
Problem Statement
Multiply two non-negative integers represented as strings; return product string.
Examples
Constraints
- 1 <= len <= 200
Approach Overview
Brute Force
Java: BigInteger
Python: int()
Optimal #1
Java: Grade-school multiply
Python: Grade-school
Optimal #2
Java: Add partial products
Python: Add partials
Java Solutions
BigInteger O(n·m) O(n+m)
Use java.math.BigInteger.
Pseudo-code
new BigInteger(a).multiply(new BigInteger(b)).toString()
Complexity
Edge cases & gotchas
- Either '0' → '0'.
Java code
import java.math.BigInteger;
class Solution {
public String multiply(String a, String b) {
return new BigInteger(a).multiply(new BigInteger(b)).toString();
}
}
Grade-school multiply O(n·m) O(n+m)
Result[i+j+1] += a[i]·b[j]; carry up.
Pseudo-code
r = new int[n+m]; for i: for j: r[i+j+1] += a[i]*b[j]; pass: handle carry; trim leading zeros
Complexity
Java code
class Solution {
public String multiply(String a, String b) {
if (a.equals("0") || b.equals("0")) return "0";
int n = a.length(), m = b.length();
int[] r = new int[n + m];
for (int i = n - 1; i >= 0; i--) for (int j = m - 1; j >= 0; j--) {
int mul = (a.charAt(i) - '0') * (b.charAt(j) - '0');
int sum = r[i + j + 1] + mul;
r[i + j + 1] = sum % 10;
r[i + j] += sum / 10;
}
StringBuilder sb = new StringBuilder();
for (int d : r) if (!(sb.length() == 0 && d == 0)) sb.append(d);
return sb.length() == 0 ? "0" : sb.toString();
}
}
Add partial products O(n·m) O(n+m)
For each digit of b, multiply by a; shift; sum.
Pseudo-code
for each digit of b right-to-left: multiply by a; shift left; addStrings
Complexity
Java code
class Solution {
public String multiply(String a, String b) {
if (a.equals("0") || b.equals("0")) return "0";
String r = "0";
for (int i = b.length() - 1; i >= 0; i--) {
int digit = b.charAt(i) - '0';
StringBuilder partial = new StringBuilder();
int carry = 0;
for (int j = a.length() - 1; j >= 0; j--) {
int p = (a.charAt(j) - '0') * digit + carry;
partial.append(p % 10);
carry = p / 10;
}
if (carry > 0) partial.append(carry);
partial.reverse();
for (int k = 0; k < b.length() - 1 - i; k++) partial.append('0');
r = add(r, partial.toString());
}
return r;
}
private String add(String a, String b) {
StringBuilder sb = new StringBuilder();
int i = a.length() - 1, j = b.length() - 1, carry = 0;
while (i >= 0 || j >= 0 || carry > 0) {
int s = carry;
if (i >= 0) s += a.charAt(i--) - '0';
if (j >= 0) s += b.charAt(j--) - '0';
sb.append(s % 10); carry = s / 10;
}
return sb.reverse().toString();
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·m) | O(n·m) | O(n·m + (n+m)·m) |
| Space | O(n+m) | O(n+m) | O(n+m) |
| Difficulty | 1/5 | 4/5 | 4/5 |
| When | Allowed | Default | Show partials |
| PE Verdict | Grade-school positional multiply. | ||
Python Solutions
int() O(n·m) O(n+m)
Cast strings to ints.
Pseudo-code
return str(int(a)*int(b))
Complexity
Python code
class Solution:
def multiply(self, a: str, b: str) -> str:
return str(int(a) * int(b))
Grade-school O(n·m) O(n+m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def multiply(self, a: str, b: str) -> str:
if a == '0' or b == '0': return '0'
n, m = len(a), len(b)
r = [0]*(n+m)
for i in range(n-1, -1, -1):
for j in range(m-1, -1, -1):
mul = (ord(a[i])-48) * (ord(b[j])-48)
s = r[i+j+1] + mul
r[i+j+1] = s % 10
r[i+j] += s // 10
s = ''.join(str(d) for d in r).lstrip('0')
return s or '0'
Add partials O(n·m) O(n+m)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def multiply(self, a: str, b: str) -> str:
if a == '0' or b == '0': return '0'
def add(x, y):
i, j, carry = len(x)-1, len(y)-1, 0
out = []
while i >= 0 or j >= 0 or carry:
s = carry
if i >= 0: s += ord(x[i])-48; i -= 1
if j >= 0: s += ord(y[j])-48; j -= 1
out.append(str(s % 10)); carry = s // 10
return ''.join(reversed(out))
r = '0'
for i in range(len(b)-1, -1, -1):
d = ord(b[i]) - 48
partial = []
carry = 0
for j in range(len(a)-1, -1, -1):
p = (ord(a[j])-48)*d + carry
partial.append(str(p % 10)); carry = p // 10
if carry: partial.append(str(carry))
partial = ''.join(reversed(partial)) + '0' * (len(b)-1-i)
r = add(r, partial)
return r
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n·m) | O(n·m) | O(n·m) |
| Space | O(n+m) | O(n+m) | O(n+m) |
| Difficulty | 1/5 | 4/5 | 4/5 |
| When | Allowed | Default | Partials |
| PE Verdict | Grade-school positional. | ||
What the interviewer is really testing
Big-integer multiplication without bignum.
Top gotchas
- Result length is at most n+m digits.
- Position formula: a[i]*b[j] lands at r[i+j+1] with carry to r[i+j].
- Strip leading zeros at the end; handle '0' input specially.
Ship-it
Grade-school positional multiply.
#143 LC 2013 Medium Detect Squares
Map point counts; for each diagonal pair, multiply the other two corners' counts.
Problem Statement
Design DS: add points; count axis-aligned squares with a query point as one corner.
Examples
Constraints
- 0 <= x, y <= 1000
Approach Overview
Brute Force
Java: Scan all 3-tuples
Python: Triple loop
Optimal #1
Java: Count map + diagonal pivot
Python: Map + diagonal pivot
Optimal #2
Java: Same idea, iterate over map entries
Python: Iterate distinct entries
Java Solutions
Scan all 3-tuples O(n³) O(n)
For each pair of distinct points, check square.
Pseudo-code
for each pair (a,b): determine third+fourth; if present count
Complexity
Edge cases & gotchas
- Zero-area squares not counted.
Java code
import java.util.*;
class DetectSquares {
private List<int[]> pts = new ArrayList<>();
public void add(int[] p) { pts.add(p); }
public int count(int[] p) {
int total = 0;
for (int[] a : pts) for (int[] b : pts) for (int[] c : pts) {
// a,b,c + p form a square?
int[][] q = {a, b, c, p};
if (isSquare(q)) total++;
}
return total / 8; // permutations
}
private boolean isSquare(int[][] q) { return false; /* simplistic placeholder */ }
}
Count map + diagonal pivot O(k) per query O(k)
For each existing point with same |dx|=|dy| from query, multiply by counts of the other two corners.
Pseudo-code
map (x,y) → count; query (qx,qy): for each (x,y): if |qx-x|==|qy-y|>0: total += cnt[(x,y)]*cnt[(x,qy)]*cnt[(qx,y)]
Complexity
Edge cases & gotchas
- Zero-length sides skipped (qx-x>0).
Java code
import java.util.*;
class DetectSquares {
private Map<Long, Integer> cnt = new HashMap<>();
private List<int[]> pts = new ArrayList<>();
private long key(int x, int y) { return (long) x * 2001 + y; }
public void add(int[] p) {
cnt.merge(key(p[0], p[1]), 1, Integer::sum);
pts.add(p);
}
public int count(int[] p) {
int qx = p[0], qy = p[1], total = 0;
for (int[] o : pts) {
int x = o[0], y = o[1];
if (Math.abs(qx - x) != Math.abs(qy - y) || qx == x) continue;
total += cnt.getOrDefault(key(x, qy), 0) * cnt.getOrDefault(key(qx, y), 0);
}
return total;
}
}
Same idea, iterate over map entries O(k) O(k)
Iterate cnt.entrySet() instead of duplicates.
Pseudo-code
for each (x,y) in cnt: same logic; multiply by cnt of that key
Complexity
Java code
import java.util.*;
class DetectSquares {
private Map<Long, Integer> cnt = new HashMap<>();
private long key(int x, int y) { return (long) x * 2001 + y; }
public void add(int[] p) { cnt.merge(key(p[0], p[1]), 1, Integer::sum); }
public int count(int[] p) {
int qx = p[0], qy = p[1], total = 0;
for (Map.Entry<Long, Integer> e : cnt.entrySet()) {
long k = e.getKey();
int x = (int) (k / 2001), y = (int) (k % 2001);
if (Math.abs(qx - x) != Math.abs(qy - y) || qx == x) continue;
total += e.getValue() * cnt.getOrDefault(key(x, qy), 0) * cnt.getOrDefault(key(qx, y), 0);
}
return total;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n) per query | O(k) per query |
| Space | O(n) | O(k+n) | O(k) |
| Difficulty | 1/5 | 4/5 | 4/5 |
| When | Don't | Default | Many duplicates |
| PE Verdict | Pivot through diagonal partner. | ||
Python Solutions
Triple loop O(n³) O(n)
Same idea (rough sketch).
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class DetectSquares:
def __init__(self):
self.pts = []; self.cnt = Counter()
def add(self, p: List[int]) -> None:
self.pts.append(tuple(p)); self.cnt[tuple(p)] += 1
def count(self, p: List[int]) -> int:
qx, qy = p; total = 0
# naive O(n^3) skeleton; opt1 is what you'd ship
for a in self.pts:
for b in self.pts:
if a == b: continue
# too slow in practice; included for illustration
pass
return self.fast_count(p)
def fast_count(self, p):
qx, qy = p; total = 0
for (x, y), c in list(self.cnt.items()):
if abs(qx-x) != abs(qy-y) or qx == x: continue
total += c * self.cnt.get((x, qy), 0) * self.cnt.get((qx, y), 0)
return total
Map + diagonal pivot O(n) per query O(k+n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class DetectSquares:
def __init__(self):
self.pts = []
self.cnt = Counter()
def add(self, p: List[int]) -> None:
self.pts.append(tuple(p))
self.cnt[tuple(p)] += 1
def count(self, p: List[int]) -> int:
qx, qy = p; total = 0
for (x, y) in self.pts:
if abs(qx-x) != abs(qy-y) or qx == x: continue
total += self.cnt.get((x, qy), 0) * self.cnt.get((qx, y), 0)
return total
Iterate distinct entries O(k) O(k)
Iterate cnt instead of pts.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from collections import Counter
class DetectSquares:
def __init__(self):
self.cnt = Counter()
def add(self, p: List[int]) -> None:
self.cnt[tuple(p)] += 1
def count(self, p: List[int]) -> int:
qx, qy = p; total = 0
for (x, y), c in self.cnt.items():
if abs(qx-x) != abs(qy-y) or qx == x: continue
total += c * self.cnt.get((x, qy), 0) * self.cnt.get((qx, y), 0)
return total
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n³) | O(n) per query | O(k) per query |
| Space | O(n) | O(k+n) | O(k) |
| Difficulty | 1/5 | 4/5 | 4/5 |
| When | Don't | Default | Duplicates |
| PE Verdict | Pivot through diagonal partner. | ||
What the interviewer is really testing
Use the diagonal opposite as pivot, then look up the two side corners.
Top gotchas
- Skip zero-area squares (qx == x).
- Multiply counts of (x, qy) and (qx, y) — duplicates contribute correctly.
- Map by (x, y) with COUNT, not just presence.
Ship-it
Pivot through diagonal partner.
Bit Manipulation
#144 LC 136 Easy Single Number
XOR everything — pairs cancel, singleton remains.
Problem Statement
Every element appears twice except one. Find it. O(n) time, O(1) space.
Examples
Constraints
- 1 <= n <= 3·10^4
Approach Overview
Brute Force
Java: HashSet
Python: Set
Optimal #1
Java: XOR fold
Python: XOR fold
Optimal #2
Java: Sort + scan pairs
Python: Sort + pair scan
Java Solutions
HashSet O(n) O(n)
Add/remove to find the odd one out.
Pseudo-code
set; for x: if in set remove else add; final set has the answer
Complexity
Edge cases & gotchas
- n=1 → nums[0].
Java code
import java.util.*;
class Solution {
public int singleNumber(int[] nums) {
Set<Integer> s = new HashSet<>();
for (int x : nums) if (!s.add(x)) s.remove(x);
return s.iterator().next();
}
}
XOR fold O(n) O(1)
Each duplicate XORs to 0; singleton remains.
Pseudo-code
r=0; for x: r ^= x; return r
Complexity
Java code
class Solution {
public int singleNumber(int[] nums) {
int r = 0;
for (int x : nums) r ^= x;
return r;
}
}
Sort + scan pairs O(n log n) O(1)
After sort, the odd one out breaks pairing.
Pseudo-code
sort; for i in 0..n by 2: if nums[i]!=nums[i+1] return nums[i]; return nums[n-1]
Complexity
Java code
import java.util.*;
class Solution {
public int singleNumber(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i + 1 < nums.length; i += 2) if (nums[i] != nums[i+1]) return nums[i];
return nums[nums.length - 1];
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default — slick | No XOR allowed |
| PE Verdict | XOR fold. | ||
Python Solutions
Set O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def singleNumber(self, nums: List[int]) -> int:
s = set()
for x in nums:
if x in s: s.remove(x)
else: s.add(x)
return s.pop()
XOR fold O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
from functools import reduce
import operator
class Solution:
def singleNumber(self, nums: List[int]) -> int:
return reduce(operator.xor, nums)
Sort + pair scan O(n log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def singleNumber(self, nums: List[int]) -> int:
nums.sort()
for i in range(0, len(nums) - 1, 2):
if nums[i] != nums[i+1]: return nums[i]
return nums[-1]
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n) | O(n) | O(n log n) |
| Space | O(n) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 2/5 |
| When | Don't | Default | No XOR |
| PE Verdict | XOR fold. | ||
What the interviewer is really testing
Canonical XOR property: a^a=0, a^0=a, XOR is associative+commutative.
Top gotchas
- Init accumulator to 0.
- Works on negatives too — XOR is bitwise.
- Generalizes to Single Number II/III with bitwise counts.
Ship-it
XOR fold.
#145 LC 191 Easy Number of 1 Bits
n & (n-1) clears the lowest set bit — count iterations.
Problem Statement
Return number of 1-bits (Hamming weight).
Examples
Constraints
- 32-bit unsigned
Approach Overview
Brute Force
Java: Bit-by-bit shift
Python: Shift loop
Optimal #1
Java: Kernighan trick
Python: Kernighan
Optimal #2
Java: Builtin bitcount
Python: bin().count
Java Solutions
Bit-by-bit shift O(32) O(1)
Check LSB; shift right.
Pseudo-code
cnt=0; for i in 0..31: cnt += (n>>i)&1; return cnt
Complexity
Edge cases & gotchas
- Negative ints — use logical right shift.
Java code
class Solution {
public int hammingWeight(int n) {
int cnt = 0;
for (int i = 0; i < 32; i++) if (((n >>> i) & 1) == 1) cnt++;
return cnt;
}
}
Kernighan trick O(popcount) O(1)
n &= n-1 clears lowest 1; iterate until zero.
Pseudo-code
cnt=0; while n: n &= n-1; cnt++; return cnt
Complexity
Java code
class Solution {
public int hammingWeight(int n) {
int cnt = 0;
while (n != 0) { n &= n - 1; cnt++; }
return cnt;
}
}
Builtin bitcount O(1) O(1)
Integer.bitCount.
Pseudo-code
return Integer.bitCount(n)
Complexity
Java code
class Solution {
public int hammingWeight(int n) { return Integer.bitCount(n); }
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(32) | O(popcount) | O(1) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 1/5 |
| When | Don't | Default | Allowed builtin |
| PE Verdict | Kernighan trick. | ||
Python Solutions
Shift loop O(32) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def hammingWeight(self, n: int) -> int:
cnt = 0
for i in range(32):
if (n >> i) & 1: cnt += 1
return cnt
Kernighan O(popcount) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def hammingWeight(self, n: int) -> int:
cnt = 0
while n:
n &= n - 1; cnt += 1
return cnt
bin().count O(1) O(1)
Python builtin.
Pseudo-code
return bin(n).count('1')
Complexity
Python code
class Solution:
def hammingWeight(self, n: int) -> int:
return bin(n).count('1')
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(32) | O(popcount) | O(1) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 1/5 |
| When | Don't | Default | Allowed builtin |
| PE Verdict | Kernighan trick. | ||
What the interviewer is really testing
Bit-trick warmup.
Top gotchas
- n & (n-1) clears the LOWEST set bit — fewer iterations than 32-shift.
- Use unsigned right shift (>>>) in Java for negative ints.
- Builtin is fine but interviewer wants Kernighan.
Ship-it
Kernighan trick.
#146 LC 338 Easy Counting Bits
DP — bits(i) = bits(i >> 1) + (i & 1).
Problem Statement
Return array ans where ans[i] = number of 1-bits in i, for 0..n.
Examples
Constraints
- 0 <= n <= 10^5
Approach Overview
Brute Force
Java: Per-i bit count
Python: Per-i count
Optimal #1
Java: DP via i>>1
Python: DP via i>>1
Optimal #2
Java: DP via i & (i-1)
Python: DP via i & i-1
Java Solutions
Per-i bit count O(n log n) O(n)
Kernighan per element.
Pseudo-code
for i in 0..n: cnt = popcount(i)
Complexity
Java code
class Solution {
public int[] countBits(int n) {
int[] r = new int[n + 1];
for (int i = 0; i <= n; i++) {
int x = i, c = 0;
while (x != 0) { x &= x - 1; c++; }
r[i] = c;
}
return r;
}
}
DP via i>>1 O(n) O(n)
bits(i) = bits(i/2) + (i & 1).
Pseudo-code
r[0]=0; for i=1..n: r[i] = r[i>>1] + (i&1)
Complexity
Edge cases & gotchas
- n=0 → [0].
Java code
class Solution {
public int[] countBits(int n) {
int[] r = new int[n + 1];
for (int i = 1; i <= n; i++) r[i] = r[i >> 1] + (i & 1);
return r;
}
}
DP via i & (i-1) O(n) O(n)
bits(i) = bits(i & i-1) + 1.
Pseudo-code
r[0]=0; for i=1..n: r[i] = r[i & (i-1)] + 1
Complexity
Java code
class Solution {
public int[] countBits(int n) {
int[] r = new int[n + 1];
for (int i = 1; i <= n; i++) r[i] = r[i & (i - 1)] + 1;
return r;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | DP via i>>1. | ||
Python Solutions
Per-i count O(n log n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def countBits(self, n: int) -> List[int]:
return [bin(i).count('1') for i in range(n+1)]
DP via i>>1 O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def countBits(self, n: int) -> List[int]:
r = [0]*(n+1)
for i in range(1, n+1): r[i] = r[i >> 1] + (i & 1)
return r
DP via i & i-1 O(n) O(n)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def countBits(self, n: int) -> List[int]:
r = [0]*(n+1)
for i in range(1, n+1): r[i] = r[i & (i - 1)] + 1
return r
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n) |
| Space | O(n) | O(n) | O(n) |
| Difficulty | 1/5 | 3/5 | 3/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | DP via i>>1. | ||
What the interviewer is really testing
DP with bit-trick recurrence.
Top gotchas
- bits(i) = bits(i/2) + (i & 1) — the last bit dropped is the +1.
- Or bits(i) = bits(i & (i-1)) + 1 — Kernighan trick recurrence.
- Output length is n+1, not n.
Ship-it
DP via i>>1.
#147 LC 190 Easy Reverse Bits
Pop LSB; shift result left and OR.
Problem Statement
Reverse the bits of a 32-bit unsigned integer.
Examples
Constraints
- 32-bit
Approach Overview
Brute Force
Java: Bit-by-bit shift
Python: Bit-by-bit
Optimal #1
Java: Pop-and-shift
Python: Pop-and-shift
Optimal #2
Java: Byte-swap with mask
Python: Byte-swap masks
Java Solutions
Bit-by-bit shift O(32) O(1)
Iterate 32 bits.
Pseudo-code
r=0; for i=0..31: r |= ((n>>i)&1) << (31-i)
Complexity
Java code
class Solution {
public int reverseBits(int n) {
int r = 0;
for (int i = 0; i < 32; i++) r |= ((n >>> i) & 1) << (31 - i);
return r;
}
}
Pop-and-shift O(32) O(1)
Build r by pushing LSB; shift n right.
Pseudo-code
r=0; for 32: r = (r<<1) | (n & 1); n >>>= 1
Complexity
Java code
class Solution {
public int reverseBits(int n) {
int r = 0;
for (int i = 0; i < 32; i++) { r = (r << 1) | (n & 1); n >>>= 1; }
return r;
}
}
Byte-swap with mask O(1) O(1)
Pairs / nibbles / bytes — O(log 32).
Pseudo-code
swap adjacent bits, then pairs, then nibbles, then bytes, then halves
Complexity
Java code
class Solution {
public int reverseBits(int n) {
n = ((n & 0xFFFF0000) >>> 16) | ((n & 0x0000FFFF) << 16);
n = ((n & 0xFF00FF00) >>> 8) | ((n & 0x00FF00FF) << 8);
n = ((n & 0xF0F0F0F0) >>> 4) | ((n & 0x0F0F0F0F) << 4);
n = ((n & 0xCCCCCCCC) >>> 2) | ((n & 0x33333333) << 2);
n = ((n & 0xAAAAAAAA) >>> 1) | ((n & 0x55555555) << 1);
return n;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(32) | O(32) | O(1) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Hot path |
| PE Verdict | Pop-and-shift. | ||
Python Solutions
Bit-by-bit O(32) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseBits(self, n: int) -> int:
r = 0
for i in range(32): r |= ((n >> i) & 1) << (31 - i)
return r
Pop-and-shift O(32) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseBits(self, n: int) -> int:
r = 0
for _ in range(32):
r = (r << 1) | (n & 1); n >>= 1
return r
Byte-swap masks O(1) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverseBits(self, n: int) -> int:
n = ((n & 0xFFFF0000) >> 16) | ((n & 0x0000FFFF) << 16)
n = ((n & 0xFF00FF00) >> 8) | ((n & 0x00FF00FF) << 8)
n = ((n & 0xF0F0F0F0) >> 4) | ((n & 0x0F0F0F0F) << 4)
n = ((n & 0xCCCCCCCC) >> 2) | ((n & 0x33333333) << 2)
n = ((n & 0xAAAAAAAA) >> 1) | ((n & 0x55555555) << 1)
return n & 0xFFFFFFFF
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(32) | O(32) | O(1) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 4/5 |
| When | Don't | Default | Hot path |
| PE Verdict | Pop-and-shift. | ||
What the interviewer is really testing
Two natural patterns: bit-by-bit and mask swaps.
Top gotchas
- Use unsigned right shift (>>>) in Java; Python ints are arbitrary precision so mask with 0xFFFFFFFF.
- Bit-swap version is constant-time, useful in performance-critical contexts.
- Build the result by pushing LSB then shifting left — order matters.
Ship-it
Pop-and-shift.
#148 LC 268 Easy Missing Number
XOR indices and values — only the missing index survives. Or Gauss sum.
Problem Statement
Array contains n distinct numbers from [0..n]. Find the missing one.
Examples
Constraints
- 1 <= n <= 10^4
Approach Overview
Brute Force
Java: Sort + scan
Python: Sort + scan
Optimal #1
Java: XOR indices & values
Python: XOR
Optimal #2
Java: Gauss sum
Python: Gauss sum
Java Solutions
Sort + scan O(n log n) O(1)
Sort; first index where nums[i] != i.
Pseudo-code
sort; for i: if nums[i] != i: return i; return n
Complexity
Edge cases & gotchas
- Missing is n itself.
Java code
import java.util.*;
class Solution {
public int missingNumber(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) if (nums[i] != i) return i;
return nums.length;
}
}
XOR indices & values O(n) O(1)
r ^= i ^ nums[i]; r ^= n.
Pseudo-code
r=n; for i: r ^= i ^ nums[i]
Complexity
Java code
class Solution {
public int missingNumber(int[] nums) {
int r = nums.length;
for (int i = 0; i < nums.length; i++) r ^= i ^ nums[i];
return r;
}
}
Gauss sum O(n) O(1)
Expected total - actual sum.
Pseudo-code
n*(n+1)/2 - sum(nums)
Complexity
Java code
class Solution {
public int missingNumber(int[] nums) {
int n = nums.length;
int total = n * (n + 1) / 2;
for (int x : nums) total -= x;
return total;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 1/5 |
| When | Don't | Default — overflow-safe | Cleanest math |
| PE Verdict | XOR — no overflow risk. | ||
Python Solutions
Sort + scan O(n log n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def missingNumber(self, nums: List[int]) -> int:
nums.sort()
for i, x in enumerate(nums):
if x != i: return i
return len(nums)
XOR O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def missingNumber(self, nums: List[int]) -> int:
r = len(nums)
for i, x in enumerate(nums): r ^= i ^ x
return r
Gauss sum O(n) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
from typing import List
class Solution:
def missingNumber(self, nums: List[int]) -> int:
n = len(nums)
return n*(n+1)//2 - sum(nums)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(n log n) | O(n) | O(n) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 2/5 | 1/5 |
| When | Don't | Default | Cleanest math |
| PE Verdict | XOR. | ||
What the interviewer is really testing
Sum vs XOR — XOR avoids overflow concerns.
Top gotchas
- Initialize r = n (since indices only go up to n-1).
- XOR each index AND each value — the missing value is the survivor.
- Gauss sum is cleaner but can overflow on large arrays in Java.
Ship-it
XOR fold.
#149 LC 371 Medium Sum of Two Integers
Add without '+': sum = a XOR b (no carry); carry = (a AND b) << 1; loop.
Problem Statement
Return a + b without using + or - operators.
Examples
Constraints
- -1000 <= a, b <= 1000
Approach Overview
Brute Force
Java: Increment loop
Python: Inc/dec loop
Optimal #1
Java: XOR + carry shift
Python: XOR + carry shift
Optimal #2
Java: Recursive XOR + carry
Python: Cleaner sign-handle
Java Solutions
Increment loop O(|b|) O(1)
Inc/dec until b is 0.
Pseudo-code
while b!=0: if b>0: a++,b--; else a--,b++; return a
Complexity
Edge cases & gotchas
- Slow for big |b|.
Java code
class Solution {
public int getSum(int a, int b) {
while (b != 0) {
if (b > 0) { a++; b--; } else { a--; b++; }
}
return a;
}
}
XOR + carry shift O(32) O(1)
Iterate until carry becomes zero.
Pseudo-code
while b != 0: carry = (a & b) << 1; a = a ^ b; b = carry
Complexity
Edge cases & gotchas
- Java handles negative ints natively in two's-complement.
Java code
class Solution {
public int getSum(int a, int b) {
while (b != 0) {
int carry = (a & b) << 1;
a ^= b;
b = carry;
}
return a;
}
}
Recursive XOR + carry O(32) O(32)
Same idea recursively.
Pseudo-code
getSum(a,b) = b==0 ? a : getSum(a^b, (a&b)<<1)
Complexity
Java code
class Solution {
public int getSum(int a, int b) {
return b == 0 ? a : getSum(a ^ b, (a & b) << 1);
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(|b|) | O(32) | O(32) |
| Space | O(1) | O(1) | O(32) |
| Difficulty | 1/5 | 4/5 | 4/5 |
| When | Don't | Default | Recursive style |
| PE Verdict | XOR + carry shift. | ||
Python Solutions
Inc/dec loop O(|b|) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def getSum(self, a: int, b: int) -> int:
while b != 0:
if b > 0: a += 1; b -= 1
else: a -= 1; b += 1
return a
XOR + carry shift O(32) O(1)
Mask to 32 bits because Python has arbitrary precision.
Pseudo-code
mask=0xFFFFFFFF; while b: a, b = (a ^ b) & mask, ((a & b) << 1) & mask; sign-extend if needed
Complexity
Edge cases & gotchas
- Negative handling: convert back from 32-bit two's complement.
Python code
class Solution:
def getSum(self, a: int, b: int) -> int:
mask = 0xFFFFFFFF
while b & mask:
a, b = (a ^ b) & mask, ((a & b) << 1) & mask
return a & mask if b > 0 else a if a <= 0x7FFFFFFF else ~(a ^ mask)
Cleaner sign-handle O(32) O(1)
Same with explicit unsign at end.
Pseudo-code
loop with mask; final: if a > INT_MAX subtract 2**32
Complexity
Python code
class Solution:
def getSum(self, a: int, b: int) -> int:
mask = 0xFFFFFFFF
MAX = 0x7FFFFFFF
while b != 0:
a, b = (a ^ b) & mask, ((a & b) << 1) & mask
return a if a <= MAX else a - (1 << 32)
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(|b|) | O(32) | O(32) |
| Space | O(1) | O(1) | O(1) |
| Difficulty | 1/5 | 5/5 | 5/5 |
| When | Don't | Default | Equivalent |
| PE Verdict | XOR + carry shift, mask to 32 bits. | ||
What the interviewer is really testing
Classic bit-arithmetic — show you know full-adder behavior.
Top gotchas
- XOR is sum without carry; AND<<1 is the carry to add next round.
- Java: native two's complement makes this trivial.
- Python: must MASK to 32 bits and convert back since Python ints are unbounded.
Ship-it
XOR + carry shift (mask in Python).
#150 LC 7 Medium Reverse Integer
Pop digits with mod 10; push to result; detect overflow BEFORE pushing.
Problem Statement
Reverse digits of 32-bit signed integer. Return 0 if reversed value overflows.
Examples
Constraints
- -2^31 <= x <= 2^31 - 1
Approach Overview
Brute Force
Java: String reverse
Python: String reverse
Optimal #1
Java: Pop-and-push with overflow check
Python: Pop-and-push (Python truncation quirk)
Optimal #2
Java: Use long, clamp at end
Python: Long compute + clamp
Java Solutions
String reverse O(log x) O(log x)
Use string ops, parse back, catch overflow.
Pseudo-code
sign = x<0; s = reverse(abs(x).toString()); try parse; on exception return 0
Complexity
Edge cases & gotchas
- Integer.MIN_VALUE — Math.abs overflows.
Java code
class Solution {
public int reverse(int x) {
boolean neg = x < 0;
String s = new StringBuilder(String.valueOf((long) Math.abs((long) x))).reverse().toString();
try {
long v = Long.parseLong(s);
if (neg) v = -v;
if (v < Integer.MIN_VALUE || v > Integer.MAX_VALUE) return 0;
return (int) v;
} catch (Exception e) { return 0; }
}
}
Pop-and-push with overflow check O(log x) O(1)
Each iteration pop x%10, push to r; check overflow before pushing.
Pseudo-code
r=0; while x!=0: pop=x%10; x/=10; if would overflow Integer: return 0; r = r*10 + pop
Complexity
Edge cases & gotchas
- Java truncates toward zero; works for negatives directly.
Java code
class Solution {
public int reverse(int x) {
int r = 0;
while (x != 0) {
int pop = x % 10;
x /= 10;
if (r > Integer.MAX_VALUE / 10 || (r == Integer.MAX_VALUE / 10 && pop > 7)) return 0;
if (r < Integer.MIN_VALUE / 10 || (r == Integer.MIN_VALUE / 10 && pop < -8)) return 0;
r = r * 10 + pop;
}
return r;
}
}
Use long, clamp at end O(log x) O(1)
Compute in long; check against int range once.
Pseudo-code
long r=0; while x: r = r*10 + x%10; x/=10; if r out of int range: return 0
Complexity
Java code
class Solution {
public int reverse(int x) {
long r = 0;
while (x != 0) { r = r * 10 + x % 10; x /= 10; }
if (r < Integer.MIN_VALUE || r > Integer.MAX_VALUE) return 0;
return (int) r;
}
}
Java Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(log x) | O(log x) | O(log x) |
| Space | O(log x) | O(1) | O(1) |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When | Don't | Default — strict int | Allowed long |
| PE Verdict | Pop-and-push with overflow check. | ||
Python Solutions
String reverse O(log x) O(log x)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverse(self, x: int) -> int:
sign = -1 if x < 0 else 1
r = sign * int(str(abs(x))[::-1])
return r if -2**31 <= r <= 2**31 - 1 else 0
Pop-and-push (Python truncation quirk) O(log x) O(1)
Python // rounds toward neg infinity — handle negatives separately.
Pseudo-code
sign; work with abs(x); build r; restore sign; check range
Complexity
Edge cases & gotchas
- Python's % differs from Java for negatives.
Python code
class Solution:
def reverse(self, x: int) -> int:
sign = -1 if x < 0 else 1
x = abs(x); r = 0
while x:
r = r * 10 + x % 10
x //= 10
r *= sign
return r if -2**31 <= r <= 2**31 - 1 else 0
Long compute + clamp O(log x) O(1)
Same.
Pseudo-code
see Java
Complexity
Python code
class Solution:
def reverse(self, x: int) -> int:
sign = -1 if x < 0 else 1
n = abs(x); r = 0
while n: r = r*10 + n % 10; n //= 10
r *= sign
return 0 if r < -2**31 or r > 2**31 - 1 else r
Python Trade-Off Table
| Dimension | Brute Force | Optimal #1 | Optimal #2 |
|---|---|---|---|
| Time | O(log x) | O(log x) | O(log x) |
| Space | O(log x) | O(1) | O(1) |
| Difficulty | 2/5 | 4/5 | 2/5 |
| When | Don't | Default | Permissive |
| PE Verdict | Pop-and-push, work on abs(x). | ||
What the interviewer is really testing
Overflow detection is the actual trick.
Top gotchas
- INT_MAX = 2147483647 — last digit constraint > 7 triggers overflow.
- INT_MIN = -2147483648 — last digit constraint < -8 triggers overflow.
- Python's // rounds toward -inf — operate on abs(x) and restore sign at the end.
Ship-it
Pop-and-push with explicit overflow check.