androidinterview.com

Android Coding Interview Questions

55 questions

Tier
Difficulty
Level

Showing all 55 questions

General

Which data structures should an Android developer know for the next interview?

Tier: CommonDifficulty: Easy

A short, practical list covers almost everything an Android interview actually tests, and it lines up closely with structures you already use in real Android code, not abstract theory.

  • Arrays and ArrayList. The default for ordered data, and the base most other structures are explained in terms of.
  • HashMap and HashSet. By far the most common tool in these interview questions, anything that says "find duplicates," "count occurrences," or "check membership fast" wants one of these, O(1) average lookup is the reason.
  • Linked lists. Less common in app code directly, but a frequent interview topic on its own, cycle detection and palindrome checks both lean on the same two pointer technique.
  • Stacks and queues. Stacks show up in anything with undo behavior or matching pairs like parentheses, queues show up in MessageQueue itself, which is worth connecting back to if it comes up.
  • Trees, specifically binary trees and BSTs. Traversal order, depth, and balance are the recurring questions, and a RecyclerView's view hierarchy or a JSON payload are both trees in practice, which is a natural bridge if an interviewer asks why this matters for app work.
  • Heaps or priority queues. Less frequent, but the right answer whenever a question is really "give me the smallest or largest n things," like the minimum meeting rooms problem.

The honest framing for an interview is that HashMap and HashSet alone answer a large share of the DSA questions you'll actually be asked, so if time is short, that's the one to be fluent in cold, everything else is worth recognizing and being able to reason through, even if it's not muscle memory.

Arrays & Sorting

Best time to buy and sell a stock, one transaction.

Tier: EssentialDifficulty: Easy

You get one buy and one later sell over an array of daily prices, and you have to return the largest profit possible. Go through the prices once carrying the cheapest day seen so far, and at every day ask what selling today against that cheapest day would make.

The problem

You are given an array where each slot is the price of the stock on that day. You return one number, the largest profit a single buy and a single later sell can make. You must buy before you sell, and if no trade makes money the answer is 0, not the smallest loss.

Input prices = [7, 1, 5, 3, 6, 4].

Output 5.

7011buy523364sell45
Prices by day with the best trade marked
Buying on day 1 at a price of 1 and selling on day 4 at 6 makes 5, and no other legal pair of days beats it.
  • Day 0 costs 7. It is the cheapest so far, and selling into itself makes nothing.
  • Day 1 costs 1, which is cheaper, so the cheapest day so far drops to 1.
  • Day 2 costs 5. Selling here against 1 makes 4, so the best answer so far is 4.
  • Day 4 costs 6. Selling here against 1 makes 5, which no later day beats.

Kotlin

fun maxProfit(prices: IntArray): Int {
    var min = Int.MAX_VALUE
    var best = 0
    for (p in prices) {
        min = minOf(min, p)
        best = maxOf(best, p - min)
    }
    return best
}

Java

static int maxProfit(int[] prices) {
    int min = Integer.MAX_VALUE;
    int best = 0;
    for (int p : prices) {
        min = Math.min(min, p);
        best = Math.max(best, p - min);
    }
    return best;
}

The rule is that you must buy before you sell, and this loop respects that for free. When you are standing on day p, min only ever holds prices from days at or before it, so p - min is always a legal trade. Updating min before you compute the profit is safe, because buying and selling on the same day just yields zero.

best starts at zero, and that is the deliberate part. If prices only ever fall, no trade is worth making, so the correct answer is to do nothing and take zero rather than the least bad loss.

The naive answer is the nested loop over every buy day and every later sell day, which is O(n squared). This is O(n) time and O(1) space. The follow up is unlimited transactions. That one collapses to something even smaller, add up every upward step, meaning sum prices[i] - prices[i - 1] whenever it is positive, because any profitable run can be sliced into single day gains.

Practice 121. Best Time to Buy and Sell Stock (opens in a new tab)122. Best Time to Buy and Sell Stock II, unlimited transactions (opens in a new tab)

Watch

Two Sum.

Tier: EssentialDifficulty: Easy

You are given an array and a target, and you have to return the positions of the two numbers that add up to that target. Walk the array once, and at each number ask a hash map whether the number that completes the pair has already gone by.

The problem

You are given an array of integers and one target integer. You return the positions of the two numbers that add up to the target, not the numbers themselves. The array is not sorted, exactly one pair works, and a number cannot pair with itself.

Input nums = [2, 6, 5, 8, 11] and target = 14.

Output [1, 3].

20615283114
The input array with the answer marked
The two accent cells hold 6 and 8, which add to 14, and the numbers under the strip are the indices you return.
  • Index 0 holds 2, so you need 12. The map is empty, so store 2 at index 0.
  • Index 1 holds 6, so you need 8. Not there, so store 6 at index 1.
  • Index 2 holds 5, so you need 9. Not there, so store 5 at index 2.
  • Index 3 holds 8, so you need 6. The map has 6 at index 1, so the answer is [1, 3].

Kotlin

fun twoSum(nums: IntArray, target: Int): IntArray {
    val seen = HashMap<Int, Int>()
    for (i in nums.indices) {
        val need = target - nums[i]
        val j = seen[need]
        if (j != null) return intArrayOf(j, i)
        seen[nums[i]] = i
    }
    return intArrayOf(-1, -1)
}

Java

static int[] twoSum(int[] nums, int target) {
    var seen = new HashMap<Integer, Integer>();
    for (int i = 0; i < nums.length; i++) {
        int need = target - nums[i];
        if (seen.containsKey(need)) return new int[] { seen.get(need), i };
        seen.put(nums[i], i);
    }
    return new int[] { -1, -1 };
}

The map turns "is this value somewhere in the array" from a scan into a single lookup. It maps a value to the index it was found at, so when the lookup hits you can hand back both positions. Checking before you insert is what stops a number from pairing with itself when the target is exactly double it.

The naive answer, and the one to say out loud first, is the nested loop over every pair, which is O(n squared). This is O(n) time and O(n) space for the map. That is the trade you name in the interview, you spend memory to buy back the inner loop.

If the interviewer says only the values matter and not the indices, there is a two pointer variant. Sort the array, put one pointer at each end, and move the left one in when the sum is too small and the right one in when it is too big. That is O(n log n) time and O(1) extra space, which wins when the input already arrived sorted.

Practice 1. Two Sum (opens in a new tab)167. Two Sum II - Input Array Is Sorted, the two pointer version (opens in a new tab)

Watch

Find the subarray with the maximum sum.

Tier: EssentialDifficulty: Medium

Find the largest sum any contiguous stretch of the array can produce. Walk it once carrying a running sum, throw that sum away the moment it goes negative, and keep the best value you ever saw. That is Kadane's algorithm.

The problem

You are given an array of integers, and the values can be negative. A subarray is a contiguous stretch of it, so you may not skip elements. Return the largest sum any such stretch can reach, the sum alone and not the stretch itself. It must hold at least one element, so an array of only negative numbers answers with its largest single value.

Input nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

Output 6

-2011-3243-142516-5748
The input, with the winning stretch marked
The stretch 4, -1, 2, 1 adds up to 6, and no other contiguous stretch of this array beats it.
  • The running sum hits -2 at the first element, which is negative, so it resets to 0
  • It climbs to 1, then -2 at the third element, so it resets again and the best so far stays 1
  • From 4 it goes 4, 3, 5, 6, and each of those updates the best, which lands on 6
  • The -5 pulls it to 1 and the final 4 only reaches 5, so 6 survives as the answer
-2011-22433455661758
The running sum at each position
The running sum is thrown away whenever it goes negative, and the highest value it ever reaches is the answer.

Kotlin

fun maxSubarraySum(nums: IntArray): Int {
    var best = nums[0]
    var sum = 0
    for (n in nums) {
        sum += n
        best = maxOf(best, sum)
        if (sum < 0) sum = 0
    }
    return best
}

Java

static int maxSubarraySum(int[] nums) {
    int best = nums[0];
    int sum = 0;
    for (int n : nums) {
        sum += n;
        best = Math.max(best, sum);
        if (sum < 0) sum = 0;
    }
    return best;
}

The reset is the whole trick. If the numbers behind you add up to something negative, dragging them along can only make whatever comes next smaller, so you drop them and start fresh. A prefix that sums to zero or more is still worth keeping, because it can only help.

The naive answer is to try every start and every end, which is O(n squared), or O(n cubed) if you re-add the slice each time. Kadane does it in O(n) time and O(1) space, since each element is looked at once and you hold two numbers.

The trap is starting best at zero, which returns zero for an all negative array when the right answer is the largest single element. Starting at nums[0] and updating before the reset handles it. The usual follow up is printing the subarray. Keep a start that moves to the next index on every reset, and record it alongside the current index whenever best improves.

Practice 53. Maximum Subarray (opens in a new tab)918. Maximum Sum Circular Subarray, the wrap-around variant (opens in a new tab)

Watch

Implement merge sort.

Tier: EssentialDifficulty: Medium

Merge sort sorts an array by splitting it in half until every piece is one element, then merging sorted pieces back together. The merge is the only real work, walking two sorted halves with two pointers and always taking the smaller front value.

The problem

You get an unsorted array of integers and return it sorted into non decreasing order, using divide and conquer rather than a library call. Duplicates are allowed. The bound has to be O(n log n) on every input, not just on average, which is the reason this and not quicksort.

Input, [3, 1, 4, 2].

Output, [1, 2, 3, 4].

10i3122j43
The two halves before the last merge
Each half is sorted on its own, and the merge takes the smaller of the two values under the pointers.
  • Split into [3, 1] and [4, 2], then split each again into single elements.
  • Merge 3 with 1 to get [1, 3], and merge 4 with 2 to get [2, 4].
  • Merge [1, 3] with [2, 4], taking 1, then 2, then 3, then 4, and the array is sorted.

Kotlin

fun mergeSort(arr: IntArray, left: Int = 0, right: Int = arr.size - 1) {
    if (left >= right) return
    val mid = left + (right - left) / 2
    mergeSort(arr, left, mid)
    mergeSort(arr, mid + 1, right)
    merge(arr, left, mid, right)
}

private fun merge(arr: IntArray, left: Int, mid: Int, right: Int) {
    val temp = IntArray(right - left + 1)
    var i = left
    var j = mid + 1
    var k = 0
    while (i <= mid && j <= right) temp[k++] = if (arr[i] <= arr[j]) arr[i++] else arr[j++]
    while (i <= mid) temp[k++] = arr[i++]
    while (j <= right) temp[k++] = arr[j++]
    temp.copyInto(arr, left)
}

Java

static void mergeSort(int[] arr) { mergeSort(arr, 0, arr.length - 1); }

static void mergeSort(int[] arr, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid);
    mergeSort(arr, mid + 1, right);
    merge(arr, left, mid, right);
}

static void merge(int[] arr, int left, int mid, int right) {
    int[] temp = new int[right - left + 1];
    int i = left, j = mid + 1, k = 0;
    while (i <= mid && j <= right) temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];
    System.arraycopy(temp, 0, arr, left, temp.length);
}

A single element is trivially sorted, so the recursion bottoms out for free. The merge is linear in the size of the two halves combined and there are log n levels of splitting, so the total is O(n log n) on every input. There is no unlucky ordering that makes it worse.

It needs O(n) extra space for temp, since you cannot overwrite a half you are still reading from. Using <= rather than < in the comparison is what makes it stable, an element from the left half wins ties, so equal values never swap their original order.

Quicksort sorts in place with O(log n) space and is usually faster in practice thanks to cache locality, but its worst case is O(n squared) on a bad pivot. Merge sort trades memory for a guaranteed bound, which is why you reach for it when you need stability, or when sorting a linked list, or data too big to hold in memory.

Practice 912. Sort an Array (opens in a new tab)148. Sort List (opens in a new tab)

Watch

Find the leaders in an array.

Tier: CommonDifficulty: Easy

A leader is an element with nothing bigger anywhere to its right. Scan from the right end carrying the largest value seen so far, and take every element that beats it.

The problem

You are given an array of integers. An element is a leader when every element to its right is smaller than it. Return the leaders as values, in the order they appear in the array. The last element is always a leader, since nothing sits to its right at all.

Input nums = [10, 22, 12, 3, 0, 6]

Output [22, 12, 6]

100221122330465
The input, with the leaders marked
22, 12 and 6 each beat everything that comes after them, and 10, 3 and 0 do not.
220221122636465
The largest value from each position to the end
A position is a leader exactly where its own value equals the largest value from there onward.
  • Coming from the right, 6 beats an empty tail, so it is a leader and the running max becomes 6
  • 0 and 3 both lose to 6, so neither is a leader
  • 12 beats 6, so it is a leader and the max becomes 12
  • 22 beats 12, so it is a leader and the max becomes 22, then 10 loses and is dropped
  • Collected backwards that is [6, 12, 22], so reverse it to get [22, 12, 6]

Kotlin

fun leaders(nums: IntArray): List<Int> {
    val out = mutableListOf<Int>()
    var max = Int.MIN_VALUE
    for (i in nums.indices.reversed()) {
        if (nums[i] > max) {
            out.add(nums[i])
            max = nums[i]
        }
    }
    return out.reversed()
}

Java

static List<Integer> leaders(int[] nums) {
    var out = new ArrayList<Integer>();
    int max = Integer.MIN_VALUE;
    for (int i = nums.length - 1; i >= 0; i--) {
        if (nums[i] > max) {
            out.add(nums[i]);
            max = nums[i];
        }
    }
    Collections.reverse(out);
    return out;
}

Going right to left is the whole idea. The question about each element is entirely about what sits after it, and arriving from the right means you already know that, it is the running max. A fresh scan of the tail at every position collapses into one comparison.

Because you collect them backwards you reverse at the end to put them back in array order. Starting the max at the smallest possible integer is what makes the last element a leader without a special case.

It is O(n) time and O(n) space for the output, or O(1) beyond it. The naive answer is a nested loop that checks every element against its whole tail, which is O(n squared). Pin one thing down before you write anything, ask whether an equal value to the right still counts. This code uses strictly greater, so in a run of equal values only the last one is a leader.

Watch

Remove duplicates from a sorted array in place.

Tier: CommonDifficulty: Easy

The array is sorted and you must collapse each run of equal values to one, without a second array. Use two pointers, one scanning for a value you have not kept yet, the other marking the end of the cleaned up front.

The problem

The array is sorted, so every copy of a value sits in one unbroken run. Rewrite the front of the array in place so it holds each distinct value once, in order, and return how many there are. You get no second array, and whatever sits past the returned length is ignored rather than cleared.

Input, [1, 1, 2, 2, 2, 3].

Output, 3, with the array now starting 1, 2, 3.

10keep11scan22232435
Before
The keep pointer marks the last value kept, and the scan pointer hunts for a value that differs from it.
102132232435
After
The first three slots hold the distinct values, and the tail past the returned length is left as it was.
  • The keep pointer sits on the first 1. The scan pointer sees another 1, a match, so it moves on.
  • The scan reaches 2, which differs, so the keep pointer steps to index 1 and 2 is written there.
  • The next two 2s match what was kept, so both are skipped.
  • The scan reaches 3, so the keep pointer steps to index 2, 3 is written, and the length is 2 plus 1.

Kotlin

fun removeDuplicates(nums: IntArray): Int {
    if (nums.isEmpty()) return 0
    var i = 0
    for (j in 1 until nums.size) {
        if (nums[j] != nums[i]) {
            i++
            nums[i] = nums[j]
        }
    }
    return i + 1
}

Java

static int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;
    int i = 0;
    for (int j = 1; j < nums.length; j++) {
        if (nums[j] != nums[i]) {
            i++;
            nums[i] = nums[j];
        }
    }
    return i + 1;
}

Because the array is sorted, every copy of a value sits in one unbroken run. So the only comparison you need is against nums[i], the last value you kept. If nums[j] matches it you are still inside that run and you skip. If it differs the run has ended, so you make room at i + 1 and write the new value there.

Everything from index zero to i is the answer, and you return i + 1 as the new length. The tail beyond that is left as it was, which is why the caller gets a length rather than a shorter array. Java and Kotlin arrays cannot resize in place.

It is O(n) time and O(1) extra space, one pass and two integers. The naive version builds a new list or a set and copies back, which costs O(n) space and, with a set, loses the ordering you already had for free. The variant to be ready for is allowing each value twice. Compare against nums[i - 1] instead and start both pointers at index two.

