LeetCode Cheat Sheet — DSA in Java

Read notes

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.

How to use this list

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.

Stage 1 — Warm upGet comfortable with arrays, strings and hashing. Almost every later problem builds on these.

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
The problem in easy words

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
The idea

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

nums 2 7 11 15 i = 0 i = 1 target = 9 Step 1 · i = 0, num = 2 need = 9 − 2 = 7 → map is empty, not found put 2 → 0 {2: 0} Step 2 · i = 1, num = 7 need = 9 − 7 = 2 → map has 2 at index 0 answer [0, 1]
Walk left to right once. Each number checks the map for its partner, then adds itself for the numbers still to come.
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.

Remember

"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.

Watch out

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.

LC 217Contains DuplicateEasyHash SetLeetCode
The problem in easy words

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
The idea

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.

nums 1 2 3 1 i = 0 i = 1 i = 2 i = 3 i = 0 1 is new → addset = {1} i = 1 2 is new → addset = {1, 2} i = 2 3 is new → addset = {1, 2, 3} i = 3 1 is already in the set → return true
One pass. Each number is checked against the set, then added. The first "already there" ends the search early.
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.

Remember

"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.)

Watch out

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²).

LC 121Best Time to Buy and Sell StockEasyRunning MinimumLeetCode
The problem in easy words

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
The idea

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.

price min so far profit today 7 1 5 3 6 4 7 1 1 1 1 1 0 0 5−1 = 4 3−1 = 2 6−1 = 5 4−1 = 3 day 0 day 1 day 2 day 3 day 4 day 5 best = 5 : buy on day 1 at 1, sell on day 4 at 6
Each day only needs two numbers: the cheapest price before it, and the best profit so far.
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.

Remember

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.

Watch out

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.

LC 283Move ZeroesEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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.

start 0 1 0 3 12 w write = 0 (read 0 is a zero: skip) read = 1 1 0 0 3 12 w swap 1 into slot 0, write = 1 read = 3 1 3 0 0 12 w swap 3 into slot 1, write = 2 read = 4 1 3 12 0 0 w swap 12 into slot 2: done
Green slots are finished. 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.

Remember

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.

Watch out

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²).

LC 26Remove Duplicates from Sorted ArrayEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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 = [1, 1, 2, 3, 3] write = 1 (slot 0 is always kept) read = 1 1 == last kept 1 → skip read = 2 2 != last kept 1 → nums[1] = 2, write = 2 [1, 2, 2, 3, 3] read = 3 3 != last kept 2 → nums[2] = 3, write = 3 [1, 2, 3, 3, 3] read = 4 3 == last kept 3 → skip return 3 → the first 3 slots are [1, 2, 3]
"Last kept" is always 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.

Remember

In a sorted array, equal values are neighbours. "Is it new?" becomes "is it different from the last one I kept?"

Watch out

Compare with nums[write - 1] (the last kept value), and return write, the count. Returning the array or write - 1 are the usual slips.

LC 27Remove ElementEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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.

read = 0 3 2 2 3 3 is val → skip, write stays 0 read = 1 2 2 2 3 2 → copy to slot 0, write = 1 read = 2 2 2 2 3 2 → copy to slot 1, write = 2 read = 3 2 2 2 3 3 is val → skip return write = 2 → nums starts with [2, 2]
Coral = the value to remove (skipped). Blue = the number being read. Green = kept slots at the front.
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.

Remember

"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.

Watch out

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.

LC 88Merge Sorted ArrayEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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.

start 1 2 3 0 0 0 nums2 = [2, 5, 6] step 1 1 2 3 0 0 6 3 vs 6 → put 6 in slot 5 step 2 1 2 3 0 5 6 3 vs 5 → put 5 in slot 4 step 3 1 2 3 3 5 6 3 vs 2 → move 3 to slot 3 step 4 1 2 2 3 5 6 2 vs 2 → take nums2's 2 nums2 is used up → stop. 1 and 2 were already in place: [1, 2, 2, 3, 5, 6]
Yellow = the slot just filled. The empty zeros at the back give us room, so nothing useful is ever overwritten.
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.

Remember

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.

Watch out

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.

LC 169Majority ElementEasyBoyer-Moore VotingLeetCode
The problem in easy words

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
The idea

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.

nums candidate count 2 2 1 1 1 2 2 2 2 2 2 1 1 2 1 2 1 0 1 0 1 new pick new pick final candidate = 2 → the majority element
Green = a vote for the current candidate (+1). Coral = a vote against (−1). When the count drops to 0, the next number starts fresh as candidate.
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.

Remember

"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.

Watch out

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.

LC 268Missing NumberEasyMathLeetCode
The problem in easy words

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
The idea

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.

should have 0 1 2 3 3 × 4 / 2 = 6 we have 3 0 1 3 + 0 + 1 = 4 missing = expected − actual = 6 − 4 = 2
The full range 0..n has a known total. Whatever is short is the missing number.
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).

Remember

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.

Watch out

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.

LC 136Single NumberEasyBit ManipulationLeetCode
The problem in easy words

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
The idea

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.

