DSA in Java — Easy Notes

The same DSA topics, explained for a total beginner. Short sentences, small examples, simple Java. Read this first, then move on to the full notes. Each topic number matches the full notes.

Part 1 — Foundations

1. How to Approach a Problem

In plain words

DSA means Data Structures (ways to store data) and Algorithms (step-by-step recipes to solve a problem). In an interview, they want to see how you think, not only the final code.

Think of cooking a new dish. First you read the recipe and check what is in the fridge. Then you cook a simple version. Only after that do you make it faster or tastier.

Key points

  • Understand the question first. Say it again in your own words.
  • Ask questions: Can the array be empty? Can numbers be negative? Are there duplicates?
  • Try a tiny example by hand, like [2, 7, 11]. Write the answer you expect.
  • Start with the simple (brute force) idea. Brute force means "try every possibility". It is slow, but correct.
  • Then improve it. Ask: "What work am I repeating? Can a HashMap or sorting help?"
  • Test your code with your tiny example and with edge cases (empty input, one item).

Step by step: "Two Sum" (find two numbers that add up to a target)

StepWhat you do
1. UnderstandInput [2, 7, 11, 15], target 9. Return the two positions (indexes).
2. Ask"Is there always exactly one answer?" — Yes.
3. Example2 + 7 = 9, so the answer is [0, 1].
4. Brute forceCheck every pair. Two loops. O(n²).
5. ImproveFor each number, remember it in a HashMap. Look up target - number. O(n).
6. TestTry [3, 3], target 6 → [0, 1]. Works.
// Step 4: the simple (brute force) answer. Check every pair.
static int[] twoSumBrute(int[] nums, int target) {
    for (int i = 0; i < nums.length; i++) {          // first number
        for (int j = i + 1; j < nums.length; j++) {  // second number, after the first
            if (nums[i] + nums[j] == target) {        // do they add up?
                return new int[] { i, j };            // yes: return both positions
            }
        }
    }
    return new int[] { -1, -1 };                      // no pair found
}

Time & space: O(n²) time — every pair is checked. O(1) extra space. (The faster HashMap version is in topic 7.)