Practice 26. Remove Duplicates from Sorted Array (opens in a new tab)80. Remove Duplicates from Sorted Array II, keep two (opens in a new tab)

Watch

Find the maximum product subarray.

Tier: CommonDifficulty: Medium

You are given an array of integers and you have to return the largest product any contiguous stretch of it can make. Run a product from the left and a product from the right in the same loop, resetting either one to 1 when it hits zero, and the best value the two of them ever reach is the answer.

The problem

You are given an array of integers that can hold negatives and zeros. You return one number, the largest product any contiguous run of neighbouring elements can make. A run of one element counts, so the answer is never an empty product.

Input nums = [2, 3, -2, 4].

Output 6.

2031-2243
The input with the winning run marked
The two accent cells multiply to 6, and every longer run drags in the -2 and turns negative.
  • The prefix product goes 2, then 6, then -12, then -48.
  • The suffix product runs the other way, 4, then -8, then -24, then -48.
  • The best value either scan ever showed is 6, which is the run [2, 3].
  • On [-2, 0, -1] the zero resets both scans, so the answer is 0 rather than 2.

Kotlin

fun maxProductSubarray(nums: IntArray): Int {
    var best = Int.MIN_VALUE
    var prefix = 1
    var suffix = 1
    for (i in nums.indices) {
        if (prefix == 0) prefix = 1
        if (suffix == 0) suffix = 1
        prefix *= nums[i]
        suffix *= nums[nums.size - 1 - i]
        best = maxOf(best, maxOf(prefix, suffix))
    }
    return best
}

Java

static int maxProductSubarray(int[] nums) {
    int best = Integer.MIN_VALUE;
    int prefix = 1;
    int suffix = 1;
    for (int i = 0; i < nums.length; i++) {
        if (prefix == 0) prefix = 1;
        if (suffix == 0) suffix = 1;
        prefix *= nums[i];
        suffix *= nums[nums.length - 1 - i];
        best = Math.max(best, Math.max(prefix, suffix));
    }
    return best;
}

Products do not behave like sums, and that is the whole difficulty. A long stretch of negatives can hold a huge positive product, so a running value that looks terrible right now can flip to the best answer on the very next multiplication. That is why you cannot just drop a bad prefix the way Kadane does.

Zeros are what let two simple scans cover it. A zero cuts the array into independent pieces, since no winning subarray ever crosses one. Inside a piece the number of negatives is either even, in which case the whole piece is the best product and the prefix scan finds it, or odd, in which case the answer skips everything up to the first negative or everything after the last one. One of those two is a prefix of the piece and the other is a suffix, so scanning from both ends catches both.

It is O(n) time and O(1) space, one pass and three numbers. The naive answer is every subarray at O(n squared). The trap is treating this like Kadane and resetting on a negative running product, that throws away exactly the case a later negative was going to rescue.

Practice 152. Maximum Product Subarray (opens in a new tab)53. Maximum Subarray, the additive version to contrast (opens in a new tab)

Watch

Find the repeating and the missing number in an array of 1 to n.

Tier: CommonDifficulty: Medium

One value between 1 and n appears twice and another never appears at all, and you have to name both. Two unknowns need two equations, so compare the array's sum and its sum of squares against the sums a clean 1 to n would give.

The problem

You are given an unsorted array of n values, each between 1 and n. Exactly one value appears twice and exactly one value never appears at all. Return the repeating value first and the missing value second. The array is not sorted, so there is no run to look for a break in.

Input nums = [3, 1, 2, 5, 4, 6, 7, 5], so n is 8

Output [5, 8]

3011225344657657
The input array
The value 5 sits at index 3 and again at index 7, and no cell anywhere holds an 8.
1021324354657687
What a clean 1 to 8 holds
Every value appears exactly once here, and the two sums below compare the input against this.
  • The array sums to 33 while 1 through 8 sums to 36, so the repeating value minus the missing one is -3
  • The squares sum to 165 while the squares of 1 through 8 sum to 204, so the difference of squares is -39
  • Dividing -39 by -3 gives 13, which is the two values added together
  • A difference of -3 and a sum of 13 can only be 5 and 8, so 5 repeats and 8 is gone

Kotlin

fun repeatingAndMissing(nums: IntArray): IntArray {
    val n = nums.size.toLong()
    var sum = 0L
    var squares = 0L
    for (v in nums) {
        sum += v
        squares += v.toLong() * v
    }
    val diff = sum - n * (n + 1) / 2
    val total = (squares - n * (n + 1) * (2 * n + 1) / 6) / diff
    val repeating = (diff + total) / 2
    return intArrayOf(repeating.toInt(), (repeating - diff).toInt())
}

Java

static int[] repeatingAndMissing(int[] nums) {
    long n = nums.length;
    long sum = 0;
    long squares = 0;
    for (int v : nums) {
        sum += v;
        squares += (long) v * v;
    }
    long diff = sum - n * (n + 1) / 2;
    long total = (squares - n * (n + 1) * (2 * n + 1) / 6) / diff;
    long repeating = (diff + total) / 2;
    return new int[] { (int) repeating, (int) (repeating - diff) };
}

Call the repeated value x and the missing one y. Every other number appears exactly once, so those cancel and the sum is off by exactly x minus y. The squares are off by x squared minus y squared, which factors into x minus y times x plus y. Divide that by the first difference and you have x plus y. From a difference and a sum, both values fall out with an add and a halve.

It is O(n) time and O(1) space, one pass and a few counters. The naive answers are sorting and looking for the break in the run at O(n log n), or a count array at O(n) extra space. Name those first, then say why you can do better.

The trap is overflow, and it is why Long is not optional. The sum of squares up to n is roughly n cubed over three, so an n near two thousand already blows past a 32 bit Int even though every value is small. The alternative worth naming is XOR. Xor the array with 1 through n to get x xor y, then split everything into two buckets on any set bit of that result and xor each bucket down. No overflow, but a harder story to tell out loud.

Practice 645. Set Mismatch (opens in a new tab)

Watch

Sort an array of 0s, 1s and 2s.

Tier: CommonDifficulty: MediumAsked at: paytm

With only three possible values you can sort the array in a single pass. Three pointers do it, low and high marking the boundaries of the 0s and 2s already placed, and mid scanning everything in between.

The problem

The array holds only the values 0, 1 and 2, in any order. Sort it ascending in place, in a single pass, so no library sort and no counting pass followed by an overwriting pass. Nothing is returned, the array itself is the answer.

Input, [2, 0, 2, 1, 1, 0].

Output, [0, 0, 1, 1, 2, 2].

20lowmid0122131405high
Before, with the three pointers
Everything from mid to high is still unexamined, which at the start is the whole array.
000112132425
After
The 0s end up before low, the 2s after high, and the 1s are whatever is left in the middle.
  • mid sees a 2, so it swaps with the back and high steps in, giving [0, 0, 2, 1, 1, 2].
  • mid now sees a 0, swaps with low, which is the same slot, and both step forward. The next 0 does the same.
  • mid sees a 2 at index 2, swaps with index 4, giving [0, 0, 1, 1, 2, 2], and high steps in again.
  • mid sees 1 twice and just walks past them, mid passes high, and the array is sorted.

Kotlin

fun sortColors(nums: IntArray) {
    var low = 0
    var mid = 0
    var high = nums.size - 1
    while (mid <= high) {
        when (nums[mid]) {
            0 -> swap(nums, low++, mid++)
            1 -> mid++
            else -> swap(nums, mid, high--) // the value swapped in is still unchecked
        }
    }
}

private fun swap(nums: IntArray, i: Int, j: Int) {
    val t = nums[i]
    nums[i] = nums[j]
    nums[j] = t
}

Java

static void sortColors(int[] nums) {
    int low = 0;
    int mid = 0;
    int high = nums.length - 1;
    while (mid <= high) {
        switch (nums[mid]) {
            case 0 -> swap(nums, low++, mid++);
            case 1 -> mid++;
            default -> swap(nums, mid, high--); // the value swapped in is still unchecked
        }
    }
}

static void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

Name it out loud, this is the Dutch national flag algorithm. The invariant is what each region already means at every step. Everything before low is a confirmed 0, everything from low up to mid is a confirmed 1, everything after high is a confirmed 2, and mid through high is still unexamined.

That invariant is also why swapping in a 2 does not advance mid. The value that came back from the high end has never been looked at, so it still needs checking on the next iteration. Swapping a 0 is different, the value coming back from low is always a 1 you have already cleared, so mid is safe to move.

The naive answer is worth mentioning first, counting the 0s, 1s and 2s in one pass and overwriting the array in a second. That is also O(n) time and O(1) space and perfectly correct. The three pointer version is really about doing it in a single pass, which is the specific thing this question tests.

Practice 75. Sort Colors (opens in a new tab)

Three Sum, find all unique triplets that sum to zero.

Tier: CommonDifficulty: Medium

You are given an array and you have to return every distinct triplet of values inside it that adds up to zero. Sort the array, then fix one number and solve two sum on the part to its right with two pointers closing in from both ends, skipping repeats so no triplet comes out twice.

The problem

You are given an array of integers that may hold duplicates, negatives and zeros. You return every distinct triplet of values that adds up to zero, so values and not indices. The same three values must never come back twice, however many ways the array can spell them.

Input [-1, 0, 1, 2, -1, -4].

Output [[-1, -1, 2], [-1, 0, 1]].

-10011223-14-45
The input array as it arrives
The array is unsorted and holds two copies of -1, which is where the duplicate triplets come from.
-40-11i-12lo031425hi
The sorted array at the first hit
With -1 fixed at index 1, the two pointers sit on -1 and 2, and those three accent cells sum to zero.
  • Sorting first gives [-4, -1, -1, 0, 1, 2].
  • Fix -4. The two pointers close in over the rest and nothing sums to 4, so no triplet.
  • Fix the first -1. The pointers sit on -1 and 2, which sums to zero, so record [-1, -1, 2].
  • Both pointers move in to 0 and 1, which also sums to zero, so record [-1, 0, 1].
  • Fix the second -1 and skip it as a repeat. Everything left is positive, so the scan ends.

Kotlin

fun threeSum(nums: IntArray): List<List<Int>> {
    nums.sort()
    val out = mutableListOf<List<Int>>()
    for (i in nums.indices) {
        if (i > 0 && nums[i] == nums[i - 1]) continue
        var lo = i + 1
        var hi = nums.size - 1
        while (lo < hi) {
            val sum = nums[i] + nums[lo] + nums[hi]
            if (sum < 0) {
                lo++
            } else if (sum > 0) {
                hi--
            } else {
                out.add(listOf(nums[i], nums[lo], nums[hi]))
                lo++
                hi--
                while (lo < hi && nums[lo] == nums[lo - 1]) lo++
                while (lo < hi && nums[hi] == nums[hi + 1]) hi--
            }
        }
    }
    return out
}

Java

static List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    var out = new ArrayList<List<Integer>>();
    for (int i = 0; i < nums.length; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        int lo = i + 1;
        int hi = nums.length - 1;
        while (lo < hi) {
            int sum = nums[i] + nums[lo] + nums[hi];
            if (sum < 0) {
                lo++;
            } else if (sum > 0) {
                hi--;
            } else {
                out.add(List.of(nums[i], nums[lo], nums[hi]));
                lo++;
                hi--;
                while (lo < hi && nums[lo] == nums[lo - 1]) lo++;
                while (lo < hi && nums[hi] == nums[hi + 1]) hi--;
            }
        }
    }
    return out;
}

Sorting is what makes the two pointers legal. Once the values increase left to right, a sum that is too small can only grow by pulling lo right, and a sum that is too big can only shrink by pulling hi left. So every move rules out a whole block of pairs instead of one, and the inner search is linear rather than quadratic.

Sorting also makes duplicates cheap, because equal values sit next to each other. Skipping a repeated nums[i] stops the same first element being fixed twice, and the two while loops after a hit push both pointers past their own repeats, so you never need a set to deduplicate.

It is O(n squared) time, one outer pass times a linear inner scan, and O(1) extra space if you do not count the output list. The brute force is three nested loops at O(n cubed) plus a set to strip duplicate triplets. The trap is deduplicating with a set at the end and calling it optimal, that hides the real work and costs memory you did not need.

Practice 15. 3Sum (opens in a new tab)18. 4Sum, the same pattern one level deeper (opens in a new tab)

Watch

Strings

Find the longest palindromic substring.

Tier: EssentialDifficulty: Medium

Find the longest run of characters that reads the same both ways. Treat every character and every gap between two characters as a possible centre, expand outward while the two sides match, and keep the widest match.

The problem

You are given a string. Return the longest substring of it that reads the same forwards and backwards, and any one of them when several tie for longest. A substring is contiguous, so you may not skip characters. One character on its own counts as a palindrome, so a non empty string always has an answer.

Input "babad"

Output "bab"

b0a1centreb2a3d4
The string, with the answer marked
Standing on the a at index 1 and stepping outward, both neighbours are b, so this palindrome reaches length 3.
  • Centred on the first b nothing extends, so the best so far is that one character
  • Centred on the a at index 1, both neighbours are b, so it grows to "bab" with length 3
  • Centred on the b at index 2 it grows to "aba", which ties at 3 and does not replace the winner
b0a1b2centrea3d4
The other centre that ties
The b at index 2 grows to aba, also length 3, so the earlier find keeps the answer.

Kotlin

fun longestPalindrome(s: String): String {
    var start = 0
    var maxLen = 0
    for (i in s.indices) {
        val len = maxOf(expand(s, i, i), expand(s, i, i + 1))
        if (len > maxLen) {
            maxLen = len
            start = i - (len - 1) / 2
        }
    }
    return s.substring(start, start + maxLen)
}

private fun expand(s: String, left: Int, right: Int): Int {
    var l = left
    var r = right
    while (l >= 0 && r < s.length && s[l] == s[r]) {
        l--
        r++
    }
    return r - l - 1
}

Java

static String longestPalindrome(String s) {
    int start = 0;
    int maxLen = 0;
    for (int i = 0; i < s.length(); i++) {
        int len = Math.max(expand(s, i, i), expand(s, i, i + 1));
        if (len > maxLen) {
            maxLen = len;
            start = i - (len - 1) / 2;
        }
    }
    return s.substring(start, start + maxLen);
}

static int expand(String s, int left, int right) {
    while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
        left--;
        right++;
    }
    return right - left - 1;
}

You need two kinds of centre because a palindrome can have an odd length, with one middle character, or an even length, with a middle gap. Calling expand on (i, i) covers the odd case and (i, i + 1) covers the even one, so running both from every index can never miss a palindrome.

The expand helper returns the length rather than the bounds, which is why the caller recovers the start with i - (len - 1) / 2. That one formula works for both centre kinds, and it is the line to double check on paper before you claim you are done.

There are n centres and each expands at most n times, so it is O(n squared) time and O(1) space beyond the answer. The naive answer is checking every substring for being a palindrome at O(n cubed), worth naming first so the improvement is visible. There is an O(n) answer, Manacher's algorithm, but nobody expects it live, naming it is enough. The usual trap is skipping the even centres, which silently breaks on "abba".

Practice 5. Longest Palindromic Substring (opens in a new tab)

Determine whether a string is an anagram (anagram strings based question).

Tier: CommonDifficulty: EasyAsked at: phonepe, paytm

Two strings are anagrams when one is a rearrangement of the other, so they hold the same characters in the same quantities. Count every letter of the first string up and every letter of the second down, then check that all the counts landed back on zero.

The problem

You are given two strings, and you may assume plain lowercase letters. Return true when one is a rearrangement of the other, meaning they hold the same letters in the same quantities. Order does not matter, quantity does, so "aab" and "abb" are not anagrams. Two strings of different lengths never are either.

Input a = "anagram" and b = "nagaram"

Output true

LetterCount after the first stringCount after the second
a30
n10
g10
r10
m10
  • Both strings have length 7, so the counting is worth starting
  • Walking the first string leaves a at 3, and n, g, r and m at 1 each
  • Walking the second string subtracts exactly those same letters
  • Every one of the 26 slots is back to zero, so the answer is true

Kotlin

fun isAnagram(a: String, b: String): Boolean {
    if (a.length != b.length) return false
    val counts = IntArray(26)
    for (c in a) counts[c - 'a']++
    for (c in b) counts[c - 'a']--
    return counts.all { it == 0 }
}