result, step by step (in binary) start 000 = 0 ^ 4 (100) → 100 = 4 ^ 1 (001) → 101 = 5 ^ 2 (010) → 111 = 7 ^ 1 (001) → 110 = 6 ^ 2 (010) → 100 = 4 regroup the same XORs (1 ^ 1) = 0 (2 ^ 2) = 0 4 ^ 0 ^ 0 = 4 order does not matter, so pairs always cancel answer = 4
XOR flips a bit when it sees a 1. Each number flips the same bits twice (back to where they were), except the single one.
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.

Remember

"Everything appears twice except one" → XOR it all. a ^ a = 0 and a ^ 0 = a are worth memorising.

Watch out

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.

LC 53Maximum SubarrayMediumKadaneLeetCode
The problem in easy words

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
The idea

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.

nums current best -2 1 -3 4 -1 2 1 -5 4 -2 1 -2 4 3 5 6 1 5 -2 1 1 4 4 5 6 6 6 yellow = start fresh (the old block was negative). best block [4, -1, 2, 1] = 6
At index 3, the old block sums to −2, so 4 on its own (4) beats joining it (2). From there the block grows until the best sum 6.
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".

Remember

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.

Watch out

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].

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
The problem in easy words

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 idea

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.

start h e l l o L R swap h and o after swap 1 o e l l h L R swap e and l after swap 2 o l l e h L = R pointers met: stop
Each swap fixes two letters at once, so we only walk to the middle: about n/2 swaps.
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.

Remember

"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.

Watch out

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.

LC 125Valid PalindromeEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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".

M a , space a m 012345 Step 1 · L=0 'M', R=5 'm' → lowercase both: m = m, move in Step 2 · L=1 'a', R=4 'a' → a = a, move in Step 3 · ',' and ' ' are not letters → L skips until it meets R Nothing left to compare → true (the cleaned string is "maam")
The pointers compare only real letters. Junk characters are stepped over, never copied.
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.

Remember

When the input has characters to ignore, skip them with the pointers instead of building a cleaned copy. Same answer, no extra memory.

Watch out

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.

LC 242Valid AnagramEasyCountingLeetCode
The problem in easy words

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
The idea

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.

anagram vs nagaram agmnr + s 3 1 1 1 1 − t 0 0 0 0 0 all zero → true rat vs car acrt + s 1 0 1 1 − t 0 −1 0 1 not all zero → false
s pushes counts up, t pulls them down. Anagrams cancel out perfectly.
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.

Remember

"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.

Watch out

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

LC 387First Unique Character in a StringEasyCountingLeetCode
The problem in easy words

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
The idea

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".

s = "loveleetcode" pass 1 (count) l:2 o:2 v:1 e:4 t:1 c:1 d:1 pass 2 (scan s in order) i = 0 'l' count 2 → repeats, skip i = 1 'o' count 2 → repeats, skip i = 2 'v' count 1 → first unique! return 2
Two simple passes beat one clever nested loop. Pass 2 can stop early at the first count of 1.
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.

Remember

"Something about how often each item appears" → count in one pass, answer in a second pass. Two passes are still linear time.

Watch out

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.

LC 14Longest Common PrefixEasyVertical ScanLeetCode
The problem in easy words

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
The idea

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.

i=0i=1i=2 f l o w e r f l o w f l i g h t column 0: f f f same column 1: l l l same column 2: o o i differ stop → return "fl"
Read down, not across. The prefix ends at the first column where the words disagree.
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.

Remember

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.

Watch out

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.

LC 205Isomorphic StringsEasyHash MapLeetCode
The problem in easy words

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
The idea

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.

paper vs title p a p e r t i t l e second p → t agrees with the map true badc vs baba b a d c b a b a d wants b, but b already belongs to b false (only the t→s map sees it)
s→t catches "one letter, two names". t→s catches "two letters, one name". You need both.
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.

Remember

"Is there a one-to-one matching between two things?" → keep two maps, one for each direction, and check both on every step.

Watch out

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.

LC 392Is SubsequenceEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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.

t a h b g d c skipskipskip s a b c j moves every step i moves only on a match i reached the end of s → true
Each letter of s grabs the first matching letter of t it can find, left to right.
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.

Remember

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.

Watch out

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.

LC 13Roman to IntegerEasyHash MapLeetCode
The problem in easy words

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
The idea

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.

s = "MCMXCIV" symbol value next rule total M 1000 C not smaller → + 1000 C 100 M smaller → − 900 M 1000 X not smaller → + 1900 X 10 C smaller → − 1890 C 100 I not smaller → + 1990 I 1 V smaller → − 1989 V 5 - last → + 1994
Every symbol is added or subtracted based only on its right-hand neighbour.
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.

Remember

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.

Watch out

Check i + 1 < s.length() before peeking, or the last symbol throws an exception. The last symbol is always added, never subtracted.

LC 383Ransom NoteEasyCountingLeetCode
The problem in easy words

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
The idea

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.

note = "aa", magazine = "aab" count magazine a:2 b:1 spend 'a' a:1 b:1 spend 'a' a:0 b:1 all counts >= 0 → true note = "aa", magazine = "ab" count magazine a:1 b:1 spend 'a' a:0 b:1 spend 'a' a:-1 b:1 went below 0 → false
Count the supply once, then every note letter costs one O(1) array update.
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.

Remember

"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.

Watch out

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.

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
The problem in easy words

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
The idea

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.

nums1 = [4, 9, 5] set {4, 9, 5} nums2 9 4 9 8 4 add add gone missing gone set after {4,5} {5} {5} {5} {5} remove after adding, so no duplicates result = [9, 4]
The set does two jobs: fast "is it there?" checks, and making sure each number is reported once.
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.

