LeetCode Cheat Sheet — DSA in Java
Which problems to solve, in which order, and how to think about each one. Every problem shows the brute force first, then the optimal approach with a picture, the complexity, and the one thing to remember. All code is Java and has been compiled and run.
Go top to bottom. The order is deliberate: each stage uses ideas from the one before. For every problem, try it yourself for 20–30 minutes first, then open the card. Read the brute force, then the optimal idea, then write the optimal code again without looking. Tick Solved when you can; your progress is saved in this browser.
Colours: Easy Medium Hard and the blue pill is the Pattern it teaches. The LeetCode link on each card opens the problem in a new tab.
Arrays
An array is a row of numbered boxes: you can jump to any box by its index in O(1), but searching for a value means walking the row. These problems teach the first big patterns: remember what you've seen in a HashMap or HashSet, track a running minimum or sum in one pass, two pointers (a read pointer and a write pointer) to change an array in place, and a few clever tricks: Boyer-Moore voting, math / XOR and Kadane's algorithm.
LC 1Two SumEasyHash MapLeetCode
You get an array of numbers and a target. Find the two positions whose numbers add up to the target. There is always exactly one answer, and you can't use the same position twice.
Example: nums = [2, 7, 11, 15], target = 9 → [0, 1], because 2 + 7 = 9.
1Brute force — try every pair
The first idea anyone has: pick each number, then check it against every number after it. If a pair adds up to the target, return their positions.
int[] twoSum(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) return new int[]{i, j};
}
}
return new int[]{}; // problem says an answer always exists
}
Time O(n²)Space O(1)
It works, but with 10,000 numbers that's about 50 million pair checks. The interviewer will ask: "can you do better?"
2Optimal — remember what you've seen in a HashMap
For each number, you already know its partner: need = target − current. So instead of searching for the partner, ask a HashMap "have I seen need before?" A HashMap answers that in O(1).
int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // number -> its index
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i]; // the partner we are looking for
if (seen.containsKey(need)) {
return new int[]{seen.get(need), i};
}
seen.put(nums[i], i); // store AFTER checking
}
return new int[]{};
}
Time O(n)Space O(n)
We trade memory for speed: the map can hold up to n numbers, but every lookup is instant, so one pass is enough.
"Find a pair that adds to X" → for each item, look up its partner in a HashMap instead of searching. This one trick shows up in dozens of problems.
Check the map before you put the current number in. If you put first, [3, 2, 4] with target 6 finds the 3 you just stored and returns [0, 0]: the same position used twice.
Pattern notes: Hashing · Open on LeetCode
LC 217Contains DuplicateEasyHash SetLeetCode
You get an array of numbers. Return true if any number shows up two or more times, and false if every number is different.
Example: nums = [1, 2, 3, 1] → true, because 1 appears twice.
1Brute force — compare every pair
Take each number and compare it with every number after it. If two are equal, we found a duplicate.
boolean containsDuplicate(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] == nums[j]) return true;
}
}
return false;
}
Time O(n²)Space O(1)
Correct, but LeetCode gives up to 100,000 numbers. That is about 5 billion comparisons, so it times out.
2Optimal — keep a HashSet of numbers you've seen
Think of a guest list at a door. Before letting someone in, you check the list. A HashSet is that list, and checking it takes O(1). So walk the array once and ask the set "have I seen this number already?" Bonus: set.add(x) returns false when x is already inside, so one call both checks and adds.
boolean containsDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int x : nums) {
// add() returns false if x was already in the set
if (!seen.add(x)) return true;
}
return false; // every number was new
}
Time O(n)Space O(n)
Each number is looked up and added once, and both take O(1). The set may hold all n numbers, so we pay O(n) memory to get the speed.
"Have I seen this before?" → use a HashSet. (Another way: sort first, then duplicates sit side by side. That is O(n log n) time with no set.)
Don't use a List and call list.contains(x). That looks just as simple, but contains on a list scans the whole list, so you are back to O(n²).
Pattern notes: Hashing · Open on LeetCode
LC 121Best Time to Buy and Sell StockEasyRunning MinimumLeetCode
prices[i] is the price of a stock on day i. You may buy once and sell once, and the sell day must come after the buy day. Return the biggest profit you can make, or 0 if every choice loses money.
Example: prices = [7, 1, 5, 3, 6, 4] → 5: buy on day 1 at price 1, sell on day 4 at price 6.
1Brute force — try every buy day with every later sell day
For each buy day i, try every sell day j > i and keep the biggest difference.
int maxProfit(int[] prices) {
int best = 0;
for (int i = 0; i < prices.length; i++) { // buy day
for (int j = i + 1; j < prices.length; j++) { // sell day, always later
best = Math.max(best, prices[j] - prices[i]);
}
}
return best;
}
Time O(n²)Space O(1)
With 100,000 days this is billions of pairs. It times out.
2Optimal — one pass, remember the cheapest day so far
Pretend each day is your selling day. Which buy day is best for it? Simply the cheapest price seen before today. So walk the days once, keep minPrice (the cheapest so far), and ask: "if I sold today, what would I earn?" Keep the best answer.
int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE; // cheapest price seen so far
int best = 0; // best profit so far (0 = don't trade)
for (int p : prices) {
if (p < minPrice) {
minPrice = p; // a new cheapest day to buy
} else {
best = Math.max(best, p - minPrice); // sell today?
}
}
return best;
}
Time O(n)Space O(1)
We never look back: the minimum so far already sums up everything the past days can offer.
When the answer for position i depends on "the best thing before i", keep that best thing in one running variable instead of re-scanning the past.
Don't just return max − min of the whole array. In [7, 6, 4, 3, 1] the max comes before the min, so you would "sell before you buy". The answer there is 0.
Pattern notes: Arrays & Strings · Open on LeetCode
LC 283Move ZeroesEasyTwo PointersLeetCode
Move every 0 to the end of the array. The other numbers must keep their original order. You must change the array in place (no copy of the array).
Example: nums = [0, 1, 0, 3, 12] → [1, 3, 12, 0, 0].
1Brute force — copy the non-zeros into a new array
Make a new array (Java fills it with zeros). Copy the non-zero numbers into it from the left, then copy everything back.
void moveZeroes(int[] nums) {
int[] tmp = new int[nums.length]; // starts as all zeros
int k = 0;
for (int x : nums) {
if (x != 0) tmp[k++] = x; // keep non-zeros in order
}
for (int i = 0; i < nums.length; i++) nums[i] = tmp[i];
}
Time O(n)Space O(n)
Fast, but it uses a whole second array. The problem asks for in place, so the interviewer wants O(1) extra memory.
2Optimal — a read pointer and a write pointer
Use two fingers on the same array. read walks over every number. write points at the next slot where a non-zero should go. When read finds a non-zero, swap it into the write slot and move write forward. The zeros get pushed to the back by the swaps.
w marks the write pointer: everything to its left is non-zero and in the original order.void moveZeroes(int[] nums) {
int write = 0; // next slot for a non-zero
for (int read = 0; read < nums.length; read++) {
if (nums[read] != 0) {
int t = nums[write]; // swap nums[read] into the write slot
nums[write] = nums[read];
nums[read] = t;
write++;
}
}
}
Time O(n)Space O(1)
One pass and only two index variables. Because write never passes read, we never overwrite a number we still need.
To filter or rearrange an array in place, use a read pointer that looks at everything and a write pointer that marks where the next "good" item goes.
Don't remove zeros with ArrayList.remove or by shifting the rest left each time. Every shift costs O(n), so the total becomes O(n²).
Pattern notes: Two Pointers · Open on LeetCode
LC 26Remove Duplicates from Sorted ArrayEasyTwo PointersLeetCode
The array is sorted. Remove the extra copies in place so each value appears once. Return k, the number of unique values. The first k slots must hold those values in order; what is left after them does not matter.
Example: nums = [1, 1, 2] → k = 2, and nums starts with [1, 2].
1Brute force — collect the unique values in a set
Put every number into a LinkedHashSet (it drops duplicates and keeps insertion order), then write the set back into the front of the array.
int removeDuplicates(int[] nums) {
Set<Integer> set = new LinkedHashSet<>();
for (int x : nums) set.add(x); // duplicates are ignored
int k = 0;
for (int x : set) nums[k++] = x; // write the unique values back
return k;
}
Time O(n)Space O(n)
It works on any array, sorted or not, but it ignores the big hint (sorted) and uses extra memory the problem does not want you to use.
2Optimal — write pointer, compare with the last kept value
Because the array is sorted, copies of a value always sit next to each other. So a number is new exactly when it differs from the last value we kept. Keep a write pointer: slots before it hold the unique values found so far. Slot 0 is always kept.
nums[write - 1]. Only new values get copied forward.int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int write = 1; // slot 0 is always unique
for (int read = 1; read < nums.length; read++) {
if (nums[read] != nums[write - 1]) { // new value?
nums[write] = nums[read];
write++;
}
}
return write; // number of unique values
}
Time O(n)Space O(1)
One pass, no extra array. Sorting did the hard work for us: duplicates are grouped, so one comparison per number is enough.
In a sorted array, equal values are neighbours. "Is it new?" becomes "is it different from the last one I kept?"
Compare with nums[write - 1] (the last kept value), and return write, the count. Returning the array or write - 1 are the usual slips.
Pattern notes: Two Pointers · Open on LeetCode
LC 27Remove ElementEasyTwo PointersLeetCode
Remove every copy of the number val from the array in place. Return k, how many numbers are left. The first k slots must hold the kept numbers (any order is accepted).
Example: nums = [3, 2, 2, 3], val = 3 → k = 2, and nums starts with [2, 2].
1Brute force — copy the keepers into a new array
Copy every number that is not val into a temporary array, then copy those back to the front.
int removeElement(int[] nums, int val) {
int[] tmp = new int[nums.length];
int k = 0;
for (int x : nums) {
if (x != val) tmp[k++] = x; // keep it
}
for (int i = 0; i < k; i++) nums[i] = tmp[i];
return k;
}
Time O(n)Space O(n)
The extra array is not needed: we can write the keepers straight into the front of nums.
2Optimal — read pointer and write pointer
Same trick as Move Zeroes. read looks at every number; write is the next free slot. Copy a number to write only if it is not val. Since write is never ahead of read, we only overwrite slots we have already read.
int removeElement(int[] nums, int val) {
int write = 0; // next free slot for a keeper
for (int read = 0; read < nums.length; read++) {
if (nums[read] != val) {
nums[write] = nums[read]; // keep it
write++;
}
}
return write; // how many we kept
}
Time O(n)Space O(1)
Each number is read once and written at most once, with no second array.
"Remove X in place" is almost always read pointer + write pointer: copy forward what you keep, ignore what you drop, return the write pointer as the new length.
Don't try to physically delete from a Java array: its size is fixed. The judge only checks the first k slots, so the leftovers at the end can be anything.
Pattern notes: Two Pointers · Open on LeetCode
LC 88Merge Sorted ArrayEasyTwo PointersLeetCode
You get two sorted arrays. nums1 has m real numbers followed by n empty slots (zeros). nums2 has n numbers. Merge nums2 into nums1 so that nums1 becomes one sorted array. Don't return anything; change nums1.
Example: nums1 = [1, 2, 3, 0, 0, 0], m = 3, nums2 = [2, 5, 6], n = 3 → nums1 = [1, 2, 2, 3, 5, 6].
1Brute force — paste nums2 at the end and sort
Copy nums2 into the empty slots, then let Java sort the whole thing.
void merge(int[] nums1, int m, int[] nums2, int n) {
for (int j = 0; j < n; j++) {
nums1[m + j] = nums2[j]; // fill the empty slots
}
Arrays.sort(nums1);
}
Time O((m+n) log(m+n))Space O(1)*
Short and correct, but it throws away the fact that both arrays are already sorted. We can merge in one linear pass. (*Java's sort for int[] uses a little stack space.)
2Optimal — three pointers, fill from the back
If we merged from the front, we would overwrite numbers of nums1 we still need. But the back of nums1 is empty space. So compare the biggest remaining number of each array and put the bigger one in the last free slot. Pointer i is on nums1's last real number, j on nums2's last number, k on the last free slot.
void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1; // last real number in nums1
int j = n - 1; // last number in nums2
int k = m + n - 1; // last free slot in nums1
while (j >= 0) { // stop when nums2 is used up
if (i >= 0 && nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--]; // nums1's number is bigger
} else {
nums1[k--] = nums2[j--]; // nums2's number is bigger (or equal)
}
}
}
Time O(m + n)Space O(1)
Every number is placed once. When nums2 runs out, whatever is left of nums1 is already in the right spot.
Merging two sorted lists = compare the fronts (or backs), take the winner, move that pointer. When writing into the same array, fill from the side that has free space.
Loop while j >= 0, and check i >= 0 before reading nums1[i]. With nums1 = [0], m = 0, nums2 = [1], i starts at -1 and reading nums1[-1] crashes.
Pattern notes: Two Pointers · Open on LeetCode
LC 169Majority ElementEasyBoyer-Moore VotingLeetCode
One number appears more than half of the time in the array. Find it. The problem promises it always exists.
Example: nums = [2, 2, 1, 1, 1, 2, 2] → 2 (it appears 4 times out of 7).
1Brute force — count every number
For each number, count how many times it appears in the whole array. The first one with a count above n / 2 is the answer.
int majorityElement(int[] nums) {
for (int x : nums) {
int count = 0;
for (int y : nums) {
if (y == x) count++;
}
if (count > nums.length / 2) return x;
}
return -1; // never happens: the majority always exists
}
Time O(n²)Space O(1)
Too slow for big inputs. A HashMap of counts gets O(n) time but needs O(n) memory. Sorting and taking the middle element also works in O(n log n). Can we get O(n) time and O(1) memory?
2Optimal — Boyer-Moore voting
Picture an election where each voter for a different number cancels out one voter for our candidate. The majority has more voters than everyone else combined, so it can never be fully cancelled. Keep a candidate and a count: a matching number adds a vote, a different number removes one, and when the count hits 0, the next number becomes the new candidate. Whoever is candidate at the end is the majority.
int majorityElement(int[] nums) {
int candidate = 0;
int count = 0;
for (int x : nums) {
if (count == 0) candidate = x; // no leader: x takes over
if (x == candidate) count++; // a vote for the leader
else count--; // a vote against: they cancel
}
return candidate;
}
Time O(n)Space O(1)
Only two variables, one pass. It works because every "against" vote can cancel at most one majority vote, and the majority has more than all others together.
"More than half" is a strong promise. Boyer-Moore voting uses it: pair up different values so they cancel, and the majority is what survives.
The final candidate is only guaranteed correct if a majority exists. Here the problem promises that. If it didn't (for example in LC 229), you would need a second pass to count and confirm the candidate.
Pattern notes: Arrays & Strings · Open on LeetCode
LC 268Missing NumberEasyMathLeetCode
The array has n different numbers taken from 0, 1, 2, …, n. That range has n + 1 numbers, so exactly one is missing. Find it.
Example: nums = [3, 0, 1] → 2. Here n = 3, so the range is 0 to 3, and 2 is not in the array.
1Brute force — sort, then look for the gap
After sorting, the number at index i should be i. The first index where it isn't is the missing number. If every index matches, the missing number is n.
int missingNumber(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i) return i; // i should be here but isn't
}
return nums.length; // 0..n-1 all present, so n is missing
}
Time O(n log n)Space O(1)*
Sorting is the slow part, and it's more work than needed: we only want one number. (*Plus the sort's small stack space.)
2Optimal — expected sum minus actual sum
We know exactly what the full set 0..n adds up to: 0 + 1 + … + n = n(n + 1) / 2 (Gauss's formula). Add up the numbers we actually have. The difference is the one that is missing, like counting the money in a wallet and noticing what's short.
int missingNumber(int[] nums) {
int n = nums.length;
int expected = n * (n + 1) / 2; // sum of 0..n
int actual = 0;
for (int x : nums) actual += x;
return expected - actual;
}
Time O(n)Space O(1)
One pass to add the numbers, one formula. Another O(n), O(1) way: XOR every index and every value together; pairs cancel (a ^ a = 0) and only the missing number is left (see LC 136).
If you know what the complete collection should look like (sum, XOR, count), compare it with what you have. The difference is the missing piece.
n is nums.length, not the biggest value in the array. In [0, 1] the missing number is 2 (the top of the range), which a check that only looks inside the array forgets.
Pattern notes: Math & Number Theory · Open on LeetCode
LC 136Single NumberEasyBit ManipulationLeetCode
Every number in the array appears exactly twice, except one number that appears once. Find that lonely number. The problem asks for O(n) time and O(1) extra memory.
Example: nums = [4, 1, 2, 1, 2] → 4.
1Brute force — count each number
For each number, count how often it appears. The one with a count of 1 is the answer.
int singleNumber(int[] nums) {
for (int x : nums) {
int count = 0;
for (int y : nums) {
if (y == x) count++;
}
if (count == 1) return x;
}
return -1; // never happens
}
Time O(n²)Space O(1)
Too slow. A HashMap of counts (or a HashSet where you add, then remove on the second sighting) is O(n) time, but it needs O(n) memory, which the problem forbids.
2Optimal — XOR everything together
XOR (^) has three magic rules: a ^ a = 0, a ^ 0 = a, and the order doesn't matter. So if we XOR all numbers, every pair cancels itself to 0 and only the single number is left. Like socks: match up the pairs and the odd sock is what remains.
int singleNumber(int[] nums) {
int result = 0; // x ^ 0 = x, so 0 is a safe start
for (int x : nums) {
result ^= x; // pairs cancel out: a ^ a = 0
}
return result; // only the single number is left
}
Time O(n)Space O(1)
One variable, one pass. It also works with negative numbers, because XOR just works on the bits.
"Everything appears twice except one" → XOR it all. a ^ a = 0 and a ^ 0 = a are worth memorising.
Start result at 0. If you start at nums[0], the loop must begin at index 1, or the first number is XORed twice and cancels itself. And XOR only cancels pairs: if numbers appear three times (LC 137), this trick alone doesn't work.
Pattern notes: Bit Manipulation · Open on LeetCode
LC 53Maximum SubarrayMediumKadaneLeetCode
A subarray is a block of neighbouring elements (no gaps). Find the subarray with the biggest sum and return that sum. It must contain at least one element, so if all numbers are negative, the answer is the least negative one.
Example: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] → 6, from the block [4, -1, 2, 1].
1Brute force — try every start, grow the end
For every start index, extend the block one step at a time and keep a running sum (so we don't re-add the whole block each time). Track the biggest sum ever seen.
int maxSubArray(int[] nums) {
int best = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) { // start of the block
int sum = 0;
for (int j = i; j < nums.length; j++) { // end of the block
sum += nums[j];
best = Math.max(best, sum);
}
}
return best;
}
Time O(n²)Space O(1)
Checking all n²/2 blocks is too slow for 100,000 numbers. (Re-adding every block from scratch would even be O(n³).)
2Optimal — Kadane's algorithm
Walk left to right and keep current = the best sum of a block that ends here. At each number there are only two choices: join the block before, or start fresh here. If the block before has a negative sum, it can only drag us down, so we drop it. In code: current = max(num, current + num). The answer is the biggest current ever seen.
int maxSubArray(int[] nums) {
int current = nums[0]; // best sum of a block ending at i
int best = nums[0]; // best sum seen anywhere
for (int i = 1; i < nums.length; i++) {
// join the previous block, or start a new one here
current = Math.max(nums[i], current + nums[i]);
best = Math.max(best, current);
}
return best;
}
Time O(n)Space O(1)
This is a tiny dynamic programming solution: the answer for "ending at i" is built from the answer for "ending at i − 1".
Best contiguous block → Kadane: at each step choose "extend or restart", and keep a separate "best so far". A negative running sum is dead weight.
Don't start best (or current) at 0. For [-3, -1, -2] the answer is -1, but starting at 0 returns 0, the sum of an empty block, which isn't allowed. Start both at nums[0].
Pattern notes: Dynamic Programming · Open on LeetCode
Strings
A string is just an array of characters, so most string problems are array problems in disguise. The problems below teach the key patterns: two pointers walking in from both ends, counting letters in an int[26] array, mapping one character to another, and scanning column by column.
LC 344Reverse StringEasyTwo PointersLeetCode
You get a string as an array of characters. Reverse it in place: change the array you were given, and don't build a second one.
Example: s = ['h','e','l','l','o'] → ['o','l','l','e','h'].
1Brute force — copy it backwards
Make a new array, fill it from the back of s, then copy it back into s. Easy to think of, but it needs a whole second array.
void reverseString(char[] s) {
char[] copy = new char[s.length];
for (int i = 0; i < s.length; i++) copy[i] = s[s.length - 1 - i];
for (int i = 0; i < s.length; i++) s[i] = copy[i];
}
Time O(n)Space O(n)
The speed is fine, but the problem asks for O(1) extra memory, and the copy breaks that rule.
2Optimal — swap from both ends
The first letter must end up last, and the last letter must end up first. So swap the two ends, then step both pointers inward. When the pointers meet in the middle, every letter is in its new place.
void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while (left < right) { // stop when the pointers meet or cross
char tmp = s[left]; // classic swap with a temp variable
s[left] = s[right];
s[right] = tmp;
left++; // both ends move one step inward
right--;
}
}
Time O(n)Space O(1)
Only two index variables and one temp character, no matter how long the string is.
"Do something to both ends of an array" → put a pointer at each end and walk them toward each other. This is the simplest form of the two-pointer pattern.
Use left < right, not a loop over the whole array. If you swap all n positions, every letter gets swapped twice and you end up with the original string.
Pattern notes: Two Pointers · Open on LeetCode
LC 125Valid PalindromeEasyTwo PointersLeetCode
A palindrome reads the same forwards and backwards. Ignore everything that is not a letter or digit (spaces, commas, colons) and ignore upper/lower case. Is the string a palindrome?
Example: "A man, a plan, a canal: Panama" → true (it cleans up to "amanaplanacanalpanama"). "race a car" → false.
1Brute force — clean it, reverse it, compare
Build a new string with only the lowercase letters and digits. Then reverse it and check if it equals itself.
boolean isPalindrome(String s) {
StringBuilder clean = new StringBuilder();
for (char c : s.toCharArray()) {
if (Character.isLetterOrDigit(c)) clean.append(Character.toLowerCase(c));
}
String forward = clean.toString();
String backward = clean.reverse().toString();
return forward.equals(backward);
}
Time O(n)Space O(n)
Correct and readable, but it builds two extra strings. An interviewer will ask you to do it without the copies.
2Optimal — two pointers that skip the junk
Compare the string with itself from both ends, like in Reverse String. The only new part: if a pointer sits on a non-letter, skip it before comparing. Compare the two letters in lowercase; one mismatch means "not a palindrome".
boolean isPalindrome(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
// skip anything that is not a letter or digit (keep left < right!)
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) left++;
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) right--;
// compare ignoring case
if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
return false;
}
left++;
right--;
}
return true;
}
Time O(n)Space O(1)
Each pointer only moves forward (or backward), so every character is looked at once, and nothing is copied.
When the input has characters to ignore, skip them with the pointers instead of building a cleaned copy. Same answer, no extra memory.
Digits count! "0P" must be false, so use Character.isLetterOrDigit, not isLetter. And keep left < right in the inner skip loops, or a string like " " runs off the end.
Pattern notes: Two Pointers · Open on LeetCode
LC 242Valid AnagramEasyCountingLeetCode
Two words are anagrams if they use exactly the same letters, the same number of times, just in a different order. Given s and t (lowercase letters), are they anagrams?
Example: s = "anagram", t = "nagaram" → true. s = "rat", t = "car" → false.
1Brute force — sort both and compare
If two words have the same letters, sorting them gives the same result: "anagram" and "nagaram" both become "aaagmnr".
boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
char[] a = s.toCharArray();
char[] b = t.toCharArray();
Arrays.sort(a);
Arrays.sort(b);
return Arrays.equals(a, b);
}
Time O(n log n)Space O(n)
Nice and short, but sorting does more work than needed. We don't care about order, only about how many of each letter.
2Optimal — count the letters
Keep one counter per letter in an int[26]. Add 1 for each letter of s, subtract 1 for each letter of t. If they are anagrams, everything cancels out and every counter ends at 0.
boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false; // different sizes can't match
int[] count = new int[26]; // count[0] is 'a', count[25] is 'z'
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++; // letter from s: +1
count[t.charAt(i) - 'a']--; // letter from t: -1
}
for (int c : count) {
if (c != 0) return false; // some letter didn't cancel out
}
return true;
}
Time O(n)Space O(1)
The array always has 26 slots, whatever the input size, so the extra memory counts as constant.
"Same letters, any order" → count, don't sort. c - 'a' turns a lowercase letter into an index 0–25, so an int[26] works as a tiny, fast HashMap.
Check the lengths first, or the single loop would read past the end of the shorter string. And int[26] only works for lowercase a–z; for Unicode input (the follow-up question) use a HashMap<Character, Integer>.
Pattern notes: Hashing · Open on LeetCode
LC 387First Unique Character in a StringEasyCountingLeetCode
Find the first letter that appears only once in the string and return its index. If every letter repeats, return -1.
Example: "loveleetcode" → 2 (the 'v'). "aabb" → -1.
1Brute force — check every letter against all others
For each position, scan the whole string to see if the same letter shows up anywhere else. The first letter with no twin is the answer.
int firstUniqChar(String s) {
for (int i = 0; i < s.length(); i++) {
boolean repeated = false;
for (int j = 0; j < s.length(); j++) {
if (i != j && s.charAt(i) == s.charAt(j)) { repeated = true; break; }
}
if (!repeated) return i;
}
return -1;
}
Time O(n²)Space O(1)
For a long string with no unique letter, every position scans the whole string again. That's a lot of repeated work.
2Optimal — count first, then find
Split the job in two passes. Pass 1 counts every letter. Pass 2 walks the string again in order and returns the first position whose count is exactly 1. Walking the string (not the alphabet) in pass 2 is what keeps the answer "first".
int firstUniqChar(String s) {
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++; // pass 1: how many of each letter
}
for (int i = 0; i < s.length(); i++) {
if (count[s.charAt(i) - 'a'] == 1) return i; // pass 2: first with count 1
}
return -1; // every letter repeats
}
Time O(n)Space O(1)
Two passes of n steps is still O(n), and the count array is a fixed 26 slots.
"Something about how often each item appears" → count in one pass, answer in a second pass. Two passes are still linear time.
In pass 2, loop over the string, not over count[0..25]. Looping over the alphabet finds the unique letter that comes first alphabetically, not the one that comes first in the string. Also return the index, not the character.
Pattern notes: Hashing · Open on LeetCode
LC 14Longest Common PrefixEasyVertical ScanLeetCode
You get a list of words. Find the longest beginning that all of them share. If they share nothing, return "".
Example: ["flower", "flow", "flight"] → "fl". ["dog", "racecar", "car"] → "".
1Brute force — try longer and longer prefixes
Take the first word. Try its first 1 letter, then first 2 letters, and so on. For each try, check that every word starts with it. Stop at the first try that fails.
String longestCommonPrefix(String[] strs) {
String best = "";
for (int len = 1; len <= strs[0].length(); len++) {
String candidate = strs[0].substring(0, len); // builds a new string each time
boolean all = true;
for (String w : strs) {
if (!w.startsWith(candidate)) { all = false; break; }
}
if (!all) break;
best = candidate;
}
return best;
}
Time O(n · m²)Space O(m)
(n words, prefix length m.) Every try re-checks the letters the previous try already confirmed, and builds a new substring each time.
2Optimal — compare column by column
Stack the words on top of each other and read down the columns. Column i is fine if every word has the same letter at position i. The first column that differs (or where a word runs out) is where the prefix ends. Each letter is checked only once.
String longestCommonPrefix(String[] strs) {
String first = strs[0];
for (int i = 0; i < first.length(); i++) { // column i
char c = first.charAt(i);
for (int k = 1; k < strs.length; k++) { // every other word
// this word is too short, or has a different letter here
if (i == strs[k].length() || strs[k].charAt(i) != c) {
return first.substring(0, i);
}
}
}
return first; // the whole first word is a common prefix
}
Time O(S)Space O(1)
S is the total number of letters. In the worst case we look at every letter once, and we build only one substring at the very end.
When many strings must agree position by position, scan column by column and stop at the first disagreement. The answer is simply the first word cut at that point.
Check i == strs[k].length() before calling charAt(i). With ["ab", "a"], column 1 doesn't exist in "a", and skipping the check throws StringIndexOutOfBoundsException.
Pattern notes: Arrays & Strings · Open on LeetCode
LC 205Isomorphic StringsEasyHash MapLeetCode
Can you turn s into t by renaming letters? Every copy of a letter must get the same new name, and two different letters can't get the same name. Both strings have the same length.
Example: "egg", "add" → true (e→a, g→d). "foo", "bar" → false (o would need to be both a and r).
1Brute force — compare every pair of positions
Renaming keeps "same" and "different" intact. So for every pair of positions i and j: if s has the same letter at both, t must too, and if s has different letters, t must too.
boolean isIsomorphic(String s, String t) {
for (int i = 0; i < s.length(); i++) {
for (int j = i + 1; j < s.length(); j++) {
boolean sameInS = s.charAt(i) == s.charAt(j);
boolean sameInT = t.charAt(i) == t.charAt(j);
if (sameInS != sameInT) return false;
}
}
return true;
}
Time O(n²)Space O(1)
Correct, but with 50,000 letters that's over a billion pair checks.
2Optimal — two maps, one for each direction
Walk both strings together and write down the renaming as you go. One map remembers s→t, a second map remembers t→s. If a pair ever disagrees with what a map already says, the answer is false. Two maps are needed because the rule works both ways.
boolean isIsomorphic(String s, String t) {
char[] sToT = new char[256]; // 0 means "no mapping yet" (works for ASCII)
char[] tToS = new char[256];
for (int i = 0; i < s.length(); i++) {
char a = s.charAt(i), b = t.charAt(i);
if (sToT[a] == 0 && tToS[b] == 0) { // both letters are new: record the pair
sToT[a] = b;
tToS[b] = a;
} else if (sToT[a] != b || tToS[b] != a) {
return false; // pair disagrees with what we recorded
}
}
return true;
}
Time O(n)Space O(1)
One pass, and the two arrays have a fixed 256 slots (one per ASCII character), so memory doesn't grow with n.
"Is there a one-to-one matching between two things?" → keep two maps, one for each direction, and check both on every step.
Only one map (s→t) is the classic bug. "badc" and "baba" pass the s→t check (b→b, a→a, d→b, c→a) but b and d both became b, so the answer must be false.
Pattern notes: Hashing · Open on LeetCode
LC 392Is SubsequenceEasyTwo PointersLeetCode
Can you get s by crossing out some letters of t (without changing the order of the rest)? That's what "subsequence" means.
Example: s = "abc", t = "ahbgdc" → true (keep a, b, c; cross out h, g, d). s = "axc" → false (there is no x).
1Brute force — try every choice
At each letter of t we have a choice: use it (if it matches the next letter of s) or skip it. Recursion can try both options and see if any path works.
boolean isSubsequence(String s, String t) {
return tryMatch(s, t, 0, 0);
}
boolean tryMatch(String s, String t, int i, int j) {
if (i == s.length()) return true; // matched all of s
if (j == t.length()) return false; // t ran out first
if (s.charAt(i) == t.charAt(j) && tryMatch(s, t, i + 1, j + 1)) return true; // use t[j]
return tryMatch(s, t, i, j + 1); // skip t[j]
}
Time O(2ⁿ)Space O(n)
With inputs like s = "aaab", t = "aaaaaaaa" the "use it or skip it" branches explode. We are exploring choices that never needed exploring.
2Optimal — greedy two pointers
There is no real choice to make: if t[j] matches the letter of s we are waiting for, taking the earliest match is never worse, because it leaves the most of t for the rest. So pointer j walks through t, and pointer i moves forward in s only on a match.
boolean isSubsequence(String s, String t) {
int i = 0; // next letter of s we need
for (int j = 0; j < t.length() && i < s.length(); j++) {
if (s.charAt(i) == t.charAt(j)) i++; // match: move on in s
} // j always moves on in t
return i == s.length(); // did we match all of s?
}
Time O(n)Space O(1)
n is the length of t. Each letter of t is looked at once, and there's no recursion stack.
Two pointers don't have to start at opposite ends. Here both walk left to right at different speeds: one over the long string, one over the short string, moving only on a match.
An empty s is a subsequence of anything, so the answer is true. The i == s.length() check handles this, but a version that reads s.charAt(0) first would crash.
Pattern notes: Two Pointers · Open on LeetCode
LC 13Roman to IntegerEasyHash MapLeetCode
Convert a Roman numeral to a number. The symbols are I=1, V=5, X=10, L=50, C=100, D=500, M=1000. Normally you add them up, but a smaller symbol before a bigger one is subtracted: IV = 4, IX = 9, XC = 90, CM = 900.
Example: "LVIII" → 58 (50 + 5 + 3). "MCMXCIV" → 1994 (1000 + 900 + 90 + 4).
1First try — a table of all 13 symbols and pairs
List the six special pairs (CM, CD, XC, XL, IX, IV) and the seven single symbols. At each position, try the pairs first, then the singles, add the value and jump ahead.
int romanToInt(String s) {
String[] symbols = {"CM","CD","XC","XL","IX","IV","M","D","C","L","X","V","I"};
int[] values = { 900, 400, 90, 40, 9, 4,1000,500,100, 50, 10, 5, 1};
int total = 0, i = 0;
while (i < s.length()) {
for (int k = 0; k < symbols.length; k++) {
if (s.startsWith(symbols[k], i)) { // pairs are listed first on purpose
total += values[k];
i += symbols[k].length();
break;
}
}
}
return total;
}
Time O(n)Space O(1)
This is already O(n), so there's no faster solution. But it tries up to 13 entries per position, and the table order must be exactly right. There is a much simpler rule.
2Cleaner — look at the next symbol
You don't need a list of pairs. If a symbol is smaller than the one after it, subtract it; otherwise add it. In "IV", I (1) is smaller than V (5), so it counts as −1, and −1 + 5 = 4. A small map gives each symbol's value.
int romanToInt(String s) {
Map<Character, Integer> value = Map.of(
'I', 1, 'V', 5, 'X', 10, 'L', 50, 'C', 100, 'D', 500, 'M', 1000);
int total = 0;
for (int i = 0; i < s.length(); i++) {
int cur = value.get(s.charAt(i));
// smaller than the next symbol? then it is the "minus" part of a pair
if (i + 1 < s.length() && cur < value.get(s.charAt(i + 1))) {
total -= cur;
} else {
total += cur;
}
}
return total;
}
Time O(n)Space O(1)
One pass with one comparison per symbol. The map always holds 7 entries, so it's constant memory.
When a rule depends on neighbours, peek at i + 1 and decide right there. Finding the one simple rule often beats listing every special case.
Check i + 1 < s.length() before peeking, or the last symbol throws an exception. The last symbol is always added, never subtracted.
Pattern notes: Arrays & Strings · Open on LeetCode
LC 383Ransom NoteEasyCountingLeetCode
You want to write ransomNote by cutting letters out of magazine. Each magazine letter can be used only once. Is it possible?
Example: ransomNote = "aa", magazine = "aab" → true. ransomNote = "aa", magazine = "ab" → false (only one a).
1Brute force — find and cross out each letter
For each letter of the note, search the magazine for it. If found, delete it so it can't be used again. If not found, return false.
boolean canConstruct(String ransomNote, String magazine) {
StringBuilder mag = new StringBuilder(magazine);
for (char c : ransomNote.toCharArray()) {
int pos = mag.indexOf(String.valueOf(c)); // linear search
if (pos == -1) return false;
mag.deleteCharAt(pos); // shifts the rest: also linear
}
return true;
}
Time O(m · n)Space O(n)
(m = note length, n = magazine length.) Each note letter searches and shifts the whole magazine again.
2Optimal — count the magazine, spend from the count
Think of the magazine as a bag of letter tiles. Count how many of each letter the bag has, then spend them for the note. If any count goes below zero, you ran out of that letter.
boolean canConstruct(String ransomNote, String magazine) {
int[] count = new int[26];
for (char c : magazine.toCharArray()) count[c - 'a']++; // what we have
for (char c : ransomNote.toCharArray()) {
if (--count[c - 'a'] < 0) return false; // spend one; ran out?
}
return true;
}
Time O(m + n)Space O(1)
One pass over each string, and the counter array is always 26 slots.
"Do I have enough of each item?" → count the supply, then subtract the demand. This is the same count trick as Valid Anagram, but here extra supply is allowed.
Count the magazine and spend with the note, not the other way round. Swapping them checks whether the magazine can be built from the note, which is a different question.
Pattern notes: Hashing · Open on LeetCode
Hash Map & Set
A HashSet answers "have I seen this before?" and a HashMap answers "how many?" or "what goes with this?", both in O(1) time. The problems below teach the key patterns: a set for membership and removing duplicates, a map for counting, two maps for one-to-one matching, and a set to catch a loop.
LC 349Intersection of Two ArraysEasyHash SetLeetCode
Return the numbers that appear in both arrays. Each number appears only once in the answer, in any order.
Example: nums1 = [1,2,2,1], nums2 = [2,2] → [2]. nums1 = [4,9,5], nums2 = [9,4,9,8,4] → [9,4].
1Brute force — nested loops
For every number in nums1, scan nums2. If you find it, add it to a result set (the set removes duplicates for you).
int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> result = new HashSet<>();
for (int a : nums1) {
for (int b : nums2) {
if (a == b) { result.add(a); break; }
}
}
int[] out = new int[result.size()];
int k = 0;
for (int v : result) out[k++] = v;
return out;
}
Time O(n · m)Space O(n)
The inner loop searches nums2 again for every number. A set can do that search in O(1).
2Optimal — put one array in a set
Put all of nums1 into a HashSet. Then walk nums2 and ask the set "do you have this?" When the answer is yes, add it and remove it from the set, so a second copy can never be added again.
int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
for (int n : nums1) set1.add(n); // duplicates in nums1 vanish here
List<Integer> result = new ArrayList<>();
for (int n : nums2) {
if (set1.remove(n)) result.add(n); // remove() returns true if it was there
}
int[] out = new int[result.size()];
for (int i = 0; i < out.length; i++) out[i] = result.get(i);
return out;
}
Time O(n + m)Space O(n)
Building the set is one pass, checking is another, and every set operation is O(1) on average.
"Is X in the other collection?" → put the other collection in a HashSet first. set.remove(x) returning true is a neat way to "check and use up" in one call.
The answer must have unique numbers. If you only use contains and never remove, nums2 = [2,2] adds 2 twice and you return [2,2] instead of [2].
Pattern notes: Hashing · Open on LeetCode
LC 350Intersection of Two Arrays IIEasyHash MapLeetCode
Same as the last problem, but now duplicates count. A number shows up in the answer as many times as it appears in both arrays (the smaller of the two counts).
Example: nums1 = [1,2,2,1], nums2 = [2,2] → [2,2]. nums1 = [4,9,5], nums2 = [9,4,9,8,4] → [4,9] (any order).
1Brute force — match and mark as used
For each number in nums1, look for an unused equal number in nums2. When you find one, mark it used so it can't be matched twice.
int[] intersect(int[] nums1, int[] nums2) {
boolean[] used = new boolean[nums2.length];
List<Integer> result = new ArrayList<>();
for (int a : nums1) {
for (int j = 0; j < nums2.length; j++) {
if (!used[j] && nums2[j] == a) {
used[j] = true;
result.add(a);
break;
}
}
}
int[] out = new int[result.size()];
for (int i = 0; i < out.length; i++) out[i] = result.get(i);
return out;
}
Time O(n · m)Space O(m)
Again, every number re-scans the other array. A set isn't enough anymore (it forgets how many), but a map can remember counts.
2Optimal — a count map
Upgrade the set to a HashMap<number, count>. Count nums1. Walk nums2: if the count is above 0, take the number and lower the count by 1. The count tells you how many copies are still available.
int[] intersect(int[] nums1, int[] nums2) {
Map<Integer, Integer> count = new HashMap<>();
for (int n : nums1) count.put(n, count.getOrDefault(n, 0) + 1);
List<Integer> result = new ArrayList<>();
for (int n : nums2) {
int left = count.getOrDefault(n, 0); // copies still available
if (left > 0) {
result.add(n);
count.put(n, left - 1); // use one up
}
}
int[] out = new int[result.size()];
for (int i = 0; i < out.length; i++) out[i] = result.get(i);
return out;
}
Time O(n + m)Space O(n)
One pass to count, one pass to match, each map operation O(1) on average.
Set vs map: use a set when you only care whether something is there, and a count map when you care how many times. getOrDefault(n, 0) + 1 is the standard counting line.
Don't forget to decrease the count after taking a number. Without it, nums1 = [1], nums2 = [1,1,1] returns [1,1,1] instead of [1].
Pattern notes: Hashing · Open on LeetCode
LC 290Word PatternEasyHash MapLeetCode
You get a pattern like "abba" and a sentence like "dog cat cat dog". Does the sentence follow the pattern? Each letter must stand for exactly one word, and each word for exactly one letter. It's Isomorphic Strings, but with words.
Example: "abba", "dog cat cat dog" → true. "abba", "dog dog dog dog" → false (a and b can't both be dog).
1Brute force — compare every pair of positions
Split the sentence into words. For every pair of positions, the letters must be equal exactly when the words are equal.
boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
if (words.length != pattern.length()) return false;
for (int i = 0; i < words.length; i++) {
for (int j = i + 1; j < words.length; j++) {
boolean sameLetter = pattern.charAt(i) == pattern.charAt(j);
boolean sameWord = words[i].equals(words[j]);
if (sameLetter != sameWord) return false;
}
}
return true;
}
Time O(n²)Space O(n)
It checks every pair, even though one pass with a record of what each letter means is enough.
2Optimal — two maps, letter→word and word→letter
Walk the letters and words together, like a dictionary you build as you go. One map says what each letter means, the other says which letter owns each word. Any pair that contradicts a map means false.
boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
if (words.length != pattern.length()) return false; // counts must match
Map<Character, String> letterToWord = new HashMap<>();
Map<String, Character> wordToLetter = new HashMap<>();
for (int i = 0; i < words.length; i++) {
char c = pattern.charAt(i);
String w = words[i];
// compare Strings with equals(), never ==
if (letterToWord.containsKey(c) && !letterToWord.get(c).equals(w)) return false;
if (wordToLetter.containsKey(w) && wordToLetter.get(w) != c) return false;
letterToWord.put(c, w);
wordToLetter.put(w, c);
}
return true;
}
Time O(n)Space O(n)
n is the length of s. Splitting and the single loop each touch every character a constant number of times.
This is the same two-map template as Isomorphic Strings. Once you know it, any "does A follow the same shape as B" question is one loop with two maps.
Check that the number of words equals the pattern length. "aaa" with "dog dog dog dog" would pass the map checks for the first three words, but the answer is false.
Pattern notes: Hashing · Open on LeetCode
LC 202Happy NumberEasyCycle DetectionLeetCode
Replace a number by the sum of the squares of its digits, again and again. If you eventually reach 1, the number is "happy". If you get stuck in a loop that never reaches 1, it isn't.
Example: 19 → true: 1²+9² = 82 → 8²+2² = 68 → 6²+8² = 100 → 1. 2 → false (it loops forever).
1Good first answer — remember every number in a set
The only hard part is knowing when to give up. If a number shows up a second time, we are in a loop and will never reach 1. A HashSet remembers what we've seen.
boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = next(n);
}
return n == 1;
}
int next(int n) { // sum of the squares of the digits
int sum = 0;
while (n > 0) {
int d = n % 10; // last digit
sum += d * d;
n /= 10; // drop the last digit
}
return sum;
}
Time O(log n)Space O(log n)
This is already fast and a perfectly good interview answer. The follow-up is: can you detect the loop without storing the numbers?
2Optimal — fast and slow pointers, no set
Think of two runners on a track. Slow takes one step at a time, fast takes two. If the track leads to 1, fast gets there first. If the track is a circle, fast eventually laps slow and they land on the same number. Either way we stop, and we stored nothing.
boolean isHappy(int n) {
int slow = n, fast = next(n);
while (fast != 1 && slow != fast) {
slow = next(slow); // one step
fast = next(next(fast)); // two steps
}
return fast == 1; // stopped at 1, or runners met in a loop
}
// next(n) is the same digit-square helper as above
Time O(log n)Space O(1)
The numbers shrink to at most three digits very quickly, so there are only a handful of steps, and we keep just two variables.
"Repeat a step until it ends or loops" → it's a hidden linked list. Detect the loop with a seen set (easy) or fast and slow pointers (O(1) memory), the same trick as Linked List Cycle.
Without any loop check, while (n != 1) runs forever for unhappy numbers like 2. In the fast/slow version, start fast one step ahead, or slow == fast is true before the loop even begins.
Pattern notes: Fast & Slow Pointers · Open on LeetCode
LC 1207Unique Number of OccurrencesEasyHash MapLeetCode
Count how many times each value appears. Return true if no two different values have the same count.
Example: [1,2,2,1,1,3] → true (1 appears 3 times, 2 twice, 3 once: all different). [1,2] → false (both appear once).
1Brute force — count with loops, compare with loops
For every position, count how often its value appears by scanning the whole array. Then compare every pair: two different values must not share a count.
boolean uniqueOccurrences(int[] arr) {
int n = arr.length;
int[] cnt = new int[n]; // cnt[i] = how often arr[i] appears
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[j] == arr[i]) cnt[i]++;
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i] != arr[j] && cnt[i] == cnt[j]) return false;
}
}
return true;
}
Time O(n²)Space O(n)
Two nested loops, both doing work a HashMap and a HashSet can do in one pass each.
2Optimal — count map, then a set of counts
Build a count map (value → count). Then pour all the counts into a HashSet. A set throws away duplicates, so if the set is smaller than the map, two values shared a count.
boolean uniqueOccurrences(int[] arr) {
Map<Integer, Integer> count = new HashMap<>();
for (int n : arr) count.put(n, count.getOrDefault(n, 0) + 1); // value -> count
Set<Integer> seenCounts = new HashSet<>(count.values()); // duplicates vanish
return seenCounts.size() == count.size(); // no count was lost?
}
Time O(n)Space O(n)
One pass to count, and building the set touches each distinct value once.
"Are all these things different?" → put them in a HashSet and compare sizes. Chaining a count map into a set is a common two-step move.
Compare the set size with the number of distinct values (count.size()), not with arr.length. For [1,2,2,1,1,3], arr.length is 6 but there are only 3 counts, so that version wrongly says false.
Pattern notes: Hashing · Open on LeetCode
Two Pointers
Two pointers means keeping two indexes into the same array and moving them towards each other (or in the same direction) instead of trying every pair. It works best on sorted data, where each comparison tells you which pointer to move. The problems below teach pair sum on a sorted array, filling a result from both ends, and moving the weaker side to search for a maximum.
LC 167Two Sum II - Input Array Is SortedMediumTwo PointersLeetCode
The array is already sorted from small to large. Find the two numbers that add up to the target and return their positions, counting from 1 (not 0). There is exactly one answer, and you must use only O(1) extra space.
Example: numbers = [2, 7, 11, 15], target = 9 → [1, 2], because 2 + 7 = 9.
1Brute force — try every pair
Check every number against every number after it, just like the original Two Sum. We completely ignore the fact that the array is sorted.
int[] twoSum(int[] numbers, int target) {
for (int i = 0; i < numbers.length; i++) {
for (int j = i + 1; j < numbers.length; j++) {
if (numbers[i] + numbers[j] == target) return new int[]{i + 1, j + 1};
}
}
return new int[]{};
}
Time O(n²)Space O(1)
Too slow for big inputs. And a HashMap would fix the speed but break the "O(1) extra space" rule. The sorted order is a gift we are not using.
2Optimal — one pointer at each end
Put left on the smallest number and right on the largest. Look at their sum. Sum too small → move left up; sum too big → move right down. Because the array is sorted, moving left can only make the sum bigger, and moving right can only make it smaller. Each step throws away one number that can never be part of the answer.
int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) return new int[]{left + 1, right + 1}; // 1-indexed!
if (sum < target) left++; // need a bigger sum: bigger left number
else right--; // need a smaller sum: smaller right number
}
return new int[]{};
}
Time O(n)Space O(1)
Each step moves one pointer, and they can only meet once, so it is at most n steps with no extra memory.
Sorted array + "find a pair" → two pointers from both ends. The comparison with the target tells you which side to move.
The answer is 1-indexed. Returning {left, right} instead of {left + 1, right + 1} is the classic wrong answer here.
Pattern notes: Two Pointers · Open on LeetCode
LC 977Squares of a Sorted ArrayEasyTwo PointersLeetCode
You get a sorted array that may contain negative numbers. Square every number and return the squares, also sorted from small to large.
Example: nums = [-4, -1, 0, 3, 10] → [0, 1, 9, 16, 100].
1Brute force — square, then sort
Square each number into a new array, then call Arrays.sort. Simple and correct.
int[] sortedSquares(int[] nums) {
int[] res = new int[nums.length];
for (int i = 0; i < nums.length; i++) res[i] = nums[i] * nums[i];
Arrays.sort(res);
return res;
}
Time O(n log n)Space O(n)
Sorting costs n log n, but the input was already sorted. We are throwing that order away and paying to rebuild it.
2Optimal — the biggest square is always at an end
Negatives become big when squared, so the array of squares looks like a valley: big on the left, big on the right, small in the middle. That means the largest remaining square is always at one of the two ends. Compare the two ends, put the bigger one at the back of the result, and move that pointer inwards.
int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] res = new int[n];
int left = 0, right = n - 1;
for (int pos = n - 1; pos >= 0; pos--) { // fill from the back
int l = nums[left] * nums[left];
int r = nums[right] * nums[right];
if (l > r) { res[pos] = l; left++; } // left end is bigger
else { res[pos] = r; right--; } // right end is bigger (or equal)
}
return res;
}
Time O(n)Space O(n)
One pass, one comparison per slot. The O(n) space is only the output array, which the problem asks for anyway.
When the biggest values sit at the ends, fill the answer from the back with two pointers. The same trick merges two sorted arrays in place.
Don't fill from the front: the smallest square is somewhere in the middle, so you don't know where to start. The largest one is always at an end.
Pattern notes: Two Pointers · Open on LeetCode
LC 11Container With Most WaterMediumTwo PointersLeetCode
Each number is the height of a vertical wall. Pick two walls; the water they hold is shorter wall × distance between them. Return the most water any pair can hold.
Example: height = [1, 8, 6, 2, 5, 4, 8, 3, 7] → 49, using the walls 8 (index 1) and 7 (index 8): min(8, 7) × 7 = 49.
1Brute force — try every pair of walls
Compute the water for every pair and keep the best.
int maxArea(int[] height) {
int best = 0;
for (int i = 0; i < height.length; i++) {
for (int j = i + 1; j < height.length; j++) {
int area = Math.min(height[i], height[j]) * (j - i);
best = Math.max(best, area);
}
}
return best;
}
Time O(n²)Space O(1)
With up to 100,000 walls that is billions of pairs, so it times out on LeetCode.
2Optimal — start wide, always move the shorter wall
Start with the widest container: one wall at each end. Now one side must move in, and the width shrinks by 1. The water level is set by the shorter wall, so keeping it can never help: every narrower container using it is still capped by that same short height. So always move the shorter wall; it is the only move that might find a taller wall and more water.
int maxArea(int[] height) {
int left = 0, right = height.length - 1, best = 0;
while (left < right) {
int h = Math.min(height[left], height[right]); // water level = shorter wall
best = Math.max(best, h * (right - left));
if (height[left] < height[right]) left++; // drop the shorter wall
else right--;
}
return best;
}
Time O(n)Space O(1)
The pointers meet after n − 1 moves, and we never lose the best answer because we only drop walls that cannot do better.
When the answer is limited by the weaker of two sides, start from both ends and move the weaker side. Moving the stronger side can only make things worse.
Width is right - left, not right - left + 1: we measure the gap between walls, not the number of walls. And use min of the two heights, never max: water spills over the shorter wall.
Pattern notes: Two Pointers · Open on LeetCode
Sliding Window
A sliding window is a range of neighbours in an array or string that moves forward one step at a time. Instead of recomputing each range from scratch, you add the item that comes in and remove the item that goes out. The problems below teach the fixed-size window (a running sum), a window kept in a HashSet, and the variable-size window that grows on the right and shrinks on the left.
LC 643Maximum Average Subarray IEasyFixed Sliding WindowLeetCode
Look at every block of exactly k neighbouring numbers. Return the largest average among these blocks.
Example: nums = [1, 12, -5, -6, 50, 3], k = 4 → 12.75, from the block [12, -5, -6, 50] with sum 51, and 51 / 4 = 12.75.
1Brute force — add up every block from scratch
For each starting position, loop over the next k numbers and add them up. Keep the biggest sum and divide by k at the end.
double findMaxAverage(int[] nums, int k) {
double best = Double.NEGATIVE_INFINITY;
for (int i = 0; i + k <= nums.length; i++) {
int sum = 0;
for (int j = i; j < i + k; j++) sum += nums[j];
best = Math.max(best, sum);
}
return best / k;
}
Time O(n·k)Space O(1)
Two neighbouring blocks share k − 1 numbers, yet we add all of them again every time. That repeated work is what we remove next.
2Optimal — slide the window, keep a running sum
Think of a frame k boxes wide sliding one step to the right. Only two things change: one number enters on the right, one leaves on the left. So new sum = old sum + entering − leaving. That is O(1) work per step instead of O(k).
double findMaxAverage(int[] nums, int k) {
int sum = 0;
for (int i = 0; i < k; i++) sum += nums[i]; // build the first window
int best = sum;
for (int i = k; i < nums.length; i++) {
sum += nums[i] - nums[i - k]; // nums[i] enters, nums[i-k] leaves
best = Math.max(best, sum);
}
return (double) best / k; // divide once, at the end
}
Time O(n)Space O(1)
Every number is added once and removed once, so the whole scan is linear.
Fixed-size window: build the first window, then slide with "add the new one, subtract the one that fell off" (nums[i - k]).
best / k with two ints does integer division (51 / 4 = 12). Cast first: (double) best / k. Also start best from the first window's sum, not 0, because every sum may be negative.
Pattern notes: Sliding Window · Open on LeetCode
LC 219Contains Duplicate IIEasySliding Window + SetLeetCode
Is there a number that appears twice with the two copies at most k positions apart? Return true or false.
Example: nums = [1, 0, 1, 1], k = 1 → true, because the 1s at index 2 and 3 are only 1 apart. (With [1, 2, 3, 1, 2, 3], k = 2 the answer is false: every pair of copies is 3 apart.)
1Brute force — compare each number with the next k
For each position, look at the next k positions and check for an equal number.
boolean containsNearbyDuplicate(int[] nums, int k) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length && j - i <= k; j++) {
if (nums[i] == nums[j]) return true;
}
}
return false;
}
Time O(n·k)Space O(1)
When k is as big as n this is O(n²). We keep re-reading the same neighbours for every i.
2Optimal — keep the last k numbers in a HashSet
Only the last k numbers matter: anything older is too far away. So keep a HashSet holding exactly the last k numbers. For each new number, ask the set "have you got me?" (O(1)). Then add it, and if the set grew beyond k, remove the number that just fell out of range, nums[i - k].
boolean containsNearbyDuplicate(int[] nums, int k) {
Set<Integer> window = new HashSet<>(); // the last k numbers
for (int i = 0; i < nums.length; i++) {
if (window.contains(nums[i])) return true; // a copy within k steps
window.add(nums[i]);
if (window.size() > k) {
window.remove(nums[i - k]); // too far behind now: slide it out
}
}
return false;
}
Time O(n)Space O(min(n, k))
Each number enters and leaves the set once, and the set never holds more than k numbers.
"Duplicate within distance k" → a HashSet that works as a sliding window: check, add, and evict the element that is now too far away.
Check contains before adding, otherwise every number "finds itself". And "at most k apart" means j - i <= k, so the set holds k numbers, not k + 1.
Pattern notes: Sliding Window · Open on LeetCode
LC 1876Substrings of Size Three with Distinct CharactersEasyFixed Sliding WindowLeetCode
Count the pieces of 3 neighbouring characters in the string where all 3 characters are different. Overlapping pieces count separately.
Example: s = "xyzzaz" → 1. The pieces are "xyz", "yzz", "zza", "zaz", and only "xyz" has no repeat.
1Brute force — put each piece into a HashSet
For every start position, add the 3 characters to a set. If the set ends up with 3 items, they were all different.
int countGoodSubstrings(String s) {
int count = 0;
for (int i = 0; i + 3 <= s.length(); i++) {
Set<Character> seen = new HashSet<>();
for (int j = i; j < i + 3; j++) seen.add(s.charAt(j));
if (seen.size() == 3) count++;
}
return count;
}
Time O(n)Space O(1)
Because the window is always size 3, this is already linear. But it creates a new set for every position, which is heavy for such a tiny check, and it hides the sliding-window shape.
2Cleaner — slide a 3-wide window and compare directly
With only 3 characters, "all different" is just a ≠ b, b ≠ c and a ≠ c. Slide the window with its right end at i, so it covers i-2, i-1, i, and check those three comparisons. No set needed. Nothing faster exists: you must look at every character at least once.
int countGoodSubstrings(String s) {
int count = 0;
for (int i = 2; i < s.length(); i++) { // window = s[i-2..i]
char a = s.charAt(i - 2), b = s.charAt(i - 1), c = s.charAt(i);
if (a != b && b != c && a != c) count++; // all three different
}
return count;
}
Time O(n)Space O(1)
Same big-O as step 1, but no objects are created, and the code states the rule directly.
For a small fixed window, loop over the window's right end and read the few items behind it. For a large or growing window, use a counter map instead.
Don't forget a != c: "zaz" passes a != b and b != c but is not distinct. Also, strings shorter than 3 must return 0; starting the loop at i = 2 handles that for free.
Pattern notes: Sliding Window · Open on LeetCode
LC 3Longest Substring Without Repeating CharactersMediumVariable Sliding WindowLeetCode
Find the longest run of neighbouring characters in which no character appears twice. Return its length.
Example: s = "abcabcbb" → 3 ("abc"). For "pwwkew" the answer is 3 ("wke"); "pwke" does not count because it is not one continuous piece.
1Brute force — grow from every start
From each start position, extend to the right, remembering characters in a set, and stop at the first repeat.
int lengthOfLongestSubstring(String s) {
int best = 0;
for (int i = 0; i < s.length(); i++) {
Set<Character> seen = new HashSet<>();
for (int j = i; j < s.length(); j++) {
if (seen.contains(s.charAt(j))) break; // repeat: this start is done
seen.add(s.charAt(j));
best = Math.max(best, j - i + 1);
}
}
return best;
}
Time O(n²)Space O(k) distinct chars
After hitting a repeat we throw the whole run away and restart from the next letter, re-reading characters we already checked.
2Optimal — grow on the right, shrink on the left
Keep a window [left, right] with no repeats, and its letters in a set. Move right forward one letter at a time. If the new letter is already inside, shrink from the left until the old copy is gone, then add it. The window is always valid, so its size after each step is a candidate answer. Like a caterpillar: the head moves forward, the tail catches up only when it has to.
int lengthOfLongestSubstring(String s) {
Set<Character> window = new HashSet<>(); // letters in s[left..right]
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
while (window.contains(c)) { // shrink until c is no longer inside
window.remove(s.charAt(left));
left++;
}
window.add(c);
best = Math.max(best, right - left + 1); // window is valid here
}
return best;
}
Time O(n)Space O(k) distinct chars
The inner while looks scary, but across the whole run left moves at most n times in total, so it is O(n), not O(n²).
Variable window template: expand right every step; while the window is broken, shrink left; then record the answer. Most "longest substring with ..." problems use exactly this shape.
Use while, not if, when shrinking: in "abcb", when the second b arrives you must drop both a and b. And test the empty string "": the answer is 0.
Pattern notes: Sliding Window · Open on LeetCode
Prefix Sum
A prefix sum is a running total: prefix[i] is the sum of everything up to position i. Build it once in O(n), and then the sum of any block of the array is one subtraction. The key patterns: build a running total in one pass, left sum + right sum = total, and range sum = prefix[right + 1] − prefix[left].
LC 1480Running Sum of 1d ArrayEasyPrefix SumLeetCode
Return a new array where position i holds the sum of nums[0] up to nums[i]. It's the running total you'd see on a till receipt.
Example: nums = [1, 2, 3, 4] → [1, 3, 6, 10] (1, 1+2, 1+2+3, 1+2+3+4).
1Brute force — add up from the start for every position
For each position i, loop from 0 to i and add everything.
int[] runningSum(int[] nums) {
int[] ans = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
int sum = 0;
for (int j = 0; j <= i; j++) sum += nums[j]; // re-add everything
ans[i] = sum;
}
return ans;
}
Time O(n²)Space O(n)
We keep re-adding the same numbers: to get position 3 we add 1 + 2 + 3 again, even though we already know 1 + 2 + 3 = 6 from the step before.
2Optimal — reuse the previous total
Each running total is the previous total + the current number. So one pass is enough, and we can even write the totals straight into nums.
int[] runningSum(int[] nums) {
for (int i = 1; i < nums.length; i++) {
nums[i] += nums[i - 1]; // previous total + this number
}
return nums;
}
Time O(n)Space O(1)
Each number is added once. Space is O(1) extra because we reuse the input array (make a new array instead if you must not change the input).
This is the prefix sum itself. Whenever you catch yourself re-adding the same numbers, keep a running total instead.
Start the loop at i = 1. Starting at 0 reads nums[-1] and throws ArrayIndexOutOfBoundsException.
Pattern notes: Prefix Sum · Open on LeetCode
LC 724Find Pivot IndexEasyPrefix SumLeetCode
Find an index where the sum of everything to its left equals the sum of everything to its right (the number at the index itself is not counted on either side). If there are several, return the leftmost. If there is none, return -1. An empty side counts as 0.
Example: nums = [1, 7, 3, 6, 5, 6] → 3: left 1 + 7 + 3 = 11, right 5 + 6 = 11.
1Brute force — add both sides for every index
For each index, loop over the left part and the right part separately and compare the two sums.
int pivotIndex(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int left = 0, right = 0;
for (int j = 0; j < i; j++) left += nums[j];
for (int j = i + 1; j < nums.length; j++) right += nums[j];
if (left == right) return i;
}
return -1;
}
Time O(n²)Space O(1)
Every index re-adds almost the whole array. With 10,000 numbers that's about 100 million additions.
2Optimal — total sum and a running left sum
First add up the whole array once: total. Then walk left to right keeping left, the sum before i. The right side doesn't need its own loop: right = total − left − nums[i]. Like a see-saw: you know the total weight, so knowing one side tells you the other.
left grows by nums[i] after each check.int pivotIndex(int[] nums) {
int total = 0;
for (int x : nums) total += x; // pass 1: whole sum
int left = 0; // sum of nums[0..i-1]
for (int i = 0; i < nums.length; i++) {
int right = total - left - nums[i]; // everything after i
if (left == right) return i;
left += nums[i]; // i moves into the left side
}
return -1;
}
Time O(n)Space O(1)
Two simple passes instead of a loop inside a loop, and no extra array: left is a prefix sum we keep in one variable.
If you know the total, one running sum gives you both sides: right = total − left − current.
Check before adding nums[i] to left. And don't forget index 0 can be the pivot: in [2, 1, -1], left is 0 (empty) and right is 1 + (-1) = 0, so the answer is 0.
Pattern notes: Prefix Sum · Open on LeetCode
LC 303Range Sum Query - ImmutableEasyPrefix SumLeetCode
Build a class NumArray that gets an array once, then answers many questions sumRange(left, right): the sum of nums[left] through nums[right], both ends included. The array never changes.
Example: nums = [-2, 0, 3, -5, 2, -1]. sumRange(0, 2) → 1, sumRange(2, 5) → -1, sumRange(0, 5) → -3.
1Brute force — loop over the range for every query
Keep the array and add up the requested range each time someone asks.
class NumArray {
private final int[] nums;
public NumArray(int[] nums) {
this.nums = nums;
}
public int sumRange(int left, int right) {
int sum = 0;
for (int i = left; i <= right; i++) sum += nums[i];
return sum;
}
}
Time O(n) per querySpace O(1)
One query is fine, but there can be up to 10,000 queries on 10,000 numbers: about 100 million additions, mostly adding the same numbers again and again.
2Optimal — precompute a prefix sum array
Do the work once in the constructor. Let prefix[k] = the sum of the first k numbers (so prefix[0] = 0). The sum from left to right is "everything up to right" minus "everything before left": prefix[right + 1] − prefix[left]. Like reading two marks on a tape measure and subtracting.
prefix[6] covers all six numbers; prefix[2] covers the two before index 2. Subtract, and only indices 2 to 5 (green) remain.class NumArray {
private final int[] prefix; // prefix[k] = sum of the first k numbers
public NumArray(int[] nums) {
prefix = new int[nums.length + 1]; // prefix[0] = 0
for (int i = 0; i < nums.length; i++) {
prefix[i + 1] = prefix[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return prefix[right + 1] - prefix[left]; // O(1) per query
}
}
Time O(n) build, O(1) per querySpace O(n)
We pay once up front and every query after that is a single subtraction. Great trade when there are many queries.
Many "sum of a range" questions on an array that doesn't change → build a prefix array once, then each range sum is prefix[r + 1] − prefix[l].
Off-by-one is the classic bug. Making the prefix array one longer (with prefix[0] = 0) means left = 0 needs no special case. Mixing up prefix[right] and prefix[right + 1] drops the last number.
Pattern notes: Prefix Sum · Open on LeetCode
Stack & Queue
A stack is a pile of plates: you only touch the top (last in, first out). A queue is a line at a shop: first in, first out. In Java use Deque<Integer> stack = new ArrayDeque<>() with push, pop, peek. The problems below teach matching pairs, undo the last action, building one structure out of another, remembering extra info per item, and the monotonic stack for "next greater" questions.
LC 20Valid ParenthesesEasyStackLeetCode
The string contains only brackets ( ) [ ] { }. It is valid if every opening bracket is closed by the same type, in the right order (the most recent one opened must close first).
Example: "([]{})" → true. "([)]" → false, because [ was opened last but ) tries to close first.
1Brute force — delete matching pairs until nothing changes
A valid string always has at least one pair like (), [] or {} sitting right next to each other. Remove them and repeat. If the string becomes empty, it was valid.
boolean isValid(String s) {
String prev = null;
while (!s.equals(prev)) { // stop when a round removes nothing
prev = s;
s = s.replace("()", "").replace("[]", "").replace("{}", "");
}
return s.isEmpty();
}
Time O(n²)Space O(n)
Deeply nested input like "(((...)))" needs about n/2 rounds, and each round copies the whole string.
2Optimal — a stack of the closers we are waiting for
The bracket opened last must be closed first: that is exactly a stack. When you see an opener, push the closer you now expect. When you see a closer, it must equal the top of the stack; pop it. At the end nothing may be left waiting.
boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')'); // opener: remember which closer we need
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
else if (stack.isEmpty() || stack.pop() != c) return false; // wrong or no opener
}
return stack.isEmpty(); // leftover openers were never closed
}
Time O(n)Space O(n)
One pass; each character is pushed or popped once.
"Most recent thing must be handled first" → stack. Pushing the expected closer keeps the check to one comparison.
Two edge cases: a closer when the stack is empty ("]") must return false, not crash; and leftover openers ("((") mean false, so return stack.isEmpty(), not true.
Pattern notes: Stacks · Open on LeetCode
LC 682Baseball GameEasyStackLeetCode
You keep a list of scores. Each operation is either a number (add that score), "+" (add a score equal to the last two added), "D" (add double the last score), or "C" (cancel the last score). Return the sum of the scores left at the end.
Example: ["5", "2", "C", "D", "+"] → 30. Scores go 5, then 5 2, then 5 (2 cancelled), then 5 10, then 5 10 15. Sum = 30.
1Brute force — simulate with a list, sum at the end
Do exactly what the rules say using an ArrayList, always working with its last items, then add everything up.
int calPoints(String[] operations) {
List<Integer> scores = new ArrayList<>();
for (String op : operations) {
int n = scores.size();
if (op.equals("+")) scores.add(scores.get(n - 1) + scores.get(n - 2));
else if (op.equals("D")) scores.add(2 * scores.get(n - 1));
else if (op.equals("C")) scores.remove(n - 1);
else scores.add(Integer.parseInt(op));
}
int total = 0;
for (int s : scores) total += s;
return total;
}
Time O(n)Space O(n)
This is already optimal: you must read every operation, and you must remember old scores because "C" can cancel back through them. Notice that we only ever touch the end of the list: that is a stack in disguise.
2Cleaner — a real stack plus a running total
"Cancel the last one" is an undo, and undo is the classic stack job. Use a stack for the scores and keep the total up to date as you go: add when you push, subtract when you pop. Then no second loop is needed.
int calPoints(String[] operations) {
Deque<Integer> stack = new ArrayDeque<>();
int total = 0;
for (String op : operations) {
if (op.equals("+")) {
int top = stack.pop(); // take the top off to see the one below
int sum = top + stack.peek();
stack.push(top); // put it back
stack.push(sum);
total += sum;
} else if (op.equals("D")) {
int d = 2 * stack.peek();
stack.push(d);
total += d;
} else if (op.equals("C")) {
total -= stack.pop(); // undo the last score
} else {
int v = Integer.parseInt(op); // a plain number, may be negative
stack.push(v);
total += v;
}
}
return total;
}
Time O(n)Space O(n)
Same big-O as step 1; the stack just says clearly "we only care about the latest scores".
Undo / cancel the last action → stack. Keeping a running total next to it saves a final pass.
Check the letters "+", "D", "C" before calling parseInt, and compare strings with equals, never ==. Scores like "-2" are valid numbers.
Pattern notes: Stacks · Open on LeetCode
LC 844Backspace String CompareEasyStack / Two PointersLeetCode
Two strings are typed into text editors. A # means "backspace": delete the character before it (if there is one). Are the two final texts equal?
Example: s = "ab#c", t = "ad#c" → true. Both become "ac".
1Brute force — actually type both strings, then compare
Use a StringBuilder as a stack: append normal characters, delete the last one on #. Build both final strings and compare them.
boolean backspaceCompare(String s, String t) {
return build(s).equals(build(t));
}
String build(String str) {
StringBuilder sb = new StringBuilder(); // works like a stack of chars
for (char c : str.toCharArray()) {
if (c != '#') sb.append(c);
else if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
Time O(n + m)Space O(n + m)
Fast and a great answer to start with. The follow-up asks for O(1) extra space, so we must avoid building new strings.
2Optimal — walk both strings backwards
Reading forwards, you don't know yet whether a character will be erased. Reading backwards, you do: a # comes before the letter it deletes. So walk from the end, count the #s you pass and skip that many letters. Each time both strings land on a real surviving letter, compare them.
boolean backspaceCompare(String s, String t) {
int i = s.length() - 1, j = t.length() - 1;
while (i >= 0 || j >= 0) {
i = nextChar(s, i); // jump to the next letter that survives
j = nextChar(t, j);
if (i < 0 && j < 0) return true; // both finished at the same time
if (i < 0 || j < 0) return false; // one text is longer
if (s.charAt(i) != t.charAt(j)) return false;
i--; j--;
}
return true;
}
// Returns the index of the next surviving char at or before i, or -1.
int nextChar(String str, int i) {
int skip = 0;
while (i >= 0) {
if (str.charAt(i) == '#') { skip++; i--; } // one more letter to erase
else if (skip > 0) { skip--; i--; } // this letter is erased
else break; // a real, surviving letter
}
return i;
}
Time O(n + m)Space O(1)
Each index moves left only, so both strings are read once, and we store just a few ints.
"Undo" characters → a stack (StringBuilder). If you need O(1) space, process from the end, where you already know what gets undone.
A # on an empty text does nothing: "a##c" equals "#a#c" (both are "c"). And #s can chain, so count them with a skip counter rather than jumping back a fixed 2 places.
Pattern notes: Stacks · Open on LeetCode
LC 232Implement Queue using StacksEasyDesign · Two StacksLeetCode
Build a queue (first in, first out) with push, pop, peek and empty, but you may only use stacks (last in, first out) inside.
Example: push(1), push(2), peek(), pop(), empty() → 1, 1, false. The first one pushed is the first one out.
1Simple design — keep the stack in queue order on every push
Keep one main stack with the oldest item on top. To push, pour everything into a temp stack, put the new item at the bottom, and pour everything back. Then pop and peek are just the top.
class MyQueue {
private Deque<Integer> main = new ArrayDeque<>(); // oldest item on top
private Deque<Integer> temp = new ArrayDeque<>();
public MyQueue() {}
public void push(int x) {
while (!main.isEmpty()) temp.push(main.pop()); // move all out
main.push(x); // new item goes to the bottom
while (!temp.isEmpty()) main.push(temp.pop()); // move all back on top of it
}
public int pop() { return main.pop(); }
public int peek() { return main.peek(); }
public boolean empty() { return main.isEmpty(); }
}
push O(n)pop / peek O(1)Space O(n)
Correct, but every single push moves the whole queue twice. n pushes cost O(n²) in total.
2Better design — an "in" stack and an "out" stack
Pouring a stack into another stack reverses it, which turns newest-on-top into oldest-on-top. So push always goes onto in. For pop or peek, read from out, and only when out is empty, pour all of in into out. Each item is poured at most once in its life.
class MyQueue {
private Deque<Integer> in = new ArrayDeque<>(); // new items land here
private Deque<Integer> out = new ArrayDeque<>(); // oldest item on top
public MyQueue() {}
public void push(int x) { in.push(x); }
public int pop() { moveIfNeeded(); return out.pop(); }
public int peek() { moveIfNeeded(); return out.peek(); }
public boolean empty() { return in.isEmpty() && out.isEmpty(); }
private void moveIfNeeded() {
if (out.isEmpty()) { // only pour when out runs dry
while (!in.isEmpty()) out.push(in.pop());
}
}
}
All operations O(1) amortizedSpace O(n)
A single pop can take O(n) when it pours, but each item is poured only once, so n operations cost O(n) in total: O(1) each on average ("amortized").
Two stacks make a queue: push to in, pop from out, refill out only when it is empty. "Amortized O(1)" means an occasional slow step is paid for by many fast ones.
Never pour into out while it still has items: new items would land on top of older ones and break the order. And empty() must check both stacks.
Pattern notes: Queues & Deques · Open on LeetCode
LC 155Min StackMediumDesign · StackLeetCode
Build a stack with push, pop, top and one extra: getMin(), which returns the smallest value currently in the stack. Every operation must be O(1).
Example: push(-2), push(0), push(-3), getMin(), pop(), top(), getMin() → -3, 0, -2.
1Simple design — scan the whole stack for the min
Use a normal stack, and when someone asks for the minimum, loop over every item.
class MinStack {
private Deque<Integer> stack = new ArrayDeque<>();
public MinStack() {}
public void push(int val) { stack.push(val); }
public void pop() { stack.pop(); }
public int top() { return stack.peek(); }
public int getMin() {
int min = Integer.MAX_VALUE;
for (int v : stack) min = Math.min(min, v); // look at everything
return min;
}
}
getMin O(n)others O(1)Space O(n)
Breaks the rule that every operation is O(1). A single variable min doesn't work either: after you pop the minimum, you have no idea what the previous minimum was.
2Better design — every item remembers the min below it
A stack only changes at the top, so the minimum of "this item and everything under it" never changes while the item sits there. So store each value together with the min at that moment: the pair [val, minSoFar]. getMin just reads the top pair. When you pop, the pair below already knows the old minimum. Like writing "the lowest so far" on each plate as you stack it.
class MinStack {
private Deque<int[]> stack = new ArrayDeque<>(); // each item: {val, minSoFar}
public MinStack() {}
public void push(int val) {
int min = stack.isEmpty() ? val : Math.min(val, stack.peek()[1]);
stack.push(new int[]{val, min});
}
public void pop() { stack.pop(); }
public int top() { return stack.peek()[0]; }
public int getMin() { return stack.peek()[1]; }
}
All operations O(1)Space O(n)
We pay one extra int per item to get the minimum instantly, even after pops.
If a stack needs a running statistic (min, max), store it with each item. Popping then undoes the statistic for free.
Duplicates of the minimum: push 0, 1, 0 then pop; the min must still be 0. Storing a min per item handles this. If you use the "second stack of mins" variant, push onto it when val <= min (with <=, not <), or duplicates break it.
Pattern notes: Stacks · Open on LeetCode
LC 496Next Greater Element IEasyMonotonic StackLeetCode
For a number in nums2, its "next greater element" is the first number to its right that is bigger (or -1 if there is none). nums1 is a list of some numbers from nums2 (all numbers are distinct). For each of them, return its next greater element in nums2.
Example: nums1 = [4, 1, 2], nums2 = [1, 3, 4, 2] → [-1, 3, -1]. Nothing bigger comes after 4; after 1 the first bigger is 3; nothing bigger comes after 2.
1Brute force — find it, then look right
For each number in nums1, find where it sits in nums2, then walk to the right until you hit something bigger.
int[] nextGreaterElement(int[] nums1, int[] nums2) {
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i] = -1;
int j = 0;
while (nums2[j] != nums1[i]) j++; // find its position
for (j = j + 1; j < nums2.length; j++) { // scan to the right
if (nums2[j] > nums1[i]) { res[i] = nums2[j]; break; }
}
}
return res;
}
Time O(n1 · n2)Space O(1)
Fine for this small problem, but each query rescans the array. With many queries, or the harder versions of this problem, it becomes O(n²).
2Optimal — a monotonic stack answers everyone in one pass
Walk through nums2 once. Keep a stack of numbers that are still waiting for a bigger number. The stack is always in decreasing order (that's what "monotonic" means). When a new number arrives, it is the answer for every smaller number on top of the stack: pop them and record "next greater = this number" in a HashMap. Then push the new number to wait its turn. Picture people in a queue for a view: a tall person arriving blocks everyone shorter in front.
int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> nextGreater = new HashMap<>(); // number -> its next greater
Deque<Integer> stack = new ArrayDeque<>(); // numbers still waiting
for (int num : nums2) {
while (!stack.isEmpty() && stack.peek() < num) {
nextGreater.put(stack.pop(), num); // num is the first bigger one to its right
}
stack.push(num); // now num waits for its own answer
}
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i] = nextGreater.getOrDefault(nums1[i], -1); // never answered = -1
}
return res;
}
Time O(n1 + n2)Space O(n2)
Each number of nums2 is pushed once and popped at most once, so the while loop adds only O(n2) work in total.
"Next greater / next smaller / how many days until warmer" → monotonic stack. The stack holds items still waiting for an answer, and the newcomer answers them as it pops.
Use while, not if: one big number can answer many waiting numbers at once (in [3, 2, 1, 5], the 5 pops 1, 2 and 3). And numbers left in the stack at the end get -1, which getOrDefault handles.
Pattern notes: Monotonic Stack & Queue · Open on LeetCode
Linked List
A linked list is a chain of nodes where each node holds a value and an arrow (next) to the following node. You can't jump to position 5; you have to walk there. The key patterns here: re-pointing arrows in place (reversal), a dummy node in front of the head, two pointers walking side by side, and slow / fast pointers to find the middle or a loop.
LC 206Reverse Linked ListEasyIn-place ReversalLeetCode
You get the head of a linked list. Turn every arrow around so the list runs backwards, and return the new head.
Example: 1 → 2 → 3 → 4 → 5 → 5 → 4 → 3 → 2 → 1.
1Brute force — copy the values, rebuild backwards
Walk the list and copy every value into an ArrayList. Then build a brand new list by reading that ArrayList from the end to the start.
ListNode reverseList(ListNode head) {
List<Integer> vals = new ArrayList<>();
for (ListNode n = head; n != null; n = n.next) vals.add(n.val); // copy values out
ListNode dummy = new ListNode(0), tail = dummy;
for (int i = vals.size() - 1; i >= 0; i--) { // rebuild backwards
tail.next = new ListNode(vals.get(i));
tail = tail.next;
}
return dummy.next;
}
Time O(n)Space O(n)
The speed is already fine, but we made a full copy of the list. The interviewer wants you to reverse it in place, using only a few pointers.
2Optimal — flip the arrows one by one with prev / curr
Walk the list once. At each node, save next, point curr back at prev, then step both forward. prev is the reversed part growing behind you; curr is the node you are fixing. You must save next first, or you lose the rest of the list the moment you flip the arrow.
ListNode reverseList(ListNode head) {
ListNode prev = null; // the part already reversed
ListNode curr = head; // the node we are fixing now
while (curr != null) {
ListNode next = curr.next; // 1. save the rest of the list
curr.next = prev; // 2. flip the arrow backwards
prev = curr; // 3. move prev forward
curr = next; // 4. move curr forward
}
return prev; // prev is the new head
}
Time O(n)Space O(1)
Every node is visited once and we only ever hold three pointers, no matter how long the list is.
The four-line dance save next, flip, move prev, move curr is the core of every "reverse part of a list" problem. Learn it by heart.
Returning head or curr at the end. After the loop head is the last node and curr is null; the new head is prev.
Pattern notes: In-place Linked List Reversal · Open on LeetCode
LC 21Merge Two Sorted ListsEasyDummy NodeLeetCode
You get two linked lists, each already sorted from small to big. Join them into one sorted list by re-using their nodes, and return its head.
Example: 1 → 2 → 4 and 1 → 3 → 4 → 1 → 1 → 2 → 3 → 4 → 4.
1Brute force — dump everything, sort, rebuild
Copy all values from both lists into one ArrayList, sort it, and build a new list from the sorted values.
ListNode mergeTwoLists(ListNode list1, ListNode list2) {
List<Integer> vals = new ArrayList<>();
for (ListNode n = list1; n != null; n = n.next) vals.add(n.val);
for (ListNode n = list2; n != null; n = n.next) vals.add(n.val);
Collections.sort(vals); // ignores that both are already sorted
ListNode dummy = new ListNode(0), tail = dummy;
for (int v : vals) { tail.next = new ListNode(v); tail = tail.next; }
return dummy.next;
}
Time O((n+m) log(n+m))Space O(n+m)
We throw away the most useful fact we were given: both lists are already sorted. Sorting again is wasted work, and so is the copy.
2Optimal — zip the two lists with a dummy node
Like merging two sorted piles of cards: look at the top card of each pile and always take the smaller front node, then attach it to the end of your result. A dummy node at the start means the result always has a "last node" to attach to, so the first node needs no special case.
ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(0); // fake start, so we never special-case the head
ListNode tail = dummy; // last node of the merged list
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) { // take the smaller front node
tail.next = list1;
list1 = list1.next;
} else {
tail.next = list2;
list2 = list2.next;
}
tail = tail.next;
}
tail.next = (list1 != null) ? list1 : list2; // glue on whatever is left
return dummy.next;
}
Time O(n+m)Space O(1)
Each node is looked at once, and we only re-point arrows, so no extra memory is needed.
When you build a new list, start with a dummy node and a tail pointer, then return dummy.next. It removes all the "is the result still empty?" checks.
Forgetting the leftovers. When one list runs out, the other may still have nodes. You don't need a loop: one line tail.next = rest attaches the whole remaining chain.
Pattern notes: Linked Lists · Open on LeetCode
LC 876Middle of the Linked ListEasyFast & Slow PointersLeetCode
Return the middle node of a linked list. If there are two middles (even length), return the second one.
Example: 1 → 2 → 3 → 4 → 5 → node 3. For 1 → 2 → 3 → 4 → 5 → 6 → node 4.
1Brute force — copy nodes into an array
A list has no index, but an array does. Put every node into an ArrayList, then return the one at position size / 2.
ListNode middleNode(ListNode head) {
List<ListNode> nodes = new ArrayList<>();
for (ListNode n = head; n != null; n = n.next) nodes.add(n); // copy nodes into an array
return nodes.get(nodes.size() / 2); // index n/2 = second middle
}
Time O(n)Space O(n)
Fast enough, but it stores every node. We can find the middle in one walk with no extra memory.
2Optimal — slow and fast pointers
Two runners start at the head. Slow moves 1 step, fast moves 2 steps. Fast covers twice the distance, so when fast reaches the end, slow is exactly halfway.
ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) { // fast can still take 2 steps
slow = slow.next; // 1 step
fast = fast.next.next; // 2 steps
}
return slow; // fast hit the end, slow is halfway
}
Time O(n)Space O(1)
One pass, two pointers. For even length, fast lands on null, which leaves slow on the second middle, exactly what the problem asks.
"Find the middle" or "find something in one pass without knowing the length" → slow moves 1, fast moves 2. You will reuse this in Palindrome Linked List and in cycle detection.
The loop check must be fast != null && fast.next != null, in that order. Checking only fast.next crashes with a NullPointerException on even-length lists.
Pattern notes: Fast & Slow Pointers · Open on LeetCode
LC 141Linked List CycleEasyFast & Slow PointersLeetCode
Does the list have a loop? That is, does some node's next point back to an earlier node, so walking forward never reaches null? Return true or false.
Example: 3 → 2 → 0 → -4, and -4 points back to 2 → true.
1Brute force — remember every node you visit
Walk the list and put each node into a HashSet. If you ever meet a node that is already in the set, you have gone around a loop.
boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<>(); // stores NODES, not values
for (ListNode n = head; n != null; n = n.next) {
if (!seen.add(n)) return true; // add() returns false if already there
}
return false; // reached null: no loop
}
Time O(n)Space O(n)
Correct, but the set can hold every node. LeetCode's follow-up asks for O(1) memory.
2Optimal — Floyd's tortoise and hare
Think of two runners on a track. If the track is a straight line, the fast one just finishes. If it is a loop, the fast runner eventually laps the slow one and they meet. Each step the gap between them shrinks by 1, so fast can never jump over slow.
boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // tortoise: 1 step
fast = fast.next.next; // hare: 2 steps
if (slow == fast) return true; // same NODE: the hare lapped the tortoise
}
return false; // hare fell off the end: no loop
}
Time O(n)Space O(1)
Without a loop, fast reaches the end in n/2 steps. With a loop, they meet within about one lap. Either way it's linear, with just two pointers.
"Is there a loop?" → slow and fast pointers; if they ever meet, there is a cycle. No memory needed.
Compare the nodes (slow == fast), not the values. Two different nodes can hold the same number. Also move first, then compare: both start on the head, so checking before moving would say "cycle" straight away.
Pattern notes: Fast & Slow Pointers · Open on LeetCode
LC 83Remove Duplicates from Sorted ListEasyPointer WalkLeetCode
The list is sorted, so equal values sit next to each other. Delete the extra copies so each value appears only once, and return the head.
Example: 1 → 1 → 2 → 3 → 3 → 1 → 2 → 3.
1Brute force — collect unique values, rebuild
Put every value into a LinkedHashSet (a set that remembers insertion order) to drop repeats, then build a fresh list from the set.
ListNode deleteDuplicates(ListNode head) {
Set<Integer> seen = new LinkedHashSet<>(); // keeps first-seen order
for (ListNode n = head; n != null; n = n.next) seen.add(n.val);
ListNode dummy = new ListNode(0), tail = dummy;
for (int v : seen) { tail.next = new ListNode(v); tail = tail.next; }
return dummy.next;
}
Time O(n)Space O(n)
This would work even on an unsorted list, which is a hint that we are not using the "sorted" gift. Sorted means copies are neighbours, so we can just unlink them.
2Optimal — compare each node with its neighbour
Stand on a node and look at the next one. If it has the same value, skip over it by pointing curr.next one further, and stay where you are (there might be a third copy). If it's different, step forward.
ListNode deleteDuplicates(ListNode head) {
ListNode curr = head;
while (curr != null && curr.next != null) {
if (curr.val == curr.next.val) {
curr.next = curr.next.next; // skip the copy, but DON'T move yet
} else {
curr = curr.next; // next value is new, step forward
}
}
return head;
}
Time O(n)Space O(1)
One walk, no extra structure. The head never changes because the first node is always kept.
To delete a node in a singly linked list, stand on the node before it and write curr.next = curr.next.next. "Sorted" usually means "the things you care about are neighbours".
Moving curr forward right after deleting. With 1 → 1 → 1 you would remove one copy, step onto the second 1, and never compare it with the third. Only move when the values differ.
Pattern notes: Linked Lists · Open on LeetCode
LC 203Remove Linked List ElementsEasyDummy NodeLeetCode
Remove every node whose value equals val, and return the new head. The head itself might need removing, even several times in a row.
Example: 1 → 2 → 6 → 3 → 4 → 5 → 6, val = 6 → 1 → 2 → 3 → 4 → 5. And 7 → 7 → 7 → 7, val = 7 → empty list.
1Brute force — copy the values you keep, rebuild
Walk the list, copy every value that is not val into an ArrayList, then build a new list from it.
ListNode removeElements(ListNode head, int val) {
List<Integer> keep = new ArrayList<>();
for (ListNode n = head; n != null; n = n.next) {
if (n.val != val) keep.add(n.val); // copy only the good values
}
ListNode dummy = new ListNode(0), tail = dummy;
for (int v : keep) { tail.next = new ListNode(v); tail = tail.next; }
return dummy.next;
}
Time O(n)Space O(n)
It builds a whole second list. We can unlink the bad nodes in place instead.
2Optimal — dummy node, then unlink in place
To delete a node you need the node before it. The head has no node before it, so we give it one: a dummy node placed in front of the head. Now every real node, the head included, has a predecessor and one simple rule handles them all.
ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0, head); // sits before the real head
ListNode curr = dummy; // we always look at curr.next
while (curr.next != null) {
if (curr.next.val == val) {
curr.next = curr.next.next; // unlink it, stay put
} else {
curr = curr.next; // keep it, move on
}
}
return dummy.next; // the head may have changed
}
Time O(n)Space O(1)
One pass, and only one extra node no matter how long the list is.
Whenever the head might be deleted or changed, put a dummy node in front and return dummy.next. It turns a special case into the normal case.
Returning head instead of dummy.next. In 6 → 1 → 6 with val = 6, the old head was removed, so returning head gives back a deleted node.
Pattern notes: Linked Lists · Open on LeetCode
LC 160Intersection of Two Linked ListsEasyTwo PointersLeetCode
Two lists may merge into one shared tail, like two roads joining. Return the first node they share (the same node object, not just the same value), or null if they never join.
Example: A = 4 → 1 → 8 → 4 → 5, B = 5 → 6 → 1 → 8 → 4 → 5, sharing the tail from node 8 → the node with value 8.
1Brute force — check every pair of nodes
For each node in A, walk all of B and see if any node in B is the very same node.
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
for (ListNode a = headA; a != null; a = a.next) {
for (ListNode b = headB; b != null; b = b.next) {
if (a == b) return a; // same node object, not just same value
}
}
return null;
}
Time O(m·n)Space O(1)
Two lists of 30,000 nodes means 900 million comparisons. (A HashSet of A's nodes gets O(m+n) time but costs O(m) memory.)
2Optimal — swap lists at the end so both walk the same distance
The lists have different lengths, so walking them side by side doesn't line up. Fix it with a trick: when pointer a finishes A, it jumps to the start of B, and b jumps to the start of A. Now each pointer walks A-only + B-only + shared, the same total, so they arrive at the join point together. If there is no join, they both reach null together.
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode a = headA, b = headB;
while (a != b) { // stop when they point at the same node (or both null)
a = (a == null) ? headB : a.next; // A finished? jump to B's start
b = (b == null) ? headA : b.next; // B finished? jump to A's start
}
return a; // the meeting node, or null if none
}
Time O(m+n)Space O(1)
Each pointer walks at most m + n nodes, and we only store two pointers. The loop always ends: in the worst case both become null at the same moment.
When two walks have different lengths, make them equal by having each one also walk the other's path. Same distance, same speed, same arrival time.
Comparing values instead of nodes. In the example both lists contain a 1, but they are different nodes; the real answer is 8. Always compare with a == b on the node references.
Pattern notes: Two Pointers · Open on LeetCode
LC 234Palindrome Linked ListEasyFast & Slow + ReversalLeetCode
Does the list read the same forwards and backwards? Return true or false.
Example: 1 → 2 → 2 → 1 → true. 1 → 2 → false.
1Brute force — copy into an array, check from both ends
A list can't walk backwards, but an array can. Copy the values into an ArrayList, then compare the first with the last, the second with the second-last, and so on.
boolean isPalindrome(ListNode head) {
List<Integer> vals = new ArrayList<>();
for (ListNode n = head; n != null; n = n.next) vals.add(n.val); // copy into an array
int i = 0, j = vals.size() - 1;
while (i < j) {
if (!vals.get(i).equals(vals.get(j))) return false; // equals, not == on Integer
i++; j--;
}
return true;
}
Time O(n)Space O(n)
Simple and a fine first answer, but LeetCode's follow-up asks for O(1) extra space.
2Optimal — find the middle, reverse the second half, compare
This combines two tricks you already know. Use slow / fast (LC 876) to reach the middle, then reverse (LC 206) the second half. Now the second half reads backwards, so you can walk both halves forward and compare node by node.
boolean isPalindrome(ListNode head) {
// 1. find the middle with slow / fast
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. reverse the second half (same code as LC 206)
ListNode prev = null, curr = slow;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
// 3. compare first half with reversed second half
ListNode left = head, right = prev;
while (right != null) {
if (left.val != right.val) return false;
left = left.next;
right = right.next;
}
return true;
}
Time O(n)Space O(1)
Three linear walks, still O(n) total, and only a handful of pointers. (In an interview, mention that this changes the input list; you could reverse the half back afterwards.)
Harder list problems are often small patterns glued together: find the middle + reverse + walk two pointers. Master the pieces and you can build the whole.
Loop on right != null, not left != null. The first half still links into the middle node, so it is the longer one; the reversed half is the one that ends cleanly at null. In the brute force, comparing two Integer objects with == fails for values above 127, so use .equals.
Pattern notes: Fast & Slow Pointers · Open on LeetCode
Binary Search
Binary search finds something in a sorted range by checking the middle and throwing away the half that can't hold the answer, like opening a dictionary in the middle instead of reading from page 1. Each check halves the range, so a billion items take only about 30 checks. The key pattern: ONE template (lo, hi, while (lo <= hi), mid, move lo or hi past mid), used either to find an exact match or, with an ans variable, to find the first or last value that passes a yes/no test.
LC 704Binary SearchEasyBinary SearchLeetCode
You get a sorted array of distinct numbers and a target. Return the index of the target, or -1 if it's not there. It must run in O(log n).
Example: nums = [-1, 0, 3, 5, 9, 12], target = 9 → 4. With target = 2 → -1.
1Brute force — check every number
Walk from left to right and stop when you see the target.
int search(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) return i;
}
return -1;
}
Time O(n)Space O(1)
This ignores that the array is sorted, and the problem explicitly demands O(log n).
2Optimal — the binary search template
Keep a range [lo, hi] where the answer could be. Look at the middle. Because the array is sorted, one comparison tells you which half to throw away: if nums[mid] is too small, everything to its left is too small too.
int search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1; // the answer lives in [lo, hi]
while (lo <= hi) { // range not empty yet
int mid = lo + (hi - lo) / 2; // middle, without overflow
if (nums[mid] == target) return mid; // found it
else if (nums[mid] < target) lo = mid + 1; // too small: throw away left half
else hi = mid - 1; // too big: throw away right half
}
return -1; // range became empty: not there
}
Time O(log n)Space O(1)
The range halves every round, so for n = 1,000,000 it takes at most about 20 rounds.
This is the template for the whole topic: lo = 0, hi = n - 1, while (lo <= hi), mid = lo + (hi - lo) / 2, then lo = mid + 1 or hi = mid - 1. Always +1 / -1, because mid has already been checked.
Writing while (lo < hi) with this template skips the last one-cell range, so [5] with target 5 wrongly returns -1. And lo = mid (no +1) can loop forever when lo and hi are neighbours.
Pattern notes: Binary Search · Open on LeetCode
LC 35Search Insert PositionEasyBinary SearchLeetCode
Sorted array of distinct numbers and a target. If the target is there, return its index. If not, return the index where it would go to keep the array sorted. O(log n).
Example: nums = [1, 3, 5, 6]: target 5 → 2, target 2 → 1, target 7 → 4.
1Brute force — find the first number that is big enough
Both cases have the same answer: the first index whose number is >= target. If there is none, the target goes at the very end.
int searchInsert(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] >= target) return i; // first spot that is big enough
}
return nums.length; // bigger than everything: goes at the end
}
Time O(n)Space O(1)
Correct, but linear. The array is sorted, so we can binary search for that "first big enough" spot.
2Optimal — same template, plus an ans variable
Ask a yes/no question at each index: "is nums[i] >= target?" The answers look like no no no yes yes, and we want the first yes. When mid says yes, write it down as the best answer so far, then keep looking to the left for an earlier yes.
int searchInsert(int[] nums, int target) {
int lo = 0, hi = nums.length - 1;
int ans = nums.length; // default: insert at the end
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] >= target) { // mid is a valid spot...
ans = mid; // ...remember it
hi = mid - 1; // ...but look for an earlier one
} else {
lo = mid + 1; // mid too small: go right
}
}
return ans;
}
Time O(log n)Space O(1)
Exactly the LC 704 loop. The only change: instead of returning on a match, we record ans and keep shrinking.
"Find the first position where something becomes true" → template + ans = mid; hi = mid - 1 when true. This is the most useful binary search variant of all.
Forgetting the default. If the target is bigger than every number, mid never says yes, so ans must start at nums.length, not 0 or -1.
Pattern notes: Binary Search · Open on LeetCode
LC 278First Bad VersionEasyBinary SearchLeetCode
Versions 1..n of a product were released. Once one version is bad, every later version is bad too. Using the given API isBadVersion(v), find the first bad version with as few calls as possible.
Example: n = 5, first bad is 4: isBadVersion(3) = false, isBadVersion(4) = true → 4.
1Brute force — test versions one by one
Ask the API about version 1, 2, 3, ... and return the first one that says bad.
int firstBadVersion(int n) {
for (int v = 1; v <= n; v++) {
if (isBadVersion(v)) return v; // first one that says "bad"
}
return n;
}
Time O(n)Space O(1)
n can be about 2 billion, so this can mean 2 billion API calls. Too slow (Time Limit Exceeded).
2Optimal — binary search for the first "bad"
The answers look like good good good bad bad: sorted, even though there is no array. That is all binary search needs. It's the same "first yes" question as LC 35, where "yes" now means isBadVersion(mid).
int firstBadVersion(int n) {
int lo = 1, hi = n;
int ans = n;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // NOT (lo + hi) / 2: that can overflow
if (isBadVersion(mid)) {
ans = mid; // mid is bad: a candidate
hi = mid - 1; // maybe an earlier one is bad too
} else {
lo = mid + 1; // mid is good: first bad is to the right
}
}
return ans;
}
Time O(log n)Space O(1)
For n = 2,126,753,390 this makes 31 API calls instead of about 2 billion.
Binary search doesn't need an array. It works on any range where a yes/no test flips only once (all no, then all yes). This is called "binary search on the answer".
(lo + hi) / 2 overflows when both are near 2 billion: the sum goes past Integer.MAX_VALUE and turns negative. This is the classic failing test on this problem. Always write lo + (hi - lo) / 2.
Pattern notes: Binary Search · Open on LeetCode
LC 69Sqrt(x)EasyBinary SearchLeetCode
Return the square root of x, rounded down to a whole number. You may not use Math.sqrt or pow. In other words: the biggest k with k × k <= x.
Example: x = 4 → 2. x = 8 → 2 (the real root is 2.83..., rounded down).
1Brute force — count up until the square is too big
Try 1, 2, 3, ... and stop just before the square passes x.
int mySqrt(int x) {
long i = 0;
while ((i + 1) * (i + 1) <= x) i++; // grow while the next square still fits
return (int) i;
}
Time O(√x)Space O(1)
For the biggest int that is about 46,000 steps. It passes, but binary search needs only about 31.
2Optimal — binary search for the last k that fits
Ask "does k × k <= x?" for k = 0, 1, 2, ... The answers are yes yes yes no no. We want the last yes, the mirror image of LC 35. So when mid fits, save it, then look to the right for a bigger one.
int mySqrt(int x) {
int lo = 0, hi = x;
int ans = 0;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if ((long) mid * mid <= x) { // long: mid * mid can overflow int
ans = mid; // mid fits: a candidate
lo = mid + 1; // try a bigger one
} else {
hi = mid - 1; // too big: go left
}
}
return ans;
}
Time O(log x)Space O(1)
Same template again. "First yes" saves and goes left; "last yes" saves and goes right. That's the only difference.
"Biggest value that still works" → template + ans = mid; lo = mid + 1 when it works. Many "find the max/min k such that..." problems are this exact shape.
mid * mid with int overflows once mid passes 46,340, turns negative, and looks "small enough". Cast to long before multiplying: (long) mid * mid. Test x = 2147483647 → 46340.
Pattern notes: Binary Search · Open on LeetCode
LC 367Valid Perfect SquareEasyBinary SearchLeetCode
Given a positive number num, return true if it is a perfect square (some whole number times itself), without using Math.sqrt.
Example: num = 16 → true (4 × 4). num = 14 → false.
1Brute force — try every k while k × k fits
Check 1 × 1, 2 × 2, 3 × 3, ... If one equals num, it is a square. Once the square passes num, stop.
boolean isPerfectSquare(int num) {
for (long i = 1; i * i <= num; i++) {
if (i * i == num) return true;
}
return false;
}
Time O(√n)Space O(1)
Fine for small numbers, but it walks one by one through a sorted range, which is exactly what binary search avoids.
2Optimal — exact-match binary search on k
The squares 1, 4, 9, 16, 25, ... are sorted, so this is LC 704 in disguise: search k in [1, num] for k × k == num. Too small, go right; too big, go left; equal, done.
boolean isPerfectSquare(int num) {
int lo = 1, hi = num;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
long sq = (long) mid * mid; // long, so it can't overflow
if (sq == num) return true; // exact hit
else if (sq < num) lo = mid + 1;
else hi = mid - 1;
}
return false;
}
Time O(log n)Space O(1)
At most about 31 rounds even for the largest int, and no floating-point rounding to worry about.
Before coding, decide which of the three shapes you need: exact match (return inside the loop), first yes (save, go left), or last yes (save, go right). The loop itself never changes.
Overflow again: mid * mid must be computed as long. With int, a huge mid can wrap around and accidentally equal num or look smaller than it.
Pattern notes: Binary Search · Open on LeetCode
Recursion, Bits & Math
Small problems about numbers that teach you how a computer thinks. You will learn recursion and memoization (a function that calls itself, plus a notebook so it never repeats work), bit tricks like n & (n - 1) that remove the lowest 1-bit, and peeling digits off a number with % 10 and / 10.
LC 509Fibonacci NumberEasyRecursion + MemoLeetCode
The Fibonacci numbers start 0, 1, and every next number is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, .... Given n, return the n-th one, F(n).
Example: n = 4 → 3, because F(4) = F(3) + F(2) = 2 + 1 = 3.
1Brute force — plain recursion, straight from the definition
The definition already says how to compute it: F(n) = F(n-1) + F(n-2), and the answer is known for n = 0 and n = 1 (the base cases, where the recursion stops).
int fib(int n) {
if (n <= 1) return n; // base cases: F(0) = 0, F(1) = 1
return fib(n - 1) + fib(n - 2); // two smaller calls
}
Time O(2ⁿ)Space O(n)
Every call makes two more calls, so the number of calls roughly doubles each level. fib(40) makes over 300 million calls, and most of them compute things that were already computed.
2Optimal — memoization: remember every answer once
Look at the tree below: f(3) and f(2) are solved again and again. Keep a notebook (an array memo). Before computing F(k), check the notebook; after computing it, write it down. Now each value from 0 to n is computed only once.
int fib(int n) {
int[] memo = new int[n + 1];
Arrays.fill(memo, -1); // -1 means "not computed yet"
return solve(n, memo);
}
int solve(int n, int[] memo) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n]; // already in the notebook
memo[n] = solve(n - 1, memo) + solve(n - 2, memo);
return memo[n]; // write it down, then return
}
Time O(n)Space O(n)
Each F(k) is computed once and then reused, so the work drops from exponential to a straight line. (You can even drop the array and keep only the last two values: see LC 70 below.)
If a recursion tree has the same call appearing many times, add a memo. That single change is the doorway into Dynamic Programming.
Forget the base case (or write only n == 1) and fib(0) recurses into negative numbers forever, ending in a StackOverflowError. Always ask "where does this stop?" first.
Pattern notes: Recursion & the Call Stack · Open on LeetCode
LC 231Power of TwoEasyBit ManipulationLeetCode
Given an integer n, return true if it is a power of two (1, 2, 4, 8, 16, ...), otherwise false.
Example: n = 16 → true (2⁴). n = 3 → false.
1Brute force — keep dividing by 2
A power of two is 2 × 2 × ... × 2. So divide by 2 while the number is even. If you end up at exactly 1, it was a power of two.
boolean isPowerOfTwo(int n) {
if (n <= 0) return false; // 0 and negatives are never powers of two
while (n % 2 == 0) n /= 2; // strip every factor of 2
return n == 1;
}
Time O(log n)Space O(1)
This is already fast (at most 31 divisions for an int). But there is a one-line answer that interviewers love, and it teaches a bit trick you will reuse.
2Optimal — one bit trick
In binary, a power of two has exactly one 1-bit: 8 is 1000. Subtracting 1 flips that bit to 0 and every bit below it to 1: 7 is 0111. So n & (n - 1) is 0 only when n has a single 1-bit.
boolean isPowerOfTwo(int n) {
// n > 0 rules out 0 and negatives; the AND checks "exactly one 1-bit"
return n > 0 && (n & (n - 1)) == 0;
}
Time O(1)Space O(1)
One subtraction and one AND, no loop at all.
n & (n - 1) removes the lowest 1-bit of n. "Is it 0 now?" means "was there only one 1-bit?". You will use the same trick in LC 191 right below.
Do not skip n > 0. For n = 0, 0 & -1 is 0 and you would wrongly say true. For Integer.MIN_VALUE (only the sign bit set) the AND is also 0. Also note the brackets: == binds tighter than & in Java.
Pattern notes: Bit Manipulation · Open on LeetCode
LC 191Number of 1 BitsEasyBit ManipulationLeetCode
Given a number, count how many 1s appear in its binary form (this count is called the "Hamming weight").
Example: n = 11 → 3, because 11 in binary is 1011.
1Brute force — check all 32 bits one by one
An int has 32 bits. Shift each bit down to the last position and look at it with & 1.
int hammingWeight(int n) {
int count = 0;
for (int i = 0; i < 32; i++) {
if (((n >>> i) & 1) == 1) count++; // is bit i a 1?
}
return count;
}
Time O(32)Space O(1)
Always 32 steps, even for n = 128 which has a single 1-bit. We waste time looking at zeros.
2Optimal — jump straight from one 1-bit to the next
From LC 231: n &= n - 1 deletes the lowest 1-bit. Keep deleting until n is 0 and count the deletions. The loop runs once per 1-bit, and never visits a 0.
int hammingWeight(int n) {
int count = 0;
while (n != 0) {
n &= n - 1; // remove the lowest 1-bit
count++; // one 1-bit gone
}
return count;
}
Time O(number of 1s)Space O(1)
At most 32 turns, and far fewer when the number has few 1s. (Java also has Integer.bitCount(n), but interviewers want to see the trick.)
To loop over only the 1-bits of a number, repeat n &= n - 1. It is called Brian Kernighan's trick.
In the brute force, use >>> (unsigned shift), not >>. If you shift n itself with >> in a while (n != 0) loop, a negative number keeps copying its sign bit and the loop never ends.
Pattern notes: Bit Manipulation · Open on LeetCode
LC 338Counting BitsEasyBits + DPLeetCode
Given n, return an array ans of length n + 1 where ans[i] is the number of 1-bits in i, for every i from 0 to n.
Example: n = 5 → [0, 1, 1, 2, 1, 2] (0 = 0, 1 = 1, 2 = 10, 3 = 11, 4 = 100, 5 = 101).
1Brute force — count the bits of each number separately
For every i, peel off its bits one at a time and count the 1s, exactly like LC 191.
int[] countBits(int n) {
int[] ans = new int[n + 1];
for (int i = 0; i <= n; i++) {
int x = i, count = 0;
while (x != 0) {
count += x & 1; // last bit
x >>= 1; // drop it
}
ans[i] = count;
}
return ans;
}
Time O(n log n)Space O(1) extra
Each number costs about log n steps. The problem's follow-up asks for a single pass in O(n).
2Optimal — reuse the answer for i / 2
i >> 1 (i divided by 2) is just i with its last bit chopped off. That smaller number is already in the array. So ans[i] = ans[i >> 1] + (i & 1): the bits of the front part, plus 1 if the last bit is 1.
int[] countBits(int n) {
int[] ans = new int[n + 1]; // ans[0] = 0 already
for (int i = 1; i <= n; i++) {
// bits of i = bits of (i without last bit) + the last bit
ans[i] = ans[i >> 1] + (i & 1);
}
return ans;
}
Time O(n)Space O(1) extra
Because i >> 1 is always smaller than i, its answer is ready when we need it. This is a tiny Dynamic Programming problem.
When you need an answer for every number from 0 to n, ask: "can I build the answer for i from a smaller one I already have?" That question is the heart of DP.
The array has size n + 1, not n, because 0 and n are both included. Also bracket (i & 1): in Java + binds tighter than &, so ans[i >> 1] + i & 1 means something else.
Pattern notes: Bit Manipulation · Open on LeetCode
LC 9Palindrome NumberEasyMath / DigitsLeetCode
Return true if an integer reads the same forwards and backwards. Negative numbers never do, because of the minus sign.
Example: 121 → true. -121 → false (backwards it is 121-). 10 → false (backwards 01).
1Brute force — turn it into a String
Convert the number to text, then compare the first and last characters, moving inward with two pointers.
boolean isPalindrome(int x) {
String s = String.valueOf(x);
int l = 0, r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r)) return false;
l++; r--;
}
return true;
}
Time O(d)Space O(d)
Here d is the number of digits. It works, but the follow-up asks: "could you solve it without converting the integer to a string?"
2Optimal — reverse only the second half of the digits
x % 10 gives the last digit and x / 10 removes it. Move digits from the end of x onto a new number rev. Stop when rev is at least as big as what is left of x: you have reversed half the digits. Now just compare the two halves. Reversing only half also means rev can never overflow.
boolean isPalindrome(int x) {
// negatives, and numbers ending in 0 (except 0 itself), can't be palindromes
if (x < 0 || (x % 10 == 0 && x != 0)) return false;
int rev = 0;
while (x > rev) {
rev = rev * 10 + x % 10; // move the last digit of x onto rev
x /= 10; // and remove it from x
}
// even length: x == rev; odd length: the middle digit sits at the end of rev
return x == rev || x == rev / 10;
}
Time O(d)Space O(1)
We look at only half the digits and store just two ints, with no String and no overflow risk.
% 10 reads the last digit, / 10 removes it, and rev * 10 + digit appends a digit. These three moves solve most "digits of a number" problems.
Numbers that end in 0, like 10, break the half-reverse: the reversed half would start with 0, and a leading 0 simply vanishes, so 10 would wrongly return true. That is why the first line returns false for them (but keeps 0, which is a palindrome).
Pattern notes: Math & Number Theory · Open on LeetCode
Trees
A binary tree is a family tree of nodes: each node holds a value and points to at most two children, left and right. Almost every tree problem is solved the same way: ask each child for an answer, then combine the answers at the parent (recursive DFS). The problems below teach the traversal orders (preorder, inorder), "return something up to the parent" (depth, diameter), walking two trees together (same / symmetric), and the BST rule (smaller left, bigger right) that lets you skip half the tree at every step.
LC 94Binary Tree Inorder TraversalEasyTree DFSLeetCode
Visit every node of a binary tree in inorder: first the whole left side, then the node itself, then the whole right side. Return the values in that order.
Example: root = [1, null, 2, 3] (1 has a right child 2, and 2 has a left child 3) → [1, 3, 2].
1First way — recursion
The definition is already recursive: "left side, me, right side". So write a helper that does exactly that and let Java's call stack remember where to come back to.
List<Integer> inorderTraversal(TreeNode root) {
List<Integer> out = new ArrayList<>();
walk(root, out);
return out;
}
void walk(TreeNode node, List<Integer> out) {
if (node == null) return; // empty spot: nothing to do
walk(node.left, out); // 1. whole left side first
out.add(node.val); // 2. then me
walk(node.right, out); // 3. then whole right side
}
Time O(n)Space O(h)
This is already O(n) and perfectly fine. LeetCode's follow-up asks: "can you do it iteratively?" On a very deep tree (h close to n) recursion can overflow Java's stack, and interviewers like to see that you know what the recursion is doing behind the scenes.
2Iterative — your own stack
Recursion secretly uses a stack. Do it by hand: slide left as far as you can, pushing every node. When you hit null, pop a node, visit it, then jump to its right child and repeat. The stack holds the "parents I still have to come back to", exactly like the call stack did.
List<Integer> inorderTraversal(TreeNode root) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) { // slide left as far as possible,
stack.push(cur); // saving each node for later
cur = cur.left;
}
cur = stack.pop(); // leftmost unvisited node
out.add(cur.val); // visit it
cur = cur.right; // then handle its right side
}
return out;
}
Time O(n)Space O(h)
Same big-O as recursion (be honest about that in an interview). The win is control: no risk of a StackOverflowError, and the same loop shape solves "kth smallest in a BST" and "BST iterator".
Inorder = left, me, right. On a BST, inorder gives the values in sorted order, which is why this traversal matters so much.
The outer loop needs both conditions: cur != null || !stack.isEmpty(). With only !stack.isEmpty() you stop too early right after popping the root, before its right side is visited.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 144Binary Tree Preorder TraversalEasyTree DFSLeetCode
Visit every node in preorder: the node itself first, then its whole left side, then its whole right side. Return the values in that order.
Example: root = [1, null, 2, 3] → [1, 2, 3].
1First way — recursion
Same helper as inorder, only the "visit me" line moves to the top.
List<Integer> preorderTraversal(TreeNode root) {
List<Integer> out = new ArrayList<>();
walk(root, out);
return out;
}
void walk(TreeNode node, List<Integer> out) {
if (node == null) return;
out.add(node.val); // 1. me first
walk(node.left, out); // 2. then left side
walk(node.right, out); // 3. then right side
}
Time O(n)Space O(h)
Already O(n). As with inorder, the follow-up is "do it without recursion", which shows you understand the stack underneath.
2Iterative — push right, then left
Pop a node and visit it straight away (that is "me first"). Then push its children. A stack is last-in-first-out, so push the right child first and the left child second: the left one comes out next, just like preorder wants.
List<Integer> preorderTraversal(TreeNode root) {
List<Integer> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
out.add(node.val); // visit on pop
if (node.right != null) stack.push(node.right); // push RIGHT first...
if (node.left != null) stack.push(node.left); // ...so LEFT pops first
}
return out;
}
Time O(n)Space O(h)
Same O(n) as recursion, with an explicit stack instead of the call stack. Swap the stack for a queue and you get level-order (BFS) instead.
Preorder = me, left, right. It is the order you use to copy or serialize a tree, because each parent is written before its children.
Pushing left before right gives [1, 3, 2]-style wrong orders: the stack reverses things. Also guard root == null before pushing, or you pop a null and crash on node.val.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 104Maximum Depth of Binary TreeEasyTree DFSLeetCode
How many nodes are on the longest path from the root down to a leaf? That count is the tree's maximum depth.
Example: root = [3, 9, 20, null, null, 15, 7] → 3 (the path 3 → 20 → 15 has three nodes).
1First way — count the levels (BFS)
Walk the tree one floor at a time with a queue, like counting floors in a building. Each time you finish a whole level, add one.
int maxDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int levels = 0;
while (!queue.isEmpty()) {
int size = queue.size(); // nodes on this level
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
}
levels++; // finished one whole level
}
return levels;
}
Time O(n)Space O(w)
Correct and O(n), where w is the widest level. It just takes a lot of code for a simple question. Recursion says the same thing in three lines.
2Cleaner — ask the children (recursive DFS)
Every node asks its two children "how deep are you?" and answers its own parent with 1 + the bigger of the two answers. An empty spot answers 0. The answers bubble up from the leaves to the root.
int maxDepth(TreeNode root) {
if (root == null) return 0; // empty tree has depth 0
int left = maxDepth(root.left); // ask the left child
int right = maxDepth(root.right); // ask the right child
return 1 + Math.max(left, right); // me + the deeper side
}
Time O(n)Space O(h)
Both ways are O(n) time: every node must be seen once. The recursive version is shorter and uses O(h) stack instead of O(w) queue memory.
The core tree template: handle null, ask left, ask right, combine. Depth, size, sum, balanced check and diameter are all this same shape.
The base case returns 0 for null, not 1. Returning 1 for null makes every answer one too big. And an empty tree (root = []) must return 0.
Pattern notes: Binary Trees · Open on LeetCode
LC 226Invert Binary TreeEasyTree DFSLeetCode
Turn the tree into its mirror image: at every node, the left child and the right child trade places. Return the root.
Example: root = [4, 2, 7, 1, 3, 6, 9] → [4, 7, 2, 9, 6, 3, 1].
1First way — build a mirrored copy
Make a brand new tree: each new node gets the mirror of the old right side as its left, and the mirror of the old left side as its right.
TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode copy = new TreeNode(root.val);
copy.left = invertTree(root.right); // new left = mirrored old right
copy.right = invertTree(root.left); // new right = mirrored old left
return copy;
}
Time O(n)Space O(n)
It works, but it allocates n new nodes. There's no need: we can just rewire the existing tree.
2Optimal — swap in place
At each node, swap the two child pointers, then do the same for each child. Like flipping a photo: flip the big picture, and every small part inside is flipped too.
TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode temp = root.left; // swap my two children
root.left = root.right;
root.right = temp;
invertTree(root.left); // then fix each subtree the same way
invertTree(root.right);
return root;
}
Time O(n)Space O(h)
Still O(n) time (every node must be swapped), but no new nodes: only the recursion stack, O(h).
A tree operation that applies "to every node" = do it here, then recurse on both children. Swapping with a temp variable is the same as swapping two array cells.
Don't write root.left = invertTree(root.right); root.right = invertTree(root.left);. The second line reads root.left after you already overwrote it. Save one side in a temp first.
Pattern notes: Binary Trees · Open on LeetCode
LC 100Same TreeEasyTree DFSLeetCode
You get two trees, p and q. Are they exactly the same, meaning the same shape and the same value in every position?
Example: p = [1, 2, 3], q = [1, 2, 3] → true. p = [1, 2], q = [1, null, 2] → false (2 is a left child in one and a right child in the other).
1Brute force — turn both trees into strings
Write each tree out as text in preorder, marking empty spots with #, then compare the two strings.
boolean isSameTree(TreeNode p, TreeNode q) {
return serialize(p).equals(serialize(q));
}
String serialize(TreeNode node) {
if (node == null) return "#"; // mark empty spots!
return node.val + "," + serialize(node.left) + "," + serialize(node.right);
}
Time O(n)Space O(n)
It builds two whole strings (and lots of temporary ones along the way) even when the roots already differ. We can compare directly and stop at the first difference.
2Optimal — walk both trees together
Put one finger on each tree and move them in lockstep. At each pair of spots: both empty is fine, one empty is a mismatch, different values is a mismatch. Otherwise, check the left pair and the right pair the same way.
boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) return true; // both empty: same
if (p == null || q == null) return false; // only one empty: different
if (p.val != q.val) return false; // different values
return isSameTree(p.left, q.left) // left sides must match
&& isSameTree(p.right, q.right); // and right sides too
}
Time O(n)Space O(h)
Worst case still visits every node (when the trees are equal), but it uses only the recursion stack and quits early on the first mismatch.
"Compare two trees" = recurse on pairs of nodes. The three base cases (both null, one null, values differ) come first, in that order.
Check the null cases before reading p.val, or you get a NullPointerException. In the string version, forgetting the # markers makes [1, 2] and [1, null, 2] look identical.
Pattern notes: Binary Trees · Open on LeetCode
LC 101Symmetric TreeEasyTree DFSLeetCode
Is the tree a mirror of itself around a vertical line through the root? Fold it down the middle: every node on the left should land on an equal node on the right.
Example: root = [1, 2, 2, 3, 4, 4, 3] → true. root = [1, 2, 2, null, 3, null, 3] → false.
1Brute force — every level must read the same both ways
Go level by level with a queue, write down each level's values (with null for gaps), and check each level is a palindrome.
boolean isSymmetric(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node == null) { level.add(null); continue; } // keep gaps!
level.add(node.val);
queue.add(node.left);
queue.add(node.right);
}
for (int i = 0, j = level.size() - 1; i < j; i++, j--) {
if (!Objects.equals(level.get(i), level.get(j))) return false;
}
}
return true;
}
Time O(n)Space O(n)
Correct, but it stores whole levels (including null placeholders) and always finishes a level before checking. The recursive mirror check is shorter and stops at the first mismatch.
2Optimal — compare the two halves as mirrors
It's "Same Tree" with a twist: compare the left subtree with the right subtree, but outside with outside, inside with inside. So a.left pairs with b.right, and a.right pairs with b.left.
boolean isSymmetric(TreeNode root) {
return isMirror(root.left, root.right);
}
boolean isMirror(TreeNode a, TreeNode b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
return a.val == b.val
&& isMirror(a.left, b.right) // outside pair
&& isMirror(a.right, b.left); // inside pair
}
Time O(n)Space O(h)
Each node is compared once with its mirror partner. Memory is just the recursion depth.
Symmetric = "Same Tree" between the two halves, with the children crossed. Learn the pair-recursion shape once and reuse it.
Checking only that the left child's value equals the right child's value at each node is not enough. [1, 2, 2, null, 3, null, 3] passes that test but is not a mirror: you must compare across the two halves.
Pattern notes: Binary Trees · Open on LeetCode
LC 112Path SumEasyTree DFSLeetCode
Is there a path from the root down to a leaf (a node with no children) whose values add up to targetSum?
Example: root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, null, 1], targetSum = 22 → true, because 5 + 4 + 11 + 2 = 22.
1Brute force — collect every path sum
Walk every root-to-leaf path, write each total into a list, and at the end ask the list whether it contains the target.
boolean hasPathSum(TreeNode root, int targetSum) {
List<Integer> sums = new ArrayList<>();
collect(root, 0, sums);
return sums.contains(targetSum);
}
void collect(TreeNode node, int soFar, List<Integer> sums) {
if (node == null) return;
soFar += node.val;
if (node.left == null && node.right == null) sums.add(soFar); // a leaf: path done
collect(node.left, soFar, sums);
collect(node.right, soFar, sums);
}
Time O(n)Space O(n)
It stores one number per leaf (up to about n/2) and always walks the entire tree, even if the very first path already hits the target.
2Optimal — subtract as you go down
Think of the target as a budget. Each node you step on pays its value out of the budget. If you reach a leaf with exactly 0 left, you found a path. || stops as soon as one side says yes.
boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false; // no path here
int remaining = targetSum - root.val; // pay for this node
if (root.left == null && root.right == null) { // leaf: path ends here
return remaining == 0;
}
return hasPathSum(root.left, remaining) // try left...
|| hasPathSum(root.right, remaining); // ...or right (stops at first true)
}
Time O(n)Space O(h)
Worst case it still visits every node, but it keeps no list and returns the moment a good path is found.
Pass information down the tree as a parameter (here: the remaining budget). Pass answers up with the return value.
The check must happen at a leaf, not at null. For root = [1, 2], targetSum = 1 the answer is false: node 1 has a child, so it is not a leaf, and the empty right spot does not count as a path end.
Pattern notes: Binary Trees · Open on LeetCode
LC 543Diameter of Binary TreeEasyTree DFSLeetCode
The diameter is the longest path between any two nodes, counted in edges (the lines between nodes). The path may or may not pass through the root.
Example: root = [1, 2, 3, 4, 5] → 3. The path 4 → 2 → 1 → 3 has three edges.
1Brute force — at every node, measure both heights
Any longest path "bends" at some top node. So for every node, compute the height of its left side plus the height of its right side, and keep the largest. Height is computed from scratch each time.
int diameterOfBinaryTree(TreeNode root) {
if (root == null) return 0;
int throughMe = height(root.left) + height(root.right); // path bending at root
int best = Math.max(diameterOfBinaryTree(root.left),
diameterOfBinaryTree(root.right)); // or entirely inside a subtree
return Math.max(throughMe, best);
}
int height(TreeNode node) { // counts nodes on the longest way down
if (node == null) return 0;
return 1 + Math.max(height(node.left), height(node.right));
}
Time O(n²)Space O(h)
Each call to height re-walks a whole subtree, and we call it at every node. On a long skinny tree that's about n²/2 steps: the same heights get recomputed over and over.
2Optimal — one pass: return height, record diameter
The height function already knows the left and right heights of every node. So while computing heights, also check left + right at each node and remember the best in a field. The function returns one thing (height) but records another (diameter).
private int best = 0; // longest path found so far (in edges)
int diameterOfBinaryTree(TreeNode root) {
height(root);
return best;
}
int height(TreeNode node) {
if (node == null) return 0;
int left = height(node.left); // how far down on the left
int right = height(node.right); // how far down on the right
best = Math.max(best, left + right); // path that bends at this node
return 1 + Math.max(left, right); // tell my parent my height
}
Time O(n)Space O(h)
Every node is visited exactly once, and each height is computed once and reused by the parent.
When a tree answer needs info from both children, return what the parent needs (height) and track the global answer on the side (diameter). This "return one thing, record another" trick also solves Balanced Tree and Max Path Sum.
The longest path does not have to pass through the root, so "left height of root + right height of root" alone is wrong. Also count edges, not nodes: left + right (heights in nodes) already equals the edge count of the bent path.
Pattern notes: Binary Trees · Open on LeetCode
LC 108Convert Sorted Array to Binary Search TreeEasyDivide & ConquerLeetCode
You get a sorted array. Build a height-balanced BST from it: smaller values to the left, bigger to the right, and the two sides of every node differ in height by at most 1.
Example: nums = [-10, -3, 0, 5, 9] → [0, -3, 9, -10, null, 5]. [0, -10, 5, null, -3, null, 9] is also accepted: any balanced BST is fine.
1First way — split the array into copies
The middle value must be the root (half the values are smaller, half bigger). Then do the same for the left half and the right half, making a new small array for each.
TreeNode sortedArrayToBST(int[] nums) {
if (nums.length == 0) return null;
int mid = nums.length / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = sortedArrayToBST(Arrays.copyOfRange(nums, 0, mid)); // new array
root.right = sortedArrayToBST(Arrays.copyOfRange(nums, mid + 1, nums.length)); // new array
return root;
}
Time O(n log n)Space O(n log n)
The idea is right, but every level of recursion copies about n numbers in total, and there are log n levels. Copying is wasted work: the numbers are already sitting in the original array.
2Optimal — same idea, with index bounds
Instead of copying halves, pass two indexes, lo and hi, that say which slice of the original array you are working on. Middle of the slice becomes the node, left slice builds its left subtree, right slice its right subtree.
TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
TreeNode build(int[] nums, int lo, int hi) {
if (lo > hi) return null; // empty range: no node
int mid = lo + (hi - lo) / 2; // middle = root of this range
TreeNode root = new TreeNode(nums[mid]);
root.left = build(nums, lo, mid - 1); // left half builds left subtree
root.right = build(nums, mid + 1, hi); // right half builds right subtree
return root;
}
Time O(n)Space O(log n)
Each value becomes exactly one node. Because we always split in the middle, the recursion is only log n deep.
Middle of a sorted range = balanced root. And whenever you are tempted to copy a sub-array into recursion, pass lo and hi indexes instead.
Inserting the values one by one in sorted order into a BST gives a straight line (every new value is bigger, so it always goes right). That is a valid BST but not balanced, so it is wrong here.
Pattern notes: Binary Search Trees · Open on LeetCode
LC 700Search in a Binary Search TreeEasyBSTLeetCode
Find the node with value val in a BST and return it (its whole subtree comes with it). If it isn't there, return null.
Example: root = [4, 2, 7, 1, 3], val = 2 → [2, 1, 3]. With val = 5 → [] (null).
1Brute force — search everywhere
Treat it like any binary tree: check this node, then search the left side, and if that fails search the right side.
TreeNode searchBST(TreeNode root, int val) {
if (root == null) return null;
if (root.val == val) return root;
TreeNode found = searchBST(root.left, val); // look left...
if (found != null) return found;
return searchBST(root.right, val); // ...and right, no matter what
}
Time O(n)Space O(h)
It ignores the one thing that makes a BST special: its ordering. It can end up looking at every node.
2Optimal — follow the BST rule down one path
In a BST, everything left of a node is smaller and everything right is bigger. So at each node go left if val is smaller, right if it's bigger. It's binary search on a tree: one comparison throws away a whole side.
TreeNode searchBST(TreeNode root, int val) {
TreeNode cur = root;
while (cur != null && cur.val != val) {
cur = (val < cur.val) ? cur.left : cur.right; // throw away the wrong half
}
return cur; // the node, or null if we fell off the tree
}
Time O(h)Space O(1)
We follow a single path from the root, so the cost is the tree's height: about log n for a balanced BST. The loop needs no stack at all.
BST problems = compare, then go one way. Search, insert, floor/ceiling and LCA in a BST all walk a single root-to-leaf path in O(h).
O(h) is only log n if the tree is balanced. A BST built from sorted inserts is a straight line with h = n, so the worst case is still O(n).
Pattern notes: Binary Search Trees · Open on LeetCode
LC 235Lowest Common Ancestor of a Binary Search TreeMediumBSTLeetCode
Given two nodes p and q in a BST, find their lowest common ancestor: the deepest node that has both of them below it (a node counts as its own ancestor).
Example: root = [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5]. With p = 2, q = 8 → 6. With p = 2, q = 4 → 2 (2 is an ancestor of 4, and of itself).
1Brute force — the general binary-tree method
Ignore the BST order. Search both sides of every node: if p is found on one side and q on the other, this node is where they split, so it's the answer.
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) return root; // p and q on different sides
return left != null ? left : right; // pass up whatever was found
}
Time O(n)Space O(h)
This works for any binary tree (it's LC 236), but it may visit every node. The BST ordering tells us which way to go without looking.
2Optimal — walk down until p and q split
Start at the root. If p and q are both smaller, they both live on the left, so go left. If both bigger, go right. Otherwise they are on different sides (or one of them is this node): the first node where they split is the answer.
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode cur = root;
while (cur != null) {
if (p.val < cur.val && q.val < cur.val) {
cur = cur.left; // both smaller: answer is on the left
} else if (p.val > cur.val && q.val > cur.val) {
cur = cur.right; // both bigger: answer is on the right
} else {
return cur; // they split here (or one IS cur)
}
}
return null;
}
Time O(h)Space O(1)
One path from the root, no recursion: log n steps on a balanced BST.
In a BST, the LCA is the first node whose value is between p and q (inclusive). Same-side means keep going; different sides means stop.
Use strict < and >. If p equals the current node (like p = 2, q = 4 at node 2), neither "both smaller" nor "both bigger" is true, so we correctly return 2. Using <= would walk past it.
Pattern notes: Binary Search Trees · Open on LeetCode
Heaps
A heap (Java's PriorityQueue) is a box that always hands you the smallest item first (or the largest, with Collections.reverseOrder()), and adding or removing costs only O(log n). Use it whenever you keep asking "what is the biggest / smallest right now?" while the data changes. The problems below teach the max-heap simulation and the famous "min-heap of size k" trick for the k-th largest / top K.
LC 1046Last Stone WeightEasyHeapLeetCode
Each turn, take the two heaviest stones and smash them. If they weigh the same, both disappear. If not, the smaller one disappears and the bigger one shrinks by the smaller's weight. When at most one stone is left, return its weight (or 0 if none).
Example: stones = [2, 7, 4, 1, 8, 1] → 1.
1Brute force — sort before every smash
Keep the stones in a list. Each round, sort it, take the two at the end, and put the leftover back.
int lastStoneWeight(int[] stones) {
List<Integer> list = new ArrayList<>();
for (int s : stones) list.add(s);
while (list.size() > 1) {
Collections.sort(list); // sort EVERY round
int y = list.remove(list.size() - 1); // heaviest
int x = list.remove(list.size() - 1); // second heaviest
if (y != x) list.add(y - x);
}
return list.isEmpty() ? 0 : list.get(0);
}
Time O(n² log n)Space O(n)
Up to n rounds, each with a full sort. We re-sort the whole list just to find the top two, even though only one number changed.
2Optimal — a max-heap
A max-heap always keeps the heaviest stone on top. Poll twice to get the two heaviest, add the leftover back. Each poll or add costs only O(log n), with no full re-sort.
int lastStoneWeight(int[] stones) {
PriorityQueue<Integer> heap = new PriorityQueue<>(Collections.reverseOrder()); // max-heap
for (int s : stones) heap.add(s);
while (heap.size() > 1) {
int y = heap.poll(); // heaviest
int x = heap.poll(); // second heaviest
if (y != x) heap.add(y - x); // leftover piece goes back in
}
return heap.isEmpty() ? 0 : heap.peek();
}
Time O(n log n)Space O(n)
At most n rounds, each doing a few O(log n) heap operations.
"Repeatedly take the biggest (or smallest), change it, put it back" = simulate with a heap. Java's PriorityQueue is a min-heap by default; pass Collections.reverseOrder() for a max-heap.
Forgetting reverseOrder() gives you the two lightest stones. And the heap can end empty ([3, 3] → 0), so don't call peek() without checking.
Pattern notes: Heaps & Priority Queues · Open on LeetCode
LC 703Kth Largest Element in a StreamEasyHeapLeetCode
Build a class that is given k and some starting numbers. Numbers keep arriving one at a time through add(val), and after each one you must return the k-th largest number seen so far.
Example: k = 3, nums = [4, 5, 8, 2]. Then add(3) → 4, add(5) → 5, add(10) → 5, add(9) → 8, add(4) → 8.
1Brute force — keep everything and sort
Store every number. On each add, sort the list and read the k-th from the end.
class KthLargest {
private final int k;
private final List<Integer> all = new ArrayList<>();
public KthLargest(int k, int[] nums) {
this.k = k;
for (int n : nums) all.add(n);
}
public int add(int val) {
all.add(val);
Collections.sort(all); // re-sort everything each time
return all.get(all.size() - k); // k-th from the end
}
}
Time O(n log n) per addSpace O(n)
The list keeps growing forever, and each add re-sorts all of it. But we only ever care about the top k numbers.
2Optimal — a min-heap that holds only the top k
Keep a "top k club" in a min-heap. The smallest member of the club sits on top, and the smallest of the top k IS the k-th largest. When a new number arrives, add it; if the club now has k + 1 members, kick out the smallest.
class KthLargest {
private final int k;
private final PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap
public KthLargest(int k, int[] nums) {
this.k = k;
for (int n : nums) add(n);
}
public int add(int val) {
heap.add(val);
if (heap.size() > k) heap.poll(); // kick out the smallest
return heap.peek(); // smallest of the top k = k-th largest
}
}
Time O(log k) per addSpace O(k)
The heap never grows past k + 1, so each add is O(log k) no matter how many numbers have streamed in.
k-th largest → min-heap of size k (top = answer). k-th smallest → max-heap of size k. The heap type is the opposite of the word in the question.
The starting array can have fewer than k numbers (even zero). Reusing add() in the constructor handles it: the heap just stays smaller until enough numbers arrive, and LeetCode guarantees there are at least k by the time you are asked.
Pattern notes: Heaps & Priority Queues · Open on LeetCode
LC 215Kth Largest Element in an ArrayMediumTop K / HeapLeetCode
Return the k-th largest number in the array. It means k-th in sorted order, so duplicates count separately (not the k-th distinct value).
Example: nums = [3, 2, 1, 5, 6, 4], k = 2 → 5. And [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4 → 4.
1Brute force — sort the whole array
Sort ascending; the k-th largest is k places from the end.
int findKthLargest(int[] nums, int k) {
Arrays.sort(nums); // smallest ... largest
return nums[nums.length - k]; // k-th from the end
}
Time O(n log n)Space O(1)*
Short and honestly fine in many interviews, but it puts all n numbers in order when we only need the top k. (*Java's sort for int[] uses O(log n) stack.)
2Better — min-heap of size k
Same "top k club" as LC 703. Walk the array; push each number into a min-heap, and whenever the heap holds more than k numbers, poll the smallest. At the end the heap holds the k biggest, and its top is the k-th largest.
int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap
for (int n : nums) {
heap.add(n);
if (heap.size() > k) heap.poll(); // drop the smallest: it can't be in the top k
}
return heap.peek(); // weakest of the top k = k-th largest
}
Time O(n log k)Space O(k)
Each of the n numbers costs O(log k) instead of O(log n). When k is small (top 10 of a million) that's a big win. Follow-up: Quickselect (partition like quicksort, but only recurse into one side) gets average O(n).
"Top K / k-th largest" → min-heap capped at size k. Push, and if it's too big, pop. This one template solves Top K Frequent, K Closest Points and more.
Don't remove duplicates first (e.g. with a Set). For [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4 the answer is 4 because both 5s count. Also, a max-heap of all n numbers polled k times works, but costs O(n) memory.
Pattern notes: Top K Elements · Open on LeetCode
Graphs
A graph is a set of nodes (people, cities, grid cells) joined by edges (friendships, roads, "is next to"). A grid is a graph too: each cell is a node connected to its 4 neighbours. The key patterns here are DFS / BFS to explore everything reachable, marking cells as visited so you never loop forever, building an adjacency list, and counting in-degree / out-degree.
LC 733Flood FillEasyGrid DFSLeetCode
This is the paint-bucket tool. You get a grid of colours, a starting cell (sr, sc) and a new color. Repaint the starting cell and every cell of the same original colour that is connected to it up, down, left or right (not diagonally).
Example: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2 → [[2,2,2],[2,2,0],[2,0,1]].
1Brute force — sweep the whole grid again and again
Mark the start cell as "in the region". Then scan every cell: if it has the original colour and touches a marked cell, mark it too. Repeat full scans until one scan adds nothing. Finally, paint every marked cell.
int[][] floodFill(int[][] image, int sr, int sc, int color) {
int m = image.length, n = image[0].length, start = image[sr][sc];
boolean[][] inRegion = new boolean[m][n];
inRegion[sr][sc] = true;
boolean changed = true;
while (changed) { // one full sweep per loop
changed = false;
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (inRegion[r][c] || image[r][c] != start) continue;
boolean touches = (r > 0 && inRegion[r-1][c]) || (r < m-1 && inRegion[r+1][c])
|| (c > 0 && inRegion[r][c-1]) || (c < n-1 && inRegion[r][c+1]);
if (touches) { inRegion[r][c] = true; changed = true; }
}
}
}
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (inRegion[r][c]) image[r][c] = color;
return image;
}
Time O((m·n)²)Space O(m·n)
A long snake-shaped region may grow by only one cell per sweep, so we can end up scanning the whole grid once for every cell.
2Optimal — DFS: spread from the start like paint
Only visit cells that can actually be reached. From a cell, paint it and then call the same function on its 4 neighbours. A neighbour that is outside the grid or has a different colour just returns. Because we paint a cell before moving on, its colour changes and we never visit it twice.
int[][] floodFill(int[][] image, int sr, int sc, int color) {
int start = image[sr][sc];
if (start == color) return image; // nothing to do (and avoids an endless loop)
dfs(image, sr, sc, start, color);
return image;
}
void dfs(int[][] image, int r, int c, int start, int color) {
if (r < 0 || r >= image.length || c < 0 || c >= image[0].length) return; // off the grid
if (image[r][c] != start) return; // wall, or already painted
image[r][c] = color; // paint first = mark as visited
dfs(image, r + 1, c, start, color); // down
dfs(image, r - 1, c, start, color); // up
dfs(image, r, c + 1, start, color); // right
dfs(image, r, c - 1, start, color); // left
}
Time O(m·n)Space O(m·n)
Each cell is painted at most once. The space is the recursion stack, which in the worst case holds every cell.
Grid DFS template: check bounds, check the cell is valid, mark it, recurse in 4 directions. Changing the cell itself can serve as the "visited" mark.
If the new colour equals the old colour, painting does not change anything, so cells never look "visited" and the DFS bounces between two cells until StackOverflowError. The early return prevents it.
Pattern notes: Matrix & Islands · Open on LeetCode
LC 997Find the Town JudgeEasyIn/Out DegreeLeetCode
A town has n people, labelled 1 to n. Each pair [a, b] in trust means "a trusts b". The town judge trusts nobody, and everybody else trusts the judge. Return the judge's label, or -1 if there is none.
Example: n = 3, trust = [[1,3],[2,3]] → 3.
1Brute force — test every person as the judge
For each person, scan the whole trust list: do they trust anyone? How many people trust them? The judge trusts nobody and is trusted by n - 1 people.
int findJudge(int n, int[][] trust) {
for (int cand = 1; cand <= n; cand++) {
boolean trustsSomeone = false;
int trustedBy = 0;
for (int[] t : trust) {
if (t[0] == cand) trustsSomeone = true;
if (t[1] == cand) trustedBy++;
}
if (!trustsSomeone && trustedBy == n - 1) return cand;
}
return -1;
}
Time O(n · t)Space O(1)
Here t is the length of trust. We re-read the whole list for every person, even though one read already tells us everything.
2Optimal — one pass with a trust score
Think of each pair as an arrow a → b. Give every person a score: +1 for each arrow coming in, −1 for each arrow going out. Only the judge can reach n - 1: that needs all n − 1 other people pointing in, and zero arrows going out.
int findJudge(int n, int[][] trust) {
int[] score = new int[n + 1]; // index = person label (1..n)
for (int[] t : trust) {
score[t[0]]--; // a trusts someone: can't be the judge
score[t[1]]++; // b is trusted by one more person
}
for (int i = 1; i <= n; i++) {
if (score[i] == n - 1) return i;
}
return -1;
}
Time O(n + t)Space O(n)
One pass over the pairs and one pass over the people. Any single "out" arrow drops a person below n − 1 for good.
In a directed graph, in-degree = arrows coming in, out-degree = arrows going out. Many "who is special?" questions are just counting these in an array.
People are labelled from 1, so the array needs size n + 1. And n = 1 with no pairs: the only person trusts nobody and needs 0 votes, so the answer is 1, not -1.
Pattern notes: Graphs & Traversal · Open on LeetCode
LC 1971Find if Path Exists in GraphEasyBFSLeetCode
There are n nodes (0 to n − 1) and a list of two-way edges. Can you walk from source to destination along the edges?
Example: n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5 → false: nodes 0, 1, 2 and nodes 3, 4, 5 are two separate groups.
1Brute force — keep sweeping the edge list
Mark the source as reachable. Go through every edge: if one end is reachable and the other is not, mark the other. Repeat full sweeps until nothing new gets marked.
boolean validPath(int n, int[][] edges, int source, int destination) {
boolean[] reach = new boolean[n];
reach[source] = true;
boolean changed = true;
while (changed) {
changed = false;
for (int[] e : edges) {
if (reach[e[0]] != reach[e[1]]) { // exactly one end reachable
reach[e[0]] = reach[e[1]] = true;
changed = true;
}
}
}
return reach[destination];
}
Time O(n · E)Space O(n)
Each sweep may add just one node, so we can sweep all E edges up to n times. With 200,000 nodes and edges that is far too slow.
2Optimal — adjacency list + BFS
First turn the edge list into an adjacency list: for each node, the list of its neighbours. Then explore like ripples in water: start a queue with the source, and keep visiting unvisited neighbours. If the destination ever comes out of the queue, a path exists. If the queue empties first, it does not.
boolean validPath(int n, int[][] edges, int source, int destination) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) { // two-way road: add both directions
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
boolean[] visited = new boolean[n];
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(source);
visited[source] = true; // mark when you ADD to the queue
while (!queue.isEmpty()) {
int node = queue.poll();
if (node == destination) return true;
for (int next : adj.get(node)) {
if (!visited[next]) {
visited[next] = true;
queue.offer(next);
}
}
}
return false; // ran out of places to go
}
Time O(n + E)Space O(n + E)
Every node enters the queue at most once, and every edge is looked at twice (once from each end).
Given an edge list, first build an adjacency list. Then "can I reach X?" is one BFS or DFS from the start with a visited array. (Union-Find is another good answer for this problem.)
Edges here are two-way: add both directions, or BFS can miss paths. And mark nodes visited when you add them to the queue, not when you remove them, or the same node can be queued many times.
Pattern notes: Graphs & Traversal · Open on LeetCode
LC 200Number of IslandsMediumGrid DFSLeetCode
A grid holds '1' (land) and '0' (water). An island is a group of land cells joined up, down, left or right. Count the islands.
Example: ["11000","11000","00100","00011"] → 3.
1First solution — BFS with a separate visited array
Scan the grid. Each time you find land you have not seen yet, that is a new island: add 1, then BFS from it with a queue and mark every connected land cell in a seen array so it is not counted again.
int numIslands(char[][] grid) {
int m = grid.length, n = grid[0].length, count = 0;
boolean[][] seen = new boolean[m][n];
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] != '1' || seen[r][c]) continue;
count++; // new island found
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{r, c});
seen[r][c] = true;
while (!q.isEmpty()) {
int[] cell = q.poll();
for (int[] d : dirs) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n
&& grid[nr][nc] == '1' && !seen[nr][nc]) {
seen[nr][nc] = true;
q.offer(new int[]{nr, nc});
}
}
}
}
}
return count;
}
Time O(m·n)Space O(m·n)
This is already the best possible time: every cell must be looked at at least once. What we can improve is the code: it is long, and the seen array costs a full extra grid.
2Cleaner — DFS that "sinks" each island
When you step on new land, count it, then sink the whole island: turn every connected '1' into '0'. The grid itself becomes the visited marker, so a sunk island can never be counted again. It is Flood Fill (LC 733) run once per island.
int numIslands(char[][] grid) {
int count = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == '1') { // untouched land = a new island
count++;
sink(grid, r, c); // erase it so we never count it again
}
}
}
return count;
}
void sink(char[][] grid, int r, int c) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) return;
if (grid[r][c] != '1') return; // water or already sunk
grid[r][c] = '0';
sink(grid, r + 1, c);
sink(grid, r - 1, c);
sink(grid, r, c + 1);
sink(grid, r, c - 1);
}
Time O(m·n)Space O(m·n)
Same speed, half the code and no extra grid. The space left is the recursion stack when one island covers the whole map. (If you must not change the input, go back to the seen version.)
"Count the groups" = scan + flood fill. Every time the scan hits an unvisited cell, that is a new group; flood it so it is never counted again.
The grid holds chars, not ints: compare with '1', not 1. grid[r][c] == 1 compiles but is always false, and you get 0 islands.
Pattern notes: Matrix & Islands · Open on LeetCode
Greedy
A greedy algorithm makes the best-looking choice right now and never goes back to change it. It only works when you can argue that the local choice never hurts later. The key patterns here are sort first, then match with two pointers, keep the most flexible resource for later, and collect every small gain.
LC 455Assign CookiesEasyGreedy + SortingLeetCode
Child i is happy only with a cookie of size at least g[i] (their "greed"). Cookie j has size s[j]. Each child gets at most one cookie. What is the most children you can make happy?
Example: g = [1, 2, 3], s = [1, 1] → 1. Both cookies have size 1, so only the child with greed 1 can be happy.
1Brute force — for each child, search all cookies
Go through the children from least to most greedy. For each one, scan every cookie and give them the smallest unused cookie that is big enough.
int findContentChildren(int[] g, int[] s) {
Arrays.sort(g); // least greedy child first
boolean[] used = new boolean[s.length];
int content = 0;
for (int child : g) {
int best = -1;
for (int j = 0; j < s.length; j++) { // find the smallest cookie that fits
if (!used[j] && s[j] >= child && (best == -1 || s[j] < s[best])) best = j;
}
if (best != -1) { used[best] = true; content++; }
}
return content;
}
Time O(n · m)Space O(m)
The choice is right, but finding the smallest fitting cookie costs a full scan of all m cookies for every child.
2Optimal — sort both, walk with two pointers
Never waste a big cookie on a child a small cookie could satisfy. Sort both arrays. Look at the smallest cookie: if it fits the least greedy child, give it; if not, it fits nobody, so throw it away. Either way, move on to the next cookie.
g = [1, 2, 3], s = [1, 1, 3]. Each cookie is looked at once; the child pointer moves only when a child is fed.int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int child = 0, cookie = 0;
while (child < g.length && cookie < s.length) {
if (s[cookie] >= g[child]) child++; // fits: this child is happy
cookie++; // this cookie is used or useless
}
return child; // number of happy children
}
Time O(n log n + m log m)Space O(1) extra
Sorting is the only expensive part; after that one walk over both arrays finishes the job.
Matching two groups (children and cookies, people and boats, tasks and workers)? Sort both and match smallest with smallest using two pointers.
Giving the biggest cookie to the first child you see feels generous but is wrong: g = [1, 3], s = [3, 1] gives the 3 to child 1 and child 3 goes hungry. Sorting prevents that.
Pattern notes: Greedy · Open on LeetCode
LC 860Lemonade ChangeEasyGreedyLeetCode
Lemonade costs $5. Customers come in order and pay with a $5, $10 or $20 bill. You start with no money. Can you give every customer the correct change?
Example: bills = [5, 5, 5, 10, 20] → true. But [5, 5, 10, 10, 20] → false: the last customer needs $15 and you only have two $10s.
1Brute force — keep every bill in a list and search it
Put every bill you receive into a list (your cash drawer). When you owe change, search the drawer for the largest bill that still fits, remove it, and repeat until the change is paid.
boolean lemonadeChange(int[] bills) {
List<Integer> drawer = new ArrayList<>();
for (int bill : bills) {
int change = bill - 5;
while (change > 0) { // pay the change bill by bill
int pick = -1;
for (int i = 0; i < drawer.size(); i++) {
int b = drawer.get(i);
if (b <= change && (pick == -1 || b > drawer.get(pick))) pick = i;
}
if (pick == -1) return false; // no bill fits: can't make change
change -= drawer.remove(pick);
}
drawer.add(bill);
}
return true;
}
Time O(n²)Space O(n)
The drawer grows with every customer and we search all of it each time. But a $20 bill is never useful as change here, and all $5 bills are the same, so we only need to know how many of each we have.
2Optimal — count the $5 and $10 bills
Keep two counters: five and ten. For a $20 you owe $15, which can be $10 + $5 or $5 + $5 + $5. Spend the $10 first, and save your $5 bills, because a $5 can pay any change and a $10 can only help with a $20.
boolean lemonadeChange(int[] bills) {
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
five++; // no change needed
} else if (bill == 10) {
if (five == 0) return false; // need one $5 back
five--; ten++;
} else { // $20: need $15 back
if (ten > 0 && five > 0) { ten--; five--; } // prefer 10 + 5
else if (five >= 3) { five -= 3; } // else 5 + 5 + 5
else return false;
}
}
return true;
}
Time O(n)Space O(1)
Two counters replace the whole drawer, and each customer takes a constant number of steps.
When you have a choice of what to spend, spend the least flexible thing first and keep the most flexible one for later.
Checking five >= 3 before trying 10 + 5 gives wrong answers: [5, 5, 5, 10, 5, 20, 10] would spend three $5s on the $20 and then have no $5 left for the final $10.
Pattern notes: Greedy · Open on LeetCode
LC 122Best Time to Buy and Sell Stock IIMediumGreedyLeetCode
prices[i] is a stock's price on day i. You may buy and sell as many times as you like, but you can hold at most one share at a time (you may sell and buy again on the same day). Return the maximum total profit.
Example: prices = [7, 1, 5, 3, 6, 4] → 7: buy at 1, sell at 5 (+4), buy at 3, sell at 6 (+3).
1Brute force — try every buy/sell decision
On each day you either do nothing, or you act: buy if you hold nothing, sell if you hold a share. Recursively try both options for every day and keep the best.
int maxProfit(int[] prices) {
return best(prices, 0, false);
}
int best(int[] p, int day, boolean holding) {
if (day == p.length) return 0; // no days left
int skip = best(p, day + 1, holding); // do nothing today
int act = holding
? p[day] + best(p, day + 1, false) // sell today
: -p[day] + best(p, day + 1, true); // buy today
return Math.max(skip, act);
}
Time O(2ⁿ)Space O(n)
Two choices per day means 2ⁿ paths. For 30,000 days this would never finish.
2Optimal — collect every price rise
Any long climb is the same as adding up its small daily steps: if prices go 1, 2, 4, 6, buying at 1 and selling at 6 (+5) equals (2−1) + (4−2) + (6−4). So add every day-to-day increase, and ignore every drop. That is exactly the profit of buying before each rise and selling at its top.
int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1]; // take every upward step
}
}
return profit;
}
Time O(n)Space O(1)
One pass, one variable. No path can beat "every rise, no fall", because each rise is money you could have had and each fall is money you can always avoid.
A big gain can be broken into many small consecutive gains. When trades are unlimited, sum all positive differences.
Do not mix it up with LC 121 (only one trade allowed). There, the answer for [7, 1, 5, 3, 6, 4] is 5 (buy 1, sell 6), not 7.
Pattern notes: Greedy · Open on LeetCode
Dynamic Programming
Dynamic Programming (DP) is recursion that never solves the same small problem twice. You write the answer for step i using answers for smaller steps, then fill those answers in order from the bottom up. The key patterns here are the recursion → table → two-variables path, "the last move was one of a few choices", and "take it or skip it".
LC 70Climbing StairsEasy1D DPLeetCode
A staircase has n steps. Each move you climb 1 or 2 steps. In how many different ways can you reach the top?
Example: n = 3 → 3: 1+1+1, 1+2, 2+1.
1Brute force — plain recursion
Think about your last move. To stand on step n, you came either from step n − 1 (a 1-step) or from step n − 2 (a 2-step). So the ways to reach n = ways to reach n − 1 + ways to reach n − 2.
int climbStairs(int n) {
if (n <= 2) return n; // 1 way to reach step 1, 2 ways to reach step 2
return climbStairs(n - 1) + climbStairs(n - 2);
}
Time O(2ⁿ)Space O(n)
This is Fibonacci in disguise, with the same exploding recursion tree as LC 509: n = 45 makes over a billion calls.
2Optimal — bottom-up, keep only the last two answers
Instead of starting at the top and recursing down, start at the bottom and walk up: ways(1) = 1, ways(2) = 2, and each next one is the sum of the two before. Each answer only needs the previous two, so two variables are enough, with no array and no recursion.
int climbStairs(int n) {
if (n <= 2) return n;
int prev2 = 1, prev1 = 2; // ways to reach step 1 and step 2
for (int i = 3; i <= n; i++) {
int cur = prev1 + prev2; // came from i-1 or from i-2
prev2 = prev1; // slide the window up one step
prev1 = cur;
}
return prev1;
}
Time O(n)Space O(1)
One loop, two variables. This is the full DP journey in one problem: recursion → memo (LC 509) → table → two variables.
DP recipe: 1) define what dp[i] means, 2) ask "what was the last move?" to get the formula, 3) set the base cases, 4) fill from small to big, 5) if you only look back a fixed number of steps, keep only those variables.
Update the variables in the right order: set prev2 = prev1 before prev1 = cur. The other way round both end up holding cur. Also handle n = 1 before the loop.
Pattern notes: Dynamic Programming — the Fundamentals · Open on LeetCode
LC 746Min Cost Climbing StairsEasy1D DPLeetCode
cost[i] is what you pay when you step on stair i. After paying you may climb 1 or 2 stairs. You may start on stair 0 or stair 1 for free. The "top" is just past the last stair. Return the cheapest total cost to reach the top.
Example: cost = [10, 15, 20] → 15: start on stair 1, pay 15, jump 2 to the top.
1Brute force — try both jumps from every stair
From stair i, the cheapest way to finish is: pay cost[i], then take the cheaper of "finish from i + 1" and "finish from i + 2". Start from both stair 0 and stair 1 and keep the smaller.
int minCostClimbingStairs(int[] cost) {
return Math.min(from(cost, 0), from(cost, 1));
}
int from(int[] cost, int i) {
if (i >= cost.length) return 0; // reached the top: nothing more to pay
return cost[i] + Math.min(from(cost, i + 1), from(cost, i + 2));
}
Time O(2ⁿ)Space O(n)
Two branches per stair and the same from(i) is recomputed many times. With 1,000 stairs it never finishes.
2Optimal — bottom-up with two variables
Let dp[i] = the cheapest cost to stand on stair i (not yet paid for it). You can stand on 0 or 1 for free. To stand on i, your last jump came from i − 1 (paying cost[i-1]) or from i − 2 (paying cost[i-2]): dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]). The answer is dp[n], the top.
int minCostClimbingStairs(int[] cost) {
int prev2 = 0, prev1 = 0; // dp[0] = dp[1] = 0: start there for free
for (int i = 2; i <= cost.length; i++) {
int cur = Math.min(prev1 + cost[i - 1], // one step from i-1
prev2 + cost[i - 2]); // two steps from i-2
prev2 = prev1;
prev1 = cur;
}
return prev1; // dp[n] = cost to reach the top
}
Time O(n)Space O(1)
Same shape as Climbing Stairs, with min instead of +. Counting ways adds the options; finding the cheapest takes the minimum of them.
Write down exactly what dp[i] means in one sentence before coding. Most DP bugs come from a fuzzy definition (here: "cost to stand on i" vs "cost including stepping off i").
The top is past the last stair, at index n, not at n - 1. Stopping the loop at i < cost.length answers the wrong question.
Pattern notes: Dynamic Programming — the Fundamentals · Open on LeetCode
LC 118Pascal's TriangleEasyDP (row by row)LeetCode
Build the first numRows rows of Pascal's triangle. Every row starts and ends with 1, and every inner number is the sum of the two numbers just above it.
Example: numRows = 5 → [[1], [1,1], [1,2,1], [1,3,3,1], [1,4,6,4,1]].
1Brute force — compute each number by recursion
Write a function value(r, c) straight from the rule: edges are 1, everything else is value(r-1, c-1) + value(r-1, c). Call it for every position.
List<List<Integer>> generate(int numRows) {
List<List<Integer>> result = new ArrayList<>();
for (int r = 0; r < numRows; r++) {
List<Integer> row = new ArrayList<>();
for (int c = 0; c <= r; c++) row.add(value(r, c));
result.add(row);
}
return result;
}
int value(int r, int c) {
if (c == 0 || c == r) return 1; // the edges
return value(r - 1, c - 1) + value(r - 1, c); // the two above
}
Time O(2ⁿ)Space O(n) stack
Each call splits into two, and the numbers of the rows above are recomputed from scratch for every single cell, even though we just built those rows.
2Optimal — build each row from the previous row
The row above is already sitting in result. So read the two numbers above from the previous row instead of recomputing them. The triangle itself is our DP table.
List<List<Integer>> generate(int numRows) {
List<List<Integer>> result = new ArrayList<>();
for (int r = 0; r < numRows; r++) {
List<Integer> row = new ArrayList<>();
for (int c = 0; c <= r; c++) {
if (c == 0 || c == r) {
row.add(1); // edges are always 1
} else {
List<Integer> above = result.get(r - 1);
row.add(above.get(c - 1) + above.get(c)); // sum of the two above
}
}
result.add(row);
}
return result;
}
Time O(n²)Space O(1) extra
There are about n²/2 numbers to output and each costs one addition, so we cannot do better. The output list itself is not counted as extra space.
If the rule says "this cell = something from the cells above", build the table top to bottom and read from it. That is DP, even without the word "min" or "max".
Row r has r + 1 numbers, so loop c <= r. And for row 0 there is no row above: the c == 0 || c == r check must come first, or result.get(-1) crashes.
Pattern notes: Dynamic Programming — the Fundamentals · Open on LeetCode
LC 198House RobberMedium1D DP (take / skip)LeetCode
Houses stand in a row, and nums[i] is the money in house i. You cannot rob two neighbouring houses (the alarm goes off). What is the most money you can take?
Example: nums = [2, 7, 9, 3, 1] → 12: rob houses 0, 2 and 4 (2 + 9 + 1).
1Brute force — take or skip, recursively
Stand at the last house i. Two choices: skip it (best of houses 0..i−1), or rob it (its money + best of houses 0..i−2, because i−1 is now off limits). Take the larger.
int rob(int[] nums) {
return best(nums, nums.length - 1);
}
int best(int[] nums, int i) {
if (i < 0) return 0; // no houses left
return Math.max(best(nums, i - 1), // skip house i
nums[i] + best(nums, i - 2)); // rob house i
}
Time O(2ⁿ)Space O(n)
The same best(k) is recomputed along many paths, exactly like the Fibonacci tree.
2Optimal — bottom-up, two variables
Walk the street from left to right and keep the best total so far. At each house: best = max(skip: best up to the previous house, rob: best up to two houses back + this money). Like Climbing Stairs, only the last two answers are ever needed.
int rob(int[] nums) {
int prev2 = 0, prev1 = 0; // best up to two houses back, and up to the previous house
for (int money : nums) {
int cur = Math.max(prev1, // skip this house
prev2 + money); // rob it (so the previous one was skipped)
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
Time O(n)Space O(1)
Starting both variables at 0 handles one or two houses without special cases.
"Choose items, but some choices block others" → at each item ask take it or skip it? and write dp[i] = max(skip, take). This take/skip idea powers knapsack and many harder DP problems.
Robbing every other house (all even or all odd positions) is not always best: for [2, 1, 1, 2] the answer is 4 (first and last house), which neither pattern gives.
Pattern notes: DP Patterns · Open on LeetCode
Finish the Beginner list first; these problems assume those tricks. Most problems here are Medium. Each card adds a How to spot the pattern box: learning to recognise the pattern from the problem statement is the real skill at this level.
Same routine: try for 30–40 minutes, open the card, then re-solve without looking two or three days later.
Arrays, Hashing & Prefix Sum
The pattern: walk the array once, and at each step ask a HashMap about something you saw earlier. In Beginner the question was simple ("have I seen this number?"). Here the harder part is choosing what to store: a sorted word as a grouping key, a running prefix sum or remainder as the key with a count as the value, a first index for "longest" questions, or a map plus a list working together. You will also meet prefix and suffix products and the "only start counting at the start of a run" trick.
LC 49Group AnagramsMediumHash Map (grouping key)LeetCode
You get a list of words. Put words that are anagrams of each other (same letters, different order) into the same group. Return the groups in any order.
Example: ["eat","tea","tan","ate","nat","bat"] → [["eat","tea","ate"], ["tan","nat"], ["bat"]].
The word "group" is the signal. Whenever you must put items into buckets by "they are the same in some way", ask: can I compute a label that is identical for items in the same bucket? Then a HashMap<label, list> does the grouping for you. In Beginner (LC 242 Valid Anagram) you compared two words; here you need a label so you never compare pairs at all.
1Brute force — compare every word with every other word
Take the first word that has no group yet, start a new group with it, then check every later word: if it is an anagram (sort both and compare), add it to this group.
List<List<String>> groupAnagrams(String[] strs) {
List<List<String>> result = new ArrayList<>();
boolean[] used = new boolean[strs.length];
for (int i = 0; i < strs.length; i++) {
if (used[i]) continue; // already in a group
List<String> group = new ArrayList<>();
group.add(strs[i]);
used[i] = true;
for (int j = i + 1; j < strs.length; j++) {
if (!used[j] && isAnagram(strs[i], strs[j])) {
group.add(strs[j]);
used[j] = true;
}
}
result.add(group);
}
return result;
}
boolean isAnagram(String a, String b) {
if (a.length() != b.length()) return false;
char[] x = a.toCharArray(), y = b.toCharArray();
Arrays.sort(x);
Arrays.sort(y);
return Arrays.equals(x, y);
}
Time O(n² · k log k)Space O(n · k)
Here n is the number of words and k the longest word. With 10,000 words that is tens of millions of pair checks, each one sorting two words.
2Optimal — sort each word once and use it as a map key
All anagrams become the same string once you sort their letters: "eat", "tea" and "ate" all become "aet". So the sorted word is the group's label. Put every word into map[sortedWord], and the map's values are the answer. Like sorting mail by postcode: you never compare two letters with each other, you just read each one's postcode.
List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>(); // sorted word -> its group
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = new String(chars); // "tea" -> "aet"
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
Time O(n · k log k)Space O(n · k)
Each word is sorted once and dropped into its bucket in O(1). computeIfAbsent means "get the list for this key, creating an empty one the first time".
"Group things that are the same in some way" → invent a key that is equal exactly when items belong together, then use Map<key, List>. For lowercase words, a 26-letter count string (like "1#0#0#…") is another key that avoids the sort and gives O(n · k).
Do not use chars.toString() as the key: on an array it prints a memory address like [C@1b6d3586, so every word gets its own group. Use new String(chars) (or String.valueOf(chars)). And the empty string "" is a valid word: it forms its own group.
Pattern notes: Hashing · Open on LeetCode
LC 238Product of Array Except SelfMediumPrefix / Suffix ProductLeetCode
For every position i, return the product of all the other numbers (everything except nums[i]). You are not allowed to use division, and it should run in O(n).
Example: nums = [1, 2, 3, 4] → [24, 12, 8, 6]. For index 2: 1 × 2 × 4 = 8.
"For every position, combine everything except itself" means: everything to the left of i, combined with everything to the right of i. That is a prefix (left running total) and a suffix (right running total). It is the same idea as Beginner's prefix sums (LC 724 Find Pivot Index), just with × instead of +. The words "without division" and "O(n)" rule out the shortcuts.
1Brute force — multiply everything else for each position
For each i, loop over the whole array and multiply every number except nums[i].
int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] answer = new int[n];
for (int i = 0; i < n; i++) {
int product = 1;
for (int j = 0; j < n; j++) {
if (j != i) product *= nums[j]; // skip only position i
}
answer[i] = product;
}
return answer;
}
Time O(n²)Space O(1) extra
Each answer redoes almost the same multiplication as its neighbour. The tempting fix, "total product ÷ nums[i]", is banned and also breaks when there is a 0.
2Optimal — left products times right products
answer[i] = (product of everything left of i) × (product of everything right of i). Pass 1 walks left to right and writes the left product into answer. Pass 2 walks right to left with one variable right that holds the product of everything after i, and multiplies it in. No division, and no extra array.
int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] answer = new int[n];
// pass 1: answer[i] = product of nums[0..i-1]
answer[0] = 1; // nothing to the left of index 0
for (int i = 1; i < n; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// pass 2: multiply in the product of nums[i+1..n-1]
int right = 1; // nothing to the right of the last index
for (int i = n - 1; i >= 0; i--) {
answer[i] *= right;
right *= nums[i]; // now includes nums[i] for the next (left) position
}
return answer;
}
Time O(n)Space O(1) extra
Two passes, and the only memory besides the output array is one int. The output array does not count as extra space on LeetCode.
"Everything except i" = left part × right part. Build the left part in one pass and the right part in a reverse pass. The same shape solves "trapping rain water" (max on the left, max on the right).
Order inside pass 2 matters: use right first, then multiply nums[i] into it. Swapping the two lines includes the number itself. Also test zeros: [-1, 1, 0, -3, 3] → [0, 0, 9, 0, 0], which the division trick gets wrong.
Pattern notes: Prefix Sum & Difference Arrays · Open on LeetCode
LC 128Longest Consecutive SequenceMediumHash SetLeetCode
The array is unsorted. Find the longest run of numbers that follow each other (like 1, 2, 3, 4), using the values in any order. Return its length. It must run in O(n).
Example: [100, 4, 200, 1, 3, 2] → 4, because 1, 2, 3, 4 are all there.
"Consecutive" means you keep asking "is x + 1 there?", which is a membership question, so a HashSet. The phrase "must run in O(n)" on an unsorted array is a strong hint that sorting (O(n log n)) is not what they want. The new trick versus Beginner: only start counting from numbers that begin a run.
1Brute force — sort, then count runs
The natural first idea: sort the array, then walk it. If the next number is one bigger, the run grows; if it is the same, ignore it; otherwise the run restarts at 1.
int longestConsecutive(int[] nums) {
if (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; // duplicate: run unchanged
if (nums[i] == nums[i - 1] + 1) run++; // continues the run
else run = 1; // gap: start a new run
best = Math.max(best, run);
}
return best;
}
Time O(n log n)Space O(1) or O(n) for the sort
Correct and simple, but the problem explicitly asks for O(n), and the sort is the slow part.
2Optimal — HashSet, and only count from run starts
Put everything in a set. A number x is the start of a run only if x − 1 is not in the set. Only from starts do we walk forward: x + 1, x + 2, … while they exist. Numbers in the middle of a run are skipped straight away, so every number is walked over at most once in total.
int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int x : nums) set.add(x);
int best = 0;
for (int x : set) { // loop over the SET, not the array
if (set.contains(x - 1)) continue; // not a start: its run is counted elsewhere
int length = 1;
while (set.contains(x + length)) length++;
best = Math.max(best, length);
}
return best;
}
Time O(n)Space O(n)
The while inside the for looks like O(n²), but it only runs from starts, and runs never overlap, so the total walking is at most n steps.
To avoid repeated work, only begin from a "start" element (one with no predecessor). The same trick counts islands or chains without counting any chain twice.
Loop over the set, not the original array. If the input has thousands of copies of a run start (like many 1s followed by 2…50000), looping over the array re-walks the same run for every copy and gets Time Limit Exceeded. And return 0 for an empty array.
Pattern notes: Hashing · Open on LeetCode
LC 36Valid SudokuMediumHash SetLeetCode
You get a partly filled 9×9 Sudoku board ('.' means empty). Check only the filled cells: no digit may repeat in any row, any column, or any of the nine 3×3 boxes. You do not have to solve it.
Example: the classic LeetCode board starting ["5","3",".",".","7",...] → true. Change the top-left 5 into an 8 and you get false, because column 0 (and the top-left box) now has two 8s.
"No repeats" is a duplicate check, which is a set (Beginner LC 217 Contains Duplicate). The twist is that every cell belongs to three groups at once (a row, a column, a box), so you keep one set per group and need a formula that tells you which box a cell is in.
1Brute force — three separate sweeps
Check each row with a fresh set, then each column, then each 3×3 box. Any digit that is already in the current set means a repeat.
boolean isValidSudoku(char[][] board) {
for (int r = 0; r < 9; r++) { // rows
Set<Character> seen = new HashSet<>();
for (int c = 0; c < 9; c++) {
char ch = board[r][c];
if (ch != '.' && !seen.add(ch)) return false; // add returns false on a repeat
}
}
for (int c = 0; c < 9; c++) { // columns
Set<Character> seen = new HashSet<>();
for (int r = 0; r < 9; r++) {
char ch = board[r][c];
if (ch != '.' && !seen.add(ch)) return false;
}
}
for (int b = 0; b < 9; b++) { // boxes
Set<Character> seen = new HashSet<>();
for (int r = (b / 3) * 3; r < (b / 3) * 3 + 3; r++) {
for (int c = (b % 3) * 3; c < (b % 3) * 3 + 3; c++) {
char ch = board[r][c];
if (ch != '.' && !seen.add(ch)) return false;
}
}
}
return true;
}
Time O(81) = O(1)Space O(1)
The board is always 9×9, so this is already as fast as it gets in big-O. But it is long and reads the board three times. Step 2 is the cleaner single-pass version interviewers expect.
2Cleaner — one pass, 27 "seen" tables
Keep 9 row tables, 9 column tables and 9 box tables. For each filled cell, check all three at once. The only new piece is the box number: box = (r / 3) * 3 + c / 3. r / 3 is the box row (0, 1 or 2), c / 3 the box column, and multiplying the box row by 3 numbers the boxes 0 to 8 like reading a page.
boolean isValidSudoku(char[][] board) {
boolean[][] rows = new boolean[9][9]; // rows[r][d]: digit d+1 already in row r
boolean[][] cols = new boolean[9][9];
boolean[][] boxes = new boolean[9][9];
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
if (board[r][c] == '.') continue;
int d = board[r][c] - '1'; // '1'..'9' -> 0..8
int b = (r / 3) * 3 + c / 3; // which 3x3 box
if (rows[r][d] || cols[c][d] || boxes[b][d]) return false;
rows[r][d] = cols[c][d] = boxes[b][d] = true;
}
}
return true;
}
Time O(81) = O(1)Space O(243) = O(1)
Boolean arrays work like tiny sets because the digits are only 1 to 9. For an n×n board this is O(n²), reading each cell once.
When one item belongs to several groups, keep one "seen" table per group and check them together. (r / 3) * 3 + c / 3 is the standard way to number 3×3 boxes.
Skip the '.' cells, otherwise the second empty cell looks like a repeat. And do not try to check whether the board is solvable: a board with no repeats is valid even if it can never be finished.
Pattern notes: Hashing · Open on LeetCode
LC 560Subarray Sum Equals KMediumPrefix Sum + Hash MapLeetCode
Count how many subarrays (continuous pieces of the array) add up to exactly k. The numbers can be negative.
Example: nums = [1, 1, 1], k = 2 → 2 (the first two 1s, and the last two 1s). [1, 2, 3], k = 3 → 2 ([1, 2] and [3]).
"Subarray sum" points to prefix sums (Beginner LC 303). "Count how many" plus "negative numbers allowed" rules out a sliding window, because with negatives a bigger window is not always a bigger sum. When you see "count subarrays with sum / property X", think prefix sum + HashMap of counts: it is Two Sum, but on prefix sums.
1Brute force — every start, growing end
Try each start position. Move the end forward one step at a time, keeping a running sum, and count every time the sum equals k.
int subarraySum(int[] nums, int k) {
int count = 0;
for (int start = 0; start < nums.length; start++) {
int sum = 0;
for (int end = start; end < nums.length; end++) {
sum += nums[end]; // sum of nums[start..end]
if (sum == k) count++;
}
}
return count;
}
Time O(n²)Space O(1)
With n up to 20,000 that is 200 million sums. We can't stop early either, because a negative number later could bring the sum back to k.
2Optimal — prefix sums, and look up the partner prefix
Let prefix be the sum of everything up to the current index. A subarray ending here has sum k exactly when some earlier prefix equals prefix − k (the part we "cut off" from the front). So count += how many earlier prefixes equal prefix − k. A HashMap from prefix value to how many times we have seen it answers that in O(1). Start the map with {0: 1}: the "empty prefix" before index 0, so subarrays that start at index 0 are counted.
int subarraySum(int[] nums, int k) {
Map<Integer, Integer> seen = new HashMap<>(); // prefix sum -> how many times seen
seen.put(0, 1); // empty prefix, before index 0
int prefix = 0, count = 0;
for (int x : nums) {
prefix += x;
count += seen.getOrDefault(prefix - k, 0); // earlier prefixes we can cut off
seen.merge(prefix, 1, Integer::sum); // store AFTER looking up
}
return count;
}
Time O(n)Space O(n)
One pass. The map stores counts (not just true/false) because the same prefix can appear several times, and each copy is a different start point.
sum(i+1..j) = prefix[j] − prefix[i]. "Count subarrays with sum k" becomes "count earlier prefixes equal to prefix − k", which is Two Sum on prefix sums. Always seed the map with {0: 1}.
Forgetting seen.put(0, 1) misses every subarray that starts at index 0: [1, 2, 3], k = 3 would return 1 instead of 2. And don't reach for a sliding window: with negatives it gives wrong answers.
Pattern notes: Prefix Sum & Difference Arrays · Open on LeetCode
LC 525Contiguous ArrayMediumPrefix Sum + Hash MapLeetCode
The array has only 0s and 1s. Find the longest continuous piece that has as many 0s as 1s, and return its length.
Example: [0, 1] → 2. [0, 1, 0] → 2 (either [0, 1] or [1, 0]). [0, 1, 1, 1, 1, 1, 0, 0, 0] → 6.
"Equal number of 0s and 1s" can be turned into a sum: count a 1 as +1 and a 0 as −1, and a balanced piece is a subarray with sum 0. "Longest subarray with sum X" → prefix sum + HashMap, but this time store the first index where each prefix appeared (for "count" questions like LC 560 you store how many times).
1Brute force — count zeros and ones for every piece
For each start, grow the end one step at a time, keeping counts of 0s and 1s. When they are equal, the piece is balanced; keep the longest.
int findMaxLength(int[] nums) {
int best = 0;
for (int start = 0; start < nums.length; start++) {
int zeros = 0, ones = 0;
for (int end = start; end < nums.length; end++) {
if (nums[end] == 0) zeros++; else ones++;
if (zeros == ones) best = Math.max(best, end - start + 1);
}
}
return best;
}
Time O(n²)Space O(1)
n can be 100,000, so about 5 billion steps. Far too slow.
2Optimal — running balance and its first appearance
Keep a balance: +1 for a 1, −1 for a 0. If the balance is the same value at two indices i < j, then everything between them added up to 0, so it is balanced, with length j − i. To make that as long as possible, remember only the first index where each balance appeared. Seed the map with {0: −1} so pieces starting at index 0 work.
int findMaxLength(int[] nums) {
Map<Integer, Integer> first = new HashMap<>(); // balance -> first index it appeared
first.put(0, -1); // balance 0 "before" index 0
int balance = 0, best = 0;
for (int i = 0; i < nums.length; i++) {
balance += (nums[i] == 1) ? 1 : -1;
if (first.containsKey(balance)) {
best = Math.max(best, i - first.get(balance)); // piece after that index up to i
} else {
first.put(balance, i); // only the FIRST time
}
}
return best;
}
Time O(n)Space O(n)
Each index is handled once. Because we never overwrite the first index, the distance i − first[balance] is always the longest possible piece ending at i.
Turn "equal counts of A and B" into +1 / −1 and sum 0. For "longest" questions store the first index of each prefix; for "count" questions (LC 560) store how many times.
Seed with {0: -1}, not {0: 0}: for [0, 1] the balance is 0 at i = 1, and the length must be 1 − (−1) = 2. And never update the index when a balance repeats, or the pieces get shorter.
Pattern notes: Prefix Sum & Difference Arrays · Open on LeetCode
LC 974Subarray Sums Divisible by KMediumPrefix Sum + RemaindersLeetCode
Count the continuous pieces of the array whose sum can be divided by k with nothing left over (sum % k == 0). Numbers can be negative.
Example: nums = [4, 5, 0, -2, -3, 1], k = 5 → 7, for example [5], [5, 0], [-2, -3] and the whole array (sum 5). [5], k = 9 → 0.
"Count subarrays whose sum …" is the LC 560 shape: prefix sum + counts. "Divisible by k" changes what "partner" means: two prefixes cut out a divisible piece when they have the same remainder mod k. So the map key becomes the remainder, and since remainders are 0 to k−1, a plain array can replace the map.
1Brute force — test every piece
Same as LC 560's brute force, but check sum % k == 0 instead of sum == k.
int subarraysDivByK(int[] nums, int k) {
int count = 0;
for (int start = 0; start < nums.length; start++) {
int sum = 0;
for (int end = start; end < nums.length; end++) {
sum += nums[end];
if (sum % k == 0) count++; // -5 % 5 is 0 too, so this check is safe
}
}
return count;
}
Time O(n²)Space O(1)
n can be 30,000, which means about 450 million sums. Too slow.
2Optimal — count prefixes by remainder
The piece between two prefixes is prefix[j] − prefix[i]. That is divisible by k exactly when both prefixes leave the same remainder when divided by k (like two times on a clock that show the same hour: the difference is a whole number of turns). So for each new prefix, add "how many earlier prefixes had this remainder", then count it. Start with count[0] = 1 for the empty prefix.
int subarraysDivByK(int[] nums, int k) {
int[] seen = new int[k]; // seen[r] = how many prefixes had remainder r
seen[0] = 1; // empty prefix
int prefix = 0, count = 0;
for (int x : nums) {
prefix += x;
int rem = ((prefix % k) + k) % k; // Java's % can be negative: -3 % 5 == -3
count += seen[rem];
seen[rem]++;
}
return count;
}
Time O(n)Space O(k)
One pass and an array of k counters. No HashMap is needed because the keys are small numbers 0 to k−1.
"Sum divisible by k" → same remainder of two prefix sums. Key the counts by prefix mod k, seeded with remainder 0 once. For negatives always use ((x % k) + k) % k.
In Java, -1 % 5 is -1, not 4. Prefixes −1 and 4 should share a bucket but land in different ones. For [-1, 2, 9], k = 2 the plain % gives 1 instead of the correct 2 (and with an array it even crashes on a negative index).
Pattern notes: Prefix Sum & Difference Arrays · Open on LeetCode
LC 380Insert Delete GetRandom O(1)MediumDesign (Map + Array)LeetCode
Build a set class with three methods, each in average O(1): insert(val) (return false if already there), remove(val) (return false if not there), and getRandom() which returns one of the current values, each with the same chance.
Example: insert(1) → true, remove(2) → false, insert(2) → true, getRandom() → 1 or 2, remove(1) → true, insert(2) → false, getRandom() → 2.
Design problems that list several operations with "O(1)" next to each one are telling you no single structure is enough. A HashSet gives O(1) insert/remove but can't pick a random element without walking it. An array gives O(1) random pick by index but O(n) remove. Combine them: a list for random access, a map to find positions.
1Brute force — only an ArrayList
Store the values in a list. Random is easy (pick a random index), but checking and removing means searching the list.
class RandomizedSet {
private List<Integer> list = new ArrayList<>();
private Random rand = new Random();
public boolean insert(int val) {
if (list.contains(val)) return false; // O(n) scan
list.add(val);
return true;
}
public boolean remove(int val) {
return list.remove(Integer.valueOf(val)); // O(n): search, then shift everything after it
}
public int getRandom() {
return list.get(rand.nextInt(list.size())); // O(1)
}
}
Time O(n) insert / removeSpace O(n)
Two problems: finding a value is a scan, and removing from the middle shifts every later element left.
2Optimal — list + map, and remove by swapping with the last
Keep the values in a list and a map value → its index in the list. Insert = append + record the index. For remove, the trick is move the last element into the hole, then delete the last slot. Removing the last slot of an ArrayList is O(1) because nothing has to shift. Only the moved element's index changes, and the map fixes that in O(1).
class RandomizedSet {
private List<Integer> values = new ArrayList<>(); // for O(1) random pick
private Map<Integer, Integer> indexOf = new HashMap<>(); // value -> index in values
private Random rand = new Random();
public boolean insert(int val) {
if (indexOf.containsKey(val)) return false;
indexOf.put(val, values.size()); // it will sit at the end
values.add(val);
return true;
}
public boolean remove(int val) {
if (!indexOf.containsKey(val)) return false;
int i = indexOf.get(val);
int last = values.get(values.size() - 1);
values.set(i, last); // move last into the hole
indexOf.put(last, i);
values.remove(values.size() - 1); // remove the end: no shifting
indexOf.remove(val); // do this AFTER updating 'last'
return true;
}
public int getRandom() {
return values.get(rand.nextInt(values.size()));
}
}
Time O(1) average, every methodSpace O(n)
The map answers "is it here, and where?", the list answers "give me the k-th one". Each covers the other's weak spot.
"Delete from an array in O(1) when order doesn't matter" → swap with the last element and pop. Pair a list with a value → index map whenever you need both lookup and random/indexed access.
When the value being removed is the last element, last == val: if you delete the key first and then put(last, i), you put it back and the map keeps a stale entry. Update last first, delete val last (as above). Also call values.remove(int index), not remove(Integer).
Pattern notes: Hashing · Open on LeetCode
Two Pointers
The pattern: sort first, then let two pointers walk toward each other, moving the one that brings you closer to the goal. In Beginner you used it once (LC 167 Two Sum II, LC 11). Here it is harder in three ways: you fix one or two numbers with outer loops and run the two-pointer walk inside (3Sum, 4Sum), you must skip duplicates so each answer appears once, and you meet three pointers splitting an array into regions (Dutch national flag) and a greedy pairing proof (Boats).
LC 153SumMediumSort + Two PointersLeetCode
Find all different triplets of numbers (from three different positions) that add up to 0. The same triplet of values must not appear twice in the answer.
Example: [-1, 0, 1, 2, -1, -4] → [[-1, -1, 2], [-1, 0, 1]]. [0, 1, 1] → []. [0, 0, 0] → [[0, 0, 0]].
"Find numbers that add up to a target" with more than two numbers: fix one number with a loop, and the rest is Two Sum for −nums[i]. "Return the values, not the indices" and "no duplicate triplets" mean sorting is allowed and helpful: sorted order lets the two-pointer walk work and puts duplicates next to each other, so they are easy to skip.
1Brute force — every triple, deduplicated with a set
Try all i < j < k. When a triple sums to 0, sort it and put it in a set, so the same values are kept only once.
List<List<Integer>> threeSum(int[] nums) {
Set<List<Integer>> found = new HashSet<>();
int n = nums.length;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
for (int k = j + 1; k < n; k++)
if (nums[i] + nums[j] + nums[k] == 0) {
List<Integer> t = Arrays.asList(nums[i], nums[j], nums[k]);
Collections.sort(t); // [2,-1,-1] and [-1,2,-1] look the same
found.add(t);
}
return new ArrayList<>(found);
}
Time O(n³)Space O(answers)
n is up to 3000, so about 4.5 billion triples. Way too slow.
2Optimal — sort, fix one number, two pointers for the other two
Sort the array. For each i, look for two numbers after it that sum to −nums[i], with left just after i and right at the end. Sum too small → left++; too big → right--; zero → save it and move both. That is Beginner's Two Sum II inside a loop. To avoid repeated triplets, skip an i equal to the previous one, and after a hit move left past equal values.
List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // same first number: same triplets
if (nums[i] > 0) break; // all later numbers are positive too
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left++; // need bigger
} else if (sum > 0) {
right--; // need smaller
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) left++; // skip repeats
}
}
}
return result;
}
Time O(n²)Space O(1) extra (besides sorting)
Sorting is O(n log n), then n outer steps each do an O(n) walk. No set is needed, because skipping equal neighbours already stops duplicates.
k-Sum = sort + (k − 2) loops + one two-pointer walk. Dedupe by skipping a value equal to the one just before it, at every level.
Skip duplicates with nums[i] == nums[i - 1] (look back), not nums[i] == nums[i + 1] (look ahead). Looking ahead skips the first -1 in [-1, -1, 2] and loses the triplet that needs both -1s.
Pattern notes: Two Pointers · Open on LeetCode
LC 163Sum ClosestMediumSort + Two PointersLeetCode
Pick three numbers (different positions) whose sum is as close as possible to target. Return that sum. There is exactly one best answer.
Example: nums = [-1, 2, 1, -4], target = 1 → 2 (-1 + 2 + 1 = 2, distance 1). [0, 0, 0], target = 1 → 0.
Three numbers and a target sum → 3Sum's shape. "Closest" instead of "equal" just means: every sum the walk visits is a candidate, so keep the best one. The two pointers still work because in sorted order, moving left right always makes the sum bigger and moving right left makes it smaller, so you always know which way gets closer.
1Brute force — check every triple
Compute every triple's sum and keep the one with the smallest distance to the target.
int threeSumClosest(int[] nums, int target) {
int best = nums[0] + nums[1] + nums[2];
for (int i = 0; i < nums.length; i++)
for (int j = i + 1; j < nums.length; j++)
for (int k = j + 1; k < nums.length; k++) {
int sum = nums[i] + nums[j] + nums[k];
if (Math.abs(sum - target) < Math.abs(best - target)) best = sum;
}
return best;
}
Time O(n³)Space O(1)
n is up to 500, so about 20 million triples: it may pass, but the interviewer wants the O(n²) walk.
2Optimal — 3Sum's walk, tracking the closest sum
Sort, fix i, walk left and right. At every step, compare the current sum with the best so far. Then steer: sum below target → left++ (go bigger), above → right-- (go smaller), equal → you can't beat distance 0, return it.
int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int best = nums[0] + nums[1] + nums[2]; // any real triple to start
for (int i = 0; i < nums.length - 2; i++) {
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (Math.abs(sum - target) < Math.abs(best - target)) best = sum;
if (sum < target) left++; // go bigger
else if (sum > target) right--; // go smaller
else return sum; // exact: distance 0
}
}
return best;
}
Time O(n²)Space O(1) extra (besides sorting)
Same walk as 3Sum. No duplicate skipping is needed since we return one number, not a list.
For "closest" versions of a two-pointer search, update the best at every step of the walk, then move the pointer that heads toward the target.
Start best with a real triple sum, not Integer.MAX_VALUE: MAX_VALUE - target overflows when the target is negative, and the comparison breaks. Return the sum, not the distance.
Pattern notes: Two Pointers · Open on LeetCode
LC 184SumMediumSort + Two PointersLeetCode
Find all different quadruplets (four different positions) that add up to target. No repeated quadruplets of values.
Example: nums = [1, 0, -1, 0, -2, 2], target = 0 → [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]. [2, 2, 2, 2, 2], target = 8 → [[2, 2, 2, 2]].
It is 3Sum with one more number. "All unique k numbers that add to a target" → sort, fix k − 2 numbers with loops, and run the two-pointer walk for the last two. The numbers go up to 10⁹, which is your cue to think about overflow.
1Brute force — four nested loops
Try every a < b < c < d, sort each hit, and collect in a set.
List<List<Integer>> fourSum(int[] nums, int target) {
Set<List<Integer>> found = new HashSet<>();
int n = nums.length;
for (int a = 0; a < n; a++)
for (int b = a + 1; b < n; b++)
for (int c = b + 1; c < n; c++)
for (int d = c + 1; d < n; d++)
if ((long) nums[a] + nums[b] + nums[c] + nums[d] == target) {
List<Integer> q = Arrays.asList(nums[a], nums[b], nums[c], nums[d]);
Collections.sort(q);
found.add(q);
}
return new ArrayList<>(found);
}
Time O(n⁴)Space O(answers)
n is up to 200, which is about 65 million quadruples. Too slow, and it only gets worse for bigger k.
2Optimal — two loops, then the two-pointer walk
Sort. Loop a, then loop b > a, and for the pair (a, b) do exactly 3Sum's walk on the rest: left++ if too small, right-- if too big, save and move both on a hit. Skip a repeated value at each level (a, b and left). Add the four numbers as long.
List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
int n = nums.length;
for (int a = 0; a < n - 3; a++) {
if (a > 0 && nums[a] == nums[a - 1]) continue; // dedupe level 1
for (int b = a + 1; b < n - 2; b++) {
if (b > a + 1 && nums[b] == nums[b - 1]) continue; // dedupe level 2
int left = b + 1, right = n - 1;
while (left < right) {
long sum = (long) nums[a] + nums[b] + nums[left] + nums[right]; // no overflow
if (sum < target) {
left++;
} else if (sum > target) {
right--;
} else {
result.add(Arrays.asList(nums[a], nums[b], nums[left], nums[right]));
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) left++; // level 3
}
}
}
}
return result;
}
Time O(n³)Space O(1) extra (besides sorting)
Two loops (n² pairs) times an O(n) walk. In general, k-Sum this way is O(n^(k−1)).
Every extra number in k-Sum is one more outer loop with its own duplicate skip; the innermost two numbers are always the two-pointer walk.
Overflow: [1000000000, 1000000000, 1000000000, 1000000000] with target -294967296 must return [], but an int sum wraps around to exactly -294967296 and wrongly "finds" it. Cast to long before adding. Also note b > a + 1 (not b > 0) in the second skip.
Pattern notes: Two Pointers · Open on LeetCode
LC 75Sort ColorsMediumThree Pointers (Dutch Flag)LeetCode
The array holds only 0s, 1s and 2s (red, white, blue). Sort it in place so all 0s come first, then 1s, then 2s, without using the library sort. Follow-up: do it in one pass with O(1) extra space.
Example: [2, 0, 2, 1, 1, 0] → [0, 0, 1, 1, 2, 2]. [2, 0, 1] → [0, 1, 2].
"Only three kinds of values", "in place", "one pass": this is partitioning an array into three zones. Beginner's Move Zeroes (LC 283) used two pointers to make two zones; with three zones you need three pointers, one for each boundary plus the reader.
1Simple way — count, then rewrite
Count how many 0s and 1s there are, then overwrite the array: that many 0s, that many 1s, and 2s for the rest.
void sortColors(int[] nums) {
int zeros = 0, ones = 0;
for (int x : nums) { // pass 1: count
if (x == 0) zeros++;
else if (x == 1) ones++;
}
for (int i = 0; i < nums.length; i++) { // pass 2: rewrite
if (i < zeros) nums[i] = 0;
else if (i < zeros + ones) nums[i] = 1;
else nums[i] = 2;
}
}
Time O(n), two passesSpace O(1)
This is already O(n) and fine in practice. But it reads the array twice, and the follow-up explicitly asks for a single pass, which is what interviewers want to see.
2One pass — Dutch national flag (low, mid, high)
Keep three zones: [0, low) is all 0s, [low, mid) is all 1s, (high, end] is all 2s, and [mid, high] is still unknown. Look at nums[mid]: 0 → swap it to low, low++ and mid++; 1 → mid++; 2 → swap it to high, high-- (and do not move mid). Stop when mid passes high.
mid stays put: the value that came back from the right (row 2) has not been looked at yet. After a 0-swap it can move, because what comes back from low is always a 1 (or itself).void sortColors(int[] nums) {
int low = 0, mid = 0, high = nums.length - 1;
while (mid <= high) { // [mid..high] is still unknown
if (nums[mid] == 0) {
swap(nums, low, mid); // 0 goes to the front zone
low++;
mid++;
} else if (nums[mid] == 1) {
mid++; // 1 is already in the middle zone
} else {
swap(nums, mid, high); // 2 goes to the back zone
high--; // do NOT move mid: new nums[mid] is unchecked
}
}
}
void swap(int[] a, int i, int j) {
int t = a[i]; a[i] = a[j]; a[j] = t;
}
Time O(n), one passSpace O(1)
Each step either moves mid forward or high back, so the unknown zone shrinks by one every step: at most n steps.
Three-way partition = low, mid, high with the invariant "0s | 1s | unknown | 2s". The same idea is used in quicksort to group values equal to the pivot.
Two classic bugs: using mid < high instead of mid <= high (the last unknown cell is never checked: [2, 0, 1] stays wrong), and doing mid++ after swapping a 2 (a 0 swapped in from the right gets stuck in the middle).
Pattern notes: Two Pointers · Open on LeetCode
LC 881Boats to Save PeopleMediumGreedy + Two PointersLeetCode
people[i] is a person's weight. Every boat carries at most two people, and their total weight must be ≤ limit. Nobody is heavier than the limit. Return the fewest boats needed to carry everyone.
Example: people = [3, 2, 2, 1], limit = 3 → 3 boats: (1, 2), (2), (3). [3, 5, 3, 4], limit = 5 → 4.
"Minimum number of groups" with "at most two per group" and a weight limit → pair items up greedily. Pairing questions usually become easy after sorting: the heaviest person is the hardest to place, and the lightest person is the best partner for anyone. Two pointers at both ends of the sorted array handle exactly that.
1Brute force — heaviest first, search for the best partner
Sort. Take the heaviest person still waiting, then scan the others (heavier ones first) for someone who still fits in the boat with them. Mark both as done.
int numRescueBoats(int[] people, int limit) {
Arrays.sort(people);
boolean[] done = new boolean[people.length];
int boats = 0;
for (int i = people.length - 1; i >= 0; i--) {
if (done[i]) continue;
done[i] = true; // heaviest waiting person
for (int j = i - 1; j >= 0; j--) { // look for a partner
if (!done[j] && people[i] + people[j] <= limit) {
done[j] = true;
break;
}
}
boats++;
}
return boats;
}
Time O(n²)Space O(n)
It gives the right count, but each boat may scan the whole array, and n is up to 50,000.
2Optimal — lightest + heaviest, two pointers
Sort, put light at the start and heavy at the end. The heaviest person needs a boat now. If the lightest person fits with them, send both; otherwise the heaviest goes alone, because if even the lightest does not fit, nobody does. Every step fills one boat and moves at least one pointer.
int numRescueBoats(int[] people, int limit) {
Arrays.sort(people);
int light = 0, heavy = people.length - 1, boats = 0;
while (light <= heavy) { // <= : one person may be left alone
if (people[light] + people[heavy] <= limit) {
light++; // lightest rides along
}
heavy--; // heaviest always leaves
boats++;
}
return boats;
}
Time O(n log n)Space O(1) extra (besides sorting)
Sorting dominates; the walk itself is O(n). It is optimal: if the heaviest can't share with the lightest, they can't share with anyone, so a solo boat is forced; if they can, pairing them never makes things worse.
Greedy pairing with a limit → sort, then match the biggest remaining item with the smallest. The heaviest item decides; the lightest is the most flexible partner.
Use light <= heavy, not <: with [3, 2, 2, 1], limit 3, the loop must still run when one person is left, or you return 2 instead of 3. And don't try to fit 3 people in a boat: the limit is two per boat, whatever their weight.
Pattern notes: Two Pointers · Open on LeetCode
Sliding Window
A sliding window is a range [left, right] that moves across an array or string. The template: move right one step to add an item, then move left while the window breaks the rule, and record the answer. In Beginner the rule was simple (for example "no repeated letter"). Here the harder part is choosing the rule and what to count: a running sum, letter counts in an int[26], "how many letters must change", or "how many different types are inside". You will also meet the fixed-size window, where both edges always move together.
LC 209Minimum Size Subarray SumMediumSliding WindowLeetCode
You get an array of positive numbers and a target. Find the shortest run of neighbouring numbers whose sum is at least target. Return its length, or 0 if no run is big enough.
Example: target = 7, nums = [2, 3, 1, 2, 4, 3] → 2, because [4, 3] adds up to 7.
"Subarray" (neighbours only) plus "minimum length with sum at least X" plus "all numbers are positive". Positive numbers matter: adding an item can only make the sum bigger, and removing one can only make it smaller. That one-way behaviour is exactly what lets a window grow and shrink safely.
1Brute force — try every starting point
For each start i, keep adding numbers to the right until the sum reaches target. That is the shortest run starting at i, so compare its length with the best so far and move on to the next start.
int minSubArrayLen(int target, int[] nums) {
int best = Integer.MAX_VALUE;
for (int i = 0; i < nums.length; i++) {
int sum = 0;
for (int j = i; j < nums.length; j++) {
sum += nums[j];
if (sum >= target) { best = Math.min(best, j - i + 1); break; }
}
}
return best == Integer.MAX_VALUE ? 0 : best;
}
Time O(n²)Space O(1)
Every new start throws away the sum it just built and adds the same numbers again. With 100,000 numbers that is billions of additions.
2Optimal — grow on the right, shrink on the left
Keep one window and its running sum. Add nums[right]. Then, while the sum is still at least target, record the length and drop nums[left]. Shrinking is where the answer gets smaller. Because all numbers are positive, once the sum drops below target there is no point shrinking further.
int minSubArrayLen(int target, int[] nums) {
int left = 0, sum = 0, best = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right]; // grow: take one more number
while (sum >= target) { // window is good enough
best = Math.min(best, right - left + 1); // record it
sum -= nums[left]; // then try a shorter one
left++;
}
}
return best == Integer.MAX_VALUE ? 0 : best;
}
Time O(n)Space O(1)
Each index enters the window once (right) and leaves once (left), so the nested while still adds up to about 2n steps in total.
For "shortest window that meets a condition", record the answer inside the shrinking loop. For "longest window that obeys a rule", record it after the shrinking loop. Knowing which one you have is half the problem.
This only works because the numbers are positive. With negative numbers, dropping an item could make the sum bigger, and the window logic breaks (that version needs prefix sums and a deque). Also remember to return 0, not Integer.MAX_VALUE, when no window is found.
Pattern notes: Sliding Window · Open on LeetCode
LC 424Longest Repeating Character ReplacementMediumSliding WindowLeetCode
You get a string of capital letters and a number k. You may change at most k letters into any other letter. What is the longest run of one repeated letter you can make?
Example: s = "AABABBA", k = 1 → 4. Change the A in the middle of "BABB" to get "BBBB".
"Longest substring" plus "you may change at most k". "At most k of something bad" inside a contiguous range is the classic sign of a sliding window: grow while you are within budget, shrink when you go over.
1Brute force — every start, extend while it fits
For each start i, extend to the right while counting letters. A window can be made all one letter if length − (count of its most common letter) ≤ k: keep the common letter and change the rest.
int characterReplacement(String s, int k) {
int best = 0;
for (int i = 0; i < s.length(); i++) {
int[] count = new int[26];
int maxCount = 0;
for (int j = i; j < s.length(); j++) {
maxCount = Math.max(maxCount, ++count[s.charAt(j) - 'A']);
if ((j - i + 1) - maxCount <= k) best = Math.max(best, j - i + 1);
else break;
}
}
return best;
}
Time O(n²)Space O(1)
Correct, but every start re-counts letters that the previous start already counted.
2Optimal — one window with a letter count
The rule for a good window is changes needed = window length − count of the most common letter ≤ k. Keep an int[26] count for the window. Add s[right]. If the window now needs more than k changes, drop letters from the left until it fits again. The biggest good window you see is the answer.
int characterReplacement(String s, int k) {
int[] count = new int[26];
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
count[s.charAt(right) - 'A']++;
// too many letters to change? shrink from the left
while ((right - left + 1) - maxOf(count) > k) {
count[s.charAt(left) - 'A']--;
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
int maxOf(int[] count) { // 26 steps, a constant
int m = 0;
for (int c : count) m = Math.max(m, c);
return m;
}
Time O(26·n) = O(n)Space O(1)
Scanning 26 counts is a fixed cost, so the whole thing is linear. Many solutions keep a maxCount variable that never goes down instead; it gives the same answer, but the version above is much easier to explain and trust in an interview.
Turn the story into a number you can check: here "can this window become one letter?" became length − most common ≤ k. Once the rule is a simple inequality, the sliding window template does the rest.
Don't try to decide which letter to keep in advance (only A's, then only B's). The count array handles every letter at once. And the index is c - 'A', not c - 'a': the input is uppercase.
Pattern notes: Sliding Window · Open on LeetCode
LC 567Permutation in StringMediumFixed WindowLeetCode
You get two lowercase strings s1 and s2. Does s2 contain some rearrangement of s1 as a block of neighbouring letters? Return true or false.
Example: s1 = "ab", s2 = "eidbaooo" → true, because "ba" sits inside s2.
"Permutation" or "anagram" means "same letters, same counts, any order", so compare letter counts, not the text. "Substring of s2" with a length that must equal s1.length() means a fixed-size window: it never grows or shrinks, it only slides.
1Brute force — sort every window
Two strings are rearrangements of each other exactly when their sorted versions are equal. So sort s1, then cut out every window of the same length from s2, sort it, and compare.
boolean checkInclusion(String s1, String s2) {
char[] a = s1.toCharArray();
Arrays.sort(a);
String key = new String(a);
for (int i = 0; i + s1.length() <= s2.length(); i++) {
char[] w = s2.substring(i, i + s1.length()).toCharArray();
Arrays.sort(w);
if (new String(w).equals(key)) return true;
}
return false;
}
Time O(n · m log m)Space O(m)
Neighbouring windows share all but one letter, yet we sort each one from scratch.
2Optimal — slide a window of letter counts
Count the letters of s1 in need[26]. Keep win[26] for the current window of s2. When the window slides one step, add the letter that comes in and remove the letter that falls out: two updates instead of a full recount. If the two count arrays are equal, we found a permutation.
s1.boolean checkInclusion(String s1, String s2) {
int m = s1.length();
if (m > s2.length()) return false;
int[] need = new int[26], win = new int[26];
for (int i = 0; i < m; i++) { // first window
need[s1.charAt(i) - 'a']++;
win[s2.charAt(i) - 'a']++;
}
if (Arrays.equals(need, win)) return true;
for (int right = m; right < s2.length(); right++) {
win[s2.charAt(right) - 'a']++; // letter coming in
win[s2.charAt(right - m) - 'a']--; // letter falling out
if (Arrays.equals(need, win)) return true;
}
return false;
}
Time O(26·n) = O(n)Space O(1)
Comparing two arrays of 26 is a fixed cost, so each slide is constant work.
Fixed window: in with right, out with right − size. "Anagram / permutation" = compare two int[26] count arrays.
If s1 is longer than s2, the first loop would read past the end of s2. Return false first. Also don't forget to check the very first window before the sliding loop starts.
Pattern notes: Sliding Window · Open on LeetCode
LC 438Find All Anagrams in a StringMediumFixed WindowLeetCode
You get strings s and p. Return every start index in s where a rearrangement (anagram) of p begins.
Example: s = "cbaebabacd", p = "abc" → [0, 6]. "cba" starts at 0 and "bac" starts at 6.
This is LC 567 again, but instead of "does one exist?" it asks for "all of them". "Anagram" means compare counts, and the answer's length is fixed at p.length(), so it is a fixed-size window that collects every match instead of stopping at the first.
1Brute force — recount every window
For every start, build a fresh count array for the next p.length() letters and compare it with the counts of p.
List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
int[] need = new int[26];
for (char c : p.toCharArray()) need[c - 'a']++;
for (int i = 0; i + p.length() <= s.length(); i++) {
int[] win = new int[26];
for (int j = i; j < i + p.length(); j++) win[s.charAt(j) - 'a']++;
if (Arrays.equals(need, win)) res.add(i);
}
return res;
}
Time O(n · m)Space O(1)
When p is long, recounting m letters for every start is slow.
2Optimal — one sliding count array
Walk right across s. Add s[right]; once the window is too wide, remove s[right − m]. When the window is full (right ≥ m − 1) and matches need, its start is right − m + 1. Add it to the list and keep going.
List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
int m = p.length();
if (m > s.length()) return res;
int[] need = new int[26], win = new int[26];
for (char c : p.toCharArray()) need[c - 'a']++;
for (int right = 0; right < s.length(); right++) {
win[s.charAt(right) - 'a']++; // letter comes in
if (right >= m) win[s.charAt(right - m) - 'a']--; // letter falls out
if (right >= m - 1 && Arrays.equals(need, win)) {
res.add(right - m + 1); // start of this window
}
}
return res;
}
Time O(26·n) = O(n)Space O(1) extra
Writing it as a single loop (instead of "first window, then slide") avoids a separate check for the first window.
"Find all positions" is the same window as "does one exist": just add to a list instead of returning. The start of a window of size m ending at right is right − m + 1.
Only compare once the window is full (right ≥ m − 1). Comparing earlier can never match, but getting the start index wrong by one is the most common bug here.
Pattern notes: Sliding Window · Open on LeetCode
LC 904Fruit Into BasketsMediumSliding WindowLeetCode
A row of trees, and fruits[i] is the type of fruit on tree i. You have two baskets, each holds only one type. Start at any tree and pick one fruit from every tree going right; stop when a fruit fits in neither basket. How many fruits can you pick at most?
In plain words: the longest run that contains at most 2 different values.
Example: fruits = [1, 2, 3, 2, 2] → 4, picking [2, 3, 2, 2].
Once you strip away the story: "longest contiguous run" with "at most 2 distinct values". "Longest subarray with at most K distinct" is a standard sliding window where you track how many of each value are inside, using a HashMap.
1Brute force — every start, stop at the third type
From each start, walk right, putting types into a set. The moment the set has 3 types, stop.
int totalFruit(int[] fruits) {
int best = 0;
for (int i = 0; i < fruits.length; i++) {
Set<Integer> types = new HashSet<>();
for (int j = i; j < fruits.length; j++) {
types.add(fruits[j]);
if (types.size() > 2) break;
best = Math.max(best, j - i + 1);
}
}
return best;
}
Time O(n²)Space O(1)
A long row of just two types makes every start walk almost to the end.
2Optimal — window with a count per type
A set is not enough, because when the left edge moves you need to know if that was the last fruit of its type. So keep a map type → count. Add fruits[right]. While the map has more than 2 keys, decrease the count of fruits[left] and delete the key when it hits 0. The map size is then the number of types in the window.
int totalFruit(int[] fruits) {
Map<Integer, Integer> count = new HashMap<>(); // type -> how many in window
int left = 0, best = 0;
for (int right = 0; right < fruits.length; right++) {
count.merge(fruits[right], 1, Integer::sum);
while (count.size() > 2) { // three types: too many
int f = fruits[left];
count.put(f, count.get(f) - 1);
if (count.get(f) == 0) count.remove(f); // that type is gone
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
Time O(n)Space O(1) (at most 3 keys)
The map never holds more than 3 types, and each tree enters and leaves the window once.
"Longest subarray with at most K distinct values" → window + count map, shrink while map.size() > K. Change 2 to K and the same code solves the general problem.
You must remove a key when its count reaches 0. A key with count 0 still counts in map.size(), so the window would keep shrinking for no reason and give a too-small answer.
Pattern notes: Sliding Window · Open on LeetCode
LC 1004Max Consecutive Ones IIIMediumSliding WindowLeetCode
You get an array of 0s and 1s and a number k. You may flip at most k zeros into ones. What is the longest run of ones you can make?
Example: nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2 → 6. Flip the zeros at index 4 and 5 to get six ones from index 4 to 9.
"Longest consecutive" plus "at most k flips". Reword it: "longest window that contains at most k zeros". Flipping is a distraction: you never actually change the array, you just count zeros in the window. Same budget idea as LC 424.
1Brute force — every start, count zeros
From each start, walk right and count zeros. Stop when the count goes over k.
int longestOnes(int[] nums, int k) {
int best = 0;
for (int i = 0; i < nums.length; i++) {
int zeros = 0;
for (int j = i; j < nums.length; j++) {
if (nums[j] == 0) zeros++;
if (zeros > k) break;
best = Math.max(best, j - i + 1);
}
}
return best;
}
Time O(n²)Space O(1)
Again, each new start forgets everything the previous one counted.
2Optimal — window with at most k zeros
Count zeros inside the window. Add nums[right]. While zeros > k, move left; if the item leaving is a 0, the count drops. After that the window is valid, so its length is a candidate answer.
int longestOnes(int[] nums, int k) {
int left = 0, zeros = 0, best = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] == 0) zeros++;
while (zeros > k) { // over budget
if (nums[left] == 0) zeros--; // a zero leaves the window
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
Time O(n)Space O(1)
This is the shortest sliding window you will write, and it is the template for every "at most k bad items" problem.
"You may change / flip / delete up to k things" usually means "the window may contain up to k bad things". Count the bad things, shrink when there are too many.
With k = 0 and all zeros, the window becomes empty (left = right + 1) and the length is 0. That is correct, so don't add special cases that break it.
Pattern notes: Sliding Window · Open on LeetCode
Linked List: Fast & Slow, Reversal
Linked list problems are all about moving pointers without losing nodes. The key patterns: fast & slow pointers (one moves 2 steps, one moves 1) to find cycles and middles, a gap of n between two pointers to find "n-th from the end", in-place reversal of part of a list, and a dummy node in front of the head so the first node is not a special case. In Beginner you reversed a whole list and detected a cycle; here you must find where a cycle starts, reverse only a middle piece, and combine several of these moves in one problem.
LC 142Linked List Cycle IIMediumFast & SlowLeetCode
A linked list may loop back on itself. If it does, return the node where the loop starts. If there is no loop, return null. Don't change the list.
Example: 3 → 2 → 0 → −4, and −4 points back to 2 → return the node 2 (index 1).
"Cycle" in a linked list is the signature of Floyd's fast & slow pointers (Beginner LC 141). The words "where the cycle begins" plus "O(1) memory" (the follow-up) mean you need the second phase of Floyd's algorithm, not just "do they meet?".
1Brute force — remember every node you visit
Walk the list and put each node (not its value) into a HashSet. The first node you see twice is where the loop starts. If you reach null, there is no loop.
ListNode detectCycle(ListNode head) {
Set<ListNode> seen = new HashSet<>();
ListNode cur = head;
while (cur != null) {
if (!seen.add(cur)) return cur; // add() returns false if already there
cur = cur.next;
}
return null;
}
Time O(n)Space O(n)
Fast and simple, but it stores every node. The follow-up asks for O(1) extra memory.
2Optimal — Floyd's algorithm, two phases
Phase 1: slow moves 1 step, fast moves 2. If there is a loop they meet inside it. Phase 2: put a new pointer at the head and move it and slow one step each. They meet exactly at the start of the loop.
Why? Say the loop starts a steps from the head, and they met b steps into the loop. Fast walked twice as far as slow, and the extra distance is a whole number of laps. That works out to a = (a whole number of laps) − b. So walking a more steps from the meeting point finishes the lap and lands on the loop start, which is the same place a pointer from the head reaches after a steps.
ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // phase 1 done: inside the loop
ListNode p = head;
while (p != slow) { // phase 2: same speed now
p = p.next;
slow = slow.next;
}
return p; // loop start
}
}
return null; // fast hit the end: no loop
}
Time O(n)Space O(1)
Two pointers and no extra memory, and each phase takes at most about n steps.
Floyd in two lines: meet with speeds 1 and 2, then restart one pointer at the head and move both at speed 1. Where they meet again is the loop start.
Compare nodes with == (same object), never by val: two different nodes can hold the same value. And the loop condition must check both fast != null and fast.next != null, or fast.next.next crashes on a list with no loop.
Pattern notes: Fast & Slow Pointers · Open on LeetCode
LC 287Find the Duplicate NumberMediumFast & SlowLeetCode
An array has n + 1 numbers, each between 1 and n. So at least one number repeats, and here exactly one value is repeated (maybe many times). Find it without changing the array and using only O(1) extra memory.
Example: nums = [1, 3, 4, 2, 2] → 2.
"Values are in the range 1..n" means every value is also a valid index. So you can treat i → nums[i] as a "next" pointer and the array becomes a linked list. "Duplicate" means two cells point to the same place: that is a cycle entrance. "Don't modify, O(1) space" rules out sorting and HashSets, which leaves Floyd.
1Brute force — HashSet
Walk the array and add each value to a set. The first value that is already there is the duplicate.
int findDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int x : nums) {
if (!seen.add(x)) return x;
}
return -1;
}
Time O(n)Space O(n)
It's fast, but it breaks the O(1) memory rule. Sorting would break the "don't modify" rule.
2Optimal — the array is a linked list with a cycle
Start at index 0 and keep jumping i = nums[i]. No value is 0, so nothing ever jumps back to index 0: it works like the head. The duplicate value has two arrows coming in, so it is exactly where the path enters its loop. Run LC 142 on indices: "next" is nums[i], and the loop start is the answer.
int findDuplicate(int[] nums) {
int slow = 0, fast = 0; // index 0 is the "head"
do {
slow = nums[slow]; // 1 step
fast = nums[nums[fast]]; // 2 steps
} while (slow != fast);
int p = 0; // phase 2: restart from the head
while (p != slow) {
p = nums[p];
slow = nums[slow];
}
return p; // loop start = duplicate value
}
Time O(n)Space O(1)
Same code shape as LC 142, with node.next replaced by nums[i]. The array is only read, never changed.
When values are valid indices, an array can be a linked list: i → nums[i]. Many "find the duplicate / missing number" problems use this view.
Use do … while (or move once before the loop). A plain while (slow != fast) never runs, because both start at 0 and are already equal.
Pattern notes: Fast & Slow Pointers · Open on LeetCode
LC 19Remove Nth Node From End of ListMediumTwo PointersLeetCode
Remove the n-th node counting from the end of a linked list and return the head. n is always valid.
Example: 1 → 2 → 3 → 4 → 5, n = 2 → 1 → 2 → 3 → 5 (the 4 was second from the end).
"From the end" in a singly linked list, where you can only walk forward. The trick for "k-th from the end" is two pointers with a fixed gap: when the front one reaches the end, the back one is exactly k from the end. "Remove" a node, maybe the head, means use a dummy node.
1Brute force — count first, then walk again
Walk once to count the length len. The node to remove is at position len − n from the start, so walk again to the node just before it and skip it.
ListNode removeNthFromEnd(ListNode head, int n) {
int len = 0;
for (ListNode c = head; c != null; c = c.next) len++;
ListNode dummy = new ListNode(0, head), prev = dummy;
for (int i = 0; i < len - n; i++) prev = prev.next;
prev.next = prev.next.next; // skip the target
return dummy.next;
}
Time O(n)Space O(1)
This is already O(n) and fine in an interview. The follow-up asks: can you do it in one pass?
2One pass — two pointers with a gap
Start both pointers at a dummy node. Move fast ahead n + 1 steps. Now move both together until fast is null. The gap never changes, so slow stops right before the node to remove, which is exactly where you need to be to unlink it.
ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head); // makes removing the head easy
ListNode fast = dummy, slow = dummy;
for (int i = 0; i <= n; i++) fast = fast.next; // n + 1 steps ahead
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // unlink the n-th from the end
return dummy.next;
}
Time O(n)Space O(1)
Same big-O as step 1, but only one walk through the list, which is what the interviewer is looking for.
"k-th from the end" → give one pointer a head start of k, then move both. Use a dummy node whenever the head itself might be removed or changed.
Remove the head ([1, 2, 3], n = 3) and without a dummy you'd need a special case. Also, return dummy.next, not head: if the head was removed, head still points at the deleted node.
Pattern notes: Linked Lists · Open on LeetCode
LC 24Swap Nodes in PairsMediumPointer RewiringLeetCode
Swap every two neighbouring nodes and return the new head. You must move the nodes themselves, not just swap the values inside them.
Example: 1 → 2 → 3 → 4 → 2 → 1 → 4 → 3. With an odd length the last node stays: 1 → 2 → 3 → 2 → 1 → 3.
"Swap nodes" and "don't change the values" means pure pointer rewiring. The head changes (it becomes node 2), which is the classic signal for a dummy node and a prev pointer that sits just before the pair you are working on.
1Brute force — put the nodes in a list, swap, relink
Copy the node references into an ArrayList, swap positions 0-1, 2-3, and so on, then link them up again in the new order.
ListNode swapPairs(ListNode head) {
List<ListNode> nodes = new ArrayList<>();
for (ListNode c = head; c != null; c = c.next) nodes.add(c);
for (int i = 0; i + 1 < nodes.size(); i += 2) Collections.swap(nodes, i, i + 1);
ListNode dummy = new ListNode(0), tail = dummy;
for (ListNode node : nodes) { tail.next = node; tail = node; }
tail.next = null; // old links could make a loop
return dummy.next;
}
Time O(n)Space O(n)
Easy to get right, but it needs an extra list of every node. We can rewire in place.
2Optimal — rewire each pair in place
prev sits before the pair a → b. Three assignments swap them: a.next = b.next, b.next = a, prev.next = b. Now the order is prev → b → a, and a is the node before the next pair, so prev = a.
ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy;
while (prev.next != null && prev.next.next != null) { // a full pair exists
ListNode a = prev.next, b = a.next;
a.next = b.next; // a now points past the pair
b.next = a; // b points back to a
prev.next = b; // the node before the pair points to b
prev = a; // a is now the second of the pair
}
return dummy.next;
}
Time O(n)Space O(1)
The order of the three assignments matters: a.next = b.next goes first, so we save the rest of the list before b.next is overwritten.
For pointer rewiring, name every node you will touch (prev, a, b) before changing anything. Then write the assignments so you never lose the rest of the list.
If you write b.next = a first, the link to node 3 is gone forever. Draw the boxes and arrows on paper and cross out each arrow as you change it.
Pattern notes: Linked Lists · Open on LeetCode
LC 92Reverse Linked List IIMediumIn-place ReversalLeetCode
Reverse only the part of the list from position left to position right (counting from 1). Everything before and after stays in place.
Example: 1 → 2 → 3 → 4 → 5, left = 2, right = 4 → 1 → 4 → 3 → 2 → 5.
"Reverse" plus "from position left to right" means in-place reversal (Beginner LC 206), but only for a piece of the list. The tricky part is reconnecting the reversed piece to the nodes around it, and left = 1 changes the head, so use a dummy node.
1Brute force — copy values, reverse, write back
Copy all values into a list, reverse the part from left − 1 to right − 1, and write the values back into the nodes in order.
ListNode reverseBetween(ListNode head, int left, int right) {
List<Integer> vals = new ArrayList<>();
for (ListNode c = head; c != null; c = c.next) vals.add(c.val);
Collections.reverse(vals.subList(left - 1, right));
ListNode c = head;
for (int v : vals) { c.val = v; c = c.next; }
return head;
}
Time O(n)Space O(n)
It passes, but it uses extra memory and swaps values, not nodes. Interviewers want the links changed in one pass.
2Optimal — move nodes to the front of the range, one by one
Walk to before, the node just before position left. Call the first node of the range first. Now repeat right − left times: take the node right after first and move it to just after before. Each move puts one more node at the front of the range, so the range ends up reversed, and first slides to the end by itself.
ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0, head);
ListNode before = dummy;
for (int i = 1; i < left; i++) before = before.next; // node before the range
ListNode first = before.next; // will end up last
for (int i = 0; i < right - left; i++) {
ListNode move = first.next; // node to move to the front
first.next = move.next; // take it out
move.next = before.next; // point it at the current front
before.next = move; // it becomes the new front
}
return dummy.next;
}
Time O(n)Space O(1)
One pass, no extra memory, and the pieces before and after the range never come loose, so there is nothing to reconnect at the end.
To reverse a piece of a list: keep the node before the piece fixed and keep moving the next node to the front. It is "head insertion", and it avoids the fiddly reconnect step of the classic three-pointer reversal.
Positions start at 1, not 0: the loop to find before runs left − 1 times from the dummy. And when left = 1, before is the dummy itself, so always return dummy.next.
Pattern notes: In-place Linked List Reversal · Open on LeetCode
LC 143Reorder ListMediumMiddle + Reverse + MergeLeetCode
Reorder the list L0 → L1 → … → Ln into L0 → Ln → L1 → Ln−1 → L2 → …: first, last, second, second-last, and so on. Change the links, not the values. Return nothing.
Example: 1 → 2 → 3 → 4 → 5 → 1 → 5 → 2 → 4 → 3.
The answer takes nodes from the front and the back in turn, but a singly linked list can't walk backwards. So flip the back half to make it walkable from the end. This problem is really three Beginner problems in a row: middle of the list (LC 876), reverse a list (LC 206), and merge two lists (LC 21).
1Brute force — put the nodes in an array
An array can be read from both ends. Store every node, then use two indexes i (front) and j (back) to link front, back, front, back.
void reorderList(ListNode head) {
List<ListNode> nodes = new ArrayList<>();
for (ListNode c = head; c != null; c = c.next) nodes.add(c);
int i = 0, j = nodes.size() - 1;
while (i < j) {
nodes.get(i).next = nodes.get(j);
i++;
if (i == j) break;
nodes.get(j).next = nodes.get(i);
j--;
}
if (head != null) nodes.get(i).next = null; // end the list
}
Time O(n)Space O(n)
Clear and correct, but it needs an array as big as the list. The follow-up wants O(1) extra memory.
2Optimal — find middle, reverse back half, merge
1. Fast & slow pointers find the middle. 2. Cut the list there and reverse the second half, so its first node is now the old last node. 3. Merge the two halves by taking one node from each in turn. The front half gives L0, L1, …; the reversed half gives Ln, Ln−1, ….
void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// 1. middle: slow stops at the end of the first half
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. cut and reverse the second half
ListNode prev = null, cur = slow.next;
slow.next = null;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
// 3. merge: one from the front half, one from the back half
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;
}
}
Time O(n)Space O(1)
Each step is one pass over at most n nodes, and only a handful of pointers are used.
When you need a list from both ends, split it in the middle and reverse the back half. The same trick checks if a list is a palindrome (LC 234).
Don't forget slow.next = null to cut the list. Without it, the first half still points into the (now reversed) second half, and you create a loop that never ends.
Pattern notes: In-place Linked List Reversal · Open on LeetCode
LC 2Add Two NumbersMediumCarry + Dummy NodeLeetCode
Two numbers are stored as linked lists, one digit per node, in reverse order (the ones digit comes first). Add them and return the sum as a list in the same format.
Example: l1 = 2 → 4 → 3 (342), l2 = 5 → 6 → 4 (465) → 7 → 0 → 8 (807).
"Digits stored in reverse order" is a gift: the head is the ones digit, which is exactly where school addition starts. So this is column addition with a carry, walking both lists together. "Return a new list" means build it with a dummy head and a tail pointer.
1Brute force — turn the lists into numbers
Read each list into a real number, add, then write the digits back as a list. The lists can be up to 100 digits long, which overflows long, so you would need BigInteger.
ListNode addTwoNumbers(ListNode l1, ListNode l2) {
BigInteger sum = toNumber(l1).add(toNumber(l2));
String digits = sum.toString();
ListNode dummy = new ListNode(0), tail = dummy;
for (int i = digits.length() - 1; i >= 0; i--) { // ones digit first
tail.next = new ListNode(digits.charAt(i) - '0');
tail = tail.next;
}
return dummy.next;
}
BigInteger toNumber(ListNode l) {
StringBuilder sb = new StringBuilder();
for (ListNode c = l; c != null; c = c.next) sb.append(c.val);
return new BigInteger(sb.reverse().toString());
}
Time O(n)Space O(n) extra
It works, but it hides the whole point behind a library. With plain int or long it simply overflows, which is the trap most people fall into first.
2Optimal — add digit by digit with a carry
Walk both lists at the same time, like adding on paper. At each column: sum = carry + digit1 + digit2, write sum % 10, and carry sum / 10 into the next column. Keep going while either list has digits or the carry is not 0.
ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0), tail = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int sum = carry;
if (l1 != null) { sum += l1.val; l1 = l1.next; } // missing digit = 0
if (l2 != null) { sum += l2.val; l2 = l2.next; }
tail.next = new ListNode(sum % 10); // write this digit
tail = tail.next;
carry = sum / 10; // 0 or 1
}
return dummy.next;
}
Time O(max(m, n))Space O(1) extra
The only memory is the answer list itself, which you have to build anyway. Lists of different lengths are handled by treating a missing digit as 0.
To build a new list, use dummy + tail: tail.next = new node; tail = tail.next, and return dummy.next. No special case for the first node.
The last carry: 5 + 5 must give 0 → 1. If the loop is only while (l1 != null || l2 != null), the final 1 is lost. That is why carry != 0 is part of the condition.
Pattern notes: Linked Lists · Open on LeetCode
LC 138Copy List with Random PointerMediumHash Map / InterleaveLeetCode
Each node has a next pointer and also a random pointer that can point to any node in the list, or null. Make a deep copy: brand-new nodes, whose next and random point only to other new nodes, in the same pattern.
Example: [[7,null],[13,0],[11,4],[10,2],[1,0]]: each pair is (value, index that random points to). The copy must look exactly the same, but share no nodes with the original.
"Deep copy" with pointers that can jump anywhere (even forward, to nodes not copied yet). The core question is: "given an old node, which new node is its copy?" A question of the form "given X, find its partner" is a HashMap. The O(1)-space version stores that link inside the list itself.
1First idea — map every old node to its copy
Pass 1: create a copy of every node and store old → new in a HashMap. Pass 2: for each old node, set copy.next = map.get(old.next) and copy.random = map.get(old.random). Every copy already exists, so forward pointers are no problem.
Node copyRandomList(Node head) {
Map<Node, Node> copy = new HashMap<>();
for (Node c = head; c != null; c = c.next) copy.put(c, new Node(c.val));
for (Node c = head; c != null; c = c.next) {
copy.get(c).next = copy.get(c.next); // get(null) returns null
copy.get(c).random = copy.get(c.random);
}
return copy.get(head);
}
Time O(n)Space O(n) extra
This is a perfectly good answer and the one to write first. The follow-up: can you do it without the map?
2Optimal — weave the copies into the list
Put each copy right after its original: A → A' → B → B' → …. Now the "map" is free: the copy of any node X is just X.next. So X'.random = X.random.next. Finally unweave the two lists, restoring the original.
X.random.next.Node copyRandomList(Node head) {
if (head == null) return null;
// 1. weave: A -> A' -> B -> B' -> ...
for (Node c = head; c != null; c = c.next.next) {
Node twin = new Node(c.val);
twin.next = c.next;
c.next = twin;
}
// 2. set randoms: the copy of c.random is c.random.next
for (Node c = head; c != null; c = c.next.next) {
if (c.random != null) c.next.random = c.random.next;
}
// 3. unweave into two separate lists
Node newHead = head.next;
for (Node c = head; c != null; c = c.next) {
Node twin = c.next;
c.next = twin.next; // restore the original
if (twin.next != null) twin.next = twin.next.next; // link the copies
}
return newHead;
}
Time O(n)Space O(1) extra
Three passes, no map. The only new nodes are the copies the problem asks for.
"Copy a structure with extra pointers" → old-to-new HashMap (works for graphs too, see LC 133 Clone Graph). For lists, weaving copies in removes the map.
Set the randoms in a separate pass before you unweave. If you split the lists while setting randoms, c.random.next may already point to an original node instead of a copy. Also, the original list must be left as it was.
Pattern notes: Hashing · Open on LeetCode
Stack & Monotonic Stack
A stack remembers things you can't finish yet and hands back the most recent one first. Two patterns here: the plain stack for nested or "last one opened, first one closed" work (expressions, brackets inside brackets), and the monotonic stack: keep a stack of items still waiting for an answer; each newcomer pops and answers every item it beats, then waits itself. In Beginner you pushed and popped single values; here the stack holds whole states (a half-built string, an index, a price with its span), wraps around a circular array, or works on times you compute first.
LC 150Evaluate Reverse Polish NotationMediumStackLeetCode
You get a maths expression written in Reverse Polish Notation: the operator comes after its two numbers. So 2 1 + means 2 + 1. The only operators are + - * /, and division cuts off the decimals (it rounds toward zero). Return the value.
Example: ["2", "1", "+", "3", "*"] → 9, because it means (2 + 1) * 3.
"An operator works on the two most recent values" is a stack in disguise. Any time a symbol has to reach back to the last thing(s) you saw, and its result becomes a new "last thing", think stack. Here there are no brackets and no precedence rules: the order of the tokens already tells you what to do.
1Brute force — find an operator, shrink the list, repeat
Put the tokens in a list. Find the first operator; the two tokens just before it are its numbers. Replace those three tokens with the result. Repeat until one number is left.
int evalRPN(String[] tokens) {
List<String> list = new ArrayList<>(Arrays.asList(tokens));
while (list.size() > 1) {
int i = 0;
while (!isOp(list.get(i))) i++; // first operator
int a = Integer.parseInt(list.get(i - 2));
int b = Integer.parseInt(list.get(i - 1));
int r = apply(a, b, list.get(i));
list.remove(i); // remove the operator
list.remove(i - 1); // remove b
list.set(i - 2, String.valueOf(r)); // a becomes the result
}
return Integer.parseInt(list.get(0));
}
boolean isOp(String t) {
return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/");
}
int apply(int a, int b, String op) {
switch (op) {
case "+": return a + b;
case "-": return a - b;
case "*": return a * b;
default: return a / b;
}
}
Time O(n²)Space O(n)
Every round rescans the list from the start and removing from the middle of an ArrayList shifts everything after it. The scan keeps going back over numbers it has already passed.
2Optimal — one pass with a stack
Read left to right. A number is not ready to use yet, so push it. An operator needs the two newest numbers, which are exactly the top two of the stack: pop b, pop a, push a op b. The result goes back on the stack because a later operator may need it. At the end the stack holds one number: the answer.
["4", "13", "5", "/", "+"] → 6. Each operator eats the top two numbers and pushes its result back.int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String t : tokens) {
if (t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/")) {
int b = stack.pop(); // top = the SECOND number
int a = stack.pop(); // below it = the FIRST number
if (t.equals("+")) stack.push(a + b);
else if (t.equals("-")) stack.push(a - b);
else if (t.equals("*")) stack.push(a * b);
else stack.push(a / b); // Java int division truncates toward zero
} else {
stack.push(Integer.parseInt(t)); // handles negatives like "-11"
}
}
return stack.pop();
}
Time O(n)Space O(n)
Each token is pushed or popped a fixed number of times, so one pass is enough.
"Combine the most recent things, and the result becomes a new recent thing" → stack. The same idea evaluates normal expressions with brackets (LC 224, LC 227), just with more rules.
The first pop is the right-hand number. Pop b first, then a, and compute a - b / a / b. Getting it backwards gives 5 / 13 = 0 instead of 2. Also, don't test "is it an operator" with t.charAt(0): "-11" starts with a minus but is a number.
Pattern notes: Stacks · Open on LeetCode
LC 394Decode StringMediumStack (nested state)LeetCode
A string is encoded like k[text], which means "write text k times". Brackets can be inside other brackets. Return the fully expanded string.
Example: "3[a2[c]]" → "accaccacc". The inside 2[c] becomes cc, so we get 3[acc], which is acc three times.
Brackets that can be nested are the classic stack signal (you met it in Valid Parentheses). The inner part must be finished before the outer part can use it, and the last [ opened is the first one closed. The new part: the stack must remember what you were building before the bracket opened, not just the bracket itself.
1Brute force — expand the innermost bracket, repeat
The first ] in the string always closes an innermost bracket (nothing nested inside it). Find it, find its [ and the number before it, replace k[inner] with inner repeated k times, and start over until no brackets are left.
String decodeString(String s) {
while (s.contains("[")) {
int close = s.indexOf(']'); // first ']' = an innermost pair
int open = s.lastIndexOf('[', close);
int numStart = open;
while (numStart > 0 && Character.isDigit(s.charAt(numStart - 1))) numStart--;
int k = Integer.parseInt(s.substring(numStart, open));
String inner = s.substring(open + 1, close);
s = s.substring(0, numStart) + inner.repeat(k) + s.substring(close + 1);
}
return s;
}
Time O(L · B)Space O(L)
L is the length of the output and B the number of bracket pairs. Every pair rebuilds the whole string, so text outside the bracket is copied again and again.
2Optimal — one pass, stack the unfinished work
Keep a cur string you are building and a number k. On [, you are about to start something new, so push (k, cur) and start a fresh cur. On ], the inside is finished: pop the saved string and the count, and set cur = saved + cur × count. It's like pausing a task: you write down where you were (push), do the sub-task, then pick the old task back up (pop) with the sub-task's result glued on.
String decodeString(String s) {
Deque<Integer> counts = new ArrayDeque<>(); // repeat count for each open '['
Deque<StringBuilder> saved = new ArrayDeque<>(); // what we had built before that '['
StringBuilder cur = new StringBuilder();
int k = 0;
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) {
k = k * 10 + (c - '0'); // numbers can have many digits: "10[a]"
} else if (c == '[') {
counts.push(k); // pause: remember the count ...
saved.push(cur); // ... and the unfinished string
cur = new StringBuilder(); // start fresh for the inside
k = 0;
} else if (c == ']') {
String inner = cur.toString();
cur = saved.pop(); // resume the outer string
cur.append(inner.repeat(counts.pop()));
} else {
cur.append(c); // a normal letter
}
}
return cur.toString();
}
Time O(L)Space O(L)
Every character of the input is read once; the work beyond that is just building the output (length L), which any solution must do.
Nested structure + "finish the inside first" → push your whole current state when you go one level deeper and pop it when you come back. This is exactly what recursion does with the call stack; a recursive solution is equally fine.
Counts can have more than one digit ("10[a]"), so build k with k * 10 + digit, and reset k to 0 after pushing it. Reading only one digit turns 10[a] into nonsense.
Pattern notes: Stacks · Open on LeetCode
LC 739Daily TemperaturesMediumMonotonic StackLeetCode
You get the temperature for each day. For every day, answer: how many days do I wait until a warmer day? If no warmer day ever comes, answer 0.
Example: [73, 74, 75, 71, 69, 72, 76, 73] → [1, 1, 4, 2, 1, 1, 0, 0]. From 75 (day 2) you wait until 76 (day 6): 4 days.
"For each element, find the next element to its right that is bigger" is word for word the Next Greater Element problem from Beginner (LC 496). The only change is that the answer is a distance, so the stack must hold indices, not values.
1Brute force — for each day, look ahead
For each day, walk forward until you find a warmer day and record how far you walked.
int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n]; // default 0 = never warmer
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (temperatures[j] > temperatures[i]) { ans[i] = j - i; break; }
}
}
return ans;
}
Time O(n²)Space O(1)
With up to 100,000 days, a falling run of temperatures makes every day scan to the end: billions of steps.
2Optimal — a stack of days still waiting
Walk the days once. Keep a stack of indices of days that haven't found a warmer day yet. Their temperatures are always falling from bottom to top (a warmer day would already have answered the colder ones). When today is warmer than the day on top, today is that day's answer: pop it and write i − j. Keep popping while today is warmer, then push today to wait.
int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n]; // days never popped keep 0
Deque<Integer> stack = new ArrayDeque<>(); // INDICES of days still waiting
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int j = stack.pop(); // day j finally found a warmer day
ans[j] = i - j;
}
stack.push(i); // today waits for its own warmer day
}
return ans;
}
Time O(n)Space O(n)
The while looks like a nested loop, but each index is pushed once and popped at most once, so all the pops together cost O(n).
Need a distance or a position as the answer? Store indices in the monotonic stack. You can always look up the value with arr[index], but you can't get the index back from a value.
Use strict <: an equal temperature is not warmer. With <=, [70, 70] would answer 1 for the first day instead of 0.
Pattern notes: Monotonic Stack · Open on LeetCode
LC 503Next Greater Element IIMediumMonotonic Stack (circular)LeetCode
Same as Next Greater Element, but the array is circular: after the last element you continue from the first one. For each number, return the first bigger number you meet going right (and wrapping around), or -1 if there is none. Numbers can repeat.
Example: [1, 2, 3, 4, 3] → [2, 3, 4, -1, 4]. The last 3 wraps around and meets 1, 2, 3, then 4.
"Next greater" again means monotonic stack. The new word is circular: the answer for an element near the end may sit at the start. The standard trick for circular arrays is to pretend the array is written twice and use i % n, instead of actually copying it.
1Brute force — for each number, walk one full lap
For each index, look at the next n − 1 positions using (i + k) % n to wrap, and stop at the first bigger one.
int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
for (int i = 0; i < n; i++) {
res[i] = -1;
for (int k = 1; k < n; k++) {
int j = (i + k) % n; // wrap around the end
if (nums[j] > nums[i]) { res[i] = nums[j]; break; }
}
}
return res;
}
Time O(n²)Space O(1)
The maximum element (and any value equal to it) has no answer, so its search always runs the full lap. Many such searches mean O(n²).
2Optimal — monotonic stack over two laps
Run the normal next-greater stack, but loop i from 0 to 2n − 1 and read nums[i % n]. Lap 1 pushes and answers as usual; lap 2 only pops: it gives the numbers still waiting at the end a chance to find a bigger number at the start. Anything still waiting after two laps has no bigger number at all, so it stays -1.
nums[i % n]. It exists only to answer numbers that are still waiting at the end.int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Arrays.fill(res, -1); // "no bigger number" by default
Deque<Integer> stack = new ArrayDeque<>(); // indices still waiting
for (int i = 0; i < 2 * n; i++) {
int num = nums[i % n]; // wrap around on lap 2
while (!stack.isEmpty() && nums[stack.peek()] < num) {
res[stack.pop()] = num;
}
if (i < n) stack.push(i); // only real positions wait for answers
}
return res;
}
Time O(n)Space O(n)
Two laps is 2n steps, and each index is pushed once and popped at most once.
Circular array → loop to 2n and use i % n. It turns "wrap around" into an ordinary left-to-right scan without copying the array.
Store indices, not values. Numbers repeat here, so a map from value to answer (the LC 496 trick) breaks: two 3s can have different answers. And pre-fill res with -1, since the default 0 would be a wrong answer.
Pattern notes: Monotonic Stack · Open on LeetCode
LC 901Online Stock SpanMediumMonotonic StackLeetCode
Prices arrive one day at a time. Build a class with next(price) that returns today's span: how many days in a row, ending today and going back, the price was less than or equal to today's price. Today itself counts.
Example: prices 100, 80, 60, 70, 60, 75, 85 → spans 1, 1, 1, 2, 1, 4, 6. For 75, the days 75, 60, 70, 60 are all ≤ 75, and 80 stops it: span 4.
"How far back until a bigger value" is the previous greater element: the mirror image of next greater. The word online (data arrives one at a time and you must answer right away) tells you to keep a structure between calls, not rescan the history each time.
1Brute force — store every price, walk back each time
Keep all prices in a list. On each call, walk backwards from today while the prices are ≤ today's price, counting.
class StockSpanner {
private final List<Integer> prices = new ArrayList<>();
public StockSpanner() { }
public int next(int price) {
prices.add(price);
int span = 0;
for (int i = prices.size() - 1; i >= 0 && prices.get(i) <= price; i--) span++;
return span;
}
}
Time O(n) per callSpace O(n)
With rising prices, every call walks back over the whole history: O(n²) for n calls.
2Optimal — stack of (price, span) pairs that swallow smaller ones
If today's price is ≥ an earlier price, that earlier day can never stop a future walk back: today blocks the view first. So pop it, and add its span to today's span (its span already counts the days it had swallowed). The stack keeps only prices that are still "walls", in decreasing order, each with how many days it covers. Walking back becomes jumping from wall to wall.
class StockSpanner {
private final Deque<int[]> stack = new ArrayDeque<>(); // {price, span}, prices decreasing
public StockSpanner() { }
public int next(int price) {
int span = 1; // today counts
while (!stack.isEmpty() && stack.peek()[0] <= price) {
span += stack.pop()[1]; // take over the days it covered
}
stack.push(new int[]{price, span});
return span;
}
}
Time O(1) amortised per callSpace O(n)
One call can pop a lot, but each price is pushed once and popped at most once over all calls, so n calls cost O(n) in total.
When you pop in a monotonic stack, you can carry information forward (here, a count). Storing a pair (value, extra) on the stack is a common upgrade that turns "walk back" into "jump back".
Pop with <=, not <: equal prices count toward the span. And start span at 1, because today is part of its own span.
Pattern notes: Monotonic Stack · Open on LeetCode
LC 853Car FleetMediumSort + Monotonic StackLeetCode
Cars drive on a one-lane road toward target. Each car has a starting position and a speed. A car can't pass the car in front: if it catches up, it slows down and they drive together as one fleet. A car that catches up exactly at the target also joins that fleet. How many fleets arrive?
Example: target = 12, position = [10, 8, 0, 5, 3], speed = [2, 4, 1, 1, 3] → 3. Cars at 10 and 8 meet at 12; cars at 5 and 3 meet at 6; car at 0 arrives alone.
"Can't pass" means only the order on the road matters, so sort by position. Then each car only cares about the car group directly in front of it: a "compare with the most recent survivor" rule, which is what a stack does. Turning positions and speeds into one number, the arrival time, is the step that makes it simple.
1Brute force — does anyone ahead arrive later than me?
Work out each car's time to target if it drove alone: (target − position) / speed. A car ends up stuck behind someone exactly when some car ahead of it would arrive at the same time or later. Count the cars that nobody ahead slows down: each one leads its own fleet.
int carFleet(int target, int[] position, int[] speed) {
int n = position.length, fleets = 0;
for (int i = 0; i < n; i++) {
double ti = (double) (target - position[i]) / speed[i];
boolean caught = false;
for (int j = 0; j < n; j++) {
double tj = (double) (target - position[j]) / speed[j];
if (position[j] > position[i] && tj >= ti) { caught = true; break; }
}
if (!caught) fleets++; // nobody ahead holds car i back
}
return fleets;
}
Time O(n²)Space O(1)
Each car checks every other car. With 100,000 cars that is 10 billion checks.
2Optimal — sort from closest to target, stack of fleet times
Process cars from the one closest to the target backwards. Keep a stack of arrival times of the fleets found so far; the top is the fleet directly ahead. If the current car's time is ≤ the top, it catches that fleet and joins it (don't push). If it is bigger, it is too slow to catch anyone, so it starts a new fleet: push its time. The stack only ever grows with bigger times, so it is monotonic. The answer is the stack size.
int carFleet(int target, int[] position, int[] speed) {
int n = position.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; i++) idx[i] = i;
Arrays.sort(idx, (x, y) -> position[y] - position[x]); // closest to target first
Deque<Double> stack = new ArrayDeque<>(); // arrival time of each fleet
for (int i : idx) {
double time = (double) (target - position[i]) / speed[i];
if (stack.isEmpty() || time > stack.peek()) {
stack.push(time); // too slow to catch the fleet ahead: new fleet
} // else: catches up and joins, arrives at stack.peek()
}
return stack.size();
}
Time O(n log n)Space O(n)
Sorting dominates; the scan itself is one pass. The joined car takes the slower fleet's time, which is already on top, so nothing needs to change.
Turn each item into the one number that decides the outcome (here, arrival time), sort by the order that matters, then a single pass with a stack or a running max finishes it. stack.peek() here is just the slowest time so far.
Use double division. (target - position) / speed with ints rounds down, so times 1.5 and 1.9 both become 1 and two separate fleets merge by mistake. And a car that catches up exactly at the target still joins, so the test is time > peek for a new fleet, not >=.
Pattern notes: Monotonic Stack · Open on LeetCode
Intervals
An interval is a [start, end] range, like a meeting from 1 to 3. The core pattern is sort, then sweep: sort the intervals (by start to merge, by end to pick greedily), then walk them once, comparing each one only with the last interval you kept. This is new compared to Beginner: the whole trick is choosing what to sort by and getting the overlap test exactly right, including whether touching ends like [1,2] and [2,3] count as overlapping.
LC 56Merge IntervalsMediumSort + SweepLeetCode
You get a list of intervals in any order. Merge every group of overlapping intervals into one, and return the result. Intervals that only touch, like [1,4] and [4,5], count as overlapping.
Example: [[1,3], [2,6], [8,10], [15,18]] → [[1,6], [8,10], [15,18]]. [1,3] and [2,6] overlap, so they become [1,6].
Words like overlapping, merge, ranges, meetings, [start, end] pairs. Overlaps are hard to find in random order but easy once sorted by start: then anything that overlaps the current group must come right after it.
1Brute force — merge any overlapping pair, repeat
Look for any two intervals that overlap. Merge them into one and start looking again. Stop when a full scan finds no overlapping pair.
int[][] merge(int[][] intervals) {
List<int[]> list = new ArrayList<>();
for (int[] in : intervals) list.add(in.clone());
boolean changed = true;
while (changed) {
changed = false;
outer:
for (int i = 0; i < list.size(); i++) {
for (int j = i + 1; j < list.size(); j++) {
int[] a = list.get(i), b = list.get(j);
if (a[0] <= b[1] && b[0] <= a[1]) { // a and b overlap
a[0] = Math.min(a[0], b[0]);
a[1] = Math.max(a[1], b[1]);
list.remove(j);
changed = true;
break outer; // scan again from the start
}
}
}
}
list.sort((x, y) -> Integer.compare(x[0], y[0]));
return list.toArray(new int[0][]);
}
Time O(n³)Space O(n)
Each merge costs a pair scan of O(n²), and there can be n merges. Without order, you can't know when you're done with an interval.
2Optimal — sort by start, then extend or start new
Sort by start. Walk the intervals, keeping the result list. Compare each interval only with the last merged one: if it starts at or before that one's end, they overlap: stretch the end to the bigger of the two ends. Otherwise there is a gap, and since everything later starts even further right, the last one is finished: start a new one.
int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); // by start
List<int[]> res = new ArrayList<>();
for (int[] in : intervals) {
if (res.isEmpty() || res.get(res.size() - 1)[1] < in[0]) {
res.add(in); // gap: start a new interval
} else {
int[] last = res.get(res.size() - 1);
last[1] = Math.max(last[1], in[1]); // overlap: stretch the end
}
}
return res.toArray(new int[0][]);
}
Time O(n log n)Space O(n)
Sorting costs O(n log n); the sweep is one O(n) pass. The output list can hold up to n intervals.
The interval template: sort by start, compare with the last kept interval. Two intervals a, b (with a starting first) overlap when b.start <= a.end.
Use Math.max for the new end, not in[1]. For [[1,4], [2,3]], the second interval sits inside the first; setting end = 3 would shrink the answer to [1,3] instead of [1,4].
Pattern notes: Merge Intervals · Open on LeetCode
LC 57Insert IntervalMediumIntervals (three phases)LeetCode
You get a list of intervals that are already sorted by start and don't overlap, plus one newInterval. Insert it, merging where needed, so the list is still sorted with no overlaps.
Example: [[1,2], [3,5], [6,7], [8,10], [12,16]], newInterval = [4,8] → [[1,2], [3,10], [12,16]]. [4,8] swallows [3,5], [6,7] and [8,10].
"Already sorted and non-overlapping" is a gift: you don't need to sort again. A sorted list splits into three parts around the new interval: completely before it, overlapping it, completely after it. That is a linear scan, just like the merge step of Merge Sort.
1Brute force — add it and run Merge Intervals
Put the new interval into the list, sort everything by start, and merge exactly like LC 56.
int[][] insert(int[][] intervals, int[] newInterval) {
int[][] all = Arrays.copyOf(intervals, intervals.length + 1);
all[intervals.length] = newInterval;
Arrays.sort(all, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> res = new ArrayList<>();
for (int[] in : all) {
if (res.isEmpty() || res.get(res.size() - 1)[1] < in[0]) res.add(new int[]{in[0], in[1]});
else res.get(res.size() - 1)[1] = Math.max(res.get(res.size() - 1)[1], in[1]);
}
return res.toArray(new int[0][]);
}
Time O(n log n)Space O(n)
It is correct and short, but it throws away the fact that the input was already sorted. The interviewer will want O(n).
2Optimal — before, overlap, after
Walk the list once in three phases. Phase 1: copy every interval that ends before the new one starts. Phase 2: while an interval starts at or before the new one's end, it overlaps, so swallow it: new start = min, new end = max. Then add the grown new interval. Phase 3: copy everything left, since it all starts after the new end.
int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> res = new ArrayList<>();
int i = 0, n = intervals.length;
// phase 1: completely before the new interval
while (i < n && intervals[i][1] < newInterval[0]) res.add(intervals[i++]);
// phase 2: overlapping - grow the new interval
int start = newInterval[0], end = newInterval[1];
while (i < n && intervals[i][0] <= end) {
start = Math.min(start, intervals[i][0]);
end = Math.max(end, intervals[i][1]);
i++;
}
res.add(new int[]{start, end});
// phase 3: completely after
while (i < n) res.add(intervals[i++]);
return res.toArray(new int[0][]);
}
Time O(n)Space O(n)
Each interval is looked at once. The space is only for the output.
Sorted input + one new item → split into before / overlap / after and scan once. Don't re-sort what is already sorted.
Phase 2 must compare with the growing end, not the original newInterval[1], since swallowing [8,10] can push the end further. Also handle the empty list: [] with [5,7] must return [[5,7]] (the code above does, because phase 2 always adds).
Pattern notes: Merge Intervals · Open on LeetCode
LC 435Non-overlapping IntervalsMediumGreedy (sort by end)LeetCode
Given some intervals, remove the fewest of them so that the rest don't overlap. Here touching ends like [1,2] and [2,3] do not overlap. Return how many you removed.
Example: [[1,2], [2,3], [3,4], [1,3]] → 1. Remove [1,3] and the other three line up without overlapping.
"Remove the minimum so none overlap" is the same as "keep the maximum number of non-overlapping intervals" (activity selection, like fitting the most meetings into one room). Maximise a count of things that must not clash → greedy, and the greedy choice for intervals is almost always sort by end.
1Brute force — DP: longest chain of non-overlapping intervals
Sort by start. dp[i] = the most intervals we can keep if interval i is the last one kept. It extends any earlier chain whose last interval ends by the time i starts (like Longest Increasing Subsequence from Beginner DP). Removed = n − best chain.
int eraseOverlapIntervals(int[][] intervals) {
int n = intervals.length;
int[][] a = intervals.clone();
Arrays.sort(a, (x, y) -> Integer.compare(x[0], y[0]));
int[] dp = new int[n]; // dp[i] = most we can keep, ending with a[i]
int best = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (a[j][1] <= a[i][0]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
best = Math.max(best, dp[i]);
}
return n - best;
}
Time O(n²)Space O(n)
Correct, but with 100,000 intervals it is 5 billion steps and times out.
2Optimal — greedy: always keep the interval that ends first
Sort by end. Walk through and keep an interval if it starts at or after the end of the last one you kept; otherwise remove it. Why the end? The interval that ends first leaves the most room for everything after it. Keeping a long interval early (like [1,10]) blocks many short ones; picking by earliest end can never do worse.
[[1,10], [2,3], [4,5], [6,7]] → 1. Sorting by start would keep [1,10] first and then have to remove all three short ones.int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1])); // by END
int kept = 0;
int lastEnd = Integer.MIN_VALUE; // end of the last interval we kept
for (int[] in : intervals) {
if (in[0] >= lastEnd) { // no clash (touching is fine)
kept++;
lastEnd = in[1];
} // else: remove it (just skip)
}
return intervals.length - kept;
}
Time O(n log n)Space O(1) extra
One sort and one pass. Space ignores the sort's own internal memory.
Merge intervals → sort by start. Pick the most non-clashing intervals (or fewest to remove) → sort by end and greedily keep whatever ends first.
Touching is allowed here, so the test is start >= lastEnd. With >, [[1,2], [2,3]] would wrongly remove one. Also compare with Integer.compare, not a[1] - b[1], which can overflow on large values.
Pattern notes: Merge Intervals · Open on LeetCode
LC 452Minimum Number of Arrows to Burst BalloonsMediumGreedy (sort by end)LeetCode
Balloons are stretched along a line; balloon [start, end] covers those x values. An arrow shot straight up at position x bursts every balloon that covers x (including ones whose edge is exactly at x). What is the fewest arrows to burst them all?
Example: [[10,16], [2,8], [1,6], [7,12]] → 2. An arrow at x = 6 bursts [2,8] and [1,6]; an arrow at x = 12 bursts [10,16] and [7,12].
"Minimum number of points that hit every interval" is the twin of LC 435: the balloons that need separate arrows are exactly a set of non-overlapping intervals. Minimise a count over intervals → sort by end and be greedy.
1Brute force — shoot, then scan everything to see what burst
Sort by end. Take the first balloon that isn't burst yet, shoot at its end, then check every balloon to mark the ones that arrow hits. Repeat.
int findMinArrowShots(int[][] points) {
int n = points.length;
int[][] p = points.clone();
Arrays.sort(p, (a, b) -> Integer.compare(a[1], b[1]));
boolean[] burst = new boolean[n];
int arrows = 0;
for (int i = 0; i < n; i++) {
if (burst[i]) continue;
arrows++;
int x = p[i][1]; // shoot at this balloon's end
for (int j = 0; j < n; j++) { // mark every balloon it hits
if (p[j][0] <= x && x <= p[j][1]) burst[j] = true;
}
}
return arrows;
}
Time O(n²)Space O(n)
Each arrow re-checks all n balloons. With many separate balloons that is n arrows × n checks.
2Optimal — one pass, shoot at the earliest end
Sort by end. The balloon that ends first must be hit by some arrow at or before its end; shooting exactly at its end is best because that is the furthest right you can go, so it hits as many later balloons as possible. Then walk on: a balloon that starts at or before the arrow's x is already burst. The first one that starts after it needs a new arrow at its own end.
int findMinArrowShots(int[][] points) {
Arrays.sort(points, (a, b) -> Integer.compare(a[1], b[1])); // by END, overflow-safe
int arrows = 1;
int arrowX = points[0][1]; // first arrow at the earliest end
for (int i = 1; i < points.length; i++) {
if (points[i][0] > arrowX) { // starts after the arrow: not hit
arrows++;
arrowX = points[i][1]; // new arrow at this balloon's end
} // else: this arrow already bursts it
}
return arrows;
}
Time O(n log n)Space O(1) extra
Sort once, then one pass. The problem guarantees at least one balloon, so points[0] is safe.
"Fewest points to stab all intervals" = "most non-overlapping intervals". Same code shape as LC 435: sort by end, keep a last end, count when a new start is beyond it.
Values go from −2³¹ to 2³¹ − 1, so the comparator a[1] - b[1] overflows and sorts wrongly. Always use Integer.compare. And an edge counts as a hit: [[1,2], [2,3]] needs only 1 arrow, so the test is >, not >=.
Pattern notes: Merge Intervals · Open on LeetCode
LC 986Interval List IntersectionsMediumTwo Pointers on IntervalsLeetCode
You get two lists of intervals. Each list is sorted and has no overlaps inside it. Return every range that is covered by both lists (the intersections), in order. A single point like [5,5] counts.
Example: A = [[0,2], [5,10], [13,23], [24,25]], B = [[1,5], [8,12], [15,24], [25,26]] → [[1,2], [5,5], [8,10], [15,23], [24,24], [25,25]].
Two sorted lists to combine is the Merge Two Sorted Lists / LC 88 signal from Beginner: one pointer per list, move the one that is "behind". The interval twist is the overlap formula and the rule for which pointer to move.
1Brute force — intersect every pair
For every interval in A and every interval in B, compute their overlap and keep it if it isn't empty. Because both lists are sorted and disjoint, the results already come out in order.
int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
List<int[]> res = new ArrayList<>();
for (int[] a : firstList) {
for (int[] b : secondList) {
int lo = Math.max(a[0], b[0]);
int hi = Math.min(a[1], b[1]);
if (lo <= hi) res.add(new int[]{lo, hi});
}
}
return res.toArray(new int[0][]);
}
Time O(m · n)Space O(1) extra
Most pairs are far apart and can't possibly overlap, but we check them anyway.
2Optimal — two pointers, drop whoever ends first
Look at A[i] and B[j]. Their overlap is lo = the bigger start, hi = the smaller end; if lo <= hi, record it. Then move past the interval that ends first: it can't reach anything further right in the other list, while the one that ends later might still overlap the next interval.
[5,10] meets [1,5] only at 5, and the lists touch again at 24 and 25.int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
List<int[]> res = new ArrayList<>();
int i = 0, j = 0;
while (i < firstList.length && j < secondList.length) {
int lo = Math.max(firstList[i][0], secondList[j][0]); // later start
int hi = Math.min(firstList[i][1], secondList[j][1]); // earlier end
if (lo <= hi) res.add(new int[]{lo, hi}); // they overlap
if (firstList[i][1] < secondList[j][1]) i++; // A[i] is done
else j++; // B[j] is done
}
return res.toArray(new int[0][]);
}
Time O(m + n)Space O(1) extra
Each step moves one pointer forward, so there are at most m + n steps. Output space is not counted.
Overlap of two intervals = [max(starts), min(ends)], and it exists when max(starts) <= min(ends). With two sorted lists, advance whichever ends first.
Use lo <= hi, not lo < hi: closed intervals that touch at one point, like [0,2] and [2,3], do intersect at [2,2]. If either list is empty, the loop never runs and the answer is [].
Pattern notes: Merge Intervals · Open on LeetCode
Modified Binary Search
This is the Beginner binary search template (lo, hi, while (lo <= hi), mid, an ans variable) used on things that do not look sorted at first. The one-line idea: find a yes/no question whose answers go no, no, no, yes, yes, yes (or the other way) and binary search for the place where they flip. What is harder than Beginner: the sorted range may be rotated, two-dimensional or only locally ordered (peaks), and in the last two problems you do not search the array at all, you binary search on the answer itself (a speed, a ship size).
LC 34Find First and Last Position of Element in Sorted ArrayMediumBinary SearchLeetCode
You get a sorted array that can contain repeats, and a target. Return the first and the last index where the target appears, or [-1, -1] if it is not there. It must run in O(log n).
Example: nums = [5, 7, 7, 8, 8, 10], target = 8 → [3, 4]. With target = 6 → [-1, -1].
"Sorted array" plus "O(log n)" always means binary search. The words "first" and "last" tell you a plain match is not enough: you need the boundary of a block of equal values, which is the "keep going after a match" version of the template.
1Brute force — scan the whole array
Walk left to right. The first time you see the target, save it as first. Every time you see it, update last.
int[] searchRange(int[] nums, int target) {
int first = -1, last = -1;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
if (first == -1) first = i; // first time we see it
last = i; // keep moving the end
}
}
return new int[]{first, last};
}
Time O(n)Space O(1)
Correct, but it ignores the sorting, and the problem asks for O(log n). An array of a million 8s makes it read every cell.
2Optimal — two binary searches that do not stop at a match
Run the normal template twice. When nums[mid] == target, save mid in ans and keep searching: go left (hi = mid - 1) to find the first one, or right (lo = mid + 1) to find the last one. The last saved ans is the edge of the block.
int[] searchRange(int[] nums, int target) {
return new int[]{findEdge(nums, target, true), findEdge(nums, target, false)};
}
// first = true: leftmost index of target. first = false: rightmost index.
int findEdge(int[] nums, int target, boolean first) {
int lo = 0, hi = nums.length - 1, ans = -1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) {
ans = mid; // a match: remember it...
if (first) hi = mid - 1; // ...and keep looking left
else lo = mid + 1; // ...or keep looking right
} else if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return ans; // -1 if we never matched
}
Time O(log n)Space O(1)
Two searches of about log n rounds each is still O(log n), even if the whole array is the target.
To find a boundary, never return at a match. Save ans = mid and keep shrinking toward the side you care about. This "save and keep going" move is the heart of every problem in this topic.
Do not find one 8 and then walk left and right with a loop to find the ends. It looks fast, but on [8, 8, 8, ..., 8] the walk is O(n) again.
Pattern notes: Binary Search · Open on LeetCode
LC 33Search in Rotated Sorted ArrayMediumModified Binary SearchLeetCode
A sorted array of distinct numbers was rotated: cut at some point and the two pieces swapped, so [0, 1, 2, 4, 5, 6, 7] might become [4, 5, 6, 7, 0, 1, 2]. Find the index of target, or -1. It must run in O(log n).
Example: nums = [4, 5, 6, 7, 0, 1, 2], target = 0 → 4. With target = 3 → -1.
"Sorted ... rotated" plus "O(log n)". The array is almost sorted: it is two sorted runs glued together. Any time the data is "sorted, but broken in one known way", think binary search with an extra check to see which part is still sorted.
1Brute force — look at every number
Forget the rotation and just scan.
int search(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) return i;
}
return -1;
}
Time O(n)Space O(1)
Always correct, but the problem explicitly asks for O(log n), so this is only a starting point.
2Optimal — one half is always sorted
Cut the range at mid. The rotation point can only be in one of the two halves, so at least one half is perfectly sorted. You can tell which: if nums[lo] <= nums[mid], the left half is sorted. For a sorted half, checking "is the target inside it?" is just comparing with its two ends. If yes, go there; if no, go to the other half.
int search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) { // left half [lo..mid] is sorted
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1; // target inside it
else lo = mid + 1;
} else { // right half [mid..hi] is sorted
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1; // target inside it
else hi = mid - 1;
}
}
return -1;
}
Time O(log n)Space O(1)
Every round still throws away half of the range; we only spend one extra comparison deciding which half is the sorted one.
In a rotated sorted array, one side of mid is always sorted. Find that side, ask "is the target between its ends?", and move. This same check is reused in many rotated-array questions.
Use nums[lo] <= nums[mid], not <. When lo == mid (two cells left, like [3, 1] searching 1) the left half is one cell and still counts as sorted; with < you check the wrong half and miss the answer.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 153Find Minimum in Rotated Sorted ArrayMediumModified Binary SearchLeetCode
Same kind of array as LC 33: sorted, distinct numbers, then rotated. Return the smallest number. It must run in O(log n).
Example: nums = [4, 5, 6, 7, 0, 1, 2] → 0. [11, 13, 15, 17] (not rotated at all) → 11.
"Rotated sorted" plus "O(log n)" again. The minimum is exactly where the "big" run ends and the "small" run begins, so the job is to find a turning point. Finding the spot where something changes is the "first index that says yes" template from Beginner.
1Brute force — scan for the smallest
Look at every number and keep the smallest.
int findMin(int[] nums) {
int min = nums[0];
for (int x : nums) min = Math.min(min, x);
return min;
}
Time O(n)Space O(1)
Fine for small inputs, but it wastes the fact that both pieces are sorted, and the problem asks for O(log n).
2Optimal — first index that is ≤ the last element
Compare every number with the last element. Numbers in the big left run are bigger than it; numbers in the small right run are smaller or equal. So the question "nums[i] <= last?" answers no, no, ..., yes, yes, and the minimum is the first "yes". That is the Beginner "first true" template with ans.
int findMin(int[] nums) {
int last = nums[nums.length - 1];
int lo = 0, hi = nums.length - 1, ans = 0;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] <= last) { // mid is in the small (right) run
ans = mid; // could be the minimum...
hi = mid - 1; // ...look left for an earlier one
} else {
lo = mid + 1; // mid is in the big (left) run: min is to the right
}
}
return nums[ans];
}
Time O(log n)Space O(1)
If the array is not rotated, every cell is "yes" and the search walks left to index 0, which is correct.
Turn the problem into a yes/no question that flips once, then binary search for the first "yes". Comparing against the last element is the trick that makes a rotated array flip exactly once.
Comparing with nums[0] instead of the last element breaks on a non-rotated array: in [11, 13, 15, 17] nothing is smaller than nums[0], so there is no "yes" at all. The last element always works.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 74Search a 2D MatrixMediumBinary SearchLeetCode
You get an m × n grid. Each row is sorted left to right, and the first number of each row is bigger than the last number of the row above. Say whether target is in the grid, in O(log(m·n)).
Example: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 → true. With target = 13 → false.
"Each row sorted" plus "first of a row is greater than the last of the previous row" means that reading the rows one after another gives one long sorted list. A sorted list plus "O(log)" means binary search; the only new part is turning a list position into a row and a column.
1Brute force — check every cell
Two loops over rows and columns.
boolean searchMatrix(int[][] matrix, int target) {
for (int[] row : matrix) {
for (int x : row) {
if (x == target) return true;
}
}
return false;
}
Time O(m·n)Space O(1)
It ignores all the sorting. A 1,000 × 1,000 grid means a million checks instead of about 20.
2Optimal — pretend it is one flat sorted array
Number the cells 0 to m·n − 1, row by row. Those numbers are sorted, so run the normal template on the range [0, m·n − 1]. To read position mid, use row = mid / n, col = mid % n (n is the number of columns). No copying needed.
boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int lo = 0, hi = m * n - 1; // pretend it is one long sorted array
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int val = matrix[mid / n][mid % n]; // 1D index -> (row, col)
if (val == target) return true;
else if (val < target) lo = mid + 1;
else hi = mid - 1;
}
return false;
}
Time O(log(m·n))Space O(1)
It is exactly LC 704 from Beginner; only the line that reads val is new.
A grid read row by row is a 1D array: index = row · n + col, and back again with row = index / n, col = index % n. This conversion shows up in many grid problems, not only binary search.
Divide by the number of columns (n = matrix[0].length), not rows. On a square grid both work, so the bug hides until a test like 3 × 4 appears.
Pattern notes: Binary Search · Open on LeetCode
LC 162Find Peak ElementMediumModified Binary SearchLeetCode
A peak is a number bigger than both of its neighbours. Pretend there is minus infinity just outside both ends, and neighbours are never equal. Return the index of any peak, in O(log n).
Example: nums = [1, 2, 3, 1] → 2. [1, 2, 1, 3, 5, 6, 4] → 1 or 5 (both are fine).
The array is not sorted, yet it says "O(log n)" and "return any peak". Any-answer plus log time means you can throw half away as long as the half you keep is guaranteed to contain some answer. Looking at mid and its right neighbour tells you which half that is.
1Brute force — find where the numbers first go down
Walk left to right. The first time nums[i] > nums[i + 1], index i is a peak (we were climbing to get there). If it never goes down, the last element is a peak.
int findPeakElement(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i + 1]) return i; // first time the numbers go down
}
return nums.length - 1; // always going up: last element is a peak
}
Time O(n)Space O(1)
Simple and correct, but the problem demands O(log n).
2Optimal — walk uphill, half at a time
Look at mid and mid + 1. If they go up (nums[mid] < nums[mid + 1]), keep climbing right: the numbers cannot rise forever because the edge is minus infinity, so a peak must exist on the right. If they go down, mid itself or something to its left is a peak. Save it as ans and look left. Same "save and keep going" template as LC 34.
int findPeakElement(int[] nums) {
int n = nums.length;
int lo = 0, hi = n - 1, ans = n - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (mid == n - 1 || nums[mid] > nums[mid + 1]) {
ans = mid; // going downhill here: a peak is at mid or to its left
hi = mid - 1;
} else {
lo = mid + 1; // going uphill: a peak is to the right
}
}
return ans;
}
Time O(log n)Space O(1)
The brute force found the peak at index 1 on this example; binary search finds index 5. Both are accepted because the problem says "any peak".
Binary search does not need a sorted array. It needs a rule that proves one half still has an answer. Here the rule is "follow the uphill direction".
Reading nums[mid + 1] when mid is the last index crashes. The mid == n - 1 check comes first and treats the outside as minus infinity (downhill), so [1, 2, 3] correctly returns 2.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 875Koko Eating BananasMediumBinary Search on AnswerLeetCode
There are piles of bananas. Koko picks a speed k (bananas per hour). Each hour she eats up to k bananas from one pile; if the pile has fewer, she finishes it and waits for the next hour. Find the smallest k that lets her finish all piles within h hours.
Example: piles = [3, 6, 7, 11], h = 8 → 4. piles = [30, 11, 23, 4, 20], h = 5 → 30.
The question asks for the "minimum speed such that she can finish". Two signals: you are asked for the smallest (or largest) number that works, and checking one guess ("can she finish at speed 5?") is easy. If a faster speed always works whenever a slower one does, the answers go no, no, yes, yes: binary search on the answer.
1Brute force — try every speed from 1 upward
Start at speed 1. Count the hours; if it fits in h, that is the answer. Otherwise try speed 2, and so on.
int minEatingSpeed(int[] piles, int h) {
for (int k = 1; ; k++) {
if (hoursNeeded(piles, k) <= h) return k;
}
}
long hoursNeeded(int[] piles, int k) {
long hours = 0;
for (int p : piles) hours += (p + k - 1) / k; // ceil(p / k) without doubles
return hours;
}
Time O(n · max)Space O(1)
A pile can hold 109 bananas, so this may try up to a billion speeds, each costing a pass over the piles.
2Optimal — binary search on the speed
The answer lives in the range [1, max pile] (at the max speed each pile takes one hour, and h is at least the number of piles). Instead of an array index, mid is a guessed speed. If that speed is fast enough, save it and try slower; if not, go faster. The check function hoursNeeded is the only new code.
hoursNeeded.int minEatingSpeed(int[] piles, int h) {
int lo = 1, hi = 0;
for (int p : piles) hi = Math.max(hi, p); // speed max(piles) always works
int ans = hi;
while (lo <= hi) {
int k = lo + (hi - lo) / 2; // try this speed
if (hoursNeeded(piles, k) <= h) {
ans = k; // fast enough: remember it...
hi = k - 1; // ...and try slower
} else {
lo = k + 1; // too slow: must eat faster
}
}
return ans;
}
long hoursNeeded(int[] piles, int k) {
long hours = 0;
for (int p : piles) hours += (p + k - 1) / k; // ceil(p / k) without doubles
return hours;
}
Time O(n · log max)Space O(1)
About 30 guesses instead of up to a billion, and each guess is one pass over the piles.
Binary search on the answer: when asked for the min/max value that makes something possible, (1) find the range of possible answers, (2) write canDo(x), (3) run the "first yes" template over the range. The array is never searched directly.
Keep the hour total in a long: with many huge piles and speed 1 the sum can pass 231 and wrap around to a negative number, which then looks like "fits". Also use (p + k - 1) / k for rounding up; plain p / k rounds down and undercounts.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 1011Capacity To Ship Packages Within D DaysMediumBinary Search on AnswerLeetCode
Packages with given weights sit on a belt and must ship in order. Each day the ship loads packages from the front until the next one would go over its capacity. Find the smallest capacity that ships everything within days days.
Example: weights = [1,2,3,4,5,6,7,8,9,10], days = 5 → 15 (days: [1..5], [6,7], [8], [9], [10]).
"Least weight capacity ... within D days" is the same shape as Koko: the smallest number that makes a task possible. A bigger ship never needs more days, so "does capacity c work?" answers no, no, yes, yes. Checking one capacity is a simple greedy pass. That combination means binary search on the answer.
1Brute force — grow the ship one unit at a time
The ship must at least carry the heaviest package. Start there and add 1 until the days fit.
int shipWithinDays(int[] weights, int days) {
int cap = 0;
for (int w : weights) cap = Math.max(cap, w); // must fit the heaviest package
while (daysNeeded(weights, cap) > days) cap++;
return cap;
}
int daysNeeded(int[] weights, int cap) {
int d = 1, load = 0;
for (int w : weights) {
if (load + w > cap) { // this package does not fit today
d++;
load = 0;
}
load += w;
}
return d;
}
Time O(n · (sum − max))Space O(1)
With 50,000 packages of weight 500 the capacities to try run into the millions, each with a full pass.
2Optimal — binary search on the capacity
The answer is between max(weights) (anything smaller cannot lift the heaviest box) and sum(weights) (everything in one day). Guess a capacity, count the days greedily, and halve the range: if it fits, save it and try smaller; if not, try bigger. Same template as Koko, different canDo.
int shipWithinDays(int[] weights, int days) {
int lo = 0, hi = 0;
for (int w : weights) {
lo = Math.max(lo, w); // smallest possible: the heaviest package
hi += w; // largest needed: everything in one day
}
int ans = hi;
while (lo <= hi) {
int cap = lo + (hi - lo) / 2;
if (daysNeeded(weights, cap) <= days) {
ans = cap; // works: try a smaller ship
hi = cap - 1;
} else {
lo = cap + 1; // too many days: need a bigger ship
}
}
return ans;
}
int daysNeeded(int[] weights, int cap) {
int d = 1, load = 0;
for (int w : weights) {
if (load + w > cap) { // this package does not fit today
d++; // start a new day
load = 0;
}
load += w;
}
return d;
}
Time O(n · log(sum))Space O(1)
The range is at most 25 million wide, so about 25 guesses, each one greedy pass.
Koko and this problem are twins. Whenever you see "minimize the maximum" or "smallest capacity/speed/size so that it fits in K", reach for binary search on the answer plus a greedy check. Split Array Largest Sum (LC 410) is the same code.
Start lo at max(weights), not 1. With a smaller capacity the heaviest package never fits, but daysNeeded does not notice: it still loads it onto an overloaded day and returns a count. For [10, 1] with 3 days, capacity 5 would get a wrong "yes".
Pattern notes: Modified Binary Search · Open on LeetCode
Tries
A trie (say "try", also called a prefix tree) stores words letter by letter along a tree: every path from the root spells a prefix, and a flag marks where a real word ends. The one-line template: each node has Node[26] next plus boolean isEnd; insert and search both walk one letter per level. What is new compared with Beginner: you design a whole class with several methods, and in LC 211 the walk has to branch into every child for a wildcard, which is a DFS on the trie.
LC 208Implement Trie (Prefix Tree)MediumTrieLeetCode
Build a class with three methods: insert(word) stores a word, search(word) says whether that exact word was stored, and startsWith(prefix) says whether any stored word begins with the prefix. Only lowercase letters.
Example: insert("apple"), search("apple") → true, search("app") → false, startsWith("app") → true, insert("app"), search("app") → true.
The word "prefix" (or "starts with", "autocomplete", "dictionary of words") is the signal. A HashSet is great for whole words but has no idea which words share a beginning; a trie stores shared beginnings only once, so prefix questions become a short walk.
1Brute force — a HashSet of words
Keep every word in a set. Exact search is easy; for a prefix, check every stored word.
class Trie {
private Set<String> words = new HashSet<>();
public Trie() {}
public void insert(String word) {
words.add(word);
}
public boolean search(String word) {
return words.contains(word);
}
public boolean startsWith(String prefix) {
for (String w : words) { // check every stored word
if (w.startsWith(prefix)) return true;
}
return false;
}
}
insert / search O(L)startsWith O(N · L)Space O(total letters)
insert and search are fine, but with 30,000 stored words every startsWith call scans all of them.
2Optimal — a tree of letters
Each node has 26 child slots, one per letter. To insert, walk down one letter at a time, creating missing nodes, then set isEnd = true on the last node. To search, walk the same way: if the path breaks, the answer is false. search also needs isEnd; startsWith only needs the path to exist.
class Trie {
private static class Node {
Node[] next = new Node[26]; // one slot per letter a..z
boolean isEnd; // does a word end exactly here?
}
private final Node root = new Node();
public Trie() {}
public void insert(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
int i = c - 'a';
if (cur.next[i] == null) cur.next[i] = new Node(); // make the path if missing
cur = cur.next[i];
}
cur.isEnd = true; // mark the end of the word
}
public boolean search(String word) {
Node node = walk(word);
return node != null && node.isEnd; // path exists AND a word ends here
}
public boolean startsWith(String prefix) {
return walk(prefix) != null; // path exists is enough
}
// follow the letters from the root; null if the path breaks
private Node walk(String s) {
Node cur = root;
for (char c : s.toCharArray()) {
cur = cur.next[c - 'a'];
if (cur == null) return null;
}
return cur;
}
}
Time O(L) per callSpace O(total letters · 26)
Every call costs only the length of its own word, no matter how many words are stored. The price is memory: each node carries 26 slots.
A trie node is 26 children + an isEnd flag. search = path exists and isEnd; startsWith = path exists. Write the shared walk helper once and reuse it.
Forgetting the isEnd check in search makes search("app") return true after inserting only "apple". The path being there does not mean the word was inserted.
Pattern notes: Tries · Open on LeetCode
LC 211Design Add and Search Words Data StructureMediumTrieLeetCode
Build a WordDictionary with addWord(word) and search(word). The search word may contain dots, and a . matches any one letter. Return whether some added word matches the whole pattern.
Example: add "bad", "dad", "mad". Then search("pad") → false, search("bad") → true, search(".ad") → true, search("b..") → true.
A stored dictionary of words that is searched letter by letter, plus a wildcard that can be "any letter". Letter-by-letter matching is a trie walk; "any letter" means "try every child here", which turns the straight walk into a small DFS.
1Brute force — compare with every word
Keep a list of words. For a search, compare the pattern with each word letter by letter, letting . match anything.
class WordDictionary {
private List<String> words = new ArrayList<>();
public WordDictionary() {}
public void addWord(String word) {
words.add(word);
}
public boolean search(String word) {
for (String w : words) {
if (matches(w, word)) return true;
}
return false;
}
private boolean matches(String w, String pattern) {
if (w.length() != pattern.length()) return false;
for (int i = 0; i < w.length(); i++) {
char p = pattern.charAt(i);
if (p != '.' && p != w.charAt(i)) return false;
}
return true;
}
}
addWord O(1)search O(N · L)Space O(total letters)
Every search touches every stored word, even when the first letter already rules most of them out.
2Optimal — trie + DFS for the dots
Store the words in a trie exactly like LC 208. Search with a recursive helper dfs(word, i, node). A normal letter follows one child. A . tries every non-null child and succeeds if any branch succeeds. When all letters are used, the answer is that node's isEnd.
class WordDictionary {
private static class Node {
Node[] next = new Node[26];
boolean isEnd;
}
private final Node root = new Node();
public WordDictionary() {}
public void addWord(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
int i = c - 'a';
if (cur.next[i] == null) cur.next[i] = new Node();
cur = cur.next[i];
}
cur.isEnd = true;
}
public boolean search(String word) {
return dfs(word, 0, root);
}
// can word[i..] be matched starting from this node?
private boolean dfs(String word, int i, Node node) {
if (node == null) return false; // path broke
if (i == word.length()) return node.isEnd; // used every letter
char c = word.charAt(i);
if (c == '.') {
for (Node child : node.next) { // '.' = try every branch
if (dfs(word, i + 1, child)) return true;
}
return false;
}
return dfs(word, i + 1, node.next[c - 'a']); // normal letter: one branch
}
}
addWord O(L) · search O(L) without dotsSpace O(total letters · 26)
With dots a search can branch, but it never visits more nodes than the trie has, and LeetCode limits a pattern to a few dots, so it stays fast.
Wildcard over a trie = DFS over the children: a letter follows one edge, a wildcard loops over all 26 and returns true on the first success. The recursion depth is just the word length.
When the pattern ends, return node.isEnd, not true. Otherwise search("ba") or search("..") wrongly matches the prefix of "bad".
Pattern notes: Tries · Open on LeetCode
Design
Design problems ask you to build a small class whose methods must each be fast. The one-line template: list the operations, write the speed each one needs, then combine two simple structures so that every operation is cheap (a HashMap for "find by key" plus a list or sorted array for "order"). What is harder than Beginner: no single structure is enough, you must keep two structures in sync, and the brute force here is a design that works but has one slow method.
LC 146LRU CacheMediumHash Map + Linked ListLeetCode
Build a cache with a fixed capacity. get(key) returns the value or -1. put(key, value) adds or updates. When the cache is full and a new key arrives, throw out the least recently used key (the one untouched for the longest time). Both methods must be O(1).
Example: capacity 2: put(1,1), put(2,2), get(1) → 1, put(3,3) (evicts 2), get(2) → -1, put(4,4) (evicts 1), get(1) → -1, get(3) → 3, get(4) → 4.
"O(1) get" means a HashMap. "Least recently used" means you must keep keys in order of use and move one to the front whenever it is touched. Moving or removing an item from the middle in O(1) is exactly what a doubly linked list does, as long as the map tells you where the node is.
1Brute force — HashMap + a list of keys in order
The map holds the values. A separate list holds keys from oldest (front) to newest (back). Touching a key means removing it from the list and adding it at the back.
class LRUCache {
private final int capacity;
private final Map<Integer, Integer> map = new HashMap<>();
private final List<Integer> order = new ArrayList<>(); // front = oldest, back = newest
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if (!map.containsKey(key)) return -1;
order.remove(Integer.valueOf(key)); // O(n): search the list
order.add(key); // now the newest
return map.get(key);
}
public void put(int key, int value) {
if (map.containsKey(key)) {
order.remove(Integer.valueOf(key));
} else if (map.size() == capacity) {
int oldest = order.remove(0); // O(n): shifts every element
map.remove(oldest);
}
map.put(key, value);
order.add(key);
}
}
get / put O(capacity)Space O(capacity)
Correct, but finding a key inside the list and shifting it out are linear, and the problem demands O(1).
2Optimal — HashMap of nodes + doubly linked list
Keep the order in a doubly linked list (newest right after head, oldest right before tail), and let the map store key → the node itself, not just the value. Now "find the node" is a map lookup, and "unhook it and put it at the front" is four pointer changes. Two dummy nodes head and tail mean there are no empty-list special cases.
class LRUCache {
private static class Node {
int key, val;
Node prev, next;
Node(int key, int val) { this.key = key; this.val = val; }
}
private final int capacity;
private final Map<Integer, Node> map = new HashMap<>(); // key -> its node
private final Node head = new Node(0, 0); // dummy: head.next is the newest
private final Node tail = new Node(0, 0); // dummy: tail.prev is the oldest
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
public int get(int key) {
Node node = map.get(key);
if (node == null) return -1;
remove(node); // unhook from its current spot
addFront(node); // it is now the most recently used
return node.val;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) { // update an existing key
node.val = value;
remove(node);
addFront(node);
return;
}
if (map.size() == capacity) { // full: evict the oldest
Node lru = tail.prev;
remove(lru);
map.remove(lru.key); // this is why the node stores its key
}
Node fresh = new Node(key, value);
addFront(fresh);
map.put(key, fresh);
}
private void remove(Node node) { // O(1): neighbours point past it
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addFront(Node node) { // O(1): insert right after head
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
}
get / put O(1)Space O(capacity)
Every step is a map lookup or a fixed number of pointer changes, so nothing grows with the cache size.
Shortcut in Java: LinkedHashMap already is "HashMap + doubly linked list". Passing true as the third constructor argument orders it by access, and overriding removeEldestEntry evicts automatically. Good to mention in an interview, but most interviewers then ask you to build the version above.
class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // true = order by ACCESS, not insertion
this.capacity = 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> eldest) {
return size() > capacity; // auto-evict the least recently used
}
}
"O(1) lookup and O(1) reorder" = HashMap<key, Node> + doubly linked list with dummy head and tail. The map finds the node, the list moves it. Store the key inside the node so eviction can clean the map.
Two classic bugs: forgetting to map.remove(lru.key) when evicting (the map keeps a dead node and grows past capacity), and treating put on an existing key as a new insert (it must update the value and move the node, not evict anything).
Pattern notes: Design Problems · Open on LeetCode
LC 981Time Based Key-Value StoreMediumHash Map + Binary SearchLeetCode
Build a TimeMap. set(key, value, timestamp) saves a value for a key at a moment in time. get(key, timestamp) returns the value saved at the latest time that is ≤ timestamp, or "" if there is none. Timestamps passed to set are strictly increasing.
Example: set("foo","bar",1), get("foo",1) → "bar", get("foo",3) → "bar", set("foo","bar2",4), get("foo",4) → "bar2", get("foo",5) → "bar2".
"Look up by key" means a HashMap. "The latest time ≤ t" is a "last value that passes a test" question, and the note that timestamps are strictly increasing is a hint in disguise: each key's list is already sorted, so it can be binary searched.
1Brute force — map of lists, scan backwards
For each key keep a list of timestamps and a matching list of values. For get, walk the list from the newest end until you find a time that is small enough.
class TimeMap {
private final Map<String, List<Integer>> times = new HashMap<>();
private final Map<String, List<String>> values = new HashMap<>();
public TimeMap() {}
public void set(String key, String value, int timestamp) {
times.computeIfAbsent(key, k -> new ArrayList<>()).add(timestamp);
values.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
public String get(String key, int timestamp) {
List<Integer> ts = times.get(key);
if (ts == null) return "";
for (int i = ts.size() - 1; i >= 0; i--) { // walk back from the newest
if (ts.get(i) <= timestamp) return values.get(key).get(i);
}
return "";
}
}
set O(1)get O(n) per keySpace O(total sets)
If one key has 100,000 versions and you keep asking about early times, every get walks almost the whole list.
2Optimal — binary search inside each key's list
Keep the same structure. Because the timestamps arrive in increasing order, each list is sorted. For get, run the template to find the last index whose time is ≤ timestamp: on a "yes", save ans = mid and look right for a later one; on a "no", look left. It is LC 34's "find last" with <= instead of ==.
class TimeMap {
private final Map<String, List<Integer>> times = new HashMap<>();
private final Map<String, List<String>> values = new HashMap<>();
public TimeMap() {}
public void set(String key, String value, int timestamp) {
// timestamps arrive in increasing order, so each list stays sorted
times.computeIfAbsent(key, k -> new ArrayList<>()).add(timestamp);
values.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
public String get(String key, int timestamp) {
List<Integer> ts = times.get(key);
if (ts == null) return "";
int lo = 0, hi = ts.size() - 1, ans = -1;
while (lo <= hi) { // last index with ts[i] <= timestamp
int mid = lo + (hi - lo) / 2;
if (ts.get(mid) <= timestamp) {
ans = mid; // a candidate: try for a later one
lo = mid + 1;
} else {
hi = mid - 1; // too new: look left
}
}
return ans == -1 ? "" : values.get(key).get(ans);
}
}
set O(1) · get O(log n)Space O(total sets)
The map jumps straight to the key, and binary search finds the right version in about 17 looks even for 100,000 versions. A TreeMap<Integer, String> per key with floorEntry(timestamp) also works in O(log n) and is a nice one-liner to mention.
Design problems often hide a sorted list inside a HashMap. "Latest version at or before time t" = HashMap to the key, then binary search for the last value ≤ t.
Handle the two "nothing found" cases: the key was never set (ts == null), and every stored time is later than the asked time (ans stays -1, e.g. get("foo", 0)). Both must return "", not crash.
Pattern notes: Design Problems · Open on LeetCode
Trees: BFS & DFS
Two tools solve almost every tree problem. BFS walks the tree level by level with a queue: "freeze the queue size, pop exactly that many, their children form the next level". DFS is recursion: "pass information down as parameters, pass answers up as return values". In Beginner you mostly returned one number (a depth, a sum). Here the harder part is deciding what to carry: a level list, a running max, a valid range (low, high), a path you add to and undo, or a node found in a subtree.
LC 102Binary Tree Level Order TraversalMediumTree BFSLeetCode
Read the tree the way you read a page: top row first, each row from left to right. Return one list per row.
Example: root = [3, 9, 20, null, null, 15, 7] → [[3], [9, 20], [15, 7]].
The words "level", "row", "level by level" or "nearest to the root" mean BFS. DFS dives down one branch before it touches the next, so it naturally mixes levels. A queue does the opposite: everything at distance 1 comes out before anything at distance 2.
1Brute force — one tree walk per level
Find the height first. Then for level 0, 1, 2, … walk the tree from the root and collect only the nodes that are exactly that many steps down.
List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
int h = height(root);
for (int d = 0; d < h; d++) { // one full tree walk per level
List<Integer> row = new ArrayList<>();
collect(root, d, row);
res.add(row);
}
return res;
Time O(n·h)Space O(h)
Every level restarts from the root. On a tree shaped like a long line (h = n) that is 1 + 2 + … + n steps, which is O(n²).
2Optimal — BFS with a frozen level size
Put the root in a queue. At the start of each round the queue holds exactly one full level. So read size = q.size() before the loop and pop exactly that many. Their children join the back of the queue and become the next round. This is the Beginner queue, plus one line to know where each level ends.
List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int size = q.size(); // freeze: this many nodes are on THIS level
List<Integer> row = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
row.add(node.val);
if (node.left != null) q.offer(node.left); // children wait behind,
if (node.right != null) q.offer(node.right); // they belong to the next level
}
res.add(row);
}
return res;
Time O(n)Space O(w)
Every node enters and leaves the queue once. w is the widest level, which can be about n/2 in a full tree.
BFS level template: while queue not empty → size = q.size() → loop size times. LC 103, 199, 637, 515 and "minimum depth" are all this loop with a different line inside.
Do not write for (int i = 0; i < q.size(); i++). The queue grows while you loop, so q.size() keeps changing and levels get mixed. Save it in a variable first. Also return an empty list for a null root.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 103Binary Tree Zigzag Level Order TraversalMediumTree BFSLeetCode
Same as level order, but the reading direction switches every row: first row left to right, second row right to left, third left to right again, and so on.
Example: root = [3, 9, 20, null, null, 15, 7] → [[3], [20, 9], [15, 7]].
"Level order" again means BFS. "Then right to left, alternating" only changes how each row is written, not which nodes are in it. That tells you to keep the LC 102 loop untouched and change only the line that stores the value.
1First way — level order, then flip every second row
Run LC 102 exactly as it is, then reverse rows 1, 3, 5, …
List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = levelOrder(root); // plain LC 102
for (int d = 1; d < res.size(); d += 2) {
Collections.reverse(res.get(d)); // flip every second row
}
return res;
Time O(n)Space O(n)
This is already O(n), because each value is reversed at most once. It is a fine interview answer. Step 2 is the cleaner one-pass version that interviewers often ask for next: build every row in the right order from the start.
2One pass — write the row from the front on odd levels
Do not change the order you pop nodes or push children. The queue always runs left to right. Instead, on a right-to-left row, add each value to the FRONT of the row. A LinkedList does addFirst in O(1), so the row comes out already reversed.
List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
boolean leftToRight = true;
while (!q.isEmpty()) {
int size = q.size();
LinkedList<Integer> row = new LinkedList<>();
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
if (leftToRight) row.addLast(node.val); // normal row: write at the back
else row.addFirst(node.val); // zig row: write at the front
if (node.left != null) q.offer(node.left); // the queue itself never
if (node.right != null) q.offer(node.right); // changes direction
}
res.add(row);
leftToRight = !leftToRight;
}
return res;
Time O(n)Space O(n)
Same big-O as step 1, but no second pass and no reversing: the direction flag decides which end of the row each value goes to.
When only the output order changes, keep the traversal the same and change where you write. A deque (add to front or back) handles "reverse every other row" for free.
A common mistake is to push the children right-first on odd levels. That flips the queue order and breaks the next level. For the tree [1, 2, 3, 4, 5, 6, 7] the last row must be [4, 5, 6, 7]; check yours on that tree.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 199Binary Tree Right Side ViewMediumTree BFS / DFSLeetCode
Stand on the right side of the tree and look at it. You see one node per level: the rightmost one. Return those values from top to bottom.
Example: root = [1, 2, 3, 4] → [1, 3, 4]. Node 4 hangs under 2 on the left, but nothing is to its right on that level, so you still see it.
"One value per level" is a level question, so BFS works directly: take the last node of each row. The DFS trick is to carry the depth as a parameter. Then "the first node I reach at this depth" is something you can check with one comparison.
1First way — full level order, keep the last of each row
Build all the rows with LC 102, then take the last value of each row.
List<Integer> rightSideView(TreeNode root) {
List<List<Integer>> levels = levelOrder(root); // LC 102
List<Integer> res = new ArrayList<>();
for (List<Integer> row : levels) res.add(row.get(row.size() - 1)); // last = rightmost
return res;
Time O(n)Space O(n)
Correct and O(n) time, but it stores every value of the tree just to keep one per level. That wastes memory.
2Optimal — DFS right side first, carry the depth
Do a DFS that always visits the right child before the left. Then, on each depth, the first node you arrive at is the rightmost one. If depth == res.size(), this depth has no answer yet, so this node is it. Any later node on that depth is hidden behind it.
List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
dfs(root, 0, res);
return res;
Time O(n)Space O(h)
Every node is visited once, and the only extra memory is the recursion (height h) plus the answer itself.
"First node seen at each depth" = DFS with a depth parameter and the check depth == res.size(). Visit left first and you get the left side view instead.
The answer is not "keep going right from the root". For [1, 2, 3, 4] that gives [1, 3] and misses 4, which you can see because level 2 has nothing to its right.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 1448Count Good Nodes in Binary TreeMediumTree DFSLeetCode
A node is good if nothing on the path from the root down to it is bigger than it. Count the good nodes. The root is always good.
Example: root = [3, 1, 4, 3, null, 1, 5] → 4 (the root 3, the lower 3, the 4 and the 5).
"On the path from the root to the node" means each node's answer depends on its ancestors, not its children. So the information has to flow down: pass it as a parameter. You only need one number from the path (the biggest so far), not the whole path.
1Brute force — for each node, climb back to the root
Record every node's parent. Then for each node, walk up the parent links and check whether any ancestor is bigger.
int goodNodes(TreeNode root) {
Map<TreeNode, TreeNode> parent = new HashMap<>();
List<TreeNode> all = new ArrayList<>();
fill(root, null, parent, all);
int count = 0;
for (TreeNode node : all) {
boolean good = true;
for (TreeNode p = parent.get(node); p != null; p = parent.get(p)) { // climb to the root
if (p.val > node.val) { good = false; break; }
}
if (good) count++;
}
return count;
Time O(n·h)Space O(n)
Each node repeats the climb its parent already did. On a line-shaped tree that is O(n²).
2Optimal — carry the path maximum down
While going down, pass along maxSoFar, the biggest value on the path above. A node is good if node.val >= maxSoFar. Then it hands max(maxSoFar, node.val) to its children. Each node gets its answer in O(1), because its parent already did the work.
int goodNodes(TreeNode root) {
return dfs(root, Integer.MIN_VALUE);
Time O(n)Space O(h)
One visit per node, and the "path summary" is a single int instead of a walk back up.
If a node's answer depends on its ancestors, pass a summary down as a parameter (max so far, sum so far, allowed range). If it depends on its children, return it up. Many Medium tree problems use both.
Use >=, not >. A node equal to the max above it is still good ("no value greater than X"). Start with Integer.MIN_VALUE, not 0, because values can be negative.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 98Validate Binary Search TreeMediumDFS with BoundsLeetCode
Check whether a tree is a valid BST: for every node, all values in its left subtree are smaller and all values in its right subtree are bigger (no duplicates allowed).
Example: [2, 1, 3] → true. [5, 1, 4, null, null, 3, 6] → false, because 4 is on the right of 5 but smaller than 5.
The rule says "all nodes in the subtree", not "the children". So each node is limited by several ancestors at once. A limit coming from above means pass it down: every node gets an allowed range (low, high) from its parent.
1Brute force — compare each node with its subtree's max and min
For each node: the biggest value on its left must be smaller than it, and the smallest value on its right must be bigger. Then check both children the same way.
boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (root.left != null && maxOf(root.left) >= root.val) return false; // whole left side smaller?
if (root.right != null && minOf(root.right) <= root.val) return false; // whole right side bigger?
return isValidBST(root.left) && isValidBST(root.right);
Time O(n²)Space O(h)
It is correct, but maxOf and minOf scan the same subtrees again at every level. On a line-shaped tree that is O(n²).
2Optimal — pass an allowed range down
The root may be anything: range (−∞, +∞). Going left, the node becomes the new ceiling. Going right, it becomes the new floor. Every node must sit strictly inside the range it was given. The range keeps the limits from all ancestors, not just the parent.
boolean isValidBST(TreeNode root) {
return check(root, Long.MIN_VALUE, Long.MAX_VALUE); // long: node values can be int extremes
Time O(n)Space O(h)
Each node is checked once against two numbers. (Another O(n) way: an inorder walk of a valid BST must be strictly increasing.)
"Every node in the subtree must…" → pass a range (low, high) down. Left child: (low, node). Right child: (node, high).
Two traps. First, checking only parent and child (the diagram's tree fools it). Second, using int bounds: a node equal to Integer.MAX_VALUE is valid, so use long bounds (or Integer with null for "no limit").
Pattern notes: Binary Search Trees · Open on LeetCode
LC 230Kth Smallest Element in a BSTMediumBST InorderLeetCode
Given a BST and a number k, return the k-th smallest value (k starts at 1).
Example: root = [5, 3, 6, 2, 4, null, null, 1], k = 3 → 3 (sorted values are 1, 2, 3, 4, 5, 6).
"BST" plus "k-th smallest" or "in sorted order" means inorder traversal, because the inorder of a BST is the sorted list. The Medium step is to stop early instead of listing everything.
1Brute force — list everything in order, then index
Do a full inorder walk into a list (it comes out sorted), then return item k − 1.
int kthSmallest(TreeNode root, int k) {
List<Integer> sorted = new ArrayList<>();
inorder(root, sorted); // BST inorder = sorted values
return sorted.get(k - 1);
Time O(n)Space O(n)
It always visits and stores all n nodes, even when k = 1 and the answer is the very first node you reach.
2Optimal — iterative inorder, stop at the k-th pop
Use the Beginner iterative inorder (LC 94): slide left pushing nodes, pop, go right. Every pop gives the next smallest value. Count down k on every pop and return the moment it hits 0. The rest of the tree is never touched.
int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) { // slide left, saving parents
stack.push(cur);
cur = cur.left;
}
cur = stack.pop(); // next smallest value
if (--k == 0) return cur.val; // the k-th one: stop right here
cur = cur.right;
}
return -1; // k is always valid on LeetCode
Time O(h + k)Space O(h)
We slide down one path (h) and pop k nodes. For small k in a balanced tree that is far less than n.
BST + "sorted order" → inorder. The iterative version lets you stop early, which recursion makes awkward. The same loop powers "BST iterator" (LC 173).
k is 1-based: the first pop is the 1st smallest. Decrement before comparing (--k == 0). Also, in a recursive version a return inside the helper only leaves that one call. Store the answer in a field or you will keep walking.
Pattern notes: Binary Search Trees · Open on LeetCode
LC 236Lowest Common Ancestor of a Binary TreeMediumTree DFSLeetCode
Given two nodes p and q in an ordinary binary tree (not a BST), find the deepest node that has both of them in its subtree. A node counts as its own ancestor.
Example: root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]. p = 5, q = 1 → 3. p = 5, q = 4 → 5.
In Beginner (LC 235) the BST order told you which way to go. Here there is no order, so you cannot know where p and q are without looking. The answer depends on what each subtree contains, which is information that flows up. So use post-order DFS: ask both children first, then decide.
1Brute force — search subtrees from the top down
At each node ask: "is p on my left? is q on my left?" If both are on one side, move to that side. If they are on different sides, this node is the answer.
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == p || root == q) return root;
boolean pLeft = contains(root.left, p);
boolean qLeft = contains(root.left, q);
if (pLeft && qLeft) return lowestCommonAncestor(root.left, p, q); // both on the left
if (!pLeft && !qLeft) return lowestCommonAncestor(root.right, p, q); // both on the right
return root; // they split here
Time O(n²)Space O(h)
Every step down runs contains over a whole subtree again. On a line-shaped tree that is O(n²).
2Optimal — one post-order pass that reports up
Each call returns "the p or q I found below me" (or null). A node that is p or q returns itself. Otherwise it asks both children. If the left AND the right both found something, p and q split right here: this node is the LCA. If only one side found something, pass that up unchanged.
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root; // found one (or nothing)
TreeNode left = lowestCommonAncestor(root.left, p, q); // what did the left side find?
TreeNode right = lowestCommonAncestor(root.right, p, q); // and the right side?
if (left != null && right != null) return root; // one on each side: I am the LCA
return left != null ? left : right; // pass up whatever was found
Time O(n)Space O(h)
Each node is visited once, and each call only combines two child answers.
"The node where two things meet" in a plain binary tree = post-order DFS that returns what it found. "Found on both sides" marks the meeting point.
Return early when root == p || root == q, even without looking below. If q is inside p's subtree, p is the answer, and this early return is exactly what makes that work (p = 5, q = 4 → 5). Compare nodes with ==, not values.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 113Path Sum IIMediumDFS + BacktrackingLeetCode
Return every root-to-leaf path whose values add up to targetSum, as lists of values. (Beginner LC 112 only asked yes or no.)
Example: root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, 5, 1], targetSum = 22 → [[5, 4, 11, 2], [5, 8, 4, 5]].
"Return all paths" means you must remember the path you are on, not just a running sum. Whenever a problem says "all", think backtracking: add a step, explore, then undo the step so one shared list can be reused.
1Brute force — build every path, then filter
Collect every root-to-leaf path, copying the list at each node so paths don't interfere. Then keep the ones with the right sum.
List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> all = new ArrayList<>();
collect(root, new ArrayList<>(), all); // step A: every root-to-leaf path
List<List<Integer>> res = new ArrayList<>();
for (List<Integer> path : all) { // step B: keep the ones that sum right
int sum = 0;
for (int x : path) sum += x;
if (sum == targetSum) res.add(path);
}
return res;
Time O(n·h)Space O(n·h)
A fresh copy at every node costs O(h) each, and it keeps every path in memory even though most are thrown away.
2Optimal — one shared path: choose, explore, un-choose
Keep one list for the current path. Going down, add the node. At a leaf with remain == 0, save a copy. On the way back up, remove the node you added, so the list is exactly as your parent left it. Only real answers ever get copied.
List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
dfs(root, targetSum, new ArrayList<>(), res);
return res;
Time O(n + a·h)Space O(h)
Each node is added and removed once. Copies (length h) happen only for the a answer paths. Extra space is the path plus the recursion, not counting the output.
Backtracking shape: add → recurse → remove. Copy the shared list only when you save an answer. This same shape solves subsets, permutations and combination sum.
res.add(path) without new ArrayList<>(path) stores a reference to the shared list, which ends up empty, so you return [[], []]. Also make sure the remove runs on every path out of the function, including the leaf case.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 105Construct Binary Tree from Preorder and Inorder TraversalMediumDivide & ConquerLeetCode
You get two lists of the same tree: its preorder (me, left, right) and its inorder (left, me, right). All values are different. Rebuild the tree.
Example: preorder = [3, 9, 20, 15, 7], inorder = [9, 3, 15, 20, 7] → [3, 9, 20, null, null, 15, 7].
Two traversals with unique values: preorder tells you who the root is (always first), and inorder tells you who is left and who is right of it. Once you split into left part and right part, each part is the same problem again, only smaller. That is divide and conquer.
1Brute force — search and copy arrays
Root = preorder[0]. Find it in inorder by scanning. Everything left of it is the left subtree (say mid values), so the next mid preorder values belong to the left too. Copy the pieces and recurse.
TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0) return null;
TreeNode root = new TreeNode(preorder[0]); // first in preorder = the root
int mid = 0;
while (inorder[mid] != preorder[0]) mid++; // linear search for the root
// mid values sit left of the root in inorder -> that is the left subtree size
root.left = buildTree(Arrays.copyOfRange(preorder, 1, mid + 1),
Arrays.copyOfRange(inorder, 0, mid));
root.right = buildTree(Arrays.copyOfRange(preorder, mid + 1, preorder.length),
Arrays.copyOfRange(inorder, mid + 1, inorder.length));
return root;
Time O(n²)Space O(n²)
The scan and the array copies are O(n) at each node. On a line-shaped tree that adds up to O(n²) time and memory.
2Optimal — HashMap for positions, index ranges instead of copies
Two fixes. 1: put every inorder value in a HashMap (value → index), so finding the root is O(1). 2: never copy: describe a subtree by an index range [lo, hi] of inorder, and read roots from preorder with one pointer that only moves forward. Preorder lists the root, then its whole left side, then its right side, so building left before right uses the values in exactly that order.
int preIdx = 0; // next unused preorder value
Map<Integer, Integer> pos = new HashMap<>(); // value -> index in inorder
TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) pos.put(inorder[i], i);
return build(preorder, 0, inorder.length - 1);
Time O(n)Space O(n)
Each value is placed once with an O(1) map lookup. The map is O(n) and the recursion is O(h).
Preorder's first value is the root (postorder's last is). Inorder splits left from right. Use a value → index map plus index ranges instead of searching and copying.
Build root.left before root.right. The shared preIdx pointer hands out roots in preorder (root, left, right). Swap the two lines and the right subtree steals the left's roots.
Pattern notes: Binary Trees · Open on LeetCode
Heaps & Top K
The Top K pattern: keep a heap of size k, push every item, and pop whenever the size goes over k. The heap's top is always the weakest item you are keeping, so it is the one that gets kicked out (a min-heap to keep the k biggest, a max-heap to keep the k smallest). In Beginner the heap held plain numbers. Here the harder part is the comparator: order by a count from a HashMap, break ties by word, order by distance, store {value, row, col} arrays to merge sorted rows, or pair the heap with a cooldown queue.
LC 347Top K Frequent ElementsMediumTop K HeapLeetCode
Return the k numbers that appear most often. The answer is unique and can be in any order.
Example: nums = [1, 1, 1, 2, 2, 3], k = 2 → [1, 2] (1 appears 3 times, 2 appears twice).
"k most frequent" = two steps. "Frequent" means count with a HashMap (the Beginner counting trick). "Top k" means you don't need everything sorted, only the best k. That is exactly what a size-k heap gives you.
1Brute force — count, then sort everything by count
Count each number, sort all distinct numbers by count from high to low, and take the first k.
int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int x : nums) count.merge(x, 1, Integer::sum); // count[x] += 1
List<Integer> keys = new ArrayList<>(count.keySet());
keys.sort((a, b) -> count.get(b) - count.get(a)); // sort ALL numbers, most frequent first
int[] res = new int[k];
for (int i = 0; i < k; i++) res[i] = keys.get(i);
return res;
Time O(n log n)Space O(n)
Sorting puts every number in order, but we only asked for the top k. When k is small, that is a lot of wasted ordering.
2Optimal — min-heap of size k
Think of k seats in a club. The weakest member sits by the door (the heap top). Each new number walks in. If there are now k + 1 people, the least frequent one on top gets thrown out. After everyone has tried, the k people still inside are the most frequent. The heap never grows past k + 1, so each step costs log k.
int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int x : nums) count.merge(x, 1, Integer::sum);
// min-heap by count: the WEAKEST of the kept numbers sits on top
PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> count.get(a) - count.get(b));
for (int x : count.keySet()) {
heap.offer(x);
if (heap.size() > k) heap.poll(); // too many? kick out the least frequent
}
int[] res = new int[k];
for (int i = k - 1; i >= 0; i--) res[i] = heap.poll(); // weakest comes out first
return res;
Time O(n log k)Space O(n)
Counting is O(n). Each of the (at most n) distinct numbers costs one push and maybe one pop on a heap of size k.
"Top k largest" → min-heap of size k ("top k smallest" → max-heap). The heap keeps the opposite of what you want on top, so it can throw it out. (Bonus: bucket sort by count gets O(n), because a count is never more than n.)
Push the distinct keys (count.keySet()), not every element of nums. Pushing each 1 of [1, 1, 1] separately puts duplicates in the heap and you can return [1, 1].
Pattern notes: Top K Elements & K-way Merge · Open on LeetCode
LC 692Top K Frequent WordsMediumTop K HeapLeetCode
Return the k most frequent words, sorted: higher count first, and words with the same count in alphabetical order.
Example: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2 → ["i", "love"] (both appear twice, and "i" comes before "love" alphabetically).
Same "k most frequent" as LC 347, so count + size-k heap. The new part is "sorted by frequency, then lexicographical order". That is a two-level comparator, and you have to flip it carefully because the heap keeps the worst word on top.
1Brute force — count, sort every word, take k
Sort all distinct words by (count high to low, then word A to Z), then take the first k.
List<String> topKFrequent(String[] words, int k) {
Map<String, Integer> count = new HashMap<>();
for (String w : words) count.merge(w, 1, Integer::sum);
List<String> all = new ArrayList<>(count.keySet());
all.sort((a, b) -> count.get(a).equals(count.get(b))
? a.compareTo(b) // same count: alphabetical
: count.get(b) - count.get(a)); // otherwise: higher count first
return all.subList(0, k);
Time O(n log n)Space O(n)
Simple and correct, but it sorts every word. LeetCode's follow-up asks for O(n log k).
2Optimal — size-k heap with a reversed tie-break
The heap top must be the word we would drop first. That is the word with the lowest count, and on a tie, the alphabetically LATER word (it is "worse", so it sits on top). So the tie-break is b.compareTo(a), the opposite of the final order. At the end, the heap pops worst to best, so reverse the list.
List<String> topKFrequent(String[] words, int k) {
Map<String, Integer> count = new HashMap<>();
for (String w : words) count.merge(w, 1, Integer::sum);
// heap top = the WORST kept word: lowest count, or on a tie the alphabetically LATER word
PriorityQueue<String> heap = new PriorityQueue<>((a, b) -> count.get(a).equals(count.get(b))
? b.compareTo(a) // tie: later word is "worse"
: count.get(a) - count.get(b)); // fewer = worse
for (String w : count.keySet()) {
heap.offer(w);
if (heap.size() > k) heap.poll(); // drop the worst
}
List<String> res = new ArrayList<>();
while (!heap.isEmpty()) res.add(heap.poll()); // comes out worst-first...
Collections.reverse(res); // ...so flip it to best-first
return res;
Time O(n log k)Space O(n)
The map holds every distinct word, but the heap never holds more than k + 1 words, so each push or pop costs log k.
For a size-k heap, write the comparator as "which item is worse?". The worst goes on top. Then flip the result at the end, because pops come out worst first.
Using a.compareTo(b) for the tie (the "natural" order) keeps the wrong word: with k = 1 and ["b", "a"] you would return ["b"] instead of ["a"]. Also compare counts with .equals, not ==: they are Integer objects.
Pattern notes: Top K Elements & K-way Merge · Open on LeetCode
LC 973K Closest Points to OriginMediumTop K HeapLeetCode
You get points [x, y] on a grid. Return the k points closest to (0, 0), in any order.
Example: points = [[3, 3], [5, -1], [-2, 4]], k = 2 → [[3, 3], [-2, 4]] (squared distances 18, 26, 20).
"k closest" is "top k smallest" by distance. "In any order" is a hint that you don't have to sort them, only pick them. Keeping the k smallest means a max-heap of size k, so the farthest kept point is on top, ready to be kicked out.
1Brute force — sort all points by distance
Sort every point by x² + y² and return the first k.
int[][] kClosest(int[][] points, int k) {
int[][] sorted = points.clone();
Arrays.sort(sorted, (a, b) -> Integer.compare(dist(a), dist(b))); // sort everything
return Arrays.copyOfRange(sorted, 0, k);
Time O(n log n)Space O(n)
We fully order all n points when we only need k of them, and not even in order.
2Optimal — max-heap of size k
Keep the k closest points seen so far in a heap whose top is the farthest of them. Each new point goes in. If that makes k + 1, pop the top: the farthest one can't be in the answer. When all points are done, the heap holds the k closest.
int[][] kClosest(int[][] points, int k) {
// MAX-heap: the FARTHEST kept point sits on top, ready to be kicked out
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(dist(b), dist(a)));
for (int[] p : points) {
heap.offer(p);
if (heap.size() > k) heap.poll(); // more than k? drop the farthest
}
return heap.toArray(new int[0][]); // any order is accepted
Time O(n log k)Space O(k)
n pushes and pops on a heap that never holds more than k + 1 points. (Quickselect can reach O(n) on average, but the heap is the expected interview answer.)
"k smallest / closest / cheapest" → max-heap of size k. Compare squared distances: the order is the same as with real distances, and you avoid slow, inexact Math.sqrt.
Mixing up the heap direction. A min-heap here would throw out the closest point every time and return the k farthest. For a max-heap, the comparator is (a, b) -> dist(b) - dist(a): b comes first.
Pattern notes: Top K Elements & K-way Merge · Open on LeetCode
LC 378Kth Smallest Element in a Sorted MatrixMediumK-way MergeLeetCode
An n × n matrix has every row sorted left to right and every column sorted top to bottom. Return the k-th smallest value overall (duplicates count separately).
Example: matrix = [[1, 5, 9], [10, 11, 13], [12, 13, 15]], k = 8 → 13 (sorted: 1, 5, 9, 10, 11, 12, 13, 13, 15).
"Several sorted lists" (here: n sorted rows) plus "k-th smallest" = k-way merge. It is "merge two sorted lists" from Beginner, but with n lists: a min-heap holds the front of each list, so the smallest remaining value is always on top.
1Brute force — flatten and sort
Copy all n² values into one array, sort it, and return index k − 1.
int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
int[] all = new int[n * n];
int i = 0;
for (int[] row : matrix) for (int x : row) all[i++] = x; // flatten
Arrays.sort(all); // ignore that rows were sorted
return all[k - 1];
Time O(n² log n)Space O(n²)
It throws away the fact that the rows are already sorted, and it sorts all n² values even when k is small.
2Optimal — min-heap over the row fronts
Put the first value of each row in a min-heap, remembering its row and column. Pop the smallest. When you pop a value, the next value in the same row steps up to take its place. After k − 1 pops, the heap top is the k-th smallest. The heap never holds more than n items, one per row.
int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
// min-heap of {value, row, col}; Integer.compare avoids overflow on big negatives
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
for (int r = 0; r < Math.min(n, k); r++) {
heap.offer(new int[]{matrix[r][0], r, 0}); // the head of each row
}
for (int i = 0; i < k - 1; i++) { // throw away the k-1 smallest
int[] top = heap.poll();
int r = top[1], c = top[2];
if (c + 1 < n) heap.offer(new int[]{matrix[r][c + 1], r, c + 1}); // next in that row steps up
}
return heap.peek()[0]; // the k-th smallest
Time O(k log n)Space O(n)
k pops and pushes on a heap of at most n entries (only min(n, k) rows are ever started). A binary search on the value range is another well-known answer.
K-way merge: heap of {value, listIndex, position}, one entry per list. Pop the smallest, push the next item from the same list. Same code for "merge k sorted lists" (LC 23) and "smallest range" (LC 632).
Values go down to −10⁹, so the lazy comparator a[0] - b[0] can overflow and put the heap in the wrong order. Use Integer.compare(a[0], b[0]). And pop only k − 1 times, then peek, or you are off by one.
Pattern notes: Top K Elements & K-way Merge · Open on LeetCode
LC 621Task SchedulerMediumGreedy + HeapLeetCode
A CPU runs tasks named with letters, one per time unit. Two runs of the same letter must be at least n units apart. The CPU may sit idle. Return the least number of units needed to finish all tasks (in any order).
Example: tasks = [A, A, A, B, B, B], n = 2 → 8: A B idle A B idle A B.
"Cooldown", "same task at least n apart" and "least time" point to a greedy choice: at each moment, run the task with the most copies left, because that is the one most likely to force idle time later. "Pick the biggest, again and again" is what a max-heap is for.
1First way — simulate with a max-heap and a cooldown queue
Tick the clock one unit at a time. Run the ready task with the most copies left, then park it in a queue until its cooldown is over. If nothing is ready, the CPU idles.
int leastInterval(char[] tasks, int n) {
int[] count = new int[26];
for (char t : tasks) count[t - 'A']++;
PriorityQueue<Integer> ready = new PriorityQueue<>(Collections.reverseOrder()); // max-heap
for (int c : count) if (c > 0) ready.offer(c);
Deque<int[]> cooling = new ArrayDeque<>(); // {copies left, time it is ready again}
int time = 0;
while (!ready.isEmpty() || !cooling.isEmpty()) {
time++;
if (!ready.isEmpty()) {
int left = ready.poll() - 1; // run the task with the most copies left
if (left > 0) cooling.offer(new int[]{left, time + n});
} // else: the CPU sits idle this unit
if (!cooling.isEmpty() && cooling.peek()[1] == time) {
ready.offer(cooling.poll()[0]); // cooldown over: back into the heap
}
}
return time;
Time O(T · log 26)Space O(26)
Correct, and a great first answer, but the loop runs once per time unit T, including every idle unit. With a big n that is up to about tasks × (n + 1) steps.
2Optimal — count the frame around the most common task
Only the most frequent task can force idle time. Lay out its copies with n gaps between them: that makes maxFreq − 1 full frames of n + 1 slots, plus a last frame holding every task that ties for the top count. Everything else fits into the gaps. answer = max((maxFreq − 1) × (n + 1) + maxCount, tasks.length). The max handles the case where there are so many tasks that no idle slot is needed.
int leastInterval(char[] tasks, int n) {
int[] count = new int[26];
for (char t : tasks) count[t - 'A']++;
int maxFreq = 0;
for (int c : count) maxFreq = Math.max(maxFreq, c);
int maxCount = 0; // how many letters share that top count
for (int c : count) if (c == maxFreq) maxCount++;
int frame = (maxFreq - 1) * (n + 1) + maxCount; // skeleton built around the busiest task
return Math.max(frame, tasks.length); // lots of tasks = no idle slots at all
Time O(m)Space O(1)
One pass to count the m tasks, then two passes over 26 letters. No clock ticking at all.
Scheduling with a cooldown → greedy "most remaining first" (max-heap + waiting queue). If only the count is asked, look for a formula built around the most frequent item. The same idea solves "Reorganize String" (LC 767).
Forgetting maxCount (several tasks tied for the top count all need a slot in the last frame), or forgetting the final max(…, tasks.length). Without it the second example returns 8, which is less than the 10 tasks you have to run.
Pattern notes: Heaps & Priority Queues · Open on LeetCode
Graphs: BFS, DFS, Union-Find, Topological Sort
The pattern is graph traversal. The template: start from one or more cells or nodes, mark each one visited the moment you reach it, then visit its neighbours with a queue (BFS) or recursion (DFS). In Beginner you flooded one island at a time. Here the jump is choosing where to start (many sources at once, or backwards from the border), what to keep in a map while you walk, and three new tools: union-find for "are these already connected?", topological sort for "what order respects the rules?", and Dijkstra for shortest paths with weights.
LC 695Max Area of IslandMediumGrid DFSLeetCode
A grid holds 1 (land) and 0 (water). An island is land joined up, down, left or right. The area of an island is how many cells it has. Return the biggest area, or 0 if there is no land.
Example: [[1,1,0,0,1],[1,0,0,1,1],[0,0,0,1,1]] → 5. The top-left island has 3 cells, the right island has 5.
"Grid", "connected 4 ways" and "island" mean flood fill, exactly like Number of Islands (LC 200). The only new part is the word "area": instead of counting islands, each flood has to return a number (how many cells it covered), and you keep the biggest one.
1Brute force — measure the island again from every land cell
For every land cell, start a fresh BFS with a brand-new seen grid and count the cells you reach. That count is the area of the island this cell belongs to. Keep the maximum.
int maxAreaOfIsland(int[][] grid) {
int m = grid.length, n = grid[0].length, best = 0;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] != 1) continue;
boolean[][] seen = new boolean[m][n]; // new visited grid every time
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{r, c});
seen[r][c] = true;
int size = 0;
while (!q.isEmpty()) {
int[] cell = q.poll();
size++;
for (int[] d : dirs) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n
&& grid[nr][nc] == 1 && !seen[nr][nc]) {
seen[nr][nc] = true;
q.offer(new int[]{nr, nc});
}
}
}
best = Math.max(best, size);
}
}
return best;
}
Time O((m·n)²)Space O(m·n)
An island of 1,000 cells gets measured 1,000 times, once from each of its cells. All that work gives the same answer again and again.
2Optimal — sink each island once and let DFS return its size
Measure each island only once. When the scan hits land, run a DFS that sinks the island (turns 1 into 0) and returns 1 + the areas of its four neighbours. Water and already-sunk cells return 0, so every cell is counted exactly once.
int maxAreaOfIsland(int[][] grid) {
int best = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
best = Math.max(best, area(grid, r, c)); // sink it and measure it
}
}
}
return best;
}
int area(int[][] grid, int r, int c) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) return 0;
if (grid[r][c] != 1) return 0; // water or already sunk
grid[r][c] = 0; // sink BEFORE the recursive calls
return 1 + area(grid, r + 1, c) + area(grid, r - 1, c)
+ area(grid, r, c + 1) + area(grid, r, c - 1);
}
Time O(m·n)Space O(m·n)
Every cell is sunk at most once, so the whole grid costs one pass. The space is the recursion stack when one island fills the map.
A DFS can return a value about the region it covers: size, sum, "does it touch the edge". Build it as 1 + the answers of the neighbours.
Sink the cell before calling the neighbours. If you sink after, two neighbours call back into each other forever and you get a StackOverflowError.
Pattern notes: Matrix & Islands · Open on LeetCode
LC 994Rotting OrangesMediumMulti-source BFSLeetCode
Each cell is 0 (empty), 1 (fresh orange) or 2 (rotten orange). Every minute, each rotten orange rots the fresh oranges next to it (up, down, left, right). How many minutes until no fresh orange is left? If some fresh orange can never be reached, return -1.
Example: [[2,1,1],[1,1,0],[0,1,1]] → 4. With [[2,1,1],[0,1,1],[1,0,1]] the bottom-left orange is cut off, so the answer is -1.
"Every minute it spreads to neighbours" is BFS: one BFS level = one minute. The key words are "each rotten orange": the spreading starts from many places at the same time. That means multi-source BFS: put all starting points in the queue before the loop begins.
1Brute force — simulate the whole grid minute by minute
Each minute, scan the whole grid. For every rotten orange, mark its fresh neighbours as rotten in a copy (they only start rotting the next minute). Stop when nothing changes. Then check if any fresh orange is left.
int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length, minutes = 0;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (true) {
int[][] next = new int[m][];
for (int r = 0; r < m; r++) next[r] = grid[r].clone();
boolean changed = false;
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] != 2) continue;
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == 1) {
next[nr][nc] = 2; // rots NEXT minute, not this one
changed = true;
}
}
}
}
if (!changed) break;
grid = next;
minutes++;
}
for (int[] row : grid) for (int v : row) if (v == 1) return -1;
return minutes;
}
Time O((m·n)²)Space O(m·n)
A long snake of oranges can take m·n minutes to rot, and every minute re-scans the full grid, even the cells that rotted long ago.
2Optimal — multi-source BFS, one level per minute
Only the oranges that rotted last minute can spread something new. So keep exactly those in a queue. Start with all rotten oranges in the queue at once, then process the queue level by level: everything in the queue now is "this minute", and what they infect is "next minute". Count fresh oranges up front so you know when you are done.
int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length, fresh = 0;
Queue<int[]> q = new ArrayDeque<>();
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] == 2) q.offer(new int[]{r, c}); // ALL rotten start together
else if (grid[r][c] == 1) fresh++;
}
}
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
int minutes = 0;
while (!q.isEmpty() && fresh > 0) {
int size = q.size(); // everything rotten at this minute
for (int i = 0; i < size; i++) {
int[] cell = q.poll();
for (int[] d : dirs) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == 1) {
grid[nr][nc] = 2; // mark when adding, not when polling
fresh--;
q.offer(new int[]{nr, nc});
}
}
}
minutes++; // one BFS level = one minute
}
return fresh == 0 ? minutes : -1;
}
Time O(m·n)Space O(m·n)
Each orange enters the queue once and is polled once. The size snapshot is what separates one minute from the next.
"Spreads from several places at once" or "distance to the nearest X" → multi-source BFS: put every source in the queue first, then run one normal BFS. Level by level = minute by minute.
Loop only while fresh > 0. If you loop until the queue is empty, the last level adds one extra minute that rotted nothing. And a grid with no fresh oranges at all must return 0, not -1.
Pattern notes: Matrix & Islands · Open on LeetCode
LC 130Surrounded RegionsMediumBorder DFSLeetCode
A board holds 'X' and 'O'. A group of connected 'O' cells is captured (turned into 'X') if it is completely surrounded by 'X'. A group that touches the edge of the board can never be surrounded, so it stays. Change the board in place.
Example: ["XXXX","XOOX","XXOX","XOXX"] → ["XXXX","XXXX","XXXX","XOXX"]. The O on the bottom edge survives.
"Connected regions" means flood fill. The clue is the rule about the border: a region is safe if and only if it touches the edge. Checking "can I escape?" from every cell is slow; the flip side, "start from the edge and see what you can reach", is one pass. When the condition is about the boundary, flood from the boundary.
1Brute force — ask every O "can you escape to the edge?"
For each 'O', run a BFS through 'O' cells. If it reaches any border cell, it is safe. If not, remember to flip it. Flip at the end so the checks all see the original board.
void solve(char[][] board) {
int m = board.length, n = board[0].length;
boolean[][] flip = new boolean[m][n];
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (board[r][c] == 'O' && !reachesBorder(board, r, c)) flip[r][c] = true;
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (flip[r][c]) board[r][c] = 'X';
}
boolean reachesBorder(char[][] board, int sr, int sc) {
int m = board.length, n = board[0].length;
boolean[][] seen = new boolean[m][n];
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{sr, sc});
seen[sr][sc] = true;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (!q.isEmpty()) {
int[] cell = q.poll();
int r = cell[0], c = cell[1];
if (r == 0 || c == 0 || r == m - 1 || c == n - 1) return true; // escaped
for (int[] d : dirs) { // inside cell: all 4 neighbours exist
int nr = r + d[0], nc = c + d[1];
if (board[nr][nc] == 'O' && !seen[nr][nc]) {
seen[nr][nc] = true;
q.offer(new int[]{nr, nc});
}
}
}
return false;
}
Time O((m·n)²)Space O(m·n)
A big trapped region is explored once for each of its cells. Same waste as measuring an island again from every cell.
2Optimal — flood from the border, then flip what is left
Turn the question around. Instead of "which O's are trapped?", ask "which O's can reach the edge?". Start a DFS from every 'O' on the border and mark what it reaches as safe ('S'). After that, any 'O' still on the board could not be reached from the edge, so it is trapped: flip it to 'X'. Finally turn every 'S' back into 'O'.
void solve(char[][] board) {
int m = board.length, n = board[0].length;
// 1. flood from every border 'O' and mark it safe as 'S'
for (int r = 0; r < m; r++) { mark(board, r, 0); mark(board, r, n - 1); }
for (int c = 0; c < n; c++) { mark(board, 0, c); mark(board, m - 1, c); }
// 2. anything still 'O' is trapped -> 'X'; every 'S' goes back to 'O'
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (board[r][c] == 'O') board[r][c] = 'X';
else if (board[r][c] == 'S') board[r][c] = 'O';
}
}
}
void mark(char[][] board, int r, int c) {
if (r < 0 || r >= board.length || c < 0 || c >= board[0].length) return;
if (board[r][c] != 'O') return; // X, or already marked safe
board[r][c] = 'S';
mark(board, r + 1, c); mark(board, r - 1, c);
mark(board, r, c + 1); mark(board, r, c - 1);
}
Time O(m·n)Space O(m·n)
Each cell is marked at most once and visited once in the final sweep. The temporary 'S' letter doubles as the visited marker.
If the rule is "safe when connected to the border", flood from the border inward. Reversing the direction turns many searches into one.
Do the two steps in order. If you flip O to X while still flooding, you can flip a cell that a later border flood would have saved. Mark first, flip after.
Pattern notes: Matrix & Islands · Open on LeetCode
LC 417Pacific Atlantic Water FlowMediumReverse DFSLeetCode
A grid of heights is an island. The Pacific touches the top and left edges, the Atlantic touches the bottom and right edges. Rain flows from a cell to a neighbour that is the same height or lower. Return every cell from which water can reach both oceans.
Example: [[1,2,3],[2,4,2],[3,1,1]] → [[0,2],[1,1],[2,0]]. The top-right corner already touches both oceans, and the 4 in the middle is the peak that can run downhill both ways.
"Can reach the edge" plus "both" is two border floods and an overlap. Like Surrounded Regions, the destination is the border, so you start there and walk backwards. Backwards, "water flows down" becomes "climb to equal or higher".
1Brute force — pour water on every cell
From each cell, run a DFS that follows the water downhill (to neighbours of equal or lower height) and notes whether it touches a Pacific edge and an Atlantic edge.
int[][] DIRS = {{1,0},{-1,0},{0,1},{0,-1}};
List<List<Integer>> pacificAtlantic(int[][] heights) {
int m = heights.length, n = heights[0].length;
List<List<Integer>> res = new ArrayList<>();
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
boolean[] oceans = new boolean[2]; // [pacific, atlantic]
flowDown(heights, r, c, new boolean[m][n], oceans);
if (oceans[0] && oceans[1]) res.add(List.of(r, c));
}
}
return res;
}
// water moves to a neighbour that is NOT higher
void flowDown(int[][] h, int r, int c, boolean[][] seen, boolean[] oceans) {
seen[r][c] = true;
if (r == 0 || c == 0) oceans[0] = true;
if (r == h.length - 1 || c == h[0].length - 1) oceans[1] = true;
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < h.length && nc >= 0 && nc < h[0].length
&& !seen[nr][nc] && h[nr][nc] <= h[r][c]) {
flowDown(h, nr, nc, seen, oceans);
}
}
}
Time O((m·n)²)Space O(m·n)
On a flat grid, every single cell's DFS floods the whole grid. 200 × 200 cells means about 1.6 billion steps.
2Optimal — climb uphill from each ocean, then overlap
Run the film backwards. Start at the ocean and climb to neighbours that are the same height or higher: every cell you reach could send water down to that ocean. Do one flood from the Pacific edges and one from the Atlantic edges. The answer is the cells marked by both floods.
int[][] DIRS = {{1,0},{-1,0},{0,1},{0,-1}};
List<List<Integer>> pacificAtlantic(int[][] heights) {
int m = heights.length, n = heights[0].length;
boolean[][] pac = new boolean[m][n], atl = new boolean[m][n];
for (int r = 0; r < m; r++) {
climb(heights, r, 0, pac); // left edge touches the Pacific
climb(heights, r, n - 1, atl); // right edge touches the Atlantic
}
for (int c = 0; c < n; c++) {
climb(heights, 0, c, pac); // top edge -> Pacific
climb(heights, m - 1, c, atl); // bottom edge -> Atlantic
}
List<List<Integer>> res = new ArrayList<>();
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (pac[r][c] && atl[r][c]) res.add(List.of(r, c));
return res;
}
// walk UPHILL from the ocean: go to neighbours that are the same height or higher
void climb(int[][] h, int r, int c, boolean[][] ok) {
if (ok[r][c]) return; // this ocean already reached it
ok[r][c] = true;
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < h.length && nc >= 0 && nc < h[0].length
&& h[nr][nc] >= h[r][c]) {
climb(h, nr, nc, ok);
}
}
}
Time O(m·n)Space O(m·n)
Each flood marks every cell at most once, so two floods cost two passes over the grid.
"Which cells can reach target T?" → start at T and walk backwards, flipping the move rule (down becomes up). "Reach A and B" → two floods and an AND.
The climbing test is >=, not >. Water flows across equal heights, so a flat grid must answer "every cell", and > would miss them.
Pattern notes: Matrix & Islands · Open on LeetCode
LC 133Clone GraphMediumDFS + HashMapLeetCode
You get one node of a connected, undirected graph. Each Node has a val and a list of neighbors. Return a deep copy: brand-new nodes with the same values and the same connections. No node in the copy may be one of the original nodes.
Example: adjList = [[2,4],[1,3],[2,4],[1,3]] (a square 1-2-3-4-1) → a new square with exactly the same shape. An empty graph (null) returns null.
"Copy", "clone" or "deep copy" of something with links that can loop back means you must remember "original → its copy" in a HashMap. Without it, a cycle like 1 → 2 → 1 would keep making new copies forever. The traversal itself is a plain DFS or BFS.
1First solution — two passes: make all nodes, then wire them
Pass 1: BFS over the original graph and create one copy per node, stored in a map. Pass 2: for each original node, add the copies of its neighbours to its copy's list. Splitting the work makes it easy to see that nothing is copied twice.
Node cloneGraph(Node node) {
if (node == null) return null;
Map<Node, Node> copy = new HashMap<>();
Queue<Node> q = new ArrayDeque<>();
q.offer(node);
copy.put(node, new Node(node.val));
while (!q.isEmpty()) { // pass 1: nodes only
Node cur = q.poll();
for (Node nb : cur.neighbors) {
if (!copy.containsKey(nb)) {
copy.put(nb, new Node(nb.val));
q.offer(nb);
}
}
}
for (Node old : copy.keySet()) { // pass 2: edges
for (Node nb : old.neighbors) {
copy.get(old).neighbors.add(copy.get(nb));
}
}
return copy.get(node);
}
Time O(V + E)Space O(V)
This is already optimal: every node and edge has to be copied once. It just walks the graph twice. The standard answer does both jobs in one DFS.
2Cleaner — one DFS that returns the copy
cloneGraph(x) means "give me the copy of x". If x is already in the map, hand back that copy. Otherwise make it, put it in the map before visiting the neighbours, then fill its neighbour list with cloneGraph(neighbour). When a cycle leads back to x, the map already has it, so the recursion stops.
Map<Node, Node> made = new HashMap<>(); // original -> its copy
Node cloneGraph(Node node) {
if (node == null) return null;
if (made.containsKey(node)) return made.get(node); // already copied: reuse it
Node clone = new Node(node.val);
made.put(node, clone); // save BEFORE visiting neighbours
for (Node nb : node.neighbors) {
clone.neighbors.add(cloneGraph(nb));
}
return clone;
}
Time O(V + E)Space O(V)
Each node is created once and each edge is followed once from each side. The map and the recursion stack both hold at most V entries.
Copying anything with pointers that can form cycles (graphs, LC 138 random-pointer lists) → HashMap from original to copy. The map is both the "visited" set and the lookup for the finished copies.
Put the new node in the map before recursing into its neighbours. If you put it after the loop, the cycle 1 → 2 → 1 does not find 1 in the map and recurses until the stack overflows.
Pattern notes: Graphs & Traversal · Open on LeetCode
LC 547Number of ProvincesMediumDFS / Union-FindLeetCode
There are n cities. isConnected[i][j] == 1 means city i and city j are directly linked. A province is a group of cities linked directly or through other cities. Count the provinces.
Example: [[1,1,0],[1,1,0],[0,0,1]] → 2. Cities 0 and 1 form one province, city 2 is alone.
"Groups linked directly or indirectly" = connected components. It is Number of Islands on a graph instead of a grid. The input is an adjacency matrix (an n × n table), so a node's neighbours are the 1s in its row. Any "count the groups" question can be solved with DFS or with union-find; this is the classic problem to learn union-find on.
1First solution — DFS from each unvisited city
Scan the cities. Each unvisited city starts a new province: add 1, then DFS through its row and mark every city you can reach.
int findCircleNum(int[][] isConnected) {
int n = isConnected.length, count = 0;
boolean[] seen = new boolean[n];
for (int i = 0; i < n; i++) {
if (!seen[i]) { // a city nobody has reached yet = new province
count++;
dfs(isConnected, i, seen);
}
}
return count;
}
void dfs(int[][] g, int city, boolean[] seen) {
seen[city] = true;
for (int other = 0; other < g.length; other++) {
if (g[city][other] == 1 && !seen[other]) dfs(g, other, seen);
}
}
Time O(n²)Space O(n)
Already optimal: reading the n × n matrix costs n² anyway. So step 2 is not faster here. It is a new tool, union-find, which wins when links arrive one at a time (as in the next problem).
2Alternative — union-find (disjoint set)
Give every city a parent. At the start each city is its own parent, so there are n groups. find(x) follows parents up to the root: the group's leader. For every link i–j, find both roots. If the roots differ, merge the groups and do groups−−. If they are the same, the two cities were already together.
int[] parent;
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving: point to grandparent
x = parent[x];
}
return x; // the root = the group's leader
}
int findCircleNum(int[][] isConnected) {
int n = isConnected.length, groups = n; // start: every city alone
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // matrix is symmetric: upper half is enough
if (isConnected[i][j] == 1) {
int a = find(i), b = find(j);
if (a != b) { // two different groups meet
parent[a] = b; // merge them
groups--;
}
}
}
}
return groups;
}
Time O(n² · α(n))Space O(n)
α(n) is a tiny number (at most 4 or 5 in practice) thanks to path halving, so this is O(n²) like the DFS, with no recursion at all.
Union-find = parent array + find (walk to the root) + union (link two roots). Start with n groups and subtract 1 on every union that joins two different roots.
Merge the roots, not the cities: write parent[find(i)] = find(j), not parent[i] = j. Linking the cities directly can cut city i away from the rest of its old group.
Pattern notes: Union-Find (Disjoint Set) · Open on LeetCode
LC 684Redundant ConnectionMediumUnion-FindLeetCode
A tree with nodes 1..n had one extra edge added, so now it has exactly one cycle. You get the n edges in order. Return the edge you can remove to get a tree back. If several would work, return the one that appears last in the input.
Example: [[1,2],[2,3],[3,4],[1,4],[1,5]] → [1,4]. Edges 1-2, 2-3, 3-4 already join 1 and 4, so 1-4 closes the loop.
Edges arrive one at a time and for each one you ask "were these two nodes already connected?". That is exactly the question union-find answers in almost O(1). "Tree plus one extra edge" = "find the edge that makes a cycle".
1Brute force — before adding an edge, search the graph so far
Build the graph edge by edge. Before adding edge u–v, run a DFS from u over the edges added so far. If it already reaches v, this edge makes a cycle: return it. Since edges are checked in order, the first cycle-making edge is also the last one that works.
int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i <= n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
// already connected before this edge? then this edge closes a cycle
if (connected(adj, e[0], e[1], new boolean[n + 1])) return e;
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
return new int[0];
}
boolean connected(List<List<Integer>> adj, int from, int to, boolean[] seen) {
if (from == to) return true;
seen[from] = true;
for (int nb : adj.get(from)) {
if (!seen[nb] && connected(adj, nb, to, seen)) return true;
}
return false;
}
Time O(n²)Space O(n)
Each of the n edges may start a DFS over the whole graph built so far. With 1,000 edges that is up to a million steps, and it grows fast.
2Optimal — union-find answers "already connected?" instantly
Keep the groups in a union-find instead of searching. For each edge u–v: if find(u) == find(v), they are already in the same group, so this edge closes a cycle: return it. Otherwise union the two groups and move on.
int[] parent;
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving
x = parent[x];
}
return x;
}
int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
parent = new int[n + 1]; // nodes are 1..n
for (int i = 1; i <= n; i++) parent[i] = i;
for (int[] e : edges) {
int a = find(e[0]), b = find(e[1]);
if (a == b) return e; // same group already: cycle!
parent[a] = b; // otherwise merge the groups
}
return new int[0];
}
Time O(n · α(n))Space O(n)
Each edge costs two finds, which are almost constant time. One pass over the edges and we are done.
Union-find detects a cycle in an undirected graph for free: an edge whose two ends already have the same root is the edge that closes a cycle.
Nodes are numbered from 1, so the parent array needs size n + 1. Size n throws ArrayIndexOutOfBoundsException on node n.
Pattern notes: Union-Find (Disjoint Set) · Open on LeetCode
LC 207Course ScheduleMediumCycle Detection (DFS)LeetCode
There are numCourses courses. [a, b] in prerequisites means "to take a, you must first take b". Can you finish all courses? You can't if the rules go in a circle, like "A needs B and B needs A".
Example: numCourses = 2, [[1,0]] → true (take 0, then 1). [[1,0],[0,1]] → false (each waits for the other).
"X must come before Y" rules are directed edges (draw b → a). "Can you finish all of them?" means "is there no cycle in this directed graph?". Any time you see prerequisites, dependencies or build order, think directed graph + cycle check / topological sort.
1Brute force — from every course, walk every path looking for a loop
Build the graph b → a. From each course, DFS and keep the courses on the current path. If you step onto a course that is already on the path, you walked in a circle. When you go back up, take the course off the path.
List<List<Integer>> build(int n, int[][] pre) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] p : pre) g.get(p[1]).add(p[0]); // edge: p[1] must come before p[0]
return g;
}
boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> g = build(numCourses, prerequisites);
for (int c = 0; c < numCourses; c++) {
if (loops(g, c, new boolean[numCourses])) return false;
}
return true;
}
boolean loops(List<List<Integer>> g, int c, boolean[] onPath) {
if (onPath[c]) return true; // came back to a course on this path
onPath[c] = true;
for (int next : g.get(c)) {
if (loops(g, next, onPath)) return true;
}
onPath[c] = false; // backtrack: path no longer uses c
return false;
}
Time O(2^V) worst caseSpace O(V + E)
It never remembers that a course was already proven safe, so it re-walks the same sub-paths again and again. Graphs with many "diamonds" (two ways to reach the same course) blow up exponentially.
2Optimal — DFS with three states
Give each course one of three states: 0 = not visited, 1 = on the current path, 2 = done (no cycle below it). Reaching a state-1 course means you looped back onto your own path: a cycle. Reaching a state-2 course means "already checked, safe": skip it instead of exploring it again. That third state is the memo that makes it linear.
boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> g = build(numCourses, prerequisites); // same build() as above
int[] state = new int[numCourses]; // 0 = new, 1 = on current path, 2 = done (safe)
for (int c = 0; c < numCourses; c++) {
if (hasCycle(g, c, state)) return false;
}
return true;
}
boolean hasCycle(List<List<Integer>> g, int c, int[] state) {
if (state[c] == 1) return true; // back on the current path: cycle
if (state[c] == 2) return false; // checked before: no cycle from here
state[c] = 1;
for (int next : g.get(c)) {
if (hasCycle(g, next, state)) return true;
}
state[c] = 2; // leave the path, remember it is safe
return false;
}
Time O(V + E)Space O(V + E)
Each course goes 0 → 1 → 2 exactly once, and each edge is looked at once. The graph lists take the O(V + E) space.
Cycle in a directed graph → DFS with three colours (new / on path / done). A plain "visited" boolean is not enough: it can't tell "on my path" (cycle) from "finished earlier" (safe).
Mind the edge direction: [a, b] means b comes before a, so the edge is b → a. For the yes/no question either direction finds the same cycles, but in Course Schedule II the wrong direction gives the order backwards.
Pattern notes: Topological Sort · Open on LeetCode
LC 210Course Schedule IIMediumTopological Sort (Kahn)LeetCode
Same courses and rules as Course Schedule, but now return an order in which you can take all the courses. Any valid order is accepted. If it is impossible (a cycle), return an empty array.
Example: numCourses = 4, [[1,0],[2,0],[3,1],[3,2]] → [0,1,2,3] (or [0,2,1,3]). numCourses = 2, [[1,0],[0,1]] → [].
"Return an order that respects every before/after rule" is the definition of a topological sort. The phrase "if impossible, return empty" tells you the same algorithm must also detect cycles.
1Brute force — keep scanning for a course you can take now
This is how a person would do it: look through all courses for one whose prerequisites are all taken. Take it. Repeat n times. If at some point no course is ready, the rules form a cycle.
int[] findOrder(int numCourses, int[][] prerequisites) {
boolean[] taken = new boolean[numCourses];
int[] order = new int[numCourses];
for (int k = 0; k < numCourses; k++) {
int pick = -1;
for (int c = 0; c < numCourses && pick == -1; c++) {
if (taken[c]) continue;
boolean ready = true;
for (int[] p : prerequisites) {
if (p[0] == c && !taken[p[1]]) { ready = false; break; }
}
if (ready) pick = c;
}
if (pick == -1) return new int[0]; // nobody is ready: there is a cycle
taken[pick] = true;
order[k] = pick;
}
return order;
}
Time O(V² · E)Space O(V)
Each of the V rounds re-checks every course against every rule. Most of that work repeats: a course's missing-prerequisite count only changes when one of its prerequisites is taken.
2Optimal — Kahn's algorithm (BFS on in-degrees)
Instead of re-checking, keep a count of missing prerequisites per course (its in-degree). Every course with count 0 is ready: put it in a queue. Take a course from the queue, add it to the order, and subtract 1 from each course it unlocks; any that hits 0 joins the queue. If the order ends up shorter than n, some courses were stuck in a cycle.
int[] findOrder(int numCourses, int[][] prerequisites) {
List<List<Integer>> next = new ArrayList<>();
for (int i = 0; i < numCourses; i++) next.add(new ArrayList<>());
int[] indeg = new int[numCourses]; // how many prereqs still missing
for (int[] p : prerequisites) {
next.get(p[1]).add(p[0]); // after p[1] you may unlock p[0]
indeg[p[0]]++;
}
Queue<Integer> ready = new ArrayDeque<>();
for (int c = 0; c < numCourses; c++) {
if (indeg[c] == 0) ready.offer(c); // no prereqs: can take now
}
int[] order = new int[numCourses];
int k = 0;
while (!ready.isEmpty()) {
int c = ready.poll();
order[k++] = c; // take the course
for (int n : next.get(c)) {
if (--indeg[n] == 0) ready.offer(n); // its last prereq just got done
}
}
return k == numCourses ? order : new int[0]; // fewer than all = cycle
}
Time O(V + E)Space O(V + E)
Each course enters the queue once and each edge lowers one count once. The same code answers Course Schedule I too: return k == numCourses.
Kahn's algorithm: count in-degrees, queue the zeros, pop and decrement, queue new zeros. Output length < n means there is a cycle. It is the brute force "take whatever is ready" with the rescanning replaced by counters.
Courses with no rules at all still belong in the order. They start with in-degree 0, so the first loop must check every course, not only those that appear in prerequisites.
Pattern notes: Topological Sort · Open on LeetCode
LC 743Network Delay TimeMediumDijkstraLeetCode
There are n nodes 1..n. times[i] = [u, v, w] means a signal goes from u to v in w time (one way). A signal starts at node k. How long until every node has received it? If some node can never get it, return -1.
Example: times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2 → 2. Node 4 is the last to hear, at time 2.
"Shortest time from one start to every node" with different weights on the edges is Dijkstra. Plain BFS only works when every edge costs the same. "Until all nodes receive it" means: find each node's shortest time, then take the largest one.
1Brute force — Bellman-Ford: relax every edge, many rounds
Start with dist[k] = 0 and infinity elsewhere. In one round, go over every edge u → v and ask "is going through u faster than what v has?". A shortest path uses at most n − 1 edges, so n − 1 rounds are always enough.
int networkDelayTime(int[][] times, int n, int k) {
int INF = Integer.MAX_VALUE;
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[k] = 0;
for (int round = 1; round < n; round++) {
for (int[] t : times) {
int u = t[0], v = t[1], w = t[2];
if (dist[u] != INF && dist[u] + w < dist[v]) dist[v] = dist[u] + w;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == INF) return -1;
ans = Math.max(ans, dist[i]);
}
return ans;
}
Time O(V · E)Space O(V)
Correct, but every round re-checks every edge even when almost nothing changes. (Bellman-Ford's real use is graphs with negative weights, which this problem does not have.)
2Optimal — Dijkstra with a min-heap
BFS with a priority queue instead of a plain queue. Always expand the node with the smallest known time next. With no negative weights, nothing found later can beat it, so its time is final. From it, try each edge; if you found a faster time for a neighbour, update it and push it. Old, slower entries left in the heap are skipped when popped.
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]}); // u -> (v, w)
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]); // (node, time)
pq.offer(new int[]{k, 0});
while (!pq.isEmpty()) {
int[] top = pq.poll();
int u = top[0], d = top[1];
if (d > dist[u]) continue; // old, stale entry: skip it
for (int[] e : g.get(u)) {
int v = e[0], nd = d + e[1];
if (nd < dist[v]) { // found a faster way to v
dist[v] = nd;
pq.offer(new int[]{v, nd});
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) return -1; // never reached
ans = Math.max(ans, dist[i]); // last node to hear it
}
return ans;
}
Time O(E log V)Space O(V + E)
Every edge can push at most one heap entry, and each push or pop costs log of the heap size. The stale-entry check keeps each node's edges from being expanded twice.
Weighted shortest path, no negative weights → Dijkstra = BFS with a min-heap on distance. Push (node, time); on pop, skip if the time is worse than dist[node].
Do not mark a node "visited" when you push it, like in plain BFS. A later, cheaper path may still arrive (node 2 above got 4 first, then 3). A node's time is final only when it is popped.
Pattern notes: Shortest Paths · Open on LeetCode
Backtracking: Subsets, Permutations, Combinations
Backtracking builds answers one choice at a time and walks a decision tree with DFS. Every problem below uses the same three-line heart: choose (add something to path), explore (recurse), un-choose (remove it again so the next branch starts clean). The only things that change are what the choices are, when a path is finished, and which branches you cut early (pruning). What is harder than Beginner: the answer list itself is exponential (2n subsets, n! permutations), so "brute force vs optimal" is usually not O(n²) vs O(n). It is generate everything and filter vs never walk into a bad branch.
void backtrack(int start, List<Integer> path) {
if (/* path is a finished answer */) {
res.add(new ArrayList<>(path)); // save a COPY, path keeps changing
return; // (subsets: record every node and don't return)
}
for (int i = start; i < choices.length; i++) {
if (/* choice i can never work */) continue; // prune (or break if sorted)
path.add(choices[i]); // 1. choose
backtrack(i + 1, path); // 2. explore (i + 1: use once, i: reuse, 0: order matters)
path.remove(path.size() - 1); // 3. un-choose
}
}
LC 78SubsetsMediumBacktrackingLeetCode
You get an array of distinct numbers. Return every possible subset: every way to pick some of them, including picking none and picking all. Order inside a subset, and the order of subsets, don't matter.
Example: nums = [1, 2, 3] → [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]] (8 subsets).
"Return all possible subsets / combinations / arrangements" means you must list every answer, not count them or find the best one. When the output itself is a list of lists, there is no clever formula: you have to build each answer piece by piece. That is backtracking. Small limits (here n ≤ 10) are the second hint: the problem expects exponential work.
1Brute force — one bitmask per subset
Each number is either in or out, so there are 2n subsets. Count mask from 0 to 2n − 1 and read its bits: bit i on means "take nums[i]". (This is the bit trick from Beginner.)
List<List<Integer>> subsets(int[] nums) {
int n = nums.length;
List<List<Integer>> res = new ArrayList<>();
for (int mask = 0; mask < (1 << n); mask++) { // 2^n masks, one per subset
List<Integer> subset = new ArrayList<>();
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) subset.add(nums[i]); // bit i on = take nums[i]
}
res.add(subset);
}
return res;
}
Time O(n · 2ⁿ)Space O(n) extra (plus the output)
Honest note: this is already optimal. There are 2n subsets and each can be n long, so nobody can beat O(n · 2n). We still learn the backtracking version because it is the template that works for every other card here, where bitmasks stop working.
2Optimal — the choose / explore / un-choose template
Grow a path. From the current node, try adding each number after the last one you used (start), so [1,2] and [2,1] can never both appear. Here every node of the tree is a subset, so you record the path the moment you enter the function, not only at the leaves.
List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), res);
return res;
}
void backtrack(int[] nums, int start, List<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path)); // every node of the tree is a subset
for (int i = start; i < nums.length; i++) {
path.add(nums[i]); // choose
backtrack(nums, i + 1, path, res); // explore (only later numbers)
path.remove(path.size() - 1); // un-choose
}
}
Time O(n · 2ⁿ)Space O(n) recursion (plus the output)
Same cost as the bitmask, but this shape bends: add a skip rule and you get Subsets II, add a sum check and you get Combination Sum.
start = "only look forward". It is what turns permutations (order matters) into combinations and subsets (order doesn't).
Write res.add(new ArrayList<>(path)), not res.add(path). There is only one path object; if you store it directly, every saved answer is the same list, and it is empty by the end.
Pattern notes: Subsets, Permutations & Combinations · Open on LeetCode
LC 90Subsets IIMediumBacktracking + Skip DuplicatesLeetCode
Same as Subsets, but now the array may contain repeated numbers. Return every subset, but no subset may appear twice.
Example: nums = [1, 2, 2] → [[], [1], [1,2], [1,2,2], [2], [2,2]]. Note there is only one [2] and one [1,2].
"All subsets" = backtracking. "May contain duplicates" + "must not contain duplicate subsets" = sort first, then skip equal neighbours. Any time a list-everything problem has repeated inputs, expect this exact skip line.
1Brute force — make all 2ⁿ subsets, throw away copies with a HashSet
Sort the array so equal subsets look identical ([1,2], never [2,1]), generate every subset with a bitmask, and keep a subset only the first time the HashSet sees it.
List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // so [1,2] and [2,1] look the same
Set<List<Integer>> seen = new HashSet<>();
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for (int mask = 0; mask < (1 << n); mask++) {
List<Integer> subset = new ArrayList<>();
for (int i = 0; i < n; i++) if ((mask & (1 << i)) != 0) subset.add(nums[i]);
if (seen.add(subset)) res.add(subset); // keep only the first copy
}
return res;
}
Time O(n · 2ⁿ) alwaysSpace O(n · 2ⁿ) for the set
It builds the duplicates and then deletes them. For [2,2,2,2,2] it makes 32 subsets to keep just 6, and it hashes every one of them.
2Optimal — sort, then skip the same value at the same level
Duplicates come from picking "a 2" at the same spot in two different ways. After sorting, equal values sit side by side. In the loop, if i > start and nums[i] == nums[i-1], skip: that value was already tried at this level. Going deeper with the second 2 (i == start) is still allowed, which is how [2,2] is made.
List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // equal numbers now sit side by side
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), res);
return res;
}
void backtrack(int[] nums, int start, List<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i - 1]) continue; // same value at the same level: skip
path.add(nums[i]); // choose
backtrack(nums, i + 1, path, res); // explore
path.remove(path.size() - 1); // un-choose
}
}
Time O(n · 2ⁿ) worst, but only unique subsets are builtSpace O(n) recursion
The worst case is still exponential (all distinct numbers), but no work is wasted on copies and no HashSet is needed.
The dedupe line if (i > start && nums[i] == nums[i-1]) continue; works for every "list all, input has duplicates" problem (Subsets II, Combination Sum II). It needs the array sorted.
It is i > start, not i > 0. With i > 0 you would also block the second 2 one level deeper, and [2,2] and [1,2,2] would go missing.
Pattern notes: Subsets, Permutations & Combinations · Open on LeetCode
LC 46PermutationsMediumBacktracking + Used ArrayLeetCode
You get an array of distinct numbers. Return every ordering of all of them. Now order matters: [1,2] and [2,1] are different answers.
Example: nums = [1, 2, 3] → [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] (3! = 6).
"All possible orderings / arrangements" = permutations. Each slot can take any unused number, not just later ones, so the loop starts at 0 every time and you need a used[] array to remember what is already in the path.
1Brute force — fill every slot with anything, then filter
Try all nⁿ ways to fill n slots (each slot picks any index, repeats allowed), by reading a counter as n digits in base n. Keep only the sequences where no index repeats.
List<List<Integer>> permute(int[] nums) {
int n = nums.length;
List<List<Integer>> res = new ArrayList<>();
int total = 1;
for (int i = 0; i < n; i++) total *= n; // n^n ways to fill n slots
for (int code = 0; code < total; code++) {
List<Integer> seq = new ArrayList<>();
Set<Integer> usedIdx = new HashSet<>();
int c = code;
for (int slot = 0; slot < n; slot++) { // read code as n digits in base n
int idx = c % n; c /= n;
usedIdx.add(idx);
seq.add(nums[idx]);
}
if (usedIdx.size() == n) res.add(seq); // keep it only if no index repeats
}
return res;
}
Time O(n · nⁿ)Space O(n) extra
For n = 6 that is 46,656 sequences to keep only 720. Most of the work builds sequences that were broken from their second slot.
2Optimal — backtracking with a used[] array
Fill the slots left to right. For the next slot, loop over all indexes from 0 and skip the ones marked used. Mark before recursing, unmark after (that is the un-choose step, done for two things now: the path and the used flag). A path is finished when its length is n.
List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, new boolean[nums.length], new ArrayList<>(), res);
return res;
}
void backtrack(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> res) {
if (path.size() == nums.length) { // every slot filled
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) { // start from 0: order matters
if (used[i]) continue; // already in this permutation
used[i] = true; path.add(nums[i]); // choose
backtrack(nums, used, path, res); // explore
used[i] = false; path.remove(path.size() - 1); // un-choose
}
}
Time O(n · n!)Space O(n) recursion + used[]
n! answers of length n must be written out, so O(n · n!) is the best possible. Still exponential, but tied to the output size.
Combinations: loop from start. Permutations: loop from 0 and use used[]. That one difference is the whole story.
Forgetting used[i] = false in the un-choose step. The first branch works, then every later branch thinks those numbers are taken, and you get only [[1,2,3]].
Pattern notes: Subsets, Permutations & Combinations · Open on LeetCode
LC 39Combination SumMediumBacktracking + PruningLeetCode
You get distinct positive numbers (candidates) and a target. Return every combination that adds up to the target. You may use the same number as many times as you like. [2,2,3] and [3,2,2] count as the same combination.
Example: candidates = [2, 3, 6, 7], target = 7 → [[2,2,3], [7]].
"Return all combinations that sum to target" = list everything, so backtracking. "Same number unlimited times" tells you to recurse with i (stay on this number) instead of i + 1. "Positive numbers" tells you a running sum only grows, so you can stop as soon as you pass the target.
1Brute force — try every sequence, dedupe at the end
At each step add any candidate, in any order, until the sum reaches or passes the target. Because order is free, [2,2,3], [2,3,2] and [3,2,2] are all found, so sort each hit and keep it in a HashSet.
List<List<Integer>> combinationSum(int[] candidates, int target) {
Set<List<Integer>> seen = new HashSet<>();
List<List<Integer>> res = new ArrayList<>();
dfs(candidates, target, new ArrayList<>(), seen, res);
return res;
}
void dfs(int[] cand, int remain, List<Integer> path, Set<List<Integer>> seen, List<List<Integer>> res) {
if (remain == 0) {
List<Integer> key = new ArrayList<>(path);
Collections.sort(key); // [2,3,2] and [3,2,2] -> same key
if (seen.add(key)) res.add(key);
return;
}
if (remain < 0) return;
for (int c : cand) { // any number, any order
path.add(c);
dfs(cand, remain - c, path, seen, res);
path.remove(path.size() - 1);
}
}
Time O(n^(T/m)) with many repeatsSpace O(T/m) depth + the set
(T = target, m = smallest candidate.) It walks every ordering of every combination, which is far more than the number of combinations, and it only notices "too big" after stepping past the target.
2Optimal — start index + sort + break
Two changes. First, recurse with i, not 0: you may reuse this number or move to later ones, but never go back, so each combination is built in one order only (no set needed). Second, sort the candidates, and when cand[i] > remain, break: every later candidate is bigger, so the whole rest of the loop is useless.
List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // lets us stop early
List<List<Integer>> res = new ArrayList<>();
backtrack(candidates, 0, target, new ArrayList<>(), res);
return res;
}
void backtrack(int[] cand, int start, int remain, List<Integer> path, List<List<Integer>> res) {
if (remain == 0) { // hit the target exactly
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < cand.length; i++) {
if (cand[i] > remain) break; // sorted: every later one is too big too
path.add(cand[i]); // choose
backtrack(cand, i, remain - cand[i], path, res); // explore: i, not i + 1 (reuse allowed)
path.remove(path.size() - 1); // un-choose
}
}
Time O(n^(T/m)) worst, much less in practiceSpace O(T/m) recursion
The worst-case bound is still exponential (the answer can be huge), but each combination is walked once instead of once per ordering, and dead branches are cut before they start.
The recursive index says how many times a number may be used: i = unlimited, i + 1 = once, 0 + used[] = any order. Pass remain down instead of summing the path each time.
break is only safe after sorting. On an unsorted array, [7, 2] with remain 5 would break on 7 and never try 2. Without sorting, use continue.
Pattern notes: Subsets, Permutations & Combinations · Open on LeetCode
LC 40Combination Sum IIMediumBacktracking + Skip DuplicatesLeetCode
Like Combination Sum, but two rules flip: each position can be used at most once, and the candidates may repeat. Return every unique combination that sums to the target, with no duplicate combinations.
Example: candidates = [10,1,2,7,6,1,5], target = 8 → [[1,1,6], [1,2,5], [1,7], [2,6]].
"Each number used once" → recurse with i + 1 (like Subsets). "Candidates may contain duplicates" + "no duplicate combinations" → the Subsets II skip line. "Sum to target" with positive numbers → sort and break like Combination Sum. This problem is literally those three pieces glued together.
1Brute force — check every subset's sum, dedupe with a set
Sort, then walk all 2n subsets with a bitmask. Keep a subset if its sum equals the target and the HashSet hasn't seen it yet.
List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
int n = candidates.length;
Set<List<Integer>> seen = new HashSet<>();
List<List<Integer>> res = new ArrayList<>();
for (int mask = 0; mask < (1 << n); mask++) { // every subset
List<Integer> pick = new ArrayList<>();
int sum = 0;
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) { pick.add(candidates[i]); sum += candidates[i]; }
}
if (sum == target && seen.add(pick)) res.add(pick);
}
return res;
}
Time O(n · 2ⁿ) alwaysSpace O(n) + the set
LeetCode allows up to 100 candidates. 2100 subsets will never finish, even though the real answer list is small because the target is at most 30.
2Optimal — sort, skip same value at same level, break when too big
Same template as Combination Sum with two changes: recurse with i + 1 (each position once), and skip cand[i] == cand[i-1] when i > start so two equal 1s can't both start the same branch. The break on cand[i] > remain keeps the tree small because the target is small.
List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // duplicates side by side + early stop
List<List<Integer>> res = new ArrayList<>();
backtrack(candidates, 0, target, new ArrayList<>(), res);
return res;
}
void backtrack(int[] cand, int start, int remain, List<Integer> path, List<List<Integer>> res) {
if (remain == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < cand.length; i++) {
if (i > start && cand[i] == cand[i - 1]) continue; // same value at same level: skip
if (cand[i] > remain) break; // too big, and so is the rest
path.add(cand[i]); // choose
backtrack(cand, i + 1, remain - cand[i], path, res); // explore: i + 1 (use once)
path.remove(path.size() - 1); // un-choose
}
}
Time O(n · 2ⁿ) worst, tiny in practiceSpace O(n) recursion
The worst-case bound is the same as the brute force, but the break means the depth is at most target / smallest number, so real inputs finish instantly.
Combination Sum II = Subsets II + a target. Sort once, and both tricks (skip duplicates, break early) come for free.
Using a used[] array or a HashSet here works but is the slow path. And don't write the skip as i > 0: that loses [1,1,6], because the second 1 is needed one level deeper.
Pattern notes: Subsets, Permutations & Combinations · Open on LeetCode
LC 17Letter Combinations of a Phone NumberMediumBacktrackingLeetCode
An old phone keypad maps digits to letters: 2 = abc, 3 = def, 4 = ghi, 5 = jkl, 6 = mno, 7 = pqrs, 8 = tuv, 9 = wxyz. Given a string of digits 2–9, return every word you could type by picking one letter per digit.
Example: digits = "23" → ["ad","ae","af","bd","be","bf","cd","ce","cf"]. And "" → [].
"Return all possible combinations", and each position has its own small menu of choices. That is a decision tree with one level per digit: pick a letter, move to the next digit. The depth is fixed (the number of digits), so a path is finished exactly when idx == digits.length().
1Brute force — build layer by layer (BFS style)
Start with [""]. For each digit, make a new list by adding each of its letters to every string you have so far.
String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
List<String> letterCombinations(String digits) {
if (digits.isEmpty()) return new ArrayList<>();
List<String> res = new ArrayList<>();
res.add("");
for (char d : digits.toCharArray()) { // one layer per digit
List<String> next = new ArrayList<>();
for (String prefix : res) {
for (char c : KEYS[d - '0'].toCharArray()) next.add(prefix + c);
}
res = next; // whole layer kept in memory
}
return res;
}
Time O(n · 4ⁿ)Space O(n · 4ⁿ) for the layers
Honest note: this is not slower in Big-O. There are up to 4n answers of length n, so both versions are O(n · 4n). The layer approach just keeps every half-built string alive and makes a new String at every step. Interviewers expect the backtracking version because it is the template.
2Optimal — backtrack with one StringBuilder
Here the "choices" are the letters of the current digit. Append a letter, recurse on the next digit, delete the letter. One shared StringBuilder is reused for the whole search; a real String is only made when a full word is ready.
String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if (digits.isEmpty()) return res; // "" gives [], not [""]
backtrack(digits, 0, new StringBuilder(), res);
return res;
}
void backtrack(String digits, int idx, StringBuilder path, List<String> res) {
if (idx == digits.length()) { // one letter picked per digit
res.add(path.toString());
return;
}
for (char c : KEYS[digits.charAt(idx) - '0'].toCharArray()) {
path.append(c); // choose
backtrack(digits, idx + 1, path, res); // explore the next digit
path.deleteCharAt(path.length() - 1); // un-choose
}
}
Time O(n · 4ⁿ)Space O(n) recursion (plus the output)
Same time, less memory: only one path of length n exists at a time instead of a full layer of strings.
When each position has its own menu, the loop runs over that position's menu and the recursion moves to the next position. No start or used[] needed.
Empty input must return []. Without the early return, the base case fires at once and you return [""], which LeetCode marks wrong.
Pattern notes: Recursion & Backtracking · Open on LeetCode
LC 22Generate ParenthesesMediumBacktracking + PruningLeetCode
Given n, return every string made of n pairs of brackets that is well-formed: every ( is closed later by a ), and you never close something that isn't open.
Example: n = 3 → ["((()))","(()())","(())()","()(())","()()()"].
"Generate all valid ..." = backtracking. At every position there are only two choices, ( or ), and "valid" gives you rules you can check while building, not just at the end. Whenever validity can be checked on a prefix, prune on the prefix.
1Brute force — every bracket string, then check each
A string of length 2n with two choices per spot: 22n strings. Build each from a bitmask and keep it if it is balanced (the counter check from Beginner's Valid Parentheses).
List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
for (int mask = 0; mask < (1 << (2 * n)); mask++) { // every string of 2n brackets
StringBuilder sb = new StringBuilder();
for (int i = 2 * n - 1; i >= 0; i--) sb.append((mask & (1 << i)) != 0 ? ')' : '(');
if (valid(sb)) res.add(sb.toString());
}
return res;
}
boolean valid(CharSequence s) {
int bal = 0;
for (int i = 0; i < s.length(); i++) {
bal += s.charAt(i) == '(' ? 1 : -1;
if (bal < 0) return false; // a ")" with nothing to close
}
return bal == 0;
}
Time O(n · 4ⁿ)Space O(n) extra
For n = 8 that is 65,536 strings to find 1,430 valid ones. Anything starting with ) is half of all strings, and it is doomed from the first character.
2Optimal — only place a bracket if it keeps the prefix valid
Track how many ( and ) you have used. Two rules: add ( if open < n; add ) if close < open. Follow those and every path you build is valid, so every leaf is an answer. Here the "loop over choices" is just two ifs.
List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
backtrack(n, 0, 0, new StringBuilder(), res);
return res;
}
void backtrack(int n, int open, int close, StringBuilder path, List<String> res) {
if (path.length() == 2 * n) { // used n "(" and n ")"
res.add(path.toString());
return;
}
if (open < n) { // still have "(" left
path.append('('); // choose
backtrack(n, open + 1, close, path, res);// explore
path.deleteCharAt(path.length() - 1); // un-choose
}
if (close < open) { // a ")" only if something is open
path.append(')');
backtrack(n, open, close + 1, path, res);
path.deleteCharAt(path.length() - 1);
}
}
Time O(4ⁿ / √n)Space O(n) recursion
The number of valid strings is the n-th Catalan number (1, 2, 5, 14, 42, ...), about 4n / n1.5. We only do work proportional to those answers, which is as good as it gets.
Brute force checks the whole thing at the end; backtracking checks each prefix and never walks into a branch that is already broken. That is what "pruning" means.
The rule for ) is close < open, not close < n. With close < n you would accept ")("-style strings that close before opening.
Pattern notes: Recursion & Backtracking · Open on LeetCode
LC 79Word SearchMediumGrid BacktrackingLeetCode
You get a grid of letters and a word. Can you spell the word by starting on some cell and stepping up, down, left or right one cell at a time, never using the same cell twice?
Example: board [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]: "ABCCED" → true, "SEE" → true, "ABCB" → false (the B would be reused).
"Adjacent cells" = grid DFS, like Number of Islands in Beginner. But "the same cell may not be used more than once in this word" means a cell is only blocked for the current path. When you back out of a path, the cell must become free again. That undo step is what makes it backtracking and not plain flood fill.
1Brute force — walk every path of the right length, compare at the end
From every cell, walk every non-repeating path of length word.length(), building the string as you go. Only when the path is full, compare it with the word.
boolean exist(char[][] board, String word) {
int m = board.length, n = board[0].length;
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (walk(board, r, c, new boolean[m][n], new StringBuilder(), word)) return true;
return false;
}
boolean walk(char[][] b, int r, int c, boolean[][] seen, StringBuilder path, String word) {
if (r < 0 || c < 0 || r >= b.length || c >= b[0].length || seen[r][c]) return false;
seen[r][c] = true; path.append(b[r][c]);
boolean found;
if (path.length() == word.length()) found = path.toString().equals(word); // check only at the end
else found = walk(b, r + 1, c, seen, path, word) || walk(b, r - 1, c, seen, path, word)
|| walk(b, r, c + 1, seen, path, word) || walk(b, r, c - 1, seen, path, word);
seen[r][c] = false; path.deleteCharAt(path.length() - 1);
return found;
}
Time O(m · n · 4^L) paths, all walkedSpace O(m · n) seen + O(L) depth
(L = word length.) If the first letter is already wrong, it still walks every path from that cell to full length before noticing.
2Optimal — match letter by letter, mark the cell in place
Carry k = "which letter of the word I need next". If this cell isn't word[k], return false right away: that prunes the branch at the first wrong letter. To mark a cell as used, overwrite it with '#' (choose), explore the 4 neighbours, then write the letter back (un-choose). No extra seen grid needed.
boolean exist(char[][] board, String word) {
for (int r = 0; r < board.length; r++) {
for (int c = 0; c < board[0].length; c++) {
if (dfs(board, r, c, word, 0)) return true; // try every start cell
}
}
return false;
}
boolean dfs(char[][] board, int r, int c, String word, int k) {
if (k == word.length()) return true; // matched every letter
if (r < 0 || c < 0 || r >= board.length || c >= board[0].length) return false;
if (board[r][c] != word.charAt(k)) return false; // wrong letter (or '#'): prune now
char saved = board[r][c];
board[r][c] = '#'; // choose: mark as used
boolean found = dfs(board, r + 1, c, word, k + 1) // explore 4 neighbours
|| dfs(board, r - 1, c, word, k + 1)
|| dfs(board, r, c + 1, word, k + 1)
|| dfs(board, r, c - 1, word, k + 1);
board[r][c] = saved; // un-choose: put the letter back
return found;
}
Time O(m · n · 3^L) worstSpace O(L) recursion
After the first step each cell has at most 3 new neighbours (one is where you came from), hence 3L. Still exponential in the worst case, but the letter check usually kills a branch after one or two steps.
Grid backtracking = grid DFS + undo the visited mark on the way out. Plain DFS (islands) keeps cells marked forever; path problems must unmark.
Check k == word.length() before the bounds check. If the last letter sits on the edge, the next call steps off the grid; checking bounds first would return false even though the word is complete.
Pattern notes: Recursion & Backtracking · Open on LeetCode
LC 131Palindrome PartitioningMediumBacktracking + PruningLeetCode
Cut a string into pieces so that every piece is a palindrome (reads the same both ways). Return every way to do it.
Example: s = "aab" → [["a","a","b"], ["aa","b"]]. ("aab" as one piece fails, and so does "ab".)
"Return all possible partitions" = list every way to cut, so backtracking. The choice at each step is where the next piece ends. The condition ("every piece is a palindrome") can be checked piece by piece, so a bad first piece prunes the whole branch, just like a bad prefix in Generate Parentheses.
1Brute force — try every set of cut positions
A string of length n has n − 1 gaps, and each gap is cut or not: 2n−1 ways. Read the cuts from a bitmask, split the string, and keep the split if every piece is a palindrome.
List<List<String>> partition(String s) {
int n = s.length();
List<List<String>> res = new ArrayList<>();
for (int mask = 0; mask < (1 << (n - 1)); mask++) { // bit i on = cut after s[i]
List<String> parts = new ArrayList<>();
int from = 0; boolean ok = true;
for (int i = 0; i < n; i++) {
if (i == n - 1 || (mask & (1 << i)) != 0) {
String piece = s.substring(from, i + 1);
if (!isPal(piece, 0, piece.length() - 1)) { ok = false; break; }
parts.add(piece);
from = i + 1;
}
}
if (ok) res.add(parts);
}
return res;
}
boolean isPal(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
Time O(n · 2ⁿ) alwaysSpace O(n) extra
Every one of the 2n−1 cut patterns is tried, even when the very first piece already fails. The same substrings are re-checked for palindrome again and again.
2Optimal — backtrack on "where does the next piece end", with a palindrome table
From position start, try every end. Only recurse if s[start..end] is a palindrome; otherwise that whole subtree is skipped. To make the check O(1), fill a table first: pal[i][j] is true when the ends match and the inside pal[i+1][j-1] is a palindrome (a small 2D DP).
List<List<String>> partition(String s) {
int n = s.length();
boolean[][] pal = new boolean[n][n]; // pal[i][j]: is s[i..j] a palindrome?
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]);
}
}
List<List<String>> res = new ArrayList<>();
backtrack(s, 0, pal, new ArrayList<>(), res);
return res;
}
void backtrack(String s, int start, boolean[][] pal, List<String> path, List<List<String>> res) {
if (start == s.length()) { // cut the whole string
res.add(new ArrayList<>(path));
return;
}
for (int end = start; end < s.length(); end++) {
if (!pal[start][end]) continue; // prune: first piece must be a palindrome
path.add(s.substring(start, end + 1)); // choose
backtrack(s, end + 1, pal, path, res); // explore the rest
path.remove(path.size() - 1); // un-choose
}
}
Time O(n · 2ⁿ) worstSpace O(n²) table + O(n) recursion
Honest note: for "aaaa...a" every cut is valid, so there are 2n−1 answers and nothing can be faster. On normal strings most branches die at the first piece, and the table removes the repeated palindrome checks.
"Split a string every possible way" = loop end from start, take s[start..end] as the next piece, recurse from end + 1. Precompute anything you check over and over.
Fill the table with i going backwards. pal[i][j] needs pal[i+1][j-1], a row below, so that row must already be done.
Pattern notes: Recursion & Backtracking · Open on LeetCode
Greedy
The pattern is greedy: walk through the input once and at each step make the choice that looks best right now, never going back. The template is one loop plus one or two running values (farthest reach, current tank, current end). In Beginner the greedy choice was obvious. Here the hard part is the reason it is safe: each problem below has one short argument ("if I got stuck here, no earlier start can work") that lets you throw away a whole O(n²) search.
LC 55Jump GameMediumGreedy ReachLeetCode
You stand on index 0 of nums. nums[i] is the maximum jump length from index i (you may jump less). Can you reach the last index?
Example: [2,3,1,1,4] → true (0 → 1 → 4). [3,2,1,0,4] → false: every route lands on the 0 at index 3 and gets stuck.
"Maximum jump" means that if you can reach index j, you can also reach every index before j. So the reachable indexes always form one block starting at 0. That turns "which jumps do I take?" into a single number: how far can I reach? One running maximum = greedy.
1Brute force — mark every reachable index
Keep a boolean array. Index 0 is reachable. For each reachable index i, mark every index it can jump to. At the end, check the last index.
boolean canJump(int[] nums) {
int n = nums.length;
boolean[] reach = new boolean[n];
reach[0] = true;
for (int i = 0; i < n; i++) {
if (!reach[i]) continue; // can't stand here
for (int j = i + 1; j <= i + nums[i] && j < n; j++) {
reach[j] = true; // mark every landing spot
}
}
return reach[n - 1];
}
Time O(n²)Space O(n)
With big jump values, each index marks up to n cells, most of them already marked. We only ever need the right edge of the marked block.
2Optimal — track the farthest reachable index
Walk left to right and keep farthest = the largest index you can reach so far. At index i, first check if i > farthest, you can't even stand here: return false. Otherwise update farthest = max(farthest, i + nums[i]). If the loop finishes, the end was reachable.
boolean canJump(int[] nums) {
int farthest = 0; // best index reachable so far
for (int i = 0; i < nums.length; i++) {
if (i > farthest) return false; // a gap: we can't even get to i
farthest = Math.max(farthest, i + nums[i]);
}
return true;
}
Time O(n)Space O(1)
One pass, one variable. You can also stop early and return true as soon as farthest >= n - 1.
When "reachable" always forms one block from the start, keep only the block's right edge. A running maximum replaces a whole table of booleans.
Check i > farthest before using nums[i]. If you update first, a big number sitting after the gap (like the 4 above) pushes farthest past the end and you wrongly return true.
Pattern notes: Greedy · Open on LeetCode
LC 45Jump Game IIMediumGreedy BFS LevelsLeetCode
Same jumping rules as Jump Game, and now reaching the last index is guaranteed. Return the fewest jumps needed to get there.
Example: [2,3,1,1,4] → 2 (jump 0 → 1, then 1 → 4). An array of one element needs 0 jumps.
"Fewest steps" normally means BFS: level 1 = everything one jump away, level 2 = two jumps away, and so on. Because of the Jump Game fact (reachable spots form a block), each BFS level is just a range of indexes. So you can run BFS with two numbers instead of a queue.
1Brute force — DP: fewest jumps to each index
best[i] = fewest jumps to reach i. From each index i, every j it can reach gets best[j] = min(best[j], best[i] + 1).
int jump(int[] nums) {
int n = nums.length;
int[] best = new int[n]; // best[i] = fewest jumps to reach i
Arrays.fill(best, Integer.MAX_VALUE);
best[0] = 0;
for (int i = 0; i < n; i++) {
if (best[i] == Integer.MAX_VALUE) continue;
for (int j = i + 1; j <= i + nums[i] && j < n; j++) {
best[j] = Math.min(best[j], best[i] + 1);
}
}
return best[n - 1];
}
Time O(n²)Space O(n)
Correct, but it updates the same cells over and over. The best values are just 0, 1, 1, 2, 2, ...: they only grow, in blocks.
2Optimal — BFS by ranges: current end and farthest
Walk the indexes of the current jump's range and keep farthest = the best reach from any of them. That is the end of the next range. When i hits the end of the current range, you must jump: jumps++, and the new end is farthest. Stop before the last index, because standing on it needs no more jumps.
int jump(int[] nums) {
int jumps = 0, end = 0, farthest = 0;
for (int i = 0; i < nums.length - 1; i++) { // stop before the last index
farthest = Math.max(farthest, i + nums[i]);
if (i == end) { // used up the range of this jump
jumps++;
end = farthest; // next jump can reach up to here
}
}
return jumps;
}
Time O(n)Space O(1)
Each index is scanned once. end marks the edge of the current BFS level, and farthest builds the next level.
"Fewest steps" on an array where each level is a contiguous range → BFS with two pointers (current end, farthest). No queue needed.
Loop to n - 1, not n. If i reaches the last index and it equals end, you would count one extra jump from a place you are already standing on. For [0] the answer must be 0.
Pattern notes: Greedy · Open on LeetCode
LC 134Gas StationMediumGreedy ResetLeetCode
Gas stations stand in a circle. At station i you can fill up gas[i], and driving to station i + 1 uses cost[i]. You start with an empty tank. Return the starting station from which you can drive all the way around once, or -1. If an answer exists, it is unique.
Example: gas = [1,2,3,4,5], cost = [3,4,5,1,2] → 3. gas = [2,3,4], cost = [3,4,3] → -1.
"Find the start so that a running total never goes negative" is a prefix-sum question. The greedy clue: if you run dry somewhere, the start you picked was wrong, and so was every station in between. That lets you skip ahead instead of trying each start. The words "answer is unique" hint that one pass is enough.
1Brute force — try every starting station
For each start, simulate the drive around the circle. If the tank ever goes below 0, this start fails; try the next one.
int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
for (int start = 0; start < n; start++) {
int tank = 0, steps = 0;
while (steps < n) {
int i = (start + steps) % n;
tank += gas[i] - cost[i];
if (tank < 0) break; // stuck before the next station
steps++;
}
if (steps == n) return start; // made the full loop
}
return -1;
}
Time O(n²)Space O(1)
With 100,000 stations that can be 10 billion steps. And a failed start teaches us something we then ignore.
2Optimal — one pass, reset the start when the tank goes negative
Two facts. (1) If total gas ≥ total cost, some start works; otherwise none does. (2) If you start at s and run dry right after station i, then no station between s and i can be the start either: you arrived at each of them with a tank ≥ 0, and starting there fresh gives you less gas, not more. So jump the start to i + 1 and reset the tank.
int canCompleteCircuit(int[] gas, int[] cost) {
int total = 0, tank = 0, start = 0;
for (int i = 0; i < gas.length; i++) {
int gain = gas[i] - cost[i];
total += gain;
tank += gain;
if (tank < 0) { // can't reach i+1 from start
start = i + 1; // so no station in start..i works: try i+1
tank = 0;
}
}
return total >= 0 ? start : -1; // enough gas overall = the answer exists
}
Time O(n)Space O(1)
One loop, three numbers. We never wrap around the circle: the total check proves the part before start will be fine too.
When a running sum goes negative, everything since the last reset is useless as a start: reset and move on. The same move is inside Kadane's algorithm (Maximum Subarray).
You need both checks. The running tank finds the only possible start; the total decides if even that start can finish the loop. Returning start without checking total gives a wrong answer on [2,3,4] / [3,4,3].
Pattern notes: Greedy · Open on LeetCode
LC 763Partition LabelsMediumGreedy + Last IndexLeetCode
Cut the string s into as many pieces as possible so that each letter appears in only one piece. Return the sizes of the pieces, in order.
Example: "ababcbacadefegdehijhklij" → [9,7,8]: "ababcbaca", "defegde", "hijhklij". And "eccbbbbdec" → [10].
"Each letter in one piece" means: once a piece contains a letter, it must stretch at least to that letter's last appearance. So the useful fact per letter is its last index. Then it becomes Jump Game again: keep extending the piece's end, and cut as soon as you reach it (cutting early = as many pieces as possible).
1Brute force — look up last positions again and again
Start a piece at start. Its end is the last position of its first letter. Walk through the piece; every letter inside may push the end further (found with lastIndexOf). When you reach the end, cut and start the next piece.
List<Integer> partitionLabels(String s) {
List<Integer> res = new ArrayList<>();
int start = 0;
while (start < s.length()) {
int end = s.lastIndexOf(s.charAt(start));
for (int i = start; i <= end; i++) { // every letter inside may push end further
end = Math.max(end, s.lastIndexOf(s.charAt(i)));
}
res.add(end - start + 1);
start = end + 1;
}
return res;
}
Time O(n²)Space O(1)
The idea is already right. The waste is lastIndexOf: it scans from the end of the string for every character, repeating the same search for the same letter many times.
2Optimal — precompute last[] once, then one pass
First pass: store the last index of each letter in an array of 26. Second pass: keep end = max(end, last[letter]). When i == end, no letter seen so far appears later, so this is the earliest safe cut. Record the size and start a new piece.
List<Integer> partitionLabels(String s) {
int[] last = new int[26];
for (int i = 0; i < s.length(); i++) last[s.charAt(i) - 'a'] = i; // last time each letter appears
List<Integer> res = new ArrayList<>();
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
end = Math.max(end, last[s.charAt(i) - 'a']); // this piece must reach at least here
if (i == end) { // nothing inside needs to go further
res.add(end - start + 1);
start = i + 1;
}
}
return res;
}
Time O(n)Space O(1)
Two passes over the string. The last array always has 26 slots, so extra space is constant.
"Everything of type X must stay together" → precompute the last index of each type, then sweep with a growing end. Cut when i == end. It is merge-intervals in disguise: each letter is the interval [first, last].
Use max when updating end. Writing end = last[letter] can move the end backwards (the b at index 1 has last 5, less than 8) and you would cut too early.
Pattern notes: Greedy · Open on LeetCode
LC 846Hand of StraightsMediumGreedy + TreeMapLeetCode
You hold cards with numbers on them. Can you split all of them into groups of exactly groupSize cards, where each group is a run of consecutive numbers (like 4, 5, 6)?
Example: hand = [1,2,3,6,2,3,4,7,8], groupSize = 3 → true: [1,2,3], [2,3,4], [6,7,8]. hand = [1,2,3,4,5], groupSize = 4 → false (5 cards can't make groups of 4).
"Consecutive groups" plus "use every card" points to the greedy fact: the smallest card left must start a group, because nothing smaller exists to come before it. So always take the smallest card and try to build its run. You need "smallest remaining" plus counts of duplicates: a sorted map of counts (TreeMap).
1Brute force — sorted list, remove cards one by one
Sort the cards into a list. Take the smallest card, then remove first, first+1, ..., first+groupSize-1 from the list. If any of them is missing, return false. Repeat until the list is empty.
boolean isNStraightHand(int[] hand, int groupSize) {
if (hand.length % groupSize != 0) return false;
List<Integer> cards = new ArrayList<>();
for (int h : hand) cards.add(h);
Collections.sort(cards);
while (!cards.isEmpty()) {
int first = cards.get(0); // smallest card must start a group
for (int k = 0; k < groupSize; k++) {
// remove(Object) searches the list: O(n) each time
if (!cards.remove(Integer.valueOf(first + k))) return false;
}
}
return true;
}
Time O(n²)Space O(n)
The greedy choice is already right. The cost is ArrayList.remove: it searches and shifts the list, O(n) per card, n cards.
2Optimal — TreeMap of counts
Store how many of each card you have in a TreeMap (sorted by card). Take firstKey(), the smallest card left, and use one of each card from it to it + groupSize − 1, lowering counts and removing keys that hit 0. A missing card means a hole in the run: return false.
boolean isNStraightHand(int[] hand, int groupSize) {
if (hand.length % groupSize != 0) return false;
TreeMap<Integer, Integer> count = new TreeMap<>(); // card -> how many, sorted
for (int h : hand) count.merge(h, 1, Integer::sum);
while (!count.isEmpty()) {
int first = count.firstKey(); // smallest card left
for (int card = first; card < first + groupSize; card++) {
Integer c = count.get(card);
if (c == null) return false; // a hole in the run
if (c == 1) count.remove(card);
else count.put(card, c - 1);
}
}
return true;
}
Time O(n log n)Space O(n)
Each card is used once, and each TreeMap operation costs O(log n). That matches the cost of sorting, which any solution needs anyway.
"Split into consecutive runs" → the smallest remaining item must start a run. TreeMap of counts gives you "smallest left" (firstKey) and fast decrement in one structure.
Check hand.length % groupSize first. Also remove keys whose count reaches 0: if you leave 2:0 in the map, firstKey() returns 2 again and the next group is built from a card you no longer have.
Pattern notes: Greedy · Open on LeetCode
1D Dynamic Programming
1D DP: the answer for one position (or one amount) is built from answers for smaller ones, so a single array dp[i] holds everything, often shrunk to two variables. Template: say what dp[i] means in words, write the recurrence from the last choice, set the base case, fill in order. What is harder than Beginner: the meaning of dp[i] is less obvious ("ending at i", "first i letters", "amount i"), sometimes you track two values at once (max and min), and knapsack problems index by the sum instead of the position.
LC 213House Robber IIMedium1D DP (two runs)LeetCode
Same as House Robber (LC 198): you can't rob two neighbouring houses. The twist: the houses stand in a circle, so the first and last house are neighbours too. What is the most money you can take?
Example: nums = [2, 3, 2] → 3. You can't take both 2s, because house 0 and house 2 touch.
"Houses", "adjacent", "maximum amount" is the House Robber take/skip DP you know from Beginner. The word circle (first and last are adjacent) is the only new part, and the trick is to break the circle into straight lines you already know how to solve.
1Brute force — try both worlds, recursively
In any valid plan, house 0 and the last house are never both robbed. So the answer lives in one of two straight streets: houses 0..n−2 (last one left out) or houses 1..n−1 (first one left out). Solve each with the plain House Robber recursion: at house i, skip it or rob it and jump back two.
int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0]; // one house: just rob it
return Math.max(best(nums, 0, n - 2), // street A: no last house
best(nums, 1, n - 1)); // street B: no first house
}
int best(int[] nums, int lo, int i) { // most money from houses lo..i
if (i < lo) return 0;
return Math.max(best(nums, lo, i - 1), // skip house i
nums[i] + best(nums, lo, i - 2)); // rob house i
}
Time O(2ⁿ)Space O(n)
The splitting idea is already right. What is slow is the recursion: best(i − 2) is recomputed along many paths, just like in House Robber I.
2Optimal — House Robber I twice, with two variables
Run the bottom-up House Robber on each straight street and take the larger: answer = max(rob(0..n−2), rob(1..n−1)). A circle problem becomes two line problems.
State: dp[i] = most money from the street's first house up to house i.
Recurrence: dp[i] = max(dp[i−1], dp[i−2] + nums[i]) (skip house i, or rob it).
Base case: before the street starts both values are 0; a single house (n = 1) is returned directly, because both streets would be empty.
int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
return Math.max(robLine(nums, 0, n - 2), // without the last house
robLine(nums, 1, n - 1)); // without the first house
}
int robLine(int[] nums, int lo, int hi) { // House Robber I on lo..hi
int prev2 = 0, prev1 = 0; // dp[i-2], dp[i-1]
for (int i = lo; i <= hi; i++) {
int cur = Math.max(prev1, prev2 + nums[i]); // skip or rob house i
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
Time O(n)Space O(1)
Two passes of n steps each, with only two numbers kept per pass.
A circle where the first and last items interact: solve it twice as a line, once without the first item and once without the last, then combine.
Handle n = 1 first: both ranges 0..n−2 and 1..n−1 are empty, and you would return 0 instead of nums[0]. Also, plain House Robber on the whole array is wrong: for [2, 7, 9, 3, 1] it says 12 (houses 0, 2, 4), but 0 and 4 touch.
Pattern notes: DP Fundamentals · Open on LeetCode
LC 322Coin ChangeMediumUnbounded knapsackLeetCode
You have coin values (unlimited coins of each) and an amount. What is the fewest coins that add up exactly to the amount? If it can't be done, return -1.
Example: coins = [1, 2, 5], amount = 11 → 3 (5 + 5 + 1). coins = [2], amount = 3 → -1.
"Fewest coins", "unlimited of each", "exactly the amount". A minimum over many ways to build a total, where greedy (take the biggest coin) can fail, is a DP where the index is the amount, not a position in an array.
1Brute force — try every coin as the last coin
To pay amount, some coin c is the last one you use. Then the rest is the best way to pay amount − c. Try every coin, recurse, keep the smallest answer plus one.
int coinChange(int[] coins, int amount) {
if (amount == 0) return 0; // nothing left to pay
if (amount < 0) return -1; // overshot: this path fails
int best = -1;
for (int c : coins) { // try each coin as the last one
int sub = coinChange(coins, amount - c);
if (sub >= 0 && (best == -1 || sub + 1 < best)) best = sub + 1;
}
return best;
}
Time O(kᵃ)Space O(a)
With k coins and amount a the tree branches k ways at every level. Amount 11 alone asks for coinChange(6) 9 separate times.
2Optimal — bottom-up table over amounts
There are only amount + 1 different questions: "fewest coins for 0, for 1, ..., for amount". Answer them from small to large, so each one reads answers already in the table.
State: dp[a] = fewest coins that make exactly a.
Recurrence: dp[a] = min over coins c ≤ a of dp[a − c] + 1.
Base case: dp[0] = 0; every other cell starts at amount + 1 (acts as "impossible").
int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1]; // dp[a] = fewest coins for amount a
Arrays.fill(dp, amount + 1); // "infinity" (never reachable)
dp[0] = 0; // 0 coins make amount 0
for (int a = 1; a <= amount; a++) {
for (int c : coins) {
if (c <= a) { // use coin c as the last coin
dp[a] = Math.min(dp[a], dp[a - c] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
Time O(a · k)Space O(a)
For each of the a amounts we try each of the k coins once. amount + 1 works as infinity because you never need more than amount coins (all 1s), and it can't overflow the way Integer.MAX_VALUE + 1 does.
"Fewest / most items to reach a total, items reusable" → unbounded knapsack: dp[total] = best over items of dp[total − item] + 1, filled from 0 upward.
Greedy is wrong: with coins [1, 3, 4] and amount 6, taking the biggest coin first gives 4 + 1 + 1 (3 coins), but 3 + 3 uses 2. And don't fill with Integer.MAX_VALUE: adding 1 overflows to a negative number that then wins every min.
Pattern notes: DP Patterns · Open on LeetCode
LC 139Word BreakMedium1D DP (prefix)LeetCode
You get a string s and a list of dictionary words. Can you cut s into pieces so that every piece is a dictionary word? Words can be reused.
Example: s = "leetcode", wordDict = ["leet", "code"] → true ("leet" + "code"). s = "catsandog" with ["cats","dog","sand","and","cat"] → false.
"Can the string be segmented into words" is a yes/no question about prefixes: if the first i letters can be split, you only need one more word to reach further. "Can it be split / can it be reached" over a string usually means dp[i] on prefix length.
1Brute force — try every first word, recursively
Starting at position start, try every possible first piece s[start..end). If it is a word, ask the same question for the rest of the string. If any choice works, the answer is yes.
boolean wordBreak(String s, List<String> wordDict) {
return canBreak(s, 0, new HashSet<>(wordDict));
}
// can the part s[start..] be split into words?
boolean canBreak(String s, int start, Set<String> dict) {
if (start == s.length()) return true; // used up the whole string
for (int end = start + 1; end <= s.length(); end++) {
String first = s.substring(start, end);
if (dict.contains(first) && canBreak(s, end, dict)) return true;
}
return false;
}
Time O(2ⁿ)Space O(n)
A string like "aaaaaaab" with words ["a", "aa", "aaa"] can be cut in exponentially many ways, and every one of them fails only at the end.
2Optimal — dp over prefix lengths
The recursion only ever asks "can the part starting at start be split?" for n different starts. Turn it around: dp[i] is true if some earlier cut point j is good and s[j..i) is a word.
State: dp[i] = the first i letters can be split into dictionary words.
Recurrence: dp[i] = true if there is a j < i with dp[j] true and s.substring(j, i) in the dictionary.
Base case: dp[0] = true: the empty prefix needs no words.
boolean wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n + 1]; // dp[i]: first i letters split OK
dp[0] = true; // the empty prefix is always fine
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) { // j = where the last word starts
if (dp[j] && dict.contains(s.substring(j, i))) {
dp[i] = true;
break; // one way is enough
}
}
}
return dp[n];
}
Time O(n³)Space O(n + w)
n² pairs (j, i), and each substring plus hash costs up to O(n). The dictionary goes into a HashSet (w = total word letters) so each lookup is fast. That is plenty for LeetCode's n ≤ 300.
"Can the string be built from pieces" → dp[i] = prefix of length i works, with dp[0] = true. The same shape solves Decode Ways and many "split the string" questions.
Use size n + 1 and let dp[i] mean "the first i letters", not "up to index i". Mixing the two shifts every substring by one. And put the words in a HashSet: List.contains scans the whole list each time.
Pattern notes: DP Patterns · Open on LeetCode
LC 91Decode WaysMedium1D DP (count ways)LeetCode
Letters are coded as numbers: A = 1, B = 2, ..., Z = 26. Given a string of digits, how many ways can it be decoded back into letters? A piece can't start with 0.
Example: "226" → 3: "2 2 6" (BBF), "22 6" (VF), "2 26" (BZ). "06" → 0.
"How many ways" plus "each step uses one or two characters" is Climbing Stairs (LC 70) in disguise: at each point you step 1 digit or 2 digits, and you add the counts. The new part is the rules about which steps are allowed.
1Brute force — take one digit or two, recursively
From position i: if the digit is 0 this path is dead. Otherwise decode one digit and count the rest, and if the next two digits form 10..26, also decode two and count the rest. Add both.
int numDecodings(String s) {
return ways(s, 0);
}
int ways(String s, int i) { // ways to decode s[i..]
if (i == s.length()) return 1; // decoded everything: 1 way
if (s.charAt(i) == '0') return 0; // no letter starts with 0
int count = ways(s, i + 1); // take one digit (1..9)
if (i + 1 < s.length()
&& Integer.parseInt(s.substring(i, i + 2)) <= 26)
count += ways(s, i + 2); // take two digits (10..26)
return count;
}
Time O(2ⁿ)Space O(n)
A string of 40 ones branches like Fibonacci: about 433 million calls, all asking the same 41 small questions.
2Optimal — Climbing Stairs with rules, two variables
The number of ways to decode the first i digits comes from the last step: last digit alone (if it is 1..9) adds dp[i−1]; last two digits together (if 10..26) add dp[i−2]. Only two previous numbers are needed.
State: dp[i] = number of ways to decode the first i digits.
Recurrence: dp[i] = (s[i−1] ≠ '0' ? dp[i−1] : 0) + (10 ≤ s[i−2..i−1] ≤ 26 ? dp[i−2] : 0).
Base case: dp[0] = 1 (one way to decode nothing), dp[1] = 1 unless the first digit is 0.
int numDecodings(String s) {
int prev2 = 1; // dp[0]: empty prefix, 1 way
int prev1 = s.charAt(0) == '0' ? 0 : 1; // dp[1]
for (int i = 2; i <= s.length(); i++) { // dp[i]: first i digits
int one = s.charAt(i - 1) - '0'; // last digit alone
int two = (s.charAt(i - 2) - '0') * 10 + one; // last two digits
int cur = 0;
if (one != 0) cur += prev1; // "1".."9"
if (two >= 10 && two <= 26) cur += prev2; // "10".."26"
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
Time O(n)Space O(1)
One pass. prev1 and prev2 play the roles of dp[i−1] and dp[i−2], exactly like Climbing Stairs.
"Count the ways, each step takes 1 or 2 items" → dp[i] = dp[i−1] + dp[i−2], with each term only added when that step is legal.
Zeros are the whole difficulty. "06" is not 6 (answer 0), "10" has exactly 1 way, and "100" has 0. Check the two-digit value is between 10 and 26, not just ≤ 26, or "06" sneaks in.
Pattern notes: DP Fundamentals · Open on LeetCode
LC 152Maximum Product SubarrayMedium1D DP (max and min)LeetCode
Find the contiguous subarray (at least one number) with the largest product, and return that product. Numbers can be negative or zero.
Example: [2, 3, -2, 4] → 6 ([2, 3]). [-2, 0, -1] → 0.
"Contiguous subarray" plus "largest" points to Kadane's algorithm (best subarray ending here). The word product with negative numbers is the warning sign: a negative times a negative flips the smallest value into the largest, so one running value is not enough.
1Brute force — every subarray
Pick every start i, then extend the end j one step at a time, multiplying as you go, and remember the largest product seen. This is not exponential: there are only n² subarrays, so a plain double loop is the natural brute force here.
int maxProduct(int[] nums) {
int best = nums[0];
for (int i = 0; i < nums.length; i++) { // every start
int prod = 1;
for (int j = i; j < nums.length; j++) { // every end
prod *= nums[j]; // product of nums[i..j]
best = Math.max(best, prod);
}
}
return best;
}
Time O(n²)Space O(1)
Fine for small inputs, but with 20,000 numbers that is 200 million products.
2Optimal — keep both the max and the min ending here
Kadane for sums keeps one number: the best sum ending here. For products, a very negative product is valuable too, because one more negative number turns it into a huge positive. So track the largest AND the smallest product ending at each position.
State: maxHere[i] / minHere[i] = largest / smallest product of a subarray that ends at i.
Recurrence: maxHere[i] = max(x, maxHere[i−1]·x, minHere[i−1]·x), and minHere[i] = the min of the same three.
Base case: both start as nums[0]; the answer is the largest maxHere seen.
int maxProduct(int[] nums) {
int maxHere = nums[0]; // biggest product of a subarray ending here
int minHere = nums[0]; // smallest product of a subarray ending here
int best = nums[0];
for (int i = 1; i < nums.length; i++) {
int x = nums[i];
int a = maxHere * x; // extend the old max
int b = minHere * x; // extend the old min
maxHere = Math.max(x, Math.max(a, b)); // ... or start fresh at x
minHere = Math.min(x, Math.min(a, b));
best = Math.max(best, maxHere);
}
return best;
}
Time O(n)Space O(1)
One pass, three numbers. Compute both products before overwriting maxHere, because the new min needs the old max.
When the operation can flip signs (multiplying by negatives), keep both extremes of the state. Today's worst can be tomorrow's best.
Updating maxHere first and then using it to compute minHere uses the wrong value. Save maxHere * x and minHere * x in temporaries first, as the code does.
Pattern notes: DP Fundamentals · Open on LeetCode
LC 300Longest Increasing SubsequenceMedium1D DP (ending at i)LeetCode
Return the length of the longest strictly increasing subsequence. A subsequence keeps the order but may skip elements.
Example: [10, 9, 2, 5, 3, 7, 101, 18] → 4 (for example 2, 3, 7, 101).
"Subsequence" (skip allowed, order kept) plus "longest" means you choose which elements to keep, and each choice depends on the last kept one. That is DP where the state is "the best chain that ends at element i".
1Brute force — take or skip every element
Walk left to right remembering the last number you took. For each element: skip it, or take it if it is bigger than the last taken. Return the longer of the two results.
int lengthOfLIS(int[] nums) {
return lis(nums, 0, -1);
}
// prev = index of the last number taken (-1 = none yet)
int lis(int[] nums, int i, int prev) {
if (i == nums.length) return 0;
int skip = lis(nums, i + 1, prev); // leave nums[i] out
int take = 0;
if (prev == -1 || nums[i] > nums[prev]) // may only take a bigger one
take = 1 + lis(nums, i + 1, i);
return Math.max(skip, take);
}
Time O(2ⁿ)Space O(n)
Every element doubles the number of paths. 2,500 elements (the LeetCode limit) is far out of reach.
2Optimal — dp[i] = longest run ending at i
Make every element the end of a chain. A chain ending at i is some chain ending at an earlier, smaller number, plus nums[i]. So dp[i] = 1 + the best dp[j] among j < i with nums[j] < nums[i].
State: dp[i] = length of the longest increasing subsequence that ends exactly at index i.
Recurrence: dp[i] = 1 + max(dp[j]) over j < i with nums[j] < nums[i] (or just 1 if there is none).
Base case: every dp[i] starts at 1 (the element alone). The answer is the max of the whole array, not dp[n−1].
int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n]; // dp[i] = longest run ENDING at i
int best = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1; // nums[i] alone
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) // nums[i] can extend the run ending at j
dp[i] = Math.max(dp[i], dp[j] + 1);
}
best = Math.max(best, dp[i]);
}
return best;
}
Time O(n²)Space O(n)
For each i we scan all earlier j. This is the answer most interviewers expect first.
Follow-up, O(n log n): keep tails[k] = the smallest possible last number of an increasing run of length k + 1. Each new number replaces the first tail that is ≥ it (binary search), or extends the list. The list length is the answer. The list itself is not a real subsequence; only its length is meaningful.
int lengthOfLIS(int[] nums) {
// tails.get(k) = smallest last number of a run of length k + 1
List<Integer> tails = new ArrayList<>();
for (int x : nums) {
int lo = 0, hi = tails.size();
while (lo < hi) { // first tail >= x
int mid = (lo + hi) / 2;
if (tails.get(mid) < x) lo = mid + 1;
else hi = mid;
}
if (lo == tails.size()) tails.add(x); // x makes a longer run
else tails.set(lo, x); // x is a smaller tail
}
return tails.size();
}
Time O(n log n)Space O(n)
For subsequence problems, define the state as "best answer ending at i" and look back at all earlier j. Then the final answer is the max over all i.
Returning dp[n − 1] is wrong: the longest run need not end at the last element ([1, 2, 0] has dp[2] = 1 but the answer is 2). And the comparison is strict: equal numbers do not extend a run.
Pattern notes: DP Patterns · Open on LeetCode
LC 5Longest Palindromic SubstringMediumInterval DPLeetCode
Return the longest substring (contiguous) of s that reads the same forwards and backwards.
Example: "babad" → "bab" ("aba" is also accepted). "cbbd" → "bb".
"Palindromic substring": a palindrome is its ends matching plus a smaller palindrome inside. When the answer for a range [i..j] depends on the range [i+1..j−1], the state is the pair (i, j): interval DP.
1Brute force — check every substring
Try every start and end, check if that substring is a palindrome with two pointers moving inwards, and keep the longest. Plain recursion is not the natural brute force here; the triple loop is.
String longestPalindrome(String s) {
String best = "";
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) { // every substring s[i..j]
if (j - i + 1 > best.length() && isPal(s, i, j))
best = s.substring(i, j + 1);
}
}
return best;
}
boolean isPal(String s, int i, int j) { // two pointers inwards
while (i < j) {
if (s.charAt(i++) != s.charAt(j--)) return false;
}
return true;
}
Time O(n³)Space O(1)
n² substrings, each checked in up to n steps. "babad" re-checks the inside "aba" when it checks "babab", and again and again.
2Optimal — table of "is s[i..j] a palindrome?"
Checking s[i..j] from scratch is wasteful: s[i..j] is a palindrome exactly when s[i] == s[j] and s[i+1..j−1] is a palindrome. Store the answers so each check is O(1).
State: dp[i][j] = the substring from i to j (inclusive) is a palindrome.
Recurrence: dp[i][j] = s[i] == s[j] && (j − i < 2 || dp[i+1][j−1]).
Base case: length 1 is always a palindrome; length 2 only needs matching ends (the j − i < 2 part covers both).
String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n]; // dp[i][j] = s[i..j] is a palindrome
int start = 0, len = 1;
for (int i = n - 1; i >= 0; i--) { // i goes DOWN: row i+1 is ready
for (int j = i; j < n; j++) {
boolean endsMatch = s.charAt(i) == s.charAt(j);
// length 1 or 2: matching ends are enough;
// longer: the inside must be a palindrome too
dp[i][j] = endsMatch && (j - i < 2 || dp[i + 1][j - 1]);
if (dp[i][j] && j - i + 1 > len) {
start = i;
len = j - i + 1;
}
}
}
return s.substring(start, start + len);
}
Time O(n²)Space O(n²)
Each of the n²/2 cells is filled in O(1). The order matters: i goes down from n − 1 so dp[i+1][..] is already filled.
Space-saving alternative, expand around centre: every palindrome has a centre (a letter, or the gap between two letters). Try all 2n − 1 centres and grow outwards while the ends match. Same O(n²) time, but O(1) space, and it is the version most people write in interviews.
String longestPalindrome(String s) {
int start = 0, len = 0;
for (int c = 0; c < s.length(); c++) {
int odd = expand(s, c, c); // centre on a letter: "aba"
int even = expand(s, c, c + 1); // centre between letters: "abba"
int l = Math.max(odd, even);
if (l > len) {
len = l;
start = c - (l - 1) / 2;
}
}
return s.substring(start, start + len);
}
int expand(String s, int l, int r) { // grow while both ends match
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
l--;
r++;
}
return r - l - 1; // length of the palindrome
}
Time O(n²)Space O(1)
"Is this range good?" that depends on the smaller range inside → dp[i][j] built from dp[i+1][j−1], filled so that shorter ranges come first.
Filling i from 0 upwards reads dp[i+1][j−1] before it is computed and every long palindrome looks false. Also remember even-length palindromes like "bb": with centres, try the gap between letters too.
Pattern notes: DP Fundamentals · Open on LeetCode
LC 416Partition Equal Subset SumMedium0/1 knapsackLeetCode
Can you split the array into two groups with the same sum? Every number goes into exactly one group.
Example: [1, 5, 11, 5] → true ({1, 5, 5} and {11}, both 11). [1, 2, 3, 5] → false.
Two equal halves means one group must sum to total / 2, and the rest is automatic. "Pick some items (each at most once) to hit an exact sum" is the classic 0/1 knapsack: the index is the sum, and each item is used once.
1Brute force — put each number in or leave it out
If the total is odd, the answer is no. Otherwise try to build total / 2: for each number, either put it in the subset or skip it. If some choice hits the target exactly, yes.
boolean canPartition(int[] nums) {
int total = 0;
for (int x : nums) total += x;
if (total % 2 == 1) return false; // odd total: no equal halves
return canMake(nums, 0, total / 2);
}
// can some of nums[i..] add up to exactly target?
boolean canMake(int[] nums, int i, int target) {
if (target == 0) return true;
if (i == nums.length || target < 0) return false;
return canMake(nums, i + 1, target - nums[i]) // put nums[i] in
|| canMake(nums, i + 1, target); // leave it out
}
Time O(2ⁿ)Space O(n)
200 numbers (the LeetCode limit) means 2²⁰⁰ subsets. But the target is at most 10,000, so the same (index, remaining) pairs repeat a lot.
2Optimal — reachable sums, one boolean row
Don't track which subset, only which sums are reachable. Start with {0}. For each number x, every reachable sum s makes s + x reachable too. Loop the sums from high to low so x is used at most once.
State: dp[s] = some of the numbers seen so far add up to exactly s.
Recurrence: for each number x, for s from half down to x: dp[s] = dp[s] || dp[s − x].
Base case: dp[0] = true (take nothing). Odd total → false immediately.
boolean canPartition(int[] nums) {
int total = 0;
for (int x : nums) total += x;
if (total % 2 == 1) return false;
int half = total / 2;
boolean[] dp = new boolean[half + 1]; // dp[s] = sum s is reachable
dp[0] = true; // the empty subset sums to 0
for (int x : nums) {
for (int s = half; s >= x; s--) { // go DOWN: use x at most once
if (dp[s - x]) dp[s] = true;
}
}
return dp[half];
}
Time O(n · half)Space O(half)
Each number walks the sums once. The 2D table (number × sum) collapses to one row because each row only reads the row above.
"Pick items, each once, to hit a sum" → 0/1 knapsack: dp[sum] of booleans, outer loop items, inner loop sums going DOWN.
Looping s upwards lets the same number be used twice in one round: with [1, 2, 5] (total 8, half 4), dp[1] then dp[2], dp[3], dp[4] all become true from the single 1, and you wrongly answer true. Downwards reads only the previous round.
Pattern notes: DP Patterns · Open on LeetCode
2D Dynamic Programming
2D DP: the state needs two numbers: a grid cell (row, column), two prefix lengths (i letters of one string, j of the other), an item and a sum, or a day and a mode. Template: draw the table, fill the base row and column, compute each cell from its neighbours (up, left, diagonal). What is harder than Beginner: picking the two dimensions and a fill order where every neighbour you read is already done, and then noticing that one row (or a few variables) is often enough.
LC 62Unique PathsMediumGrid DPLeetCode
A robot starts in the top-left corner of an m × n grid and can only move right or down. How many different paths reach the bottom-right corner?
Example: m = 3, n = 7 → 28. m = 3, n = 2 → 3.
"Grid", "only right or down", "how many paths". Because moves only go right or down, a cell can only be entered from above or from the left, so its count is the sum of those two cells.
1Brute force — count paths recursively
The number of ways to reach cell (r, c) is the ways to reach the cell above plus the ways to reach the cell on the left. Cells in the top row or left column have only one way (a straight line).
int uniquePaths(int m, int n) {
return paths(m - 1, n - 1);
}
int paths(int r, int c) { // ways to reach cell (r, c)
if (r == 0 || c == 0) return 1; // edge: one straight line
return paths(r - 1, c) + paths(r, c - 1); // from above + from the left
}
Time O(2^(m+n))Space O(m + n)
The middle cells are recomputed from every direction. A 20 × 20 grid already takes billions of calls.
2Optimal — fill the grid row by row, one array
Fill the table from the top-left, so the cell above and the cell to the left are always done. Each cell = above + left. And since a row only needs the row above it, one array is enough.
State: dp[r][c] = number of paths from the start to cell (r, c).
Recurrence: dp[r][c] = dp[r−1][c] + dp[r][c−1].
Base case: the whole first row and first column are 1.
int uniquePaths(int m, int n) {
int[] dp = new int[n]; // dp[c] = ways to reach column c, this row
Arrays.fill(dp, 1); // first row: one way to each cell
for (int r = 1; r < m; r++) {
for (int c = 1; c < n; c++) {
dp[c] = dp[c] + dp[c - 1]; // above (old dp[c]) + left
}
}
return dp[n - 1];
}
Time O(m · n)Space O(n)
In the one-row version, before the update dp[c] still holds the row above, and dp[c − 1] already holds this row's left cell. So dp[c] += dp[c − 1] is "above + left".
Grid, moves only right/down → dp[r][c] = from above + from left. One row of memory is enough because each row only looks at the one above.
The answer can get big: for LeetCode's limits it fits in an int (they promise ≤ 2·10⁹), but a counting variant on a bigger grid needs long. Also don't mix up rows and columns: the array has n (columns) cells.
Pattern notes: DP Patterns · Open on LeetCode
LC 64Minimum Path SumMediumGrid DPLeetCode
Each cell of a grid has a cost. Move from the top-left to the bottom-right, only right or down, and return the smallest total cost of the cells you pass through.
Example: [[1,3,1],[1,5,1],[4,2,1]] → 7 (1 → 3 → 1 → 1 → 1).
Same moves as Unique Paths (right or down only), but the question changes from "how many" to "minimum cost". Same table, different combine step: min instead of +, then add the cell's own cost.
1Brute force — cheapest of the two ways in
The cheapest way to reach (r, c) is its own cost plus the cheaper of: the cheapest way to reach the cell above, or the cell on the left. Stepping off the grid counts as impossible (a huge value).
int minPathSum(int[][] grid) {
return cost(grid, grid.length - 1, grid[0].length - 1);
}
int cost(int[][] grid, int r, int c) { // cheapest way to reach (r, c)
if (r == 0 && c == 0) return grid[0][0];
if (r < 0 || c < 0) return Integer.MAX_VALUE; // off the grid
int best = Math.min(cost(grid, r - 1, c), // came from above
cost(grid, r, c - 1)); // came from the left
return grid[r][c] + best;
}
Time O(2^(m+n))Space O(m + n)
Same explosion as Unique Paths: each cell is reached by many recursive routes and recomputed each time.
2Optimal — one row, min of above and left
Walk the grid row by row. cost to reach a cell = its own cost + min(cost above, cost left). The first row can only come from the left, the first column only from above.
State: dp[r][c] = the cheapest total cost to reach cell (r, c).
Recurrence: dp[r][c] = grid[r][c] + min(dp[r−1][c], dp[r][c−1]).
Base case: dp[0][0] = grid[0][0]; first row = running sum from the left; first column = running sum from above.
int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[] dp = new int[n]; // dp[c] = cheapest cost to column c, this row
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
int here = grid[r][c];
if (r == 0 && c == 0) dp[c] = here;
else if (r == 0) dp[c] = dp[c - 1] + here; // top row: left only
else if (c == 0) dp[c] = dp[c] + here; // left col: above
else dp[c] = Math.min(dp[c], dp[c - 1]) + here;
}
}
return dp[n - 1];
}
Time O(m · n)Space O(n)
Every cell is filled once. You could also write the totals straight into grid for O(1) extra space, but changing the input is often frowned on.
"Min / max cost path with right/down moves" → grid DP with cell + min(above, left). Handle the first row and column separately because they have only one way in.
Don't treat the missing neighbour as 0. If the first row takes min(above, left) with a fake "above" of 0, every top cell costs just itself and the example returns 3 instead of 7. The first row and column must use their only real neighbour.
Pattern notes: DP Patterns · Open on LeetCode
LC 221Maximal SquareMediumGrid DP (min of 3)LeetCode
A grid holds the characters '0' and '1'. Find the largest square made only of 1s and return its area.
Example: [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] → 4 (a 2 × 2 square).
"Largest square of 1s" in a grid: a big square is built from three smaller overlapping squares (above, left, diagonal). When a cell's answer depends on its up, left and up-left neighbours, it is grid DP.
1Brute force — the recurrence, without memory
Let side(i, j) be the biggest square whose bottom-right corner is (i, j). If the cell is 0, it is 0. Otherwise, the square can only grow as far as the weakest of the three squares ending above, to the left, and on the diagonal: 1 + min(of the three). Try every cell as a corner.
int maximalSquare(char[][] matrix) {
int best = 0;
for (int i = 0; i < matrix.length; i++)
for (int j = 0; j < matrix[0].length; j++)
best = Math.max(best, side(matrix, i, j)); // every corner
return best * best; // the question asks for AREA
}
// side of the biggest square of 1s whose bottom-right corner is (i, j)
int side(char[][] m, int i, int j) {
if (i < 0 || j < 0 || m[i][j] == '0') return 0;
int up = side(m, i - 1, j);
int left = side(m, i, j - 1);
int diag = side(m, i - 1, j - 1);
return 1 + Math.min(up, Math.min(left, diag));
}
Time O(3^(m+n)) per cellSpace O(m + n)
Each call makes three more calls, and the same cells are asked for over and over.
2Optimal — same recurrence, stored in a table
Why the min? A square of side k ending at (i, j) needs a square of side k − 1 ending above, to the left and on the diagonal. The smallest of those three is the limit. Fill the table top-left to bottom-right and each cell is O(1).
State: dp[i][j] = side of the largest all-1 square whose bottom-right corner is (i, j).
Recurrence: if matrix[i][j] == '1': dp[i][j] = 1 + min(above, left, diagonal), else 0.
Base case: cells in the first row or column are just 0 or 1. The code adds an extra row and column of 0s so no special case is needed.
int maximalSquare(char[][] matrix) {
int m = matrix.length, n = matrix[0].length;
// dp[i+1][j+1] = side of the biggest square ending at (i, j);
// the extra row and column of 0s mean no bounds checks
int[][] dp = new int[m + 1][n + 1];
int best = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
int up = dp[i][j + 1], left = dp[i + 1][j], diag = dp[i][j];
dp[i + 1][j + 1] = 1 + Math.min(up, Math.min(left, diag));
best = Math.max(best, dp[i + 1][j + 1]);
}
}
}
return best * best;
}
Time O(m · n)Space O(m · n)
One pass over the grid. The table can be cut to one row plus a variable holding the old diagonal value, but the 2D version is much easier to get right in an interview.
"Largest square / shape of 1s ending here" → dp = 1 + min(up, left, diagonal). The min is the bottleneck: one short side stops the square.
The input holds characters '1', not the number 1: compare with == '1'. And return side × side, not the side.
Pattern notes: DP Fundamentals · Open on LeetCode
LC 1143Longest Common SubsequenceMediumTwo-string DPLeetCode
Given two strings, return the length of their longest common subsequence: letters that appear in both, in the same order, but not necessarily next to each other.
Example: "abcde", "ace" → 3 ("ace"). "abc", "def" → 0.
Two strings and "subsequence" (order kept, gaps allowed). With two strings the state needs two numbers: how much of each string you have used. That gives a 2D table, dp[i][j].
1Brute force — match or drop a letter, recursively
Compare the first letters. If they match, that letter is in the LCS: count 1 and move on in both strings. If not, one of them is useless: try dropping it from the first string, then from the second, and keep the better.
int longestCommonSubsequence(String text1, String text2) {
return lcs(text1, text2, 0, 0);
}
int lcs(String a, String b, int i, int j) { // LCS of a[i..] and b[j..]
if (i == a.length() || j == b.length()) return 0; // one is used up
if (a.charAt(i) == b.charAt(j))
return 1 + lcs(a, b, i + 1, j + 1); // match: keep it, move both
return Math.max(lcs(a, b, i + 1, j), // drop a letter from a
lcs(a, b, i, j + 1)); // or drop one from b
}
Time O(2^(m+n))Space O(m + n)
Two strings of length 30 with no matches branch twice at every step. But there are only m × n different (i, j) pairs.
2Optimal — fill the (m+1) × (n+1) table
Look at the last letters of both prefixes. If they match, take the diagonal + 1; if not, take the better of dropping one letter (up or left). Row 0 and column 0 are the empty string.
State: dp[i][j] = LCS length of the first i letters of text1 and the first j letters of text2.
Recurrence: letters match: dp[i][j] = dp[i−1][j−1] + 1; otherwise dp[i][j] = max(dp[i−1][j], dp[i][j−1]).
Base case: row 0 and column 0 are 0 (one string is empty).
int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
// dp[i][j] = LCS of the first i letters of text1 and first j of text2;
// row 0 and column 0 stay 0 (one string is empty)
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1))
dp[i][j] = dp[i - 1][j - 1] + 1; // match: diagonal + 1
else // no match: up or left
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m][n];
}
Time O(m · n)Space O(m · n)
Every cell is filled once in O(1). Each row only reads the row above, so two rows (O(n)) are enough if memory matters.
Two strings → dp[i][j] over prefix lengths, with an empty row and column. Match → diagonal + 1; no match → max(up, left). Edit Distance (LC 72) uses the same table.
Off by one: dp is (m+1) × (n+1) and cell (i, j) compares text1.charAt(i − 1) with text2.charAt(j − 1). Using charAt(i) skips the first letter and crashes at the end.
Pattern notes: DP Patterns · Open on LeetCode
LC 518Coin Change IIMediumUnbounded knapsack (count)LeetCode
Coins of given values (unlimited of each) and an amount. How many different combinations of coins add up to the amount? Order does not matter: 1 + 2 and 2 + 1 are the same.
Example: amount = 5, coins = [1, 2, 5] → 4 (5, 2+2+1, 2+1+1+1, 1+1+1+1+1).
"Number of combinations" to make an amount with unlimited coins. It is Coin Change (LC 322) counting instead of minimising. The 2D state is (which coins are allowed, amount); the words "order does not matter" decide the loop order.
1Brute force — use coin i again, or never again
Look at coin i. Either use it (stay on coin i, because it may be used again) or decide never to use it (move to coin i + 1). Add the two counts. Because we never go back to earlier coins, each combination is counted exactly once.
int change(int amount, int[] coins) {
return count(coins, 0, amount);
}
// ways to make amount using only coins[i..]
int count(int[] coins, int i, int amount) {
if (amount == 0) return 1; // paid exactly: one way
if (amount < 0 || i == coins.length) return 0;
return count(coins, i, amount - coins[i]) // use coin i (maybe again)
+ count(coins, i + 1, amount); // never use coin i again
}
Time O(2^(a))Space O(a)
With a coin of value 1 the recursion is at least amount deep and branches at every step.
2Optimal — coins outside, amounts inside
Think of the 2D table dp[coin][amount] and keep only one row. Handle the coins one at a time: after coin c is processed, dp[a] counts combinations that use only the coins seen so far, so each combination is built in one fixed coin order and never counted twice.
State: dp[a] = number of combinations that make a using the coins handled so far.
Recurrence: for each coin c, for a from c up to amount: dp[a] += dp[a − c].
Base case: dp[0] = 1 (one way to make 0: use no coins).
int change(int amount, int[] coins) {
int[] dp = new int[amount + 1]; // dp[a] = ways to make a so far
dp[0] = 1; // one way to make 0: take nothing
for (int c : coins) { // coins OUTSIDE: combos counted once
for (int a = c; a <= amount; a++) { // go UP: coin c can repeat
dp[a] += dp[a - c];
}
}
return dp[amount];
}
Time O(a · k)Space O(a)
k coins × a amounts, one array. The upward inner loop is what makes the coins unlimited (compare LC 416, where downward means once).
Counting combinations: coins in the outer loop. Counting ordered sequences (permutations, like Combination Sum IV, LC 377): amounts in the outer loop. Inner loop up = reuse allowed; down = each item once.
Swapping the loops (amounts outside, coins inside) counts 1+2 and 2+1 as different and gives 9 instead of 4 for the example. Also, amount = 0 should return 1, not 0.
Pattern notes: DP Patterns · Open on LeetCode
LC 309Best Time to Buy and Sell Stock with CooldownMediumState machine DPLeetCode
You know the stock price for each day. Buy and sell as many times as you like, but hold at most one share, and after you sell you must wait one day (cooldown) before buying again. Return the maximum profit.
Example: [1, 2, 3, 0, 2] → 3 (buy, sell, cooldown, buy, sell).
"Buy / sell" with a rule about what you may do next (cooldown, fee, at most k trades). Each day you are in one of a few modes (holding, just sold, free), so the state is (day, mode): a small 2D DP, also called a state machine.
1Brute force — decide each day, recursively
On day i, if you hold a share: wait, or sell (get the price, and jump to day i + 2 because of the cooldown). If you hold nothing: wait, or buy (pay the price). Take the best.
int maxProfit(int[] prices) {
return best(prices, 0, false);
}
int best(int[] prices, int i, boolean holding) { // best profit from day i
if (i >= prices.length) return 0;
int wait = best(prices, i + 1, holding); // do nothing today
if (holding) { // sell, then cool down
int sell = prices[i] + best(prices, i + 2, false);
return Math.max(wait, sell);
}
int buy = -prices[i] + best(prices, i + 1, true);
return Math.max(wait, buy);
}
Time O(2ⁿ)Space O(n)
Two choices a day, and the same (day, holding) pair is reached along many paths.
2Optimal — three states per day, three variables
Name the modes: hold (own a share), sold (sold today, so tomorrow is cooldown) and rest (no share, free to buy). Buying is only allowed from rest, and sold only turns into rest the next day: that is the cooldown.
State: hold[i], sold[i], rest[i] = best profit at the end of day i if you are in that mode.
Recurrence: hold[i] = max(hold[i−1], rest[i−1] − p); sold[i] = hold[i−1] + p; rest[i] = max(rest[i−1], sold[i−1]).
Base case: before day 0: rest = 0, and hold, sold are impossible (a very negative number).
int maxProfit(int[] prices) {
int hold = Integer.MIN_VALUE / 2; // own a share (impossible at start)
int sold = Integer.MIN_VALUE / 2; // sold TODAY, so tomorrow is cooldown
int rest = 0; // no share and free to buy
for (int p : prices) {
int prevSold = sold;
sold = hold + p; // sell the share we held
hold = Math.max(hold, rest - p); // keep it, or buy (only from rest)
rest = Math.max(rest, prevSold); // stay free, or cooldown is over
}
return Math.max(sold, rest);
}
Time O(n)Space O(1)
One pass. Save the old sold before overwriting it, because rest needs yesterday's value. Integer.MIN_VALUE / 2 is "impossible" without overflowing when a price is added.
Stock problems with rules → list the modes you can be in each day and write one line per mode saying where it can come from. Cooldown, fees and "at most k trades" are all small changes to these lines.
Buying from sold (instead of rest) breaks the cooldown and gives 4 for the example instead of 3. And the answer is max(sold, rest): ending while holding a share is never best.
Pattern notes: DP Fundamentals · Open on LeetCode
This list assumes Beginner and Intermediate are done. Most problems are Hard, but almost every Hard problem is an Intermediate pattern plus one extra insight. Each card names that insight in its "The idea" box and points back to the Intermediate problem it builds on.
Give each problem 45–60 minutes before opening the card. If you are stuck, read only "How to spot the pattern" first and try again.
Arrays & Two Pointers
You already know the tools: prefix/suffix arrays (LC 238), two pointers from both ends (LC 11) and using indexes as keys (LC 268, LC 287). The Hard problems here combine them with one extra insight. In Trapping Rain Water the water above a bar is set by the lower of its two tallest walls, which you compute with prefix maxima and then with two pointers. In First Missing Positive the array becomes its own hash table: value v lives at index v − 1. What is harder than Intermediate is the space rule: the best answers use O(1) extra memory.
LC 42Trapping Rain WaterHardPrefix Max + Two PointersLeetCode
Each number is the height of a bar, all bars 1 wide, standing side by side. Rain falls and fills the dips between bars. How many squares of water stay trapped?
Example: height = [4, 2, 0, 3, 2, 5] → 9. The 4 on the left and the 5 on the right hold water over the four bars in between: 2 + 4 + 1 + 2 = 9.
"Trapped between" bars, water "held" by walls on both sides. The water above one bar depends on the tallest bar to its left and the tallest bar to its right. Whenever an answer at position i needs "the best thing on the left" and "the best thing on the right", think of prefix/suffix arrays (like LC 238 Product of Array Except Self), and then ask whether two pointers can replace them.
1Brute force — for every bar, scan both sides
Look at one bar at a time. Water rises above it up to the lower of the two tallest walls around it (the tallest on the left and the tallest on the right), because water spills over the lower wall. So water[i] = min(leftMax, rightMax) − height[i]. The naive way finds both maxima by scanning each time.
int trap(int[] height) {
int n = height.length, water = 0;
for (int i = 0; i < n; i++) {
int leftMax = 0, rightMax = 0;
for (int j = 0; j <= i; j++) leftMax = Math.max(leftMax, height[j]); // tallest on the left (incl. i)
for (int j = i; j < n; j++) rightMax = Math.max(rightMax, height[j]); // tallest on the right (incl. i)
water += Math.min(leftMax, rightMax) - height[i];
}
return water;
}
Time O(n²)Space O(1)
The formula is right, but every bar rescans the whole array. With the maximum 20,000 bars that is about 400 million steps, too slow. Notice that the scans for bar i and bar i + 1 are almost the same work done twice.
2Optimal — precompute leftMax and rightMax
This is the prefix/suffix idea from LC 238 with max instead of multiply. Build leftMax[i] in one pass from the left and rightMax[i] in one pass from the right, then apply the same formula. Each maximum is just "the previous maximum or me, whichever is bigger", so it costs O(1) per bar.
int trap(int[] height) {
int n = height.length;
int[] leftMax = new int[n], rightMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) leftMax[i] = Math.max(leftMax[i - 1], height[i]);
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) rightMax[i] = Math.max(rightMax[i + 1], height[i]);
int water = 0;
for (int i = 0; i < n; i++) {
water += Math.min(leftMax[i], rightMax[i]) - height[i]; // never negative: both maxima include height[i]
}
return water;
}
Time O(n)Space O(n)
Three simple passes instead of a nested loop. This is the version to explain first in an interview: it makes the formula obvious. (LeetCode guarantees at least one bar, so height[0] is safe.)
3Even better — two pointers, O(1) space
Do we really need both arrays? Put left at the start and right at the end, and keep leftMax and rightMax seen so far. If height[left] < height[right], then somewhere on the right there is a wall at least as tall as height[right], which is taller than the left side. So for bar left the left wall is the lower one, and leftMax alone decides its water. Settle that bar and move left. Otherwise do the same from the right. It is the "move the shorter side" rule from LC 11 Container With Most Water.
int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0, water = 0;
while (left < right) {
if (height[left] < height[right]) { // right side has a taller wall for sure
leftMax = Math.max(leftMax, height[left]);
water += leftMax - height[left]; // left wall decides this bar
left++;
} else { // left side has a wall at least as tall
rightMax = Math.max(rightMax, height[right]);
water += rightMax - height[right];
right--;
}
}
return water;
}
Time O(n)Space O(1)
Each bar is settled exactly once, by whichever pointer reaches it, and we only keep four numbers.
The water above a bar is min(tallest left, tallest right) − its height. Get it working with prefix/suffix max arrays, then squeeze the space with two pointers that always move the side with the smaller wall.
In the two-pointer version, update leftMax before adding water (so a new tall bar adds 0, never a negative amount). And you must move the smaller side: moving the taller side uses a wall you are not yet sure about. Also, the answer is not "tallest bar minus each bar": water is capped by the lower of the two walls.
Pattern notes: Two Pointers · Open on LeetCode
LC 41First Missing PositiveHardCyclic SortLeetCode
You get an unsorted array of whole numbers (they can be negative, zero, huge or repeated). Return the smallest positive number that is not in it. The catch: O(n) time and O(1) extra space.
Example: nums = [3, 4, -1, 1] → 2. 1 is there, 2 is not.
"Smallest missing positive" with numbers that should be in the range 1..n, plus "O(1) extra space". With n numbers the answer is always between 1 and n + 1, so every useful value has a natural home: value v belongs at index v − 1. When values map onto indexes and you may not use extra memory, use the array itself as the hash table (cyclic sort, or marking indexes with a minus sign).
1Brute force — try 1, 2, 3, … and search for each
Ask "is 1 in the array?" by scanning it. If yes, ask about 2, then 3, and so on. The first number you cannot find is the answer. The easier fix, a HashSet, gives O(n) time but uses O(n) extra memory, which the problem forbids.
int firstMissingPositive(int[] nums) {
for (int want = 1; ; want++) {
boolean found = false;
for (int x : nums) {
if (x == want) { found = true; break; }
}
if (!found) return want; // at most n + 1 rounds
}
}
Time O(n²)Space O(1)
For [1, 2, …, 100000] it checks 100,001 numbers with a full scan each: about 10 billion steps. Far too slow.
2Optimal — cyclic sort: put every value in its home slot
This is index-as-a-key thinking from LC 268 Missing Number and LC 287 Find the Duplicate, plus one extra step: swap each value v into slot v − 1 until it is home. Values that are ≤ 0, > n, or duplicates have no free home, so they stay wherever they are. Afterwards, the first slot i that does not hold i + 1 gives the answer. If every slot is right, the answer is n + 1.
i forward when the value at i is home or has no home. −1 has no home and ends up in slot 1, so 2 is missing.int firstMissingPositive(int[] nums) {
int n = nums.length, i = 0;
while (i < n) {
int v = nums[i];
if (v >= 1 && v <= n && nums[v - 1] != v) { // v has a home and it is not taken by a v yet
nums[i] = nums[v - 1]; // swap v into slot v-1
nums[v - 1] = v; // do NOT move i: check the new value here
} else {
i++; // home already, out of range, or a duplicate
}
}
for (int j = 0; j < n; j++) {
if (nums[j] != j + 1) return j + 1; // first slot without its owner
}
return n + 1; // 1..n are all present
}
Time O(n)Space O(1)
The loop looks nested, but every swap puts one value into its final home, and a value that is home never moves again. So there are at most n swaps plus n i++ steps.
3Another O(1) way — mark slots with a minus sign
If swapping feels fiddly, use the sign of nums[v − 1] as a "v was seen" flag. First replace every useless value (≤ 0 or > n) with n + 1, so all numbers are positive. Then for each value v in range, make nums[v − 1] negative. The first index still positive is the missing number.
int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
if (nums[i] <= 0 || nums[i] > n) nums[i] = n + 1; // useless values become harmless
}
for (int i = 0; i < n; i++) {
int v = Math.abs(nums[i]); // it may already be marked negative
if (v <= n && nums[v - 1] > 0) nums[v - 1] = -nums[v - 1]; // mark "v is present"
}
for (int i = 0; i < n; i++) {
if (nums[i] > 0) return i + 1; // nobody marked this slot
}
return n + 1;
}
Time O(n)Space O(1)
Same idea (index = value − 1), stored as a sign instead of a position. Use Math.abs when reading, because the value may already have been flipped.
The answer to "smallest missing positive" is always in 1..n+1, so the array has exactly enough slots to act as its own lookup table. Value v goes to index v − 1, then scan for the first wrong slot.
Check nums[v - 1] != v, not nums[i] != i + 1. With duplicates like [1, 1], the second 1 would keep swapping with the first one forever. Also check the range 1..n before using v - 1 as an index, or negatives and huge values crash with an out-of-bounds error.
Pattern notes: Cyclic Sort · Open on LeetCode
Sliding Window
Same template as Intermediate: grow right, shrink left while the rule is broken, record the answer. What is new is what the window has to track. Minimum Window Substring needs a single "still missing" counter so the check is O(1). Sliding Window Maximum needs a monotonic deque, because a maximum cannot be undone like a sum. K Different Integers turns "exactly K" into atMost(K) − atMost(K − 1). Concatenation of All Words slides over whole words, once per starting offset.
LC 76Minimum Window SubstringHardSliding WindowLeetCode
Given strings s and t, find the shortest substring of s that contains every letter of t, including repeats (if t has two as, the window needs two). Return "" if there is none.
Example: s = "ADOBECODEBANC", t = "ABC" → "BANC".
"Minimum window", "substring" (neighbours only), and "contains all characters of t". That is a window with a coverage rule: grow until the window covers t, then shrink while it still does. It mixes two Intermediate problems: the letter counts of LC 438 Find All Anagrams and the shrink-to-find-the-minimum loop of LC 209.
1Brute force — try every start, grow until covered
For each start i, add letters one by one and stop at the first j where the window covers t. That is the shortest good window starting at i. "Covers" means: for every letter, the window has at least as many as t.
String minWindow(String s, String t) {
int[] need = new int[128];
for (char c : t.toCharArray()) need[c]++;
int bestStart = 0, bestLen = Integer.MAX_VALUE;
for (int i = 0; i < s.length(); i++) {
int[] have = new int[128];
for (int j = i; j < s.length(); j++) {
have[s.charAt(j)]++;
if (covers(have, need)) { // 128 comparisons
if (j - i + 1 < bestLen) { bestLen = j - i + 1; bestStart = i; }
break; // longer windows from i are worse
}
}
}
return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen);
}
boolean covers(int[] have, int[] need) {
for (int c = 0; c < 128; c++) if (have[c] < need[c]) return false;
return true;
}
Time O(m² · 128)Space O(1)
With s up to 100,000 letters that is up to 10 billion window steps, each with a 128-letter check. Every new start rebuilds counts the previous start already had.
2Optimal — sliding window with a "missing" counter
This is the LC 209 window (grow right, shrink left, record while shrinking) with LC 438's letter counts, plus one extra insight: keep a single number missing = how many letters of t the window still lacks. Then "is the window good?" is just missing == 0, O(1) instead of comparing 128 counts. Store need[c] = how many more of c we still want; it goes negative for extra copies, which is fine.
missing hits 0 we record the window and shrink until a needed letter falls out.String minWindow(String s, String t) {
int[] need = new int[128]; // need[c] > 0: still want c; < 0: extra copies
for (char c : t.toCharArray()) need[c]++;
int missing = t.length(); // letters of t not yet covered
int left = 0, bestStart = 0, bestLen = Integer.MAX_VALUE;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (need[c] > 0) missing--; // this letter was actually useful
need[c]--;
while (missing == 0) { // window covers t: record, then try smaller
if (right - left + 1 < bestLen) { bestLen = right - left + 1; bestStart = left; }
char out = s.charAt(left);
need[out]++;
if (need[out] > 0) missing++; // we just dropped a letter we needed
left++;
}
}
return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen);
}
Time O(m + n)Space O(1)
Each letter of s enters once and leaves once, and we count t once. The two arrays have a fixed 128 slots, so the extra space does not grow with the input.
For "smallest window that covers X": keep counts plus one counter of what is still missing. Grow until it is 0, then shrink while it stays 0, recording the answer inside the shrink loop.
Only change missing when the letter matters: decrease it only if need[c] > 0 before the decrement, and increase it only if need[out] > 0 after the increment. Otherwise extra copies (like a second B) would be counted as useful. Also save bestStart and bestLen, not a substring on every step (that costs O(m) each time).
Pattern notes: Sliding Window · Open on LeetCode
LC 239Sliding Window MaximumHardMonotonic DequeLeetCode
A window of size k slides over the array one step at a time. Return the maximum of each window.
Example: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 → [3, 3, 5, 5, 6, 7].
"Window of size k" moving "one position at a time" is a fixed-size window (like LC 643). But the question is the maximum, and a maximum cannot be "un-added" like a sum: when the biggest number leaves, you do not know the next biggest. "Max or min of every window" is the signal for a monotonic deque.
1Brute force — scan every window
For every window start, loop over its k numbers and take the largest.
int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1];
for (int i = 0; i + k <= n; i++) {
int max = nums[i];
for (int j = i; j < i + k; j++) max = Math.max(max, nums[j]);
ans[i] = max;
}
return ans;
}
Time O(n · k)Space O(1)
With n = 100,000 and k = 50,000 that is about 2.5 billion comparisons. Neighbouring windows share k − 1 numbers, yet we compare them all again.
2Optimal — a deque that keeps the window's maximum at the front
This is the fixed-size window from LC 643 plus the "next greater" stack from LC 739 Daily Temperatures, turned into a deque. Key insight: if a newer number is bigger, every older smaller number can never be a maximum again, because the newer one will outlive it in the window. So keep indexes in a deque with values decreasing from front to back. The front is the maximum. On each step: drop the front if it slid out of the window, pop smaller values from the back, push the new index.
int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1];
Deque<Integer> dq = new ArrayDeque<>(); // indexes; values decrease front -> back
for (int i = 0; i < n; i++) {
if (!dq.isEmpty() && dq.peekFirst() <= i - k) {
dq.pollFirst(); // front slid out of the window
}
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) {
dq.pollLast(); // smaller and older: can never win again
}
dq.offerLast(i);
if (i >= k - 1) ans[i - k + 1] = nums[dq.peekFirst()]; // window is full: record max
}
return ans;
}
Time O(n)Space O(k)
Every index is pushed once and popped at most once, so the total work is about 2n, no matter how big k is. The deque never holds more than k indexes.
"Max (or min) of every window" → monotonic deque of indexes. Pop from the back what the new item beats, pop from the front what fell out of the window, read the answer at the front.
Store indexes, not values: you need them to know when the front has left the window (index <= i − k), and duplicates like [2, 2, 2] would confuse a value-only deque. A PriorityQueue also works but is O(n log n) and needs lazy removal of old items, so it is the weaker answer.
Pattern notes: Monotonic Stack & Queue · Open on LeetCode
LC 992Subarrays with K Different IntegersHardSliding WindowLeetCode
Count the subarrays (neighbouring runs) that contain exactly k different numbers.
Example: nums = [1, 2, 1, 2, 3], k = 2 → 7: [1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].
"Count subarrays" with "exactly k different". A plain window handles "at most k" easily (LC 904 Fruit Into Baskets is "at most 2 types"), but "exactly" does not shrink cleanly: from one window, several different left edges can all be valid. When you see exactly K in a counting problem, think atMost(K) − atMost(K − 1).
1Brute force — every start, grow until too many
For each start i, extend to the right while counting different numbers. Every time the count is exactly k, that subarray counts. Once it goes above k, stop: it can only grow from there.
int subarraysWithKDistinct(int[] nums, int k) {
int n = nums.length, count = 0;
for (int i = 0; i < n; i++) {
int[] freq = new int[n + 1]; // values are 1..n
int distinct = 0;
for (int j = i; j < n; j++) {
if (freq[nums[j]]++ == 0) distinct++;
if (distinct == k) count++;
else if (distinct > k) break;
}
}
return count;
}
Time O(n²)Space O(n)
With n = 20,000 that is up to 200 million inner steps (plus a fresh array per start), right at the edge of the time limit. Interviewers expect O(n).
2Optimal — exactly(k) = atMost(k) − atMost(k − 1)
This is the "at most k types" window from LC 904, plus one extra insight: windows with exactly k = windows with at most k minus windows with at most k − 1. Counting "at most k" is easy: for each right, shrink until the window has at most k types; then every subarray ending at right and starting anywhere in [left, right] is valid. That is right − left + 1 subarrays in one go.
right; it adds right − left + 1 subarrays. Subtracting the "at most 1" count leaves exactly the ones with 2 different numbers.int subarraysWithKDistinct(int[] nums, int k) {
return atMost(nums, k) - atMost(nums, k - 1);
}
// number of subarrays with AT MOST k different values
int atMost(int[] nums, int k) {
int[] freq = new int[nums.length + 1];
int left = 0, distinct = 0, count = 0;
for (int right = 0; right < nums.length; right++) {
if (freq[nums[right]]++ == 0) distinct++; // a new value entered
while (distinct > k) { // too many types: shrink
if (--freq[nums[left]] == 0) distinct--;
left++;
}
count += right - left + 1; // all windows ending at right
}
return count;
}
Time O(n)Space O(n)
Two linear passes. atMost(nums, 0) returns 0 on its own (the window always shrinks to empty), so k = 1 needs no special case.
"Count subarrays with exactly K" is hard to slide; "at most K" is easy. Use exactly(K) = atMost(K) − atMost(K − 1), and in atMost add right − left + 1 per step. The same trick solves LC 1248 Count Number of Nice Subarrays.
Add right − left + 1, not 1: you are counting all subarrays that end at right, not just the longest one. And the trick needs "at most" to be monotonic (a smaller window of a valid window is still valid), which is true for "number of different values".
Pattern notes: Sliding Window · Open on LeetCode
LC 30Substring with Concatenation of All WordsHardSliding WindowLeetCode
You get a string s and a list of words, all the same length. Find every start index in s where some ordering of all the words appears glued together (each word used exactly as many times as it is listed).
Example: s = "barfoothefoobarman", words = ["foo", "bar"] → [0, 9] ("barfoo" and "foobar").
"Concatenation of all words in any order" is an anagram question (LC 438 Find All Anagrams), just with whole words instead of letters. "All words have the same length" is the unlock: it lets you cut s into word-sized pieces and slide a window one word at a time, keeping a count map of words.
1Brute force — check a block at every start
The glued block has length k · L (k words of length L). For each possible start, chop the next k pieces and count them in a map. If a piece is not a word, or appears too often, stop early.
List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
int L = words[0].length(), total = L * words.length;
Map<String, Integer> need = new HashMap<>();
for (String w : words) need.merge(w, 1, Integer::sum);
for (int i = 0; i + total <= s.length(); i++) {
Map<String, Integer> seen = new HashMap<>();
int j = 0;
for (; j < words.length; j++) {
String w = s.substring(i + j * L, i + (j + 1) * L);
int c = seen.merge(w, 1, Integer::sum);
if (c > need.getOrDefault(w, 0)) break; // unknown word or one copy too many
}
if (j == words.length) res.add(i);
}
return res;
}
Time O(n · k · L)Space O(k · L)
Thanks to the early break this often passes on LeetCode (the limits are small: s up to 10,000). But starts that are exactly L apart cut the same words again and again, and the interviewer will ask you to reuse that work.
2Optimal — L separate sliding windows, moving one word at a time
This is the LC 438 count-matching window on words, plus one extra insight: there are only L different ways to cut s into words (offsets 0 to L − 1). For each offset, slide a window word by word. Add the word on the right; if it is not a word at all, reset the window after it; if it appears too often, drop words from the left until it fits. When the window holds exactly k words, record left.
s into bar | foo | the | foo | bar | man. The window moves by whole words; a non-word wipes it, too many copies shrink it from the left.List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
int L = words[0].length(), k = words.length;
Map<String, Integer> need = new HashMap<>();
for (String w : words) need.merge(w, 1, Integer::sum);
for (int offset = 0; offset < L; offset++) { // L different ways to cut s
Map<String, Integer> seen = new HashMap<>();
int left = offset, used = 0; // used = words in the window
for (int right = offset; right + L <= s.length(); right += L) {
String w = s.substring(right, right + L);
if (!need.containsKey(w)) { // not a word: nothing can cross it
seen.clear(); used = 0; left = right + L;
continue;
}
seen.merge(w, 1, Integer::sum);
used++;
while (seen.get(w) > need.get(w)) { // too many copies of w: shrink
seen.merge(s.substring(left, left + L), -1, Integer::sum);
used--;
left += L;
}
if (used == k) { // exactly all words
res.add(left);
seen.merge(s.substring(left, left + L), -1, Integer::sum); // slide on by one word
used--;
left += L;
}
}
}
return res;
}
Time O(n · L)Space O(k · L)
For each of the L offsets, every word-piece enters and leaves the window once, and cutting a piece costs L. The answer indexes come out grouped by offset, not sorted; LeetCode accepts any order.
When all items have the same length, a string problem becomes an array problem: cut into pieces and slide by whole pieces, once per starting offset. Then it is the familiar count-map window.
Do not forget the other offsets: sliding only from index 0 misses answers like start 1 or 2. Also, words can contain duplicates (["word", "good", "best", "word"]), so use a count map, not a set.
Pattern notes: Sliding Window · Open on LeetCode
Monotonic Stack
In Intermediate (LC 739 Daily Temperatures, LC 503, LC 901) the monotonic stack answered "who is the next greater item?". The template is the same here: keep a stack that is always sorted; when a newcomer breaks the order, pop, and do the work for each popped item. What is harder at this level is what you do at the pop. When a bar is popped, the new top and the newcomer are its two walls, which gives a rectangle width (LC 84, 85) or a count of subarrays (LC 907). In the last two problems the stack is the answer: you pop a bigger digit or letter to make the result smaller (a "greedy" stack, LC 402 and LC 316).
LC 84Largest Rectangle in HistogramHardMonotonic StackLeetCode
You get the heights of bars standing side by side, each bar 1 wide. Find the biggest rectangle that fits inside the bars. A rectangle over several bars can only be as tall as the shortest of them.
Example: heights = [2, 1, 5, 6, 2, 3] → 10: the bars 5 and 6 give a rectangle of height 5 and width 2.
For each bar, the best rectangle using that bar's full height stretches left and right until it hits a shorter bar. "Nearest shorter bar on the left and on the right" is exactly the "next smaller element" question, and next smaller / next greater always means a monotonic stack.
1Brute force — every pair of edges
Try every left edge i. Move the right edge j outward, keeping the minimum height seen so far. The rectangle from i to j has area minHeight × width.
int largestRectangleArea(int[] heights) {
int best = 0;
for (int i = 0; i < heights.length; i++) {
int minH = Integer.MAX_VALUE;
for (int j = i; j < heights.length; j++) {
minH = Math.min(minH, heights[j]); // the shortest bar limits the height
best = Math.max(best, minH * (j - i + 1));
}
}
return best;
}
Time O(n²)Space O(1)
With up to 100,000 bars that is about 5 billion pairs, which times out.
2Optimal — increasing stack, measure at the pop
This is the "next smaller element" stack from Intermediate (LC 739 with the sign flipped), plus one extra insight: keep bar indexes in a stack with increasing heights. When a shorter bar arrives, the top bar cannot stretch any further right. At that moment you know both of its walls: the right wall is the newcomer, the left wall is the bar just below it in the stack. So width = i − newTop − 1 and you can measure its rectangle right there. A fake bar of height 0 at the end pops everything that is left.
int largestRectangleArea(int[] heights) {
int n = heights.length, best = 0;
Deque<Integer> stack = new ArrayDeque<>(); // indexes, heights increasing
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i]; // fake bar of height 0 at the end
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()]; // this bar can't go further right
int left = stack.isEmpty() ? -1 : stack.peek(); // its left wall
int width = i - left - 1; // between the two walls
best = Math.max(best, height * width);
}
stack.push(i);
}
return best;
}
Time O(n)Space O(n)
Every bar is pushed once and popped once, so the inner while adds up to n pops in total.
In a monotonic stack, the moment of the pop is when an item learns both neighbours: the newcomer on the right and the new top on the left. "For each item, how far can it stretch?" is answered at the pop.
Without the extra height-0 step at i == n, bars that never meet a shorter bar (like [1, 2, 3, 4, 5]) stay in the stack and are never measured. Also, when the stack becomes empty the left wall is -1, not 0: the bar stretches all the way to the start.
Pattern notes: Monotonic Stack · Open on LeetCode
LC 85Maximal RectangleHardMonotonic StackLeetCode
You get a grid of '0' and '1' characters. Find the largest rectangle made only of 1s and return its area.
Example:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
→ 6: the 2 by 3 block in rows 1–2, columns 2–4.
"Largest rectangle" in a 0/1 grid. Stand on one row and look up: each column has a run of 1s ending at this row. Those runs are bars, so each row turns into a histogram, and the largest rectangle in a histogram is LC 84, which you just solved.
1Brute force — grow from every top-left corner
Pick every cell as the top-left corner. Go down row by row; in each row count how many 1s continue to the right, but never more than the row above allowed (the rectangle must stay a rectangle). Area = rows so far × allowed width.
int maximalRectangle(char[][] matrix) {
int rows = matrix.length, cols = matrix[0].length, best = 0;
for (int r1 = 0; r1 < rows; r1++) {
for (int c1 = 0; c1 < cols; c1++) {
int limit = cols; // width allowed so far
for (int r2 = r1; r2 < rows; r2++) {
int c = c1;
while (c < limit && matrix[r2][c] == '1') c++;
limit = c; // can only shrink going down
if (limit == c1) break; // hit a 0 at the corner column
best = Math.max(best, (r2 - r1 + 1) * (limit - c1));
}
}
}
return best;
}
Time O(R² · C²)Space O(1)
On a 200 by 200 grid full of 1s this is about 400 million steps, slow enough to time out.
2Optimal — one histogram per row, then LC 84
This is LC 84 plus one extra insight: keep an array heights[c] = how many 1s stack up in column c ending at the current row. Moving down one row, a 1 adds one to the height, a 0 resets it to 0. Every rectangle of 1s has a bottom row, and at that row it is a rectangle in the histogram. So run LC 84 on every row and keep the best.
int maximalRectangle(char[][] matrix) {
int cols = matrix[0].length, best = 0;
int[] heights = new int[cols];
for (char[] row : matrix) {
for (int c = 0; c < cols; c++) {
heights[c] = (row[c] == '1') ? heights[c] + 1 : 0; // grow or reset
}
best = Math.max(best, largestRectangleArea(heights)); // LC 84
}
return best;
}
int largestRectangleArea(int[] heights) {
int n = heights.length, best = 0;
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()];
int left = stack.isEmpty() ? -1 : stack.peek();
best = Math.max(best, height * (i - left - 1));
}
stack.push(i);
}
return best;
}
Time O(R · C)Space O(C)
Each row costs O(C) to update the heights and O(C) for the stack, and there are R rows.
A 2D problem is often a 1D problem repeated once per row, with a small array carried from row to row. When you see "rectangle in a grid", ask: "what does each row look like if I compress everything above it?"
The cells are characters, so compare with '1', not the number 1 (row[c] == 1 compiles but is always false). And a 0 must reset the height to 0, not just skip adding: the column of 1s is broken.
Pattern notes: Monotonic Stack · Open on LeetCode
LC 907Sum of Subarray MinimumsMediumMonotonic Stack · ContributionLeetCode
Look at every subarray (every run of neighbouring numbers). Take the minimum of each one and add all those minimums together. The answer can be huge, so return it modulo 1,000,000,007.
Example: arr = [3, 1, 2, 4] → 17. The 10 subarrays have minimums 3, 1, 2, 4, 1, 1, 2, 1, 1, 1.
"Sum over all subarrays of the min (or max)". There are about n²/2 subarrays, far too many to list. Flip the question: instead of "what is the min of each subarray?", ask "for how many subarrays is this element the min?". That count depends on the nearest smaller element on each side: a monotonic stack job.
1Brute force — every subarray with a running minimum
For each start i, extend the end j and keep the minimum so far. Add it to the total each time.
int sumSubarrayMins(int[] arr) {
final int MOD = 1_000_000_007;
long total = 0;
for (int i = 0; i < arr.length; i++) {
int min = Integer.MAX_VALUE;
for (int j = i; j < arr.length; j++) {
min = Math.min(min, arr[j]); // min of arr[i..j]
total = (total + min) % MOD;
}
}
return (int) total;
}
Time O(n²)Space O(1)
With 30,000 numbers that is about 450 million subarrays; far too slow to count one by one.
2Optimal — each element's contribution
This is the "nearest smaller on each side" stack from LC 84, plus the contribution trick. For arr[i], let left = how many start positions keep it the minimum (distance back to the previous smaller number), and right = how many end positions do (distance to the next smaller one). Then it is the minimum of left × right subarrays, and the answer is the sum of arr[i] × left[i] × right[i]. Two stack passes find all the distances.
int sumSubarrayMins(int[] arr) {
final int MOD = 1_000_000_007;
int n = arr.length;
int[] left = new int[n], right = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) { // previous STRICTLY smaller
while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) stack.pop();
left[i] = stack.isEmpty() ? i + 1 : i - stack.peek();
stack.push(i);
}
stack.clear();
for (int i = n - 1; i >= 0; i--) { // next smaller OR EQUAL
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) stack.pop();
right[i] = stack.isEmpty() ? n - i : stack.peek() - i;
stack.push(i);
}
long total = 0;
for (int i = 0; i < n; i++) {
total = (total + (long) arr[i] * left[i] % MOD * right[i]) % MOD;
}
return (int) total;
}
Time O(n)Space O(n)
Two passes of a monotonic stack plus one pass to add up the contributions.
Contribution technique: when asked to sum something over all subarrays, turn it around and ask how much each element adds. "How many subarrays is it the min/max of?" = (choices of start) × (choices of end).
Equal values. In [2, 2], the subarray [2, 2] must be counted once. Make one side strict (>) and the other not (>=). With strict on both sides you count it zero times and get 4 instead of 6; with >= on both sides you count it twice. Also multiply in long: 30000 × 15000 × 15000 does not fit in an int.
Pattern notes: Monotonic Stack · Open on LeetCode
LC 402Remove K DigitsMediumGreedy Monotonic StackLeetCode
You get a number as a string and a count k. Remove exactly k digits (the others keep their order) so that the number left is as small as possible. Drop leading zeros; if nothing is left, return "0".
Example: num = "1432219", k = 3 → "1219" (remove 4, 3 and one 2). num = "10200", k = 1 → "200".
"Smallest result after removing items, keeping the order". The leftmost digits matter most, so you want the front of the result to be as small as possible. "A new small item makes earlier bigger items worth throwing away" is a stack that pops bigger items: an increasing monotonic stack used greedily.
1Brute force — remove the best single digit, k times
Try removing each digit, keep the smallest string you get, and repeat k times. (All candidates have the same length, so comparing strings compares the numbers.)
String removeKdigits(String num, int k) {
String cur = num;
for (int step = 0; step < k; step++) {
String best = null;
for (int i = 0; i < cur.length(); i++) {
String cand = cur.substring(0, i) + cur.substring(i + 1);
if (best == null || cand.compareTo(best) < 0) best = cand;
}
cur = best;
}
int i = 0;
while (i < cur.length() - 1 && cur.charAt(i) == '0') i++; // drop leading zeros
cur = cur.substring(i);
return cur.isEmpty() ? "0" : cur;
}
Time O(k · n²)Space O(n)
Each round builds n strings of length n. With 100,000 digits even one round is 10 billion character copies. (Trying every set of k digits to remove is even worse: exponential.)
2Optimal — increasing stack, pop while the digit is bigger
This is the monotonic stack from Intermediate, plus one extra insight: the stack is the answer being built, and a pop is a removal. Read digits left to right. While you still have removals left and the top of the stack is bigger than the new digit, pop it: a smaller digit moved one place to the left always makes the number smaller. If removals are left at the end, the stack is increasing, so cut from the end (the biggest digits).
String removeKdigits(String num, int k) {
StringBuilder stack = new StringBuilder(); // used as a stack of digits
for (char c : num.toCharArray()) {
while (k > 0 && stack.length() > 0
&& stack.charAt(stack.length() - 1) > c) {
stack.deleteCharAt(stack.length() - 1); // bigger digit before a smaller: remove it
k--;
}
stack.append(c);
}
stack.setLength(stack.length() - k); // still owe removals: cut the tail
int i = 0;
while (i < stack.length() && stack.charAt(i) == '0') i++; // skip leading zeros
String ans = stack.substring(i);
return ans.isEmpty() ? "0" : ans;
}
Time O(n)Space O(n)
Each digit is appended once and deleted at most once; deleting from the end of a StringBuilder is O(1).
"Make the smallest (or largest) sequence by deleting items" → a greedy stack: pop the top while it is worse than the newcomer and you are still allowed to delete. The same shape solves LC 316 next, and LC 1673 and LC 321.
Three edge cases: "12345" never pops, so you must cut k digits from the end; "10200" leaves "0200", so strip leading zeros; "10" with k = 2 leaves nothing, so return "0", not "".
Pattern notes: Monotonic Stack · Open on LeetCode
LC 316Remove Duplicate LettersMediumGreedy Monotonic StackLeetCode
Remove letters from a lowercase string so that every letter appears exactly once, keeping the order of what is left. Among all ways to do that, return the one that comes first in dictionary order.
Example: s = "bcabc" → "abc". s = "cbacdcbc" → "acdb".
"Smallest in lexicographical order" after removing items and keeping the order: the same signal as LC 402. The twist is a rule about which items you may drop: each letter must survive once. So before popping a letter, ask "will I see it again later?"
1Brute force — try every order of the distinct letters
The answer uses each distinct letter once, so it is some ordering of them. Generate the orderings in alphabetical order and return the first one that is a subsequence of s.
String removeDuplicateLetters(String s) {
StringBuilder letters = new StringBuilder();
for (char c = 'a'; c <= 'z'; c++) if (s.indexOf(c) >= 0) letters.append(c);
return firstPerm("", letters.toString(), s);
}
// tries orderings in alphabetical order; the first one that fits is the answer
String firstPerm(String built, String left, String s) {
if (left.isEmpty()) return isSubsequence(built, s) ? built : null;
for (int i = 0; i < left.length(); i++) {
String res = firstPerm(built + left.charAt(i),
left.substring(0, i) + left.substring(i + 1), s);
if (res != null) return res;
}
return null;
}
boolean isSubsequence(String t, String s) {
int j = 0;
for (int i = 0; i < s.length() && j < t.length(); i++) {
if (s.charAt(i) == t.charAt(j)) j++;
}
return j == t.length();
}
Time O(d! · n)Space O(d)
With d distinct letters there are d! orderings. For 26 letters that is about 4 × 1026: hopeless.
2Optimal — greedy stack that only pops letters that come back
This is LC 402's greedy stack plus two extra rules. First, store each letter's last index. Then read the string: if the letter is already in the stack, skip it. Otherwise, pop the top while it is bigger than the new letter AND it appears again later (you can pick it up later, in a better place). A letter that never comes back must stay where it is.
d is bigger than b but it is the last d, so it stays.String removeDuplicateLetters(String s) {
int[] last = new int[26];
for (int i = 0; i < s.length(); i++) last[s.charAt(i) - 'a'] = i;
boolean[] inStack = new boolean[26];
StringBuilder stack = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (inStack[c - 'a']) continue; // already placed, keep the earlier one
while (stack.length() > 0) {
char top = stack.charAt(stack.length() - 1);
if (top > c && last[top - 'a'] > i) { // bigger AND comes again later
stack.deleteCharAt(stack.length() - 1);
inStack[top - 'a'] = false;
} else break;
}
stack.append(c);
inStack[c - 'a'] = true;
}
return stack.toString();
}
Time O(n)Space O(1)
Each letter is pushed and popped at most once. The arrays have 26 slots and the stack holds at most 26 letters, so extra space is constant.
Greedy stack + a "can I still get it later?" check. Precomputing the last index of each item is what makes a greedy pop safe. (LC 1081 is the same problem with a different name.)
Don't forget the inStack skip. Without it, in "abacb" the second a would pop b and push another a, giving duplicates. And set inStack back to false when you pop, or that letter can never be added again.
Pattern notes: Monotonic Stack · Open on LeetCode
Binary Search on the Answer
In Intermediate (LC 875 Koko, LC 1011 Ship Packages) you learned to binary search on the answer: guess a value, run a yes/no check, and look for the spot where the answers flip from no to yes. The template: range of possible answers → canDo(x) → "first yes" binary search. What is harder here: the check itself is a small puzzle (greedy pieces, runs of neighbours, a two-pointer count), and in LC 4 the thing you binary search is not a value at all but where to cut an array.
LC 4Median of Two Sorted ArraysHardBinary Search on PartitionLeetCode
You get two sorted arrays. Find the median of all their numbers together: the middle one if the total count is odd, or the average of the two middle ones if it is even. The follow-up asks for O(log(m + n)) time.
Example: [1, 3] and [2] → 2.0. [1, 2] and [3, 4] → 2.5.
Two sorted arrays plus a log time requirement: you are not allowed to walk through them, so you must binary search something. The median splits all numbers into a left half and a right half. If you decide how many numbers the left half takes from the first array, the rest must come from the second. That one choice is what you binary search.
1Brute force — merge, then take the middle
Merge the two sorted arrays like in merge sort, then read the middle element (or the two middle ones).
double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[] merged = new int[m + n];
int i = 0, j = 0, k = 0;
while (i < m && j < n) merged[k++] = (nums1[i] <= nums2[j]) ? nums1[i++] : nums2[j++];
while (i < m) merged[k++] = nums1[i++];
while (j < n) merged[k++] = nums2[j++];
int total = m + n;
if (total % 2 == 1) return merged[total / 2];
return (merged[total / 2 - 1] + merged[total / 2]) / 2.0;
}
Time O(m + n)Space O(m + n)
With at most 2,000 numbers this actually passes, but it misses the point: the problem asks for log time, and interviewers ask this question only for the binary search.
2Optimal — binary search on where to cut the shorter array
This is binary search from Intermediate, plus the partition insight. The left half has half = (m + n + 1) / 2 numbers. If it takes i from nums1, it takes j = half − i from nums2. The cut is correct when every number on the left is ≤ every number on the right; since both arrays are sorted you only check the two crossing pairs: A[i−1] ≤ B[j] and B[j−1] ≤ A[i]. If A[i−1] is too big, cut nums1 further left; otherwise cut it further right.
double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) return findMedianSortedArrays(nums2, nums1); // search the shorter
int m = nums1.length, n = nums2.length;
int half = (m + n + 1) / 2; // size of the left half
int lo = 0, hi = m; // how many numbers nums1 gives to the left
while (lo <= hi) {
int i = (lo + hi) / 2;
int j = half - i; // nums2 gives the rest
int aLeft = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int aRight = (i == m) ? Integer.MAX_VALUE : nums1[i];
int bLeft = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int bRight = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (aLeft <= bRight && bLeft <= aRight) { // valid cut
if ((m + n) % 2 == 1) return Math.max(aLeft, bLeft);
return (Math.max(aLeft, bLeft) + (double) Math.min(aRight, bRight)) / 2.0;
} else if (aLeft > bRight) {
hi = i - 1; // nums1 gave too many: cut left
} else {
lo = i + 1; // nums1 gave too few: cut right
}
}
throw new IllegalArgumentException("inputs are not sorted");
}
Time O(log min(m, n))Space O(1)
The binary search runs over the shorter array only, so it is even better than the O(log(m + n)) the problem asks for.
Binary search does not need a value to search for; it needs a choice where "too small" and "too big" can be told apart. Here the choice is where to cut, and the crossing pairs tell you which way to move.
Always search the shorter array; otherwise j = half − i can go negative or past the end. Use MIN_VALUE / MAX_VALUE for an empty side of a cut (so [] and [1] works). And divide by 2.0, not 2, or (2 + 3) / 2 becomes 2.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 410Split Array Largest SumHardBinary Search on AnswerLeetCode
Cut an array into exactly k non-empty pieces of neighbouring numbers. Each piece has a sum; the worst piece is the one with the biggest sum. Choose the cuts so that this biggest piece sum is as small as possible, and return it.
Example: nums = [7, 2, 5, 10, 8], k = 2 → 18: [7, 2, 5] and [10, 8], sums 14 and 18.
"Minimize the largest" (or "maximize the smallest") is the strongest sign of binary search on the answer. Guess a limit: "can I cut the array into at most k pieces with every sum ≤ limit?" is easy to check greedily, and a bigger limit only makes it easier.
1Brute force — DP over every place to put the last cut
Trying every set of cut positions is exponential. The Intermediate way to tame that is DP: dp[p][i] = the best "largest sum" for splitting the first i numbers into p pieces. The last piece is nums[j..i−1] for some j.
int splitArray(int[] nums, int k) {
int n = nums.length;
long[] prefix = new long[n + 1];
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
long[][] dp = new long[k + 1][n + 1];
for (long[] row : dp) Arrays.fill(row, Long.MAX_VALUE);
dp[0][0] = 0;
for (int p = 1; p <= k; p++) {
for (int i = 1; i <= n; i++) {
for (int j = p - 1; j < i; j++) { // last piece is nums[j..i-1]
if (dp[p - 1][j] == Long.MAX_VALUE) continue;
long worst = Math.max(dp[p - 1][j], prefix[i] - prefix[j]);
dp[p][i] = Math.min(dp[p][i], worst);
}
}
}
return (int) dp[k][n];
}
Time O(k · n²)Space O(k · n)
With n = 1,000 and k = 50 that is 50 million steps: it passes, but only just, and it does not scale.
2Optimal — binary search the limit, check greedily
This is exactly LC 1011 Ship Packages from Intermediate, with "days" renamed to "pieces". The only extra insight is to see it: the answer lies between max(nums) (a piece must hold the biggest number) and sum(nums) (one piece). For a guessed limit, fill each piece greedily until the next number would break the limit, then start a new piece. If that needs at most k pieces, the limit works; try smaller.
int splitArray(int[] nums, int k) {
long lo = 0, hi = 0;
for (int x : nums) { lo = Math.max(lo, x); hi += x; } // answer is in [max, sum]
long ans = hi;
while (lo <= hi) {
long mid = lo + (hi - lo) / 2; // guessed limit
if (partsNeeded(nums, mid) <= k) {
ans = mid; // works: remember it...
hi = mid - 1; // ...and try a smaller limit
} else {
lo = mid + 1; // too many pieces: allow bigger sums
}
}
return (int) ans;
}
int partsNeeded(int[] nums, long limit) {
int parts = 1;
long sum = 0;
for (int x : nums) {
if (sum + x > limit) { parts++; sum = 0; } // x starts a new piece
sum += x;
}
return parts;
}
Time O(n · log(sum))Space O(1)
About 30 guesses (the sum is at most 109), each one a single pass.
"Split into k groups and minimize the maximum" = binary search on the maximum + a greedy check. Books to students, ships, painters, and this problem are all the same code.
Start lo at max(nums), not 0 or 1: with a limit smaller than one number, the greedy check puts that number in a piece on its own that is still over the limit, and it wrongly says yes ([1, 10] with k = 2 would return 1 instead of 10). Also keep sums in long if values can be large.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 1482Minimum Number of Days to Make m BouquetsMediumBinary Search on AnswerLeetCode
Flowers grow in a row; flower i blooms on day bloomDay[i]. A bouquet needs k neighbouring bloomed flowers, and each flower can be used once. What is the first day you can make m bouquets? Return -1 if it is never possible.
Example: bloomDay = [7, 7, 7, 7, 12, 7, 7], m = 2, k = 3 → 12. On day 7 the 12 has not bloomed and splits the row into runs of 4 and 2: only one bouquet.
"Minimum number of days so that you can ...". Waiting longer never hurts: every flower bloomed on day d is still bloomed on day d + 1. So the answers go no, no, yes, yes over the days. And checking one day is a single pass. That is binary search on the answer.
1Brute force — try each bloom day in order
The answer is always one of the bloom days (nothing changes between them). Sort them and check each one from the earliest.
int minDays(int[] bloomDay, int m, int k) {
if ((long) m * k > bloomDay.length) return -1; // not enough flowers at all
int[] days = bloomDay.clone();
Arrays.sort(days);
for (int day : days) {
if (bouquets(bloomDay, day, k) >= m) return day;
}
return -1;
}
int bouquets(int[] bloomDay, int day, int k) {
int made = 0, run = 0; // run = bloomed neighbours so far
for (int d : bloomDay) {
if (d <= day) {
run++;
if (run == k) { made++; run = 0; } // use these k for one bouquet
} else {
run = 0; // a closed flower breaks the run
}
}
return made;
}
Time O(n²)Space O(n)
Up to 100,000 candidate days, each a pass over 100,000 flowers: 10 billion steps.
2Optimal — binary search the day
This is Koko (LC 875) from Intermediate with a different check. Search days in [min bloom day, max bloom day]. The extra insight is the check: walk the row counting a run of bloomed neighbours; every time the run reaches k, make a bouquet and restart the run. A flower that has not bloomed resets the run to 0.
int minDays(int[] bloomDay, int m, int k) {
if ((long) m * k > bloomDay.length) return -1; // long: m * k can reach 10^11
int lo = Integer.MAX_VALUE, hi = 0;
for (int d : bloomDay) { lo = Math.min(lo, d); hi = Math.max(hi, d); }
int ans = hi;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // guessed day
if (bouquets(bloomDay, mid, k) >= m) {
ans = mid; // enough: try an earlier day
hi = mid - 1;
} else {
lo = mid + 1; // not enough: wait longer
}
}
return ans;
}
int bouquets(int[] bloomDay, int day, int k) {
int made = 0, run = 0;
for (int d : bloomDay) {
if (d <= day) {
run++;
if (run == k) { made++; run = 0; }
} else {
run = 0;
}
}
return made;
}
Time O(n · log(max day))Space O(1)
Days go up to 109, so about 30 guesses, each a single pass.
Check "is it possible at all?" before binary searching. If even the last day fails (m · k > n), return -1 right away; then the search is guaranteed to find a day.
m can be 106 and k can be 105: m * k in int overflows and may wrongly look small. Cast to long. Also reset run to 0 after making a bouquet, since a flower can't be used twice.
Pattern notes: Modified Binary Search · Open on LeetCode
LC 719Find K-th Smallest Pair DistanceHardBinary Search + Two PointersLeetCode
The distance of a pair is |nums[i] − nums[j]|. Look at all pairs i < j, sort their distances, and return the k-th smallest one.
Example: nums = [1, 3, 1], k = 1 → 0 (the pair of 1s). nums = [1, 3, 4, 8], k = 3 → 3: the distances sorted are 1, 2, 3, 4, 5, 7.
"K-th smallest" among a huge set you can't afford to list (n² pairs). Flip it into a counting question: "how many pairs have distance ≤ d?" That count grows as d grows, so the k-th smallest is the first d whose count reaches k: binary search on the answer.
1Brute force — list every distance and sort
Compute all n(n−1)/2 distances, sort them, and pick position k − 1.
int smallestDistancePair(int[] nums, int k) {
int n = nums.length;
int[] dist = new int[n * (n - 1) / 2];
int idx = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
dist[idx++] = Math.abs(nums[i] - nums[j]);
Arrays.sort(dist);
return dist[k - 1];
}
Time O(n² log n)Space O(n²)
With 10,000 numbers that is 50 million distances (about 200 MB of ints) plus a big sort: too slow and too much memory.
2Optimal — binary search the distance, count with two pointers
Binary search on the answer from Intermediate, plus a sliding-window count as the check. Sort nums. The answer is between 0 and max − min. To count pairs with distance ≤ d, move right across the array and push left forward while nums[right] − nums[left] > d; then every index between left and right pairs with right, adding right − left pairs. Find the smallest d with count ≥ k.
left only moves forward, so one count is O(n).int smallestDistancePair(int[] nums, int k) {
Arrays.sort(nums);
int lo = 0, hi = nums[nums.length - 1] - nums[0];
int ans = hi;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // guessed distance
if (countPairs(nums, mid) >= k) {
ans = mid; // at least k pairs fit: try smaller
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return ans;
}
int countPairs(int[] nums, int d) { // pairs with distance <= d
int count = 0, left = 0;
for (int right = 0; right < nums.length; right++) {
while (nums[right] - nums[left] > d) left++;
count += right - left; // nums[left..right-1] all pair with right
}
return count;
}
Time O(n log n + n · log W)Space O(1)
W is the biggest distance (up to 106), so about 20 counting passes after the sort.
"K-th smallest of something too big to list" → binary search on the value, with a count(≤ x) check. The same trick solves LC 378 (k-th smallest in a sorted matrix) and LC 668 (multiplication table).
Search for the first d with count ≥ k, not count == k: with repeated distances the count can jump from below k to above k and never equal it (in [1, 1, 1] with k = 2, count(0) is already 3 and no d gives exactly 2). Also sort first: the two-pointer count only works on a sorted array.
Pattern notes: Modified Binary Search · Open on LeetCode
Heaps: Two Heaps & K-way Merge
Two patterns built on the heap from Intermediate. Two Heaps: split the items into a lower half (max-heap) and an upper half (min-heap), so the middle is always at the two tops; a close cousin pairs a sorted list with a heap, so choices unlock in order and the best one is on top. K-way Merge: keep one "front" item per sorted list in a min-heap, pop the smallest, push the next from the same list. What is harder than Intermediate: keeping two heaps balanced, deleting items that leave a window (TreeSet or lazy deletion), and tracking a second value (the max) beside the heap.
LC 295Find Median from Data StreamHardTwo HeapsLeetCode
Numbers arrive one at a time. Build a class with addNum(num) to take in a number and findMedian() to return the median of everything seen so far: the middle value once sorted, or the average of the two middle values when the count is even.
Example: addNum(1), addNum(2), findMedian() → 1.5, then addNum(3), findMedian() → 2.0.
The words are "median" and "data stream": numbers keep coming and you are asked about the middle again and again. A single heap gives you only the min or the max. The middle is where the top of the lower half meets the top of the upper half, so you use two heaps.
1Brute force — keep a sorted list
Keep every number in a list that is always sorted. To add, walk to the right spot and insert there. The median is then just the middle element (or the two middle ones).
class MedianFinder {
private final List<Integer> nums = new ArrayList<>(); // always kept sorted
public void addNum(int num) {
int i = 0;
while (i < nums.size() && nums.get(i) < num) i++; // find its place
nums.add(i, num); // shifts everything after it
}
public double findMedian() {
int n = nums.size();
if (n % 2 == 1) return nums.get(n / 2);
return (nums.get(n / 2 - 1) + (double) nums.get(n / 2)) / 2;
}
}
Time O(n) per addSpace O(n)Time O(1) per median
Each insert shifts up to n items. With up to 5·104 calls that is around 109 steps in the worst case: too slow, and the interviewer wants each add to be O(log n).
2Optimal — two heaps that meet in the middle
This is the heap from LC 703 / LC 347, plus one extra insight: split the numbers into a lower half and an upper half, each in its own heap. The lower half is a max-heap (its top is the biggest small number), the upper half is a min-heap (its top is the smallest big number). Those two tops sit right next to each other in sorted order, so the median is one of them or their average.
Keep two rules after every add: every number in small is ≤ every number in large, and small has the same size as large or one more. Three moves guarantee both: push into small, move small's top to large, and if large got bigger, move its top back.
class MedianFinder {
// small: max-heap holding the smaller half, its top is the biggest of the small half
private final PriorityQueue<Integer> small = new PriorityQueue<>(Collections.reverseOrder());
// large: min-heap holding the larger half, its top is the smallest of the large half
private final PriorityQueue<Integer> large = new PriorityQueue<>();
public void addNum(int num) {
small.offer(num); // 1. always go into small first
large.offer(small.poll()); // 2. its biggest moves to large (keeps order: small <= large)
if (large.size() > small.size()) {
small.offer(large.poll()); // 3. rebalance: small may have one extra, never large
}
}
public double findMedian() {
if (small.size() > large.size()) return small.peek(); // odd count
return (small.peek() + (double) large.peek()) / 2; // even count
}
}
Time O(log n) per addSpace O(n)Time O(1) per median
Each add does at most three heap operations, and the median just peeks at the tops. Passing every new number through small first is what keeps the order rule true without any if-else on values.
Whenever you need "the middle" of a changing set, think two heaps facing each other: a max-heap for the lower half and a min-heap for the upper half. Their tops are the middle.
The average of two ints can overflow: (a + b) / 2 breaks near 231 and also does integer division. Cast to double before adding, as in (small.peek() + (double) large.peek()) / 2. And decide once which heap may hold the extra element; mixing that up gives the wrong median for odd counts.
Pattern notes: Two Heaps · Open on LeetCode
LC 480Sliding Window MedianHardTwo HeapsLeetCode
A window of size k slides over the array one step at a time. Return the median of every window.
Example: nums = [1,3,-1,-3,5,3,6,7], k = 3 → [1,-1,-1,3,5,6]. The first window is [1,3,-1], sorted [-1,1,3], median 1.
"Median" points to two heaps (LC 295). "Window of size k that moves right" points to a sliding window. When both appear, you need two halves that also support removing the item that leaves the window.
1Brute force — sort every window
For each window, copy its k numbers, sort them and read the middle.
double[] medianSlidingWindow(int[] nums, int k) {
double[] ans = new double[nums.length - k + 1];
for (int i = 0; i + k <= nums.length; i++) {
int[] w = Arrays.copyOfRange(nums, i, i + k); // copy the window
Arrays.sort(w); // sort it
ans[i] = (k % 2 == 1) ? w[k / 2] : ((double) w[k / 2 - 1] + w[k / 2]) / 2;
}
return ans;
}
Time O(n · k log k)Space O(k)
With n = 105 and k = 5·104 there are 5·104 windows, each sorted from scratch: about 1010 steps. It times out. Neighbouring windows share k − 1 numbers, and we throw that work away every time.
2Optimal — two halves you can delete from
This is LC 295 two heaps plus a sliding window, with one extra insight: you must delete the leaving number, and a PriorityQueue cannot delete from the middle quickly (remove(x) is O(k)). So use a TreeSet as a "heap you can delete from": last() and first() are the tops, and remove(x) is O(log k).
A TreeSet drops duplicates, so store indexes, ordered by value and then by index. Now two equal values are still two different items, and the leaving item is simply index i − k.
double[] medianSlidingWindow(int[] nums, int k) {
// store INDEXES, ordered by value then index, so equal values are still different items
Comparator<Integer> cmp = (a, b) -> nums[a] != nums[b]
? Integer.compare(nums[a], nums[b]) : Integer.compare(a, b);
TreeSet<Integer> small = new TreeSet<>(cmp); // lower half, last() = its biggest
TreeSet<Integer> large = new TreeSet<>(cmp); // upper half, first() = its smallest
double[] ans = new double[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
// add the new item: same 3 moves as LC 295
small.add(i);
large.add(small.pollLast());
if (large.size() > small.size()) small.add(large.pollFirst());
if (i >= k) { // remove the item that just left the window
int out = i - k;
if (!small.remove(out)) large.remove(out); // O(log k), no search needed
// fix the sizes again: small must have the same count or one more
if (small.size() < large.size()) small.add(large.pollFirst());
else if (small.size() > large.size() + 1) large.add(small.pollLast());
}
if (i >= k - 1) {
ans[i - k + 1] = (k % 2 == 1) ? nums[small.last()]
: ((double) nums[small.last()] + nums[large.first()]) / 2;
}
}
return ans;
}
Time O(n log k)Space O(k)
Each step does a constant number of TreeSet operations on sets of size about k/2. The alternative is two PriorityQueues with lazy deletion: keep a map of "numbers to delete later" and only throw them away when they reach a top. It has the same complexity, but the size bookkeeping is much easier to get wrong.
Heap + "remove an arbitrary item" → use a TreeSet / TreeMap (sorted set with O(log n) remove), or a heap with lazy deletion. Store indexes so duplicates are safe.
Three classic bugs. (1) Putting values in a TreeSet: duplicates like [2,2,2] silently disappear. (2) A comparator written as nums[a] - nums[b] overflows for values like −231 and 231−1; use Integer.compare. (3) Integer overflow in the average: cast to double before adding.
Pattern notes: Two Heaps · Open on LeetCode
LC 23Merge k Sorted ListsHardK-way MergeLeetCode
You get k linked lists, each sorted from small to large. Merge them all into one sorted linked list.
Example: [[1,4,5],[1,3,4],[2,6]] → [1,1,2,3,4,4,5,6].
"k sorted lists (or arrays, or rows)" and "produce one sorted result" is the K-way merge signal. At every moment the next output is the smallest of the k front items, and "smallest of k things, over and over" is exactly what a min-heap does.
1Brute force — merge the lists one at a time
Use LC 21 Merge Two Sorted Lists from Beginner: merge list 1 into the result, then list 2, then list 3, and so on.
ListNode mergeKLists(ListNode[] lists) {
ListNode result = null;
for (ListNode list : lists) {
result = mergeTwo(result, list); // fold each list into the result
}
return result;
}
ListNode mergeTwo(ListNode a, ListNode b) { // LC 21 from Beginner
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;
}
Time O(k · N)Space O(1)
N is the total number of nodes. The result keeps growing, and every merge walks the whole result again, so early nodes are copied about k times. With k = 104 tiny lists that is around 5·107 steps: it passes, but it is slow and not what the interviewer wants.
2Optimal — a min-heap of the k fronts
This is LC 21 merge two lists plus one extra insight: with k lists, let a min-heap pick the smallest front. It is the same move as LC 378 (Kth smallest in a sorted matrix) from Intermediate. Put the head of every list in the heap. Pop the smallest node, attach it to the answer, and push that node's next. The heap always holds at most one node per list.
ListNode mergeKLists(ListNode[] lists) {
// min-heap of the current front node of each list
PriorityQueue<ListNode> heap = new PriorityQueue<>((x, y) -> Integer.compare(x.val, y.val));
for (ListNode head : lists) {
if (head != null) heap.offer(head); // skip empty lists
}
ListNode dummy = new ListNode(0), tail = dummy;
while (!heap.isEmpty()) {
ListNode smallest = heap.poll(); // smallest front of all k lists
tail.next = smallest;
tail = smallest;
if (smallest.next != null) heap.offer(smallest.next); // its list's next node takes its place
}
return dummy.next;
}
Time O(N log k)Space O(k)
Every node is pushed and popped once, and the heap has at most k nodes, so each operation costs log k instead of the brute force's repeated walks.
3Also good — divide and conquer, no heap
Merge the lists in pairs (1+2, 3+4, …), then merge the results in pairs again, like merge sort. There are log k rounds, and each round touches every node once.
ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) return null;
for (int step = 1; step < lists.length; step *= 2) { // pair up: 1+2, 3+4, ... then again
for (int i = 0; i + step < lists.length; i += 2 * step) {
lists[i] = mergeTwo(lists[i], lists[i + step]);
}
}
return lists[0];
}
Time O(N log k)Space O(1) extra
Same speed as the heap, uses mergeTwo from step 1, and needs no extra memory. Worth mentioning if the interviewer asks for a version without a heap.
K-way merge: heap of one "front" per source. Pop the smallest, then push the next item from the same source. It works for lists, arrays (store {value, listIndex, position}) and matrix rows.
Empty lists: lists can be [] or contain null heads. Skip null before heap.offer, or the comparator throws a NullPointerException. And use Integer.compare(x.val, y.val) rather than x.val - y.val.
Pattern notes: Top K & K-way Merge · Open on LeetCode
LC 632Smallest Range Covering Elements from K ListsHardK-way MergeLeetCode
You get k sorted lists. Find the smallest range [a, b] that contains at least one number from each list. If two ranges have the same width, pick the one that starts lower.
Example: [[4,10,15,24,26],[0,9,12,20],[5,18,22,30]] → [20,24]: 24 from list 1, 20 from list 2, 22 from list 3.
"k sorted lists" is the K-way merge signal from LC 23. "One from each list, as close together as possible" means you look at one value per list at a time: the range is from the smallest to the largest of those k values.
1Brute force — try every left end
Try each number a as the left end of the range. For every list, binary search the first value ≥ a; the right end b must reach the largest of those. Keep the narrowest.
int[] smallestRange(List<List<Integer>> nums) {
List<Integer> starts = new ArrayList<>();
for (List<Integer> list : nums) starts.addAll(list);
Collections.sort(starts); // try every value as the left end, small to big
int[] best = null;
for (int a : starts) {
int b = a;
for (List<Integer> list : nums) {
int j = firstAtLeast(list, a); // this list's closest value that is >= a
if (j == list.size()) return best; // some list has nothing >= a: bigger a won't help
b = Math.max(b, list.get(j)); // the range must stretch to reach it
}
if (best == null || b - a < best[1] - best[0]) best = new int[]{a, b};
}
return best;
}
int firstAtLeast(List<Integer> list, int x) { // binary search (lower bound)
int lo = 0, hi = list.size();
while (lo < hi) {
int mid = (lo + hi) / 2;
if (list.get(mid) < x) lo = mid + 1; else hi = mid;
}
return lo;
}
Time O(N · k log m)Space O(N)
N is the total count, m the list length. With k = 3500 lists of 50 numbers, N = 175,000, so this is about 175,000 × 3500 × 6 ≈ 4·109 steps. It times out.
2Optimal — heap of k pointers + the current max
This is LC 23 K-way merge plus one extra insight: also track the largest value in the heap, so [heap top, max] is a valid range at every step. Start with the first value of each list. The only way to shrink the range is to raise its minimum, so pop the minimum and move that list's pointer forward (raising the max if needed). When a list runs out, you can no longer take one from each list, so stop.
int[] smallestRange(List<List<Integer>> nums) {
// heap entry: {value, which list, index inside that list}; smallest value on top
PriorityQueue<int[]> heap = new PriorityQueue<>((x, y) -> Integer.compare(x[0], y[0]));
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.size(); i++) {
int v = nums.get(i).get(0);
heap.offer(new int[]{v, i, 0}); // one pointer per list, at its first value
max = Math.max(max, v);
}
int[] best = {heap.peek()[0], max};
while (true) {
int[] top = heap.poll(); // current range is [top value, max]
if (max - top[0] < best[1] - best[0]) best = new int[]{top[0], max};
int list = top[1], next = top[2] + 1;
if (next == nums.get(list).size()) break; // this list is used up: can't cover it any more
int v = nums.get(list).get(next);
heap.offer(new int[]{v, list, next}); // move ONLY the smallest pointer forward
max = Math.max(max, v);
}
return best;
}
Time O(N log k)Space O(k)
Each number enters and leaves the heap at most once. Using a strict < when comparing widths keeps the earlier range on a tie, and since the minimum only goes up, the earlier one is the one that starts lower, just as the problem asks.
K-way merge can do more than merge. Here the heap is a "one pointer per list" window, and you always advance the smallest pointer. The max never needs a heap: it only goes up, so one variable is enough.
Stop the moment any list is used up. Don't keep popping the other lists: without a value from every list the range is not valid. Also compare widths as b − a with a strict <, or you break the "smaller start wins" tie rule.
Pattern notes: Top K & K-way Merge · Open on LeetCode
LC 502IPOHardTwo HeapsLeetCode
You start with capital w and may finish at most k projects. Project i needs capital capital[i] to start (you don't spend it) and adds profits[i] to your capital when done. Pick the projects to get the largest final capital.
Example: k = 2, w = 0, profits = [1,2,3], capital = [0,1,1] → 4. Do project 0 (w becomes 1), then project 2 (w becomes 4).
"At most k picks", each pick "unlocks" more options (your capital only grows), and you want the best total. The greedy choice is clear, "take the most profitable project you can afford right now", so the question is only how to find it quickly: one list sorted by cost, and one heap by profit.
1Brute force — scan all projects every round
In each of the k rounds, scan all projects, find the most profitable one you can afford that isn't done, and do it.
int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
boolean[] done = new boolean[profits.length];
for (int round = 0; round < k; round++) {
int pick = -1;
for (int i = 0; i < profits.length; i++) { // scan everything, every round
if (!done[i] && capital[i] <= w && (pick == -1 || profits[i] > profits[pick])) pick = i;
}
if (pick == -1) break; // nothing affordable
done[pick] = true;
w += profits[pick];
}
return w;
}
Time O(k · n)Space O(n)
With k = n = 105 this is 1010 steps: it times out. Each round re-checks projects that were already affordable last round.
2Optimal — sort by cost, max-heap by profit
This is the greedy idea from Intermediate plus one extra insight: affordability only grows, so a pointer over projects sorted by capital unlocks each one exactly once. Unlocked projects go into a max-heap of profits. Each round: move the pointer to unlock everything with capital ≤ w, then pop the heap top. Projects you skip stay in the heap for later rounds.
int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
int n = profits.length;
Integer[] order = new Integer[n];
for (int i = 0; i < n; i++) order[i] = i;
Arrays.sort(order, (a, b) -> Integer.compare(capital[a], capital[b])); // cheapest first
PriorityQueue<Integer> affordable = new PriorityQueue<>(Collections.reverseOrder()); // max-heap of profits
int next = 0; // first project not yet unlocked
for (int round = 0; round < k; round++) {
while (next < n && capital[order[next]] <= w) {
affordable.offer(profits[order[next]]); // unlock everything we can now afford
next++;
}
if (affordable.isEmpty()) break; // can't afford anything, and never will
w += affordable.poll(); // do the most profitable one
}
return w;
}
Time O(n log n + k log n)Space O(n)
Sorting costs n log n, and each project is pushed and popped at most once. If the heap is empty, nothing is affordable now and nothing ever will be, because w did not change, so stop early.
When choices unlock over time: sort by the unlock condition and walk a pointer (that part never goes backwards), then keep the unlocked choices in a heap ordered by value. It is a sorted list feeding a heap.
Don't rebuild the heap every round and don't sort by profit. The pointer over the capital order must only move forward, and projects already in the heap stay there. Also remember the loop can end before k rounds when the heap is empty.
Pattern notes: Two Heaps · Open on LeetCode
Intervals & Sweep Line
The sweep line: turn intervals into events on a number line, sort them, walk left to right, and keep the "active" intervals in a structure (TreeMap, heap) that answers your question at each point. In Intermediate the sweep only compared each interval with the last one kept. Here that is not enough: many intervals are active at once, so you need a sorted structure for them, careful tie-breaking at equal positions, and sometimes sorting the queries as well.
LC 218The Skyline ProblemHardSweep LineLeetCode
Each building is [left, right, height], a rectangle standing on the ground. Return the skyline, the outline you see from far away, as a list of key points [x, height]: the x where the outline height changes, and the new height there.
Example: [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]] → [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]].
Many intervals (buildings) overlap, and the question is about "the tallest one at each x". Something that only changes at interval starts and ends is a sweep line: walk the x-axis event by event and keep the set of "open" intervals. "Tallest open one" means that set must give the max quickly.
1Brute force — check the height at every edge
The outline can only change at a building edge. For each edge x, find the tallest building that covers x (left ≤ x < right). If it differs from the previous height, it is a key point.
List<List<Integer>> getSkyline(int[][] buildings) {
TreeSet<Integer> xs = new TreeSet<>();
for (int[] b : buildings) { xs.add(b[0]); xs.add(b[1]); } // the only places the height can change
List<List<Integer>> result = new ArrayList<>();
int prev = 0;
for (int x : xs) {
int h = 0;
for (int[] b : buildings) { // tallest building covering x (right edge not included)
if (b[0] <= x && x < b[1]) h = Math.max(h, b[2]);
}
if (h != prev) { result.add(List.of(x, h)); prev = h; }
}
return result;
}
Time O(n²)Space O(n)
With n = 104 buildings: 2·104 edges × 104 buildings = 2·108 checks. Borderline in Java, and hopeless if n grew to 105. Most buildings are far away from any given x, yet we test them all.
2Optimal — sweep events, keep the open heights
This is the interval sweep from Intermediate (LC 56) plus one extra insight: keep the heights of the "open" buildings in a sorted multiset, and add a point only when its max changes. Turn each building into two events: a start at left and an end at right. Sort them and walk left to right. At a start add the height, at an end remove one copy of it. A TreeMap of height → count does both in O(log n), and lastKey() is the current tallest.
Ties at the same x matter. Encoding a start as -height and an end as +height and sorting by (x, value) puts starts before ends, taller starts first and lower ends first, so no fake point appears in between.
List<List<Integer>> getSkyline(int[][] buildings) {
List<int[]> events = new ArrayList<>();
for (int[] b : buildings) {
events.add(new int[]{b[0], -b[2]}); // start: negative height
events.add(new int[]{b[1], b[2]}); // end: positive height
}
// by x; at the same x: starts before ends, taller starts first, lower ends first
events.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
TreeMap<Integer, Integer> heights = new TreeMap<>(); // height -> how many buildings are "open"
heights.put(0, 1); // the ground, never removed
List<List<Integer>> result = new ArrayList<>();
int prev = 0;
for (int[] e : events) {
if (e[1] < 0) {
heights.merge(-e[1], 1, Integer::sum); // building starts
} else {
int h = e[1]; // building ends: remove ONE copy
if (heights.get(h) == 1) heights.remove(h); else heights.put(h, heights.get(h) - 1);
}
int cur = heights.lastKey(); // tallest open building
if (cur != prev) { // the outline changes only here
result.add(List.of(e[0], cur));
prev = cur;
}
}
return result;
}
Time O(n log n)Space O(n)
Sorting 2n events is n log n, and each event is one TreeMap update. A max-heap with lazy removal also works; the TreeMap is simpler because removing one exact height is direct.
Sweep line: turn intervals into start/end events, sort, and keep the active set in a structure that answers your question (here: max). Output only when the answer changes.
Ties at the same x. Two buildings touching end to start (like [0,2,3] and [2,5,3]) must give [[0,3],[5,0]], not an extra [2,0] or [2,3]. The -height trick handles this. Compare with Integer.compare, not a[1] - b[1]: heights go up to 231 − 1, so the subtraction can overflow. And add the ground height 0 so the map is never empty.
Pattern notes: Merge Intervals · Open on LeetCode
LC 352Data Stream as Disjoint IntervalsHardIntervals + TreeMapLeetCode
Numbers arrive one by one (addNum). At any time, getIntervals() must return everything seen so far as sorted, non-overlapping ranges of consecutive numbers.
Example: add 1, 3, 7, 2, 6. After adding 1, 3, 7 the answer is [[1,1],[3,3],[7,7]]. Adding 2 glues them into [[1,3],[7,7]], and adding 6 gives [[1,3],[6,7]].
"Stream" plus "disjoint intervals" is Insert Interval (LC 57) done over and over. Inserting into an array would cost O(n) each time. You only need the two neighbours of the new number: the interval just before it and the one just after. "Nearest key below / above" is what a TreeMap gives with floorKey and ceilingKey.
1Brute force — store numbers, build intervals on demand
Keep all numbers in a TreeSet (sorted, no duplicates). When asked, walk it in order and group consecutive numbers into runs.
class SummaryRanges {
private final TreeSet<Integer> seen = new TreeSet<>(); // every number, sorted, no duplicates
public void addNum(int value) { seen.add(value); }
public int[][] getIntervals() {
List<int[]> out = new ArrayList<>();
for (int x : seen) {
if (!out.isEmpty() && out.get(out.size() - 1)[1] == x - 1) {
out.get(out.size() - 1)[1] = x; // x continues the last run
} else {
out.add(new int[]{x, x}); // gap: start a new run
}
}
return out.toArray(new int[0][]);
}
}
Time O(n) per getIntervalsSpace O(n)Time O(log n) per add
Adding is cheap, but every getIntervals walks all distinct numbers (up to 104), even when they form only a few intervals. With 3·104 calls that is up to 3·108 steps, and the follow-up asks exactly for "many numbers, few intervals".
2Optimal — a TreeMap of start → end
This is LC 57 Insert Interval plus one extra insight: keep the intervals in a TreeMap and look up only the two neighbours of the new number. floorKey(value) is the interval starting at or before it; ceilingKey(value + 1) is the one starting after it. Then there are just four cases: already inside, glue both sides, extend the left one, or extend the right one; otherwise it starts a new interval.
class SummaryRanges {
private final TreeMap<Integer, Integer> map = new TreeMap<>(); // start -> end
public void addNum(int value) {
Integer lo = map.floorKey(value); // interval starting at or before value
Integer hi = map.ceilingKey(value + 1); // interval starting right after value
if (lo != null && map.get(lo) >= value) return; // already covered
boolean joinLeft = lo != null && map.get(lo) == value - 1;
boolean joinRight = hi != null && hi == value + 1;
if (joinLeft && joinRight) { // value fills the gap: glue two intervals
map.put(lo, map.get(hi));
map.remove(hi);
} else if (joinLeft) { // extend left neighbour by one
map.put(lo, value);
} else if (joinRight) { // right neighbour now starts at value
map.put(value, map.get(hi));
map.remove(hi);
} else {
map.put(value, value); // brand new single-point interval
}
}
public int[][] getIntervals() {
int[][] out = new int[map.size()][];
int i = 0;
for (Map.Entry<Integer, Integer> e : map.entrySet()) out[i++] = new int[]{e.getKey(), e.getValue()};
return out;
}
}
Time O(log m) per addSpace O(m)Time O(m) per getIntervals
m is the number of intervals, which is at most the number of distinct values and often much smaller. Output is already sorted because the TreeMap keeps keys in order.
Intervals that change one number at a time → TreeMap start → end. floorKey / ceilingKey find the neighbours in O(log n), and each insert touches at most two intervals.
Check "already covered" first: if lo's end is ≥ value, do nothing (a repeated number, or one inside an interval). And compare boxed Integer keys carefully: hi == value + 1 is fine because one side is an int, but comparing two Integer objects with == fails for values above 127.
Pattern notes: Merge Intervals · Open on LeetCode
LC 1851Minimum Interval to Include Each QueryHardSweep LineLeetCode
You get intervals [left, right] (size = right − left + 1) and a list of query numbers. For each query q, return the size of the smallest interval that contains q, or -1 if none does.
Example: intervals = [[1,4],[2,4],[3,6],[4,4]], queries = [2,3,4,5] → [3,3,1,4]. For q = 4, [4,4] has size 1.
Many queries against many intervals, and the answers don't depend on each other, so you can answer the queries in any order you like (offline). Sorting the queries turns it into a sweep: as q grows, intervals start being useful (left ≤ q) and later stop (right < q). "Smallest among the active ones" means a min-heap.
1Brute force — check every interval for every query
For each query, loop over all intervals, keep those that contain it, and take the smallest size.
int[] minInterval(int[][] intervals, int[] queries) {
int[] ans = new int[queries.length];
for (int j = 0; j < queries.length; j++) {
int best = -1;
for (int[] iv : intervals) { // check every interval for every query
if (iv[0] <= queries[j] && queries[j] <= iv[1]) {
int size = iv[1] - iv[0] + 1;
if (best == -1 || size < best) best = size;
}
}
ans[j] = best;
}
return ans;
}
Time O(n · q)Space O(1) extra
With 105 intervals and 105 queries that is 1010 checks. It times out.
2Optimal — sort queries, sweep with a min-heap
This is the sweep line from LC 218 plus one extra insight: sort the queries too (remember their original positions), so each interval is added once and removed once. Sort intervals by left end. For each query, small to big: (1) push every interval whose left ≤ q into a min-heap ordered by size; (2) pop the top while it ends before q (it can never help a bigger query either); (3) the top's size is the answer.
Step 2 is lazy removal: dead intervals deeper in the heap are left alone until they reach the top. That is fine, because only the top is ever read.
int[] minInterval(int[][] intervals, int[] queries) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); // by left end
Integer[] order = new Integer[queries.length];
for (int j = 0; j < queries.length; j++) order[j] = j;
Arrays.sort(order, (a, b) -> Integer.compare(queries[a], queries[b])); // queries small to big, keep positions
// heap entry: {size, right end}; smallest size on top
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
int[] ans = new int[queries.length];
int i = 0;
for (int j : order) {
int q = queries[j];
while (i < intervals.length && intervals[i][0] <= q) { // 1. add intervals that have started
heap.offer(new int[]{intervals[i][1] - intervals[i][0] + 1, intervals[i][1]});
i++;
}
while (!heap.isEmpty() && heap.peek()[1] < q) heap.poll(); // 2. drop ended ones (lazily)
ans[j] = heap.isEmpty() ? -1 : heap.peek()[0]; // 3. smallest one still covering q
}
return ans;
}
Time O(n log n + q log q)Space O(n + q)
Two sorts, and each interval is pushed and popped at most once. Sorting an array of query indexes lets you write each answer back to its original position.
Offline queries: when queries are independent, sort them and sweep. Add things when they become valid, and lazily drop them from a heap top once they expire.
Don't sort the queries array itself: the answer must be in the original order. Sort indexes (Integer[] order) instead. And remember the size is right − left + 1, not right − left.
Pattern notes: Merge Intervals · Open on LeetCode
Linked List
Hard linked list problems use the same moves you already know (a dummy node, in-place reversal, fast & slow pointers, merging two sorted lists), but several of them in one solution. Template: break the list into pieces, fix each piece with a move you know, and glue the pieces back without losing a pointer. What is harder than Intermediate: you repeat the move many times (every group, every level of a sort) and the bookkeeping of "who points to whom" at the joins is the whole difficulty.
LC 25Reverse Nodes in k-GroupHardIn-place ReversalLeetCode
Cut the list into groups of k nodes from the front and reverse each group. If the last group has fewer than k nodes, leave it as it is. You must move the nodes themselves, not just swap their values.
Example: 1→2→3→4→5, k = 2 → 2→1→4→3→5. With k = 3 → 3→2→1→4→5.
"Reverse" plus "part of a list" is the in-place reversal from LC 206 and LC 92. "Groups of k" and "leftover stays as is" tell you it is the same reversal done in a loop, with a check before each round that k nodes still exist. LC 24 (Swap Nodes in Pairs) is exactly this problem with k = 2.
1Brute force — copy the values into an array
Read all values into a list, reverse each full block of k inside the list, then write the values back into the nodes in order.
ListNode reverseKGroup(ListNode head, int k) {
List<Integer> vals = new ArrayList<>();
for (ListNode p = head; p != null; p = p.next) vals.add(p.val); // copy out
for (int start = 0; start + k <= vals.size(); start += k) { // only full groups
Collections.reverse(vals.subList(start, start + k));
}
ListNode p = head; // write back
for (int v : vals) { p.val = v; p = p.next; }
return head;
}
Time O(n)Space O(n)
It is fast, but it cheats twice: it uses O(n) extra memory, and it changes node values, which the problem forbids. The follow-up asks for O(1) extra memory with real node moves.
2Optimal — reverse one group at a time, in place
This is LC 92 (Reverse Linked List II) from Intermediate, repeated for every group. The one extra insight: keep a pointer groupPrev = the node just before the current group. Walk k steps from it to find kth; if you fall off the end, stop. Otherwise reverse the group, point groupPrev at the new first node (kth), and the old first node becomes the next groupPrev.
Start the reversal with prev = groupNext instead of null: the first node of the group then points straight at the rest of the list, so the right side is glued for free.
kth (no full group means stop), reverse the k nodes between groupPrev and groupNext, glue both ends, jump forward.ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0, head);
ListNode groupPrev = dummy; // node just before the current group
while (true) {
ListNode kth = groupPrev; // walk k steps: is there a full group?
for (int i = 0; i < k && kth != null; i++) kth = kth.next;
if (kth == null) break; // fewer than k nodes left: stop
ListNode groupNext = kth.next; // first node after the group
// reverse the group (LC 206), but point the first node at groupNext
ListNode prev = groupNext, cur = groupPrev.next;
while (cur != groupNext) {
ListNode nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
ListNode oldFirst = groupPrev.next; // it is now the group's last node
groupPrev.next = kth; // kth is the new first node
groupPrev = oldFirst; // move on to the next group
}
return dummy.next;
}
Time O(n)Space O(1)
Every node is visited twice (once while looking for kth, once while reversing), and only a few pointers are kept.
To reverse many pieces of a list, keep a dummy in front and a pointer to the node before each piece. Starting the reversal with prev = nodeAfterPiece reconnects the tail automatically.
Check that a full group exists before reversing. If you reverse first and count later, the short tail (5 in the k = 2 example) gets reversed too and you must undo it. Also save groupPrev.next (the old first node) before overwriting it: it is where the next group starts.
Pattern notes: In-place Linked List Reversal · Open on LeetCode
LC 148Sort ListMediumMerge Sort on a ListLeetCode
Sort a linked list in ascending order and return the new head. Aim for O(n log n) time. The follow-up asks for O(1) extra memory.
Example: 4→2→1→3 → 1→2→3→4. -1→5→3→4→0 → -1→0→3→4→5.
"Sort" plus "linked list" plus "O(n log n)" points to merge sort. Quick sort and heap sort need jumping to any index, which a list can't do. Merge sort only needs "split in half" and "merge two sorted lists", and a list is great at both: finding the middle is LC 876, merging is LC 21, and merging needs no extra array.
1Brute force — copy out, sort the array, write back
Put all values into an ArrayList, sort it with the library, then walk the list again and overwrite the values in order.
ListNode sortList(ListNode head) {
List<Integer> vals = new ArrayList<>();
for (ListNode p = head; p != null; p = p.next) vals.add(p.val); // copy out
Collections.sort(vals); // sort the array
ListNode p = head;
for (int v : vals) { p.val = v; p = p.next; } // write back
return head;
}
Time O(n log n)Space O(n)
The time is already fine; the problem is the O(n) copy and that it rewrites values instead of sorting the nodes. An interviewer will ask you to sort the list itself.
2Optimal — top-down merge sort on the nodes
Two Beginner problems glued with recursion: LC 876 (middle of the list) to split, LC 21 (merge two sorted lists) to join. The extra insight is cut the list at the middle by setting slow.next = null, so each half is a real, separate list you can sort on its own.
Start fast at head.next (not head) so that on a 2-node list slow stops on the first node and the split is 1 + 1, not 2 + 0.
ListNode sortList(ListNode head) {
if (head == null || head.next == null) return head; // 0 or 1 node: sorted
// 1. find the middle (LC 876); slow stops at the END of the left half
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode right = slow.next;
slow.next = null; // 2. cut into two lists
ListNode left = sortList(head); // 3. sort each half
right = sortList(right);
return merge(left, right); // 4. merge (LC 21)
}
ListNode merge(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; // glue the leftover
return dummy.next;
}
Time O(n log n)Space O(log n)
There are log n levels of halving, and each level does O(n) work in total (finding middles plus merging). The only extra memory is the recursion stack, log n calls deep.
3Follow-up — bottom-up merge sort, O(1) space
The recursion stack is the only thing that is not O(1). Remove it by working bottom-up: first merge neighbouring runs of size 1, then of size 2, 4, 8... until one run covers the list. cut detaches the next size nodes and returns what follows.
ListNode sortList(ListNode head) {
int n = 0;
for (ListNode p = head; p != null; p = p.next) n++;
ListNode dummy = new ListNode(0, head);
for (int size = 1; size < n; size *= 2) { // sorted runs of 1, 2, 4, ...
ListNode tail = dummy, cur = dummy.next;
while (cur != null) {
ListNode left = cur;
ListNode right = cut(left, size); // left = first 'size' nodes
cur = cut(right, size); // right = next 'size' nodes
tail.next = merge(left, right); // same merge as above
while (tail.next != null) tail = tail.next;
}
}
return dummy.next;
}
ListNode cut(ListNode node, int size) { // keep 'size' nodes, return the rest
for (int i = 1; node != null && i < size; i++) node = node.next;
if (node == null) return null;
ListNode rest = node.next;
node.next = null;
return rest;
}
Time O(n log n)Space O(1)
Same merges as step 2, just in a loop instead of a call stack. Worth knowing; top-down is usually enough in an interview.
Sorting a linked list = merge sort: middle with fast/slow, cut, sort both halves, merge. For O(1) space, merge runs of size 1, 2, 4, ... bottom-up.
If you start fast = head, a 2-node list splits into "2 nodes + nothing" and the recursion never gets smaller: a stack overflow. Start fast = head.next, and don't forget slow.next = null, or the left half still runs into the right half.
Pattern notes: Sorting · Open on LeetCode
Trees
Hard tree problems are still one DFS, but the recursive function has to return one thing to its parent while recording a different thing globally, or return a small state instead of a number. Template: decide what a node must tell its parent, compute it from the children's answers (post-order), and update the global answer on the way. What is harder than Intermediate: the value you return is not the answer itself (path sum, cameras), you may need to encode a whole tree as text, or sort nodes by (column, row, value) positions.
LC 124Binary Tree Maximum Path SumHardTree DFS (return vs record)LeetCode
A path is any chain of connected nodes, each used at most once. It does not have to go through the root and can go up then down (like an upside-down V). Node values can be negative. Return the largest possible sum of a path (at least one node).
Example: root = [-10, 9, 20, null, null, 15, 7] → 42, the path 15 → 20 → 7.
"Path between any two nodes, not necessarily through the root" is the shape of LC 543 (Diameter of Binary Tree) from Beginner: every path has one highest node where it bends. "Maximum sum" with negative values adds one twist: a subtree may be worth skipping.
1Brute force — try every node as the top of the path
Every path has a highest node (its "top"). For each node as the top, the best path is: its value, plus the best downward chain on the left (if positive), plus the best downward chain on the right (if positive). Compute those chains with a separate recursion every time.
int maxPathSum(TreeNode root) {
if (root == null) return Integer.MIN_VALUE;
// best path whose TOP (highest node) is root: root plus the good downward chains
int here = root.val + Math.max(0, down(root.left)) + Math.max(0, down(root.right));
// ...or the top is somewhere below: try every node as the top
return Math.max(here, Math.max(maxPathSum(root.left), maxPathSum(root.right)));
}
int down(TreeNode node) { // best chain that starts at node and goes down
if (node == null) return 0;
return node.val + Math.max(0, Math.max(down(node.left), down(node.right)));
}
Time O(n²)Space O(h)
For every node we re-walk its whole subtree to find chains. On a skewed tree with the maximum 3 × 10⁴ nodes that is about 4.5 × 10⁸ steps, which times out.
2Optimal — one post-order DFS that returns a chain and records a path
This is LC 543 (Diameter) with sums instead of lengths. The helper gain(node) returns the best chain going down from node (one side only, because a parent can extend a chain, not a V). While it has both children's gains, it also checks the path that bends at node. The extra insight: clamp negative gains to 0, meaning "don't take that side at all".
int best = Integer.MIN_VALUE; // best full path seen anywhere
int maxPathSum(TreeNode root) {
gain(root);
return best;
}
// returns the best chain going DOWN from node (can be handed to the parent)
int gain(TreeNode node) {
if (node == null) return 0;
int left = Math.max(0, gain(node.left)); // a negative chain? take nothing
int right = Math.max(0, gain(node.right));
best = Math.max(best, node.val + left + right); // path that bends at node
return node.val + Math.max(left, right); // parent may use only ONE side
}
Time O(n)Space O(h)
Each node is visited once and does O(1) work; the stack is as deep as the tree's height h.
"Best path anywhere in a tree": the DFS returns the best one-sided chain to the parent and records the best two-sided path in a global. Return and record are different values.
Start best at Integer.MIN_VALUE, not 0: in an all-negative tree like [-3] the answer is -3. And never return left + right + node.val to the parent: that is a V shape, and the parent can't attach to the middle of a V.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 297Serialize and Deserialize Binary TreeHardPreorder with null markersLeetCode
Write a class with two methods: serialize(root) turns a tree into a string, and deserialize(data) turns that string back into the exact same tree (same shape, same values). You choose the format. Values can repeat and be negative.
Example: root = [1, 2, 3, null, null, 4, 5] → some string → [1, 2, 3, null, null, 4, 5] again.
"Turn a tree into text and back" means: pick a traversal order and write down enough to know where each subtree ends. A plain preorder list is ambiguous (1, 2 could be 2 on the left or the right). Recording the null children fixes that. You already rebuilt a tree from two traversals in LC 105; here one traversal plus null markers is enough.
1Brute force — store it like a heap array
Number the slots like a binary heap: root at slot 0, children of slot i at 2i+1 and 2i+2. Make an array big enough for a full tree of the same height, fill in the values, write # in empty slots.
public class Codec {
// store the tree like a heap: node at slot i, children at 2i+1 and 2i+2
public String serialize(TreeNode root) {
int h = height(root);
String[] slots = new String[(1 << h) - 1]; // 2^h - 1 slots
Arrays.fill(slots, "#"); // '#' = no node here
fill(root, 0, slots);
return String.join(",", slots);
}
void fill(TreeNode node, int i, String[] slots) {
if (node == null) return;
slots[i] = String.valueOf(node.val);
fill(node.left, 2 * i + 1, slots);
fill(node.right, 2 * i + 2, slots);
}
int height(TreeNode node) {
return node == null ? 0 : 1 + Math.max(height(node.left), height(node.right));
}
public TreeNode deserialize(String data) {
if (data.isEmpty()) return null;
return build(data.split(","), 0);
}
TreeNode build(String[] slots, int i) {
if (i >= slots.length || slots[i].equals("#")) return null;
TreeNode node = new TreeNode(Integer.parseInt(slots[i]));
node.left = build(slots, 2 * i + 1);
node.right = build(slots, 2 * i + 2);
return node;
}
}
Time O(2ʰ)Space O(2ʰ)
Fine for bushy trees, hopeless for tall ones: a chain of just 31 nodes needs 2³¹ − 1 (about 2 billion) slots, and the problem allows 10⁴ nodes. The size should depend on the number of nodes, not the height.
2Optimal — preorder with "#" for null, read back in the same order
Write the tree in preorder (node, left subtree, right subtree) and write # for every null child. To read it back, use the same recursion: take one token; # means null, a number means "make a node, then read its left subtree, then its right subtree". The extra insight: the # markers tell the reader exactly where each subtree stops, so no sizes or second traversal are needed.
public class Codec {
// preorder, writing "#" for every null child
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
write(root, sb);
return sb.toString();
}
void write(TreeNode node, StringBuilder sb) {
if (node == null) { sb.append("#,"); return; }
sb.append(node.val).append(','); // root first...
write(node.left, sb); // ...then the whole left subtree
write(node.right, sb); // ...then the whole right subtree
}
public TreeNode deserialize(String data) {
Deque<String> tokens = new ArrayDeque<>(Arrays.asList(data.split(",")));
return read(tokens);
}
TreeNode read(Deque<String> tokens) { // read in the SAME order we wrote
String t = tokens.poll();
if (t.equals("#")) return null;
TreeNode node = new TreeNode(Integer.parseInt(t));
node.left = read(tokens); // consumes exactly the left subtree
node.right = read(tokens);
return node;
}
}
Time O(n)Space O(n)
n nodes plus n + 1 null markers, each written once and read once. The string and the recursion stack are both O(n) at worst.
Preorder + null markers uniquely describes a binary tree. Deserialize with the same recursion shape as serialize, reading from a shared queue (or an index) of tokens.
Use a separator (,): values like -12 or 100 are more than one character. And the reader must share one position across all calls (a queue or a field), not a local index that each recursive call copies.
Pattern notes: Binary Trees · Open on LeetCode
LC 987Vertical Order Traversal of a Binary TreeHardDFS + Sort by positionLeetCode
Give every node a position: the root is at (row 0, column 0), a left child is one row down and one column left, a right child one row down and one column right. Return the values column by column, left to right. Inside a column, go top to bottom; if two nodes share the same row and column, the smaller value comes first.
Example: root = [1, 2, 3, 4, 6, 5, 7] → [[4], [2], [1, 5, 6], [3], [7]]. 5 and 6 are both at row 2, column 0, so 5 goes first.
"Column", "row" and a tie-break by value mean each node has a 3-part sort key (column, row, value). Collecting positions is a plain DFS that passes row + 1 and col ± 1 to children; the Hard part is only getting the order right, which a sort does for you.
1Brute force — one full walk per column
First find the leftmost and rightmost column. Then, for each column, walk the whole tree collecting the nodes in that column, and sort just those by (row, value).
int minCol, maxCol;
List<List<Integer>> verticalTraversal(TreeNode root) {
span(root, 0); // how wide is the tree?
List<List<Integer>> result = new ArrayList<>();
for (int c = minCol; c <= maxCol; c++) { // one full walk PER column
List<int[]> here = new ArrayList<>(); // {row, val}
collect(root, 0, 0, c, here);
here.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
List<Integer> col = new ArrayList<>();
for (int[] p : here) col.add(p[1]);
result.add(col);
}
return result;
}
void span(TreeNode n, int col) {
if (n == null) return;
minCol = Math.min(minCol, col);
maxCol = Math.max(maxCol, col);
span(n.left, col - 1);
span(n.right, col + 1);
}
void collect(TreeNode n, int row, int col, int want, List<int[]> out) {
if (n == null) return;
if (col == want) out.add(new int[]{row, n.val});
collect(n.left, row + 1, col - 1, want, out);
collect(n.right, row + 1, col + 1, want, out);
}
Time O(n · W)Space O(n)
W is the number of columns, which can be close to n (a zig-zag tree), so this is O(n²). With the limit of 1000 nodes it still passes, but it walks the tree W times for no reason.
2Optimal — one DFS, one sort on (col, row, val)
It is a normal DFS from Intermediate (like LC 199 / LC 102, but carrying coordinates), plus one extra insight: turn every node into a triple (col, row, val) and sort all triples once. The sort puts columns in order, rows in order inside a column, and breaks ties by value, which are exactly the three rules of the problem. Then cut the sorted list wherever the column changes.
List<List<Integer>> verticalTraversal(TreeNode root) {
List<int[]> nodes = new ArrayList<>(); // {col, row, val}
dfs(root, 0, 0, nodes);
// sort by column, then row (top first), then value (small first)
nodes.sort((a, b) -> a[0] != b[0] ? a[0] - b[0]
: a[1] != b[1] ? a[1] - b[1]
: a[2] - b[2]);
List<List<Integer>> result = new ArrayList<>();
int prevCol = Integer.MIN_VALUE;
for (int[] t : nodes) {
if (t[0] != prevCol) { // a new column starts
result.add(new ArrayList<>());
prevCol = t[0];
}
result.get(result.size() - 1).add(t[2]);
}
return result;
}
void dfs(TreeNode n, int row, int col, List<int[]> out) {
if (n == null) return;
out.add(new int[]{col, row, n.val});
dfs(n.left, row + 1, col - 1, out); // left child: one column left
dfs(n.right, row + 1, col + 1, out); // right child: one column right
}
Time O(n log n)Space O(n)
The DFS is O(n); sorting n triples is O(n log n) and dominates.
When the output order has several rules ("by column, then row, then value"), collect tuples and sort with a comparator that checks the rules in that order. One sort beats nested maps.
A BFS by column with a plain list (the LC 314 approach) is wrong here: it keeps same-row, same-column nodes in visiting order (6 before 5), not sorted by value. You need the value in the sort key.
Pattern notes: Tree BFS & DFS · Open on LeetCode
LC 968Binary Tree CamerasHardGreedy DFS (3 states)LeetCode
You can put cameras on tree nodes. A camera watches its own node, its parent and its children. What is the fewest cameras so that every node is watched?
Example: root = [0, 0, null, 0, 0] → 1: one camera on the root's left child watches all four nodes.
"Minimum number of things placed on a tree" where each thing covers its neighbours. Trying all placements is exponential. On a tree you can decide bottom-up: a node's best choice only depends on what its children report, so a post-order DFS with a few states works, like the "what do I tell my parent" DFS in LC 124.
1Brute force — try every set of camera nodes
List all nodes and remember each one's parent. For every subset (a bitmask), check that each node is watched by itself, its parent or a child. Keep the smallest valid subset.
List<TreeNode> nodes = new ArrayList<>();
Map<TreeNode, TreeNode> parent = new HashMap<>();
int minCameraCover(TreeNode root) {
collect(root, null);
int n = nodes.size(), best = n;
for (int mask = 0; mask < (1 << n); mask++) { // every subset of nodes
Set<TreeNode> cams = new HashSet<>();
for (int i = 0; i < n; i++) if ((mask >> i & 1) == 1) cams.add(nodes.get(i));
boolean allCovered = true;
for (TreeNode v : nodes) { // is v watched by someone?
if (!cams.contains(v) && !cams.contains(parent.get(v))
&& !cams.contains(v.left) && !cams.contains(v.right)) {
allCovered = false;
break;
}
}
if (allCovered) best = Math.min(best, cams.size());
}
return best;
}
void collect(TreeNode node, TreeNode par) {
if (node == null) return;
nodes.add(node);
parent.put(node, par);
collect(node.left, node);
collect(node.right, node);
}
Time O(2ⁿ · n)Space O(n)
Already 25 nodes means about 8 × 10⁸ checks, and the problem allows 1000 nodes. It is only useful to test the fast version on tiny trees.
2Optimal — post-order greedy with three states
A tree DFS (as in LC 124) where each node returns a state instead of a number: NOT_COVERED, HAS_CAMERA or COVERED. The extra insight is a greedy rule: never put a camera on a leaf; put it on the leaf's parent. The parent's camera covers the leaf too, plus the parent and grandparent, so it is never worse.
From the bottom: a null child counts as COVERED (it needs nothing, and it must not force a camera onto a leaf). If any child is NOT_COVERED, this node must hold a camera. Else if any child has a camera, this node is COVERED. Otherwise it is NOT_COVERED and waits for its parent.
static final int NOT_COVERED = 0, HAS_CAMERA = 1, COVERED = 2;
int cameras = 0;
int minCameraCover(TreeNode root) {
if (dfs(root) == NOT_COVERED) cameras++; // nobody above the root to help
return cameras;
}
int dfs(TreeNode node) {
if (node == null) return COVERED; // null needs nothing, wants no camera
int left = dfs(node.left); // post-order: children decide first
int right = dfs(node.right);
if (left == NOT_COVERED || right == NOT_COVERED) {
cameras++; // a child is exposed: I MUST watch it
return HAS_CAMERA;
}
if (left == HAS_CAMERA || right == HAS_CAMERA) {
return COVERED; // a child's camera watches me
}
return NOT_COVERED; // leave me for my parent
}
Time O(n)Space O(h)
One post-order pass. The order of the checks matters: "a child is exposed" wins over "a child has a camera".
Covering / placing problems on trees: go bottom-up and return a small state to the parent. Delay every decision as high as possible (leaf → parent), because a higher camera covers more.
Two classic bugs: returning NOT_COVERED for null (then every leaf gets a camera), and forgetting the root. The root has no parent to cover it, so if the DFS returns NOT_COVERED for the root, add one more camera (a single node [0] needs 1).
Pattern notes: Greedy · Open on LeetCode
LC 99Recover Binary Search TreeMediumIn-order DFSLeetCode
In a binary search tree, the values of exactly two nodes were swapped by mistake. Fix the tree without changing its shape (swap the two values back).
Example: root = [3, 1, 4, null, null, 2] → [2, 1, 4, null, null, 3]: 3 and 2 were swapped.
"BST" plus "something is out of order" → look at the in-order traversal, which must be sorted (LC 98, Validate BST). Two swapped values make the sorted sequence go down once or twice. Finding those "drops" finds the two nodes.
1Brute force — in-order list, sort, write back
Collect the nodes in in-order position, take their values, sort them, and write the sorted values back into the nodes in the same in-order positions.
void recoverTree(TreeNode root) {
List<TreeNode> nodes = new ArrayList<>();
inorder(root, nodes); // nodes in in-order position
List<Integer> vals = new ArrayList<>();
for (TreeNode n : nodes) vals.add(n.val);
Collections.sort(vals); // what the values SHOULD be
for (int i = 0; i < nodes.size(); i++) nodes.get(i).val = vals.get(i);
}
void inorder(TreeNode n, List<TreeNode> out) {
if (n == null) return;
inorder(n.left, out);
out.add(n);
inorder(n.right, out);
}
Time O(n log n)Space O(n)
Correct, but it sorts everything to fix only two values and stores the whole tree in a list. The follow-up asks for O(1) extra space.
2Optimal — spot the drops during in-order
Same in-order walk with a prev pointer as LC 98 (Validate BST). The extra insight is what the drops mean: at the first drop the bigger value (prev) is wrong; at the last drop the smaller value (node) is wrong. If the swapped nodes are neighbours in the in-order, there is only one drop, and it gives both.
first from the first drop and second from the last.TreeNode first, second, prev;
void recoverTree(TreeNode root) {
inorder(root);
int tmp = first.val; // swap the two values back
first.val = second.val;
second.val = tmp;
}
void inorder(TreeNode node) {
if (node == null) return;
inorder(node.left);
if (prev != null && prev.val > node.val) { // a "drop" in the sorted order
if (first == null) first = prev; // 1st drop: the bigger one is wrong
second = node; // last drop: the smaller one is wrong
}
prev = node;
inorder(node.right);
}
Time O(n)Space O(h)
One in-order pass, then one swap. The only memory is the recursion stack, as deep as the tree.
3Follow-up — Morris traversal, O(1) space
To drop the stack, use Morris in-order: before going left, make the rightmost node of the left subtree point back to cur (a temporary "thread"). When you come back up the thread, the left side is finished, so you visit cur, remove the thread and go right. The visit check is the same as in step 2.
TreeNode first, second, prev;
void recoverTree(TreeNode root) {
TreeNode cur = root;
while (cur != null) {
if (cur.left == null) { visit(cur); cur = cur.right; }
else {
TreeNode pred = cur.left; // rightmost node of the left subtree
while (pred.right != null && pred.right != cur) pred = pred.right;
if (pred.right == null) { // first visit: leave a thread back to cur
pred.right = cur;
cur = cur.left;
} else { // came back up the thread: left is done
pred.right = null; // remove the thread (tree restored)
visit(cur);
cur = cur.right;
}
}
}
int tmp = first.val; first.val = second.val; second.val = tmp;
}
void visit(TreeNode node) { // same check as step 2
if (prev != null && prev.val > node.val) {
if (first == null) first = prev;
second = node;
}
prev = node;
}
Time O(n)Space O(1)
Each edge is walked at most a few times, and every thread is removed, so the tree ends up unchanged except for the fix.
BST problems → think of the sorted in-order sequence. Two swapped items in a sorted list: first = bigger value at the first drop, second = smaller value at the last drop.
Update second at every drop, not only when first is already set, or the neighbour case (one drop, like 1 3 2 4) leaves second null. And swap values, not nodes: moving nodes would change the shape.
Pattern notes: Binary Search Trees · Open on LeetCode
Design
Design problems ask for a class where every method is fast. Template: list the operations and their target cost, then combine structures so each one covers another's weak spot (a HashMap for "find it", a linked list or LinkedHashSet for "order", a heap for "best of many"). What is harder than Intermediate (LRU Cache, LC 146): you track two orders at once (how often and how recently), or you merge data from many users on every call.
LC 460LFU CacheHardHashMap + Frequency BucketsLeetCode
Build a cache with a fixed capacity. get(key) returns the value or -1. put(key, value) inserts or updates. When the cache is full and a new key arrives, remove the key used the fewest times; if several keys tie, remove the one used least recently. Both get and put count as a use. Both must run in O(1) on average.
Example: capacity 2: put(1,1), put(2,2), get(1) → 1, put(3,3) evicts key 2 (used once, key 1 used twice), get(2) → -1, get(3) → 3, put(4,4) evicts key 1 (1 and 3 both used twice, 1 is older), get(1) → -1, get(3) → 3, get(4) → 4.
"Cache", "capacity", "evict", "O(1)" is the LRU Cache family (LC 146). "Least frequently used, ties by least recently used" means two keys of order. You can't keep one list sorted by both in O(1), so group keys by frequency and keep an LRU order inside each group.
1Brute force (naive design) — one map, scan to evict
Store key → {value, freq, lastUsed} with a global clock. get and updates are O(1). To evict, scan every key and pick the smallest freq, breaking ties by the smallest lastUsed.
class LFUCache {
private final int capacity;
private int time = 0; // a clock: bigger = more recent
private Map<Integer, int[]> map = new HashMap<>(); // key -> {value, freq, lastUsed}
public LFUCache(int capacity) { this.capacity = capacity; }
public int get(int key) {
int[] e = map.get(key);
if (e == null) return -1;
e[1]++; // one more use
e[2] = ++time;
return e[0];
}
public void put(int key, int value) {
if (capacity == 0) return;
int[] e = map.get(key);
if (e != null) { e[0] = value; e[1]++; e[2] = ++time; return; }
if (map.size() == capacity) { // scan EVERY key for the victim
int victim = -1;
int[] worst = null;
for (Map.Entry<Integer, int[]> en : map.entrySet()) {
int[] c = en.getValue();
if (worst == null || c[1] < worst[1]
|| (c[1] == worst[1] && c[2] < worst[2])) {
worst = c;
victim = en.getKey();
}
}
map.remove(victim);
}
map.put(key, new int[]{value, 1, ++time});
}
}
Time O(capacity) per put when fullSpace O(capacity)
With capacity 10⁴ and up to 2 × 10⁵ calls, eviction scans can reach about 2 × 10⁹ steps, and the problem explicitly asks for O(1).
2Optimal — one LRU list per frequency, plus minFreq
This is LRU Cache (LC 146) from Intermediate, one per frequency. buckets[f] is a LinkedHashSet of the keys used exactly f times, oldest first (a LinkedHashSet keeps insertion order and removes in O(1)). On every use, move the key from bucket f to the end of bucket f+1. The extra insight: keep minFreq; it only changes in two easy ways. It becomes 1 when a new key is inserted, and it goes up by 1 when a use empties bucket minFreq. To evict, remove the first key of buckets[minFreq].
minFreq points at the bucket to evict from, and the oldest key in that bucket goes first.class LFUCache {
private final int capacity;
private int minFreq = 0; // smallest freq in the cache
private Map<Integer, Integer> vals = new HashMap<>(); // key -> value
private Map<Integer, Integer> freq = new HashMap<>(); // key -> how many uses
// freq -> keys with that freq, oldest first (a tiny LRU list per frequency)
private Map<Integer, LinkedHashSet<Integer>> buckets = new HashMap<>();
public LFUCache(int capacity) { this.capacity = capacity; }
public int get(int key) {
if (!vals.containsKey(key)) return -1;
touch(key);
return vals.get(key);
}
public void put(int key, int value) {
if (capacity == 0) return;
if (vals.containsKey(key)) { // update = a use, too
vals.put(key, value);
touch(key);
return;
}
if (vals.size() == capacity) { // evict: least freq, then oldest
LinkedHashSet<Integer> lowest = buckets.get(minFreq);
int victim = lowest.iterator().next(); // first = least recently used
lowest.remove(victim);
vals.remove(victim);
freq.remove(victim);
}
vals.put(key, value);
freq.put(key, 1);
buckets.computeIfAbsent(1, f -> new LinkedHashSet<>()).add(key);
minFreq = 1; // a brand-new key has freq 1
}
private void touch(int key) { // move key from bucket f to f+1
int f = freq.get(key);
freq.put(key, f + 1);
buckets.get(f).remove(key);
if (f == minFreq && buckets.get(f).isEmpty()) minFreq++;
buckets.computeIfAbsent(f + 1, x -> new LinkedHashSet<>()).add(key); // goes to the END = newest
}
}
Time O(1) average, get and putSpace O(capacity)
Every step is a HashMap or LinkedHashSet operation, all O(1) on average. No scanning, no sorting.
Two orders (frequency, then recency) in O(1): bucket by the first order, keep the second order inside each bucket, and track the smallest non-empty bucket with a pointer that only moves in predictable ways.
Evict before inserting the new key, and only when the key is new: updating an existing key never evicts. Set minFreq = 1 after evicting, since the victim is found through the old minFreq. Handle capacity = 0 (older test cases) by ignoring puts.
Pattern notes: Design Problems · Open on LeetCode
LC 895Maximum Frequency StackHardStack per FrequencyLeetCode
Build a stack-like class. push(val) adds a value. pop() removes and returns the most frequent value in the stack; if several values tie, return the one closest to the top (pushed most recently).
Example: push 5, 7, 5, 7, 4, 5, then pop four times → 5, 7, 5, 4.
"Most frequent, ties by most recent" is the same two-order puzzle as LFU, flipped (take the max, not the min). Frequencies only go up or down by 1 on each push or pop, which hints that you can keep a stack per frequency level and a maxFreq pointer instead of a heap.
1Brute force (naive design) — plain stack, recount on pop
Keep a normal list. On pop, count every value, find the maximum count, then scan from the top for the first value with that count and remove it.
class FreqStack {
private List<Integer> stack = new ArrayList<>(); // plain stack, top = end
public void push(int val) { stack.add(val); }
public int pop() {
Map<Integer, Integer> count = new HashMap<>(); // recount everything
int maxFreq = 0;
for (int v : stack) {
count.merge(v, 1, Integer::sum);
maxFreq = Math.max(maxFreq, count.get(v));
}
for (int i = stack.size() - 1; i >= 0; i--) { // from the top down
if (count.get(stack.get(i)) == maxFreq) { // first most-frequent one
return stack.remove(i); // shifts the rest: O(n)
}
}
return -1; // never happens
}
}
Time O(n) per popSpace O(n)
With up to 2 × 10⁴ calls, pops can cost around 10⁸ steps in total. Too slow for the O(1) the interviewer expects.
2Optimal — a stack for each frequency level
Keep freq[val] and a stack group[f] for each frequency f. When val reaches frequency f, push it onto group[f]. The extra insight: a value with count 3 lives in group 1, 2 AND 3 at the same time. Popping from group[maxFreq] removes only its top copy; the lower copies are still in the right place. Each group is a normal stack, so "newest first" breaks ties for free.
class FreqStack {
private Map<Integer, Integer> freq = new HashMap<>(); // value -> count now
private Map<Integer, Deque<Integer>> group = new HashMap<>(); // f -> values that reached f
private int maxFreq = 0;
public void push(int val) {
int f = freq.getOrDefault(val, 0) + 1;
freq.put(val, f);
maxFreq = Math.max(maxFreq, f);
group.computeIfAbsent(f, x -> new ArrayDeque<>()).push(val); // val's f-th copy
}
public int pop() {
Deque<Integer> top = group.get(maxFreq); // most frequent values, newest on top
int val = top.pop();
freq.put(val, freq.get(val) - 1); // its lower copies stay in lower groups
if (top.isEmpty()) maxFreq--; // next level down always has something
return val;
}
}
Time O(1) per push and popSpace O(n)
If group[maxFreq] becomes empty, group[maxFreq − 1] can't be empty (every value there also passed through it), so maxFreq-- is always correct.
When a count changes by exactly 1 at a time, buckets indexed by count plus a max/min pointer replace a heap and give O(1). Keeping a value in every level it passed through removes all bookkeeping on pop.
Don't move the value from group f−1 to group f on push; leave the old copy there. If you move it, then after a pop you would have to put it back in the lower group at the right position, which breaks the "newest first" order.
Pattern notes: Design Problems · Open on LeetCode
LC 355Design TwitterMediumDesign + K-way MergeLeetCode
Build a tiny Twitter: postTweet(userId, tweetId), follow(a, b), unfollow(a, b), and getNewsFeed(userId), which returns the 10 most recent tweet ids from the user and everyone they follow, newest first.
Example: postTweet(1, 5), getNewsFeed(1) → [5], follow(1, 2), postTweet(2, 6), getNewsFeed(1) → [6, 5], unfollow(1, 2), getNewsFeed(1) → [5].
"Most recent 10" taken from several people's timelines, where each timeline is already in time order, is merge k sorted lists. That means a heap holding one candidate per list, like the heap tricks in LC 378 (Kth Smallest in a Sorted Matrix).
1Brute force (naive design) — one global list of all tweets
Store every tweet ever posted in one list. For a feed, walk it from the newest end and keep tweets whose author is the user or someone they follow, until you have 10.
class Twitter {
private List<int[]> allTweets = new ArrayList<>(); // {userId, tweetId}, oldest first
private Map<Integer, Set<Integer>> follows = new HashMap<>();
public void postTweet(int userId, int tweetId) {
allTweets.add(new int[]{userId, tweetId});
}
public List<Integer> getNewsFeed(int userId) {
Set<Integer> mine = follows.getOrDefault(userId, Set.of());
List<Integer> feed = new ArrayList<>();
for (int i = allTweets.size() - 1; i >= 0 && feed.size() < 10; i--) { // newest first
int author = allTweets.get(i)[0];
if (author == userId || mine.contains(author)) feed.add(allTweets.get(i)[1]);
}
return feed;
}
public void follow(int followerId, int followeeId) {
follows.computeIfAbsent(followerId, k -> new HashSet<>()).add(followeeId);
}
public void unfollow(int followerId, int followeeId) {
follows.getOrDefault(followerId, new HashSet<>()).remove(followeeId);
}
}
Time O(T) per feedSpace O(T + follows)
T is the number of tweets by everyone. It passes LeetCode's small limits, but a quiet user following nobody active still scans the whole platform's history on every feed.
2Optimal — per-user timelines, merged with a max-heap
Each user keeps their own tweets in posting order, stamped with a global time. For a feed, the extra insight is only look at the people involved, and merge their timelines with a heap: put each person's newest tweet in a max-heap by time, pop the newest, push the next-older tweet from the same person, and stop after 10 pops.
class Twitter {
private int time = 0; // global clock
private Map<Integer, List<int[]>> tweets = new HashMap<>(); // user -> {time, tweetId}, oldest first
private Map<Integer, Set<Integer>> follows = new HashMap<>();
public void postTweet(int userId, int tweetId) {
tweets.computeIfAbsent(userId, k -> new ArrayList<>()).add(new int[]{time++, tweetId});
}
public List<Integer> getNewsFeed(int userId) {
Set<Integer> people = new HashSet<>(follows.getOrDefault(userId, Set.of()));
people.add(userId); // my own tweets count too
// max-heap of {time, tweetId, userId, index in that user's list}
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> b[0] - a[0]);
for (int u : people) { // each user's NEWEST tweet
List<int[]> list = tweets.get(u);
if (list != null && !list.isEmpty()) {
int i = list.size() - 1;
heap.add(new int[]{list.get(i)[0], list.get(i)[1], u, i});
}
}
List<Integer> feed = new ArrayList<>();
while (!heap.isEmpty() && feed.size() < 10) {
int[] top = heap.poll(); // newest tweet overall
feed.add(top[1]);
int u = top[2], i = top[3] - 1; // that user's next-older tweet
if (i >= 0) {
int[] t = tweets.get(u).get(i);
heap.add(new int[]{t[0], t[1], u, i});
}
}
return feed;
}
public void follow(int followerId, int followeeId) {
if (followerId == followeeId) return;
follows.computeIfAbsent(followerId, k -> new HashSet<>()).add(followeeId);
}
public void unfollow(int followerId, int followeeId) {
Set<Integer> s = follows.get(followerId);
if (s != null) s.remove(followeeId);
}
}
Time O(F + 10 log F) per feedSpace O(T + follows)
F is the number of people in the feed (followees + 1). Building the heap touches each once; then only 10 pops and pushes. Post, follow and unfollow are O(1).
"Top k newest across many sorted streams" = k-way merge: a heap of one pointer per stream, pop the best, advance that stream. Store data per owner so a query only touches what it needs.
Tweet ids are not times: sort by a global counter you increment on every post. A user must see their own tweets even without following themselves, and unfollow of someone never followed (or of yourself) must not crash.
Pattern notes: Top K Elements & K-way Merge · Open on LeetCode
Tries
Tries: a tree of letters (or bits) where words that share a beginning share one path. Template: insert everything once, then walk the trie letter by letter (or bit by bit) and stop the moment the path breaks. What is harder than Intermediate (LC 208, LC 211): the trie is now a helper inside another algorithm. It steers a grid DFS, it stores numbers as 0/1 paths so you can greedily build the biggest XOR, or it stores words backwards so you can find mirror images fast.
LC 212Word Search IIHardTrie + backtrackingLeetCode
You get a letter grid and a list of words. Return every word that can be spelled by a path of neighbouring cells (up, down, left, right), without using the same cell twice in one word.
Example: board oaan / etae / ihkr / iflv, words = ["oath", "pea", "eat", "rain"] → ["eat", "oath"].
"Grid", "adjacent cells", "same cell not used twice" is Word Search (LC 79) backtracking. "A list of many words" is the new part: you don't want to run the search once per word. Many words that share beginnings is exactly what a trie is for, so one DFS can follow the trie and chase every word at once.
1Brute force — run LC 79 once per word
For each word, try every start cell and backtrack letter by letter, exactly like Word Search I.
List<String> findWords(char[][] board, String[] words) {
List<String> found = new ArrayList<>();
for (String w : words) {
if (exist(board, w)) found.add(w); // LC 79, once per word
}
return found;
}
boolean exist(char[][] b, String w) {
for (int r = 0; r < b.length; r++)
for (int c = 0; c < b[0].length; c++)
if (dfs(b, w, 0, r, c)) return true;
return false;
}
boolean dfs(char[][] b, String w, int k, int r, int c) {
if (k == w.length()) return true; // matched every letter
if (r < 0 || c < 0 || r >= b.length || c >= b[0].length
|| b[r][c] != w.charAt(k)) return false;
char saved = b[r][c];
b[r][c] = '#'; // used on this path
boolean ok = dfs(b, w, k + 1, r + 1, c) || dfs(b, w, k + 1, r - 1, c)
|| dfs(b, w, k + 1, r, c + 1) || dfs(b, w, k + 1, r, c - 1);
b[r][c] = saved; // undo
return ok;
}
Time O(W · m·n · 3ᴸ)Space O(L)
W words, an m×n board, words up to L letters. With 30,000 words on a 12×12 board that is already 4.3 million DFS starts before any branching. Worse, words like "oatha", "oathb", "oathc" walk the same o-a-t-h path again and again. That's a time out.
2Optimal — one DFS that walks the grid and the trie together
This is Word Search (LC 79) plus the Trie from LC 208. Put all words into one trie. Then DFS from every cell, and move to a neighbour only if the trie has a child for that letter. The trie tells you instantly when no word at all can continue this path, so the DFS stops there.
Two small extras make it fast: store the whole word on its last node (no need to rebuild strings), set it to null once found (no duplicates), and cut a node off the trie once it has no word and no children left, so later searches don't walk into dead branches.
class Node {
Node[] next = new Node[26];
String word; // set on the node where a word ends
}
List<String> findWords(char[][] board, String[] words) {
Node root = new Node();
for (String w : words) { // 1. all words into ONE trie
Node cur = root;
for (char ch : w.toCharArray()) {
int i = ch - 'a';
if (cur.next[i] == null) cur.next[i] = new Node();
cur = cur.next[i];
}
cur.word = w;
}
List<String> found = new ArrayList<>();
for (int r = 0; r < board.length; r++) // 2. one DFS per cell finds
for (int c = 0; c < board[0].length; c++) // every word at once
dfs(board, r, c, root, found);
return found;
}
void dfs(char[][] b, int r, int c, Node parent, List<String> found) {
if (r < 0 || c < 0 || r >= b.length || c >= b[0].length) return;
char ch = b[r][c];
if (ch == '#' || parent.next[ch - 'a'] == null) return; // no word goes this way
Node node = parent.next[ch - 'a'];
if (node.word != null) { // a whole word is spelled
found.add(node.word);
node.word = null; // never report it twice
}
b[r][c] = '#'; // used on this path
dfs(b, r + 1, c, node, found);
dfs(b, r - 1, c, node, found);
dfs(b, r, c + 1, node, found);
dfs(b, r, c - 1, node, found);
b[r][c] = ch; // undo
if (node.word == null && isEmpty(node)) // branch fully used up:
parent.next[ch - 'a'] = null; // cut it off the trie
}
boolean isEmpty(Node node) {
for (Node child : node.next) if (child != null) return false;
return true;
}
Time O(m·n · 4·3ᴸ⁻¹)Space O(total letters in words)
The worst case looks the same as one Word Search, but now it is paid once for all words, not once per word, and the pruning cuts most paths after one or two letters. Building the trie costs one step per letter of input.
"Search for many words in the same place" → put the words in a trie and let one search walk the trie alongside the data. The trie says "stop" the moment no word can continue.
Don't return when you find a word: "oath" and "oaths" can share a path, so keep searching below. And set node.word = null after adding it, or a word reachable from two different start cells is reported twice.
Pattern notes: Tries · Open on LeetCode
LC 421Maximum XOR of Two Numbers in an ArrayMediumBit trieLeetCode
Pick two numbers from the array (they may be the same position) so that a XOR b is as large as possible. Return that value. XOR gives a 1 in every bit where the two numbers differ.
Example: nums = [3, 10, 5, 25, 2, 8] → 28, from 5 XOR 25 (00101 XOR 11001 = 11100).
"Maximum XOR of a pair" with up to 200,000 numbers. XOR is decided bit by bit, and a bigger number is decided by its highest bit first. A trie over the bits (only two children: 0 and 1) lets you ask "is there a number whose next bit is the opposite of mine?" in one step per bit.
1Brute force — XOR every pair
Two loops, keep the best XOR. (A pair of the same number gives 0, so starting at 0 is fine.)
int findMaximumXOR(int[] nums) {
int best = 0;
for (int i = 0; i < nums.length; i++)
for (int j = i + 1; j < nums.length; j++)
best = Math.max(best, nums[i] ^ nums[j]); // try every pair
return best;
}
Time O(n²)Space O(1)
With n = 200,000 that's about 2·10¹⁰ pairs. It will time out.
2Optimal — greedy walk down a 0/1 trie
This is the Trie from LC 208, with an alphabet of just {0, 1}, plus one greedy insight: a 1 in a high bit is worth more than all lower bits together (10000 = 16 > 01111 = 15). So for each number x, walk from the top bit down and always try to go the opposite way from x's bit. If that child exists, this XOR bit becomes 1. If not, you are forced to follow the same bit and this XOR bit is 0.
Insert all numbers as 31-bit paths first (values fit in 31 bits), then run this walk for every number and keep the best.
class Node {
Node[] child = new Node[2]; // child[0] = bit 0, child[1] = bit 1
}
int findMaximumXOR(int[] nums) {
Node root = new Node();
for (int x : nums) { // 1. insert every number, top bit first
Node cur = root;
for (int b = 30; b >= 0; b--) {
int bit = (x >> b) & 1;
if (cur.child[bit] == null) cur.child[bit] = new Node();
cur = cur.child[bit];
}
}
int best = 0;
for (int x : nums) { // 2. best partner for each x
Node cur = root;
int xor = 0;
for (int b = 30; b >= 0; b--) {
int bit = (x >> b) & 1;
int want = 1 - bit; // the opposite bit makes this XOR bit 1
if (cur.child[want] != null) {
xor |= 1 << b;
cur = cur.child[want];
} else {
cur = cur.child[bit]; // no choice: this XOR bit is 0
}
}
best = Math.max(best, xor);
}
return best;
}
Time O(31 · n)Space O(31 · n)
Every insert and every walk is exactly 31 steps, so the whole thing is linear in n. The trie has at most 31 new nodes per number.
"Best XOR partner" → store the numbers as bit paths in a trie (top bit first) and greedily take the opposite bit whenever it exists. Higher bits always win over lower ones.
Insert and query from the top bit (bit 30) down, never from bit 0 up: the greedy choice is only safe when the most valuable bit is decided first. Also use the same number of bits for every number, or short numbers end at different depths.
Pattern notes: Tries · Open on LeetCode
LC 336Palindrome PairsHardHash map + palindrome splitLeetCode
You get a list of different words. Find every ordered pair of positions [i, j] (i ≠ j) such that words[i] + words[j] reads the same forwards and backwards.
Example: ["abcd", "dcba", "lls", "s", "sssll"] → [[0,1], [1,0], [3,2], [2,4]]: "abcddcba", "dcbaabcd", "slls", "llssssll".
"Pairs" out of up to 5,000 words means the n² pair loop is too slow, so for each word you want to look up its partner directly, like Two Sum. The partner of a word is (roughly) its reverse, and any extra letters must be a palindrome by themselves. Words of up to 300 letters means you can afford to try every cut point of each word.
1Brute force — glue every pair and check
Try every ordered pair, build the joined string and test it with two pointers.
List<List<Integer>> palindromePairs(String[] words) {
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < words.length; i++)
for (int j = 0; j < words.length; j++)
if (i != j && isPal(words[i] + words[j])) // glue and check
res.add(List.of(i, j));
return res;
}
boolean isPal(String s) {
for (int lo = 0, hi = s.length() - 1; lo < hi; lo++, hi--)
if (s.charAt(lo) != s.charAt(hi)) return false;
return true;
}
Time O(n² · k)Space O(k)
n words of length up to k. With n = 5,000 and k = 300 that is 25 million pairs, each building and checking up to 600 characters: around 10¹⁰ steps. Time out.
2Optimal — cut each word, look up the mirror half
This is Two Sum's "look up the partner in a HashMap", plus one insight about palindromes. Cut word w into left | right at every position:
w + other is a palindrome if other is the reverse of left and right is a palindrome on its own (it sits in the middle). other + w works if other is the reverse of right and left is a palindrome. So store every word reversed in a map, and for each cut do two lookups.
ss | sll. Put "lls" (the reverse of "sll") in front, and the palindrome "ss" ends up in the middle.List<List<Integer>> palindromePairs(String[] words) {
Map<String, Integer> rev = new HashMap<>(); // reversed word -> its index
for (int i = 0; i < words.length; i++)
rev.put(new StringBuilder(words[i]).reverse().toString(), i);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < words.length; i++) {
String w = words[i];
for (int cut = 0; cut <= w.length(); cut++) {
String left = w.substring(0, cut), right = w.substring(cut);
// w + other: other reversed == left, and right is a palindrome
Integer j = rev.get(left);
if (j != null && j != i && isPal(right)) res.add(List.of(i, j));
// other + w: other reversed == right, and left is a palindrome
j = rev.get(right);
if (cut > 0 && j != null && j != i && isPal(left)) res.add(List.of(j, i));
}
}
return res;
}
boolean isPal(String s) {
for (int lo = 0, hi = s.length() - 1; lo < hi; lo++, hi--)
if (s.charAt(lo) != s.charAt(hi)) return false;
return true;
}
Time O(n · k²)Space O(n · k)
Each word has k + 1 cuts, and each cut costs O(k) for the substring, the lookup and the palindrome check. That is about 5,000 · 300² ≈ 4.5·10⁸ simple steps in the worst case, versus 10¹⁰ before.
3Trie version — the same idea, words stored backwards
Insert every word reversed into a trie. While inserting, each node remembers which words have a palindrome left over from that point (palBelow). Then walk each word forwards: if a shorter word ends on the way and the rest of w is a palindrome, that is a pair; when w is used up, every word in palBelow pairs with it.
class Node {
Node[] next = new Node[26];
int word = -1; // index of the word that ends here
List<Integer> palBelow = new ArrayList<>(); // words whose leftover here is a palindrome
}
List<List<Integer>> palindromePairs(String[] words) {
Node root = new Node();
for (int i = 0; i < words.length; i++) { // insert every word BACKWARDS
String w = words[i];
Node cur = root;
for (int k = w.length() - 1; k >= 0; k--) {
if (isPal(w, 0, k)) cur.palBelow.add(i); // leftover w[0..k] reads the same both ways
int c = w.charAt(k) - 'a';
if (cur.next[c] == null) cur.next[c] = new Node();
cur = cur.next[c];
}
cur.word = i;
cur.palBelow.add(i); // empty leftover is a palindrome too
}
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < words.length; i++) { // walk each word FORWARDS
String w = words[i];
Node cur = root;
for (int k = 0; k < w.length() && cur != null; k++) {
if (cur.word >= 0 && cur.word != i && isPal(w, k, w.length() - 1))
res.add(List.of(i, cur.word)); // other is shorter; rest of w is a palindrome
cur = cur.next[w.charAt(k) - 'a'];
}
if (cur == null) continue;
for (int j : cur.palBelow) // other is as long or longer
if (j != i) res.add(List.of(i, j));
}
return res;
}
boolean isPal(String s, int lo, int hi) {
for (; lo < hi; lo++, hi--)
if (s.charAt(lo) != s.charAt(hi)) return false;
return true;
}
Time O(n · k²)Space O(n · k)
Same complexity, but no substrings are built, so it runs faster in practice. The map version is easier to get right in an interview.
Two strings glued into a palindrome: one is the reverse of part of the other, and the leftover part is a palindrome by itself. Try every cut and look up the reverse in a map (or a reversed trie).
Duplicates: for "abcd" and "dcba" the pair [0, 1] is found by "abcd" at cut = length (first check) and again by "dcba" at cut 0 (second check), so skip the second check when cut == 0. And don't forget the empty string: "" pairs with every palindrome word, in both orders.
Pattern notes: Tries · Open on LeetCode
Segment Trees & Fenwick Trees
Range trees: when the array keeps changing and you keep asking for sums (or counts) over ranges. A prefix-sum array answers a range in O(1), but one change forces you to rewrite every prefix after it. Template: split the array into pre-summed blocks of size 1, 2, 4, 8…, so an update touches only log n blocks and a query adds only log n blocks. What is harder than Intermediate: the tree is stored in a plain array, and for counting problems (LC 315, LC 493) you first turn "how many smaller to my right" into "sum over a range of values".
LC 307Range Sum Query - MutableMediumSegment tree / BITLeetCode
Build a class over an integer array with two methods: update(index, val) changes one number, and sumRange(left, right) returns the sum from left to right (both included). Both are called many times, mixed together.
Example: nums = [1, 3, 5]: sumRange(0, 2) → 9, update(1, 2), sumRange(0, 2) → 8.
"Sum of a range" says prefix sums (LC 303, LC 560). But the word "update" mixed with many queries says the array changes between questions, and prefix sums hate change. Many point updates + many range queries is the textbook signal for a segment tree or a Fenwick tree (BIT).
1Brute force — plain array, loop on every query
Keep the array as it is. Updates are instant, queries add up the range one by one.
class NumArray {
private int[] nums;
public NumArray(int[] nums) {
this.nums = nums;
}
public void update(int index, int val) {
nums[index] = val; // O(1)
}
public int sumRange(int left, int right) {
int sum = 0;
for (int i = left; i <= right; i++) sum += nums[i]; // O(n) every time
return sum;
}
}
update O(1)sumRange O(n)Space O(1)
With n = 30,000 and 30,000 queries that is up to 9·10⁸ additions. The opposite choice doesn't help either. Here is why a prefix array breaks as soon as a value changes:
Every prefix after the changed spot contains that number, so all of them go stale. One side is always O(n).
2Optimal — segment tree: pre-summed halves, quarters, eighths
This is prefix sums plus divide and conquer. Instead of one sum per prefix, keep one sum per block: the whole array, its two halves, their halves, down to single numbers. Each number sits in only log n blocks (one per level), so an update fixes log n sums, and any range is glued from at most about 2·log n whole blocks.
What the tree buys you: both operations become O(log n). No side is O(n) any more.
Storage trick: node 1 is the whole array, and node i has children 2i and 2i + 1, just like a heap, so a plain int[4n] is enough.
class NumArray {
private int n;
private int[] tree; // tree[node] = sum of that node's range
public NumArray(int[] nums) {
n = nums.length;
tree = new int[4 * n]; // 4n is always enough room
build(nums, 1, 0, n - 1); // node 1 = the whole array
}
private void build(int[] nums, int node, int lo, int hi) {
if (lo == hi) { tree[node] = nums[lo]; return; } // leaf: one element
int mid = (lo + hi) / 2;
build(nums, 2 * node, lo, mid); // left child
build(nums, 2 * node + 1, mid + 1, hi); // right child
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
public void update(int index, int val) {
update(1, 0, n - 1, index, val);
}
private void update(int node, int lo, int hi, int i, int val) {
if (lo == hi) { tree[node] = val; return; }
int mid = (lo + hi) / 2;
if (i <= mid) update(2 * node, lo, mid, i, val); // go to the half holding i
else update(2 * node + 1, mid + 1, hi, i, val);
tree[node] = tree[2 * node] + tree[2 * node + 1]; // fix sums on the way up
}
public int sumRange(int left, int right) {
return query(1, 0, n - 1, left, right);
}
private int query(int node, int lo, int hi, int l, int r) {
if (r < lo || hi < l) return 0; // no overlap: ignore
if (l <= lo && hi <= r) return tree[node]; // fully inside: use stored sum
int mid = (lo + hi) / 2; // partly inside: ask both kids
return query(2 * node, lo, mid, l, r) + query(2 * node + 1, mid + 1, hi, l, r);
}
}
update O(log n)sumRange O(log n)Space O(n)
Building is O(n) (each node summed once). The query code has only three cases: no overlap, full overlap, partial overlap. Learn those three and you can write a segment tree for min, max or gcd too.
3Shorter — Fenwick tree (Binary Indexed Tree)
A BIT only stores prefix-like blocks: bit[i] holds the sum of the last lowbit(i) numbers ending at position i (1-based), where lowbit(i) = i & -i is the lowest 1-bit of i. A prefix sum hops down by stripping that bit; an update hops up by adding it. Both take at most log n hops.
class NumArray {
private int[] nums; // current values (to know the change)
private int[] bit; // Fenwick tree, 1-indexed
public NumArray(int[] nums) {
this.nums = new int[nums.length];
bit = new int[nums.length + 1];
for (int i = 0; i < nums.length; i++) update(i, nums[i]);
}
public void update(int index, int val) {
int delta = val - nums[index];
nums[index] = val;
for (int i = index + 1; i < bit.length; i += i & -i) // climb to every box
bit[i] += delta; // that covers index
}
public int sumRange(int left, int right) {
return prefix(right + 1) - prefix(left);
}
private int prefix(int i) { // sum of the first i numbers
int sum = 0;
for (; i > 0; i -= i & -i) sum += bit[i]; // strip the lowest 1-bit each step
return sum;
}
}
update O(log n)sumRange O(log n)Space O(n)
Same speed as the segment tree with a third of the code. A BIT only answers things you can subtract (sums, counts); for range min or max, use the segment tree.
Point updates + range sums: prefix sums make one side O(n). Store sums of blocks instead (segment tree or BIT) and both sides become O(log n). The BIT loops are two lines: i += i & -i going up, i -= i & -i going down.
A BIT is 1-indexed: position 0 would give 0 & -0 = 0 and loop forever, so shift every index by one. And update sets a new value, but the BIT only knows how to add, so keep the old values and add val − old.
Pattern notes: Segment Trees & Fenwick Trees · Open on LeetCode
LC 315Count of Smaller Numbers After SelfHardBIT + compressionLeetCode
For every position i, count how many numbers to its right are strictly smaller than nums[i]. Return those counts as a list.
Example: nums = [5, 2, 6, 1] → [2, 1, 1, 0]. To the right of 5 are 2 and 1 (two smaller), to the right of 2 is only 1, and so on.
"How many smaller numbers to the right" for every element, with n up to 100,000. That is a counting question over a range of values ("values below mine") on a set that grows as you walk. A growing set + "how many below x" is exactly LC 307's point update + prefix sum, with the values as the positions.
1Brute force — look to the right from every position
List<Integer> countSmaller(int[] nums) {
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
int count = 0;
for (int j = i + 1; j < nums.length; j++) // look at everything to the right
if (nums[j] < nums[i]) count++;
res.add(count);
}
return res;
}
Time O(n²)Space O(1) extra
With n = 100,000 that is about 5·10⁹ comparisons. Time out.
2Optimal — walk from the right, count seen values in a BIT
This is the BIT from LC 307, but indexed by value instead of by position. Walk from right to left. Everything already visited is exactly "the numbers to my right". Keep a BIT where bit position r counts how many seen numbers have rank r. Then "how many to my right are smaller" = prefix sum of ranks below mine, and afterwards you add 1 at your own rank.
The extra step is coordinate compression: values can be negative or huge, so sort the distinct values and use each value's position in that sorted list (its rank, 1..m) as the BIT index. Only the order matters, not the actual values.
[2, 1, 1, 0].List<Integer> countSmaller(int[] nums) {
// 1. coordinate compression: value -> rank 1..m (smallest value = rank 1)
int[] sorted = Arrays.stream(nums).distinct().sorted().toArray();
int m = sorted.length;
int[] bit = new int[m + 1]; // bit counts how many of each rank we've seen
Integer[] res = new Integer[nums.length];
for (int i = nums.length - 1; i >= 0; i--) { // 2. walk from the RIGHT
int rank = Arrays.binarySearch(sorted, nums[i]) + 1;
int smaller = 0;
for (int k = rank - 1; k > 0; k -= k & -k) smaller += bit[k]; // seen with rank < mine
res[i] = smaller;
for (int k = rank; k <= m; k += k & -k) bit[k]++; // now I am "seen"
}
return Arrays.asList(res);
}
Time O(n log n)Space O(n)
Sorting for the ranks is O(n log n); then each of the n numbers does one binary search, one BIT query and one BIT update, all O(log n).
3Alternative — merge sort that counts
During merge sort, when a left-half item is placed, every right-half item placed before it is smaller and was to its right. So add j − (mid + 1) to its count. Sort the indices, not the values, so you know whose count to increase.
int[] count; // answer, by original index
List<Integer> countSmaller(int[] nums) {
int n = nums.length;
count = new int[n];
int[] idx = new int[n]; // sort INDICES, so we remember who is who
for (int i = 0; i < n; i++) idx[i] = i;
sort(nums, idx, new int[n], 0, n - 1);
List<Integer> res = new ArrayList<>();
for (int c : count) res.add(c);
return res;
}
void sort(int[] nums, int[] idx, int[] tmp, int lo, int hi) {
if (lo >= hi) return;
int mid = (lo + hi) / 2;
sort(nums, idx, tmp, lo, mid);
sort(nums, idx, tmp, mid + 1, hi);
int i = lo, j = mid + 1, k = lo;
while (i <= mid || j <= hi) {
if (j > hi || (i <= mid && nums[idx[i]] <= nums[idx[j]])) {
count[idx[i]] += j - (mid + 1); // right-half items already placed are smaller
tmp[k++] = idx[i++];
} else {
tmp[k++] = idx[j++];
}
}
for (k = lo; k <= hi; k++) idx[k] = tmp[k];
}
Time O(n log n)Space O(n)
No compression needed. The same "count while merging" trick solves LC 493 below.
"How many smaller / larger on one side" → walk from that side and keep a BIT over value ranks: query below your rank, then add yourself. Compress values to ranks first so the BIT stays size n.
"Strictly smaller" means query up to rank − 1, not rank, or equal values count each other. In the merge sort version the same rule shows up as <= in the comparison: an equal right-half item must be placed after the left one.
Pattern notes: Segment Trees & Fenwick Trees · Open on LeetCode
LC 493Reverse PairsHardMerge sort countLeetCode
Count the pairs i < j where nums[i] > 2 · nums[j]: an earlier number that is more than double a later one.
Example: nums = [2, 4, 3, 5, 1] → 3: the pairs (4, 1), (3, 1) and (5, 1).
"Count pairs i < j" with a comparison between the two values, and n up to 50,000. That is an inversion count in disguise. Merge sort splits every pair into "both on the left", "both on the right" or "one on each side", and the last kind is easy to count once both halves are sorted.
1Brute force — check every pair
int reversePairs(int[] nums) {
int count = 0;
for (int i = 0; i < nums.length; i++)
for (int j = i + 1; j < nums.length; j++)
if ((long) nums[i] > 2L * nums[j]) count++; // long: 2 * nums[j] can overflow
return count;
}
Time O(n²)Space O(1)
With n = 50,000 that is about 1.25·10⁹ pair checks. Time out.
2Optimal — merge sort, count before you merge
This is merge sort plus a two-pointer count (the same trick as LC 315, step 3). Recursively count pairs inside each half and sort each half. Now every left item is before every right item, so only the values matter. Because both halves are sorted, the pointer j into the right half only moves forward: if a[i] > 2 · a[j], then any bigger a[i] later on also beats that a[j]. Add j − (mid + 1) for each i, then do an ordinary merge.
Counting and merging are two separate loops, because the count uses 2 · a[j] but the sort uses a[j].
int reversePairs(int[] nums) {
return sort(nums, new int[nums.length], 0, nums.length - 1);
}
int sort(int[] a, int[] tmp, int lo, int hi) {
if (lo >= hi) return 0;
int mid = (lo + hi) / 2;
int count = sort(a, tmp, lo, mid) + sort(a, tmp, mid + 1, hi); // pairs inside each half
// both halves are sorted now: count pairs with i on the left, j on the right
int j = mid + 1;
for (int i = lo; i <= mid; i++) {
while (j <= hi && (long) a[i] > 2L * a[j]) j++; // j only moves forward
count += j - (mid + 1); // a[mid+1 .. j-1] all qualify
}
// ordinary merge, so the caller gets a sorted range
int i = lo, k = lo;
j = mid + 1;
while (i <= mid && j <= hi) tmp[k++] = a[i] <= a[j] ? a[i++] : a[j++];
while (i <= mid) tmp[k++] = a[i++];
while (j <= hi) tmp[k++] = a[j++];
for (k = lo; k <= hi; k++) a[k] = tmp[k];
return count;
}
Time O(n log n)Space O(n)
log n levels, and on each level the count loop and the merge loop are both linear. A BIT over compressed values of nums and 2·nums also works, like LC 315, but needs more setup.
"Count pairs i < j with some comparison" → merge sort. Inside each merge the halves are sorted, so one forward-only pointer counts all cross pairs in O(n). Count first, merge second.
Overflow: nums[j] can be up to 2³¹ − 1, so 2 * nums[j] overflows an int. Write 2L * a[j]. And don't merge while counting with the 2· rule: the merge must still compare plain values.
Pattern notes: Divide & Conquer · Open on LeetCode
Advanced Graphs
Advanced graphs are still the BFS, DFS, Dijkstra, union-find and topological sort you used in Intermediate (LC 200, 994, 207, 210, 684, 743). The template: decide what a node is, what an edge is and what an edge costs, then pick the algorithm that fits those costs. What is harder here: the graph is often hidden (words, bus routes, grid cells, even "node + set of visited nodes"), the path cost can be a maximum instead of a sum, and a few problems need a named classic (Bellman-Ford, Prim, Hierholzer, Tarjan) that is short once you have seen it.
| If the problem says… | Use | Problems below |
|---|---|---|
| Fewest steps, every move costs the same | BFS, level by level | 127, 815 |
| Fewest steps, but "where you have been" matters | BFS over (node, extra state), e.g. a bitmask | 847 |
| Cheapest path, costs ≥ 0 (sum or max along the path) | Dijkstra with a min-heap | 1631, 778 |
| Cheapest path with at most k edges | Bellman-Ford, k + 1 rounds | 787 |
| Longest path, moves only go "uphill" (no cycles) | DFS + memo, or topological order | 329 |
| Connect everything for the least total cost | Minimum spanning tree: Prim (dense) or Kruskal (sparse) | 1584 |
| Use every edge exactly once | Hierholzer (Eulerian path) | 332 |
| Which edges, if cut, split the graph | Tarjan low-link (bridges) | 1192 |
LC 127Word LadderHardBFS on implicit graphLeetCode
You start at beginWord and want to reach endWord. Each move changes exactly one letter, and every word you step on (except the first) must be in wordList. Return the number of words in the shortest ladder, counting both ends, or 0 if there is none.
Example: hit → cog with [hot, dot, dog, lot, log, cog] → 5 (hit → hot → dot → dog → cog). Without cog in the list the answer is 0.
"Shortest sequence", "change one letter at a time": each word is a node, a one-letter change is an edge, and every edge costs 1. Fewest equal-cost moves is BFS, like Rotting Oranges (LC 994). Nobody hands you the edges, so the real work is finding the neighbours fast.
1Brute force — BFS that compares against every word
Run a normal BFS. To find the neighbours of a word, loop over the whole list and keep the words that differ in exactly one position.
int ladderLength(String beginWord, String endWord, List<String> wordList) {
Queue<String> q = new ArrayDeque<>();
Set<String> seen = new HashSet<>();
q.add(beginWord);
seen.add(beginWord);
for (int steps = 1; !q.isEmpty(); steps++) { // steps = words in the ladder so far
for (int size = q.size(); size > 0; size--) {
String w = q.poll();
if (w.equals(endWord)) return steps;
for (String next : wordList) { // scan the WHOLE list every time
if (!seen.contains(next) && oneApart(w, next)) {
seen.add(next);
q.add(next);
}
}
}
}
return 0;
}
boolean oneApart(String a, String b) {
int diff = 0;
for (int i = 0; i < a.length(); i++)
if (a.charAt(i) != b.charAt(i)) diff++;
return diff == 1;
}
Time O(N² · L)Space O(N)
Each of the N words is compared with all N words, letter by letter. With the limits (N = 5000 words of length L = 10) that is about 250 million letter checks: too slow.
2Optimal — BFS with wildcard buckets
This is the BFS you know, plus one trick for the neighbours: group words by a pattern with one letter blanked out. hot, dot and lot all land in the bucket *ot. Two words are neighbours exactly when they share a bucket, so a word's neighbours are just the contents of its L buckets. No more scanning the whole list.
Bonus: once a bucket has been expanded, every word in it is already queued, so we delete the bucket and never read it again.
int ladderLength(String beginWord, String endWord, List<String> wordList) {
if (!wordList.contains(endWord)) return 0;
int L = beginWord.length();
Map<String, List<String>> buckets = new HashMap<>(); // "h*t" -> [hot, hit, ...]
for (String w : wordList)
for (int i = 0; i < L; i++)
buckets.computeIfAbsent(w.substring(0, i) + '*' + w.substring(i + 1),
k -> new ArrayList<>()).add(w);
Queue<String> q = new ArrayDeque<>();
Set<String> seen = new HashSet<>();
q.add(beginWord);
seen.add(beginWord);
for (int steps = 1; !q.isEmpty(); steps++) {
for (int size = q.size(); size > 0; size--) {
String w = q.poll();
if (w.equals(endWord)) return steps;
for (int i = 0; i < L; i++) {
String key = w.substring(0, i) + '*' + w.substring(i + 1);
for (String next : buckets.getOrDefault(key, List.of()))
if (seen.add(next)) q.add(next); // add() is false if already seen
buckets.remove(key); // bucket used up: never scan it again
}
}
}
return 0;
}
Time O(N · L²)Space O(N · L²)
Every word makes L keys, and building each key costs L. For N = 5000 and L = 10 that is about half a million steps instead of 250 million.
When the graph is hidden, ask "how do I list a node's neighbours without checking every other node?" A shared key (here the wildcard pattern) turns an all-pairs scan into hash lookups.
The answer counts words, not moves: hit → hot is 2, so BFS starts at steps = 1. And if endWord is not in wordList, return 0 immediately, even if it is one letter away.
Pattern notes: Graphs & Traversal · Open on LeetCode
LC 815Bus RoutesHardBFS over routesLeetCode
routes[i] lists the stops bus i visits (it loops forever). You stand at stop source and want to reach stop target. What is the fewest buses you must ride? Return -1 if you can't get there.
Example: routes = [[1,2,7],[3,6,7]], source = 1, target = 6 → 2. Take bus 0 to stop 7, change to bus 1, ride to stop 6.
"Fewest buses" is a shortest path where every bus ride costs 1, no matter how many stops it passes. So BFS, but the thing that costs 1 is a bus, not a stop. When the cost is counted in something other than the obvious nodes, make that thing the node.
1Brute force — BFS over stops, scanning every bus
BFS level by level over stops. For each stop in the current level, look through every route to see if it stops there; if so, all its stops join the next level.
int numBusesToDestination(int[][] routes, int source, int target) {
if (source == target) return 0;
Set<Integer> seen = new HashSet<>(List.of(source));
List<Integer> level = List.of(source); // stops reachable with 'buses' rides
for (int buses = 1; !level.isEmpty(); buses++) {
List<Integer> next = new ArrayList<>();
for (int stop : level)
for (int[] route : routes) { // check EVERY bus at EVERY stop
if (!contains(route, stop)) continue;
for (int s : route) {
if (s == target) return buses;
if (seen.add(s)) next.add(s);
}
}
level = next;
}
return -1;
}
boolean contains(int[] route, int stop) {
for (int s : route) if (s == stop) return true;
return false;
}
Time O(S · T)Space O(S)
S is the number of different stops and T the total length of all routes. Both can be about 10⁵, so the worst case is around 10¹⁰ steps, far too slow. It also rides the same bus again from every stop on it.
2Optimal — BFS where the nodes are buses
This is plain BFS (Intermediate), plus a change of view: a node is a bus, and two buses are connected if they share a stop. A map stop → buses gives the connections. Start with every bus that stops at source (level 1), and the level where some bus reaches target is the answer.
Each bus is boarded once (taken[]), and each stop's list of buses is read once (seenStop), so no work is repeated.
int numBusesToDestination(int[][] routes, int source, int target) {
if (source == target) return 0;
Map<Integer, List<Integer>> busesAt = new HashMap<>(); // stop -> buses that stop there
for (int r = 0; r < routes.length; r++)
for (int s : routes[r])
busesAt.computeIfAbsent(s, k -> new ArrayList<>()).add(r);
boolean[] taken = new boolean[routes.length]; // bus already boarded?
Set<Integer> seenStop = new HashSet<>();
Queue<Integer> q = new ArrayDeque<>();
for (int r : busesAt.getOrDefault(source, List.of())) { // level 1: buses at the source
taken[r] = true;
q.add(r);
}
for (int buses = 1; !q.isEmpty(); buses++) {
for (int size = q.size(); size > 0; size--) {
int r = q.poll();
for (int s : routes[r]) { // ride bus r to every stop
if (s == target) return buses;
if (!seenStop.add(s)) continue; // this stop's buses already queued
for (int nr : busesAt.get(s))
if (!taken[nr]) { taken[nr] = true; q.add(nr); }
}
}
}
return -1;
}
Time O(T)Space O(T)
Every route is ridden once and every stop's bus list is read once, so the total work is the total length of all routes.
Pick as a node the thing whose count you minimise. Here the answer counts buses, so BFS over buses; stops are only the "glue" between them.
If source == target the answer is 0, even when no bus stops there: check it first. And mark a stop as used after reading its buses once; without that, a stop shared by many buses makes you re-read its long list again and again.
Pattern notes: Graphs & Traversal · Open on LeetCode
LC 329Longest Increasing Path in a MatrixHardDFS + memoLeetCode
In a grid of numbers you can step up, down, left or right, but only onto a strictly bigger number. How long is the longest such path (counting cells)?
Example: [[9,9,4],[6,6,8],[2,1,1]] → 4, the path 1 → 2 → 6 → 9.
"Longest path" on a grid, but only "strictly increasing" moves. Increasing moves can never loop back, so the grid is a DAG (a graph with no cycles). Longest path in a DAG = DFS with a memo, or process cells in topological order like Course Schedule II (LC 210).
1Brute force — DFS from every cell, no memory
From each cell, try all four bigger neighbours recursively and keep the longest. No visited set is needed, because you can never step back onto a smaller number.
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int longestIncreasingPath(int[][] matrix) {
int best = 0;
for (int i = 0; i < matrix.length; i++)
for (int j = 0; j < matrix[0].length; j++)
best = Math.max(best, walk(matrix, i, j)); // try every start cell
return best;
}
int walk(int[][] m, int i, int j) { // longest path starting at (i, j)
int best = 1;
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && y >= 0 && x < m.length && y < m[0].length && m[x][y] > m[i][j])
best = Math.max(best, 1 + walk(m, x, y)); // recomputed again and again
}
return best;
}
Time exponentialSpace O(m · n)
The same cell is re-solved from every path that reaches it. On a 200 × 200 grid laid out as a long zig-zag staircase, the number of paths explodes and this never finishes.
2Optimal — the same DFS with a memo
This is grid DFS from Intermediate, plus one insight: "increasing only" means no cycles, so the answer from a cell never changes. Store it in memo[i][j] the first time and reuse it. That is exactly top-down DP.
State: memo[i][j] = longest increasing path that starts at (i, j).
Recurrence: 1 + max(memo of each strictly bigger neighbour), or 1 if there is none.
Base case: a peak (no bigger neighbour) has length 1.
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int longestIncreasingPath(int[][] matrix) {
int rows = matrix.length, cols = matrix[0].length, best = 0;
int[][] memo = new int[rows][cols]; // 0 = not computed yet
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
best = Math.max(best, walk(matrix, i, j, memo));
return best;
}
int walk(int[][] m, int i, int j, int[][] memo) {
if (memo[i][j] != 0) return memo[i][j]; // already solved: reuse
int best = 1;
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && y >= 0 && x < m.length && y < m[0].length && m[x][y] > m[i][j])
best = Math.max(best, 1 + walk(m, x, y, memo));
}
return memo[i][j] = best; // save before returning
}
Time O(m · n)Space O(m · n)
Each cell is computed once and looks at 4 neighbours. The recursion can go m·n deep on a staircase grid, which Java handles at these sizes.
3Also good — peel the grid in topological order
No recursion at all: count for each cell how many smaller neighbours it has (its in-degree). Cells with 0 are "valleys" and start the queue. Peel layer by layer like Kahn's algorithm in LC 210; the number of layers is the longest path.
int longestIncreasingPath(int[][] matrix) {
int rows = matrix.length, cols = matrix[0].length;
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int[][] smaller = new int[rows][cols]; // in-degree: smaller neighbours
Queue<int[]> q = new ArrayDeque<>();
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++) {
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && y >= 0 && x < rows && y < cols && matrix[x][y] < matrix[i][j])
smaller[i][j]++;
}
if (smaller[i][j] == 0) q.add(new int[]{i, j}); // valleys start
}
int layers = 0;
while (!q.isEmpty()) { // peel one layer per round
layers++;
for (int size = q.size(); size > 0; size--) {
int[] c = q.poll();
for (int[] d : DIRS) {
int x = c[0] + d[0], y = c[1] + d[1];
if (x >= 0 && y >= 0 && x < rows && y < cols
&& matrix[x][y] > matrix[c[0]][c[1]] && --smaller[x][y] == 0)
q.add(new int[]{x, y});
}
}
}
return layers; // number of layers = longest path
}
Time O(m · n)Space O(m · n)
"Only move to strictly bigger" = no cycles = memo is safe. Longest path is hard in general graphs, but easy in a DAG: DFS + memo, or topological order.
Use strictly greater (>). With >=, two equal neighbours point at each other, the recursion loops forever and the memo is wrong. Also, don't add a visited set: it is not needed and it breaks the memo.
Pattern notes: Topological Sort · Open on LeetCode
LC 787Cheapest Flights Within K StopsMediumBellman-Ford k roundsLeetCode
There are n cities and one-way flights [from, to, price]. Find the cheapest way from src to dst that uses at most k stops in between (so at most k + 1 flights). Return -1 if there is none.
Example: flights 0→1 (100), 1→2 (100), 2→0 (100), 1→3 (600), 2→3 (200), src = 0, dst = 3, k = 1 → 700 (0 → 1 → 3). The route 0 → 1 → 2 → 3 costs only 400, but it has 2 stops.
"Cheapest price" says shortest path with weights, but "at most k stops" limits the number of edges. Plain Dijkstra (LC 743) only tracks cost and may throw away a pricier path with fewer stops. A limit on edges points to Bellman-Ford, where round i finds the best paths with at most i edges.
1Brute force — try every route up to k + 1 flights
DFS from src, taking every flight out of the current city, until the flight budget runs out. Keep the cheapest cost that reaches dst.
int best;
int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] f : flights) adj.get(f[0]).add(new int[]{f[1], f[2]});
best = Integer.MAX_VALUE;
fly(adj, src, dst, k + 1, 0); // k stops = at most k + 1 flights
return best == Integer.MAX_VALUE ? -1 : best;
}
void fly(List<List<int[]>> adj, int city, int dst, int flightsLeft, int cost) {
if (city == dst) { best = Math.min(best, cost); return; }
if (flightsLeft == 0) return;
for (int[] f : adj.get(city)) // try every flight out of this city
fly(adj, f[0], dst, flightsLeft - 1, cost + f[1]);
}
Time O(nᵏ⁺¹)Space O(n + E)
Each level can branch into up to n − 1 flights. With n = 100 and k up to 99 the number of routes is astronomical.
2Optimal — Bellman-Ford for exactly k + 1 rounds
This is shortest paths from Intermediate, plus one insight: one round of "try every flight once" = one more flight allowed. After round i, cost[c] is the cheapest price to reach c with at most i flights. So run exactly k + 1 rounds.
The key detail: in each round, read from the old array and write to a copy. Otherwise a price updated in this round could be used again in the same round, which sneaks in an extra flight.
int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
final int INF = Integer.MAX_VALUE;
int[] cost = new int[n]; // cheapest price using the rounds so far
Arrays.fill(cost, INF);
cost[src] = 0;
for (int round = 0; round <= k; round++) { // k stops = k + 1 flights = k + 1 rounds
int[] next = cost.clone(); // read OLD, write NEW
for (int[] f : flights) {
int from = f[0], to = f[1], price = f[2];
if (cost[from] != INF && cost[from] + price < next[to])
next[to] = cost[from] + price;
}
cost = next;
}
return cost[dst] == INF ? -1 : cost[dst];
}
Time O(k · E)Space O(n)
k + 1 rounds, each looking at every flight once. With E ≈ 5000 flights and k < 100 that is about half a million steps.
"At most k edges" → Bellman-Ford with k + 1 rounds and a copied array per round. Round i = best paths using at most i edges.
Forgetting clone(): updating cost in place lets 0 → 1 → 2 → 3 all happen inside one round, so the example returns 400 instead of 700 (and 400 even for k = 0, where the answer is -1). Also, k counts stops, so the number of flights (rounds) is k + 1.
Pattern notes: Shortest Paths · Open on LeetCode
LC 1631Path With Minimum EffortMediumDijkstra on gridLeetCode
A hiker walks on a grid of heights from the top-left to the bottom-right cell, moving up, down, left or right. The effort of a route is its biggest single height difference between two neighbouring cells. Find the route with the smallest effort.
Example: [[1,2,2],[3,8,2],[5,3,5]] → 2, going down the left side: 1 → 3 → 5 → 3 → 5, every step is 2.
"Minimum effort" from one corner to another on a grid with different costs per step: BFS no longer works (steps don't cost the same), so it's Dijkstra, like Network Delay Time (LC 743). The only twist is that a path's cost is a max, not a sum.
1Brute force — try every simple path
Backtrack through every path that doesn't revisit a cell, track the biggest step so far, and keep the best finish. Prune a path once it is no better than the best found.
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int best;
int minimumEffortPath(int[][] heights) {
best = Integer.MAX_VALUE;
walk(heights, 0, 0, 0, new boolean[heights.length][heights[0].length]);
return best;
}
void walk(int[][] h, int i, int j, int effort, boolean[][] onPath) {
if (effort >= best) return; // can't beat what we have
if (i == h.length - 1 && j == h[0].length - 1) { best = effort; return; }
onPath[i][j] = true;
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && y >= 0 && x < h.length && y < h[0].length && !onPath[x][y])
walk(h, x, y, Math.max(effort, Math.abs(h[x][y] - h[i][j])), onPath);
}
onPath[i][j] = false; // backtrack: free the cell
}
Time exponentialSpace O(R · C)
The number of simple paths in a grid grows exponentially; even a 10 × 10 grid is too many, and the limit is 100 × 100.
2Optimal — Dijkstra where a path costs its biggest step
This is Dijkstra from LC 743, plus one change: new cost = max(cost so far, this step) instead of cost + step. Dijkstra still works, because taking one more step can never make a path's maximum smaller (just like adding a non-negative weight can't make a sum smaller).
The heap always expands the cell with the smallest effort, so the first time the bottom-right cell comes out of the heap, its effort is final.
int minimumEffortPath(int[][] heights) {
int rows = heights.length, cols = heights[0].length;
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int[][] effort = new int[rows][cols]; // best known effort to reach a cell
for (int[] row : effort) Arrays.fill(row, Integer.MAX_VALUE);
effort[0][0] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // {effort, i, j}
pq.add(new int[]{0, 0, 0});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int e = cur[0], i = cur[1], j = cur[2];
if (e > effort[i][j]) continue; // stale entry, skip
if (i == rows - 1 && j == cols - 1) return e; // first pop of the target is final
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x < 0 || y < 0 || x >= rows || y >= cols) continue;
int ne = Math.max(e, Math.abs(heights[x][y] - heights[i][j])); // MAX, not sum
if (ne < effort[x][y]) {
effort[x][y] = ne;
pq.add(new int[]{ne, x, y});
}
}
}
return 0;
}
Time O(R·C · log(R·C))Space O(R · C)
Each cell is improved a few times at most, and every heap operation costs a log. About 10,000 cells for the biggest grid: instant.
Dijkstra works for any path cost that never goes down as the path gets longer: a sum of non-negative weights, or a maximum. Just change the line that combines the costs.
Using e + diff (sum) instead of max(e, diff) answers a different question. Also skip stale heap entries (e > effort[i][j]) and return when the target is popped, not when it is first pushed: the first push may not be the best.
Pattern notes: Shortest Paths · Open on LeetCode
LC 778Swim in Rising WaterHardDijkstra (minimax)LeetCode
An n × n grid holds the heights 0 … n²−1, each once. At time t the water level is t, and you can swim into a cell only if its height is ≤ t (swimming itself is instant). What is the earliest time you can get from the top-left to the bottom-right?
Example: [[0,2],[1,3]] → 3: the bottom-right cell itself is 3, so you must wait until t = 3.
You need a path whose highest cell is as low as possible: "minimise the maximum along a path". That is exactly LC 1631 again, only the cost sits on the cells instead of the steps. Minimax path → Dijkstra with max, or binary search on the answer + BFS.
1Brute force — try t = 0, 1, 2, … until BFS gets through
For each time t, flood-fill from the start through cells with height ≤ t. The first t that reaches the corner is the answer.
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int swimInWater(int[][] grid) {
for (int t = grid[0][0]; ; t++) // try every time, smallest first
if (canSwim(grid, t)) return t;
}
boolean canSwim(int[][] grid, int t) { // BFS through cells with height <= t
int n = grid.length;
boolean[][] seen = new boolean[n][n];
Queue<int[]> q = new ArrayDeque<>();
q.add(new int[]{0, 0});
seen[0][0] = true;
while (!q.isEmpty()) {
int[] c = q.poll();
if (c[0] == n - 1 && c[1] == n - 1) return true;
for (int[] d : DIRS) {
int x = c[0] + d[0], y = c[1] + d[1];
if (x >= 0 && y >= 0 && x < n && y < n && !seen[x][y] && grid[x][y] <= t) {
seen[x][y] = true;
q.add(new int[]{x, y});
}
}
}
return false;
}
Time O(n⁴)Space O(n²)
Up to n² values of t, each a BFS over n² cells. With n ≤ 50 that is about 6 million steps, so it actually passes here; but it does not scale (n = 1000 would be 10¹² steps), and interviewers expect better.
2Optimal — Dijkstra on "highest cell so far"
This is the Dijkstra from LC 1631, with the key time to reach a cell = max(time so far, height of that cell). Always expand the cell you can reach the earliest; the moment the corner comes out of the heap, that time is the answer.
int swimInWater(int[][] grid) {
int n = grid.length;
int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
boolean[][] done = new boolean[n][n];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // {time, i, j}
pq.add(new int[]{grid[0][0], 0, 0});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int t = cur[0], i = cur[1], j = cur[2];
if (done[i][j]) continue;
done[i][j] = true;
if (i == n - 1 && j == n - 1) return t;
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && y >= 0 && x < n && y < n && !done[x][y])
pq.add(new int[]{Math.max(t, grid[x][y]), x, y}); // wait for the higher cell
}
}
return -1;
}
Time O(n² log n)Space O(n²)
n² cells, each pushed a few times into a heap of size O(n²); log(n²) = 2·log n.
3Also good — binary search on t
If you can finish at time t, you can finish at any later time too. That "no, no, …, no, yes, yes" shape is binary search on the answer (like Koko, LC 875): search t in [grid[0][0], n²−1] and test each guess with the BFS from step 1.
int swimInWater(int[][] grid) {
int n = grid.length;
int lo = grid[0][0], hi = n * n - 1; // answer is somewhere in here
while (lo < hi) {
int mid = (lo + hi) / 2;
if (canSwim(grid, mid)) hi = mid; // works: try smaller
else lo = mid + 1; // too early: need more water
}
return lo;
}
// canSwim(grid, t) is the same BFS as in step 1
Time O(n² log n)Space O(n²)
"Lowest possible maximum along a path" has two standard answers: Dijkstra with max, or binary search the answer + a yes/no BFS. (Union-find, adding cells from low to high until the corners connect, is a third.)
The start cell counts too: begin with time grid[0][0], not 0. And the corner's own height matters: in [[0,2],[1,3]] the answer is 3 even though you could reach its neighbours at t = 1.
Pattern notes: Shortest Paths · Open on LeetCode
LC 1584Min Cost to Connect All PointsMediumMinimum spanning treeLeetCode
You get points on a plane. Connecting two points costs their Manhattan distance |x1 − x2| + |y1 − y2|. Connect all points (every point reachable from every other) for the least total cost.
Example: [[0,0],[2,2],[3,10],[5,2],[7,0]] → 20.
"Connect all points" with the "minimum total cost", and no start or end point: that is a minimum spanning tree (MST). Here every pair of points is an edge, so the graph is dense (about n²/2 edges), which favours Prim.
1Straightforward — Kruskal on all n² edges
The truly naive idea, trying every possible tree, is hopeless (there are nⁿ⁻² of them). The first real solution is Kruskal: list every pair as an edge, sort by cost, and take an edge whenever it joins two different groups (union-find, as in Redundant Connection, LC 684).
int minCostConnectPoints(int[][] points) {
int n = points.length;
List<int[]> edges = new ArrayList<>(); // {cost, i, j} for EVERY pair
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
edges.add(new int[]{dist(points, i, j), i, j});
edges.sort((a, b) -> a[0] - b[0]); // cheapest first
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
int total = 0, used = 0;
for (int[] e : edges) {
int a = find(parent, e[1]), b = find(parent, e[2]);
if (a == b) continue; // already connected: would make a loop
parent[a] = b;
total += e[0];
if (++used == n - 1) break; // a tree has n - 1 edges
}
return total;
}
int find(int[] parent, int x) {
while (parent[x] != x) x = parent[x] = parent[parent[x]]; // path halving
return x;
}
int dist(int[][] p, int i, int j) {
return Math.abs(p[i][0] - p[j][0]) + Math.abs(p[i][1] - p[j][1]);
}
Time O(n² log n)Space O(n²)
Correct and it passes for n = 1000, but it builds and sorts about 500,000 edges. We can avoid storing edges at all.
2Optimal — Prim with a plain array
Grow one tree from point 0. Keep cheapest[v] = the cheapest link from the tree to v. Each round, add the outside point with the smallest link, then let it offer cheaper links to the rest. This is the greedy "always take the cheapest safe edge" idea; the extra insight for dense graphs is to skip the heap: a linear scan for the minimum costs O(n), and there are no stored edges.
int minCostConnectPoints(int[][] points) {
int n = points.length, total = 0;
boolean[] inTree = new boolean[n];
int[] cheapest = new int[n]; // cheapest link from the tree to i
Arrays.fill(cheapest, Integer.MAX_VALUE);
cheapest[0] = 0; // start the tree at point 0
for (int round = 0; round < n; round++) {
int u = -1;
for (int i = 0; i < n; i++) // closest point not yet in the tree
if (!inTree[i] && (u == -1 || cheapest[i] < cheapest[u])) u = i;
inTree[u] = true;
total += cheapest[u];
for (int v = 0; v < n; v++) { // u joined: maybe v is closer now
if (inTree[v]) continue;
int d = Math.abs(points[u][0] - points[v][0]) + Math.abs(points[u][1] - points[v][1]);
if (d < cheapest[v]) cheapest[v] = d;
}
}
return total;
}
Time O(n²)Space O(n)
n rounds, each with two O(n) scans: about 2 million steps for n = 1000, with no edge list and no sorting.
MST: Kruskal (sort edges + union-find) when edges are given and few; Prim when the graph is dense or complete. For a complete graph, array Prim in O(n²) beats everything.
A spanning tree has exactly n − 1 edges; stop there, and never take an edge between two points already in the same group (that makes a loop). The cost is Manhattan distance, not straight-line distance.
Pattern notes: Topological Sort, MST & More · Open on LeetCode
LC 332Reconstruct ItineraryHardEulerian path (Hierholzer)LeetCode
You have a pile of plane tickets [from, to]. Use every ticket exactly once in one trip starting from "JFK". If several trips work, return the one that is smallest in dictionary order. A valid trip always exists.
Example: [[JFK,KUL],[JFK,NRT],[NRT,JFK]] → [JFK, NRT, JFK, KUL]. Going to KUL first looks smaller, but then you are stuck with tickets left.
"Use all the tickets (edges) exactly once" is an Eulerian path: a walk that covers every edge once. It is not a "visit every node" problem. Eulerian path has a neat linear-time algorithm, Hierholzer's.
1Brute force — backtracking in sorted order
Sort each airport's destinations. DFS, always trying the smallest unused ticket first; if you get stuck with tickets left, undo and try the next one. The first complete trip is the answer.
List<String> findItinerary(List<List<String>> tickets) {
Map<String, List<String>> adj = new HashMap<>();
for (List<String> t : tickets)
adj.computeIfAbsent(t.get(0), k -> new ArrayList<>()).add(t.get(1));
for (List<String> dests : adj.values()) Collections.sort(dests); // try smallest first
List<String> route = new ArrayList<>(List.of("JFK"));
fly("JFK", tickets.size(), adj, route);
return route;
}
boolean fly(String from, int left, Map<String, List<String>> adj, List<String> route) {
if (left == 0) return true; // every ticket used: done
List<String> dests = adj.getOrDefault(from, new ArrayList<>());
for (int i = 0; i < dests.size(); i++) {
String to = dests.get(i);
if (to == null) continue; // ticket already used
dests.set(i, null); // use it
route.add(to);
if (fly(to, left - 1, adj, route)) return true;
route.remove(route.size() - 1); // dead end: undo and try the next
dests.set(i, to);
}
return false;
}
Time exponential (worst case)Space O(E)
Usually fast, but with up to 300 tickets and many dead ends (lots of tickets between the same few airports), the undo-and-retry can blow up.
2Optimal — Hierholzer: add airports when you get stuck
This is DFS from Intermediate, plus one insight: write an airport into the route only when you are stuck there, and write it at the front (post-order, then reverse). A dead end like KUL gets stuck first, so it ends up last in the route, and the loop JFK → NRT → JFK that was skipped gets spliced in before it. No undoing is ever needed.
A min-heap per airport makes sure we always take the smallest destination first, which gives the smallest trip.
List<String> findItinerary(List<List<String>> tickets) {
Map<String, PriorityQueue<String>> adj = new HashMap<>(); // smallest destination on top
for (List<String> t : tickets)
adj.computeIfAbsent(t.get(0), k -> new PriorityQueue<>()).add(t.get(1));
LinkedList<String> route = new LinkedList<>();
visit("JFK", adj, route);
return route;
}
void visit(String from, Map<String, PriorityQueue<String>> adj, LinkedList<String> route) {
PriorityQueue<String> dests = adj.get(from);
while (dests != null && !dests.isEmpty())
visit(dests.poll(), adj, route); // use the ticket, keep flying
route.addFirst(from); // stuck here: prepend (post-order)
}
Time O(E log E)Space O(E)
Each ticket is pushed and polled once from a heap. The log comes from keeping destinations in order.
"Use every edge exactly once" → Hierholzer: DFS that consumes edges, and adds a node after its edges are used up, then reverse (or add to the front).
Adding the airport before the loop (pre-order) gives [JFK, KUL, NRT, JFK] for the example, which is not a real trip. And poll the ticket (remove it) as you use it; just reading it lets you fly the same ticket forever.
Pattern notes: Graphs & Traversal · Open on LeetCode
LC 1192Critical Connections in a NetworkHardTarjan bridgesLeetCode
n servers are joined by two-way cables, and every server can reach every other. A cable is critical if cutting it would split the network. Return all critical cables (in any order).
Example: n = 4, cables [[0,1],[1,2],[2,0],[1,3]] → [[1,3]]. Cables 0-1, 1-2, 2-0 form a triangle, so any one of them can be cut safely.
"Which edges, if removed, disconnect the graph" is the textbook definition of a bridge. An edge is a bridge exactly when it is not on any cycle. Finding them all in one DFS is Tarjan's low-link trick.
1Brute force — cut each cable and check
For every cable: pretend it is gone, run a DFS from server 0, and count how many servers you reach. Fewer than n means the cable was critical.
List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) {
List<List<Integer>> result = new ArrayList<>();
for (int skip = 0; skip < connections.size(); skip++) // cut one cable at a time
if (reachable(n, connections, skip) < n)
result.add(connections.get(skip));
return result;
}
int reachable(int n, List<List<Integer>> connections, int skip) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int e = 0; e < connections.size(); e++) {
if (e == skip) continue;
int a = connections.get(e).get(0), b = connections.get(e).get(1);
adj.get(a).add(b);
adj.get(b).add(a);
}
boolean[] seen = new boolean[n];
Deque<Integer> stack = new ArrayDeque<>(List.of(0));
seen[0] = true;
int count = 1;
while (!stack.isEmpty())
for (int v : adj.get(stack.pop()))
if (!seen[v]) { seen[v] = true; count++; stack.push(v); }
return count;
}
Time O(E · (V + E))Space O(V + E)
One full DFS per cable. With 10⁵ servers and 10⁵ cables that is about 10¹⁰ steps.
2Optimal — one DFS with discovery times and low-links
This is DFS from Intermediate, plus two numbers per node. disc[u] is when DFS first visited u. low[u] is the earliest visit time that u, or anything below it in the DFS, can reach using one "shortcut" edge back up. Then tree edge u–v is a bridge exactly when low[v] > disc[u]: nothing under v can climb back to u or above without that edge.
int[] disc, low; // disc = visit time, low = earliest time reachable
int timer = 0;
List<List<Integer>> bridges = new ArrayList<>();
List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (List<Integer> c : connections) {
adj.get(c.get(0)).add(c.get(1));
adj.get(c.get(1)).add(c.get(0));
}
disc = new int[n];
low = new int[n];
Arrays.fill(disc, -1); // -1 = not visited
dfs(0, -1, adj);
return bridges;
}
void dfs(int u, int parent, List<List<Integer>> adj) {
disc[u] = low[u] = timer++;
for (int v : adj.get(u)) {
if (v == parent) continue; // don't walk the same cable back
if (disc[v] == -1) { // tree edge: go deeper
dfs(v, u, adj);
low[u] = Math.min(low[u], low[v]);
if (low[v] > disc[u]) // v's subtree can't get above u
bridges.add(List.of(u, v));
} else {
low[u] = Math.min(low[u], disc[v]); // back edge: a shortcut up
}
}
}
Time O(V + E)Space O(V + E)
A single DFS: each node and each cable is looked at a constant number of times.
Bridge test: low[v] > disc[u]. Articulation point (critical node) test is the same with >=, plus a special rule for the root. Both come from one DFS that tracks disc and low.
Skip only the edge back to the parent; otherwise every edge looks like a cycle and no bridge is found. For a back edge, use disc[v] (not low[v]) to update low[u]. And the test is strictly greater: >= would wrongly flag edges on cycles.
Pattern notes: Topological Sort, MST & More · Open on LeetCode
LC 847Shortest Path Visiting All NodesHardBFS over (node, bitmask)LeetCode
An undirected, connected graph has n ≤ 12 nodes. Find the length of the shortest walk that visits every node. You may start and end anywhere, and you may reuse nodes and edges.
Example: graph = [[1,2,3],[0],[0],[0]] (node 0 in the middle of a star) → 4, e.g. 1 → 0 → 2 → 0 → 3.
"Shortest" with equal-cost edges says BFS, but "visit all nodes" means the same node can be worth revisiting, so "which node am I on" is not enough. The tiny limit n ≤ 12 is the giveaway: 2¹² = 4096, so you can afford to remember which nodes are visited as a bitmask.
1Brute force — try every visiting order
Compute the BFS distance between every pair of nodes. Then try every order of visiting the nodes and add up the distances between consecutive ones; keep the smallest total.
int best;
int shortestPathLength(int[][] graph) {
int n = graph.length;
int[][] d = new int[n][n]; // d[a][b] = BFS distance a -> b
for (int s = 0; s < n; s++) d[s] = bfs(graph, s);
best = Integer.MAX_VALUE;
for (int s = 0; s < n; s++) // try every visiting order
tryOrders(d, s, 1 << s, 0);
return best;
}
void tryOrders(int[][] d, int last, int visited, int cost) {
int n = d.length;
if (visited == (1 << n) - 1) { best = Math.min(best, cost); return; }
for (int next = 0; next < n; next++)
if ((visited & (1 << next)) == 0)
tryOrders(d, next, visited | (1 << next), cost + d[last][next]);
}
int[] bfs(int[][] graph, int s) {
int[] dist = new int[graph.length];
Arrays.fill(dist, -1);
dist[s] = 0;
Queue<Integer> q = new ArrayDeque<>(List.of(s));
while (!q.isEmpty()) {
int u = q.poll();
for (int v : graph[u])
if (dist[v] == -1) { dist[v] = dist[u] + 1; q.add(v); }
}
return dist;
}
Time O(n! · n)Space O(n²)
For n = 12 there are 12! ≈ 479 million orders. Too slow already, and n = 13 would be 13 times worse.
2Optimal — BFS where a state is (node, visited mask)
This is multi-source BFS (Rotting Oranges, LC 994), plus one insight: make the BFS node a pair (where I am, which nodes I have seen). The "seen" set is an int whose bit i means "node i visited". Start from every node at once with only its own bit set. The first state whose mask is all ones gives the answer.
Walking back through node 0 is fine: (0, 0011) and (0, 0111) are different states, so BFS doesn't block the revisit, but it never repeats the same state.
int shortestPathLength(int[][] graph) {
int n = graph.length, full = (1 << n) - 1;
boolean[][] seen = new boolean[n][1 << n]; // seen[node][mask]
Queue<int[]> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) { // start everywhere at once
q.add(new int[]{i, 1 << i});
seen[i][1 << i] = true;
}
for (int steps = 0; !q.isEmpty(); steps++) {
for (int size = q.size(); size > 0; size--) {
int[] cur = q.poll();
int node = cur[0], mask = cur[1];
if (mask == full) return steps; // everyone visited
for (int next : graph[node]) {
int nm = mask | (1 << next); // mark next as visited
if (!seen[next][nm]) {
seen[next][nm] = true;
q.add(new int[]{next, nm});
}
}
}
}
return -1;
}
Time O(2ⁿ · n²)Space O(2ⁿ · n)
There are n · 2ⁿ states (about 49,000 for n = 12), and each tries at most n neighbours: well under a million steps.
When "where you are" isn't enough to decide the future, add the missing info to the state and BFS over states. Small n (≤ 20) plus "visit all" → the extra info is a bitmask.
A visited array per node is wrong here: the star needs node 0 three times. Mark seen[node][mask] instead. And n = 1 must return 0: the start state is already "all visited".
Pattern notes: DP Patterns · Open on LeetCode
Backtracking with Pruning
This is the choose / explore / un-choose template from Intermediate (LC 46, LC 79, LC 131), but the search trees are now huge, so the whole game is pruning: cut a branch the moment it can no longer lead to an answer, and never solve the same sub-problem twice. Template: keep small lookup tables (used columns, used digits, how many brackets must still go) so "is this choice legal?" costs O(1), check before you recurse, and add a memo when different paths reach the same state. What is harder than Intermediate: the brute force is not "a bit slow", it is 99 or 951 or 225 steps, so the pruning rule is the solution.
LC 51N-QueensHardBacktracking + O(1) checksLeetCode
Place n queens on an n × n chess board so that no two attack each other: no two in the same row, the same column or the same diagonal. Return every such board, drawn as strings with 'Q' and '.'.
Example: n = 4 → 2 boards: [".Q..","...Q","Q...","..Q."] and ["..Q.","Q...","...Q",".Q.."]. n = 1 → [["Q"]].
"Return all placements" and "no two may attack" means: build the answer piece by piece and throw away a partial board as soon as it breaks a rule. That is backtracking. The rule "one per row" also tells you the shape of the tree: level r of the recursion chooses the column for row r.
1Brute force — every column for every row, check at the end
Put exactly one queen in each row (that rule is free), try every column for each row, and only when all n rows are filled check whether any two queens clash.
List<List<String>> solveNQueens(int n) {
List<List<String>> res = new ArrayList<>();
place(0, new int[n], n, res); // q[r] = column of the queen in row r
return res;
}
void place(int row, int[] q, int n, List<List<String>> res) {
if (row == n) { // board full: only now check it
if (valid(q)) res.add(build(q));
return;
}
for (int c = 0; c < n; c++) { // no checking on the way down
q[row] = c;
place(row + 1, q, n, res);
}
}
boolean valid(int[] q) { // any two queens in a column or diagonal?
for (int a = 0; a < q.length; a++)
for (int b = a + 1; b < q.length; b++)
if (q[a] == q[b] || Math.abs(q[a] - q[b]) == b - a) return false;
return true;
}
List<String> build(int[] q) { // turn columns into "..Q." strings
List<String> board = new ArrayList<>();
for (int c : q) {
char[] row = new char[q.length];
Arrays.fill(row, '.');
row[c] = 'Q';
board.add(new String(row));
}
return board;
}
Time O(nⁿ · n²)Space O(n)
At the limit n = 9 that is 9⁹ ≈ 387 million full boards, each checked with 36 pair tests: over 10 billion steps. Almost all of those boards were already broken after two rows.
2Optimal — check each queen as you place it, in O(1)
This is the Intermediate backtracking template (LC 46 Permutations: "pick an unused item for this slot") plus one extra insight: every diagonal has a fixed number: r − c on one direction, r + c on the other. Keep three boolean arrays (col, diag, anti) and a square is safe if all three are free. Check before recursing, so a bad partial board is never extended.
List<List<String>> solveNQueens(int n) {
List<List<String>> res = new ArrayList<>();
boolean[] col = new boolean[n];
boolean[] diag = new boolean[2 * n]; // index r - c + n (shifted, never negative)
boolean[] anti = new boolean[2 * n]; // index r + c
dfs(0, n, new int[n], col, diag, anti, res);
return res;
}
void dfs(int r, int n, int[] q, boolean[] col, boolean[] diag, boolean[] anti,
List<List<String>> res) {
if (r == n) { res.add(build(q)); return; } // every row has a safe queen
for (int c = 0; c < n; c++) {
if (col[c] || diag[r - c + n] || anti[r + c]) continue; // prune: attacked
q[r] = c;
col[c] = diag[r - c + n] = anti[r + c] = true; // choose
dfs(r + 1, n, q, col, diag, anti, res); // explore
col[c] = diag[r - c + n] = anti[r + c] = false; // un-choose
}
}
// build(q) is the same helper as in step 1
Time O(n!)Space O(n)
Row 0 has n choices, row 1 at most n − 1 safe ones, and so on, so the tree is at most n! leaves, and in practice far fewer. The three arrays turn "is this square attacked?" from a loop into three lookups. Checked for n = 1 to 9: 1, 0, 0, 2, 10, 4, 40, 92, 352 boards.
Backtracking gets fast when the "is this choice legal?" test is O(1) and done before recursing. For grids, label diagonals with r − c and r + c; that trick comes back in many board problems.
r − c can be negative (row 0, column 3 gives −3), so shift it: diag[r − c + n] with an array of size 2n. And undo all three marks on the way back, or later branches see squares as attacked that are not.
Pattern notes: Recursion & Backtracking · Open on LeetCode
LC 37Sudoku SolverHardBacktracking + O(1) checksLeetCode
Fill the empty cells ('.') of a 9 × 9 Sudoku in place so that every row, every column and every 3 × 3 box contains the digits 1 to 9 exactly once. The puzzle has exactly one solution.
Example: the LeetCode board starting with row "53..7...." becomes the solved board starting with "534678912".
"Fill the cells so that rules hold" with no clever formula is a constraint puzzle: guess, check, and undo a guess when it leads nowhere. It is the same shape as N-Queens (LC 51): one decision per empty cell, and three kinds of conflict (row, column, box) to test. You already know the row/column/box bookkeeping from Intermediate LC 36 Valid Sudoku.
1Brute force — fill everything, check at the end
Walk the 81 cells in order. For each empty cell try every digit 1 to 9, without looking at the rules. Only when the last cell is filled, check the whole board.
void solveSudoku(char[][] board) { fill(board, 0); }
boolean fill(char[][] b, int pos) { // pos = 0..80, row = pos / 9, col = pos % 9
if (pos == 81) return allValid(b); // only now look at the rules
int r = pos / 9, c = pos % 9;
if (b[r][c] != '.') return fill(b, pos + 1);
for (char d = '1'; d <= '9'; d++) {
b[r][c] = d;
if (fill(b, pos + 1)) return true;
}
b[r][c] = '.';
return false;
}
boolean allValid(char[][] b) { // LC 36 on a full board
for (int i = 0; i < 9; i++) {
boolean[] row = new boolean[10], col = new boolean[10], box = new boolean[10];
for (int j = 0; j < 9; j++) {
int x = b[i][j] - '0', y = b[j][i] - '0';
int z = b[i / 3 * 3 + j / 3][i % 3 * 3 + j % 3] - '0';
if (row[x] || col[y] || box[z]) return false;
row[x] = col[y] = box[z] = true;
}
}
return true;
}
Time O(9ᵐ · 81)Space O(m)
With m empty cells there are 9m fillings. The LeetCode example has 51 blanks: 951 is about 1048, which would never finish. It only works on a board with 4 or 5 blanks.
2Optimal — only try digits that are still legal
Same trick as N-Queens: keep row[r][d], col[c][d], box[k][d] tables (box number k = r / 3 * 3 + c / 3, from LC 36). Before placing a digit, ask the three tables; if any says "used", skip the digit. A wrong guess now fails at the very next cell it breaks, not 50 cells later. When a cell has no legal digit, return false and the caller tries its next digit.
boolean[][] row = new boolean[9][10], col = new boolean[9][10], box = new boolean[9][10];
void solveSudoku(char[][] board) {
for (int r = 0; r < 9; r++) // record the given digits
for (int c = 0; c < 9; c++)
if (board[r][c] != '.') {
int d = board[r][c] - '0';
row[r][d] = col[c][d] = box[r / 3 * 3 + c / 3][d] = true;
}
solve(board, 0);
}
boolean solve(char[][] b, int pos) {
if (pos == 81) return true; // every cell filled legally
int r = pos / 9, c = pos % 9, k = r / 3 * 3 + c / 3;
if (b[r][c] != '.') return solve(b, pos + 1); // given digit: move on
for (int d = 1; d <= 9; d++) {
if (row[r][d] || col[c][d] || box[k][d]) continue; // prune now
b[r][c] = (char) ('0' + d);
row[r][d] = col[c][d] = box[k][d] = true; // choose
if (solve(b, pos + 1)) return true; // solved: stop
row[r][d] = col[c][d] = box[k][d] = false; // un-choose
b[r][c] = '.';
}
return false; // no digit fits: caller backs up
}
Time O(9ᵐ) worst case, fast in practiceSpace O(81)
The worst-case bound does not change on paper, but pruning cuts almost every branch after one or two cells, so a real puzzle solves in milliseconds. Returning true all the way up stops the search at the first solution and leaves the board filled.
"Search that stops at the first answer": make the recursion return boolean and bubble true up. Keep one "used" table per rule so every legality test is O(1). A known speed-up: fill the empty cell with the fewest legal digits first.
Record the given digits in the tables before you start, or the solver will happily place a second 5 in row 0. And when you undo a guess, reset the cell to '.' as well as the three tables.
Pattern notes: Recursion & Backtracking · Open on LeetCode
LC 140Word Break IIHardBacktracking + memoLeetCode
You get a string with no spaces and a dictionary of words. Add spaces to turn the string into a sentence where every piece is a dictionary word. Return all such sentences (words may be reused).
Example: s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"] → ["cats and dog", "cat sand dog"]. s = "catsandog" with the same kind of dictionary → [].
"Return all ways to split" means backtracking over "which word comes next". The hint for the memo: the rest of the string from index i has the same sentences no matter how you got to i ("cat|sand" and "cats|and" both land on "dog"). Same sub-problem, reached many times: remember it. It is Intermediate LC 139 Word Break, but returning the sentences instead of true/false.
1Brute force — try every dictionary prefix, recurse
From position start, try every end: if s[start..end) is a word, add it to the path and solve the rest. When start reaches the end, the path is a sentence.
List<String> wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
List<String> res = new ArrayList<>();
dfs(s, 0, dict, new ArrayList<>(), res);
return res;
}
void dfs(String s, int start, Set<String> dict, List<String> path, List<String> res) {
if (start == s.length()) { res.add(String.join(" ", path)); return; }
for (int end = start + 1; end <= s.length(); end++) {
String word = s.substring(start, end);
if (!dict.contains(word)) continue; // this prefix is not a word
path.add(word); // choose
dfs(s, end, dict, path, res); // explore the rest
path.remove(path.size() - 1); // un-choose
}
}
Time O(2ⁿ · n)Space O(n)
At LeetCode's limit of 20 letters this still passes (219 ≈ 500,000 splits). But it re-explores the same tail again and again, and dead ends are the worst case: "aaa…ab" (30 a's) with ["a","aa","aaa","aaaa"] took about 30 seconds to find zero sentences.
2Optimal — memo: start index → all sentences of the tail
Backtracking from step 1, plus one extra insight: make the function return the sentences for s[start..] instead of pushing them into a global list. Then the result only depends on start, so store it in a map and reuse it the next time any path reaches the same start. An empty list is stored too: "no sentence from here" is the most valuable thing to remember.
Map<Integer, List<String>> memo = new HashMap<>();
List<String> wordBreak(String s, List<String> wordDict) {
return build(s, 0, new HashSet<>(wordDict));
}
// every sentence that spells s[start..]
List<String> build(String s, int start, Set<String> dict) {
if (memo.containsKey(start)) return memo.get(start); // solved before
List<String> out = new ArrayList<>();
if (start == s.length()) {
out.add(""); // one way: the empty sentence
return out;
}
for (int end = start + 1; end <= s.length(); end++) {
String word = s.substring(start, end);
if (!dict.contains(word)) continue;
for (String rest : build(s, end, dict)) { // sentences for the tail
out.add(rest.isEmpty() ? word : word + " " + rest);
}
}
memo.put(start, out); // remember, even if empty
return out;
}
Time O(n³ + total answer size)Space O(total answer size)
Each start index is solved once, with n substring checks, so the search itself is n² substring checks of up to n letters each. On top of that you pay for copying the sentences, which can be exponential if the answer really has that many (no algorithm can avoid writing the output). The dead-end input above now takes under a millisecond.
Backtracking + memo works when the recursion can return its result and that result depends only on a small key (here: the start index). Then "all sentences" is just LC 139's DP carrying lists instead of booleans.
The base case must return a list with one empty string, not an empty list: an empty list means "no way", and every sentence would vanish. Then join with care, so you don't get a trailing space like "cat sand dog ".
Pattern notes: Recursion & Backtracking · Open on LeetCode
LC 301Remove Invalid ParenthesesHardBacktracking + countingLeetCode
A string has letters and brackets ( ). Remove the fewest brackets so that the brackets are balanced. Return every different string you can get that way (any order).
Example: "()())()" → ["(())()", "()()()"]. "(a)())()" → ["(a())()", "(a)()()"]. ")(" → [""].
"Remove the minimum number" plus "return all results" means: first work out how many must go (a counting pass, like Beginner LC 20 Valid Parentheses), then backtrack over which ones, allowed to delete exactly that many. The count is the pruning rule.
1Brute force — try every subset of characters
Every answer is a subsequence of s. So try all 2n ways to keep or drop each character, keep the valid ones, and among them keep only the longest.
List<String> removeInvalidParentheses(String s) {
int n = s.length(), bestLen = -1;
Set<String> best = new HashSet<>();
for (int mask = 0; mask < (1 << n); mask++) { // bit i = keep s[i]
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) if ((mask >> i & 1) == 1) sb.append(s.charAt(i));
String t = sb.toString();
if (!isValid(t) || t.length() < bestLen) continue;
if (t.length() > bestLen) { bestLen = t.length(); best.clear(); }
best.add(t);
}
return new ArrayList<>(best);
}
boolean isValid(String t) {
int open = 0;
for (char c : t.toCharArray()) {
if (c == '(') open++;
else if (c == ')' && --open < 0) return false; // ')' with nothing open
}
return open == 0;
}
Time O(2ⁿ · n)Space O(n)
At the limit of 25 characters that is 225 ≈ 33 million subsets, each built and checked in 25 steps: close to a billion operations, a time-out. Most of those subsets delete far more than needed.
2Optimal — count what must go, then DFS with that budget
Intermediate LC 22 Generate Parentheses taught "track the open count, never let it go negative". Add one extra insight: one scan tells you exactly how many '(' (left) and ')' (right) must be removed. A ')' that arrives with nothing open must go; the '(' still open at the end must go. Then DFS: at each bracket, either drop it (if that kind still has budget) or keep it (if the open count stays ≥ 0). A Set removes duplicate strings.
List<String> removeInvalidParentheses(String s) {
int left = 0, right = 0; // how many '(' and ')' MUST go
for (char c : s.toCharArray()) {
if (c == '(') left++;
else if (c == ')') {
if (left > 0) left--; // matches an earlier '('
else right++; // nothing to match: extra ')'
}
}
Set<String> res = new HashSet<>(); // removes duplicate answers
dfs(s, 0, left, right, 0, new StringBuilder(), res);
return new ArrayList<>(res);
}
void dfs(String s, int i, int left, int right, int open, StringBuilder sb, Set<String> res) {
if (left < 0 || right < 0 || open < 0) return; // prune: over budget or invalid
if (i == s.length()) {
if (left == 0 && right == 0 && open == 0) res.add(sb.toString());
return;
}
char c = s.charAt(i);
int len = sb.length();
if (c == '(') dfs(s, i + 1, left - 1, right, open, sb, res); // drop this '('
if (c == ')') dfs(s, i + 1, left, right - 1, open, sb, res); // drop this ')'
sb.append(c); // keep c
if (c == '(') dfs(s, i + 1, left, right, open + 1, sb, res);
else if (c == ')') dfs(s, i + 1, left, right, open - 1, sb, res);
else dfs(s, i + 1, left, right, open, sb, res); // letters: always keep
sb.setLength(len); // un-choose
}
Time O(2ᵖ · n) worst, far less in practiceSpace O(n)
p is the number of brackets (at most 20). The budget means the DFS only ever builds strings with the minimum number of removals, and the open < 0 check kills invalid prefixes early. Cross-checked against step 1 on 5,000 random strings.
"Fewest removals, list all results": compute the minimum first with a greedy scan, then backtrack with that exact budget. The alternative is BFS by number of removals: level 0 is s, level 1 is every string with one bracket removed, and you stop at the first level that has a valid string.
Duplicates: removing the ")" at index 3 or at index 4 of "()())()" gives the same string, so collect into a Set (or skip a bracket equal to the one before it). And ")(" must return [""], a list holding the empty string, not an empty list.
Pattern notes: Recursion & Backtracking · Open on LeetCode
DP on Strings
DP on strings is the table you built for LCS (LC 1143) in Intermediate, now used for Hard questions. One template covers this whole section: two strings → dp[i][j] = the answer for the prefixes s[0..i) and t[0..j). Make an (m+1) × (n+1) table. Row 0 and column 0 stand for an empty prefix (the base cases). Each cell looks only at the last letters s[i−1] and t[j−1] and reads cells that are already filled: diagonal dp[i−1][j−1] (both last letters used up), up dp[i−1][j] (s's last letter dropped or used), left dp[i][j−1] (t's last letter dropped or used). Fill row by row; the answer sits at dp[m][n]. What is harder than Intermediate: the base row and column are no longer all zeros, a * in a pattern turns into a move that can repeat (up again and again), and LC 32 has only one string, so the same "prefix that ends here" idea becomes a 1D dp[i]. In every diagram, yellow is the base, blue are the cells being read and green is the cell being computed.
LC 72Edit DistanceMediumTwo-string DPLeetCode
Turn word1 into word2. In one step you may insert a letter, delete a letter or replace a letter. What is the fewest steps?
Example: word1 = "horse", word2 = "ros" → 3: horse → rorse (replace h with r) → rose (delete r) → ros (delete e).
Two strings and "minimum number of operations to convert one into the other", where each operation touches one letter. That is the template: the question for the whole strings shrinks to the same question for shorter prefixes. Three operations means three neighbours per cell.
1Brute force — decide the last letter, recursively
Look at the last letters of both prefixes. If they are equal, they cost nothing: drop both. If not, try all three operations on the last letter and keep the cheapest. This is already the template's recurrence, just without a table.
int minDistance(String word1, String word2) {
return solve(word1, word2, word1.length(), word2.length());
}
// fewest edits to turn a[0..i) into b[0..j)
int solve(String a, String b, int i, int j) {
if (i == 0) return j; // insert all j letters
if (j == 0) return i; // delete all i letters
if (a.charAt(i - 1) == b.charAt(j - 1))
return solve(a, b, i - 1, j - 1); // last letters equal: free
return 1 + Math.min(solve(a, b, i - 1, j - 1), // replace a's last letter
Math.min(solve(a, b, i - 1, j), // delete a's last letter
solve(a, b, i, j - 1))); // insert b's last letter
}
Time O(3^(m+n))Space O(m + n)
Every mismatch branches three ways. Two 12-letter words with no letter in common already take about 377 million calls; LeetCode allows 500 letters. Yet there are only (m+1)(n+1) different (i, j) pairs.
2Optimal — fill the (m+1) × (n+1) table
This is LCS (LC 1143) from Intermediate with a different question in each cell. The extra insight: every operation is exactly one neighbour of the cell. Replace = diagonal, delete = up, insert = left, each costing 1. Equal last letters are free and just copy the diagonal.
State: dp[i][j] = fewest steps to turn word1[0..i) into word2[0..j).
Recurrence: letters equal: dp[i][j] = dp[i−1][j−1]; otherwise dp[i][j] = 1 + min(dp[i−1][j−1], dp[i−1][j], dp[i][j−1]).
Base case: dp[i][0] = i (delete all i letters), dp[0][j] = j (insert all j letters).
int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1]; // dp[i][j]: word1[0..i) -> word2[0..j)
for (int i = 0; i <= m; i++) dp[i][0] = i; // delete everything
for (int j = 0; j <= n; j++) dp[0][j] = j; // insert everything
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // free: diagonal
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], // replace: diagonal
Math.min(dp[i - 1][j], // delete: up
dp[i][j - 1])); // insert: left
}
}
}
return dp[m][n];
}
Time O(m · n)Space O(m · n)
Each cell is O(1) work. A row only reads the row above it, so two rows of length n + 1 are enough if memory matters.
"Cheapest way to turn one string into another" → dp[i][j] over prefixes, where each allowed operation is one neighbour: diagonal, up or left.
The base row and column are not zeros: dp[i][0] = i and dp[0][j] = j. With zeros, ("abc", "") returns 0 instead of 3. And when the letters are equal, don't add 1.
Pattern notes: DP Patterns · Open on LeetCode
LC 97Interleaving StringMediumTwo-string DPLeetCode
Can s3 be made by shuffling s1 and s2 together, like riffling two card decks, where each string keeps its own letter order?
Example: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac" → true. With s3 = "aadbbbaccc" → false.
"Interleaving" of two strings with order kept. When the next letter of s3 could come from either string, you can't decide greedily; you must try both, and the same (letters used from s1, letters used from s2) situation comes back over and over. Two strings to walk through → dp[i][j].
1Brute force — where did the last letter come from?
The last letter of s3[0..i+j) was taken either from the end of s1[0..i) or from the end of s2[0..j). Try both, recursively.
boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) return false;
return can(s1, s2, s3, s1.length(), s2.length());
}
// can s3[0..i+j) be built from s1[0..i) and s2[0..j)?
boolean can(String a, String b, String c, int i, int j) {
if (i == 0 && j == 0) return true; // nothing left to build
char last = c.charAt(i + j - 1); // last letter of s3's prefix
if (i > 0 && a.charAt(i - 1) == last && can(a, b, c, i - 1, j))
return true; // it came from s1
return j > 0 && b.charAt(j - 1) == last && can(a, b, c, i, j - 1); // from s2
}
Time O(2^(m+n))Space O(m + n)
When both strings are full of the same letter, both branches are always open. With 14 a's in each and an s3 that fails at its first letter, that is 115 million calls; with 18 each it is 26 billion. LeetCode allows 100 letters each.
2Optimal — a boolean table over (i, j)
This is grid DP from Intermediate (Unique Paths, LC 62: you arrive from up or from left) on the template's table. The extra insight: s3 needs no third dimension. After using i letters of s1 and j of s2 you are exactly at s3[i + j], so the letter to explain is always s3[i+j−1].
State: dp[i][j] = s1[0..i) and s2[0..j) can be shuffled into s3[0..i+j).
Recurrence: 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]).
Base case: dp[0][0] = true; if m + n != s3.length() the answer is false right away.
boolean isInterleave(String s1, String s2, String s3) {
int m = s1.length(), n = s2.length();
if (m + n != s3.length()) return false; // letters must add up
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true; // empty + empty = empty
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 && j == 0) continue;
char last = s3.charAt(i + j - 1);
boolean fromS1 = i > 0 && dp[i - 1][j] && s1.charAt(i - 1) == last; // up
boolean fromS2 = j > 0 && dp[i][j - 1] && s2.charAt(j - 1) == last; // left
dp[i][j] = fromS1 || fromS2;
}
}
return dp[m][n];
}
Time O(m · n)Space O(m · n)
Each cell does two O(1) checks. Only the row above is read, so one boolean row of length n + 1 also works.
If a third string's position is fixed by the other two (here i + j), it does not need its own dimension. A True cell means "this corner of the table is reachable".
Check m + n == s3.length() first, or s1 = "a", s2 = "b", s3 = "a" reads past the end of s3. And greedy "take from s1 whenever it matches" fails as soon as both strings offer the same letter.
Pattern notes: DP Patterns · Open on LeetCode
LC 32Longest Valid ParenthesesHardPrefix DP / StackLeetCode
A string has only '(' and ')'. Find the length of the longest substring (no gaps) that is a correctly matched bracket string.
Example: ")()())" → 4 (the part "()()"). "(()" → 2. "" → 0.
"Longest valid (well-formed) substring" of brackets. Matching brackets points to a stack (Valid Parentheses, LC 20, from Beginner). "Longest substring" over one string points to dp[i] = best answer for a part that ends exactly here. Both work; this card shows both.
1Brute force — every start, extend with a counter
From each start, walk right keeping balance = opens − closes. If it drops below 0, a ')' has no partner and nothing longer from this start can be valid. Whenever it is exactly 0, the part so far is valid.
int longestValidParentheses(String s) {
int best = 0;
for (int start = 0; start < s.length(); start++) {
int balance = 0; // opens minus closes
for (int end = start; end < s.length(); end++) {
balance += s.charAt(end) == '(' ? 1 : -1;
if (balance < 0) break; // a ')' with no partner
if (balance == 0) best = Math.max(best, end - start + 1);
}
}
return best;
}
Time O(n²)Space O(1)
With up to 3 · 10⁴ letters that is about 450 million steps on a string like "(((((…" (the counter never drops, so no early stop), too slow. The fully naive version, testing every substring separately with a stack, is O(n³).
2Optimal — dp[i] for the part ending at the end of the prefix
The template with one string: dp[i] is about the prefix s[0..i), and it only counts valid parts that end at its last letter. A valid part can only end in ')'. The extra insight is how to find its partner: jump over the valid block right before it (length dp[i−1]) to j = i − dp[i−1] − 2; if s[j] is '(' they pair up, and you glue on dp[j], the valid part ending just before the partner. The simple case "()" is the same formula with dp[i−1] = 0.
State: dp[i] = length of the longest valid part ending at s[i−1] (0 if none).
Recurrence: if s[i−1] == ')' and s[j] == '(' with j = i − dp[i−1] − 2 ≥ 0: dp[i] = dp[i−1] + 2 + dp[j].
Base case: dp[0] = dp[1] = 0. The answer is the largest dp[i], not dp[n].
int longestValidParentheses(String s) {
int n = s.length(), best = 0;
int[] dp = new int[n + 1]; // dp[i]: longest valid part ENDING at s[i-1]
for (int i = 2; i <= n; i++) {
if (s.charAt(i - 1) != ')') continue; // '(' never ends one
int j = i - dp[i - 1] - 2; // must be the partner '('
if (j >= 0 && s.charAt(j) == '(') {
dp[i] = dp[i - 1] + 2 + dp[j]; // inside + this pair + glued left
best = Math.max(best, dp[i]);
}
}
return best;
}
Time O(n)Space O(n)
Each dp[i] reads two earlier cells in O(1), so one pass is enough.
3Also optimal — a stack of indices with a "wall"
Push the index of every '('. A ')' pops its partner. The bottom of the stack always holds a wall: the index just before the current valid run (start with -1). After a pop, the run length is i − stack.peek(). If the pop empties the stack, this ')' had no partner and becomes the new wall.
int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // wall: index just before the current run
int best = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i); // wait for a partner
} else {
stack.pop(); // partner found (or the wall is gone)
if (stack.isEmpty()) stack.push(i); // unmatched ')': new wall
else best = Math.max(best, i - stack.peek()); // run since the top
}
}
return best;
}
Time O(n)Space O(n)
Each index is pushed and popped at most once. (There is also an O(1)-space trick: count opens and closes left to right, then right to left, and record a length whenever they are equal.)
"Longest valid part" with one string → dp[i] = best part ending here, and the answer is the max over all i. For brackets: jump over the inner block to find the partner, then glue on what ends before it.
Don't forget the glue + dp[j]: without it "()(())" gives 4 instead of 6. In the stack version, forgetting the initial -1 makes the first ')' pop from an empty stack and measures runs from the wrong place.
Pattern notes: DP Patterns · Stacks · Open on LeetCode
LC 115Distinct SubsequencesHardTwo-string DP (count)LeetCode
In how many different ways can you pick letters out of s, keeping their order, so that they spell t? Two ways are different if they use different positions of s.
Example: s = "rabbbit", t = "rabbit" → 3 (leave out the first, second or third b). s = "babgbag", t = "bag" → 5.
Two strings, "subsequence" and "number of" ways. Subsequence of two strings is the template; "number of" means the cell adds its choices instead of taking a min or max.
1Brute force — use or skip each letter of s
Look at the last letter of s[0..i). You can always skip it. If it equals the last letter of t[0..j), you can also use it for that letter. Count both.
int numDistinct(String s, String t) {
return count(s, t, s.length(), t.length());
}
// ways to pick t[0..j) out of s[0..i)
int count(String s, String t, int i, int j) {
if (j == 0) return 1; // empty t: pick nothing, 1 way
if (i == 0) return 0; // s used up but t is not
int ways = count(s, t, i - 1, j); // don't use s[i-1]
if (s.charAt(i - 1) == t.charAt(j - 1))
ways += count(s, t, i - 1, j - 1); // use s[i-1] as t[j-1]
return ways;
}
Time O(2^m)Space O(m)
Every matching letter splits the count in two. For s = 30 a's and t = 15 a's the recursion makes about 1.2 billion calls; LeetCode allows 1000 letters.
2Optimal — a counting table
This is LCS (LC 1143) from Intermediate turned into a counter. The extra insight: skipping s[i−1] (up) and using it for t[j−1] (diagonal) never produce the same pick, so you add them. And the base is not 0: the empty t can be picked from any prefix in exactly one way.
State: dp[i][j] = number of ways to pick t[0..j) out of s[0..i).
Recurrence: dp[i][j] = dp[i−1][j] + (s[i−1] == t[j−1] ? dp[i−1][j−1] : 0).
Base case: dp[i][0] = 1 for every i; dp[0][j] = 0 for j > 0.
int numDistinct(String s, String t) {
int m = s.length(), n = t.length();
int[][] dp = new int[m + 1][n + 1]; // dp[i][j]: ways t[0..j) hides in s[0..i)
for (int i = 0; i <= m; i++) dp[i][0] = 1; // empty t: one way
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = dp[i - 1][j]; // skip s[i-1]: up
if (s.charAt(i - 1) == t.charAt(j - 1))
dp[i][j] += dp[i - 1][j - 1]; // use it: diagonal
}
}
return dp[m][n];
}
Time O(m · n)Space O(m · n)
One O(1) cell at a time. Middle cells can grow bigger than an int; Java just wraps them around, and because the cells are only ever added, the last cell still comes out right (LeetCode promises the answer fits in an int).
3Follow-up — one row, filled right to left
Row i only reads row i−1 (up and diagonal). Keep one array and walk j from right to left, so dp[j − 1] still holds the old row's value when you read it.
int numDistinct(String s, String t) {
int n = t.length();
int[] dp = new int[n + 1]; // one row: dp[j] for the current i
dp[0] = 1;
for (int i = 1; i <= s.length(); i++) {
for (int j = n; j >= 1; j--) { // right to left: dp[j-1] is still row i-1
if (s.charAt(i - 1) == t.charAt(j - 1)) dp[j] += dp[j - 1];
}
}
return dp[n];
}
Time O(m · n)Space O(n)
Counting ways on two strings → same table, but add the choices. "Empty target" is 1 way, not 0.
If column 0 is left as 0, every cell stays 0. In the one-row version, a left-to-right loop reads the new dp[j − 1] and counts the same letter of s twice: s = "a", t = "aa" would give 1 instead of 0.
Pattern notes: DP Patterns · Open on LeetCode
LC 44Wildcard MatchingHardTwo-string DP (pattern)LeetCode
A pattern p may contain '?' (exactly one letter, any letter) and '*' (any run of letters, even an empty one). Does p match the whole string s?
Example: s = "aa", p = "a" → false. p = "*" → true. s = "cb", p = "?a" → false. s = "adceb", p = "*a*b" → true.
A text, a pattern and "matches the entire string", with a symbol that can cover any number of letters. You don't know how much each * should eat, so you would have to try all amounts, and the same (text prefix, pattern prefix) question repeats. Two strings → the template.
1Brute force — let the star eat one letter at a time
Look at the last pattern letter. A normal letter or '?' must match the last text letter. A '*' either matches nothing (drop it) or eats the last text letter and stays, ready to eat more.
boolean isMatch(String s, String p) {
return match(s, p, s.length(), p.length());
}
// does s[0..i) match p[0..j)?
boolean match(String s, String p, int i, int j) {
if (j == 0) return i == 0; // empty pattern: only empty text
char pc = p.charAt(j - 1);
if (pc == '*')
return match(s, p, i, j - 1) // * matches nothing
|| (i > 0 && match(s, p, i - 1, j)); // * eats s[i-1] and stays
return i > 0 && (pc == '?' || pc == s.charAt(i - 1))
&& match(s, p, i - 1, j - 1); // one letter for one letter
}
Time exponentialSpace O(m + n)
Each star can stop at any point, and every choice is explored again for the stars before it. The text "b" + 21 a's against "a*a*a*a*a*a*a*a*" already takes 1.8 million calls; LeetCode allows 2000 letters in each.
2Optimal — a boolean table, star = left or up
The template, plus one insight for the star: you never need to try "star eats 0, 1, 2, … letters" by hand. A '*' cell reads only left (star matches nothing) and up (star eats one more letter and stays). "Up" can repeat row after row, so chains of ups cover every length. '?' and equal letters read the diagonal.
State: dp[i][j] = s[0..i) matches p[0..j).
Recurrence: p[j−1] == '*': dp[i][j] = dp[i][j−1] || dp[i−1][j]; '?' or equal letters: dp[i][j] = dp[i−1][j−1]; otherwise false.
Base case: dp[0][0] = true; dp[0][j] is true only while p[0..j) is all stars; dp[i][0] = false for i > 0.
boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1]; // dp[i][j]: s[0..i) matches p[0..j)
dp[0][0] = true;
for (int j = 1; j <= n; j++) // empty text: only a run of *s
dp[0][j] = dp[0][j - 1] && p.charAt(j - 1) == '*';
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char pc = p.charAt(j - 1);
if (pc == '*')
dp[i][j] = dp[i][j - 1] // * matches nothing: left
|| dp[i - 1][j]; // * eats s[i-1]: up
else if (pc == '?' || pc == s.charAt(i - 1))
dp[i][j] = dp[i - 1][j - 1]; // one for one: diagonal
}
}
return dp[m][n];
}
Time O(m · n)Space O(m · n)
Every cell is O(1). The star's "any length" became a chain of single steps through the table.
3Follow-up — greedy with one saved star, O(1) space
Walk both strings. On a star, remember where it is and let it match nothing. On a mismatch, go back to the last star and let it eat one more letter. Only the last star matters: anything an earlier star could have eaten, the later star can eat too.
boolean isMatch(String s, String p) {
int i = 0, j = 0, star = -1, mark = 0; // star: index of the last '*' seen
while (i < s.length()) {
if (j < p.length() && (p.charAt(j) == '?' || p.charAt(j) == s.charAt(i))) {
i++; j++; // plain match, move both
} else if (j < p.length() && p.charAt(j) == '*') {
star = j++; mark = i; // star matches nothing for now
} else if (star != -1) {
j = star + 1; i = ++mark; // mismatch: star eats one more letter
} else {
return false; // mismatch and no star to help
}
}
while (j < p.length() && p.charAt(j) == '*') j++; // leftover stars match ""
return j == p.length();
}
Time O(m · n) worstSpace O(1)
Usually close to linear in practice. The DP is easier to get right in an interview; mention this as the follow-up.
A symbol that matches "any amount" = left (take none) or up (take one more, stay). No inner loop over lengths needed.
Row 0 is not just dp[0][0]: s = "", p = "***" is true, and p = "*a" is false. Forgetting the leading-stars row makes every pattern that starts with * fail.
Pattern notes: DP Patterns · Open on LeetCode
LC 10Regular Expression MatchingHardTwo-string DP (pattern)LeetCode
A pattern may contain '.' (any one letter) and '*', which means zero or more copies of the letter just before it. Does the pattern match the whole string?
Example: s = "aa", p = "a" → false. p = "a*" → true. s = "ab", p = ".*" → true. s = "aab", p = "c*a*b" → true (zero c's, two a's, one b).
Same signals as Wildcard Matching (LC 44): text, pattern, "entire string", and a star that repeats. The difference is in the words "preceding element": the star is glued to the letter before it, so x* is one two-letter unit.
1Brute force — zero copies, or one more copy
If the pattern ends in x*: either use it zero times (drop both letters) or, if the last text letter matches x, let it eat that letter and keep x* for more. Otherwise the last letters must match one for one.
boolean isMatch(String s, String p) {
return match(s, p, s.length(), p.length());
}
// does s[0..i) match p[0..j)?
boolean match(String s, String p, int i, int j) {
if (j == 0) return i == 0;
if (p.charAt(j - 1) == '*') {
char x = p.charAt(j - 2); // the letter that repeats
if (match(s, p, i, j - 2)) return true; // "x*" used zero times
return i > 0 && same(s.charAt(i - 1), x)
&& match(s, p, i - 1, j); // "x*" eats s[i-1], stays
}
return i > 0 && same(s.charAt(i - 1), p.charAt(j - 1))
&& match(s, p, i - 1, j - 1); // one letter for one letter
}
boolean same(char c, char pc) { return pc == '.' || pc == c; }
Time exponentialSpace O(m + n)
LeetCode keeps both strings at 20 letters or fewer, so this plain recursion is actually accepted. It is still exponential: s = "baaaaaaaaaaaaaaaaaaa" against ten "a*"s already makes 50 million calls, and at 40 letters it would never finish. Interviewers expect the table.
2Optimal — the same table, star jumps two columns
This is Wildcard Matching (LC 44, just above) plus one change. "x*" is one unit: zero copies jumps two columns left, dp[i][j−2]; one more copy goes up, dp[i−1][j], but only if s[i−1] matches x. '.' matches any letter, both on its own and as the x of .*.
State: dp[i][j] = s[0..i) matches p[0..j).
Recurrence: p[j−1] == '*': dp[i][j] = dp[i][j−2] || (same(s[i−1], p[j−2]) && dp[i−1][j]); otherwise dp[i][j] = same(s[i−1], p[j−1]) && dp[i−1][j−1].
Base case: dp[0][0] = true; dp[0][j] = dp[0][j−2] when p[j−1] == '*' (a run like "a*b*" can vanish).
boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1]; // dp[i][j]: s[0..i) matches p[0..j)
dp[0][0] = true;
for (int j = 2; j <= n; j++) // empty text: "a*b*c*" can vanish
dp[0][j] = p.charAt(j - 1) == '*' && dp[0][j - 2];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char pc = p.charAt(j - 1);
if (pc == '*') {
char x = p.charAt(j - 2); // the repeated letter
dp[i][j] = dp[i][j - 2] // zero copies: two left
|| (same(s.charAt(i - 1), x) &&
dp[i - 1][j]); // one more copy: up
} else {
dp[i][j] = same(s.charAt(i - 1), pc)
&& dp[i - 1][j - 1]; // one for one: diagonal
}
}
}
return dp[m][n];
}
boolean same(char c, char pc) { return pc == '.' || pc == c; }
Time O(m · n)Space O(m · n)
Each cell is O(1). LeetCode guarantees every '*' has a letter before it, so p.charAt(j − 2) is always safe.
When the star belongs to the letter before it, "zero copies" means skip two pattern letters (j − 2) and "one more copy" means up, guarded by a letter check.
Unlike LC 44, the star is not "anything": "a*" only eats a's. Forgetting the same(s[i−1], x) check on the up move makes s = "ab", p = "a*" wrongly true. And don't skip row 0: s = "", p = "c*" is true.
Pattern notes: DP Patterns · Open on LeetCode
Advanced DP
Same recipe as the Intermediate DP section: say what the state means in words, write the recurrence from the last choice, set the base case, fill in order. The Hard problems below each add one new idea to a pattern you already know: a state machine with a transaction counter (stock problems, from LC 309), interval DP where you pick what happens last (Burst Balloons), and sort first, then a known DP with binary search to make it O(n log n) (envelopes, job scheduling). What is harder than Intermediate: finding the state is the whole problem, and the obvious state ("which balloon goes first") is often the one that does not work.
LC 123Best Time to Buy and Sell Stock IIIHardDP state machineLeetCode
You get the price of a stock for each day. You may make at most two trades (buy then sell), and you must sell before you buy again. What is the biggest total profit?
Example: prices = [3,3,5,0,0,3,1,4] → 6 (buy at 0, sell at 3, then buy at 1, sell at 4). [1,2,3,4,5] → 4. [7,6,4,3,1] → 0.
Stock prices day by day with a rule about what you may do next ("at most two transactions", "cooldown", "fee") is a state machine DP: each day you are in one of a few modes, and the rule decides which modes lead to which. LC 309 (Intermediate) had modes hold / sold / rest; here the modes also remember how many trades you have used.
1Brute force — try every split day
Two trades never overlap, so there is a day k where the first trade is over and the second one has not started. For every k, run the one-trade answer (LC 121: track the lowest price so far) on the left part and on the right part, and add them.
int maxProfit(int[] prices) {
int n = prices.length, best = 0;
for (int k = 0; k < n; k++) { // first trade ends by day k,
best = Math.max(best, // second trade starts from day k
oneTrade(prices, 0, k) + oneTrade(prices, k, n - 1));
}
return best;
}
int oneTrade(int[] p, int lo, int hi) { // LC 121 on p[lo..hi]
int min = Integer.MAX_VALUE, best = 0;
for (int i = lo; i <= hi; i++) {
min = Math.min(min, p[i]);
best = Math.max(best, p[i] - min);
}
return best;
}
Time O(n²)Space O(1)
With up to 105 days that is 1010 steps: a time-out. (A left-to-right and a right-to-left array of one-trade answers would make it O(n), but the state machine below is simpler and generalises to k trades.)
2Optimal — four running states, one pass
This is LC 309's state machine, plus one extra insight: put the number of trades into the state. Four modes, each "best money in my pocket if I am in this mode now":
State: buy1 = holding the 1st share, sell1 = done 1 trade, buy2 = holding the 2nd share, sell2 = done 2 trades.
Recurrence (each day, price p): buy1 = max(buy1, −p), sell1 = max(sell1, buy1 + p), buy2 = max(buy2, sell1 − p), sell2 = max(sell2, buy2 + p).
Base case: the buys start at "impossible" (−∞), the sells at 0. The answer is sell2 (doing fewer trades is covered, because a trade can buy and sell on the same day for 0).
int maxProfit(int[] prices) {
int buy1 = Integer.MIN_VALUE, sell1 = 0; // after 1st buy / after 1st sell
int buy2 = Integer.MIN_VALUE, sell2 = 0; // after 2nd buy / after 2nd sell
for (int p : prices) {
buy1 = Math.max(buy1, -p); // buy the first share today
sell1 = Math.max(sell1, buy1 + p); // or sell it today
buy2 = Math.max(buy2, sell1 - p); // buy again, paid from profit 1
sell2 = Math.max(sell2, buy2 + p); // sell the second share
}
return sell2;
}
Time O(n)Space O(1)
Four numbers per day, one pass. Integer.MIN_VALUE is safe here because each buy is replaced by a real value (like −p) before anything is added to it.
Stock problems = state machine. List the modes you can be in, write "today's mode = best of (stay, or come from the mode before and pay/earn p)". A limit on trades just means more modes: buy1 → sell1 → buy2 → sell2.
Two separate "best single trade" answers do not add up: taking the best trade and then the second-best trade can overlap in time. Also don't start buy1 at 0: that would mean you own a share for free.
Pattern notes: DP Patterns · Open on LeetCode
LC 188Best Time to Buy and Sell Stock IVHardDP state machineLeetCode
Same as LC 123, but instead of two trades you may make at most k trades. Buy before you sell, and hold at most one share at a time.
Example: k = 2, prices = [2,4,1] → 2. k = 2, prices = [3,2,6,5,0,3] → 7 (buy 2 sell 6, buy 0 sell 3).
"At most k transactions" where k is an input: the four variables of LC 123 become two arrays of length k + 1. Whenever a problem's state includes "how many times I have used something" (trades, skips, deletions), that count becomes an index of the DP.
1Brute force — on each day, do nothing or act
Recursion on (day, trades left, holding?). Each day you either do nothing, or buy (if not holding), or sell (if holding, using up one trade).
int maxProfit(int k, int[] prices) { return go(prices, 0, k, false); }
// best profit from day i on, with k sells left, holding a share or not
int go(int[] p, int i, int k, boolean holding) {
if (i == p.length || k == 0) return 0;
int skip = go(p, i + 1, k, holding); // do nothing today
if (holding) return Math.max(skip, p[i] + go(p, i + 1, k - 1, false)); // sell
return Math.max(skip, -p[i] + go(p, i + 1, k, true)); // buy
}
Time O(2ⁿ)Space O(n)
Two branches per day. Just 30 days with k = 15 already took about 7 seconds; LeetCode allows 1,000 days. Note there are only n · k · 2 different arguments, which is the hint that a table will work.
2Optimal — buy[j] and sell[j] for every trade number j
LC 123 had the chain buy1 → sell1 → buy2 → sell2. Here the chain is k links long, so store it in two arrays and, for each price, walk the chain once.
State: buy[j] = best money while holding the share of trade j; sell[j] = best money after finishing j trades.
Recurrence: buy[j] = max(buy[j], sell[j−1] − p), sell[j] = max(sell[j], buy[j] + p).
Base case: sell[0] = 0 (no trades yet), every buy[j] starts "impossible". Answer: sell[k].
int maxProfit(int k, int[] prices) {
int[] buy = new int[k + 1]; // buy[j]: holding the share of trade j
int[] sell = new int[k + 1]; // sell[j]: finished j trades
Arrays.fill(buy, Integer.MIN_VALUE / 2); // "impossible", and safe to add p to
for (int p : prices) {
for (int j = 1; j <= k; j++) {
buy[j] = Math.max(buy[j], sell[j - 1] - p); // start trade j
sell[j] = Math.max(sell[j], buy[j] + p); // finish trade j
}
}
return sell[k];
}
Time O(n · k)Space O(k)
1,000 days × 100 trades = 100,000 updates. With k = 2 this is exactly the LC 123 solution. If k ≥ n / 2 the limit never bites, and you can use the "take every rise" greedy from LC 122 instead.
A counter in the state ("trades used", "deletions left") turns a few variables into an array indexed by the counter. Write the small version (k = 2) first, then generalise the pattern.
Use Integer.MIN_VALUE / 2 as "impossible", not MIN_VALUE: then any small change to it (adding or subtracting a price) stays very negative, while MIN_VALUE − 1 wraps around to a huge positive number. And k counts trades (buy + sell pairs), not single actions.
Pattern notes: DP Patterns · Open on LeetCode
LC 312Burst BalloonsHardInterval DPLeetCode
Balloons in a row have numbers. When you burst balloon i you earn left neighbour × i × right neighbour (a missing neighbour counts as 1), and then its two neighbours become next to each other. Burst them all; what is the most coins you can earn?
Example: nums = [3,1,5,8] → 167: burst 1 (3·1·5 = 15), then 5 (3·5·8 = 120), then 3 (1·3·8 = 24), then 8 (1·8·1 = 8). [1,5] → 10.
"Remove items one by one, and the cost depends on who is next to it at that moment" on a row of up to 300 items: the answer for a range [l, r] is built from smaller ranges, so it is interval DP (the family of LC 5 Longest Palindromic Substring). The small n (300) also hints at O(n³).
1Brute force — try every order
Pick any balloon to burst next, earn its coins, remove it from the list, recurse on what is left, and put it back.
int maxCoins(int[] nums) {
List<Integer> b = new ArrayList<>();
for (int x : nums) b.add(x);
return go(b);
}
int go(List<Integer> b) { // try every balloon as the NEXT one
int best = 0;
for (int i = 0; i < b.size(); i++) {
int left = i > 0 ? b.get(i - 1) : 1;
int right = i < b.size() - 1 ? b.get(i + 1) : 1;
int x = b.remove(i); // burst it
best = Math.max(best, left * x * right + go(b));
b.add(i, x); // put it back
}
return best;
}
Time O(n! · n)Space O(n)
10 balloons already take a quarter of a second (10! = 3.6 million orders); 13 would take minutes, and LeetCode allows 300. Memo does not rescue this version cleanly: the state is "which balloons are left", and there are 2n of those.
2Optimal — choose which balloon bursts LAST in each range
Thinking "which one first" fails, because bursting a balloon glues its neighbours together, so the two sides are no longer independent. The extra insight: ask which balloon k bursts LAST between walls l and r. While k is still there, the left side (l, k) and the right side (k, r) can never touch, so they are two independent smaller problems. And when k finally bursts, its neighbours are exactly the walls: it earns a[l] · a[k] · a[r].
State: pad the array with a 1 at each end; dp[l][r] = most coins from bursting everything strictly between l and r.
Recurrence: dp[l][r] = max over l < k < r of dp[l][k] + dp[k][r] + a[l]·a[k]·a[r].
Base case: dp[l][l+1] = 0 (nothing between). Fill by range length, short to long. Answer: dp[0][n+1].
int maxCoins(int[] nums) {
int n = nums.length;
int[] a = new int[n + 2]; // pad with 1 on both ends
a[0] = a[n + 1] = 1;
for (int i = 0; i < n; i++) a[i + 1] = nums[i];
// dp[l][r] = best coins from bursting everything strictly between l and r
int[][] dp = new int[n + 2][n + 2];
for (int len = 2; len <= n + 1; len++) { // gap r - l, small to big
for (int l = 0; l + len <= n + 1; l++) {
int r = l + len;
for (int k = l + 1; k < r; k++) { // k bursts LAST in (l, r)
dp[l][r] = Math.max(dp[l][r],
dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
}
}
}
return dp[0][n + 1];
}
Time O(n³)Space O(n²)
About n³ / 6 ≈ 4.5 million steps for 300 balloons. Filling by length guarantees dp[l][k] and dp[k][r] (shorter ranges) are ready before dp[l][r] needs them.
When removing an item changes its neighbours, think backwards: the item removed last splits the range into two independent halves. Interval DP: dp[l][r] over a split point k, filled by increasing length.
The walls l and r are not burst inside dp[l][r]; that is why the padded 1s work and why k runs strictly between them. Looping l from 0 upward with r inside (instead of by length) reads cells that are not filled yet.
Pattern notes: DP Patterns · Open on LeetCode
LC 354Russian Doll EnvelopesHardSort + LISLeetCode
Each envelope has a width and a height. One envelope fits inside another only if it is strictly smaller in both. What is the largest number of envelopes you can nest inside each other (like Russian dolls)? You can't rotate them.
Example: [[5,4],[6,4],[6,7],[2,3]] → 3 ([2,3] → [5,4] → [6,7]). [[1,1],[1,1],[1,1]] → 1.
"Longest chain where each item is bigger than the one before" is Longest Increasing Subsequence (Intermediate LC 300). The twist is that "bigger" has two dimensions. Sorting by one dimension removes it, and LIS runs on the other.
1Brute force — sort by width, then O(n²) LIS
Sort by width so every envelope that can go inside i comes before it. Then do the LC 300 DP: dp[i] = longest chain ending with envelope i, checking every earlier envelope that is smaller in both.
int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length, best = 0;
Arrays.sort(envelopes, (a, b) -> a[0] - b[0]); // by width
int[] dp = new int[n]; // longest chain ending at i
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (envelopes[j][0] < envelopes[i][0] && envelopes[j][1] < envelopes[i][1])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
best = Math.max(best, dp[i]);
}
return best;
}
Time O(n²)Space O(n)
Correct, but LeetCode has up to 105 envelopes: 5 × 109 pair checks, a time-out.
2Optimal — clever sort, then LIS with binary search
LC 300's O(n log n) follow-up (the tails array) works on one dimension. So sort by width, and run it on heights. The one extra insight is the tie-break: for equal widths, sort heights DESCENDING. Then two envelopes of the same width can never both be in an increasing run of heights, which is exactly the rule "same width can't nest".
int maxEnvelopes(int[][] envelopes) {
// width up; for EQUAL widths, height DOWN
Arrays.sort(envelopes, (a, b) -> a[0] != b[0] ? a[0] - b[0] : b[1] - a[1]);
int[] tails = new int[envelopes.length]; // tails[k]: smallest last height
int len = 0; // of a chain of length k + 1
for (int[] e : envelopes) {
int h = e[1], lo = 0, hi = len;
while (lo < hi) { // first tail >= h
int mid = (lo + hi) / 2;
if (tails[mid] < h) lo = mid + 1;
else hi = mid;
}
tails[lo] = h; // replace, or extend when lo == len
if (lo == len) len++;
}
return len;
}
Time O(n log n)Space O(n)
One sort, then one binary search per envelope. Searching for the first tail ≥ h (not > h) keeps the chain strictly increasing, so equal heights can't nest either.
A 2D "chain" problem: sort by one dimension to kill it, then run a 1D algorithm on the other. Choose the tie-break so that items that must not chain together (equal first dimension) can't form an increasing run.
Sorting equal widths by height ascending is the classic bug: [[1,1],[2,2],[2,3]] would give heights 1, 2, 3 and answer 3, but [2,2] can't go inside [2,3]. The right answer is 2.
Pattern notes: DP Patterns · Open on LeetCode
LC 1235Maximum Profit in Job SchedulingHardDP + binary searchLeetCode
Each job has a start time, an end time and a profit. You can't do two jobs that overlap in time (a job may start exactly when another ends). Pick jobs to get the largest total profit.
Example: startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70] → 120 (the job 1–3 for 50 and the job 3–6 for 70).
"Choose non-overlapping intervals" is the Intermediate intervals family (LC 435), but "maximise profit" instead of "maximise count" breaks the greedy: a long job can be worth more than many short ones. So it is take or skip DP, like House Robber, where "the previous job I can combine with" is found by binary search on end times.
1Brute force — take or skip, scanning back for a compatible job
Sort jobs by end time. dp[i] = best profit using only the first i jobs. For job i, either skip it (dp[i−1]) or take it and add the best from the jobs that end before it starts, found by walking backwards.
int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = profit.length;
int[][] jobs = new int[n][];
for (int i = 0; i < n; i++) jobs[i] = new int[]{startTime[i], endTime[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // by end time
int[] dp = new int[n + 1]; // dp[i]: best using first i jobs
for (int i = 1; i <= n; i++) {
int[] job = jobs[i - 1];
int j = i - 1;
while (j > 0 && jobs[j - 1][1] > job[0]) j--; // walk back to a job that fits
dp[i] = Math.max(dp[i - 1], dp[j] + job[2]); // skip it, or take it
}
return dp[n];
}
Time O(n²)Space O(n)
The DP is already right; the backward walk is the slow part. With 5 × 104 jobs it can take about 1.25 × 109 steps (for example when every job overlaps every other one).
2Optimal — same DP, binary search for the compatible job
This is House Robber's "skip it, or take it plus the best from before", plus one extra insight: because jobs are sorted by end time, the jobs that end at or before job i's start form a prefix. So binary search on end times finds how many jobs fit before job i in O(log n).
State: dp[i] = best profit from the first i jobs (sorted by end).
Recurrence: dp[i] = max(dp[i−1], dp[j] + profit_i), where j = number of jobs whose end ≤ starti.
Base case: dp[0] = 0. Answer: dp[n].
int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = profit.length;
int[][] jobs = new int[n][];
for (int i = 0; i < n; i++) jobs[i] = new int[]{startTime[i], endTime[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // by end time
int[] dp = new int[n + 1]; // dp[i]: best using first i jobs
for (int i = 1; i <= n; i++) {
int start = jobs[i - 1][0];
// lo = how many of the first i-1 jobs end at or before 'start'
int lo = 0, hi = i - 1;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (jobs[mid][1] <= start) lo = mid + 1;
else hi = mid;
}
dp[i] = Math.max(dp[i - 1], // skip job i
dp[lo] + jobs[i - 1][2]); // take job i
}
return dp[n];
}
Time O(n log n)Space O(n)
The sort is O(n log n) and each of the n jobs does one O(log n) search. Checked against an all-subsets brute force on 20,000 random small inputs.
Weighted intervals: sort by end, take-or-skip DP, binary search for the last compatible item. The same trick works whenever "the previous item I can combine with" is a prefix of a sorted list.
Sort by end, not start: dp[i] must mean "best among jobs that finish first". And a job ending at time 3 is compatible with one starting at 3, so search for end ≤ start, not < start.
Pattern notes: DP Patterns · Open on LeetCode