Java

static boolean isAnagram(String a, String b) {
    if (a.length() != b.length()) return false;
    var counts = new int[26];
    for (var c : a.toCharArray()) counts[c - 'a']++;
    for (var c : b.toCharArray()) counts[c - 'a']--;
    return Arrays.stream(counts).allMatch(n -> n == 0);
}

One array does both jobs. Adding for the first string and subtracting for the second means a surplus shows up as a positive count and a shortfall as a negative one, so a single sweep at the end settles it. The length check is a cheap early exit worth saying out loud.

There are two honest answers and it helps to name both. Sorting both strings and comparing is the easiest to write under pressure, at O(n log n) time and O(n) space. The counting version is O(n) time and O(1) space, since 26 is a constant and does not grow with the input.

Ask the interviewer whether the input is guaranteed lowercase ASCII. If it is not, a fixed array stops working and you reach for a map from character to count, still O(n) time but O(k) space for the distinct characters actually seen.

Practice 242. Valid Anagram (opens in a new tab)

Find the longest common prefix of a list of strings.

Tier: CommonDifficulty: Easy

You are given a list of strings and you have to return the longest starting stretch that every one of them shares. Scan the words vertically instead of one at a time, comparing character zero of every word, then character one, and stopping the moment a word disagrees or runs out.

The problem

You are given an array of strings. You return the longest run of characters that every one of them starts with, or an empty string when they share nothing. Comparison is exact, so casing matters, and the shortest word in the list caps how long the answer can be.

Input ["interview", "internet", "internal", "interval"].

Output "inter".

i0n1t2e3r4v5i6e7w8
The first word with the shared prefix marked
The first word is the ceiling for the answer, and the five accent characters are the ones where all four words still agree.
IndexCharacter in each wordAll four agree
0 to 3i, then n, then t, then eyes
4r, r, r, ryes
5v, n, n, vno
  • The scan is vertical, one index across all four words, rather than one whole word at a time.
  • At index 5 the first word has v and the second has n, so the scan stops there.
  • The answer is the first five characters of the first word, inter.

Kotlin

fun longestCommonPrefix(words: Array<String>): String {
    if (words.isEmpty()) return ""
    val first = words[0]
    for (i in first.indices) {
        for (word in words) {
            if (i == word.length || word[i] != first[i]) return first.substring(0, i)
        }
    }
    return first
}

Java

static String longestCommonPrefix(String[] words) {
    if (words.length == 0) return "";
    var first = words[0];
    for (int i = 0; i < first.length(); i++) {
        for (var word : words) {
            if (i == word.length() || word.charAt(i) != first.charAt(i)) {
                return first.substring(0, i);
            }
        }
    }
    return first;
}

The first word is the ceiling. The answer is a prefix of every word, so it can never be longer than the first one, and that is why the outer loop only walks its indices. At index i you check that character against index i of every other word. Two things end the scan, a word that has already run out of characters, and a word whose character differs. Either way the answer is the first i characters of the first word.

Worst case is O(n times m) for n words of length m, with O(1) extra space beyond the string you return. In practice it stops at the first mismatch, so on a real list it usually looks at very few characters.

The other answer worth having ready is to sort the words and compare only the first and the last. Sorting is alphabetical, so after the sort those two are the most different in the list. Anything they both start with is shared by everything sitting between them, so their common prefix is the answer for the whole list. It is a neat trick, though the sort makes it slower than the plain vertical scan.

Practice 14. Longest Common Prefix (opens in a new tab)

Linked Lists

Detect a cycle in a linked list (Linked List Cycle).

Tier: EssentialDifficulty: EasyAsked at: spotify

You are handed the head of a singly linked list and asked whether it loops back on itself. Walk it with two pointers, one moving a node at a time and one moving two, and see whether the fast one ever lands on the slow one.

The problem

You are handed the head of a singly linked list, which may be empty. Return true if following next from the head ever revisits a node, false if it reaches null. You may not modify the list, and the answer the interviewer wants uses no extra memory.

Input, the list 3, 2, 0, -4 where the last node points back at the node holding 2.

Output, true.

320-4
The input list
The tail points back at the second node, so walking next from the head never reaches null.
320-4slowfast
Where the pointers meet
After three steps both pointers stand on the same node, which is the proof that the list loops.
  • Both pointers start on 3. After one step slow is on 2 and fast is on 0.
  • After two steps slow is on 0 and fast has wrapped around to 2.
  • After three steps both are on -4, so they have met and the answer is true.
  • On the same list without the back link, fast would run off the end and the loop would exit false.

Kotlin

class Node(val value: Int, var next: Node? = null)

fun hasCycle(head: Node?): Boolean {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
        if (slow === fast) return true
    }
    return false
}

Java

class Node {
    int value;
    Node next;

    Node(int value) { this.value = value; }
}

static boolean hasCycle(Node head) {
    var slow = head;
    var fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

Say the name out loud in the interview, this is Floyd's cycle detection, or the tortoise and hare. The reasoning worth stating is why the two must meet. Once both are inside the loop, the fast pointer gains one step on the slow one every iteration, so the gap shrinks by one each time until it hits zero. They cannot skip past each other on a singly linked list.

It runs in O(n) time and O(1) space, which is the whole reason this question exists. The naive answer is a HashSet of visited nodes, worth mentioning as the first thing you would reach for, but it costs O(n) space. The two pointer trick is what the interviewer is checking you know.

The natural follow up is finding where the loop starts. After the meeting point, move one pointer back to the head and advance both one step at a time. They meet again exactly at the first node of the cycle.

Practice 141. Linked List Cycle (opens in a new tab)142. Linked List Cycle II (opens in a new tab)

Watch

Find the middle of a linked list.

Tier: EssentialDifficulty: Easy

You are handed the head of a singly linked list and you have to return the middle node in one pass, without knowing the length. Walk two pointers from the head, one moving a node at a time and the other two at a time, and when the fast one runs off the end the slow one is standing on the middle.

The problem

You are handed the head of a singly linked list and you are not told its length. You return the middle node itself, not its value, in a single pass. On an even length list you return the second of the two middle nodes, and an empty list gives null back.

Input 1 -> 2 -> 3 -> 4 -> 5.

Output the node holding 3.

123slow45nullfast
The list when the walk ends
Both pointers start on the first node and fast covers two nodes a step, so it runs out of list exactly as slow reaches the accent node.
  • Both pointers start on 1.
  • One step later slow is on 2 and fast is on 3.
  • One more step and slow is on 3 while fast is on 5.
  • Node 5 has no next, so the loop stops with slow sitting on 3.

Kotlin

class Node(val value: Int, var next: Node? = null)

fun middleNode(head: Node?): Node? {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
    }
    return slow
}

Java

class Node {
    int value;
    Node next;

    Node(int value) { this.value = value; }
}