Remember

"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.

Watch out

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].

LC 350Intersection of Two Arrays IIEasyHash MapLeetCode
The problem in easy words

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
The idea

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.

nums1 = [1, 2, 2, 1] → count = {1:2, 2:2} nums2 count before action count after 2 2 take, count-1 {1:2, 2:1} result [2] 2 1 take, count-1 {1:2, 2:0} result [2, 2] LC 349 (set) would return [2]. LC 350 (count map) returns [2, 2].
A set says yes or no. A count map says "yes, and this many more times".
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.

Remember

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.

Watch out

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].

LC 290Word PatternEasyHash MapLeetCode
The problem in easy words

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
The idea

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.

pattern = "abba", s = "dog cat cat dog" i letter word letter→word word→letter 0 a dog {a:dog} {dog:a} new pair, record 1 b cat {a:dog, b:cat} {dog:a, cat:b} new pair, record 2 b cat b is cat cat is b agrees 3 a dog a is dog dog is a agrees → true pattern = "abba", s = "dog dog dog dog" 0 a dog {a:dog} {dog:a} new pair, record 1 b dog b is new dog already is a! → false
The word→letter map catches two letters trying to share the same word.
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.

Remember

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.

Watch out

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.

LC 202Happy NumberEasyCycle DetectionLeetCode
The problem in easy words

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
The idea

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.

19 82 68 100 1 reaches 1: happy 2 4 16 37 58 20 42 145 89 never reaches 1: a loop, not happy
Every number either walks into 1 or into the same loop (4 → 16 → … → 20 → 4). Fast and slow detect which one.
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.

Remember

"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.

Watch out

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.

LC 1207Unique Number of OccurrencesEasyHash MapLeetCode
The problem in easy words

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
The idea

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.

arr = [1, 2, 2, 1, 1, 3] count map {1:3, 2:2, 3:1} 3 values set of counts {3, 2, 1} 3 counts 3 == 3 → true arr = [1, 2] count map {1:1, 2:1} 2 values set of counts {1} 1 count 1 != 2 → false (a count repeated)
Comparing sizes is a quick way to ask "were there any duplicates?"
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.

Remember

"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.

Watch out

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.

Stage 2 — Your first patternsFour patterns that turn O(n²) loops into O(n) passes.

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 problem in easy words

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
The idea

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.

left pointer right pointer target = 9 step 1 1 3 4 5 7 11 1 + 11 = 12 > 9 → right-- step 2 1 3 4 5 7 11 1 + 7 = 8 < 9 → left++ step 3 1 3 4 5 7 11 3 + 7 = 10 > 9 → right-- step 4 1 3 4 5 7 11 3 + 5 = 8 < 9 → left++ step 5 1 3 4 5 7 11 4 + 5 = 9 → answer [3, 4]
Every step rules out one number for good (shown faded). The pointers squeeze inwards until they land on the pair.
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.

Remember

Sorted array + "find a pair" → two pointers from both ends. The comparison with the target tells you which side to move.

Watch out

The answer is 1-indexed. Returning {left, right} instead of {left + 1, right + 1} is the classic wrong answer here.

LC 977Squares of a Sorted ArrayEasyTwo PointersLeetCode
The problem in easy words

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
The idea

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.

nums (left = sky, right = coral) compare squares result, filled from the back -4 -1 0 3 10 16 vs 100 100 -4 -1 0 3 10 16 vs 9 16 100 -4 -1 0 3 10 1 vs 9 9 16 100 -4 -1 0 3 10 1 vs 0 1 9 16 100 -4 -1 0 3 10 only 0 left 0 1 9 16 100
Each row: compare the squares at the two ends, write the bigger one into the next free slot from the right (green), then move that pointer in.
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.

Remember

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.

Watch out

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.

LC 11Container With Most WaterMediumTwo PointersLeetCode
The problem in easy words

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
The idea

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.

0 1 2 3 4 5 6 7 8 water = min(8, 7) × (8 − 1) = 49 left / right: area → move L0 R8: 1 × 8 = 8 L++ L1 R8: 7 × 7 = 49 R-- L1 R7: 3 × 6 = 18 R-- L1 R6: 8 × 5 = 40 R-- ... smaller from here best = 49
The short wall at index 0 is dropped first. After that, the right wall is always the shorter (or equal) one, so right keeps moving in.
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.

Remember

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.

Watch out

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.

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
The problem in easy words

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
The idea

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

in window enters leaves 1 12 -5 -6 50 3 first: 1 + 12 − 5 − 6 = 2 1 12 -5 -6 50 3 2 + 50 − 1 = 51 (best) 1 12 -5 -6 50 3 51 + 3 − 12 = 42 best sum 51 → average 51 / 4 = 12.75
Each slide costs one addition and one subtraction, no matter how big k is.
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.

Remember

Fixed-size window: build the first window, then slide with "add the new one, subtract the one that fell off" (nums[i - k]).

Watch out

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.

LC 219Contains Duplicate IIEasySliding Window + SetLeetCode
The problem in easy words

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
The idea

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].

