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.
1. How to Approach a Problem
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)
| Step | What you do |
|---|---|
| 1. Understand | Input [2, 7, 11, 15], target 9. Return the two positions (indexes). |
| 2. Ask | "Is there always exactly one answer?" — Yes. |
| 3. Example | 2 + 7 = 9, so the answer is [0, 1]. |
| 4. Brute force | Check every pair. Two loops. O(n²). |
| 5. Improve | For each number, remember it in a HashMap. Look up target - number. O(n). |
| 6. Test | Try [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.)
Practice: Two Sum (#1), Contains Duplicate (#217).
2. Big-O & Complexity
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)).
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?
| n | O(log n) | O(n) | O(n log n) | O(n²) |
|---|---|---|---|---|
| 10 | ~3 | 10 | ~33 | 100 |
| 1,000 | ~10 | 1,000 | ~10,000 | 1,000,000 |
| 1,000,000 | ~20 | 1,000,000 | ~20,000,000 | 1,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).
Practice: think about the Big-O of every problem you solve from now on.
3. Recursion & the Call Stack
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.
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)
| Call | Is it the base case? | What it returns |
|---|---|---|
| fact(3) | No | 3 * fact(2) = 3 * 2 = 6 |
| fact(2) | No | 2 * 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.
Practice: Fibonacci Number (#509), Reverse String (#344), Power of Two (#231).
4. Java Toolkit for DSA
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 oldStackclass.)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, notint:List<Integer>.
| I need… | Use | Main methods |
|---|---|---|
| A growing list | ArrayList | add, get, size |
| Fast lookup by key | HashMap | put, get, getOrDefault, containsKey |
| Unique items | HashSet | add, contains, remove |
| Stack (last in, first out) | ArrayDeque | push, pop, peek |
| Queue (first in, first out) | ArrayDeque | offer, poll, peek |
| Smallest item fast | PriorityQueue | offer, 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).
Practice: Valid Anagram (#242), Contains Duplicate (#217).
5. Bit Manipulation
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 = 0andx ^ 0 = x. - Shift:
x << 1doubles x.x >> 1halves x. (n & 1) == 1means n is odd.n & (n - 1)removes the lowest 1 bit.
Dry run: 5 & 3, 5 | 3, 5 ^ 3
| Bits | Number | |
|---|---|---|
| 5 | 101 | 5 |
| 3 | 011 | 3 |
| 5 & 3 | 001 | 1 |
| 5 | 3 | 111 | 7 |
| 5 ^ 3 | 110 | 6 |
// 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.
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).
6. Arrays & Strings
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.
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. UseArrayListif 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]
| i | arr[i] | Bigger than max? | max after this step |
|---|---|---|---|
| start | — | — | 3 (first item) |
| 1 | 1 | No | 3 |
| 2 | 4 | Yes | 4 |
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.
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
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
TreeMapif you need sorted keys (topic 14).
Dry run: Two Sum, nums = [2, 7, 11], target = 9
| i | nums[i] | need = 9 - nums[i] | need in map? | map after |
|---|---|---|---|---|
| 0 | 2 | 7 | No | {2: 0} |
| 1 | 7 | 2 | Yes, at index 0 | return [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.
Practice: Two Sum (#1), Valid Anagram (#242), Group Anagrams (#49).
8. Linked Lists
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.
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
nullbefore you usenode.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
| Step | cur points to | count |
|---|---|---|
| start | 3 | 0 |
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | null → stop | 3 |
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.
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
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.
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 "([])"
| Char | Action | Stack after (top on the right) |
|---|---|---|
( | opening → push | ( |
[ | opening → push | ( [ |
] | closing → top is [, match → pop | ( |
) | closing → top is (, match → pop | empty |
| end | stack 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.
Practice: Valid Parentheses (#20), Min Stack (#155), Baseball Game (#682).
10. Queues & Deques
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()returnsnullwhen the queue is empty — checkisEmpty()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
| Operation | Returns | Queue 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.
Practice: Number of Recent Calls (#933), Implement Queue using Stacks (#232).
11. Heaps & Priority Queues
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).offerandpollare O(log n).- Java's
PriorityQueueis 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
| Number | Heap after offer | Size > 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] |
| end | top 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.
Practice: Last Stone Weight (#1046), Kth Largest Element in a Stream (#703), Kth Largest Element in an Array (#215).
12. Binary Trees
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.
Key points
- Each node has a value, a
leftchild and arightchild (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
| Node | left depth | right depth | returns 1 + max |
|---|---|---|---|
| 4, 5, 3 (leaves) | 0 | 0 | 1 |
| 2 | 1 (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.
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
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.
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 node | Compare | Go |
|---|---|---|
| 8 | 6 < 8 | left |
| 3 | 6 > 3 | right |
| 6 | 6 == 6 | found |
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.
Practice: Search in a Binary Search Tree (#700), Insert into a Binary Search Tree (#701), Validate Binary Search Tree (#98).
14. Balanced Trees & TreeMap
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}
| Call | Answer | Why |
|---|---|---|
| floor(25) | 20 | biggest value ≤ 25 |
| ceiling(25) | 30 | smallest value ≥ 25 |
| floor(5) | null | nothing ≤ 5 |
| first() | 10 | smallest 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.
floorKey can be null.Practice: Time Based Key-Value Store (#981), My Calendar I (#729).
15. Tries
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"
| Word | Path | New nodes made |
|---|---|---|
| cat | root → c → a → t (isEnd) | c, a, t |
| car | root → c → a → r (isEnd) | only r (c and a are shared) |
| startsWith("ca") | root → c → a exists | true |
| search("ca") | node a has isEnd = false | false |
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.
Practice: Longest Common Prefix (#14), Implement Trie (#208).
16. Graphs & Traversal
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.
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
| Poll | New neighbours added | Queue after | Visited |
|---|---|---|---|
| start | — | [0] | {0} |
| 0 | 1, 2 | [1, 2] | {0, 1, 2} |
| 1 | 3 (0 already visited) | [2, 3] | {0, 1, 2, 3} |
| 2 | none | [3] | same |
| 3 | none | [] → done | same |
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.
Practice: Find if Path Exists in Graph (#1971), Flood Fill (#733), Number of Islands (#200).
17. Union-Find
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)
| Step | parent 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 2 | yes, 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.
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
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?
| Method | Update one item | Sum of a range |
|---|---|---|
| Plain array (loop to add) | O(1) | O(n) |
| Prefix sum array | O(n) (rebuild) | O(1) |
| Fenwick / segment tree | O(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.
Practice: Range Sum Query - Immutable (#303) first, then Range Sum Query - Mutable (#307).
19. Sorting
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]
| i | Card to place | What happens | Array after |
|---|---|---|---|
| start | — | — | [3, 1, 2] |
| 1 | 1 | 3 > 1, move 3 right; put 1 at index 0 | [1, 3, 2] |
| 2 | 2 | 3 > 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
}
}
| Sort | Time | Note |
|---|---|---|
| Bubble / Selection / Insertion | O(n²) | easy to write, slow |
| Merge sort | O(n log n) | always fast, uses O(n) extra space (topic 23) |
| Quick sort | O(n log n) average | fast in practice, O(n²) in the worst case |
| Counting sort | O(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).
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
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.
Key points
- The array must be sorted.
- Keep two indexes:
lo(left end) andhi(right end). Loop whilelo <= 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]
| lo | hi | mid | arr[mid] | Action |
|---|---|---|---|---|
| 0 | 6 | 3 | 7 | 7 < 9 → lo = 4 |
| 4 | 6 | 5 | 11 | 11 > 9 → hi = 4 |
| 4 | 4 | 4 | 9 | found → 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.
lo <= hi and move to mid + 1 / mid - 1.Practice: Binary Search (#704), Search Insert Position (#35), First Bad Version (#278).
21. Backtracking
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]
| Call | path | Saved | Next |
|---|---|---|---|
| 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 |
| end | answers: [], [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).
Practice: Subsets (#78), Letter Case Permutation (#784), Combination Sum (#39).
22. Greedy
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 |
|---|---|---|---|
| 1 | 1 | Yes → move both | 1 |
| 2 | 1 | No → try next cookie | 1 |
| 2 | none left | stop | 1 |
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.
Practice: Assign Cookies (#455), Best Time to Buy and Sell Stock II (#122), Jump Game (#55).
23. Divide & Conquer
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]
| Step | What happens | Result |
|---|---|---|
| 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.
Practice: Merge Two Sorted Lists (#21), Sort an Array (#912), Maximum Subarray (#53).
24. Dynamic Programming
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.
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
| i | dp[i-1] | dp[i-2] | dp[i] |
|---|---|---|---|
| 0, 1 | base cases | 1, 1 | |
| 2 | 1 | 1 | 2 |
| 3 | 2 | 1 | 3 |
| 4 | 3 | 2 | 5 |
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.
Practice: Climbing Stairs (#70), Fibonacci Number (#509), Min Cost Climbing Stairs (#746).
25. DP Patterns
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
| Pattern | State means | Example problem |
|---|---|---|
| 1D line | dp[i] = best answer using the first i items | House Robber (#198) |
| Grid | dp[r][c] = answer to reach cell (r, c) | Unique Paths (#62) |
| Knapsack (pick or skip) | dp[x] = best answer for amount x | Coin Change (#322) |
| Two strings | dp[i][j] = answer for first i chars of A and first j chars of B | Longest 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 0 | col 1 | col 2 | |
|---|---|---|---|
| row 0 | 1 | 1 | 1 |
| row 1 | 1 | 1 + 1 = 2 | 1 + 2 = 3 |
| row 2 | 1 | 2 + 1 = 3 | 3 + 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.
Practice: House Robber (#198), Unique Paths (#62), Coin Change (#322), Longest Common Subsequence (#1143).
26. Shortest Paths
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
distarray. Start with "infinity" everywhere, and 0 for the start. - When you find a shorter way to a node, update
distand 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 heap | Update | dist [0, 1, 2] |
|---|---|---|
| start | dist[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, skip | same |
// 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.
Practice: Shortest Path in Binary Matrix (#1091), Network Delay Time (#743).
27. Topo Sort, MST & More
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
| Tool | What it does | When |
|---|---|---|
| Topological sort | orders 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 out | same as Kruskal |
| Cycle check | Union-Find (undirected) or DFS colours (directed) | "is there a loop?" |
- Kruskal = sorting + Union-Find (topic 17). If
unionsays "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? | Action | total |
|---|---|---|---|
| 0-1, cost 1 | Yes | take it | 1 |
| 1-2, cost 2 | Yes | take it | 3 |
| 0-2, cost 3 | No (already joined) | skip | 3 |
// 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.
Practice: Course Schedule (#207), Min Cost to Connect All Points (#1584).
28. String Algorithms
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 i | Compare | Match? |
|---|---|---|
| 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).)
Practice: Find the Index of the First Occurrence in a String (#28), Repeated Substring Pattern (#459).
29. Math & Number Theory
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
longto avoid overflow.
Dry run: gcd(12, 8)
| a | b | a % b |
|---|---|---|
| 12 | 8 | 4 |
| 8 | 4 | 0 |
| 4 | 0 | b 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.
% 10 gets the last digit, / 10 drops it. Check primes up to √n.Practice: Fizz Buzz (#412), Palindrome Number (#9), Count Primes (#204).
30. Two Pointers
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.
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?
| left | right | chars | Same? |
|---|---|---|---|
| 0 | 6 | r, r | Yes |
| 1 | 5 | a, a | Yes |
| 2 | 4 | c, c | Yes |
| 3 | 3 | left is not < right → stop | palindrome |
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.
Practice: Valid Palindrome (#125), Move Zeroes (#283), Two Sum II - Input Array Is Sorted (#167, it wants 1-based positions).
31. Sliding Window
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.
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]
| Window | sum | best |
|---|---|---|
| [2, 1, 5] | 8 | 8 |
| [1, 5, 1] | 8 + 1 - 2 = 7 | 8 |
| [5, 1, 3] | 7 + 3 - 1 = 9 | 9 |
| [1, 3, 2] | 9 + 2 - 5 = 6 | 9 |
// 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.
Practice: Maximum Average Subarray I (#643), Longest Substring Without Repeating Characters (#3), Minimum Size Subarray Sum (#209).
32. Prefix Sum & Difference
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], withprefix[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] += vanddiff[R + 1] -= v, then take running totals at the end.
Dry run: arr = [1, 2, 3, 4]
| i | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| arr[i] | 1 | 2 | 3 | 4 | — |
| prefix[i] | 0 | 1 | 3 | 6 | 10 |
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.
Practice: Running Sum of 1d Array (#1480), Range Sum Query - Immutable (#303), Subarray Sum Equals K (#560).
33. Fast & Slow Pointers
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 != nullbefore 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
| Step | slow | fast |
|---|---|---|
| start | 1 | 1 |
| 1 | 2 | 3 |
| 2 | 3 | 5 |
| stop | 3 = middle | 5 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.
Practice: Middle of the Linked List (#876), Linked List Cycle (#141), Happy Number (#202).
34. Monotonic Stack & Queue
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]
| i | value | Action | stack (indexes) | answer |
|---|---|---|---|---|
| 0 | 2 | push 0 | [0] | [-1, -1, -1] |
| 1 | 1 | 1 is not bigger than 2 → push 1 | [0, 1] | [-1, -1, -1] |
| 2 | 3 | 3 > 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.
Practice: Next Greater Element I (#496), Daily Temperatures (#739), Sliding Window Maximum (#239, hard).
35. Merge Intervals
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 interval | Overlaps 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.
Practice: Merge Intervals (#56), Insert Interval (#57), Non-overlapping Intervals (#435).
36. Cyclic Sort
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 + 1shows a problem number. - O(n) time and O(1) extra space — better than sorting or a HashSet.
Dry run: [3, 1, 2]
| i | nums[i] | Its home index | Action | Array after |
|---|---|---|---|---|
| 0 | 3 | 2 | swap with index 2 | [2, 1, 3] |
| 0 | 2 | 1 | swap with index 1 | [1, 2, 3] |
| 0 | 1 | 0 | already home → i++ | [1, 2, 3] |
| 1, 2 | already 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.
number - 1.Practice: Missing Number (#268), Find All Numbers Disappeared in an Array (#448), Find the Duplicate Number (#287).
37. In-place List Reversal
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.nextbefore 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,
previs the new head. - Draw it on paper. Three boxes and arrows make it clear.
Dry run: reverse 1 → 2 → 3
| Step | prev | cur | Arrows so far |
|---|---|---|---|
| start | null | 1 | 1 → 2 → 3 |
| 1 | 1 | 2 | null ← 1 2 → 3 |
| 2 | 2 | 3 | null ← 1 ← 2 3 |
| 3 | 3 | null | null ← 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.
Practice: Reverse Linked List (#206), Palindrome Linked List (#234), Reverse Linked List II (#92).
38. Tree BFS & DFS
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)
| Round | Queue at start | size | Level saved | Queue 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.
Practice: Binary Tree Level Order Traversal (#102), Average of Levels in Binary Tree (#637), Path Sum (#112).
39. Top K & K-way Merge
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]
| Step | Result |
|---|---|
| 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.
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
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:
lowhas the same number of items ashigh, 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
| Add | low (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.
Practice: Find Median from Data Stream (#295, hard but it is the classic one).
41. Subsets & Permutations
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
| Type | For [1, 2, 3] | How many | Trick |
|---|---|---|---|
| Subsets | [], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3] | 2ⁿ = 8 | loop from start (topic 21) |
| Permutations | [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] | n! = 6 | used[] array, loop from 0 |
| Combinations (k = 2) | [1,2], [1,3], [2,3] | C(n, k) = 3 | subsets, 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)
| path | used | Action |
|---|---|---|
| [] | [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.
start. Permutations → loop from 0 with used[].Practice: Subsets (#78), Permutations (#46), Combinations (#77).
42. Modified Binary Search
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]
| lo | hi | mid | arr[mid] | Action | answer |
|---|---|---|---|---|---|
| 0 | 4 | 2 | 2 | found, keep looking left: hi = 1 | 2 |
| 0 | 1 | 0 | 1 | too small: lo = 1 | 2 |
| 1 | 1 | 1 | 2 | found, keep looking left: hi = 0 | 1 |
| 1 | 0 | lo > hi → stop | 1 | ||
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.
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
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 out | in-degree [0, 1, 2, 3] after | Queue after | Order |
|---|---|---|---|
| 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.
Practice: Course Schedule (#207), Course Schedule II (#210).
44. Matrix & Islands
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
| Grid | What happens |
|---|---|
1 1 00 1 00 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.
Practice: Flood Fill (#733), Number of Islands (#200), Max Area of Island (#695).
45. Design Problems
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,
LinkedHashMapalready 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
| Call | Returns | Cache (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.
Practice: Min Stack (#155), Design HashMap (#706), LRU Cache (#146).
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 question | Try this pattern | Example 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 times | Prefix 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 list | In-place reversal (37) | Reverse Linked List (#206) |
| Brackets, "undo", most recent first | Stack (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 arriving | Two heaps (40) | Find Median from Data Stream (#295) |
| Intervals, meetings, overlapping ranges | Sort + merge intervals (35) | Merge Intervals (#56) |
| "Numbers from 1 to n", missing / duplicate | Cyclic 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 fill | DFS / BFS on a matrix (44) | Number of Islands (#200) |
| "Shortest path", all steps cost the same | BFS (16) | Shortest Path in Binary Matrix (#1091) |
| "Shortest path" with different costs | Dijkstra (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 repeat | Dynamic programming (24, 25) | Climbing Stairs (#70) |
| "Prefix", "starts with", autocomplete | Trie (15) | Implement Trie (#208) |
| Best choice now seems always safe | Greedy (22) | Assign Cookies (#455) |
Big-O Cheat Sheet
Average times for the most common operations. "n" is the number of items.
| Structure | Get / find | Add | Remove | Note |
|---|---|---|---|---|
Array int[] | O(1) by index, O(n) by value | O(n) in the middle | O(n) in the middle | fixed size |
ArrayList | O(1) by index | O(1) at the end | O(n) in the middle | grows by itself |
| Linked list | O(n) | O(1) at the head | O(1) at the head | no index |
Stack / Queue (ArrayDeque) | O(1) top / front | O(1) | O(1) | only the ends |
HashMap / HashSet | O(1) | O(1) | O(1) | no order |
TreeMap / TreeSet | O(log n) | O(log n) | O(log n) | sorted keys |
Heap (PriorityQueue) | O(1) peek min | O(log n) | O(log n) poll | min first |
| BST (balanced) | O(log n) | O(log n) | O(log n) | O(n) if it becomes a line |
| Trie | O(L) | O(L) | O(L) | L = word length |
| Algorithm | Time | Extra space |
|---|---|---|
| Linear search / one loop | O(n) | O(1) |
| Binary search | O(log n) | O(1) |
| Simple sorts (bubble, insertion) | O(n²) | O(1) |
Merge sort / Arrays.sort | O(n log n) | O(n) / O(log n) |
| Two pointers / sliding window | O(n) | O(1) or O(k) |
| BFS / DFS on a graph | O(V + E) | O(V) |
| Dijkstra | O(E log V) | O(V + E) |
| Subsets / permutations | O(2ⁿ) / O(n!) | O(n) |
| 1D DP / 2D DP | O(n) / O(n × m) | O(n) / O(n × m) |
What input size allows what speed?
| n up to about… | You can afford |
|---|---|
| 10–20 | O(2ⁿ) or O(n!) — backtracking is fine |
| 1,000–5,000 | O(n²) |
| 100,000–1,000,000 | O(n log n) or O(n) |
| bigger | O(log n) or O(1) |
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.
- 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). - Arrays & strings (6) — loops, indexes, StringBuilder.
Problems: Reverse String (#344), Best Time to Buy and Sell Stock (#121), Merge Sorted Array (#88). - Hashing (7) — HashMap and HashSet.
Problems: Two Sum (#1), Contains Duplicate (#217), Valid Anagram (#242). - Two pointers (30) — from both ends.
Problems: Valid Palindrome (#125), Move Zeroes (#283), Two Sum II (#167). - Sliding window & prefix sum (31, 32).
Problems: Maximum Average Subarray I (#643), Longest Substring Without Repeating Characters (#3), Range Sum Query - Immutable (#303). - Stacks & queues (9, 10).
Problems: Valid Parentheses (#20), Min Stack (#155), Implement Queue using Stacks (#232). - Linked lists (8, 33, 37).
Problems: Reverse Linked List (#206), Middle of the Linked List (#876), Linked List Cycle (#141). - Recursion (3) — base case and call stack.
Problems: Fibonacci Number (#509), Power of Two (#231), Merge Two Sorted Lists (#21). - Binary search (20, 42).
Problems: Binary Search (#704), Search Insert Position (#35), First Bad Version (#278). - 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). - Heaps (11, 39).
Problems: Last Stone Weight (#1046), Kth Largest Element in a Stream (#703), Top K Frequent Elements (#347). - Backtracking (21, 41).
Problems: Subsets (#78), Permutations (#46), Combination Sum (#39). - Graphs & grids (16, 44, 43).
Problems: Flood Fill (#733), Number of Islands (#200), Course Schedule (#207). - Dynamic programming (24, 25).
Problems: Climbing Stairs (#70), House Robber (#198), Coin Change (#322). - 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.