static Node middleNode(Node head) {
    Node slow = head;
    Node fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

The ratio does everything. Fast covers exactly twice the ground slow does, so by the time fast has crossed the whole list slow has crossed half of it. One pass, no length to store, nothing allocated.

For an even length this returns the second of the two middles. On a list of one through six it hands back four, not three. The loop condition is the reason. It keeps going while fast still has a next node, so on six nodes fast reaches node five, sees a next, takes one more double step and falls off, leaving slow one step past centre. If the interviewer wants the first middle instead, loop while fast.next and fast.next.next both exist, and on that same list you get three.

It is O(n) time and O(1) space. The naive answer is two passes, one to count the nodes and one to walk half that many, which is the same big O but not what they are asking for. The empty list falls out for free, both pointers start null, the loop never runs, and null comes back.

Practice 876. Middle of the Linked List (opens in a new tab)141. Linked List Cycle, the same slow and fast pointers (opens in a new tab)

Watch

Reverse a linked list.

Tier: EssentialDifficulty: Easy

You are handed the head of a singly linked list and you have to return the head of the same list running backwards. Walk it once and flip each node's next pointer to point behind it instead of ahead, using three pointers, previous, current and next, so you never lose your place.

The problem

You are handed the head of a singly linked list. You return the head of the same list running backwards, rewiring the existing nodes rather than allocating new ones. The node that used to be last is the one you hand back, and the old head now points at null.

Input 1 -> 2 -> 3 -> 4 -> 5.

Output 5 -> 4 -> 3 -> 2 -> 1.

1head2345null
The input list
Every node points at the one on its right, and the last one points at null.
5head4321null
The same nodes after the walk
The same five nodes with every next pointer flipped, so the old tail is now the accent node you return.
  • Start with prev null and curr on 1. Save 2, point 1 back at null, and move both forward.
  • Now prev is 1 and curr is 2. Save 3, point 2 back at 1, move forward.
  • The same step runs for 3 and for 4, so the reversed part grows to 4 -> 3 -> 2 -> 1.
  • On 5, save null, point 5 back at 4, and curr becomes null. prev is 5, the new head.

Kotlin

class Node(val value: Int, var next: Node? = null)

fun reverseList(head: Node?): Node? {
    var prev: Node? = null
    var curr = head
    while (curr != null) {
        val next = curr.next
        curr.next = prev
        prev = curr
        curr = next
    }
    return prev
}

Java

class Node {
    int value;
    Node next;

    Node(int value) { this.value = value; }
}

static Node reverseList(Node head) {
    Node prev = null;
    Node curr = head;
    while (curr != null) {
        Node next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

The moment you rewrite curr.next you lose the only way to reach the rest of the list, which is why next gets saved before that line runs. prev is the piece of the list you have already reversed, curr is the node you are rewiring right now, and once curr runs out prev is sitting on the new head. It is one pass, so this is O(n) time and O(1) space, and that is the whole answer an interviewer wants.

A near certain follow up is to do it recursively. You reverse the rest of the list first, then point the next node's next back at the current one. The new head comes back up the call stack. It reads shorter, but it costs O(n) stack space, one frame per node, so say out loud that the iterative version is the one you would ship.

Practice 206. Reverse Linked List (opens in a new tab)92. Reverse Linked List II (opens in a new tab)

Watch

Determine if a linked list is a palindrome (Palindrome Linked List).

Tier: CommonDifficulty: MediumAsked at: spotify

You are handed the head of a singly linked list and you have to say whether its values read the same forwards and backwards. Find the middle with a slow and fast pointer, reverse the second half in place, then walk the first half and the reversed second half together comparing values.

The problem

You are handed the head of a singly linked list. You return true when its values read the same forwards and backwards, false otherwise. Pointers only run one way, so there is no walking back from the tail, and an empty list or a single node both count as palindromes.

Input 1 -> 2 -> 3 -> 2 -> 1.

Output true.

123slow21nullfast
The input list when the pointers stop
Fast moves two nodes for every one slow moves, so fast reaches the last node just as slow lands on the accent node in the middle.
123null
The second half after it is reversed
Reversing from the middle gives a half that is walked against the front of the original, comparing 1 with 1, then 2 with 2.
  • The two pointers leave slow on the middle node, the 3.
  • Reversing from there turns the tail into 1 -> 2 -> 3.
  • Walking both together compares 1 with 1, then 2 with 2, then 3 with 3.
  • Nothing disagreed and the reversed half ran out, so the answer is true.

Kotlin

class Node(val value: Int, var next: Node? = null)

fun isPalindrome(head: Node?): Boolean {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
    }
    var second = reverse(slow)
    var first = head
    while (second != null) {
        if (first?.value != second.value) return false
        first = first.next
        second = second.next
    }
    return true
}

fun reverse(head: Node?): Node? {
    var prev: Node? = null
    var curr = head
    while (curr != null) {
        val next = curr.next
        curr.next = prev
        prev = curr
        curr = next
    }
    return prev
}

Java

class Node {
    int value;
    Node next;

    Node(int value) { this.value = value; }
}

static boolean isPalindrome(Node head) {
    Node slow = head;
    Node fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    Node second = reverse(slow);
    Node first = head;
    while (second != null) {
        if (first.value != second.value) return false;
        first = first.next;
        second = second.next;
    }
    return true;
}

static Node reverse(Node head) {
    Node prev = null;
    Node curr = head;
    while (curr != null) {
        Node next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

Say the plan before the code, find the middle, reverse from there, compare from both ends inward. The first answer most candidates give is to copy the list into an array or a stack, and that is fine to offer, O(n) time and O(n) space. The reverse in place version is the one interviewers want, since it reaches O(n) time and O(1) extra space by reusing the list's own nodes.

Stopping when the reversed half runs out makes the odd length case free, because the middle node is shared by both walks and always matches itself. Worth saying out loud, this mutates the list. The second half stays reversed unless you reverse it back, which is a fair tradeoff to name, or to just fix.

Practice 234. Palindrome Linked List (opens in a new tab)

Stacks & Queues

Find the next greater element for every element.

Tier: EssentialDifficulty: Medium

For each position, report the first value to its right that is bigger than it. Scan from the right with a stack that holds only the values still worth comparing against, throwing away everything that the current value already blocks.

The problem

You are given an array of integers. For one position, the next greater element is the first value to its right that is strictly larger, and it is a value, not an index. Return an array of the same length holding that answer for every position, with -1 where nothing to the right is larger. Only the right side counts, so the last position is always -1.

Input nums = [4, 5, 2, 10]

Output [5, 10, 10, -1]

405122103
The input
Each position looks only rightward, so the 10 at the end has nowhere left to look.
50101102-13
The answer, one slot per position
The 4 is beaten by the 5 next door, the 5 and the 2 are both beaten by the 10, and the 10 answers -1.
  • Start at 10 with an empty stack, so its answer is -1, then push 10
  • At 2 the top is 10, which is bigger, so the answer is 10, then push 2
  • At 5 the 2 on top loses, so pop it, and the 10 underneath is the answer
  • At 4 the 5 on top wins straight away, so the answer is 5

Kotlin

fun nextGreater(nums: IntArray): IntArray {
    val out = IntArray(nums.size)
    val stack = ArrayDeque<Int>()
    for (i in nums.indices.reversed()) {
        while (stack.isNotEmpty() && stack.last() <= nums[i]) stack.removeLast()
        out[i] = if (stack.isEmpty()) -1 else stack.last()
        stack.addLast(nums[i])
    }
    return out
}

Java

static int[] nextGreater(int[] nums) {
    var out = new int[nums.length];
    var stack = new ArrayDeque<Integer>();
    for (int i = nums.length - 1; i >= 0; i--) {
        while (!stack.isEmpty() && stack.peekLast() <= nums[i]) stack.removeLast();
        out[i] = stack.isEmpty() ? -1 : stack.peekLast();
        stack.addLast(nums[i]);
    }
    return out;
}

The stack stays monotonic, meaning its values decrease from bottom to top, and that is the whole idea. Suppose a value to my right is smaller than or equal to me. It can never be the answer for anything further left, because those positions meet me first and I already block it. So it is safe to drop forever, and dropping it is what keeps the stack small.

Once the clearing is done, whatever sits on top is the nearest value to the right that actually beats the current one. An empty stack means nothing to the right is bigger, so the answer is -1. Then the current value goes on top, ready for the positions still to come on its left.

Each value is pushed once and popped at most once, so it is O(n) time and O(n) space, against the O(n squared) brute force of scanning right from every position. The follow up is the circular version where the array wraps. Run the same loop over twice the indices, read with i % n, and keep only the answers written during the second half of the walk.

Practice 496. Next Greater Element I (opens in a new tab)503. Next Greater Element II, circular array (opens in a new tab)

Watch

Convert a postfix expression to infix.

Tier: CommonDifficulty: Easy

Postfix writes each operator after its two operands, and infix writes it between them with brackets to hold the meaning. Keep a stack of half built strings, push every operand on its own, and let every operator pop the two most recent strings and push them back joined.

The problem

You are given a valid postfix expression, where each operator comes after its two operands and there are no brackets anywhere. Operands are single characters. Return the same expression in infix, the notation people write, with the operator between its two operands. Bracket every join, so the reading order can never be lost.

Input "AB*C+"

Output "((A*B)+C)"

TokenWhat happensStack, bottom to top
Apush itA
Bpush itA B
*pop two, join, push back(A*B)
Cpush it(A*B) C
+pop two, join, push back((A*B)+C)
  • A and B are operands, so they go on the stack as one character strings
  • * pops B then A and pushes back "(A*B)"
  • C is an operand, so it goes on top of that
  • + pops C then "(A*B)" and pushes "((A*B)+C)", which is the only thing left

Kotlin

fun postfixToInfix(expr: String): String {
    val stack = ArrayDeque<String>()
    for (ch in expr) {
        if (ch.isLetterOrDigit()) {
            stack.addLast(ch.toString())
        } else {
            val right = stack.removeLast()
            val left = stack.removeLast()
            stack.addLast("($left$ch$right)")
        }
    }
    return stack.last()
}

Java

static String postfixToInfix(String expr) {
    var stack = new ArrayDeque<String>();
    for (char ch : expr.toCharArray()) {
        if (Character.isLetterOrDigit(ch)) {
            stack.addLast(String.valueOf(ch));
        } else {
            var right = stack.removeLast();
            var left = stack.removeLast();
            stack.addLast("(" + left + ch + right + ")");
        }
    }
    return stack.peekLast();
}

Postfix is already written in evaluation order, so this is the same loop that would evaluate the expression, only it builds a string where an evaluator would compute a number. Order matters when you pop. The first value off is the right operand and the second is the left, because the right one was pushed most recently. Swapping those two lines still runs and quietly reverses every subtraction and division.

The brackets are not decoration. Postfix carries no precedence at all, the whole meaning sits in the order, so AB*C+ has to come back as ((A*B)+C) and never as A*B+C read some other way. Wrapping every join preserves the meaning exactly, at the cost of brackets a human would not write, and that is the accepted answer.

One pass with one push or two pops per character, so O(n) time and O(n) space for the stack. Prefix is the mirror image. Walk the expression from right to left, and when you join, put the operator in front of the two operands instead of between them.

Watch

Find the next smaller element for every element.

Tier: CommonDifficulty: Easy

For every element you want the first value to its right that is smaller than it. Walk the array from right to left carrying a stack of candidates, popping everything on top that is greater than or equal to the current value.

The problem

You get an array of integers, duplicates allowed. Return a new array of the same length where each slot holds the first value to the right of that position that is strictly smaller than it. Where nothing smaller lies to the right, the slot holds -1. Strictly smaller matters, an equal value is not an answer.

Input, [4, 8, 5, 2, 25].

Output, [2, 5, 2, -1, -1].

40815223254
The input
The array as given, with the indices underneath each value.
205122-13-14
The answer
Each slot holds the nearest smaller value to its right, and the two accented slots have nothing smaller to their right at all.
  • Start at 25 with an empty stack, so its answer is -1, and 25 goes on the stack.
  • At 2, pop 25 because it is bigger, the stack empties, so the answer is -1, and 2 goes on.
  • At 5, the top is 2 which is smaller, so the answer is 2, and 5 goes on above it.
  • At 8, the top is 5, so the answer is 5. At 4, pop 8 and 5, and the surviving top 2 is the answer.

Kotlin

fun nextSmallerElements(nums: IntArray): IntArray {
    val result = IntArray(nums.size)
    val stack = ArrayDeque<Int>()
    for (i in nums.indices.reversed()) {
        while (stack.isNotEmpty() && stack.last() >= nums[i]) stack.removeLast()
        result[i] = if (stack.isEmpty()) -1 else stack.last()
        stack.addLast(nums[i])
    }
    return result
}

Java

static int[] nextSmallerElements(int[] nums) {
    int[] result = new int[nums.length];
    var stack = new ArrayDeque<Integer>();
    for (int i = nums.length - 1; i >= 0; i--) {
        while (!stack.isEmpty() && stack.peek() >= nums[i]) stack.pop();
        result[i] = stack.isEmpty() ? -1 : stack.peek();
        stack.push(nums[i]);
    }
    return result;
}

The popping is the part worth saying out loud. If a value to the right is bigger than the current one, it can never be the answer for anything further left, because the current value sits closer and is already smaller. So it is safe to throw away forever. That leaves the stack increasing from the top down, and the top is always the nearest smaller value to the right.

The naive answer to mention first is a second loop scanning right from every index for the first smaller value, which is O(n squared). The stack version is O(n) time, because each element is pushed once and popped at most once, and O(n) space for the stack and the output.

This is the exact mirror of next greater element. Flip the pop condition from greater than or equal to less than or equal and you have that instead. The trap is the equality. Popping on greater than or equal gives the next strictly smaller value, which is what is usually asked, while popping only on greater would let an equal value stand as the answer.

Practice 1475. Final Prices With a Special Discount in a Shop, next smaller element in disguise (opens in a new tab)739. Daily Temperatures, the same monotonic stack (opens in a new tab)

Watch

Implement a queue using stacks.

Tier: CommonDifficulty: Easy

Build first in first out behaviour out of two last in first out stacks. Push every new value onto one stack, and only when a dequeue finds the second stack empty, pour the whole first stack across, which flips the order.

The problem

Build first in first out behaviour where the only storage you may use is stacks, so push, pop and a test for empty and nothing else. Enqueue adds a value at the back, and dequeue removes and returns the oldest value still waiting. Each operation should cost O(1) on average, which is not the same as O(1) on every call.

Input, the calls enqueue 1, enqueue 2, dequeue, enqueue 3, dequeue, dequeue.

Output, 1, then 2, then 3.

The state after each call, with both stacks written bottom to top.

CallIn stackOut stackReturned
enqueue 11empty
enqueue 21, 2empty
dequeueempty21
enqueue 332
dequeue3empty2
dequeueemptyempty3
  • The first dequeue finds the out stack empty, pours the in stack across so it holds 2 with 1 on top, and pops 1.
  • The second dequeue pops 2 straight off the out stack, so the 3 sitting in the in stack is never touched.
  • The last dequeue finds the out stack empty again, pours 3 across, and returns 3.

Kotlin

class QueueUsingStacks<T> {
    private val inStack = ArrayDeque<T>()
    private val outStack = ArrayDeque<T>()

    fun enqueue(x: T) {
        inStack.addLast(x)
    }

    fun dequeue(): T {
        if (outStack.isEmpty()) {
            while (inStack.isNotEmpty()) outStack.addLast(inStack.removeLast())
        }
        return outStack.removeLast()
    }
}

Java

class QueueUsingStacks<T> {
    private final Deque<T> inStack = new ArrayDeque<>();
    private final Deque<T> outStack = new ArrayDeque<>();

    public void enqueue(T x) {
        inStack.addLast(x);
    }

    public T dequeue() {
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) outStack.addLast(inStack.removeLast());
        }
        return outStack.removeLast();
    }
}

The in stack collects values in arrival order, newest on top. Popping them all onto the out stack reverses that order, so the oldest value, the one a queue hands back first, ends up on top. The transfer only happens when the out stack runs dry, which is the lazy part. Anything already sitting there gets popped straight off without touching the in stack again.

Any single dequeue that triggers a transfer costs O(n), but each value is moved across exactly once in its whole lifetime. Spread that one move over the operations it takes to get a value in and back out and the average settles at O(1). Amortised is the word to say here, because it tells the interviewer you know one call can still cost O(n).

The common trap is transferring eagerly on every enqueue instead of lazily on dequeue. It still works, but it throws away the amortised saving and makes every operation pay the full O(n).

Practice 232. Implement Queue using Stacks (opens in a new tab)

Watch

Implement a stack using queues.

Tier: CommonDifficulty: Easy

You have to give last in first out behaviour using only a structure that hands back the oldest item first. Keep everything in a single queue and rotate it after every push, so the newest value ends up at the front and the front of the queue is the top of the stack.

The problem

You build a stack, last in first out, out of a queue, which is first in first out. It supports four operations, push, pop, top and isEmpty. Inside the class you may only add at the back of the queue and remove from its front, so no array and no second kind of container.

Input the calls push 1, push 2, push 3, then top, then pop.

Output 3 from top, and 3 again from the pop after it.

StepCallQueue, front firstReturns
1push 11nothing
2push 22, 1nothing
3push 33, 2, 1nothing
4top3, 2, 13
5pop2, 13
  • Pushing 1 leaves the queue as [1] with nothing to rotate.
  • Pushing 2 makes it [1, 2], then one rotation moves the 1 behind, giving [2, 1].
  • Pushing 3 makes it [2, 1, 3], then two rotations give [3, 2, 1].
  • The newest value is at the front, so top reads it and pop removes it.

Kotlin

class StackUsingQueue<T> {
    private val queue = ArrayDeque<T>()

    fun push(x: T) {
        queue.addLast(x)
        repeat(queue.size - 1) { queue.addLast(queue.removeFirst()) }
    }

    fun pop(): T = queue.removeFirst()

    fun top(): T = queue.first()

    fun isEmpty(): Boolean = queue.isEmpty()
}

Java

class StackUsingQueue<T> {
    private final Deque<T> queue = new ArrayDeque<>();

    public void push(T x) {
        queue.addLast(x);
        for (int i = queue.size() - 1; i > 0; i--) {
            queue.addLast(queue.removeFirst());
        }
    }

    public T pop() { return queue.removeFirst(); }

    public T top() { return queue.getFirst(); }

    public boolean isEmpty() { return queue.isEmpty(); }
}

The new value goes on the back like any queue insert, then rotating the older values from front to back, one at a time, walks them all around behind it. After that rotation the newest value is at the front, so pop and top just read the front of the queue as if it were the top of a stack.

The rotation is what costs you. push moves up to n elements, so it is O(n), while pop and top are O(1) because the queue is already in the right order by the time they are called. That trade is the whole point of the question. A queue naturally gives you the oldest item first, so you pay to flip that order back to newest first on every insert.

The two queue version is the same idea spread across two containers. You push into an empty queue, drain the other one into it, then swap which queue is active. It is the same O(n) push and O(1) pop. The natural follow up is to flip the cost, making push O(1) and paying the rotation on pop instead, which moves the work without reducing it.

Practice 225. Implement Stack using Queues (opens in a new tab)

Watch

Convert an infix expression to postfix.

Tier: CommonDifficulty: Medium

Infix puts an operator between its two operands, postfix puts it after both, and the brackets disappear. Send operands straight to the output and park operators on a stack, popping anything already there that binds at least as tightly.

The problem

You are given a valid infix expression, the notation people write, where every operator sits between its two operands. Operands are single characters, and the operators are the four arithmetic ones, ^ for power, and round brackets. Return the same expression in postfix, where every operator comes after both of its operands. Postfix needs no brackets at all, because the order of the operators carries the meaning on its own.

Input "(p+q)*(m-n)"

Output "pq+mn-*"

Tokens readStack, bottom to topOutput so far
(p+q( +pq
)emptypq+
*(m-n* ( -pq+mn
)*pq+mn-
end of inputemptypq+mn-*
  • p goes out, + waits on the stack above the open bracket, q goes out
  • The closing bracket drains down to that bracket, so + joins the output and both brackets vanish
  • * finds an empty stack and waits, then the second bracket repeats the pattern and sends out mn-
  • Only * is left waiting, so the final drain appends it and the output reads pq+mn-*

Kotlin

fun infixToPostfix(expr: String): String {
    val out = StringBuilder()
    val ops = ArrayDeque<Char>()
    for (ch in expr) {
        when {
            ch.isLetterOrDigit() -> out.append(ch)
            ch == '(' -> ops.addLast(ch)
            ch == ')' -> {
                while (ops.last() != '(') out.append(ops.removeLast())
                ops.removeLast()
            }
            // '^' is right associative, so nothing on the stack outranks it.
            else -> {
                while (ch != '^' && ops.isNotEmpty() && rank(ops.last()) >= rank(ch)) out.append(ops.removeLast())
                ops.addLast(ch)
            }
        }
    }
    while (ops.isNotEmpty()) out.append(ops.removeLast())
    return out.toString()
}

private fun rank(op: Char) = when (op) { '^' -> 3; '*', '/' -> 2; '+', '-' -> 1; else -> 0 }

Java

static String infixToPostfix(String expr) {
    var out = new StringBuilder();
    var ops = new ArrayDeque<Character>();
    for (char ch : expr.toCharArray()) {
        if (Character.isLetterOrDigit(ch)) out.append(ch);
        else if (ch == '(') ops.addLast(ch);
        else if (ch == ')') {
            while (ops.peekLast() != '(') out.append(ops.removeLast());
            ops.removeLast();
        } else {
            // '^' is right associative, so nothing on the stack outranks it.
            while (ch != '^' && !ops.isEmpty() && rank(ops.peekLast()) >= rank(ch)) out.append(ops.removeLast());
            ops.addLast(ch);
        }
    }
    while (!ops.isEmpty()) out.append(ops.removeLast());
    return out.toString();
}

static int rank(char op) { return switch (op) { case '^' -> 3; case '*', '/' -> 2; case '+', '-' -> 1; default -> 0; }; }

Operands never wait, their order is the same in both notations. Operators do wait, because a postfix operator comes after both of its operands and you have only seen one when you read it. The stack holds those pending operators and rank decides when each is allowed to leave.

A + arriving on top of a * means the * already has both operands, so it pops first. A * arriving on top of a + means the + is still short one, so it stays. Equal ranks pop too, which gives left to right order for plus and minus. Giving ( a rank of zero makes it a wall no comparison pops past, and ) drains down to that wall and discards both brackets. Every character is pushed and popped at most once, so it is O(n) time and O(n) space. The usual slip is forgetting the drain at the end, since whatever is still on the stack belongs at the tail of the output.

Watch

Trapping rain water.

Tier: CommonDifficulty: Hard

Given bar heights, work out how much rain settles between them. Water above a bar is the smaller of the tallest bar to its left and the tallest to its right, minus its own height, so two pointers walk inward carrying those two maxima.

The problem

The array holds the heights of bars standing side by side, each one unit wide, and a height may be zero. Return the total units of water that settle between them after rain. Water sits above a bar only when a taller bar stands somewhere to its left and somewhere to its right, so the two outermost bars never hold any.

Input, [4, 2, 0, 3, 2, 5].

Output, 9.

40left2102332455right
The bars
The two pointers start on the outermost bars and walk inward, the shorter side moving each time.
002142132405
Water above each bar
Each slot is the smaller of the tallest bar to its left and to its right, minus its own height, and the slots total 9.
  • Both pointers start at the ends. The left bar 4 is shorter than the right bar 5, so the left side moves and leftMax becomes 4.
  • Bar 2 is still on the short side, so it traps 4 minus 2, which is 2. Bar 0 traps 4 and bar 3 traps 1, running the total to 7.
  • The last bar 2 traps another 2 for a total of 9, the pointers meet, and 9 is the answer.

Kotlin

fun trap(height: IntArray): Int {
    var left = 0
    var right = height.size - 1
    var leftMax = 0
    var rightMax = 0
    var water = 0
    while (left < right) {
        if (height[left] <= height[right]) {
            leftMax = maxOf(leftMax, height[left])
            water += leftMax - height[left++]
        } else {
            rightMax = maxOf(rightMax, height[right])
            water += rightMax - height[right--]
        }
    }
    return water
}

Java

static int trap(int[] height) {
    int left = 0;
    int right = height.length - 1;
    int leftMax = 0;
    int rightMax = 0;
    int water = 0;
    while (left < right) {
        if (height[left] <= height[right]) {
            leftMax = Math.max(leftMax, height[left]);
            water += leftMax - height[left++];
        } else {
            rightMax = Math.max(rightMax, height[right]);
            water += rightMax - height[right--];
        }
    }
    return water;
}

Start with the version you would write first, two passes filling a prefix max array and a suffix max array, then a third pass adding the smaller of the two minus the height. That is O(n) time but O(n) extra space, and it is the honest first answer.

The two pointer version removes those arrays. When the left bar is the shorter of the two ends, some bar at or beyond the right pointer is at least that tall, so the right side can only help. That means leftMax alone already decides the water over the left bar, and you can settle that column and step forward. The same argument runs the other way when the right end is shorter. It is O(n) time and O(1) space. There is also a monotonic stack version that fills water layer by layer as each bar pops a shorter one off the stack, worth naming but harder to get right under pressure. The trap in every version is the edges, and here the pointers converge before the first or last bar can wrongly collect water.

Practice 42. Trapping Rain Water (opens in a new tab)84. Largest Rectangle in Histogram, the other monotonic stack classic (opens in a new tab)

Watch

Trees

Print the level order traversal of a binary tree.

Tier: EssentialDifficulty: Easy

You are given the root of a binary tree and you have to return its values level by level, left to right, with each level as its own list. Push the root into a queue and read the queue's size at the start of every round, because that size is exactly how many nodes sit on the current level.

The problem

You are given the root of a binary tree, which may be null. You return a list of lists, one list per depth, each holding that depth's values from left to right. The tree is not sorted, and the levels come out top down.

Input the tree below, written level by level as [3, 9, 20, null, null, 15, 7].

Output [[3], [9, 20], [15, 7]].

9315207
The input tree
A root of 3 with children 9 and 20, where 9 has no children and 20 carries 15 and 7.
9315207
The tree during round two
Round two drains exactly the two accent nodes, which is why the size of the queue is read once before the round starts.
  • Round one, the queue holds 3. Take it, record [3], and push 9 and 20.
  • Round two, the size is 2. Take 9 and 20, record [9, 20], and push 15 and 7.
  • Round three, the size is 2. Take 15 and 7, record [15, 7], and nothing goes back in.

Kotlin

class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun levelOrder(root: TreeNode?): List<List<Int>> {
    val result = mutableListOf<List<Int>>()
    if (root == null) return result
    val queue = ArrayDeque<TreeNode>()
    queue.add(root)
    while (queue.isNotEmpty()) {
        val level = mutableListOf<Int>()
        repeat(queue.size) {
            val node = queue.removeFirst()
            level.add(node.value)
            node.left?.let { queue.add(it) }
            node.right?.let { queue.add(it) }
        }
        result.add(level)
    }
    return result
}

Java

class TreeNode {
    int value;
    TreeNode left, right;

    TreeNode(int value) { this.value = value; }
}

static List<List<Integer>> levelOrder(TreeNode root) {
    var result = new ArrayList<List<Integer>>();
    if (root == null) return result;
    var queue = new ArrayDeque<TreeNode>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int levelSize = queue.size();
        var level = new ArrayList<Integer>();
        for (int i = 0; i < levelSize; i++) {
            var node = queue.removeFirst();
            level.add(node.value);
            if (node.left != null) queue.add(node.left);
            if (node.right != null) queue.add(node.right);
        }
        result.add(level);
    }
    return result;
}

Reading queue.size once, before you start draining, is the one line that makes this work. Read it again inside the loop and it keeps growing as children arrive, so you lose the boundary between one level and the next. Kotlin's repeat takes that count up front and the Java loop copies it into levelSize for the same reason. Everything else is a plain breadth first search, dequeue a node, record it, enqueue its children.

It is O(n) time and O(n) space, since every node is visited once and a complete tree's last level holds about half of them. A common follow up is a zigzag traversal, where alternate levels come out reversed, and that is the same code with a boolean that flips each round.

Practice 102. Binary Tree Level Order Traversal (opens in a new tab)103. Binary Tree Zigzag Level Order Traversal (opens in a new tab)

Watch

Find the diameter of a binary tree.

Tier: EssentialDifficulty: Medium

The diameter is the longest path between any two nodes, and it does not have to pass through the root. One postorder walk does it, where every node returns its height upward and records the path bending through it on the way.

The problem

You get the root of a binary tree and return the length of its longest path, counted in edges. The path can run between any two nodes and does not have to pass through the root. Edges and not nodes, so a tree holding a single node has diameter zero.

Input, the tree [1, 2, 3, 4, 5] in level order.

Output, 3, the path 4 to 2 to 1 to 3.

42513
The sample tree
The longest path climbs from 4 through 2 to 1 and back down to 3, which is three edges.
  • Leaves 4 and 5 see two empty children, record a bend of 0, and each return height 1.
  • Node 2 gets 1 from each side, records a bend of 2, and returns height 2.
  • Node 3 is a leaf, records 0, and returns height 1.
  • Node 1 gets 2 on the left and 1 on the right, records a bend of 3, which beats 2 and is the answer.

Kotlin

class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun diameter(root: TreeNode?): Int {
    var best = 0
    fun height(node: TreeNode?): Int {
        if (node == null) return 0
        val left = height(node.left)
        val right = height(node.right)
        best = maxOf(best, left + right)
        return 1 + maxOf(left, right)
    }
    height(root)
    return best
}

Java

class TreeNode {
    int value;
    TreeNode left, right;

    TreeNode(int value) { this.value = value; }
}

static int diameter(TreeNode root) {
    int[] best = { 0 };
    height(root, best);
    return best[0];
}

static int height(TreeNode node, int[] best) {
    if (node == null) return 0;
    int left = height(node.left, best);
    int right = height(node.right, best);
    best[0] = Math.max(best[0], left + right);
    return 1 + Math.max(left, right);
}

The key observation is that the longest path has a highest point, one node where it turns from going down the left side to going down the right side. So if you visit every node and ask how long the path bending here would be, one of those answers has to be the diameter. That length is just the left height plus the right height.

The two numbers a node deals in are different, and mixing them up is the classic mistake. What it reports upward is a height, one plus the taller child, because a parent can only continue down one side. What it records is a diameter, left plus right, because a path may bend here but nowhere above.

The naive version recomputes height from scratch at every node, which is O(n squared) on a skewed tree. One postorder pass makes it O(n) time and O(h) space for the recursion stack. Note this counts edges, so a single node has diameter zero. If the question asks for nodes on the path, add one.

Practice 543. Diameter of Binary Tree (opens in a new tab)104. Maximum Depth of Binary Tree, the height helper it is built on (opens in a new tab)

Watch

Print the left view of a binary tree.

Tier: CommonDifficulty: Easy

The left view is what you see standing to the left of the tree, one node per level. Do a preorder walk that goes left before right and record a node the first time you reach a new level.

The problem

You get the root of a binary tree and return one value per level, from the top down. Each value is the leftmost node at that depth, meaning the first one you would meet reading that level left to right. That node is not always a left child, it is simply the one nothing else at that depth sits to the left of.

Input, the tree [1, 2, 3, 4, 5, null, 6] in level order.

Output, [1, 2, 4].

425136
The sample tree
The three nodes you see standing to the left of the tree, one for each level.
  • Node 1 arrives at level 0 with an empty result, so 1 is recorded.
  • Going left, node 2 arrives at level 1 with one value stored, so 2 is recorded, then node 4 at level 2 records 4.
  • Node 5 is also at level 2, but the result already holds three values, so it is skipped.
  • Back on the right, node 3 at level 1 and node 6 at level 2 are both skipped for the same reason.

Kotlin

class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun leftView(root: TreeNode?): List<Int> {
    val result = mutableListOf<Int>()
    fun walk(node: TreeNode?, level: Int) {
        if (node == null) return
        if (level == result.size) result.add(node.value)
        walk(node.left, level + 1)
        walk(node.right, level + 1)
    }
    walk(root, 0)
    return result
}

Java

class TreeNode {
    int value;
    TreeNode left, right;

    TreeNode(int value) { this.value = value; }
}

static List<Integer> leftView(TreeNode root) {
    var result = new ArrayList<Integer>();
    walk(root, 0, result);
    return result;
}

static void walk(TreeNode node, int level, List<Integer> result) {
    if (node == null) return;
    if (level == result.size()) result.add(node.value);
    walk(node.left, level + 1, result);
    walk(node.right, level + 1, result);
}

The check level == result.size is doing the work of a highest level seen so far. The result holds one entry per level visited, so its size is exactly the next level waiting to be filled. Because left is recursed before right, the first call to reach a given level always comes from the leftmost node at that depth, so the check fires once per level.

The other common way is level order BFS with a queue. Take the first node dequeued at the start of each level and skip the rest. Same idea, iterative instead of recursive, and easier to reason about if recursion depth on a skewed tree worries you. Both are O(n) time, and space runs from O(h) to O(n) depending on whether you count the stack or the queue.

The right view is the mirror of this. Recurse right before left, or take the last node dequeued at each level instead of the first.

Practice 199. Binary Tree Right Side View, the mirror (opens in a new tab)

Watch

Print the top view of a binary tree.

Tier: CommonDifficulty: Medium

The top view is what you would see looking straight down at the tree, one node per vertical column. Give every node a horizontal distance from the root, then walk the tree breadth first and keep only the first node you meet at each distance.

The problem

You are given the root of a binary tree. Give every node a horizontal distance, 0 at the root, one less for a left child and one more for a right child, so nodes line up in vertical columns. The top view is the shallowest node in each column, what you would see looking straight down. Return one value per column, leftmost column first.

Input the tree [1, 2, 3, 4, 5, null, 6] in level order, null for a missing child

Output [4, 2, 1, 3, 6]

425136
The sample tree
The marked nodes are the ones visible from above, and the 5 is hidden because the root already owns its column.
horizontal distance-2-1012
first node seen42136
  • The root claims distance 0 with the value 1
  • The next level gives 2 at distance -1 and 3 at distance 1, both new columns, so both are kept
  • The bottom level keeps 4 at distance -2 and 6 at distance 2, but hides the 5, since it lands at distance 0 and the root claimed that column first

Kotlin

class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun topView(root: TreeNode?): List<Int> {
    if (root == null) return emptyList()
    val firstAtDistance = sortedMapOf<Int, Int>()
    val queue = ArrayDeque<Pair<TreeNode, Int>>()
    queue.add(root to 0)
    while (queue.isNotEmpty()) {
        val (node, hd) = queue.removeFirst()
        if (hd !in firstAtDistance) firstAtDistance[hd] = node.value
        node.left?.let { queue.add(it to hd - 1) }
        node.right?.let { queue.add(it to hd + 1) }
    }
    return firstAtDistance.values.toList()
}

Java

class TreeNode {
    int value;
    TreeNode left, right;

    TreeNode(int value) { this.value = value; }
}

record Visit(TreeNode node, int hd) {}

static List<Integer> topView(TreeNode root) {
    if (root == null) return List.of();
    var firstAtDistance = new TreeMap<Integer, Integer>();
    var queue = new ArrayDeque<Visit>();
    queue.add(new Visit(root, 0));
    while (!queue.isEmpty()) {
        var visit = queue.removeFirst();
        int hd = visit.hd();
        firstAtDistance.putIfAbsent(hd, visit.node().value);
        if (visit.node().left != null) queue.add(new Visit(visit.node().left, hd - 1));
        if (visit.node().right != null) queue.add(new Visit(visit.node().right, hd + 1));
    }
    return new ArrayList<>(firstAtDistance.values());
}

The sorted map does two jobs at once, it tells you which columns you have already claimed and it hands the values back leftmost to rightmost for free. A plain hash map with a tracked minimum and maximum distance works too, you just walk the range yourself at the end.

Breadth first is the right traversal, not depth first. It visits by level, so the first node reached in a column is always the shallowest one, which is exactly what the top view wants. Depth first can dive to a deep node in a column before a shallower one on another branch, so a first seen check there picks the wrong node.

It is O(n) time and O(n) space for the queue and the map. The trap is two nodes landing on the same distance at the same depth on different branches. Whichever one the queue reaches first wins, and both readings are defensible, so say that out loud rather than guessing.

Practice 987. Vertical Order Traversal of a Binary Tree (opens in a new tab)

Watch

Find the maximum path sum in a binary tree.

Tier: CommonDifficulty: Hard

You want the largest sum along any path in the tree, and the path can start and end anywhere. Use the same postorder shape as the diameter, where each node returns the best one sided sum and records the best path bending through it.

The problem

A path is any run of connected nodes with no node used twice, and it may start and end anywhere, so it need not touch the root. Node values can be negative, and a single node counts as a path. Return the largest sum of the values along one path.

Input, the tree [-10, 9, 20, null, null, 15, 7] in level order.

Output, 42, the path 15 to 20 to 7.

9-1015207
The sample tree
The winning path bends through 20 and never climbs to the root, because the root would only cost it 10.
  • Leaves 9, 15 and 7 each record their own value as a bend and return it as a gain.
  • Node 20 bends through both children, 20 + 15 + 7 is 42, which becomes the best so far.
  • Node 20 hands its parent only one side, 20 + 15 is 35, since a parent can pass through just once.
  • Node -10 bends through both, -10 + 9 + 35 is 34, which loses to 42, so 42 stands.

Kotlin

class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun maxPathSum(root: TreeNode?): Int {
    var best = Int.MIN_VALUE
    fun gain(node: TreeNode?): Int {
        if (node == null) return 0
        val left = maxOf(gain(node.left), 0)
        val right = maxOf(gain(node.right), 0)
        best = maxOf(best, node.value + left + right)
        return node.value + maxOf(left, right)
    }
    gain(root)
    return best
}

Java

class TreeNode {
    int value;
    TreeNode left, right;

    TreeNode(int value) { this.value = value; }
}

static int maxPathSum(TreeNode root) {
    int[] best = { Integer.MIN_VALUE };
    gain(root, best);
    return best[0];
}

static int gain(TreeNode node, int[] best) {
    if (node == null) return 0;
    int left = Math.max(gain(node.left, best), 0);
    int right = Math.max(gain(node.right, best), 0);
    best[0] = Math.max(best[0], node.value + left + right);
    return node.value + Math.max(left, right);
}

Again the winning path has a single highest node, so asking every node what the best path bending here is will find it. At that turning point you may take both children, so the candidate is the node plus both gains. What you hand to your parent is different, because the parent continues upward through you, so you can only offer the better one side.

The clamp is what makes negative values work. A child returning a negative gain would only drag the total down, and since a path is allowed to stop at the current node, refusing that child is always legal. Writing it as maxOf(gain, 0) says exactly that, take this branch only if it pays.

It is O(n) time, one visit per node, and O(h) space for the recursion. Two traps. Start best at the smallest integer, not zero, or a tree of all negative values wrongly answers zero instead of the largest single node. And never clamp the node's own value, the path has to include the node you are standing on.

Practice 124. Binary Tree Maximum Path Sum (opens in a new tab)543. Diameter of Binary Tree, the same return-one-branch trick (opens in a new tab)

Watch

Serialize and deserialize a binary tree.

Tier: CommonDifficulty: Hard

You have to turn a binary tree into a string and turn that string back into the identical tree. Write a preorder walk with a marker for every missing child, then read it back with the same walk, taking the next token and letting recursion fill the two children in the same order.

The problem

Serialize takes the root of a binary tree and returns a string. Deserialize takes that string back and returns a tree of the same shape holding the same values. Any format is allowed as long as the round trip is exact, so the shape has to survive the trip and not just the values.

Input the tree below, written level by level as [1, 2, 3, null, null, 4, 5].

Output the string 1,2,#,#,3,4,#,#,5,#,#, where # stands for a missing child.

21435
The tree the string has to rebuild
A root of 1 with 2 on its left and 3 on its right, where 2 is a leaf and 3 carries 4 and 5.
  • The walk writes 1, goes left and writes 2, then writes # twice because 2 has no children.
  • Back at the root it goes right, writes 3, and the last tokens hang 4 and 5 under it.
  • Reading back takes the tokens in that same order, so the first token becomes the root.
  • A # returns null, and that is what tells the reader where a branch ends.

Kotlin

class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun serialize(root: TreeNode?): String = buildString {
    fun write(node: TreeNode?) {
        if (node == null) {
            append("#,")
            return
        }
        append(node.value).append(',')
        write(node.left)
        write(node.right)
    }
    write(root)
}

fun deserialize(data: String): TreeNode? {
    val tokens = data.split(",").iterator()
    fun read(): TreeNode? {
        val token = tokens.next()
        if (token == "#") return null
        return TreeNode(token.toInt(), read(), read())
    }
    return read()
}

Java

class TreeNode {
    int value;
    TreeNode left, right;

    TreeNode(int value) { this.value = value; }
}

static String serialize(TreeNode node) {
    var out = new StringBuilder();
    write(node, out);
    return out.toString();
}

static void write(TreeNode node, StringBuilder out) {
    if (node == null) {
        out.append("#,");
        return;
    }
    out.append(node.value).append(',');
    write(node.left, out);
    write(node.right, out);
}

static TreeNode deserialize(String data) {
    return read(new ArrayDeque<>(List.of(data.split(","))));
}

static TreeNode read(Deque<String> tokens) {
    var token = tokens.poll();
    if (token.equals("#")) return null;
    var node = new TreeNode(Integer.parseInt(token));
    node.left = read(tokens);
    node.right = read(tokens);
    return node;
}

The null markers are the whole answer. A plain preorder listing of values is ambiguous, since a root with only a left child and a root with only a right child produce the identical string. That is why the textbook says you need two traversals to rebuild a tree. Writing a # wherever a child is missing spells the shape out in the string itself, so one traversal is suddenly enough.

Reading back works because preorder consumes tokens in exactly the order it wrote them. The cursor is shared across the whole recursion, so each call takes the next token and leaves it where the next call expects it. An iterator, a queue or a shared index all do that job.

Both directions are O(n) time and O(n) space, one token per node plus one per missing child. Level order with the same markers is the usual alternative, and it produces a friendlier looking string, but it needs a queue on both sides and more care around trailing markers. Do not forget negative values, splitting on commas handles them and splitting on a minus sign would not.

Practice 297. Serialize and Deserialize Binary Tree (opens in a new tab)105. Construct Binary Tree from Preorder and Inorder Traversal (opens in a new tab)

Watch

Graphs & Grids

Rotten oranges, find the minimum time for every orange to rot.

Tier: EssentialDifficulty: Medium

Rot spreads from every rotten orange to its four neighbours once a minute, and you want the minute the last fresh one turns. This is a multi source BFS, where you seed the queue with every rotten orange at once and each level of the search is one minute.

The problem

You get a grid where 0 is an empty cell, 1 is a fresh orange and 2 is a rotten one. Every minute, each rotten orange rots the fresh oranges directly above, below, left and right of it. Return the number of minutes until nothing fresh is left, or -1 if some fresh orange can never be reached. A grid with no fresh oranges at all answers 0.

Input, the grid [[2, 1, 1], [1, 1, 0], [0, 1, 1]].

Output, 4.

211110011
The starting grid
One rotten orange in the top left corner, six fresh oranges around it and two empty cells.
01212..34
The minute each orange rots
The minute every orange turns, a dot for an empty cell, so the answer is the largest of them.
  • Minute 1, the corner rots its two neighbours and the fresh count drops to 4.
  • Minute 2, those two rot the top right orange and the middle one, leaving 2 fresh.
  • Minute 3 rots the orange below the middle, minute 4 rots the last one in the bottom right, the count hits zero and the loop stops.

Kotlin

fun orangesRotting(grid: Array<IntArray>): Int {
    val dirs = arrayOf(-1 to 0, 1 to 0, 0 to -1, 0 to 1)
    val queue = ArrayDeque<Pair<Int, Int>>()
    var fresh = 0
    for (r in grid.indices) for (c in grid[0].indices)
        if (grid[r][c] == 2) queue.add(r to c) else if (grid[r][c] == 1) fresh++
    var minutes = 0
    while (queue.isNotEmpty() && fresh > 0) {
        repeat(queue.size) {
            val (r, c) = queue.removeFirst()
            for ((dr, dc) in dirs) {
                val (nr, nc) = r + dr to c + dc
                if (nr !in grid.indices || nc !in grid[0].indices || grid[nr][nc] != 1) continue
                grid[nr][nc] = 2
                fresh--
                queue.add(nr to nc)
            }
        }
        minutes++
    }
    return if (fresh == 0) minutes else -1
}

Java

static int orangesRotting(int[][] grid) {
    int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
    int rows = grid.length, cols = grid[0].length;
    var queue = new ArrayDeque<int[]>();
    int fresh = 0;
    for (int r = 0; r < rows; r++) for (int c = 0; c < cols; c++)
        if (grid[r][c] == 2) queue.add(new int[] { r, c }); else if (grid[r][c] == 1) fresh++;
    int minutes = 0;
    while (!queue.isEmpty() && fresh > 0) {
        for (int i = queue.size(); i > 0; i--) {
            int[] cell = queue.removeFirst();
            for (int[] d : dirs) {
                int nr = cell[0] + d[0], nc = cell[1] + d[1];
                if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || grid[nr][nc] != 1) continue;
                grid[nr][nc] = 2;
                fresh--;
                queue.add(new int[] { nr, nc });
            }
        }
        minutes++;
    }
    return fresh == 0 ? minutes : -1;
}

Seeding the queue with every rotten orange up front, not just one, is the whole trick. They all rot their neighbours in the same minute, so they all belong at the same level of the search. Draining the queue by its size at the start of each round keeps the levels separate, which is what lets you count minutes instead of cells. DFS is the wrong tool here, it would walk all the way out from one orange before backtracking, which gives a path length, not a time.

Each cell is enqueued and visited once, so it is O(rows times columns) in time and the same in space for the queue in the worst case. The trap is forgetting to count the fresh oranges up front. If the loop ends because the queue emptied while fresh is still above zero, some oranges were walled off and the answer is -1.

Practice 994. Rotting Oranges (opens in a new tab)200. Number of Islands (opens in a new tab)

Watch

Matrices

Rotate a matrix by 90 degrees clockwise in place.

Tier: CommonDifficulty: Medium

Turn a square grid a quarter turn clockwise without allocating a second grid. Do it in two passes, transpose by swapping across the main diagonal, then reverse each row.

The problem

You are given a square matrix of integers, n rows by n columns. Rotate it a quarter turn clockwise, so the first row becomes the last column read top to bottom. Do it in place, meaning you change the caller's own matrix and allocate nothing beyond a temporary variable. There is no value to return.

Input the rows 1 2 3, 4 5 6, 7 8 9

Output the rows 7 4 1, 8 5 2, 9 6 3

123456789
The input matrix
The top row is marked so you can follow one row through the turn.
741852963
After the quarter turn
That same row now runs down the rightmost column, top to bottom, which is what clockwise means.
  • The transpose swaps matrix[i][j] with matrix[j][i], so the top row becomes the left column
  • Reversing each row slides that column over to the right, which is the quarter turn
steprow 0row 1row 2
input1 2 34 5 67 8 9
after transpose1 4 72 5 83 6 9
after reversing rows7 4 18 5 29 6 3

Kotlin

fun rotate(matrix: Array<IntArray>) {
    val n = matrix.size
    for (i in 0 until n) {
        for (j in i + 1 until n) {
            val tmp = matrix[i][j]
            matrix[i][j] = matrix[j][i]
            matrix[j][i] = tmp
        }
    }
    for (row in matrix) row.reverse()
}

Java

static void rotate(int[][] matrix) {
    int n = matrix.length;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            int tmp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = tmp;
        }
    }
    for (int[] row : matrix) {
        for (int lo = 0, hi = n - 1; lo < hi; lo++, hi--) {
            int tmp = row[lo];
            row[lo] = row[hi];
            row[hi] = tmp;
        }
    }
}