in the set (last k numbers) current number i i = 0 1 0 1 1 1 in {}? no → add → {1} i = 1 1 0 1 1 0 in {1}? no → add, drop 1 → {0} i = 2 1 0 1 1 1 in {0}? no → add, drop 0 → {1} i = 3 1 0 1 1 1 in {1}? yes → return true
The set is a window of the last k numbers. The first 1 (index 0) was dropped in time, so it never counts; the 1s at index 2 and 3 do.
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.

Remember

"Duplicate within distance k" → a HashSet that works as a sliding window: check, add, and evict the element that is now too far away.

Watch out

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.

LC 1876Substrings of Size Three with Distinct CharactersEasyFixed Sliding WindowLeetCode
The problem in easy words

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
The idea

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.

s = x y z z a z [x y z] x, y, z all different → count = 1 [y z z] z repeats → skip [z z a] z repeats → skip [z a z] z repeats (a ≠ c fails) → skip answer = 1
The window moves one character at a time. Each step is three quick comparisons.
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.

Remember

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.

Watch out

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.

LC 3Longest Substring Without Repeating CharactersMediumVariable Sliding WindowLeetCode
The problem in easy words

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
The idea

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.

window new letter (right) dropped from the left right = 2 a b c a b c b b window abc, len 3, best 3 right = 3 a b c a b c b b a repeats → drop a → bca right = 6 a b c a b c b b b repeats → drop a, b → cb right = 7 a b c a b c b b b repeats → drop c, b → b best = 3
The left edge only ever moves forward, so each letter is added once and removed at most once.
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²).

Remember

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.

Watch out

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.

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
The problem in easy words

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
The idea

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.

nums [1, 2, 3, 4] i = 1 nums[1] = 2 + nums[0] (1) = 3 [1, 3, 3, 4] i = 2 nums[2] = 3 + nums[1] (3) = 6 [1, 3, 6, 4] i = 3 nums[3] = 4 + nums[2] (6) = 10 [1, 3, 6, 10] answer [1, 3, 6, 10]
Position 0 already holds its own total. Every later position adds the total just before it.
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).

Remember

This is the prefix sum itself. Whenever you catch yourself re-adding the same numbers, keep a running total instead.

Watch out

Start the loop at i = 1. Starting at 0 reads nums[-1] and throws ArrayIndexOutOfBoundsException.

LC 724Find Pivot IndexEasyPrefix SumLeetCode
The problem in easy words

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
The idea

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.

1 7 3 6 5 6 nums total = 28 left = 11 i = 3 right = 11 i = 0 left = 0 right = 28 − 0 − 1 = 27 no i = 1 left = 1 right = 28 − 1 − 7 = 20 no i = 2 left = 8 right = 28 − 8 − 3 = 17 no i = 3 left = 11 right = 28 − 11 − 6 = 11 left == right at i = 3 → return 3
One pass to get the total, one pass to find the pivot. 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.

Remember

If you know the total, one running sum gives you both sides: right = total − left − current.

Watch out

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.

LC 303Range Sum Query - ImmutableEasyPrefix SumLeetCode
The problem in easy words

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
The idea

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.

nums -2 0 3 -5 2 -1 0 1 2 3 4 5 prefix 0 -2 -2 1 -4 -2 -3 [0] [1] [2] [3] [4] [5] [6] sum of the first k sumRange(2, 5) = prefix[6] − prefix[2] = −3 − (−2) = −1
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.

Remember

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].

Watch out

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.

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 problem in easy words

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 idea

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.

( [ ] { } ) ) ) ] ) ) } ) empty push ) push ] pop ], match push } pop }, match pop ), match stack is empty at the end → valid
Top row: the character being read. Below: the stack after reading it (top of the stack is the upper box).
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.

Remember

"Most recent thing must be handled first" → stack. Pushing the expected closer keeps the check to one comparison.

Watch out

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.

LC 682Baseball GameEasyStackLeetCode
The problem in easy words

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
The idea

"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.

op stack (top on the right) total "5" [5] 5 "2" [5, 2] 7 "C" [5] 5 pop 2, subtract it "D" [5, 10] 15 push 2 × top "+" [5, 10, 15] 30 push top + the one below it answer = 30
Every operation only looks at the top one or two scores, so the stack is the natural fit.
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".

Remember

Undo / cancel the last action → stack. Keeping a running total next to it saves a final pass.

Watch out

Check the letters "+", "D", "C" before calling parseInt, and compare strings with equals, never ==. Scores like "-2" are valid numbers.

LC 844Backspace String CompareEasyStack / Two PointersLeetCode
The problem in easy words

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
The idea

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.

s a b # c t a d # c scan right to left 1. c vs c → equal, step both left 2. # → skip = 1 3. b and d are erased (skip used) 4. a vs a → equal 5. both run out together → true
Green letters survive and get compared, coral is a backspace, sand letters are erased and skipped.
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.

Remember

"Undo" characters → a stack (StringBuilder). If you need O(1) space, process from the end, where you already know what gets undone.

Watch out

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.

LC 232Implement Queue using StacksEasyDesign · Two StacksLeetCode
The problem in easy words

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
The idea

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.

1. push 1, 2, 3 1 2 3 empty in out 2. pop: out empty, pour empty 3 2 1 in out 3. pop returns 1 empty 3 2 in out
Pouring reverses the order, so the oldest item (1) ends up on top of out. The next pops (2, then 3) need no pouring at all.
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").

Remember

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.