Remember: understand → example → brute force → improve → test. Talk out loud at every step.
Practice: Two Sum (#1), Contains Duplicate (#217).

2. Big-O & Complexity

In plain words

Big-O tells you how the work grows when the input grows. We write n for the input size (for example, how many items are in an array). Time complexity = how many steps. Space complexity = how much extra memory.

Finding a friend's name in your phone contacts. Scrolling one by one gets slower when you have more contacts (O(n)). Typing the name in search is almost instant, even with 5,000 contacts (close to O(1) or O(log n)).

input size n → steps (time) O(1) O(log n) O(n) O(n log n) O(n²) Lower curve = faster code. O(n²) gets slow very quickly.
How the number of steps grows. For big inputs, the shape of the curve matters much more than small details.

Key points

  • O(1) = constant. Same time for any size. Example: arr[5].
  • O(log n) = we cut the problem in half each step. Example: binary search.
  • O(n) = we look at each item once. Example: one loop.
  • O(n log n) = good sorting. O(n²) = a loop inside a loop.
  • Drop constants: O(2n) is just O(n). Keep the biggest part: O(n² + n) is O(n²).
  • Rough rule: a computer does about 100 million simple steps per second.

How many steps?

nO(log n)O(n)O(n log n)O(n²)
10~310~33100
1,000~101,000~10,0001,000,000
1,000,000~201,000,000~20,000,0001,000,000,000,000 (too slow!)
static void examples(int[] arr) {
    int first = arr[0];                       // O(1): one step, any size

    int sum = 0;
    for (int x : arr) {                       // O(n): one pass over the array
        sum += x;
    }

    int pairs = 0;
    for (int i = 0; i < arr.length; i++) {     // O(n²): loop inside a loop
        for (int j = 0; j < arr.length; j++) {
            pairs++;
        }
    }
    System.out.println(first + " " + sum + " " + pairs);
}

Time & space: count the loops. One loop over n = O(n). A loop inside a loop = O(n²). Halving each time = O(log n).

Remember: Big-O is about growth. If n can be 100,000, an O(n²) answer is usually too slow — look for O(n) or O(n log n).
Practice: think about the Big-O of every problem you solve from now on.

3. Recursion & the Call Stack

In plain words

Recursion is when a function calls itself with a smaller problem. It needs a base case: a small case where it stops and answers directly. The call stack is where Java remembers every function that is still waiting for an answer.

Russian dolls. You open a doll and find a smaller doll inside. You keep opening until you reach the tiny doll that does not open. That tiny doll is the base case.

Call stack (newest on top) fact(1) base case fact(2) waits: 2 * fact(1) fact(3) waits: 3 * fact(2) main calls fact(3) calls go up returns 1 returns 2 × 1 = 2 returns 3 × 2 = 6 Answers flow back down, one frame at a time, and each frame is removed.
Each call waits on the stack until the call above it returns. Too many calls without a base case = StackOverflowError.

Key points

  • Every recursive function needs: a base case (stop) and a recursive case (call itself on a smaller input).
  • Forget the base case → the function never stops → StackOverflowError.
  • Each waiting call uses memory on the stack. So recursion depth n = O(n) space.
  • Trust the function: assume fact(n - 1) already works, and just use its answer.
  • Recursion is the base of trees, backtracking, and divide & conquer.

Dry run: fact(3)

CallIs it the base case?What it returns
fact(3)No3 * fact(2) = 3 * 2 = 6
fact(2)No2 * fact(1) = 2 * 1 = 2
fact(1)Yes (n <= 1)1
static int fact(int n) {
    if (n <= 1) {              // base case: stop here
        return 1;
    }
    return n * fact(n - 1);    // recursive case: smaller problem
}

Time & space: O(n) time — n calls. O(n) space — n calls wait on the stack at the same time.

Remember: base case first, then a smaller call. Draw the call stack when you feel lost.
Practice: Fibonacci Number (#509), Reverse String (#344), Power of Two (#231).

4. Java Toolkit for DSA

In plain words

Java already gives you ready-made data structures in java.util. You do not need to build them yourself. You just need to know which one to pick.

A toolbox. A hammer for nails, a screwdriver for screws. ArrayList for a growing list, HashMap for fast lookup, ArrayDeque for a stack or a queue.

Key points

  • int[] = fixed size array. ArrayList = array that can grow.
  • HashMap = key → value pairs. HashSet = unique items. Both find things in O(1) on average.
  • ArrayDeque = use it as a stack or a queue. (Avoid the old Stack class.)
  • PriorityQueue = always gives you the smallest item first (a heap).
  • Arrays.sort(arr) sorts an array. Collections.sort(list) sorts a list.
  • Collections hold objects, so write Integer, not int: List<Integer>.
I need…UseMain methods
A growing listArrayListadd, get, size
Fast lookup by keyHashMapput, get, getOrDefault, containsKey
Unique itemsHashSetadd, contains, remove
Stack (last in, first out)ArrayDequepush, pop, peek
Queue (first in, first out)ArrayDequeoffer, poll, peek
Smallest item fastPriorityQueueoffer, poll, peek
static void toolkitDemo() {
    List<Integer> list = new ArrayList<>();       // growing list
    list.add(5);                                    // [5]

    Map<String, Integer> age = new HashMap<>();   // key -> value
    age.put("Ali", 25);
    int a = age.getOrDefault("Sara", 0);            // 0 if key is missing

    Set<Integer> seen = new HashSet<>();          // unique items
    seen.add(3);
    boolean has = seen.contains(3);                 // true

    Deque<Integer> stack = new ArrayDeque<>();    // stack
    stack.push(1);
    int top = stack.pop();                          // 1

    Queue<Integer> queue = new ArrayDeque<>();    // queue
    queue.offer(1);
    int front = queue.poll();                       // 1

    PriorityQueue<Integer> heap = new PriorityQueue<>();  // min-heap
    heap.offer(4);
    heap.offer(2);
    int smallest = heap.poll();                     // 2

    int[] arr = { 3, 1, 2 };
    Arrays.sort(arr);                               // [1, 2, 3]
    System.out.println(a + " " + has + " " + top + " " + front + " " + smallest);
}

Time & space: HashMap/HashSet get and put: O(1) on average. PriorityQueue offer/poll: O(log n). Sorting: O(n log n).

Remember: HashMap for "have I seen this?", ArrayDeque for stack and queue, PriorityQueue for "smallest/largest first".
Practice: Valid Anagram (#242), Contains Duplicate (#217).

5. Bit Manipulation

In plain words

Computers store numbers in binary: only 0s and 1s. Each 0 or 1 is called a bit. Example: 5 = 101, 3 = 011. Bit tricks work on these 0s and 1s directly. They are very fast.

A row of light switches. Each switch is on (1) or off (0). Bit operations flip, check, or compare the switches.

Key points

  • AND a & b: 1 only if both bits are 1.
  • OR a | b: 1 if at least one bit is 1.
  • XOR a ^ b: 1 if the bits are different. Important: x ^ x = 0 and x ^ 0 = x.
  • Shift: x << 1 doubles x. x >> 1 halves x.
  • (n & 1) == 1 means n is odd. n & (n - 1) removes the lowest 1 bit.

Dry run: 5 & 3, 5 | 3, 5 ^ 3

BitsNumber
51015
30113
5 & 30011
5 | 31117
5 ^ 31106
// Single Number: every number appears twice except one.
// Pairs cancel out with XOR (x ^ x = 0), so only the single one is left.
static int singleNumber(int[] nums) {
    int result = 0;
    for (int x : nums) {
        result = result ^ x;      // XOR everything together
    }
    return result;
}

// Count how many bits are 1
static int countOnes(int n) {
    int count = 0;
    while (n != 0) {
        n = n & (n - 1);          // remove the lowest 1 bit
        count++;
    }
    return count;
}

Time & space: singleNumber is O(n) time, O(1) space — one pass, one variable.

Remember: XOR cancels pairs. n & 1 checks odd/even. This topic is less common — learn the basics and move on.
Practice: Single Number (#136), Number of 1 Bits (#191), Counting Bits (#338).
Part 2 — Linear Structures

6. Arrays & Strings

In plain words

An array is a row of boxes, side by side in memory. Each box has a number called an index. The first index is 0, not 1. A string is like an array of characters.

A row of lockers in a school. Each locker has a number. If you know the number, you walk straight to it. You don't open all the other lockers.

arr = 3 1 4 1 5 index 0 1 2 3 4 arr[2] is 4 — found in one step, O(1) length = 5, so the last index is 5 - 1 = 4
Indexes start at 0. The last index is always length - 1. Going past it gives ArrayIndexOutOfBoundsException.

Key points

  • Reading or changing arr[i] is O(1) — instant.
  • Inserting or deleting in the middle is O(n) — all later items must move over.
  • An int[] has a fixed size. Use ArrayList if it must grow.
  • Java strings can't be changed (they are immutable). To build a string piece by piece, use StringBuilder.
  • Useful: s.length(), s.charAt(i), s.toCharArray().

Dry run: find the largest number in [3, 1, 4]

iarr[i]Bigger than max?max after this step
start——3 (first item)
11No3
24Yes4
static int findMax(int[] arr) {
    int max = arr[0];                         // start with the first item
    for (int i = 1; i < arr.length; i++) {    // look at every other item
        if (arr[i] > max) {                   // found a bigger one?
            max = arr[i];                     // remember it
        }
    }
    return max;
}

static String reverse(String s) {
    StringBuilder sb = new StringBuilder();   // fast way to build a string
    for (int i = s.length() - 1; i >= 0; i--) {  // walk from the end
        sb.append(s.charAt(i));
    }
    return sb.toString();                     // "abc" -> "cba"
}

Time & space: findMax is O(n) — we look at each item once. O(1) extra space.

Remember: index starts at 0, ends at length - 1. Use StringBuilder to build strings in a loop.
Practice: Reverse String (#344), Best Time to Buy and Sell Stock (#121), Valid Palindrome (#125).

7. Hashing

In plain words

Hashing turns a key (like a name) into a box number. So you can store and find things almost instantly. In Java you use HashMap (key → value) and HashSet (only keys).

A coat check at a theatre. You give your coat and get ticket 42. Later you show ticket 42, and they go straight to hook 42. Nobody searches through every coat.

Key points

  • Add, find and remove are O(1) on average.
  • Use a HashSet for "have I seen this before?"
  • Use a HashMap for counting: "how many times does each word appear?"
  • Use a HashMap to remember positions: "where did I see this number?"
  • Hashing trades memory for speed: O(n) extra space to save time.
  • A HashMap has no order. Use TreeMap if you need sorted keys (topic 14).

Dry run: Two Sum, nums = [2, 7, 11], target = 9

inums[i]need = 9 - nums[i]need in map?map after
027No{2: 0}
172Yes, at index 0return [0, 1]
static int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> seen = new HashMap<>();   // number -> its index
    for (int i = 0; i < nums.length; i++) {
        int need = target - nums[i];                   // the partner we want
        if (seen.containsKey(need)) {                  // seen the partner before?
            return new int[] { seen.get(need), i };    // yes: done
        }
        seen.put(nums[i], i);                          // no: remember this number
    }
    return new int[] { -1, -1 };                       // no answer
}

// Count how often each character appears
static Map<Character, Integer> countChars(String s) {
    Map<Character, Integer> count = new HashMap<>();
    for (char c : s.toCharArray()) {
        count.put(c, count.getOrDefault(c, 0) + 1);   // old count + 1
    }
    return count;
}

Time & space: O(n) time — one pass, each lookup is O(1). O(n) space for the map.

Remember: when you see "find a pair", "count", or "seen before", think HashMap or HashSet.
Practice: Two Sum (#1), Valid Anagram (#242), Group Anagrams (#49).

8. Linked Lists

In plain words

A linked list is a chain of nodes. Each node holds a value and a pointer (arrow) to the next node. The first node is the head. The last node points to null (nothing).

A treasure hunt. Each clue tells you where the next clue is. To reach clue 5, you must follow clues 1, 2, 3 and 4 first.

head 3 next 1 next 2 next null value | pointer to the next node To reach the 3rd node you must walk from the head: O(n)
Nodes can live anywhere in memory. Only the arrows keep them in order.

Key points

  • No index. To find the 5th item, walk from the head. That is O(n).
  • Adding or removing at the head is O(1) — just change one arrow.
  • Always check for null before you use node.next.
  • A dummy node (a fake first node) makes many problems easier. You return dummy.next.
  • Doubly linked list: each node also points back to the previous node.

Dry run: count the nodes in 3 → 1 → 2

Stepcur points tocount
start30
111
222
3null → stop3
class ListNode {
    int val;           // the value stored here
    ListNode next;     // arrow to the next node (or null)
    ListNode(int val) { this.val = val; }
}

static int length(ListNode head) {
    int count = 0;
    ListNode cur = head;          // start at the head
    while (cur != null) {         // until we fall off the end
        count++;
        cur = cur.next;           // move to the next node
    }
    return count;
}

static ListNode addToFront(ListNode head, int value) {
    ListNode node = new ListNode(value);   // make a new node
    node.next = head;                      // point it at the old head
    return node;                           // the new node is the new head
}

Time & space: length is O(n) — we visit each node once. addToFront is O(1). Both use O(1) extra space.

Remember: walk with cur = cur.next, and always check for null. Draw the arrows on paper.
Practice: Middle of the Linked List (#876), Merge Two Sorted Lists (#21), Reverse Linked List (#206).

9. Stacks

In plain words

A stack is a pile. You add to the top (push) and remove from the top (pop). The last thing in is the first thing out. This is called LIFO (Last In, First Out).

A stack of plates. You put a clean plate on top. You also take a plate from the top. You never pull one from the bottom.

1 (bottom) 2 3 (top) push(4) adds on top pop() → 3 removes the top Only the top is reachable. Last in, first out.
push, pop and peek all work on the top only, so they are all O(1).

Key points

  • push(x) adds on top. pop() removes the top. peek() looks at the top without removing it.
  • All three are O(1).
  • In Java use Deque<Integer> stack = new ArrayDeque<>();
  • Use a stack for matching brackets, "undo" features, and going back to the most recent thing.
  • Recursion uses a stack inside (the call stack, topic 3).

Dry run: Valid Parentheses on "([])"

CharActionStack after (top on the right)
(opening → push(
[opening → push( [
]closing → top is [, match → pop(
)closing → top is (, match → popempty
endstack is empty → valid
static boolean isValid(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : s.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);                        // opening: save it
        } else {
            if (stack.isEmpty()) return false;    // closing with nothing open
            char top = stack.pop();               // the latest opening bracket
            if (c == ')' && top != '(') return false;
            if (c == ']' && top != '[') return false;
            if (c == '}' && top != '{') return false;
        }
    }
    return stack.isEmpty();                       // nothing left open = valid
}

Time & space: O(n) time — each character is pushed and popped at most once. O(n) space for the stack.

Remember: stack = plates = last in, first out. Think "stack" when the newest item must be handled first.
Practice: Valid Parentheses (#20), Min Stack (#155), Baseball Game (#682).

10. Queues & Deques

In plain words

A queue is a line. You join at the back (offer) and leave from the front (poll). The first in is the first out: FIFO. A deque ("deck") is a line where you can add or remove at both ends.

The queue at a ticket counter. The person who came first gets served first. New people join at the back.

Key points

  • offer(x) adds at the back. poll() removes from the front. peek() looks at the front.
  • All are O(1) with ArrayDeque.
  • poll() returns null when the queue is empty — check isEmpty() first.
  • Queues are the heart of BFS (breadth-first search, topics 16 and 38): visit things level by level.
  • A deque has offerFirst, offerLast, pollFirst, pollLast.

Dry run: a queue step by step

OperationReturnsQueue after (front on the left)
offer(5)—[5]
offer(8)—[5, 8]
offer(2)—[5, 8, 2]
poll()5[8, 2]
peek()8[8, 2]
// Number of Recent Calls: count calls in the last 3000 ms
class RecentCounter {
    Queue<Integer> times = new ArrayDeque<>();   // call times, oldest at the front

    int ping(int t) {
        times.offer(t);                              // new call joins at the back
        while (times.peek() < t - 3000) {            // oldest call is too old?
            times.poll();                            // remove it from the front
        }
        return times.size();                         // calls left are all recent
    }
}

Time & space: each time is added once and removed once, so O(1) on average per call. O(n) space for the queue.

Remember: queue = ticket line = first in, first out. Stack for "newest first", queue for "oldest first".
Practice: Number of Recent Calls (#933), Implement Queue using Stacks (#232).

11. Heaps & Priority Queues

In plain words

A heap always keeps the smallest (or largest) item ready at the top. A min-heap gives the smallest first. A max-heap gives the largest first. In Java it is called PriorityQueue.

A hospital emergency room. Patients are not seen in arrival order. The most serious patient always goes first.

Key points

  • peek() (see the top) is O(1). offer and poll are O(log n).
  • Java's PriorityQueue is a min-heap by default.
  • Max-heap: new PriorityQueue<>(Collections.reverseOrder()).
  • Great for "the k largest", "the k smallest", "the next most important".
  • Trick: to find the k largest, keep a min-heap of size k. The top is the k-th largest.

Dry run: 2nd largest in [3, 2, 1, 5], k = 2

NumberHeap after offerSize > 2?Heap after
3[3]No[3]
2[2, 3]No[2, 3]
1[1, 2, 3]Yes → poll 1[2, 3]
5[2, 3, 5]Yes → poll 2[3, 5]
endtop of the heap = 3 = 2nd largest
static int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> heap = new PriorityQueue<>();  // min-heap
    for (int x : nums) {
        heap.offer(x);                // add the number
        if (heap.size() > k) {        // too many?
            heap.poll();              // remove the smallest
        }
    }
    return heap.peek();               // smallest of the k largest
}

Time & space: O(n log k) time — n numbers, each heap step costs log k. O(k) space.

Remember: "k largest" → min-heap of size k. "k smallest" → max-heap of size k.
Practice: Last Stone Weight (#1046), Kth Largest Element in a Stream (#703), Kth Largest Element in an Array (#215).
Part 3 — Trees & Graphs

12. Binary Trees

In plain words

A tree is made of nodes connected like branches. The top node is the root. In a binary tree, each node has at most two children: left and right. A node with no children is a leaf.

A family tree, upside down. The grandparent is at the top (root). Each person has up to two children below them.

1 2 3 4 5 root leaf leaf leaf BFS (level by level): 1, 2, 3, 4, 5 DFS preorder (root, left, right): 1, 2, 4, 5, 3 DFS inorder (left, root, right): 4, 2, 5, 1, 3
BFS goes across, one level at a time. DFS goes deep down one branch first, then comes back.

Key points

  • Each node has a value, a left child and a right child (either can be null).
  • Height / depth = the number of levels from the root to the deepest leaf.
  • Most tree problems use recursion: solve for the left side, solve for the right side, combine.
  • The base case is almost always if (root == null).
  • DFS (depth-first) uses recursion. BFS (breadth-first) uses a queue. More in topic 38.

Dry run: maxDepth on the tree above

Nodeleft depthright depthreturns 1 + max
4, 5, 3 (leaves)001
21 (node 4)1 (node 5)2
1 (root)2 (node 2)1 (node 3)3
class TreeNode {
    int val;
    TreeNode left, right;          // children (can be null)
    TreeNode(int val) { this.val = val; }
}

static int maxDepth(TreeNode root) {
    if (root == null) return 0;            // empty tree has depth 0
    int left = maxDepth(root.left);        // depth of the left side
    int right = maxDepth(root.right);      // depth of the right side
    return 1 + Math.max(left, right);      // this node + the deeper side
}

static void preorder(TreeNode root, List<Integer> out) {
    if (root == null) return;
    out.add(root.val);                     // 1. visit the node
    preorder(root.left, out);              // 2. then the left side
    preorder(root.right, out);             // 3. then the right side
}

Time & space: O(n) time — each node is visited once. O(h) space for the call stack, where h is the tree height.

Remember: base case root == null, then trust the recursion on left and right.
Practice: Maximum Depth of Binary Tree (#104), Invert Binary Tree (#226), Same Tree (#100).

13. Binary Search Trees

In plain words

A BST (Binary Search Tree) is a binary tree with one rule: everything on the left is smaller than the node, and everything on the right is bigger. This rule makes searching fast.

A "guess the number" game. "Is it 8?" — "Smaller." Now you ignore everything above 8. Each answer throws away half the choices.

8 3 10 1 6 14 6 < 8: go left 6 > 3: go right found 6! left side is always smaller, right side is always bigger
Searching for 6 takes only 3 steps. Each step throws away one half of the tree.

Key points

  • Search, insert and delete take O(h) steps, where h is the height.
  • If the tree is balanced (bushy), h is about log n, so it is fast.
  • If you insert sorted numbers (1, 2, 3, 4…), the tree becomes a long line. Then h = n and it is slow.
  • Inorder traversal (left, root, right) of a BST gives the values in sorted order.
  • Validate a BST by checking each node is inside a (min, max) range, not just against its parent.

Dry run: search for 6

At nodeCompareGo
86 < 8left
36 > 3right
66 == 6found
static TreeNode search(TreeNode root, int target) {
    TreeNode cur = root;
    while (cur != null) {
        if (target == cur.val) return cur;     // found it
        if (target < cur.val) cur = cur.left;  // smaller: go left
        else cur = cur.right;                  // bigger: go right
    }
    return null;                               // not in the tree
}

static TreeNode insert(TreeNode root, int value) {
    if (root == null) return new TreeNode(value);   // empty spot: put it here
    if (value < root.val) root.left = insert(root.left, value);
    else root.right = insert(root.right, value);
    return root;
}

Time & space: O(h) time — one path from root down. O(log n) if balanced, O(n) in the worst case.

Remember: smaller → left, bigger → right. Inorder of a BST is sorted.
Practice: Search in a Binary Search Tree (#700), Insert into a Binary Search Tree (#701), Validate Binary Search Tree (#98).

14. Balanced Trees & TreeMap

In plain words

A balanced tree fixes itself after every insert, so it never becomes a long line. Its height stays about log n. Java's TreeMap and TreeSet are balanced trees (red-black trees) that are ready to use. Advanced — come back later for how the balancing works. For now, just learn to use TreeMap.

A dictionary. Words are always kept in order. You can quickly find a word, or "the word just before" or "the word just after" it.

Key points

  • You almost never write AVL or red-black trees in interviews. You use TreeMap / TreeSet.
  • Like HashMap, but the keys are kept sorted. Each operation is O(log n).
  • floorKey(x) = biggest key ≤ x. ceilingKey(x) = smallest key ≥ x.
  • firstKey() = smallest key. lastKey() = biggest key.
  • Use it when you need "the closest value" or sorted order while adding items.

Dry run: TreeSet with {10, 20, 30}

CallAnswerWhy
floor(25)20biggest value ≤ 25
ceiling(25)30smallest value ≥ 25
floor(5)nullnothing ≤ 5
first()10smallest value
static void treeMapDemo() {
    TreeMap<Integer, String> map = new TreeMap<>();
    map.put(30, "c");
    map.put(10, "a");
    map.put(20, "b");                     // keys are kept sorted: 10, 20, 30

    Integer below = map.floorKey(25);     // 20
    Integer above = map.ceilingKey(25);   // 30
    Integer none = map.floorKey(5);       // null: always check for null!
    int smallest = map.firstKey();        // 10

    System.out.println(below + " " + above + " " + none + " " + smallest);
}

Time & space: O(log n) for put, get, floor and ceiling. O(n) space.

Remember: need sorted keys or "nearest value"? Use TreeMap/TreeSet. Results like floorKey can be null.
Practice: Time Based Key-Value Store (#981), My Calendar I (#729).

15. Tries

In plain words

A trie (say "try") is a tree for words. Each step down the tree is one letter. Words that start the same way share the same path. It is great for prefix searches (a prefix is the start of a word, like "ca" in "cat").

The search box on your phone. You type "ca" and it suggests "cat", "car", "call". All of them share the start "c" → "a".

Key points

  • Each node has up to 26 children (one per letter a–z) and a flag isEnd ("a word ends here").
  • Insert and search take O(L), where L is the length of the word. The number of words does not matter.
  • startsWith(prefix) is very fast — that is the main reason to use a trie.
  • Used for autocomplete, spell check and word games.

Dry run: insert "cat" and "car"

WordPathNew nodes made
catroot → c → a → t (isEnd)c, a, t
carroot → c → a → r (isEnd)only r (c and a are shared)
startsWith("ca")root → c → a existstrue
search("ca")node a has isEnd = falsefalse
class Trie {
    Trie[] children = new Trie[26];   // one slot per letter a-z
    boolean isEnd = false;            // does a word end here?

    void insert(String word) {
        Trie node = this;
        for (char c : word.toCharArray()) {
            int i = c - 'a';                       // 'a' -> 0, 'b' -> 1 ...
            if (node.children[i] == null) {
                node.children[i] = new Trie();     // make the path if missing
            }
            node = node.children[i];               // step down
        }
        node.isEnd = true;                         // mark the end of the word
    }

    boolean search(String word) {
        Trie node = walk(word);
        return node != null && node.isEnd;         // path exists AND a word ends here
    }

    boolean startsWith(String prefix) {
        return walk(prefix) != null;               // path exists
    }

    Trie walk(String s) {                          // follow letters, null if missing
        Trie node = this;
        for (char c : s.toCharArray()) {
            node = node.children[c - 'a'];
            if (node == null) return null;
        }
        return node;
    }
}

Time & space: O(L) per operation — one step per letter. Space grows with the total letters stored.

Remember: "prefix" or "starts with" in the question → think trie.
Practice: Longest Common Prefix (#14), Implement Trie (#208).

16. Graphs & Traversal

In plain words

A graph is a set of nodes (also called vertices) joined by edges (lines). Unlike a tree, a graph can have loops (cycles) and no root. We usually store it as an adjacency list: for each node, a list of its neighbours.

A map of cities and roads. Cities are nodes. Roads are edges. "Can I drive from city A to city B?" is a graph question.

0 1 2 3 nodes + edges Adjacency list 0 → [1, 2] 1 → [0, 3] 2 → [0, 3] 3 → [1, 2] BFS from 0 visits: 0, 1, 2, 3
The same graph drawn as a picture and stored as an adjacency list. Each edge appears twice because it goes both ways.

Key points

  • Undirected edge = a two-way road. Directed edge = a one-way road. Weighted edge = a road with a length.
  • Store it as List<List<Integer>>: graph.get(u) = neighbours of u.
  • Always keep a visited set or array. Without it, cycles make you loop forever.
  • BFS (queue) explores in rings, nearest first. It finds the shortest path when all edges have the same cost.
  • DFS (recursion or stack) goes as deep as possible, then backs up.

Dry run: BFS from node 0

PollNew neighbours addedQueue afterVisited
start—[0]{0}
01, 2[1, 2]{0, 1, 2}
13 (0 already visited)[2, 3]{0, 1, 2, 3}
2none[3]same
3none[] → donesame
static List<Integer> bfs(List<List<Integer>> graph, int start) {
    List<Integer> order = new ArrayList<>();          // visit order
    boolean[] visited = new boolean[graph.size()];
    Queue<Integer> queue = new ArrayDeque<>();

    queue.offer(start);
    visited[start] = true;                              // mark when you add it
    while (!queue.isEmpty()) {
        int node = queue.poll();                        // take the oldest
        order.add(node);
        for (int next : graph.get(node)) {              // look at neighbours
            if (!visited[next]) {
                visited[next] = true;
                queue.offer(next);                      // visit it later
            }
        }
    }
    return order;
}

static void dfs(List<List<Integer>> graph, int node, boolean[] visited) {
    visited[node] = true;                               // mark this node
    for (int next : graph.get(node)) {
        if (!visited[next]) {
            dfs(graph, next, visited);                  // go deeper
        }
    }
}

Time & space: O(V + E) time — each node (V) and each edge (E) is looked at once. O(V) space for visited and the queue.

Remember: graph = adjacency list + visited. BFS for "shortest steps", DFS for "can I reach / explore everything".
Practice: Find if Path Exists in Graph (#1971), Flood Fill (#733), Number of Islands (#200).

17. Union-Find

In plain words

Union-Find (also called Disjoint Set) keeps track of groups. It answers two questions fast: "Are A and B in the same group?" (find) and "Join the groups of A and B" (union). Advanced — come back later if graphs still feel new.

Friend circles at school. Each circle has a leader. To check if two students are in the same circle, ask each one "who is your leader?". If the leaders are the same, they are in the same circle.

Key points

  • Each item has a parent. The item that is its own parent is the group's leader (root).
  • find(x) follows parents up to the leader.
  • union(a, b) makes one leader the parent of the other leader.
  • Path compression: while finding, point everyone straight to the leader. This makes it almost O(1).
  • Use it for: counting connected groups, and finding a cycle in an undirected graph.

Dry run: 4 items, union(0,1), union(1,2)

Stepparent array [0, 1, 2, 3]Groups
start[0, 1, 2, 3]{0} {1} {2} {3}
union(0, 1)[1, 1, 2, 3]{0, 1} {2} {3}
union(1, 2)[1, 2, 2, 3]{0, 1, 2} {3}
find(0) == find(2)?both lead to 2yes, same group
class UnionFind {
    int[] parent;

    UnionFind(int n) {
        parent = new int[n];
        for (int i = 0; i < n; i++) parent[i] = i;   // everyone leads their own group
    }

    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);   // path compression: jump to the leader
        }
        return parent[x];
    }

    boolean union(int a, int b) {
        int rootA = find(a), rootB = find(b);
        if (rootA == rootB) return false;  // already in the same group
        parent[rootA] = rootB;             // join the two groups
        return true;
    }
}

Time & space: almost O(1) per find/union with path compression. O(n) space for the parent array.

Remember: "groups", "connected", "same set" → Union-Find. If union returns false, that edge makes a cycle.
Practice: Find if Path Exists in Graph (#1971), Number of Provinces (#547), Redundant Connection (#684).

18. Segment & Fenwick Trees

In plain words

Advanced — come back later. These trees answer "what is the sum (or min, or max) of items from index L to R?" fast, even when the items keep changing. If the array never changes, a simple prefix sum (topic 32) is enough.

A school with class totals and a school total. If one student's mark changes, you update their class total and the school total. You don't add up every student again.

Key points

  • Array never changes → use prefix sums: O(1) per query.
  • Array changes often → use a Fenwick tree (Binary Indexed Tree) or a segment tree.
  • Both do update and range-query in O(log n).
  • Fenwick tree: shorter code, good for sums. Segment tree: more code, but works for min, max and more.
  • These are rare in beginner interviews. Learn them after you are comfortable with everything else.

The idea in one table: why not a plain array?

MethodUpdate one itemSum of a range
Plain array (loop to add)O(1)O(n)
Prefix sum arrayO(n) (rebuild)O(1)
Fenwick / segment treeO(log n)O(log n)
// Minimal Fenwick tree for sums. Indexes are 1-based inside.
class Fenwick {
    int[] tree;
    Fenwick(int n) { tree = new int[n + 1]; }

    void add(int i, int delta) {            // add delta at position i (1-based)
        for (; i < tree.length; i += i & (-i)) {
            tree[i] += delta;
        }
    }

    int prefixSum(int i) {                  // sum of positions 1..i
        int sum = 0;
        for (; i > 0; i -= i & (-i)) {
            sum += tree[i];
        }
        return sum;
    }

    int rangeSum(int left, int right) {     // sum of positions left..right
        return prefixSum(right) - prefixSum(left - 1);
    }
}

Time & space: O(log n) per update and per query. O(n) space.

Remember: no updates → prefix sum. Many updates + range sums → Fenwick or segment tree. Come back to this later.
Practice: Range Sum Query - Immutable (#303) first, then Range Sum Query - Mutable (#307).
Part 4 — Algorithms

19. Sorting

In plain words

Sorting means putting items in order, like smallest to largest. Many problems become easy after sorting. In real code you just call Arrays.sort(). But you should know how a simple sort works inside.

Sorting playing cards in your hand. You pick up one card at a time and slide it left until it is in the right place. That is insertion sort.

Key points

  • Simple sorts (bubble, selection, insertion) are O(n²). Fine for small arrays, slow for big ones.
  • Fast sorts (merge sort, quick sort) are O(n log n).
  • Arrays.sort(int[]) is O(n log n). Use it in interviews unless they ask you to write a sort.
  • A stable sort keeps equal items in their original order. Merge sort is stable.
  • Sort objects with a comparator: Arrays.sort(people, (a, b) -> a.age - b.age).

Dry run: insertion sort on [3, 1, 2]

iCard to placeWhat happensArray after
start——[3, 1, 2]
113 > 1, move 3 right; put 1 at index 0[1, 3, 2]
223 > 2, move 3 right; 1 < 2, stop; put 2 at index 1[1, 2, 3]
static void insertionSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int card = arr[i];                    // the item we want to place
        int j = i - 1;
        while (j >= 0 && arr[j] > card) {     // bigger items on the left?
            arr[j + 1] = arr[j];              // move them one step right
            j--;
        }
        arr[j + 1] = card;                    // drop the card in the gap
    }
}
SortTimeNote
Bubble / Selection / InsertionO(n²)easy to write, slow
Merge sortO(n log n)always fast, uses O(n) extra space (topic 23)
Quick sortO(n log n) averagefast in practice, O(n²) in the worst case
Counting sortO(n + k)only for small whole numbers (k = range of values)

Time & space: insertion sort is O(n²) time, O(1) extra space. Arrays.sort is O(n log n).

Remember: in interviews, sort with Arrays.sort and say "that's O(n log n)". Sorting first often unlocks two pointers or greedy.
Practice: Merge Sorted Array (#88), Sort Colors (#75), Sort an Array (#912).

20. Binary Search

In plain words

Binary search finds an item in a sorted array by looking at the middle. If the middle is too small, throw away the left half. If too big, throw away the right half. Repeat.

Finding a word in a paper dictionary. You open it in the middle. "M"? Your word starts with "D", so you only look in the first half. You never read every page.

step 1 1 3 5 7 9 11 13 7 < 9 → go right step 2 thrown away 9 11 13 11 > 9 → go left step 3 9 found at index 4! 7 items → only 3 checks. 1,000,000 items → only about 20 checks.
Each step throws away half of what is left. That is why binary search is O(log n).

Key points

  • The array must be sorted.
  • Keep two indexes: lo (left end) and hi (right end). Loop while lo <= hi.
  • Middle: mid = lo + (hi - lo) / 2. This avoids number overflow for huge indexes.
  • Too small → lo = mid + 1. Too big → hi = mid - 1.
  • Off-by-one mistakes are common. Test with 1 item and 2 items.

Dry run: find 9 in [1, 3, 5, 7, 9, 11, 13]

lohimidarr[mid]Action
06377 < 9 → lo = 4
4651111 > 9 → hi = 4
4449found → return 4
static int binarySearch(int[] arr, int target) {
    int lo = 0, hi = arr.length - 1;
    while (lo <= hi) {                     // still something to check
        int mid = lo + (hi - lo) / 2;      // the middle index
        if (arr[mid] == target) {
            return mid;                    // found it
        } else if (arr[mid] < target) {
            lo = mid + 1;                  // too small: search the right half
        } else {
            hi = mid - 1;                  // too big: search the left half
        }
    }
    return -1;                             // not found
}

Time & space: O(log n) time — the search area halves every step. O(1) space.

Remember: sorted input + "find" → binary search. Use lo <= hi and move to mid + 1 / mid - 1.
Practice: Binary Search (#704), Search Insert Position (#35), First Bad Version (#278).

21. Backtracking

In plain words

Backtracking tries every choice, one step at a time. If a path is done (or wrong), it goes back one step and tries the next choice. The pattern is always: choose → explore → un-choose.

Walking through a maze. At each fork you pick a path. If you hit a dead end, you walk back to the last fork and try another path.

Key points

  • It is recursion that builds an answer bit by bit in a list (often called path).
  • Choose: add an item to path. Explore: recurse. Un-choose: remove the item.
  • When saving an answer, save a copy: new ArrayList<>(path). Otherwise it changes later.
  • It is slow (often O(2ⁿ) or O(n!)), because it tries everything. That is OK — the questions expect it.
  • Used for: subsets, permutations, combinations, Sudoku, N-Queens, word search.

Dry run: all subsets of [1, 2]

CallpathSavedNext
start = 0[][]choose 1
start = 1[1][1]choose 2
start = 2[1, 2][1, 2]nothing left, go back, remove 2, then remove 1
start = 2[2][2]nothing left, go back
endanswers: [], [1], [1, 2], [2]
static List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), result);
    return result;
}

static void backtrack(int[] nums, int start, List<Integer> path,
                      List<List<Integer>> result) {
    result.add(new ArrayList<>(path));          // save a COPY of the current path
    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);                       // choose
        backtrack(nums, i + 1, path, result);    // explore
        path.remove(path.size() - 1);            // un-choose (go back)
    }
}

Time & space: O(2ⁿ × n) time — there are 2ⁿ subsets and copying each costs up to n. O(n) space for the recursion (plus the answers).

Remember: choose → explore → un-choose. Save copies, not the same list.
Practice: Subsets (#78), Letter Case Permutation (#784), Combination Sum (#39).

22. Greedy

In plain words

A greedy algorithm makes the best-looking choice right now and never changes its mind. Sometimes this gives the best total answer. Sometimes it does not — so you must check.

Giving change with the fewest coins: take the biggest coin that fits, again and again. With normal coins (25, 10, 5, 1) this works. With strange coins it can fail.

Key points

  • Greedy is usually sort + one loop. Fast and short.
  • It works only when a local best choice never hurts the final answer.
  • Test it with small examples. If you find a case where greedy fails, use DP (topic 24) instead.
  • Common greedy ideas: pick the earliest finish time, the smallest that fits, the biggest jump.

Dry run: Assign Cookies, children's greed g = [1, 2, 3], cookie sizes s = [1, 1]

child (need)cookie (size)Fits?happy children
11Yes → move both1
21No → try next cookie1
2none leftstop1
static int findContentChildren(int[] greed, int[] cookies) {
    Arrays.sort(greed);                  // least greedy child first
    Arrays.sort(cookies);                // smallest cookie first
    int child = 0, cookie = 0;
    while (child < greed.length && cookie < cookies.length) {
        if (cookies[cookie] >= greed[child]) {
            child++;                     // this child is happy, next child
        }
        cookie++;                        // this cookie is used or too small
    }
    return child;                        // number of happy children
}

Time & space: O(n log n) time because of sorting. O(1) extra space.

Remember: greedy = best choice now, no going back. Prove it with examples, or switch to DP.
Practice: Assign Cookies (#455), Best Time to Buy and Sell Stock II (#122), Jump Game (#55).

23. Divide & Conquer

In plain words

Divide and conquer has three steps. Divide the problem into smaller parts. Conquer (solve) each part, usually with recursion. Combine the answers. Merge sort is the classic example.

Sorting a big pile of exam papers with a friend. You each sort half. Then you merge your two sorted piles by always taking the smaller top paper.

Key points

  • Merge sort: split the array in half, sort each half, then merge the two sorted halves.
  • Merging two sorted lists is easy: compare the fronts, take the smaller one.
  • Splitting in half gives log n levels. Each level does n work. Total: O(n log n).
  • Binary search and quick sort are also divide and conquer.

Dry run: merge sort on [3, 1, 2]

StepWhat happensResult
Divide[3, 1, 2] → [3] and [1, 2]
Divide[1, 2] → [1] and [2]
Merge[1] + [2][1, 2]
Merge[3] + [1, 2]: take 1, take 2, take 3[1, 2, 3]
static void mergeSort(int[] arr, int lo, int hi) {   // sorts arr[lo..hi]
    if (lo >= hi) return;                  // 0 or 1 item: already sorted
    int mid = lo + (hi - lo) / 2;
    mergeSort(arr, lo, mid);               // sort the left half
    mergeSort(arr, mid + 1, hi);           // sort the right half
    merge(arr, lo, mid, hi);               // combine them
}

static void merge(int[] arr, int lo, int mid, int hi) {
    int[] temp = new int[hi - lo + 1];
    int i = lo, j = mid + 1, k = 0;
    while (i <= mid && j <= hi) {          // both halves have items
        if (arr[i] <= arr[j]) temp[k++] = arr[i++];   // take the smaller
        else temp[k++] = arr[j++];
    }
    while (i <= mid) temp[k++] = arr[i++]; // copy what is left
    while (j <= hi) temp[k++] = arr[j++];
    for (k = 0; k < temp.length; k++) {
        arr[lo + k] = temp[k];             // put the merged items back
    }
}
// Call it like this: mergeSort(arr, 0, arr.length - 1);

Time & space: O(n log n) time — log n levels, n work each. O(n) extra space for the temp arrays.

Remember: divide, solve the halves, combine. Merge sort is the one to know by heart.
Practice: Merge Two Sorted Lists (#21), Sort an Array (#912), Maximum Subarray (#53).

24. Dynamic Programming

In plain words

Dynamic Programming (DP) means: solve small problems first, save their answers, and build bigger answers from them. You never solve the same small problem twice.

Someone asks "what is 1+1+1+1+1?" You count: 5. Then they add "+1". You don't count again from the start. You remember 5 and say 6. That memory is DP.

ways[i] step i 1 1 2 3 5 8 0 1 2 3 4 5 ways[4] = ways[3] + ways[2] = 3 + 2 = 5 Fill the table left to right. Each box uses two boxes we already filled. Base cases: ways[0] = 1, ways[1] = 1
Climbing Stairs: you can take 1 or 2 steps. To reach step i, you came from step i-1 or step i-2. So add their answers.

Key points

  • DP works when the big problem is made of smaller, repeating problems.
  • State: what does dp[i] mean? Write it in words first. Example: "ways to reach step i".
  • Recurrence (the rule): how dp[i] uses smaller answers. Example: dp[i] = dp[i-1] + dp[i-2].
  • Base case: the smallest answers you know without work.
  • Top-down = recursion + a memo (saved answers). Bottom-up = fill a table with a loop.

Dry run: Climbing Stairs, n = 4

idp[i-1]dp[i-2]dp[i]
0, 1base cases1, 1
2112
3213
4325
static int climbStairs(int n) {
    int[] dp = new int[n + 1];           // dp[i] = ways to reach step i
    dp[0] = 1;                           // base: one way to stand at the start
    dp[1] = 1;                           // base: one way to reach step 1
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];   // came from 1 step below or 2 steps below
    }
    return dp[n];
}

// Same idea, top-down with a memo (saved answers)
static int fib(int n, int[] memo) {      // call with memo = new int[n + 1]
    if (n <= 1) return n;                // base cases
    if (memo[n] != 0) return memo[n];    // already solved? reuse it
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
    return memo[n];
}

Time & space: O(n) time — each step is computed once. O(n) space for the table.

Remember: state in words → rule → base case → fill the table. DP is "recursion + memory".
Practice: Climbing Stairs (#70), Fibonacci Number (#509), Min Cost Climbing Stairs (#746).

25. DP Patterns

In plain words

Most DP problems follow a few shapes. Learn the shapes and new problems look familiar. The steps are always the same: define the state, write the rule, set the base cases.

Recipes in a cookbook. Once you know how to make one soup, you can make most soups. Only the ingredients change.

Key points

PatternState meansExample problem
1D linedp[i] = best answer using the first i itemsHouse Robber (#198)
Griddp[r][c] = answer to reach cell (r, c)Unique Paths (#62)
Knapsack (pick or skip)dp[x] = best answer for amount xCoin Change (#322)
Two stringsdp[i][j] = answer for first i chars of A and first j chars of BLongest Common Subsequence (#1143)
  • Grid: you can only come from the top or the left, so add (or take min of) those two cells.
  • Pick or skip: for each item, the answer is the better of "take it" and "leave it".
  • Draw the table on paper for a tiny input before you code.

Dry run: Unique Paths in a 3 × 3 grid (move only right or down)

col 0col 1col 2
row 0111
row 111 + 1 = 21 + 2 = 3
row 212 + 1 = 33 + 3 = 6
static int uniquePaths(int rows, int cols) {
    int[][] dp = new int[rows][cols];        // dp[r][c] = ways to reach (r, c)
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (r == 0 || c == 0) {
                dp[r][c] = 1;                // first row/column: only one way
            } else {
                dp[r][c] = dp[r - 1][c] + dp[r][c - 1];   // from top + from left
            }
        }
    }
    return dp[rows - 1][cols - 1];
}

static int rob(int[] houses) {               // House Robber: no two neighbours
    int skip = 0, take = 0;                  // best so far without / with last house
    for (int money : houses) {
        int newTake = skip + money;          // rob this one: last one must be skipped
        skip = Math.max(skip, take);         // skip this one: keep the best so far
        take = newTake;
    }
    return Math.max(skip, take);
}

Time & space: uniquePaths is O(rows × cols) time and space. rob is O(n) time, O(1) space.

Remember: name the pattern first (line, grid, pick-or-skip, two strings), then fill the table.
Practice: House Robber (#198), Unique Paths (#62), Coin Change (#322), Longest Common Subsequence (#1143).

26. Shortest Paths

In plain words

A shortest path is the cheapest way from one node to another. If every edge costs the same, plain BFS works. If edges have different costs (weights), use Dijkstra's algorithm. It always grows from the closest node not yet finished.

Google Maps. It does not count the number of roads. It adds up the minutes on each road and picks the fastest total route.

Key points

  • All edges equal → BFS, O(V + E).
  • Different weights, none negative → Dijkstra with a PriorityQueue, O(E log V).
  • Negative weights → Bellman-Ford (advanced — come back later).
  • Keep a dist array. Start with "infinity" everywhere, and 0 for the start.
  • When you find a shorter way to a node, update dist and put it in the heap.

Dry run: edges 0→1 (cost 4), 0→2 (cost 1), 2→1 (cost 2). Start at 0.

Poll from heapUpdatedist [0, 1, 2]
startdist[0] = 0[0, ∞, ∞]
node 0 (dist 0)1: 0+4 = 4, 2: 0+1 = 1[0, 4, 1]
node 2 (dist 1)1: 1+2 = 3 < 4, update[0, 3, 1]
node 1 (dist 3)no edges out[0, 3, 1]
node 1 (old dist 4)4 > 3, out of date, skipsame
// graph.get(u) holds edges as {neighbour, cost}
static int[] dijkstra(List<List<int[]>> graph, int start) {
    int n = graph.size();
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);             // "infinity" at first
    dist[start] = 0;

    // heap of {node, distance}, smallest distance first
    PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
    heap.offer(new int[] { start, 0 });

    while (!heap.isEmpty()) {
        int[] cur = heap.poll();
        int node = cur[0], d = cur[1];
        if (d > dist[node]) continue;                 // old, out-of-date entry: skip
        for (int[] edge : graph.get(node)) {
            int next = edge[0], cost = edge[1];
            if (d + cost < dist[next]) {              // found a shorter way?
                dist[next] = d + cost;
                heap.offer(new int[] { next, dist[next] });
            }
        }
    }
    return dist;
}

Time & space: O(E log V) time — each edge may push into the heap. O(V + E) space.

Remember: same cost → BFS. Different costs → Dijkstra (always expand the closest node first).
Practice: Shortest Path in Binary Matrix (#1091), Network Delay Time (#743).

27. Topo Sort, MST & More

In plain words

Two more famous graph tools. Topological sort puts tasks in an order where every task comes after the tasks it depends on (full details in topic 43). A Minimum Spanning Tree (MST) connects all nodes with the cheapest total set of edges. Advanced — come back later for MST.

MST: a town wants to connect every house with water pipes, using the least pipe in total. It does not need every road — just enough to reach every house.

Key points

ToolWhat it doesWhen
Topological sortorders tasks by dependencies"course prerequisites", "build order"
Kruskal (MST)sort edges by cost, add each if it joins two groups"connect everything for the least cost"
Prim (MST)grow from one node, always add the cheapest edge outsame as Kruskal
Cycle checkUnion-Find (undirected) or DFS colours (directed)"is there a loop?"
  • Kruskal = sorting + Union-Find (topic 17). If union says "already joined", skip that edge — it would make a loop.
  • An MST of n nodes always has exactly n - 1 edges.

Dry run: Kruskal on 3 nodes. Edges: (0-1, cost 1), (1-2, cost 2), (0-2, cost 3)

Edge (sorted)Different groups?Actiontotal
0-1, cost 1Yestake it1
1-2, cost 2Yestake it3
0-2, cost 3No (already joined)skip3
// edges are {from, to, cost}. Uses the UnionFind class from topic 17.
static int kruskal(int n, int[][] edges) {
    Arrays.sort(edges, Comparator.comparingInt(e -> e[2]));   // cheapest first
    UnionFind uf = new UnionFind(n);
    int total = 0;
    for (int[] e : edges) {
        if (uf.union(e[0], e[1])) {      // joins two different groups?
            total += e[2];               // yes: use this edge
        }                                // no: it would make a loop, skip
    }
    return total;
}

Time & space: O(E log E) time — sorting the edges is the slow part. O(V) space for Union-Find.

Remember: dependencies → topological sort. Cheapest way to connect everything → MST (Kruskal).
Practice: Course Schedule (#207), Min Cost to Connect All Points (#1584).

28. String Algorithms

In plain words

Advanced — come back later. Special string algorithms like KMP and rolling hash find a small word (the pattern) inside a long text faster than the simple way. First learn the simple way well. Most interviews only need the simple way, a HashMap, or two pointers.

Looking for a word in a book. The simple way: start at each letter and compare. KMP is smarter: when a match fails, it remembers how much already matched, so it doesn't start again from zero.

Key points

  • Simple search: try every start position, compare letter by letter. O(n × m) (n = text length, m = pattern length).
  • KMP: uses a helper table so it never goes backwards in the text. O(n + m).
  • Rolling hash (Rabin-Karp): turns each window of text into a number, compares numbers. O(n + m) on average.
  • In Java, text.indexOf(pattern) does the simple search for you.
  • Many "string" problems are really hashing (anagrams), two pointers (palindromes) or sliding window.

Dry run: find "ll" in "hello"

Start iCompareMatch?
0"he" vs "ll"No (h ≠ l)
1"el" vs "ll"No (e ≠ l)
2"ll" vs "ll"Yes → return 2
static int strStr(String text, String pattern) {
    int n = text.length(), m = pattern.length();
    for (int i = 0; i + m <= n; i++) {        // every possible start position
        int j = 0;
        while (j < m && text.charAt(i + j) == pattern.charAt(j)) {
            j++;                              // letters match, keep going
        }
        if (j == m) return i;                 // all m letters matched
    }
    return -1;                                // not found
}

Time & space: O(n × m) time in the worst case. O(1) space. (KMP gets this down to O(n + m).)

Remember: master the simple search first. KMP and rolling hash are for later.
Practice: Find the Index of the First Occurrence in a String (#28), Repeated Substring Pattern (#459).

29. Math & Number Theory

In plain words

Some problems need a little math: GCD (greatest common divisor — the biggest number that divides both), prime numbers (numbers divisible only by 1 and themselves), and modulo (%, the remainder after dividing). Gentle topic — learn the few tools below and move on.

Modulo is like a clock. 14 o'clock is 2 o'clock, because 14 % 12 = 2. After 12, the numbers wrap around.

Key points

  • a % b = remainder. n % 10 = last digit. n / 10 = remove the last digit.
  • GCD with Euclid's trick: gcd(a, b) = gcd(b, a % b), stop when b is 0.
  • To check if n is prime, test divisors only up to √n.
  • Sieve of Eratosthenes: find all primes up to n by crossing out multiples.
  • Big answers: problems often say "return answer % 1,000,000,007". Use long to avoid overflow.

Dry run: gcd(12, 8)

aba % b
1284
840
40b is 0 → answer 4
static int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b;       // remainder
        a = b;               // shift: (a, b) becomes (b, r)
        b = r;
    }
    return a;
}

static boolean isPrime(int n) {
    if (n < 2) return false;               // 0 and 1 are not prime
    for (int d = 2; d * d <= n; d++) {     // only up to the square root
        if (n % d == 0) return false;      // found a divisor
    }
    return true;
}

static int sumOfDigits(int n) {
    int sum = 0;
    while (n > 0) {
        sum += n % 10;                     // take the last digit
        n = n / 10;                        // remove the last digit
    }
    return sum;
}

Time & space: gcd is O(log n). isPrime is O(√n). sumOfDigits is O(number of digits). All O(1) space.

Remember: % 10 gets the last digit, / 10 drops it. Check primes up to √n.
Practice: Fizz Buzz (#412), Palindrome Number (#9), Count Primes (#204).
Part 5 — Patterns

30. Two Pointers

In plain words

Two pointers means using two indexes that move through the array. Often one starts at the left end and one at the right end, and they walk towards each other. This turns many O(n²) problems into O(n).

Two people searching a long shelf for a pair of books, one from each end. They walk towards each other, and each step they decide who should move.

1 2 4 6 8 9 sorted array, target = 12 L R L start R start L moved right twice, R moved left once 1 + 9 = 10 → too small, L++ 2 + 9 = 11 → too small, L++ 4 + 9 = 13 → too big, R-- 4 + 8 = 12 → found!
Because the array is sorted, moving L makes the sum bigger and moving R makes it smaller. Each step throws away one number for good.

Key points

  • Opposite ends (L and R move towards each other): pair sums in a sorted array, palindromes, reversing.
  • Same direction (slow and fast both move right): remove duplicates, move zeroes.
  • Often the array must be sorted first.
  • Loop while left < right.
  • Each pointer only moves forward, so the total work is O(n).

Dry run: is "racecar" a palindrome?

leftrightcharsSame?
06r, rYes
15a, aYes
24c, cYes
33left is not < right → stoppalindrome
static boolean isPalindrome(String s) {
    int left = 0, right = s.length() - 1;
    while (left < right) {
        if (s.charAt(left) != s.charAt(right)) return false;   // mismatch
        left++;                                   // move inwards
        right--;
    }
    return true;
}

// Pair with a given sum in a SORTED array (0-based positions)
static int[] pairSum(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    while (left < right) {
        int sum = arr[left] + arr[right];
        if (sum == target) return new int[] { left, right };
        if (sum < target) left++;                 // need bigger: move left up
        else right--;                             // need smaller: move right down
    }
    return new int[] { -1, -1 };
}

Time & space: O(n) time — the pointers together cross the array once. O(1) space.

Remember: sorted array + "pair" or "palindrome" → two pointers from both ends.
Practice: Valid Palindrome (#125), Move Zeroes (#283), Two Sum II - Input Array Is Sorted (#167, it wants 1-based positions).

31. Sliding Window

In plain words

A window is a block of neighbouring items in an array or string (a subarray or substring). Sliding window moves this block step by step. Instead of adding everything again, you add the new item on the right and remove the old item on the left.

Looking out of a train window. The view slides along. A new tree appears on one side, and an old one disappears on the other. You don't look at the whole route again.

2 1 5 1 3 2 window (size 3): 5 + 1 + 3 = 9 previous window: 1 + 5 + 1 = 7 new sum = old sum + new right item - old left item = 7 + 3 - 1 = 9
Each slide costs only two small steps (add one, remove one), so the whole pass is O(n) instead of O(n × k).

Key points

  • Fixed window: size k is given. Add the right item, remove the left item, update the answer.
  • Variable window: grow the right side; when the window breaks a rule, shrink from the left.
  • Clues in the question: "subarray", "substring", "contiguous" (next to each other), "longest", "of size k".
  • Often uses a HashMap or HashSet to track what is inside the window.
  • Both ends only move right, so it is O(n).

Dry run: biggest sum of 3 in a row, [2, 1, 5, 1, 3, 2]

Windowsumbest
[2, 1, 5]88
[1, 5, 1]8 + 1 - 2 = 78
[5, 1, 3]7 + 3 - 1 = 99
[1, 3, 2]9 + 2 - 5 = 69
// Fixed window: biggest sum of k items in a row
static int maxSumOfK(int[] arr, int k) {
    int sum = 0;
    for (int i = 0; i < k; i++) sum += arr[i];    // first window
    int best = sum;
    for (int right = k; right < arr.length; right++) {
        sum += arr[right];                         // add the new item
        sum -= arr[right - k];                     // remove the old item
        best = Math.max(best, sum);
    }
    return best;
}

// Variable window: longest substring with no repeated letter
static int lengthOfLongestSubstring(String s) {
    Set<Character> inWindow = new HashSet<>();
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        while (inWindow.contains(c)) {             // repeat! shrink from the left
            inWindow.remove(s.charAt(left));
            left++;
        }
        inWindow.add(c);                           // grow on the right
        best = Math.max(best, right - left + 1);   // window size
    }
    return best;
}

Time & space: O(n) time — each item enters and leaves the window once. O(1) for the fixed version, O(k) for the set.

Remember: "longest / shortest / best subarray or substring" → sliding window. Add on the right, remove on the left.
Practice: Maximum Average Subarray I (#643), Longest Substring Without Repeating Characters (#3), Minimum Size Subarray Sum (#209).

32. Prefix Sum & Difference

In plain words

A prefix sum array stores running totals: prefix[i] = sum of the first i items. Then the sum of any range is just one subtraction. A difference array does the opposite trick: it makes "add 5 to every item from L to R" fast.

Your bank statement shows the balance after each day. To know how much you spent from Tuesday to Friday, you don't add each purchase. You subtract: Monday's balance minus Friday's balance.

Key points

  • Build once in O(n): prefix[i + 1] = prefix[i] + arr[i], with prefix[0] = 0.
  • Sum from index L to R = prefix[R + 1] - prefix[L]. That is O(1).
  • The extra 0 at the start removes special cases for L = 0.
  • Prefix sum + HashMap solves "count subarrays with sum k".
  • Difference array: to add v to range [L, R], do diff[L] += v and diff[R + 1] -= v, then take running totals at the end.

Dry run: arr = [1, 2, 3, 4]

i01234
arr[i]1234—
prefix[i]013610

Sum of index 1 to 2 (2 + 3) = prefix[3] - prefix[1] = 6 - 1 = 5. Correct!

class RangeSum {
    int[] prefix;

    RangeSum(int[] arr) {
        prefix = new int[arr.length + 1];          // one extra slot for the 0
        for (int i = 0; i < arr.length; i++) {
            prefix[i + 1] = prefix[i] + arr[i];    // running total
        }
    }

    int sumRange(int left, int right) {            // sum of arr[left..right]
        return prefix[right + 1] - prefix[left];
    }
}

Time & space: O(n) to build, then O(1) for every range question. O(n) space.

Remember: many "sum of a range" questions → build a prefix sum once, answer each in O(1).
Practice: Running Sum of 1d Array (#1480), Range Sum Query - Immutable (#303), Subarray Sum Equals K (#560).

33. Fast & Slow Pointers

In plain words

Two pointers move through a linked list at different speeds. The slow one moves 1 step. The fast one moves 2 steps. If there is a loop (cycle), fast will catch up with slow. If there is no loop, fast reaches the end. When fast reaches the end, slow is in the middle.

Two runners on a track. If the track is a circle, the fast runner will lap the slow one and they meet. If it is a straight road, the fast runner just finishes first.

Key points

  • Find the middle of a list in one pass.
  • Detect a cycle (loop) with O(1) extra memory — no HashSet needed.
  • Always check fast != null && fast.next != null before moving fast 2 steps.
  • Also works on numbers, like Happy Number (the "list" is the next value each time).

Dry run: middle of 1 → 2 → 3 → 4 → 5

Stepslowfast
start11
123
235
stop3 = middle5 has no next → stop
static ListNode middleNode(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;              // 1 step
        fast = fast.next.next;         // 2 steps
    }
    return slow;                       // slow is in the middle
}

static boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true; // they met: there is a loop
    }
    return false;                      // fast reached the end: no loop
}

Time & space: O(n) time, O(1) space — just two pointers.

Remember: slow 1 step, fast 2 steps. They meet → cycle. Fast ends → slow is the middle.
Practice: Middle of the Linked List (#876), Linked List Cycle (#141), Happy Number (#202).

34. Monotonic Stack & Queue

In plain words

A monotonic stack is a stack that is always kept in order (for example, always getting smaller from bottom to top). Before pushing a new item, you pop every item that breaks the order. It answers "what is the next bigger (or smaller) item?" for every position in O(n).

People standing in a line, looking right. A tall person arriving blocks the view of all shorter people before them. Those shorter people have found their "next taller person" — so they leave the line.

Key points

  • Clue: "next greater", "next smaller", "how many days until warmer".
  • Store indexes in the stack, not values. Then you can find both the value and the distance.
  • When the new item is bigger than the top, the top has found its "next greater". Pop it and record the answer.
  • Each item is pushed once and popped once, so it is O(n).
  • A monotonic queue (deque) does the same for a sliding window maximum. (Advanced — come back later.)

Dry run: next greater for [2, 1, 3]

ivalueActionstack (indexes)answer
02push 0[0][-1, -1, -1]
111 is not bigger than 2 → push 1[0, 1][-1, -1, -1]
233 > 1 → ans[1] = 3, pop. 3 > 2 → ans[0] = 3, pop. Push 2.[2][3, 3, -1]
static int[] nextGreater(int[] nums) {
    int[] answer = new int[nums.length];
    Arrays.fill(answer, -1);                       // -1 means "none found"
    Deque<Integer> stack = new ArrayDeque<>();     // holds indexes
    for (int i = 0; i < nums.length; i++) {
        while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
            int j = stack.pop();                   // nums[i] is j's next greater
            answer[j] = nums[i];
        }
        stack.push(i);                             // wait for my next greater
    }
    return answer;
}

Time & space: O(n) time — each index is pushed and popped at most once. O(n) space.

Remember: "next greater / next smaller" → monotonic stack of indexes.
Practice: Next Greater Element I (#496), Daily Temperatures (#739), Sliding Window Maximum (#239, hard).

35. Merge Intervals

In plain words

An interval is a range with a start and an end, like [1, 3]. Two intervals overlap if one starts before the other ends. To merge them, first sort by start, then walk through and join the ones that overlap.

Your calendar has meetings 1–3 pm and 2–6 pm. They overlap, so you are busy from 1 to 6 pm. That is merging.

Key points

  • Step 1: sort by start time. This is almost always the first step.
  • Overlap check (after sorting): next.start <= current.end.
  • Merge: current.end = Math.max(current.end, next.end).
  • No overlap → save the current interval and start a new one.
  • Used for meeting rooms, calendars, booking systems.

Dry run: [[1, 3], [2, 6], [8, 10]] (already sorted)

Next intervalOverlaps last merged?Merged list
[1, 3]first one[[1, 3]]
[2, 6]2 ≤ 3 → yes, end = max(3, 6) = 6[[1, 6]]
[8, 10]8 ≤ 6? no → add new[[1, 6], [8, 10]]
static int[][] merge(int[][] intervals) {
    Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));   // sort by start
    List<int[]> merged = new ArrayList<>();
    for (int[] cur : intervals) {
        if (merged.isEmpty() || cur[0] > merged.get(merged.size() - 1)[1]) {
            merged.add(cur);                              // no overlap: new interval
        } else {
            int[] last = merged.get(merged.size() - 1);   // overlap: stretch the end
            last[1] = Math.max(last[1], cur[1]);
        }
    }
    return merged.toArray(new int[0][]);
}

Time & space: O(n log n) time for sorting, then one pass. O(n) space for the answer.

Remember: intervals → sort by start first, then compare each one with the last merged interval.
Practice: Merge Intervals (#56), Insert Interval (#57), Non-overlapping Intervals (#435).

36. Cyclic Sort

In plain words

When an array holds numbers from 1 to n (or 0 to n), each number has a "home": number x belongs at index x - 1. Cyclic sort keeps swapping each number into its home. Then a missing or duplicate number is easy to spot.

Students with seat numbers walking into a classroom. Each student goes straight to their own numbered seat. An empty seat at the end shows who is absent.

Key points

  • Clue: "numbers in the range 1 to n", "find the missing / duplicate number".
  • If nums[i] is not at its home, swap it there. Do not move i yet — check the new number too.
  • If the number is already home (or its home already has the same number), move on: i++.
  • After sorting, any index i where nums[i] != i + 1 shows a problem number.
  • O(n) time and O(1) extra space — better than sorting or a HashSet.

Dry run: [3, 1, 2]

inums[i]Its home indexActionArray after
032swap with index 2[2, 1, 3]
021swap with index 1[1, 2, 3]
010already home → i++[1, 2, 3]
1, 2already home → done: [1, 2, 3]
static void cyclicSort(int[] nums) {          // numbers are 1..n
    int i = 0;
    while (i < nums.length) {
        int home = nums[i] - 1;               // where this number belongs
        if (nums[i] != nums[home]) {          // not home yet (and no duplicate there)
            int temp = nums[i];               // swap it home
            nums[i] = nums[home];
            nums[home] = temp;
        } else {
            i++;                              // fine: move to the next index
        }
    }
}

// Missing Number: numbers 0..n with one missing (simple math version)
static int missingNumber(int[] nums) {
    int n = nums.length;
    int expected = n * (n + 1) / 2;           // sum of 0..n
    int actual = 0;
    for (int x : nums) actual += x;
    return expected - actual;                 // the difference is the missing one
}

Time & space: O(n) time — every swap puts one number home for good. O(1) space.

Remember: "numbers from 1 to n" → put each number at index number - 1.
Practice: Missing Number (#268), Find All Numbers Disappeared in an Array (#448), Find the Duplicate Number (#287).

37. In-place List Reversal

In plain words

To reverse a linked list, you turn every arrow around so it points backwards. "In place" means you don't make a new list — you only change the existing arrows. You need three pointers: prev, cur and next.

A line of people, each with a hand on the shoulder of the person in front. To reverse the line, each person in turn takes their hand off and puts it on the person behind them.

Key points

  • Save next = cur.next before you change the arrow, or you lose the rest of the list.
  • Turn the arrow: cur.next = prev.
  • Move both forward: prev = cur, cur = next.
  • At the end, prev is the new head.
  • Draw it on paper. Three boxes and arrows make it clear.

Dry run: reverse 1 → 2 → 3

StepprevcurArrows so far
startnull11 → 2 → 3
112null ← 1   2 → 3
223null ← 1 ← 2   3
33nullnull ← 1 ← 2 ← 3   (new head = 3)
static ListNode reverseList(ListNode head) {
    ListNode prev = null;              // the reversed part (empty at first)
    ListNode cur = head;               // the node we are working on
    while (cur != null) {
        ListNode next = cur.next;      // 1. remember the rest of the list
        cur.next = prev;               // 2. turn the arrow backwards
        prev = cur;                    // 3. move prev forward
        cur = next;                    // 4. move cur forward
    }
    return prev;                       // prev is the new head
}

Time & space: O(n) time — each node once. O(1) space — only three pointers.

Remember: save next, turn the arrow, move prev, move cur. Return prev.
Practice: Reverse Linked List (#206), Palindrome Linked List (#234), Reverse Linked List II (#92).

38. Tree BFS & DFS

In plain words

There are two ways to visit every node of a tree. BFS (breadth-first) visits level by level, using a queue. DFS (depth-first) goes deep down one branch first, using recursion. Look again at the tree picture in topic 12.

BFS is like reading a company chart floor by floor: the CEO, then all managers, then all staff. DFS is like following one chain of command all the way down before trying the next.

Key points

  • BFS: put the root in a queue. Each round, take out all nodes of the current level and add their children.
  • Use BFS for: level order, "minimum depth", "rightmost node of each level".
  • DFS: preorder (node, left, right), inorder (left, node, right), postorder (left, right, node).
  • Use DFS for: height, path sums, checking a property of every subtree.
  • Trick for BFS levels: save size = queue.size() at the start of each level, then loop exactly that many times.

Dry run: level order of the tree 1 (children 2, 3), 2 (children 4, 5)

RoundQueue at startsizeLevel savedQueue after
1[1]1[1][2, 3]
2[2, 3]2[2, 3][4, 5]
3[4, 5]2[4, 5][] → done
static List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    Queue<TreeNode> queue = new ArrayDeque<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        int size = queue.size();                 // nodes in this level
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            level.add(node.val);
            if (node.left != null) queue.offer(node.left);    // next level
            if (node.right != null) queue.offer(node.right);
        }
        result.add(level);
    }
    return result;
}

// DFS: is there a root-to-leaf path that adds up to target?
static boolean hasPathSum(TreeNode root, int target) {
    if (root == null) return false;
    if (root.left == null && root.right == null) {   // leaf
        return root.val == target;
    }
    int rest = target - root.val;                     // what is still needed
    return hasPathSum(root.left, rest) || hasPathSum(root.right, rest);
}

Time & space: O(n) time for both — each node once. BFS uses O(width) space, DFS uses O(height) space.

Remember: "level" in the question → BFS with a queue. "path" or "depth" → DFS with recursion.
Practice: Binary Tree Level Order Traversal (#102), Average of Levels in Binary Tree (#637), Path Sum (#112).

39. Top K & K-way Merge

In plain words

Top K means "find the k biggest / smallest / most frequent items". Use a heap of size k. K-way merge means "merge k sorted lists into one sorted list". Put the first item of each list in a min-heap, and keep taking the smallest.

Top K: a talent show keeps only the best 3 acts on stage. When a better act arrives, the weakest one on stage leaves. K-way merge: k queues at a bank, and the next served is always the person with the earliest ticket among all queue fronts.

Key points

  • k largest → min-heap of size k (the smallest of the best k sits on top, easy to kick out).
  • k most frequent → count with a HashMap first, then use the heap on the counts.
  • K-way merge → heap holds one item from each list. Poll the smallest, then push the next item from that same list.
  • Time is O(n log k), which beats sorting everything (O(n log n)) when k is small.

Dry run: top 2 frequent in [1, 1, 1, 2, 2, 3]

StepResult
Count{1: 3, 2: 2, 3: 1}
Add 1 (count 3)heap: [1]
Add 2 (count 2)heap: [2, 1] (lowest count on top)
Add 3 (count 1)size 3 > 2 → poll lowest count (3) → heap: [2, 1]
Answer[1, 2] (any order)
static int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> count = new HashMap<>();
    for (int x : nums) count.put(x, count.getOrDefault(x, 0) + 1);   // 1. count

    // 2. min-heap ordered by count: the least frequent is on top
    PriorityQueue<Integer> heap =
        new PriorityQueue<>(Comparator.comparingInt(n -> count.get(n)));
    for (int num : count.keySet()) {
        heap.offer(num);
        if (heap.size() > k) heap.poll();        // drop the least frequent
    }

    int[] result = new int[k];                   // 3. the heap holds the answer
    for (int i = 0; i < k; i++) result[i] = heap.poll();
    return result;
}

Time & space: O(n log k) time — counting is O(n), each heap step is O(log k). O(n) space for the counts.

Remember: "top k", "k closest", "k most frequent" → heap of size k. "Merge k sorted" → min-heap of list fronts.
Practice: Kth Largest Element in a Stream (#703), Top K Frequent Elements (#347), K Closest Points to Origin (#973), Merge k Sorted Lists (#23, hard).

40. Two Heaps

In plain words

Use two heaps to split numbers into a smaller half and a bigger half. A max-heap holds the smaller half (its top is the biggest of the small ones). A min-heap holds the bigger half (its top is the smallest of the big ones). The median (middle value) is always at the tops.

A class lines up by height and splits into two groups at the middle. You only need to look at the tallest of the short group and the shortest of the tall group to find the middle height.

Key points

  • low = max-heap (smaller half). high = min-heap (bigger half).
  • Keep sizes balanced: low has the same number of items as high, or one more.
  • Odd count → median = top of low. Even count → average of both tops.
  • Clue: "median of a stream" (numbers keep arriving), "sliding window median".

Dry run: add 5, then 2, then 8

Addlow (max-heap)high (min-heap)Median
5[5][]5
2[2][5](2 + 5) / 2 = 3.5
8[5, 2][8]5
class MedianFinder {
    PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder()); // max-heap
    PriorityQueue<Integer> high = new PriorityQueue<>();                         // min-heap

    void addNum(int num) {
        low.offer(num);                       // 1. add to the small half
        high.offer(low.poll());               // 2. move the biggest small one up
        if (high.size() > low.size()) {       // 3. keep low the same size or bigger
            low.offer(high.poll());
        }
    }

    double findMedian() {
        if (low.size() > high.size()) return low.peek();     // odd count
        return (low.peek() + high.peek()) / 2.0;              // even count
    }
}

Time & space: O(log n) per add, O(1) for the median. O(n) space.

Remember: median of changing data → max-heap for the small half, min-heap for the big half.
Practice: Find Median from Data Stream (#295, hard but it is the classic one).

41. Subsets & Permutations

In plain words

These are backtracking problems (topic 21). A subset is any group of the items (order does not matter). A permutation is an ordering of all the items (order matters). A combination is a group of exactly k items.

Pizza toppings. Subsets: every possible set of toppings you could choose. Permutations: every order you could put 3 toppings on the pizza, one after another.

Key points

TypeFor [1, 2, 3]How manyTrick
Subsets[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]2ⁿ = 8loop from start (topic 21)
Permutations[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]n! = 6used[] array, loop from 0
Combinations (k = 2)[1,2], [1,3], [2,3]C(n, k) = 3subsets, but save only when size = k
  • Permutations: every position can use any item that is not used yet.
  • Save the answer when path.size() == nums.length.
  • With duplicates in the input: sort first, then skip equal neighbours.

Dry run: permutations of [1, 2] (first steps)

pathusedAction
[][F, F]choose 1
[1][T, F]choose 2
[1, 2][T, T]full → save. Go back, un-choose 2, un-choose 1
[2][F, T]choose 1 → [2, 1] full → save
answers: [1, 2], [2, 1]
static List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    build(nums, new boolean[nums.length], new ArrayList<>(), result);
    return result;
}

static void build(int[] nums, boolean[] used, List<Integer> path,
                  List<List<Integer>> result) {
    if (path.size() == nums.length) {            // all items placed
        result.add(new ArrayList<>(path));       // save a copy
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;                   // already in the path
        used[i] = true;                          // choose
        path.add(nums[i]);
        build(nums, used, path, result);         // explore
        path.remove(path.size() - 1);            // un-choose
        used[i] = false;
    }
}

Time & space: O(n! × n) time — n! orderings, each copied. O(n) space for the recursion.

Remember: subsets → loop from start. Permutations → loop from 0 with used[].
Practice: Subsets (#78), Permutations (#46), Combinations (#77).

42. Modified Binary Search

In plain words

Binary search (topic 20) can do more than "find x". You can change it to find the first or last position of x, search in a rotated sorted array, or even search for the answer itself ("what is the smallest speed that works?").

Looking for the first page of chapter 5 in a book. When you land on a chapter 5 page, you don't stop — you keep looking to the left to see if chapter 5 starts earlier.

Key points

  • First position: when you find x, save it, then keep searching left (hi = mid - 1).
  • Last position: same, but keep searching right.
  • Rotated array (like [4, 5, 6, 1, 2, 3]): one half is always sorted. Check if x is in that half.
  • Search on the answer: if "does value v work?" is no, no, no, yes, yes, yes, binary search for the first yes.
  • The question's clue is often "sorted" plus "O(log n)".

Dry run: first position of 2 in [1, 2, 2, 2, 3]

lohimidarr[mid]Actionanswer
0422found, keep looking left: hi = 12
0101too small: lo = 12
1112found, keep looking left: hi = 01
10lo > hi → stop1
static int firstPosition(int[] arr, int target) {
    int lo = 0, hi = arr.length - 1;
    int answer = -1;                      // -1 if never found
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        if (arr[mid] == target) {
            answer = mid;                 // found one, save it
            hi = mid - 1;                 // but maybe there is an earlier one
        } else if (arr[mid] < target) {
            lo = mid + 1;
        } else {
            hi = mid - 1;
        }
    }
    return answer;
}

Time & space: O(log n) time, O(1) space — still halving every step.

Remember: found it? Save the answer and keep searching in the direction you need.
Practice: Search Insert Position (#35), Find First and Last Position of Element in Sorted Array (#34), Search in Rotated Sorted Array (#33).

43. Topological Sort

In plain words

Topological sort puts tasks in an order where every task comes after the tasks it depends on. It works on a directed graph with no cycles. The in-degree of a node is how many arrows point into it — how many tasks must happen before it.

Getting dressed. Socks before shoes. Shirt before jacket. You can start with anything that has nothing before it, and the order follows from there.

Key points

  • Kahn's algorithm: start with all nodes that have in-degree 0 (nothing before them). Put them in a queue.
  • Take one out, add it to the order, and lower the in-degree of its neighbours by 1.
  • When a neighbour reaches in-degree 0, it is ready — add it to the queue.
  • If the final order has fewer than n nodes, there is a cycle (impossible to finish).
  • Clue: "prerequisites", "dependencies", "build order", "can all courses be finished?"

Dry run: 4 courses. 0 before 1, 0 before 2, 1 before 3, 2 before 3

Take outin-degree [0, 1, 2, 3] afterQueue afterOrder
start[0, 1, 1, 2][0][]
0[0, 0, 0, 2][1, 2][0]
1[0, 0, 0, 1][2][0, 1]
2[0, 0, 0, 0][3][0, 1, 2]
3—[][0, 1, 2, 3]
// prereqs[i] = {course, before}: "before" must be taken before "course"
static List<Integer> courseOrder(int n, int[][] prereqs) {
    List<List<Integer>> graph = new ArrayList<>();
    for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
    int[] inDegree = new int[n];
    for (int[] p : prereqs) {
        graph.get(p[1]).add(p[0]);        // arrow: before -> course
        inDegree[p[0]]++;                 // course has one more thing before it
    }

    Queue<Integer> queue = new ArrayDeque<>();
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 0) queue.offer(i);   // ready to start
    }

    List<Integer> order = new ArrayList<>();
    while (!queue.isEmpty()) {
        int course = queue.poll();
        order.add(course);
        for (int next : graph.get(course)) {
            inDegree[next]--;             // one less thing to wait for
            if (inDegree[next] == 0) queue.offer(next);
        }
    }
    return order.size() == n ? order : new ArrayList<>();   // empty = cycle
}

Time & space: O(V + E) time — each course and each arrow once. O(V + E) space for the graph.

Remember: start from in-degree 0, remove it, lower the neighbours. Fewer than n in the order → cycle.
Practice: Course Schedule (#207), Course Schedule II (#210).

44. Matrix & Islands

In plain words

A matrix (grid) is a 2D array: rows and columns. Many grid problems are graph problems in disguise. Each cell is a node, and its 4 neighbours (up, down, left, right) are its edges. An island is a group of connected land cells.

A paint bucket tool in a drawing app. You click one pixel, and the colour spreads to every touching pixel of the same colour. That spreading is DFS on a grid.

Key points

  • A cell is grid[r][c]: r = row, c = column.
  • Always check the borders first: r >= 0 && r < rows && c >= 0 && c < cols.
  • Mark visited cells, often by changing '1' to '0' (sinking the land).
  • Number of islands = how many times you start a new DFS on unvisited land.
  • Use BFS instead when the question asks for the shortest number of steps.

Dry run: count islands

GridWhat happens
1 1 0
0 1 0
0 0 1
(0,0) is land → island 1. DFS sinks (0,0), (0,1), (1,1).
(2,2) is land → island 2. DFS sinks it.
Answer: 2.
static int numIslands(char[][] grid) {
    int count = 0;
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[0].length; c++) {
            if (grid[r][c] == '1') {       // new land we have not visited
                count++;                   // found a new island
                sink(grid, r, c);          // mark the whole island as visited
            }
        }
    }
    return count;
}

static void sink(char[][] grid, int r, int c) {
    if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) return;  // outside
    if (grid[r][c] != '1') return;         // water or already visited
    grid[r][c] = '0';                      // mark as visited
    sink(grid, r + 1, c);                  // down
    sink(grid, r - 1, c);                  // up
    sink(grid, r, c + 1);                  // right
    sink(grid, r, c - 1);                  // left
}

Time & space: O(rows × cols) time — each cell is visited once. O(rows × cols) space for the recursion in the worst case.

Remember: grid = graph. Check borders, mark visited, explore 4 directions.
Practice: Flood Fill (#733), Number of Islands (#200), Max Area of Island (#695).

45. Design Problems

In plain words

In a design problem, you build a small class with a few methods, and each method must be fast. The trick is to combine data structures. Example: an LRU cache (Least Recently Used) keeps the most recently used items and throws out the oldest one when it is full.

A small desk that fits only 2 books. When you need a third book, you put away the book you haven't touched for the longest time.

Key points

  • First, list each method and the speed it needs (often O(1)).
  • Then pick structures: HashMap for fast lookup, a linked list or deque for order, a heap for min/max.
  • Min Stack: a normal stack plus a second stack that tracks the minimum so far.
  • LRU Cache: HashMap + doubly linked list. In Java, LinkedHashMap already combines them.
  • Interviewers may ask you to build the linked list yourself — see the full notes for that version.

Dry run: LRU cache with capacity 2

CallReturnsCache (oldest → newest)
put(1, 1)—[1]
put(2, 2)—[1, 2]
get(1)1[2, 1] (1 is now newest)
put(3, 3)—full → remove oldest (2) → [1, 3]
get(2)-1[1, 3] (2 was removed)
// LRU Cache using LinkedHashMap (keeps items in "last used" order)
class LRUCache extends LinkedHashMap<Integer, Integer> {
    int capacity;

    LRUCache(int capacity) {
        super(16, 0.75f, true);          // true = order by last access, not insert
        this.capacity = capacity;
    }

    int get(int key) {
        return getOrDefault(key, -1);    // reading also marks it "recently used"
    }

    void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;        // too big? Java removes the oldest
    }
}

// Min Stack: getMin() in O(1)
class MinStack {
    Deque<Integer> stack = new ArrayDeque<>();
    Deque<Integer> mins = new ArrayDeque<>();     // min so far, at each level

    void push(int x) {
        stack.push(x);
        mins.push(mins.isEmpty() ? x : Math.min(x, mins.peek()));
    }
    void pop()    { stack.pop(); mins.pop(); }
    int top()     { return stack.peek(); }
    int getMin()  { return mins.peek(); }
}

Time & space: every method is O(1). O(n) space for the stored items.

Remember: design = pick the right mix of structures so every method is fast. HashMap + ordered list is the most common mix.
Practice: Min Stack (#155), Design HashMap (#706), LRU Cache (#146).
Revision

Which Pattern When?

Read the question slowly and look for these clues. The clue usually tells you which tool to try first.

Clue in the questionTry this patternExample problem
"Find a pair", "seen before", "count how many times"HashMap / HashSet (7)Two Sum (#1)
Sorted array + "pair" / "palindrome"Two pointers (30)Valid Palindrome (#125)
"Subarray" / "substring", "longest", "of size k"Sliding window (31)Longest Substring Without Repeating Characters (#3)
"Sum of a range" asked many timesPrefix sum (32)Range Sum Query - Immutable (#303)
Sorted array + "find" + O(log n)Binary search (20, 42)Binary Search (#704)
Linked list "middle" or "cycle"Fast & slow pointers (33)Linked List Cycle (#141)
"Reverse" a linked listIn-place reversal (37)Reverse Linked List (#206)
Brackets, "undo", most recent firstStack (9)Valid Parentheses (#20)
"Next greater" / "next smaller"Monotonic stack (34)Daily Temperatures (#739)
"Top k", "k largest", "k most frequent"Heap of size k (11, 39)Kth Largest Element in an Array (#215)
"Median" of numbers that keep arrivingTwo heaps (40)Find Median from Data Stream (#295)
Intervals, meetings, overlapping rangesSort + merge intervals (35)Merge Intervals (#56)
"Numbers from 1 to n", missing / duplicateCyclic sort (36)Missing Number (#268)
Tree "level by level"BFS with a queue (38)Binary Tree Level Order Traversal (#102)
Tree "depth", "path", "every subtree"DFS recursion (12, 38)Maximum Depth of Binary Tree (#104)
Grid, "islands", "regions", flood fillDFS / BFS on a matrix (44)Number of Islands (#200)
"Shortest path", all steps cost the sameBFS (16)Shortest Path in Binary Matrix (#1091)
"Shortest path" with different costsDijkstra (26)Network Delay Time (#743)
"Prerequisites", "dependencies", "order of tasks"Topological sort (43)Course Schedule (#207)
"Connected groups", "same set"Union-Find (17)Number of Provinces (#547)
"All combinations / subsets / permutations"Backtracking (21, 41)Subsets (#78)
"How many ways", "min / max cost", choices repeatDynamic programming (24, 25)Climbing Stairs (#70)
"Prefix", "starts with", autocompleteTrie (15)Implement Trie (#208)
Best choice now seems always safeGreedy (22)Assign Cookies (#455)
Remember: if nothing fits, write the brute force first. Then ask: "What am I repeating?" The answer often points to a HashMap, sorting, or DP.

Big-O Cheat Sheet

Average times for the most common operations. "n" is the number of items.

StructureGet / findAddRemoveNote
Array int[]O(1) by index, O(n) by valueO(n) in the middleO(n) in the middlefixed size
ArrayListO(1) by indexO(1) at the endO(n) in the middlegrows by itself
Linked listO(n)O(1) at the headO(1) at the headno index
Stack / Queue (ArrayDeque)O(1) top / frontO(1)O(1)only the ends
HashMap / HashSetO(1)O(1)O(1)no order
TreeMap / TreeSetO(log n)O(log n)O(log n)sorted keys
Heap (PriorityQueue)O(1) peek minO(log n)O(log n) pollmin first
BST (balanced)O(log n)O(log n)O(log n)O(n) if it becomes a line
TrieO(L)O(L)O(L)L = word length
AlgorithmTimeExtra space
Linear search / one loopO(n)O(1)
Binary searchO(log n)O(1)
Simple sorts (bubble, insertion)O(n²)O(1)
Merge sort / Arrays.sortO(n log n)O(n) / O(log n)
Two pointers / sliding windowO(n)O(1) or O(k)
BFS / DFS on a graphO(V + E)O(V)
DijkstraO(E log V)O(V + E)
Subsets / permutationsO(2ⁿ) / O(n!)O(n)
1D DP / 2D DPO(n) / O(n × m)O(n) / O(n × m)

What input size allows what speed?

n up to about…You can afford
10–20O(2ⁿ) or O(n!) — backtracking is fine
1,000–5,000O(n²)
100,000–1,000,000O(n log n) or O(n)
biggerO(log n) or O(1)
Remember: look at the size limit of n in the problem. It quietly tells you how fast your answer must be.

Learning Path

Go in this order. Do the easy problems at each step before moving on. It is fine to look at a solution when stuck for 30 minutes — then try again the next day without looking.

  1. Big-O and Java basics (topics 2, 4) — loops, arrays, ArrayList, HashMap.
    Problems: Fizz Buzz (#412), Running Sum of 1d Array (#1480), Richest Customer Wealth (#1672).
  2. Arrays & strings (6) — loops, indexes, StringBuilder.
    Problems: Reverse String (#344), Best Time to Buy and Sell Stock (#121), Merge Sorted Array (#88).
  3. Hashing (7) — HashMap and HashSet.
    Problems: Two Sum (#1), Contains Duplicate (#217), Valid Anagram (#242).
  4. Two pointers (30) — from both ends.
    Problems: Valid Palindrome (#125), Move Zeroes (#283), Two Sum II (#167).
  5. Sliding window & prefix sum (31, 32).
    Problems: Maximum Average Subarray I (#643), Longest Substring Without Repeating Characters (#3), Range Sum Query - Immutable (#303).
  6. Stacks & queues (9, 10).
    Problems: Valid Parentheses (#20), Min Stack (#155), Implement Queue using Stacks (#232).
  7. Linked lists (8, 33, 37).
    Problems: Reverse Linked List (#206), Middle of the Linked List (#876), Linked List Cycle (#141).
  8. Recursion (3) — base case and call stack.
    Problems: Fibonacci Number (#509), Power of Two (#231), Merge Two Sorted Lists (#21).
  9. Binary search (20, 42).
    Problems: Binary Search (#704), Search Insert Position (#35), First Bad Version (#278).
  10. Binary trees (12, 13, 38) — DFS recursion and BFS levels.
    Problems: Maximum Depth of Binary Tree (#104), Invert Binary Tree (#226), Binary Tree Level Order Traversal (#102).
  11. Heaps (11, 39).
    Problems: Last Stone Weight (#1046), Kth Largest Element in a Stream (#703), Top K Frequent Elements (#347).
  12. Backtracking (21, 41).
    Problems: Subsets (#78), Permutations (#46), Combination Sum (#39).
  13. Graphs & grids (16, 44, 43).
    Problems: Flood Fill (#733), Number of Islands (#200), Course Schedule (#207).
  14. Dynamic programming (24, 25).
    Problems: Climbing Stairs (#70), House Robber (#198), Coin Change (#322).
  15. Then — intervals, greedy, tries, Union-Find, Dijkstra. Advanced topics (segment trees, KMP, MST) last. Now read the full notes and practise with the LeetCode cheat sheet and the interview Q&A.
Remember: a little every day beats a lot once a week. 2 problems a day for 3 months is enough to feel confident.