Think about where one cell has to land. A clockwise turn sends row i to column n - 1 - i, so the top row becomes the rightmost column. The transpose does the first half of that and the row reversal fixes the ordering.

The inner loop starting at j = i + 1 is not a detail you can skip. Starting at zero would swap every pair twice and leave the matrix exactly as it was.

It is O(n squared) time, unavoidable since every cell has to move, and O(1) extra space because every write is a swap. The naive answer allocates a second matrix and copies each cell to its new home, easier to reason about but O(n squared) memory. Two follow ups come up. Counter clockwise is the same trick with the steps flipped, reverse the rows first and then transpose. And a non square matrix cannot rotate in place at all, since the shape itself changes, so there you do need the second matrix.

Practice 48. Rotate Image (opens in a new tab)54. Spiral Matrix, the other in-place matrix walk (opens in a new tab)

Watch

Traverse a matrix in spiral order.

Tier: CommonDifficulty: Medium

You are given a grid and you have to read every cell once, winding inwards clockwise from the top left corner. Keep four boundaries, top, bottom, left and right, peel one full ring off the outside on every pass, and move each boundary inward as soon as its side is walked.

The problem

You are given a matrix of m rows by n columns, which need not be square. You return a flat list holding every value once, read clockwise inwards starting at the top left corner. Every cell appears exactly once, so a lone middle row or column must not come back twice.