Watch out

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.

LC 155Min StackMediumDesign · StackLeetCode
The problem in easy words

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
The idea

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.

after push -2, 0, -3 val min so far -3 -3 0 -2 -2 -2 ← top getMin() = -3 (read the top pair) after pop() val min so far 0 -2 -2 -2 ← top top() = 0, getMin() = -2
Popping -3 automatically "restores" the old minimum, because the pair underneath saved 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.

Remember

If a stack needs a running statistic (min, max), store it with each item. Popping then undoes the statistic for free.

Watch out

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.

LC 496Next Greater Element IEasyMonotonic StackLeetCode
The problem in easy words

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
The idea

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.

read stack after (bottom → top) what happened 1 1 stack empty → push 1 3 3 3 > 1: pop 1, map 1 → 3 4 4 4 > 3: pop 3, map 3 → 4 2 4 2 2 < 4: just push 2 left waiting (4, 2) → -1. nums1 [4, 1, 2] → [-1, 3, -1]
The stack always goes from big (bottom) to small (top). A bigger newcomer pops and answers everyone smaller than it.
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.

Remember

"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.

Watch out

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.

Stage 3 — Pointers & halvingLinked lists, binary search and the small math tricks interviewers love.

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
The problem in easy words

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
The idea

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.

Start null 1 2 3 null prev curr After step 1 null 1 2 3 null prev curr After step 2 null 1 2 3 null prev curr After step 3 null 1 2 3 null prev curr curr is null: stop, return prev
Each step flips one arrow (shown in accent) and moves prev and curr one node right. When curr falls off the end, prev is the new head.
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.

Remember

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.

Watch out

Returning head or curr at the end. After the loop head is the last node and curr is null; the new head is prev.

LC 21Merge Two Sorted ListsEasyDummy NodeLeetCode
The problem in easy words

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
The idea

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.

list1 list2 1 2 4 null 1 3 4 null #1 #3 #5 #2 #4 #6 tie 1 = 1: take list1 first (<=) list1 empties first: glue the rest merged D 1 1 2 3 4 4 null dummy return dummy.next
The small numbers show the order nodes are taken. No new nodes are made: we just re-point the existing arrows.
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.

Remember

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.

Watch out

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.

LC 876Middle of the Linked ListEasyFast & Slow PointersLeetCode
The problem in easy words

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
The idea

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.

Start 1 2 3 4 5 null slow, fast both at head Step 1 1 2 3 4 5 null slow fast slow +1, fast +2 Step 2 1 2 3 4 5 null slow fast fast.next is null: stop Answer: slow is at 3, the middle
Fast stops when it can't take two more steps. At that moment slow sits on the middle node.
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.

Remember

"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.

Watch out

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.

LC 141Linked List CycleEasyFast & Slow PointersLeetCode
The problem in easy words

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
The idea

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.

list 3 2 0 -4 -4 points back to 2: a loop there is no null to reach Start slow = 3 fast = 3 Step 1 slow = 2 fast = 0 Step 2 slow = 0 fast = 2 Step 3 slow = -4 fast = -4 same node: cycle found
Once both are inside the loop, fast gains one node per step on slow, so they must land on the same node.
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.

Remember

"Is there a loop?" → slow and fast pointers; if they ever meet, there is a cycle. No memory needed.

Watch out

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.

LC 83Remove Duplicates from Sorted ListEasyPointer WalkLeetCode
The problem in easy words

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
The idea

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.

curr = 1 1 1 2 3 3 null curr next is 1 too: skip it, stay curr = 1 1 2 3 3 null curr next is 2: new value, move curr = 3 1 2 3 3 null curr next is 3 too: skip it, stay Result: 1 → 2 → 3
The coral node is the duplicate. The accent arc is the new arrow that jumps over it. curr only moves when the next value is new.
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.

Remember

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".

Watch out

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.

LC 203Remove Linked List ElementsEasyDummy NodeLeetCode
The problem in easy words

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
The idea

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.

Step 1 D 6 1 6 null curr curr.next is 6: unlink it Step 2 D 1 6 null curr curr.next is 1: keep, move Step 3 D 1 6 null curr curr.next is 6: unlink it Return dummy.next → 1 (the old head 6 is gone)
curr always looks one node ahead. Bad next node: jump over it and stay. Good next node: move onto it.
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.

Remember

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.

Watch out

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.

LC 160Intersection of Two Linked ListsEasyTwo PointersLeetCode
The problem in easy words

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 idea

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.

A B 4 1 5 6 1 8 4 5 null shared tail A alone: 2 nodes B alone: 3 nodes shared: 3 nodes a walks 4 1 8 4 5 null | 5 6 1 | 8 b walks 5 6 1 8 4 5 null | 4 1 | 8 Both walk 2 + 3 + 3 nodes (+ one null), so they land on 8 at the same moment
After the swap, both pointers have walked exactly 2 + 3 + 3 nodes, so they step onto node 8 at the same time.
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.

Remember

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.

Watch out

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.

LC 234Palindrome Linked ListEasyFast & Slow + ReversalLeetCode
The problem in easy words

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
The idea

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.

1. find middle 1 2 2 1 null slow fast fast is null: stop 2. reverse from slow 1 2 2 1 null left right second half now points back 3. compare 1 = 1, 2 = 2, right is null palindrome
After reversing, "right" starts at the old tail and walks back toward the middle, while "left" walks forward from the head.
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.)