Input the three by three grid below.

Output [1, 2, 3, 6, 9, 8, 7, 4, 5].

123456789
The input grid with the first ring marked
The first pass peels the eight accent cells clockwise from the top left, leaving only the 5 in the middle for the pass after it.
  • Walk the top row left to right for 1, 2, 3, then push top down to row one.
  • Walk the right column down for 6 and 9, then pull right in to column one.
  • Walk the bottom row backwards for 8 and 7, then pull bottom up to row one.
  • Walk the left column upwards for 4, then push left in, leaving only 5 for the last pass.

Kotlin

fun spiralOrder(matrix: Array<IntArray>): List<Int> {
    if (matrix.isEmpty()) return emptyList()
    val out = mutableListOf<Int>()
    var top = 0
    var bottom = matrix.size - 1
    var left = 0
    var right = matrix[0].size - 1
    while (top <= bottom && left <= right) {
        for (c in left..right) out.add(matrix[top][c])
        top++
        for (r in top..bottom) out.add(matrix[r][right])
        right--
        if (top <= bottom) {
            for (c in right downTo left) out.add(matrix[bottom][c])
            bottom--
        }
        if (left <= right) {
            for (r in bottom downTo top) out.add(matrix[r][left])
            left++
        }
    }
    return out
}

Java

static List<Integer> spiralOrder(int[][] matrix) {
    var out = new ArrayList<Integer>();
    if (matrix.length == 0) return out;
    int top = 0;
    int bottom = matrix.length - 1;
    int left = 0;
    int right = matrix[0].length - 1;
    while (top <= bottom && left <= right) {
        for (int c = left; c <= right; c++) out.add(matrix[top][c]);
        top++;
        for (int r = top; r <= bottom; r++) out.add(matrix[r][right]);
        right--;
        if (top <= bottom) {
            for (int c = right; c >= left; c--) out.add(matrix[bottom][c]);
            bottom--;
        }
        if (left <= right) {
            for (int r = bottom; r >= top; r--) out.add(matrix[r][left]);
            left++;
        }
    }
    return out;
}

Each pass does the same four walks in the same order, and shrinking a boundary right after its walk is what stops the next pass touching a cell twice.

The two guards cover the single row and single column cases, and they are the part people drop. On a one row matrix the first walk prints it and pushes top past bottom, so the third walk must be skipped or that row comes out again backwards. One column is the mirror, the second walk consumes it and the fourth would repeat it.

Every cell is visited once, so it is O(m times n) time and O(1) extra space beyond the list you return. The other easy slip is writing the loop condition as top < bottom, which quietly drops the middle row of a matrix with an odd number of rows.

Practice 54. Spiral Matrix (opens in a new tab)59. Spiral Matrix II, fill instead of read (opens in a new tab)

Watch

Heaps & Priority Queues

Find the kth largest element in an array.

Tier: EssentialDifficulty: Medium

You are given an array and a number k, and you have to return the value that would sit kth from the end if the array were sorted. Keep a min heap holding only the k largest values seen so far, dropping its smallest whenever it grows past k, and at the end its top is the answer.

The problem

You are given an unsorted array of integers, which may repeat, and a number k between one and the array's length. You return the value that would sit kth from the end if the array were sorted. Position in sorted order is what counts, not distinct values, so in [5, 5, 4] the second largest is 5 and not 4.

Input nums = [3, 2, 1, 5, 6, 4] and k = 2.

Output 5.

302112536445
The input array with the answer marked
Sorted, the array reads 1, 2, 3, 4, 5, 6, so the second largest value is the accent cell holding 5.
StepNumber pushedHeap after the drop
133
222, 3
312, 3
453, 5
565, 6
645, 6
  • Any push that takes the heap past size two is followed by dropping its smallest member.
  • So the heap always holds the two largest values seen so far, and its top is the smaller of them.
  • After the last number the top is 5, which is the answer.

Kotlin

fun kthLargest(nums: IntArray, k: Int): Int {
    val heap = PriorityQueue<Int>()
    for (n in nums) {
        heap.add(n)
        if (heap.size > k) heap.poll()
    }
    return heap.peek()
}

Java

static int kthLargest(int[] nums, int k) {
    var heap = new PriorityQueue<Integer>();
    for (int n : nums) {
        heap.add(n);
        if (heap.size() > k) heap.poll();
    }
    return heap.peek();
}

A min heap sounds backwards for a largest question, and that is exactly why it works. The heap is a bag of the current top k, and its smallest member is the weakest one in that bag. When a new number arrives, the only value that can be evicted is that weakest one, and the heap hands it to you in constant time.

The first answer to offer is sorting and indexing at n - k, which is O(n log n) and perfectly acceptable if k is close to n. The heap is O(n log k) time and O(k) space, better when k is small, and it also works on a stream where you never hold the whole array.

The follow up an interviewer usually wants is quickselect. Partition around a random pivot the way quicksort does, then recurse into only the side that holds the position you want. That is O(n) on average with O(1) extra space, though it mutates the input and has an O(n squared) worst case. Name it as the theoretically faster option and say why the heap is often the one you would ship.

Practice 215. Kth Largest Element in an Array (opens in a new tab)347. Top K Frequent Elements (opens in a new tab)

Merge k sorted linked lists.

Tier: CommonDifficulty: Medium

You are given k linked lists that are each already sorted, and you have to splice them into one sorted list. Put the head of every list into a min heap, then keep popping the smallest node, appending it to the answer, and pushing that node's successor in its place.

The problem

You are given k heads, each starting a linked list that is already sorted on its own, and some of those heads may be null. You return one sorted list spliced out of the same nodes, allocating nothing new. The lists can be different lengths and the same value can appear in several of them.

Input the lists 1 -> 4 -> 5, 1 -> 3 -> 4 and 2 -> 6.

Output 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6.

11234456null
The merged output list
Every node of the three inputs relinked into one chain, where the two accent nodes are the 1 that headed each of the first two lists.
StepHeap, smallest firstAppended
11, 1, 21
21, 2, 41
32, 3, 42
43, 4, 63
54, 4, 64
  • The heap holds one candidate per list, so it never grows past k however long the lists are.
  • Popping the smallest and pushing that node's successor keeps that true for the next round.

Kotlin

class Node(val value: Int, var next: Node? = null)

fun mergeKLists(lists: List<Node?>): Node? {
    val heap = PriorityQueue<Node>(compareBy { it.value })
    for (head in lists) if (head != null) heap.add(head)
    val dummy = Node(0)
    var tail = dummy
    while (heap.isNotEmpty()) {
        val node = heap.poll()
        tail.next = node
        tail = node
        node.next?.let { heap.add(it) }
    }
    return dummy.next
}

Java

class Node {
    int value;
    Node next;

    Node(int value) { this.value = value; }
}

static Node mergeKLists(List<Node> lists) {
    var heap = new PriorityQueue<Node>((a, b) -> Integer.compare(a.value, b.value));
    for (var head : lists) if (head != null) heap.add(head);
    var dummy = new Node(0);
    var tail = dummy;
    while (!heap.isEmpty()) {
        var node = heap.poll();
        tail.next = node;
        tail = node;
        if (node.next != null) heap.add(node.next);
    }
    return dummy.next;
}

The heap is a running answer to one question, which of the k candidate nodes is smallest right now. Since every list is already sorted, the global smallest remaining value is always one of the k current heads, so you never need to look deeper. Pushing the successor right after popping keeps that true for the next round.

Say the naive version first, collect every value and sort, which is O(N log N) where N is the total number of nodes. The heap version is O(N log k), because each node is pushed and popped once and the heap holds at most k. Extra space is O(k).

The dummy head keeps the loop free of a special case for the first node. The last node popped can never have a successor, since one would have gone back into the heap, so the tail already ends in null. The other answer worth naming is pairwise merging, combining lists two at a time like a tournament, which is also O(N log k) and needs no heap. Filtering out null heads before the first push is the whole guard against empty input lists.

Practice 23. Merge k Sorted Lists (opens in a new tab)21. Merge Two Sorted Lists, the building block (opens in a new tab)

Watch

Find the median from a data stream.

Tier: CommonDifficulty: Hard

Numbers arrive one at a time and after any of them you can be asked for the median so far. Keep the smaller half in a max heap and the larger half in a min heap, hold their sizes within one of each other, and the median is then sitting on top of one or both.

The problem

You build a small class rather than a single function. It takes numbers one at a time through an add operation, and a median operation must return the middle of everything added so far, at any moment. The median is the middle value once sorted, or the average of the middle two when the count is even, so it comes back as a decimal.

Input the adds 5, 15, 1 and 3, with the median read after each one.

Output the medians 5.0, 10.0, 5.0 and 4.0.

StepNumber addedLower half, a max heapUpper half, a min heapMedian
155empty5.0
21551510.0
315, 1155.0
433, 15, 154.0
  • The lower half is written with its largest first, the upper half with its smallest first, since that is what each heap hands you.
  • The two halves never differ in size by more than one, and the spare element lives in the lower half.
  • With an odd count the median is the lower half's top, which is 5 after three adds.
  • With an even count you average the two tops, 3 and 5 after four adds, giving 4.0.