Remember

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.

Watch out

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.

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 problem in easy words

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
The idea

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.

f(5) f(4) f(3) f(3) f(2) f(2) f(1) f(2) f(1) f(1) f(0) f(1) f(0) f(1) f(0) computed the first time base case, instant repeat: memo answers it
Without memo, every box runs. With memo, the coral repeats are read from the notebook and the dashed parts under them never happen.
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.)

Remember

If a recursion tree has the same call appearing many times, add a memo. That single change is the doorway into Dynamic Programming.

Watch out

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.

LC 231Power of TwoEasyBit ManipulationLeetCode
The problem in easy words

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
The idea

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.

n = 8 1 0 0 0 n − 1 = 7 0 1 1 1 AND 0 0 0 0 is 0 → true n = 6 0 1 1 0 n − 1 = 5 0 1 0 1 AND 0 1 0 0 not 0 → false
With one 1-bit, n and n − 1 share no 1s. With two or more 1-bits, the higher ones survive the AND.
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.

Remember

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.

Watch out

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.

LC 191Number of 1 BitsEasyBit ManipulationLeetCode
The problem in easy words

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
The idea

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.

start 1 0 1 1 n = 11 count = 0 n &= n − 1 1 0 1 0 n = 10 count = 1 n &= n − 1 1 0 0 0 n = 8 count = 2 n &= n − 1 0 0 0 0 n = 0 count = 3 stop, answer 3 coral = next 1 to be removed
Three 1-bits, three loop turns. The zeros in between are never looked at.
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.)

Remember

To loop over only the 1-bits of a number, repeat n &= n - 1. It is called Brian Kernighan's trick.

Watch out

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.

LC 338Counting BitsEasyBits + DPLeetCode
The problem in easy words

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
The idea

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.

i binary ans[i] 00 11 210 311 4100 5101 6110 7111 0 1 1 2 1 2 2 3 6 = 110 → 6>>1 = 3 (11), last bit 0 → 2 + 0 = 2 7 = 111 → 7>>1 = 3 (11), last bit 1 → 2 + 1 = 3
Every number points back to its half, which was filled in earlier. One lookup and one add per cell.
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.

Remember

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.

Watch out

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.

LC 9Palindrome NumberEasyMath / DigitsLeetCode
The problem in easy words

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
The idea

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.

x (left part)rev (right part, reversed) start 1 2 2 1 0 step 1 1 2 2 1 rev = 0·10 + 1 = 1 step 2 1 2 1 2 rev = 1·10 + 2 = 12 x = 12 is no longer bigger than rev = 12 → stop. x == rev → palindrome, true
Coral is the digit about to move. For an odd length like 12321 you stop at x = 12, rev = 123, and compare x with rev / 10 (dropping the middle digit).
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.

Remember

% 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.

Watch out

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

Stage 4 — Trees & graphsThink recursively: trees, heaps and your first graph traversals.

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
The problem in easy words

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
The idea

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.

1 2 3 4 5 4th 2nd 5th 1st 3rd blue = pushed while sliding left actionstack (top right) slide left[1, 2, 4] pop 4, out 4[1, 2] pop 2, out 2, go to 5[1, 5] pop 5, out 5[1] pop 1, out 1, go to 3[3] pop 3, out 3[ ] output: 4, 2, 5, 1, 3
The stack is a list of "parents waiting for their turn". A node is visited only after its whole left side is done.
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".

Remember

Inorder = left, me, right. On a BST, inorder gives the values in sorted order, which is why this traversal matters so much.

Watch out

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.

LC 144Binary Tree Preorder TraversalEasyTree DFSLeetCode
The problem in easy words

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
The idea

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.

1 2 3 4 5 1st 2nd 5th 3rd 4th actionstack (top right) start[1] pop 1, push 3, 2[3, 2] pop 2, push 5, 4[3, 5, 4] pop 4 (leaf)[3, 5] pop 5 (leaf)[3] pop 3 (leaf)[ ] output: 1, 2, 4, 5, 3
Node 3 waits at the bottom of the stack the whole time the left side (2, 4, 5) is being explored.
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.

Remember

Preorder = me, left, right. It is the order you use to copy or serialize a tree, because each parent is written before its children.

Watch out

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.

LC 104Maximum Depth of Binary TreeEasyTree DFSLeetCode
The problem in easy words

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)
The idea

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.

3 9 20 15 7 returns 3 returns 1 returns 2 returns 1 returns 1 answers flow bottom-up depth(15) = 1 + max(0,0) = 1 depth(7) = 1 + max(0,0) = 1 depth(9) = 1 + max(0,0) = 1 depth(20) = 1 + max(1,1) = 2 depth(3) = 1 + max(1,2) = 3
Leaves answer first. Each parent adds itself (the "+1") to its deeper child's answer.
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.

Remember

The core tree template: handle null, ask left, ask right, combine. Depth, size, sum, balanced check and diameter are all this same shape.

Watch out

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.

LC 226Invert Binary TreeEasyTree DFSLeetCode
The problem in easy words

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
The idea

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.

before 4 2 7 1 3 6 9 swap after 4 7 2 9 6 3 1 the blue and green halves trade sides, and inside each half the children swap too
One swap per node, done at every node, mirrors the whole tree.
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).

Remember

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.

Watch out

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.

LC 100Same TreeEasyTree DFSLeetCode
The problem in easy words

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
The idea

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.

p 1 2 null q 1 null 2 pair 1 · roots 1 vs 1 → equal, go deeper pair 2 · left children 2 vs null → return false the right pair is never checked: && stops at the first false
Both fingers move the same way at the same time. The moment the shapes disagree, we stop.
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.

Remember

"Compare two trees" = recurse on pairs of nodes. The three base cases (both null, one null, values differ) come first, in that order.

Watch out

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.

LC 101Symmetric TreeEasyTree DFSLeetCode
The problem in easy words

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
The idea

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.

1 2 2 3 4 4 3 mirror line isMirror(2, 2): values equal outside: isMirror(3, 3) → true a.left with b.right inside: isMirror(4, 4) → true a.right with b.left
Same colours are mirror partners. Blue pairs are the outside edges, yellow pairs meet in the middle.
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.

Remember

Symmetric = "Same Tree" between the two halves, with the children crossed. Learn the pair-recursion shape once and reuse it.

Watch out

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.

LC 112Path SumEasyTree DFSLeetCode
The problem in easy words

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
The idea

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.

5 4 8 11 13 4 7 2 1 left 17 left 13 left 2 left −5 left 0 budget = 22 5 → 22 − 5 = 17 4 → 17 − 4 = 13 11 → 13 − 11 = 2 7 (leaf) → −5, no 2 (leaf) → 0, yes return true, stop
The budget shrinks on the way down. Only a leaf is allowed to check "is it exactly 0?". The right side (8, 13, 4, 1) is never visited.
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.

Remember

Pass information down the tree as a parameter (here: the remaining budget). Pass answers up with the return value.

Watch out

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.

LC 543Diameter of Binary TreeEasyTree DFSLeetCode
The problem in easy words

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 idea

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

1 2 3 4 5 h = 3 h = 2 h = 1 h = 1 h = 1 node left + right best 4 0 + 0 = 0 0 5 0 + 0 = 0 0 2 1 + 1 = 2 2 3 0 + 0 = 0 2 1 2 + 1 = 3 3 diameter = 3 (path 4 - 2 - 1 - 3)
Post-order: children report their heights first, then the parent checks "what if the longest path bends at me?".
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.

Remember

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.

Watch out

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.

LC 108Convert Sorted Array to Binary Search TreeEasyDivide & ConquerLeetCode
The problem in easy words

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
The idea

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.

-10 -3 0 5 9 lo = 0 mid = 2 hi = 4 0 -10 -3 5 9 left half [0..1] mid = 0 → -10 right half [3..4] mid = 3 → 5
Colours show which slice of the array each part of the tree came from. Nothing is copied: only lo and hi change.
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.

Remember

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.

Watch out

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.

LC 700Search in a Binary Search TreeEasyBSTLeetCode
The problem in easy words

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
The idea

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.

4 2 7 1 3 skipped looking for val = 2 at 4 2 < 4 → go left at 2 2 == 2 → return this node 2 comparisons instead of visiting all 5 nodes
Each step drops one whole side of the tree, just like binary search drops half the array.
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.

Remember

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

Watch out

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

LC 235Lowest Common Ancestor of a Binary Search TreeMediumBSTLeetCode
The problem in easy words

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
The idea

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.

6 2 8 0 4 7 9 3 5 pq p = 3, q = 5 at 6: both < 6 → go left at 2: both > 2 → go right at 4: 3 < 4 < 5 split → answer 4
While p and q are on the same side, keep walking down that side. The first node that sits between them is the LCA.
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.

Remember

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.

Watch out

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.

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
The problem in easy words

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
The idea

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.

heap (heaviest first)two polls → result 8 7 4 2 1 1 8 − 7 = 1 → add 1 back 4 2 1 1 1 4 − 2 = 2 → add 2 back 2 1 1 1 2 − 1 = 1 → add 1 back 1 1 1 1 − 1 = 0 → both gone 1 one stone left → answer 1
Red = the two heaviest, polled this round. Only the leftover piece goes back in; the heap reorders itself in O(log n).
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.

Remember

"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.

Watch out

Forgetting reverseOrder() gives you the two lightest stones. And the heap can end empty ([3, 3] → 0), so don't call peek() without checking.

LC 703Kth Largest Element in a StreamEasyHeapLeetCode
The problem in easy words

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
The idea

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.

club after the constructor (k = 3) 4 5 8 top = 3rd largest 2 was kicked out addkick outclub (min on top)return 33{4, 5, 8}4 54{5, 5, 8}5 105{5, 8, 10}5 95{8, 9, 10}8 44{8, 9, 10}8 the club never holds more than k numbers
Like a leaderboard with k seats: a newcomer must beat the weakest seated player, who then leaves.
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.

Remember

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.

Watch out

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.

LC 215Kth Largest Element in an ArrayMediumTop K / HeapLeetCode
The problem in easy words

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
The idea

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.

nums = [3, 2, 1, 5, 6, 4], k = 2 (min-heap, smallest on the left) add 3 → [3] add 2 → [2, 3] add 1 → [1, 2, 3] size 3 > 2, poll 1 → [2, 3] add 5 → [2, 3, 5] size 3 > 2, poll 2 → [3, 5] add 6 → [3, 5, 6] size 3 > 2, poll 3 → [5, 6] add 4 → [4, 5, 6] size 3 > 2, poll 4 → [5, 6] peek → 5 the smaller of the top 2 = 2nd 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).