Kotlin

class MedianFinder {
    private val lower = PriorityQueue<Int>(compareByDescending { it })
    private val upper = PriorityQueue<Int>()

    fun add(num: Int) {
        lower.add(num)
        upper.add(lower.poll())
        if (upper.size > lower.size) lower.add(upper.poll())
    }

    fun median(): Double =
        if (lower.size > upper.size) lower.peek().toDouble()
        else (lower.peek() + upper.peek()) / 2.0
}

Java

class MedianFinder {
    private final PriorityQueue<Integer> lower = new PriorityQueue<>(Comparator.reverseOrder());
    private final PriorityQueue<Integer> upper = new PriorityQueue<>();

    void add(int num) {
        lower.add(num);
        upper.add(lower.poll());
        if (upper.size() > lower.size()) lower.add(upper.poll());
    }

    double median() {
        return lower.size() > upper.size() ? lower.peek() : (lower.peek() + upper.peek()) / 2.0;
    }
}

The two line dance inside add does both jobs at once. Pushing into lower and immediately moving its largest into upper guarantees the new number lands on the correct side no matter where it belongs, because the max heap sorts that out for you. The last line only fixes the sizes, moving one back when upper has grown ahead. The result is that lower is never smaller than upper and never more than one bigger.

That invariant is the whole answer. Everything in lower is at most everything in upper, so the two tops are the middle of the sorted stream. With an odd count the extra element lives in lower, so its top is the median. With an even count the two tops are the middle pair and you average them.

Adding is O(log n) and reading the median is O(1), with O(n) space for the two heaps. The naive version to mention first is keeping a sorted list and inserting each number in place, which is O(n) per insert because of the shifting. The trap in Java is integer division, write the divisor as 2.0 or you will truncate every even median.

Practice 295. Find Median from Data Stream (opens in a new tab)480. Sliding Window Median, the harder two heap version (opens in a new tab)

Dynamic Programming & Greedy

Fractional knapsack.

Tier: CommonDifficulty: Easy

You are filling a bag of fixed capacity with items you are allowed to cut up. Sort the items by value per unit of weight, take whole items while they fit, and slice the one that does not.

The problem

Every item has a weight and a value, and you may cut an item, half of it giving half its value. You get the weights, the values and a bag capacity. Return the largest total value the bag can hold, as a decimal, since a partial item rarely lands on a whole number.

Input, weights [10, 20, 30], values [60, 100, 120], capacity 50.

Output, 240.0.

605142
Value per unit of weight
Each item's value divided by its weight, which for this input already runs highest to lowest.
100201202
Weight taken from each item
The first two items go in whole, and the last 20 of the capacity is filled with two thirds of the third.
  • Take the first whole, value 60, and 40 of the capacity is left.
  • Take the second whole, running total 160, and 20 of the capacity is left.
  • The third weighs 30 and will not fit, so take two thirds of it for 80, giving 240 and a full bag.

Kotlin

fun fractionalKnapsack(weights: IntArray, values: IntArray, capacity: Int): Double {
    val order = weights.indices.sortedByDescending { values[it].toDouble() / weights[it] }
    var left = capacity.toDouble()
    var total = 0.0
    for (i in order) {
        if (weights[i] <= left) {
            total += values[i]
            left -= weights[i]
        } else {
            total += values[i] * left / weights[i]
            break
        }
    }
    return total
}

Java

static double fractionalKnapsack(int[] weights, int[] values, int capacity) {
    var order = new Integer[weights.length];
    for (int i = 0; i < order.length; i++) order[i] = i;
    Arrays.sort(order, (x, y) -> Double.compare(
            (double) values[y] / weights[y], (double) values[x] / weights[x]));
    double left = capacity;
    double total = 0;
    for (int i : order) {
        if (weights[i] <= left) {
            total += values[i];
            left -= weights[i];
        } else {
            total += values[i] * left / weights[i];
            break;
        }
    }
    return total;
}

Think of the bag as holding weight, not items. Every kilogram you put in earns whatever that item pays per kilogram, so the best bag is filled with the highest paying kilograms available. Sorting by ratio lines those kilograms up in order, and the loop breaks after the first partial take because there is no space left for anything after it.

This is O(n log n) for the sort and O(n) for the walk, with O(n) space for the ordering. The exchange argument is the proof worth saying. If a bag skipped some of a higher ratio item to carry a lower ratio one, swapping one kilogram back raises the total, so no bag out of ratio order can be optimal.

That argument dies the moment items become all or nothing. In 0/1 knapsack a great ratio item can simply be too heavy to fit alongside the rest, and greedy walks past the better combination. Two items of weight three and value six each with capacity four is enough to show it, which is why 0/1 needs dynamic programming. Guard against a zero weight item if the input allows it, the ratio would divide by zero.

Practice 1710. Maximum Units on a Truck, the same value density greedy (opens in a new tab)

Watch

0/1 knapsack.

Tier: CommonDifficulty: Medium

Each item is either taken whole or left behind, and you want the most valuable load that fits. Keep one array where dp[c] is the best value for capacity c, and for each item sweep the capacities downwards.

The problem

Every item has a weight and a value, and an item is either taken whole or left behind, never cut. Given the weights, the values and a bag capacity, return the largest total value whose combined weight fits. Each item may be used at most once, which is the whole difference from the unbounded version.

Input, weights [1, 2, 4, 5], values [5, 4, 8, 6], capacity 5.

Output, 13, from the items weighing 1 and 4.

000102030405
The dp array before any item
One slot per capacity from 0 to 5, and the number under each slot is that capacity.
0051529394135
The dp array after every item
Each slot holds the best value that fits in that capacity, so the last slot is the answer.
  • After the item weighing 1, every capacity from 1 up holds 5, so dp is [0, 5, 5, 5, 5, 5].
  • After the item weighing 2, capacity 3 can hold both for 9, and dp becomes [0, 5, 5, 9, 9, 9].
  • After the item weighing 4, capacity 5 pairs it with dp[1] for 13, and the last item, weighing 5, only offers 6, so 13 stands.

Kotlin

fun knapsack(weights: IntArray, values: IntArray, capacity: Int): Int {
    val dp = IntArray(capacity + 1)
    for (i in weights.indices) {
        for (c in capacity downTo weights[i]) {
            dp[c] = maxOf(dp[c], values[i] + dp[c - weights[i]])
        }
    }
    return dp[capacity]
}

Java

static int knapsack(int[] weights, int[] values, int capacity) {
    int[] dp = new int[capacity + 1];
    for (int i = 0; i < weights.length; i++) {
        for (int c = capacity; c >= weights[i]; c--) {
            dp[c] = Math.max(dp[c], values[i] + dp[c - weights[i]]);
        }
    }
    return dp[capacity];
}

The version to describe first is the two dimensional table, one row per item and one column per capacity, where each cell asks the only question there is, take this item or not. Skipping copies the cell above. Taking adds this item's value to the cell above at capacity minus this item's weight. That table is O(n times W) in both time and space.

Since every row only reads the row above, you can drop the table and reuse a single array, and that is why the inner loop counts downwards. Going down, dp[c - weights[i]] has not been touched yet on this item's pass, so it still holds the previous row and the item is used at most once. Sweeping upwards would read a cell this same item already updated, letting you take it again and again. That upward sweep is not a bug, it is exactly the unbounded knapsack, so one loop direction is the whole difference between the two problems.

Time stays O(n times W) and space drops to O(W). Say out loud that this is pseudo polynomial, the cost grows with the numeric value of the capacity rather than the size of the input, so a huge capacity hurts even with a handful of items.

Practice 416. Partition Equal Subset Sum, 0/1 knapsack with a fixed target (opens in a new tab)494. Target Sum, the same subset DP with signs (opens in a new tab)

Watch

Intervals & Scheduling

Merge overlapping intervals.

Tier: EssentialDifficulty: Medium

Collapse a pile of start and end pairs into the fewest ranges that cover the same ground. Sort by start time and walk them in order, either stretching the end of the last range you kept or pushing a new one.

The problem

You are given a list of intervals, each one a start and an end, in no particular order. Two intervals overlap when one starts at or before the other ends. Return the shortest list of intervals that covers exactly the same ground, sorted by start, with no two of them overlapping.

Input [[1, 3], [2, 6], [8, 10], [15, 18]]

Output [[1, 6], [8, 10], [15, 18]]

Interval readDoes it reach the last kept oneOutput so far
[1, 3]nothing kept yet[1, 3]
[2, 6]yes, 2 lands inside [1, 3][1, 6]
[8, 10]no, 8 is past 6[1, 6], [8, 10]
[15, 18]no, 15 is past 10[1, 6], [8, 10], [15, 18]
  • The list is already sorted by start, so push [1, 3] as the first output interval
  • [2, 6] starts at 2, which is inside [1, 3], so stretch that end from 3 out to 6
  • [8, 10] starts after 6, so nothing can reach it and it becomes a new output interval
  • [15, 18] starts after 10, so it is another new one, leaving three intervals

Kotlin

fun merge(intervals: Array<IntArray>): Array<IntArray> {
    intervals.sortBy { it[0] }
    val out = mutableListOf<IntArray>()
    for (interval in intervals) {
        val last = out.lastOrNull()
        if (last != null && interval[0] <= last[1]) {
            last[1] = maxOf(last[1], interval[1])
        } else {
            out.add(intArrayOf(interval[0], interval[1]))
        }
    }
    return out.toTypedArray()
}

Java

static int[][] merge(int[][] intervals) {
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
    var out = new ArrayList<int[]>();
    for (int[] interval : intervals) {
        int[] last = out.isEmpty() ? null : out.get(out.size() - 1);
        if (last != null && interval[0] <= last[1]) {
            last[1] = Math.max(last[1], interval[1]);
        } else {
            out.add(new int[] { interval[0], interval[1] });
        }
    }
    return out.toArray(new int[0][]);
}

Sorting by start is what makes one comparison enough. Once the intervals arrive in start order, anything that overlaps the current group has to overlap the last interval you pushed, because that one has the latest start so far. So you never look further back than one interval.

The max on the end matters more than it looks. An interval can sit entirely inside the one you are holding, like 2 to 3 inside 1 to 10, and without the max you would shrink a merged interval instead of growing it. Copying into the output rather than pushing the caller's array keeps you from mutating their input while you extend ends.

It is O(n log n) time, dominated by the sort, and O(n) for the output. The naive approach compares every interval against every other and merges repeatedly until nothing changes, which is O(n squared) or worse. Ask whether touching endpoints count as overlapping. Here 1 to 4 and 4 to 5 merge, because the check is less than or equal, and a strict less than is the other reading.

Practice 56. Merge Intervals (opens in a new tab)57. Insert Interval (opens in a new tab)

Watch

Solve the minimum meeting rooms scheduling problem.

Tier: CommonDifficulty: MediumAsked at: phonepe

Work out how many rooms you need so that no two meetings ever share one. Split every meeting into a start event and an end event, sort the two lists separately, then sweep through time counting rooms up on a start and down on an end.

The problem

You are given a list of meetings, each with a start time and an end time, in no particular order. Two meetings that overlap in time need two rooms. Return the smallest number of rooms that holds every meeting, a single count and not an assignment. A meeting that ends exactly when another starts hands its room over, so touching times need only one room.

Input [[0, 30], [5, 10], [15, 20]]

Output 2

Next event in time orderRooms in use
a meeting starts at 01
a meeting starts at 52
a meeting ends at 101
a meeting starts at 152
  • The starts sort to [0, 5, 15] and the ends sort to [10, 20, 30]
  • The 0 start comes before the 10 end, so one room is in use
  • The 5 start also comes before the 10 end, so a second room opens and the peak reaches 2
  • The 10 end comes before the 15 start, so a room frees up, then the 15 start takes it back and the peak stays 2

Kotlin

fun minMeetingRooms(intervals: List<IntArray>): Int {
    val starts = intervals.map { it[0] }.sorted()
    val ends = intervals.map { it[1] }.sorted()
    var rooms = 0
    var maxRooms = 0
    var s = 0
    var e = 0
    while (s < starts.size) {
        if (starts[s] < ends[e]) {
            rooms++
            s++
        } else {
            rooms--
            e++
        }
        maxRooms = maxOf(maxRooms, rooms)
    }
    return maxRooms
}

Java

static int minMeetingRooms(List<int[]> intervals) {
    var starts = intervals.stream().mapToInt(i -> i[0]).sorted().toArray();
    var ends = intervals.stream().mapToInt(i -> i[1]).sorted().toArray();
    int rooms = 0;
    int maxRooms = 0;
    int s = 0;
    int e = 0;
    while (s < starts.length) {
        if (starts[s] < ends[e]) {
            rooms++;
            s++;
        } else {
            rooms--;
            e++;
        }
        maxRooms = Math.max(maxRooms, rooms);
    }
    return maxRooms;
}

Say the intuition before the code. At any moment the rooms in use are exactly the meetings that have started and not yet ended, so the answer is the peak of that count as you move through time. Breaking each meeting apart and sorting starts and ends separately is what lets one linear sweep with two pointers find that peak.

The loop stops when the starts run out, and that is correct, because the count can only rise on a start. Anything after the last start is a run of ends, and those can only take the count down.

It is O(n log n) for the two sorts with an O(n) sweep, and O(n) space for the two arrays. The other standard answer is a min heap of end times, pushing each meeting as you go and popping ends that have already passed, where the heap size is the room count. Same complexity, and it generalises better when you also need to say which room each meeting got.

Practice 253. Meeting Rooms II, premium (opens in a new tab)1094. Car Pooling, the same sweep (opens in a new tab)

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Arrays & Sorting

Output a delta encoding for a sequence: the first element is reproduced as-is, each subsequent element is the numeric difference from the element before it.

Tier: Less commonDifficulty: MediumAsked at: booking-com

You are given a sequence of numbers and you have to output the first one unchanged and every later one as its gap from the number before it. Walk the array once, copy the first element across, and write each remaining slot as the current value minus its left neighbour.

The problem

You are given an array of integers. You return a new array of the same length, where slot zero copies the first input value and every later slot holds that value minus the one on its left. Differences can be negative or zero, and an empty input gives an empty output.

Input nums = [3, 8, 6, 10, 10].

Output [3, 5, -2, 4, 0].

308162103104
The input sequence
The original values, where each one after the first is measured against its left neighbour.
3051-224304
The delta encoded output
The accent cell is copied straight across, and every other slot is a gap, so a fall gives a negative and a repeat gives 0.
  • Slot 0 is copied straight across as 3.
  • Slot 1 is 8 minus 3, which is 5.
  • Slot 2 is 6 minus 8, which is -2, so deltas can be negative.
  • Slots 3 and 4 are 10 minus 6 and 10 minus 10, giving 4 and 0.

Kotlin

fun deltaEncode(nums: IntArray): IntArray {
    if (nums.isEmpty()) return nums
    val out = IntArray(nums.size)
    out[0] = nums[0]
    for (i in 1 until nums.size) out[i] = nums[i] - nums[i - 1]
    return out
}

Java

static int[] deltaEncode(int[] nums) {
    if (nums.length == 0) return nums;
    var out = new int[nums.length];
    out[0] = nums[0];
    for (int i = 1; i < nums.length; i++) out[i] = nums[i] - nums[i - 1];
    return out;
}

There is no trick here, which is worth saying plainly rather than hunting for one. The interviewer is checking that you read the spec correctly and do not overcomplicate a single pass problem. It is O(n) time and O(n) space for the output array, and the empty input guard is the only edge case.

The one thing worth raising yourself is whether the output may overwrite the input. Going left to right cannot, because computing slot i needs the original value at i - 1, which you would already have replaced. Walking from the end backwards does work, since each difference is taken before the element it used gets touched, and that buys you O(1) extra space.

The natural follow up is the decoder. Running totals undo it, keep a sum, add each delta, and print the sum, which restores the original sequence in one pass. Mentioning that the encoding is lossless and reversible unprompted is what separates a correct answer from a strong one.

Three integer arrays are given with duplicate numbers. Find the common elements among the three arrays.

Tier: Less commonDifficulty: MediumAsked at: booking-com

You want the values present in all three arrays, counted once each despite the duplicates. Turn two of the arrays into hash sets, then keep the values of the third that both sets contain.