Remember

"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.

Watch out

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.

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
The problem in easy words

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
The idea

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.

before 1 1 1 1 1 0 1 0 1 start (1, 1) DFS fills after 2 2 2 2 2 0 2 0 1 stays 1: touches only diagonally
The paint spreads through 4-direction neighbours only. Zeros act like walls, and diagonal cells are not connected.
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.

Remember

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.

Watch out

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.

LC 997Find the Town JudgeEasyIn/Out DegreeLeetCode
The problem in easy words

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
The idea

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.

1 2 3 trusts score = in − out (n = 3, judge needs 2) person 1: in 0, out 1 → −1 person 2: in 0, out 1 → −1 person 3: in 2, out 0 → +2 2 == n − 1 → judge is 3
Read the trust list once, update two scores per pair, then look for the score n − 1.
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.

Remember

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.

Watch out

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.

LC 1971Find if Path Exists in GraphEasyBFSLeetCode
The problem in easy words

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
The idea

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.

0 1 2 3 4 5 source destination no edge crosses here queue: [0] → [1, 2] → [2] → [] visited {0, 1, 2} 5 never seen → false
BFS explores everything connected to the source, and nothing else. An empty queue means "all reachable nodes checked".
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).

Remember

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

Watch out

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.

LC 200Number of IslandsMediumGrid DFSLeetCode
The problem in easy words

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
The idea

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.

1 2 3 scan row by row; grey = water (0,0) land → count 1, sink 4 cells (2,2) land → count 2, sink 1 cell (3,3) land → count 3, sink 2 cells every other cell is water or already sunk → answer 3
The number marks where the scan first lands on each island. Sinking makes the rest of that island look like water.
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.)

Remember

"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.

Watch out

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.

Stage 5 — First algorithmsMake the best choice (greedy) and reuse answers (dynamic programming).

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
The problem in easy words

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
The idea

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.

children (g) 1 2 3 cookies (s) 1 1 3 fits too small for 2: skip fits 2 no cookie left answer 2
Example 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.

Remember

Matching two groups (children and cookies, people and boats, tasks and workers)? Sort both and match smallest with smallest using two pointers.

Watch out

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.

LC 860Lemonade ChangeEasyGreedyLeetCode
The problem in easy words

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
The idea

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.

customer pays change given $5 bills $10 bills $5 $5 $5 $10 $20 none none none 5 10 + 5 1 2 3 2 1 0 0 0 1 0 everyone got change → true (and one $5 is still saved)
For the $20, using the $10 keeps a $5 in hand. Paying 5 + 5 + 5 would also work here, but it drains the bills you need most.
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.

Remember

When you have a choice of what to spend, spend the least flexible thing first and keep the most flexible one for later.

Watch out

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.

LC 122Best Time to Buy and Sell Stock IIMediumGreedyLeetCode
The problem in easy words

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
The idea

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.

715 364 +4 +3 day 0day 1 day 2day 3 day 4day 5 buy (sea) at each valley, sell (coral) at each peak → profit = 4 + 3 = 7
Highlighted segments are the rises. Going downhill costs nothing because you simply do not hold the stock then.
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.

Remember

A big gain can be broken into many small consecutive gains. When trades are unlimited, sum all positive differences.

Watch out

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.

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
The problem in easy words

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
The idea

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.

ways stair 1stair 2 stair 3stair 4 stair 5 1 2 3 5 8 5 + 3 last move: a 2-step from stair 3 or a 1-step from stair 4 (prev2, prev1): (1, 2) → (2, 3) → (3, 5) → (5, 8) answer = prev1 = 8
Each cell is the sum of the two cells before it. The pair of variables slides right, forgetting what it no longer needs.
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.

Remember

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.

Watch out

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.

LC 746Min Cost Climbing StairsEasy1D DPLeetCode
The problem in easy words

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
The idea

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.

stair cost dp 01 2top 1015 20— 0 0 10 15 0 + 15 (jump 2 from stair 1) dp[2] = min(dp[1] + 15, dp[0] + 10) = min(15, 10) = 10 dp[top] = min(dp[2] + 20, dp[1] + 15) = min(30, 15) = 15
The cheapest route skips stair 2 entirely: start free on stair 1, pay 15 once, jump to 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.

Remember

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").

Watch out

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.

LC 118Pascal's TriangleEasyDP (row by row)LeetCode
The problem in easy words

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 idea

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.

1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 row 0row 1row 2 row 3row 4 row4[2] = row3[1] + row3[2] = 6 blue edges: always 1
Each inner number is looked up from the finished row above: one addition per cell.
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.

Remember

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".

Watch out

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.

LC 198House RobberMedium1D DP (take / skip)LeetCode
The problem in easy words

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
The idea

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.

money best 2 7 9 3 1 house 0house 1 house 2house 3 house 4 2 7 11 11 12 max(7, 2+9) max(11, 7+3) max(11, 11+1) rob skip rob yellow houses robbed: 2 + 9 + 1 = 12, no two side by side
The "best" row is the DP table. Each cell looks back one cell (skip) and two cells (rob) and keeps the larger.
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.

Remember

"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.

Watch out

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.