The problem

Three unsorted integer arrays are given and any of them may repeat a value. Return the distinct values that appear in all three. Each value comes back once however often it repeats, and the order of the result does not matter.

Input, a = [1, 5, 5, 10, 20], b = [3, 4, 5, 5, 10], c = [5, 5, 10, 20].

Output, the two values 5 and 10.

105152103204
Array a
The slots of a that survive the filter, where 5 is kept once even though it passes twice.
Value in aIn bIn cKept
1nonono
5yesyesyes
10yesyesyes
20noyesno
  • Build a set from b, which is 3, 4, 5, 10, and a set from c, which is 5, 10, 20.
  • Walk a, keeping only the values both sets contain, so 1 and 20 are dropped.
  • The second copy of 5 passes the same test, but the result is a set, so it lands once.

Kotlin

fun commonElements(a: IntArray, b: IntArray, c: IntArray): Set<Int> {
    val setB = b.toHashSet()
    val setC = c.toHashSet()
    return a.filterTo(HashSet()) { it in setB && it in setC }
}

Java

static Set<Integer> commonElements(int[] a, int[] b, int[] c) {
    var setB = Arrays.stream(b).boxed().collect(Collectors.toSet());
    var setC = Arrays.stream(c).boxed().collect(Collectors.toSet());
    return Arrays.stream(a).boxed()
            .filter(n -> setB.contains(n) && setC.contains(n))
            .collect(Collectors.toSet());
}

Collecting into a set is the detail worth explaining. The arrays are stated to have duplicates, and without deduplicating you would either report the same common value several times, or have to track counts across all three, which is a different question.

Membership in a hash set is O(1) on average, so building the two sets and filtering the third is O(a plus b plus c) in time and the same in space. The alternative without extra space is sorting all three and walking them with three pointers, advancing whichever pointer holds the smallest value. That is O(n log n) and worth naming if the interviewer rules out the memory.

Ask about ordering before you finish. A hash set promises none, so if the caller expects the values in input order or ascending, use a LinkedHashSet or sort the result. If the question really means counts, the answer becomes the smallest of the three frequencies per value, not a set.

Practice 349. Intersection of Two Arrays (opens in a new tab)

Strings

Check if two strings are rotations of each other. If they are, print the number of rotations.

Tier: Less commonDifficulty: MediumAsked at: paytm

Two strings are rotations of each other when moving some number of characters from the front of one to its back produces the other. Concatenate the first string with itself, and the second string turns up inside it at exactly the rotation count.

The problem

You are given two strings. A left rotation moves one character from the front of a string to its back, so "abc" rotated once is "bca". Return how many left rotations turn the first string into the second, and return -1 when no number of rotations does. Two strings of different lengths are never rotations of each other.

Input a = "abcde" and b = "cdeab"

Output 2

Window startWindow of "abcdeabcde"Is it b
0abcdeno
1bcdeano
2cdeabyes
  • Doubling the first string gives "abcdeabcde"
  • Every rotation of "abcde" is one five character window of that doubled string
  • The window starting at index 2 is "cdeab", which is the second string
  • So the answer is that starting index, 2

Kotlin

fun rotationCount(a: String, b: String): Int {
    if (a.length != b.length) return -1
    return (a + a).indexOf(b)
}

Java

static int rotationCount(String a, String b) {
    if (a.length() != b.length()) return -1;
    return (a + a).indexOf(b);
}

The doubled string is the whole insight, everything after it is typing. Rotating by k moves the first k characters to the back, and that is the same as reading the doubled string from index k. So the index where the match starts is the rotation count, and a missing match gives the -1 you already want.

The length check up front is not optional. Without it a shorter second string could match some fragment and report a rotation that never happened.

The naive answer is to rotate the first string one step at a time and compare, which is O(n squared). Here indexOf is O(n) on typical input, but it is a naive substring search underneath, so the worst case is still O(n squared). Say that out loud, then name KMP for a real O(n) guarantee. Space is O(n) for the doubled string.

Practice 796. Rotate String (opens in a new tab)

Given a list of positive words, negative words, and a review, determine if the review is positive, negative or neutral.

Tier: Less commonDifficulty: MediumAsked at: booking-com

You are given two word lists and a block of review text, and you have to label the review positive, negative or neutral. Put both word lists into hash sets, split the review into lowercased words, and add one for every positive hit and subtract one for every negative hit, then read the sign of the total.

The problem

You are given a list of positive words, a list of negative words, and one review string. Every word of the review scores one, minus one, or nothing, and you return the label that the sign of the total gives, positive, negative or neutral. A total of zero is neutral, whether the praise and the complaints cancelled out or the review held no listed words at all.

Input the positive words [good, great, love], the negative words [bad, terrible], and the review "The room was great but the food was bad and the service was terrible."

Output negative.

WordScoreRunning total
great11
bad-10
terrible-1-1
  • Lowercasing and splitting on runs of non letters gives fourteen plain words.
  • Only the three words in the table are in either list, and everything else scores nothing.
  • The total lands at minus one, so the review is negative.

Kotlin

fun classify(review: String, positive: Set<String>, negative: Set<String>): String {
    val score = review.lowercase().split(Regex("\\W+")).sumOf {
        when (it) {
            in positive -> 1
            in negative -> -1
            else -> 0
        }
    }
    return when {
        score > 0 -> "positive"
        score < 0 -> "negative"
        else -> "neutral"
    }
}

Java

static String classify(String review, Set<String> positive, Set<String> negative) {
    int score = 0;
    for (var word : review.toLowerCase().split("\\W+")) {
        if (positive.contains(word)) score++;
        else if (negative.contains(word)) score--;
    }
    if (score > 0) return "positive";
    if (score < 0) return "negative";
    return "neutral";
}

The two decisions worth stating out loud are why sets and not lists, and how you tokenize. A set answers membership in O(1), so the whole review costs O(n) in its word count, where scanning a list for every word would cost O(n times the size of the word lists). Splitting on a run of non letters after lowercasing handles punctuation and casing together, so "Great!" and "great" both count.

Say what neutral means before you write code. A score of zero covers two different situations, a review with equal praise and complaint, and a review with no sentiment words at all. Confirm with the interviewer whether those should really share a label, because that question is half of what they are testing.

Time is O(n) in the review's word count plus O(m) to build the sets, and space is O(m) for the sets. The obvious follow ups are negation, where "not good" should flip sign, and weighting, where "terrible" counts more than "bad". Both are easy to describe and worth naming rather than building.

Given a list of words as input, output another list of strings, each containing words that are mutual anagrams.

Tier: Less commonDifficulty: MediumAsked at: booking-com

Bucket the words so that every bucket holds words that are rearrangements of each other. Give each word a key that is the same for every anagram of it and different for everything else, its sorted letters, then group on that key in one pass.

The problem

You are given a list of words. Two words are mutual anagrams when one is a rearrangement of the other, holding the same letters the same number of times. Return the words grouped, each group holding words that are anagrams of one another. Every word lands in exactly one group, and a word with no partner forms a group on its own.

Input ["eat", "tea", "tan", "ate", "nat", "bat"]

Output [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

WordSorted lettersGroup
eataetfirst
teaaetfirst
tanantsecond
ateaetfirst
natantsecond
batabtthird
  • Sorting the letters of "eat" gives "aet", which is a brand new key, so it opens a bucket
  • "tea" and later "ate" both sort to "aet" as well, so they drop into that same bucket
  • "tan" sorts to "ant" and opens a second bucket, which "nat" then joins
  • "bat" sorts to "abt", matches nothing, and ends up alone in a third bucket

Kotlin

fun groupAnagrams(words: List<String>): List<List<String>> {
    val groups = LinkedHashMap<String, MutableList<String>>()
    for (word in words) {
        val key = word.toCharArray().sorted().joinToString("")
        groups.getOrPut(key) { mutableListOf() }.add(word)
    }
    return groups.values.toList()
}

Java

static List<List<String>> groupAnagrams(List<String> words) {
    var groups = new LinkedHashMap<String, List<String>>();
    for (var word : words) {
        var letters = word.toCharArray();
        Arrays.sort(letters);
        groups.computeIfAbsent(new String(letters), k -> new ArrayList<>()).add(word);
    }
    return new ArrayList<>(groups.values());
}

Say the insight out loud first. Sorting the letters of a word gives a canonical form, one that every anagram of that word shares and no other word produces. Once you see that, grouping is just a map from the canonical key to the original words that produced it.

Using a LinkedHashMap rather than a plain HashMap costs nothing and gives you the groups in the order the words first appeared, which makes the output stable and easy to test.

Sorting one word is O(k log k) for a word of length k, so over n words the whole thing is O(n times k log k). Space is O(n times k), since every word is stored once inside the map. If the interviewer pushes for a faster key, build it from a 26 slot letter count instead of a sort, which gets each key in O(k) and trades a little clarity for a better bound.

Practice 49. Group Anagrams (opens in a new tab)

Hashing & Sets

Given an array, determine if there are repeated elements. If an element is repeated more than 3 times, return those elements.

Tier: Less commonDifficulty: MediumAsked at: booking-com

The question is a frequency count wearing a disguise. Count every value once with a hash map, then read the answer off the counts, anything above one is a repeat and anything above three goes in the result.

The problem

You get an array of integers in no promised order. Return the distinct values that appear more than three times, in any order. Strictly more than three, so a value seen exactly three times does not qualify, and an empty result means nothing crossed the line rather than that the array is free of duplicates.

Input, [1, 2, 1, 3, 1, 1, 2, 4].

Output, [1].

1021123314152647
The input
The four slots holding 1 are in the accent, the only value that appears more than three times.
ValueTimes seenOver the threshold
14yes
22no
31no
41no
  • One pass builds those counts, and any count above one already answers the first half of the question, so this array does have repeats.
  • Filtering the map for counts above three keeps only the entry for 1.
  • If the threshold were two instead, the same filter would return 1 and 2 with no other change.

Kotlin

fun repeatedMoreThanThree(nums: IntArray): List<Int> {
    val counts = HashMap<Int, Int>()
    for (n in nums) counts[n] = (counts[n] ?: 0) + 1
    return counts.filterValues { it > 3 }.keys.toList()
}

Java

static List<Integer> repeatedMoreThanThree(int[] nums) {
    var counts = new HashMap<Integer, Integer>();
    for (int n : nums) counts.merge(n, 1, Integer::sum);
    return counts.entrySet().stream()
            .filter(e -> e.getValue() > 3)
            .map(Map.Entry::getKey)
            .toList();
}

The thing worth saying explicitly is why a hash map and not sorting first. Sorting reaches the same answer by walking runs of equal adjacent values, which is O(n log n) time and O(1) extra space if you sort in place. The map is O(n) time at the cost of O(n) space. Either is a reasonable answer, and naming that trade is exactly what the interviewer wants.

A plain set is not enough on its own. It tells you an element repeated at all, the moment an add fails because the value is already there, but it cannot say how many times without a count beside it. Since the question asks for more than three, the count has to be stored.

An empty result does not mean the array is free of duplicates, only that nothing crossed the threshold. If the caller needs both answers, return the map or a flag alongside the list rather than making them count again.

Practice 347. Top K Frequent Elements (opens in a new tab)

Intervals & Scheduling

Given dates and the number of check-ins and check-outs on those dates, find the busiest day in the hotel (merge interval type question).

Tier: Less commonDifficulty: HardAsked at: booking-com

You are given, for each date, how many guests checked in and how many checked out, and you have to name the date the hotel was fullest. Treat every date as a net change in occupancy, sort the dates, then sweep through keeping a running total, and the date where that total peaks is the answer.

The problem

You are given one record per date, each holding that date, how many guests checked in and how many checked out. You return the single date on which the hotel held the most guests, counted after that date's arrivals and departures are both applied. The records do not arrive in date order, and occupancy carries over from one date to the next rather than resetting.

Input the records (3, 0 in, 4 out), (1, 5 in, 0 out) and (2, 3 in, 2 out).

Output 2.

DateInOutOccupancy after
1505
2326
3042
  • Sorting by date puts day 1 first, then day 2, then day 3.
  • Occupancy is a running total, so day 2 stacks its net one guest on top of day 1's five.
  • Six is the highest that total ever reaches, so day 2 is the answer.

Kotlin

data class Stay(val date: Int, val checkIns: Int, val checkOuts: Int)

fun busiestDay(stays: List<Stay>): Int {
    var occupancy = 0
    var best = 0
    var peak = Int.MIN_VALUE
    for (s in stays.sortedBy { it.date }) {
        occupancy += s.checkIns - s.checkOuts
        if (occupancy > peak) {
            peak = occupancy
            best = s.date
        }
    }
    return best
}

Java

record Stay(int date, int checkIns, int checkOuts) {}

static int busiestDay(List<Stay> stays) {
    int occupancy = 0;
    int best = 0;
    int peak = Integer.MIN_VALUE;
    for (var s : stays.stream().sorted(Comparator.comparingInt(Stay::date)).toList()) {
        occupancy += s.checkIns() - s.checkOuts();
        if (occupancy > peak) {
            peak = occupancy;
            best = s.date();
        }
    }
    return best;
}

This is the sweep line idea behind the meeting rooms problem, and saying that connection out loud is a strong signal. You are tracking guests instead of rooms, and each date already carries both its arrivals and its departures, so you net them into one change rather than processing two separate event streams.

The running total is the occupancy carried forward, not that day's traffic. That is the part candidates get wrong. A quiet day with two arrivals can still be the busiest day of the year if a hundred guests are already in the building.

Sorting costs O(n log n) and the sweep is a single O(n) pass over the sorted records, holding only a running total and the best seen so far. Worth confirming with the interviewer whether check outs should be applied before check ins on the same date, since that ordering decides whether a guest leaving and another arriving on one day counts as a room held continuously or briefly empty.

Practice 1094. Car Pooling (opens in a new tab)56. Merge Intervals (opens in a new tab)

Geometry & Math

Identify whether four sides (given by four integers) can form a square, a rectangle or neither.

Tier: Less commonDifficulty: EasyAsked at: booking-com

A rectangle needs its four sides to fall into two equal pairs, and a square is the case where both pairs happen to be the same length. Sort the four numbers and the pairing check becomes two comparisons.

The problem

You are given four integers, four side lengths in no particular order. Return square when all four are equal, rectangle when they form two matching pairs of different lengths, and neither for anything else. Only the lengths are given, no angles and no coordinates, so a matching pair is the only evidence you have. A length of zero or less is not a real side, so that input is neither.

Input [3, 5, 5, 3]

Output rectangle

SidesSortedPairsAnswer
[3, 5, 5, 3][3, 3, 5, 5]3 with 3, then 5 with 5rectangle
[4, 4, 4, 4][4, 4, 4, 4]4 with 4, then 4 with 4square
[2, 2, 3, 5][2, 2, 3, 5]2 with 2, then 3 with 5neither
  • All four are positive, so the shape check is worth doing
  • Sorting gives [3, 3, 5, 5], so the two smallest match and the two largest match, which makes it a rectangle
  • The small pair is 3 and the large pair is 5, and those differ, so it is not a square

Kotlin

fun classify(sides: IntArray): String {
    if (sides.any { it <= 0 }) return "neither"
    val s = sides.sorted()
    if (s[0] != s[1] || s[2] != s[3]) return "neither"
    return if (s[0] == s[2]) "square" else "rectangle"
}

Java

static String classify(int[] sides) {
    for (int side : sides) if (side <= 0) return "neither";
    var s = sides.clone();
    Arrays.sort(s);
    if (s[0] != s[1] || s[2] != s[3]) return "neither";
    return s[0] == s[2] ? "square" : "rectangle";
}

Sorting first is what makes the check trivial. In sorted order a rectangle's sides pair up as the two smallest matching each other and the two largest matching each other, so s[0] == s[1] and s[2] == s[3] settles it. Comparing s[0] with s[2] then asks whether the two pairs are the same length, which is exactly what makes a square.

Say the validation step out loud before the geometry. A zero or negative length is not a side, so rejecting it up front stops you reporting a rectangle for degenerate input. Copying the array before sorting keeps the caller's input untouched.

It is O(n log n) for the sort, which on exactly four elements is a constant, and O(1) space. The follow up is the four points version of the same question, where you are handed coordinates instead of lengths and have to check the angles too, since four equal pairs also describe a parallelogram.