Data Structures, Algorithms and Programming Patterns · Part 8 of 9

Data Structures and Algorithms, Part 8: Programming Patterns for Solving Problems

Khairul Alam·October 2026·~50 min with exercises
Data structuresAlgorithmsPatternsTwo pointersSliding windowKotlinFor beginners

Most problems are old shapes in new clothes. This part teaches the shapes: two pointers, a sliding window, prefix sums, binary search on the answer, a monotonic stack, intervals, a heap for the best k, and a few in-place and bit tricks. For each one you learn the clue, the picture, the slow way, the fast way, and then you run it.

This part in 60 seconds
  • A pattern is a reusable way of moving through data so you avoid repeated work.
  • Two pointers and sliding windows turn many O(n²) loops into O(n).
  • Prefix sums answer range questions in O(1); with a hash map they count subarrays with a given sum.
  • Binary search works on a range of possible answers when a yes/no check is monotonic.
  • Monotonic stacks, interval sweeps, heaps of size k and bit tricks each have a clear clue.
  • The decision table at the end maps the words in a problem to the pattern to try first.
Contents
  1. Why patterns? Recognise the shape, reuse the recipe
  2. Two pointers
  3. Fast and slow pointers
  4. Sliding window
  5. Prefix sums
  6. Hash map as memory (a recap)
  7. Binary search on the answer
  8. Monotonic stack
  9. Intervals
  10. Top-k with a heap
  11. In-place tricks: Dutch flag and cyclic sort
  12. Bit manipulation basics
  13. Which pattern? A one-page decision table
  14. Complete pattern catalogue
  15. Takeaways

1. Why patterns? Recognise the shape, reuse the recipe

After seven parts you know the tools: arrays, maps, stacks, heaps, trees, graphs, sorting and recursion. This part is about something different. It is about problem-solving patterns: small, reusable recipes that appear again and again, in different clothes, on top of those tools.

A pattern is not a new data structure. It is a way of moving through the data that avoids repeated work. Two markers walking toward each other. A window that slides along a list. A running total you keep so you never add the same numbers twice. Once you have seen each recipe, new problems start to look familiar, and that is the real skill: spotting the shape.

Think of chess openings. Players do not invent a plan from nothing every game. They know a handful of standard shapes and ask, “which one does this position look like?” Patterns here work the same way. The clue in the problem tells you which opening to play.

For every pattern we use the same five steps, so you can compare them:

  1. The clue. The words in a problem that suggest the pattern.
  2. A picture. What the pointers, window or stack look like.
  3. The naive way and its cost. Usually brute force from Part 1.
  4. The pattern and its cost. Often O(n²) down to O(n).
  5. A program you can run, then an exercise with tests.

The last section is a one-page table that maps clues to patterns. If you only keep one thing from this part, keep that table.

2. Two pointers

A pointer here just means an index (a position) into an array or string. The two-pointers pattern keeps two indexes and moves them according to a simple rule, so one pass does the work of a nested loop.

The clue

The picture

Opposite ends: find two numbers that add up to 9 in a sorted array 1 0 2 1 4 2 7 3 8 4 11 5 l r 1 + 11 = 12 too big -> move r left 1 2 4 7 8 11 l r 1 + 8 = 9 found it Same direction: remove duplicates in place (w = where to write, r = what we read) 1 1 2 2 2 3 w r a[r] differs from a[w-1]: write it, move w
Left and right pointers close in on each other; a read pointer runs ahead of a write pointer.

Naive versus pattern

To find a pair summing to a target, brute force tries every pair: two nested loops, O(n²) time. With a sorted array we can reason: if the sum is too small, only a bigger left number can help, so move l right. If it is too big, move r left. Every step throws away one candidate for good, so the cost is O(n) time and O(1) space.

ProblemNaiveTwo pointers
Pair with sum (sorted)O(n²)O(n), space O(1)
Reverse in placecopy into a new array, O(n) spaceO(n) time, space O(1)
Remove duplicates (sorted)build a new list or shift items, up to O(n²)O(n) time, space O(1)
Container with most waterO(n²) pairsO(n)

“Container with most water” deserves a word. You have vertical walls of given heights. Pick two walls; the water held is the shorter height times the distance between them. Start with the widest pair. The shorter wall limits the water, so moving the taller wall inward can only make things worse (narrower, same limit). Move the shorter one instead and keep the best area seen.

Run it: four classic two-pointer jobs

The first line prints the first pair the pointers meet. Try changing the target and predict which pair comes out.

fun pairSumSorted(a: IntArray, target: Int): Pair<Int, Int>? {
    var l = 0
    var r = a.size - 1
    while (l < r) {
        val sum = a[l] + a[r]
        if (sum == target) return l to r
        if (sum < target) l++ else r--
    }
    return null
}

fun reverseInPlace(a: IntArray) {
    var l = 0
    var r = a.size - 1
    while (l < r) {
        val t = a[l]; a[l] = a[r]; a[r] = t
        l++; r--
    }
}

// Returns the new length; the first that-many items are the unique values.
fun removeDuplicates(a: IntArray): Int {
    if (a.isEmpty()) return 0
    var w = 1                       // next slot to write
    for (r in 1 until a.size) {
        if (a[r] != a[w - 1]) { a[w] = a[r]; w++ }
    }
    return w
}

fun maxWater(h: IntArray): Int {
    var l = 0
    var r = h.size - 1
    var best = 0
    while (l < r) {
        best = maxOf(best, minOf(h[l], h[r]) * (r - l))
        if (h[l] < h[r]) l++ else r--      // move the shorter wall
    }
    return best
}

fun main() {
    println(pairSumSorted(intArrayOf(1, 2, 4, 7, 8, 11), 9))     // (0, 4): 1 + 8
    println(pairSumSorted(intArrayOf(1, 2, 3), 100))             // null

    val a = intArrayOf(1, 2, 3, 4, 5)
    reverseInPlace(a)
    println(a.toList())

    val b = intArrayOf(1, 1, 2, 2, 2, 3)
    val k = removeDuplicates(b)
    println("unique count $k -> ${b.copyOf(k).toList()}")

    println(maxWater(intArrayOf(1, 8, 6, 2, 5, 4, 8, 3, 7)))    // 49
}
Open in Kotlin Playground ↗

Try it: remove duplicates in place

Write removeDuplicates for a sorted array. Keep one copy of each value at the front of the same array and return how many unique values there are. Do not create a second array. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun removeDuplicates(a: IntArray): Int {
    TODO("a write pointer and a read pointer")
}
Open in Kotlin Playground ↗

Hint: The first item is always kept. Compare each new item with the last item you wrote, a[w - 1].

Show a solution
fun removeDuplicates(a: IntArray): Int {
    if (a.isEmpty()) return 0
    var w = 1
    for (r in 1 until a.size) {
        if (a[r] != a[w - 1]) { a[w] = a[r]; w++ }
    }
    return w
}

3. Fast and slow pointers

This is two pointers moving in the same direction at different speeds: the slow one takes one step, the fast one takes two. It is mostly used on linked lists (Part 3), where you cannot jump to the middle by index, and on any sequence that follows a rule like “the next value comes from the current one”.

The clue

The picture

Middle: when fast falls off the end, slow is halfway. 1 2 3 4 5 null s,f start 1 2 3 4 5 null s f 1 2 3 4 5 null s f fast jumped two each time; middle = 3 Cycle: fast laps slow, like two runners on a circular track. 1 2 3 4 slow moves 1, fast moves 2. Inside the loop the gap shrinks by 1 each step, so they must meet.
Fast moves two steps per slow step. On a loop the gap closes by one each round.

Naive versus pattern

To find the middle the naive way, walk once to count the nodes, then walk again to the halfway point: two passes, O(n) time. Fast and slow does it in one pass, still O(n) but simpler. To detect a cycle the naive way, store every node you visit in a hash set and stop if you see one twice: O(n) time but O(n) extra space. Fast and slow needs only two variables: O(1) space.

The happy-number idea: replace a number by the sum of the squares of its digits, again and again. Either you reach 1 (happy) or you loop forever. A loop in a sequence is exactly a cycle, so the same two-speed trick applies without storing anything.

Run it: middle, cycle and happy number

Run it and check the cycle detector before and after the loop is closed.

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

fun build(vararg values: Int): Node? {
    var head: Node? = null
    for (v in values.reversed()) head = Node(v, head)
    return head
}

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

fun hasCycle(head: Node?): Boolean {
    var slow = head
    var fast = head
    while (fast != null && fast.next != null) {
        slow = slow!!.next
        fast = fast.next!!.next
        if (slow === fast) return true      // === means "the very same node"
    }
    return false
}

fun digitSquares(n: Int): Int {
    var x = n
    var sum = 0
    while (x > 0) { val d = x % 10; sum += d * d; x /= 10 }
    return sum
}

fun isHappy(n: Int): Boolean {
    var slow = n
    var fast = digitSquares(n)
    while (fast != 1 && slow != fast) {
        slow = digitSquares(slow)
        fast = digitSquares(digitSquares(fast))
    }
    return fast == 1
}

fun main() {
    println("middle of 1..5 = ${middle(build(1, 2, 3, 4, 5))?.value}")
    println("middle of 1..4 = ${middle(build(1, 2, 3, 4))?.value}   (second middle)")

    val a = Node(1); val b = Node(2); val c = Node(3); val d = Node(4)
    a.next = b; b.next = c; c.next = d
    println("cycle before: ${hasCycle(a)}")
    d.next = b                                  // close a loop back to node 2
    println("cycle after:  ${hasCycle(a)}")

    println("happy numbers up to 30: ${(1..30).filter { isHappy(it) }}")
}
Open in Kotlin Playground ↗

Try it: is this number happy?

Write isHappy(n). A number is happy if repeatedly replacing it with the sum of the squares of its digits reaches 1. If it loops forever it is not happy. The helper digitSquares is given. Use two speeds, not a set. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun digitSquares(n: Int): Int {
    var x = n
    var sum = 0
    while (x > 0) { val d = x % 10; sum += d * d; x /= 10 }
    return sum
}

fun isHappy(n: Int): Boolean {
    TODO("slow = one step, fast = two steps; stop at 1 or when they meet")
}
Open in Kotlin Playground ↗

Hint: Start slow at n and fast one step ahead. Loop while fast != 1 and the two are different.

Show a solution
fun digitSquares(n: Int): Int {
    var x = n
    var sum = 0
    while (x > 0) { val d = x % 10; sum += d * d; x /= 10 }
    return sum
}

fun isHappy(n: Int): Boolean {
    var slow = n
    var fast = digitSquares(n)
    while (fast != 1 && slow != fast) {
        slow = digitSquares(slow)
        fast = digitSquares(digitSquares(fast))
    }
    return fast == 1
}

4. Sliding window

A window is a stretch of consecutive items, a subarray or substring. The sliding window pattern keeps a window and a running summary of it (a sum, a set of letters). To move the window, you add the new item on the right and remove the old item on the left, instead of recomputing everything.

The clue

The picture

Fixed window, k = 3. Slide right: add the new item, drop the old one. 2 1 5 1 3 2 sum = 8 2 1 5 1 3 2 sum = 8 - 2 + 1 = 7 2 1 5 1 3 2 sum = 7 - 1 + 3 = 9 best so far Variable window: grow on the right, shrink on the left when the rule breaks. s = "abcabcbb" a b c a b c b b window "abc" length 3 a b c a b c b b 'a' repeats, left jumps past the old 'a' a b c a b c b b keep going; remember the best length
Brackets mark the window. Only the two edges change on each step.

Naive versus pattern

The naive way looks at every window and adds its items up from scratch. For windows of size k that is about n * k steps. Sliding keeps the sum and updates it with one addition and one subtraction, so it is O(n). For variable windows, brute force checks every start and end pair (and then checks the rule inside), O(n²) or O(n³). The sliding version moves each edge only forward, so each edge makes at most n moves: O(n) total.

Two shapes

Run it: fixed and variable windows

Three windows in one file. Add a println inside the loops to watch left and right move.

// Fixed: largest sum of any k consecutive numbers.
fun maxSumOfK(a: IntArray, k: Int): Int? {
    if (k <= 0 || a.size < k) return null
    var window = 0
    for (i in 0 until k) window += a[i]
    var best = window
    for (i in k until a.size) {
        window += a[i] - a[i - k]          // add the new, drop the old
        best = maxOf(best, window)
    }
    return best
}

// Variable: longest substring with no repeated letter.
fun longestUnique(s: String): Int {
    val lastSeen = HashMap<Char, Int>()
    var left = 0
    var best = 0
    for ((right, c) in s.withIndex()) {
        val prev = lastSeen[c]
        if (prev != null && prev >= left) left = prev + 1   // jump past the repeat
        lastSeen[c] = right
        best = maxOf(best, right - left + 1)
    }
    return best
}

// Variable: shortest subarray whose sum is at least target (all numbers positive).
fun smallestSubarray(target: Int, a: IntArray): Int {
    var left = 0
    var sum = 0
    var best = Int.MAX_VALUE
    for (right in a.indices) {
        sum += a[right]
        while (sum >= target) {                // valid: record, then shrink
            best = minOf(best, right - left + 1)
            sum -= a[left]
            left++
        }
    }
    return if (best == Int.MAX_VALUE) 0 else best
}

fun main() {
    println(maxSumOfK(intArrayOf(2, 1, 5, 1, 3, 2), 3))   // 9
    println(maxSumOfK(intArrayOf(1, 2), 5))               // null
    println(longestUnique("abcabcbb"))                    // 3
    println(longestUnique("pwwkew"))                      // 3
    println(smallestSubarray(7, intArrayOf(2, 3, 1, 2, 4, 3)))   // 2  (4 + 3)
    println(smallestSubarray(100, intArrayOf(1, 2, 3)))          // 0  (none)
}
Open in Kotlin Playground ↗

Why does the variable window stay O(n)? It has a loop inside a loop, but left only ever moves forward and never passes right. Over the whole run left moves at most n times and right moves at most n times. Two passes, not n passes.

Try it: longest substring without repeats

Write longestUnique(s): the length of the longest stretch of s in which no letter appears twice. The test abba is the classic trap: the left edge must never move backwards. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun longestUnique(s: String): Int {
    TODO("variable window with a map from letter to last position")
}
Open in Kotlin Playground ↗

Hint: When you see a letter again, move left to one past its previous position, but only if that position is inside the current window (prev >= left).

Show a solution
fun longestUnique(s: String): Int {
    val lastSeen = HashMap<Char, Int>()
    var left = 0
    var best = 0
    for ((right, c) in s.withIndex()) {
        val prev = lastSeen[c]
        if (prev != null && prev >= left) left = prev + 1
        lastSeen[c] = right
        best = maxOf(best, right - left + 1)
    }
    return best
}

5. Prefix sums

A prefix sum array stores, at position i, the total of everything before position i. Build it once in O(n). After that, the sum of any range is one subtraction.

The clue

The picture

prefix p a 0 p0 3 p1 4 p2 8 p3 9 p4 14 p5 3 a[0] 1 a[1] 4 a[2] 1 a[3] 5 a[4] p[i] = sum of the first i items sum of a[1..3] (1 + 4 + 1 = 6) = p[4] - p[1] = 9 - 3 = 6 Subarray sum = k: a range ends at "now" when p[now] - p[earlier] = k i.e. p[earlier] = p[now] - k so ask a hash map: "how many earlier prefixes equal p[now] - k?"
A range sum is the prefix at the end minus the prefix just before the start.

Naive versus pattern

The naive way adds up the range each time you are asked: O(n) per query, so O(n * q) for q queries. With a prefix array: O(n) once to build, then O(1) per query. It costs O(n) extra space.

For “subarray sum equals k”, brute force tries every start and end: O(n²). The trick: keep a running total and a hash map counting how often each running total has appeared. A subarray ending here sums to k exactly when an earlier running total equals now - k. One pass, O(n) time and space. Seed the map with 0 -> 1 so subarrays that start at the very beginning are counted.

Run it: range queries and subarray sum equals k

The map version also works with negative numbers, which a sliding window cannot handle.

class RangeSum(a: IntArray) {
    private val prefix = IntArray(a.size + 1)
    init { for (i in a.indices) prefix[i + 1] = prefix[i] + a[i] }

    // sum of a[from..to], both included
    fun sum(from: Int, to: Int): Int = prefix[to + 1] - prefix[from]
}

fun countSubarraysWithSum(a: IntArray, k: Int): Int {
    val seen = HashMap<Int, Int>()      // running total -> how many times
    seen[0] = 1
    var running = 0
    var count = 0
    for (x in a) {
        running += x
        count += seen[running - k] ?: 0
        seen[running] = (seen[running] ?: 0) + 1
    }
    return count
}

fun main() {
    val rs = RangeSum(intArrayOf(3, 1, 4, 1, 5))
    println(rs.sum(1, 3))      // 6
    println(rs.sum(0, 4))      // 14
    println(rs.sum(2, 2))      // 4

    println(countSubarraysWithSum(intArrayOf(1, 1, 1), 2))        // 2
    println(countSubarraysWithSum(intArrayOf(1, -1, 0), 0))       // 3, negatives are fine
    println(countSubarraysWithSum(intArrayOf(), 5))               // 0
}
Open in Kotlin Playground ↗

A brief look at two dimensions

For a grid, let p[r][c] be the sum of the rectangle from the top-left corner to just before row r, column c. Any inner rectangle is then four lookups: take the big one, subtract the strip above and the strip to the left, and add back the corner you subtracted twice.

rect(r1,c1 .. r2,c2) = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1] corner above left WANT c1 c2 r1 r2 big rectangle minus above minus left plus corner (it was removed twice)
The rectangle sum is the big prefix block, minus the strip above and the strip to the left, plus the corner that was removed twice.

Run it: rectangle sums on a grid

class GridSum(g: Array<IntArray>) {
    private val rows = g.size
    private val cols = if (rows == 0) 0 else g[0].size
    private val p = Array(rows + 1) { IntArray(cols + 1) }

    init {
        for (r in 0 until rows) for (c in 0 until cols) {
            p[r + 1][c + 1] = g[r][c] + p[r][c + 1] + p[r + 1][c] - p[r][c]
        }
    }

    fun rect(r1: Int, c1: Int, r2: Int, c2: Int): Int =
        p[r2 + 1][c2 + 1] - p[r1][c2 + 1] - p[r2 + 1][c1] + p[r1][c1]
}

fun main() {
    val grid = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(4, 5, 6),
        intArrayOf(7, 8, 9),
    )
    val gs = GridSum(grid)
    println(gs.rect(0, 0, 2, 2))   // 45, the whole grid
    println(gs.rect(1, 1, 2, 2))   // 5 + 6 + 8 + 9 = 28
    println(gs.rect(0, 1, 1, 1))   // 2 + 5 = 7
}
Open in Kotlin Playground ↗

Try it: count subarrays with sum k

Write subarraySumK(a, k): how many contiguous subarrays add up to k? Numbers may be negative or zero. Use a running total and a map; the last test is too big for a double loop. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun subarraySumK(a: IntArray, k: Int): Int {
    TODO("running total + HashMap of how often each total was seen")
}
Open in Kotlin Playground ↗

Hint: Before adding the current total to the map, look up running - k and add its count to your answer. Start with the map holding 0 to 1.

Show a solution
fun subarraySumK(a: IntArray, k: Int): Int {
    val seen = HashMap<Int, Int>()
    seen[0] = 1
    var running = 0
    var count = 0
    for (x in a) {
        running += x
        count += seen[running - k] ?: 0
        seen[running] = (seen[running] ?: 0) + 1
    }
    return count
}

6. Hash map as memory (a recap)

You met hash maps and sets in Part 2. Here is the pattern in one sentence: when you keep asking “have I seen this before?” or “where was it?”, store what you have seen in a hash map or set, and each question becomes O(1) on average.

The clue

A new problem: the longest run of consecutive numbers

Given unsorted numbers, find the length of the longest run of consecutive values (like 1, 2, 3, 4), in any order. The naive way sorts first: O(n log n). With a set we can do O(n): only start counting at a number whose predecessor is missing, because that number is the start of a run.

numbers 100 4 200 1 3 2 set = {100, 4, 200, 1, 3, 2} 100 99 not in set -> start of a run. 101? no. run length 1 4 3 is in set -> not a start, skip 200 199 not in set -> start. 201? no. run length 1 1 0 not in set -> start. 2, 3, 4 are in set. run length 4 best
Each number is walked over as part of exactly one run, so the total work is linear.

Run it: longest consecutive run with a set

The inner while looks scary, but it only runs from the start of a run, never from the middle.

fun longestConsecutive(a: IntArray): Int {
    val set = a.toHashSet()
    var best = 0
    for (x in set) {
        if (x - 1 !in set) {                // x starts a run
            var y = x
            while (y + 1 in set) y++
            best = maxOf(best, y - x + 1)
        }
    }
    return best
}

fun main() {
    println(longestConsecutive(intArrayOf(100, 4, 200, 1, 3, 2)))    // 4
    println(longestConsecutive(intArrayOf(5, 5, 5)))                 // 1
    println(longestConsecutive(intArrayOf()))                        // 0
}
Open in Kotlin Playground ↗

7. Binary search on the answer

In Part 6 you used binary search to find a value in a sorted list. This pattern applies the same halving idea to a different thing: not a list, but the range of possible answers.

The clue

The picture

candidate answers 1 2 3 4 5 6 7 8 9 10 does it work? no no no no no YES YES YES YES YES smallest that works lo=1 hi=10 mid=5 no -> lo = 6 mid=8 yes -> hi = 8 mid=7 yes -> hi = 7 mid=6 yes -> hi = 6 lo == hi: answer 6
Halve the range of answers, keep the half that must contain the boundary between no and yes.

Naive versus pattern

The naive way tries every candidate answer from the smallest up, running the check each time. If the answers range up to M and the check costs O(n), that is O(M * n). Binary search needs only about log M checks: O(n log M).

Two problems, one shape. Koko and the bananas: piles of bananas, h hours; each hour she picks one pile and eats up to k bananas from it. What is the smallest k that finishes in time? Shipping: packages must go in order, one ship, d days; what is the smallest ship capacity? In both, the check is a simple loop.

Run it: smallest speed and smallest capacity

Notice that the search code is almost identical in both. Only the yes/no check changes.

// Koko: smallest eating speed k so all piles finish within h hours.
fun hoursNeeded(piles: IntArray, k: Int): Long {
    var hours = 0L
    for (p in piles) hours += (p + k - 1) / k      // round up
    return hours
}

fun minEatingSpeed(piles: IntArray, h: Int): Int {
    if (piles.isEmpty()) return 0
    var lo = 1
    var hi = piles.max()                  // eating the biggest pile in an hour always works
    while (lo < hi) {
        val mid = lo + (hi - lo) / 2
        if (hoursNeeded(piles, mid) <= h) hi = mid else lo = mid + 1
    }
    return lo
}

// Ship packages in order within the given number of days.
fun daysNeeded(weights: IntArray, capacity: Int): Int {
    var days = 1
    var load = 0
    for (w in weights) {
        if (load + w > capacity) { days++; load = 0 }
        load += w
    }
    return days
}

fun minCapacity(weights: IntArray, days: Int): Int {
    if (weights.isEmpty()) return 0
    var lo = weights.max()                // must at least carry the heaviest package
    var hi = weights.sum()                // one day carries everything
    while (lo < hi) {
        val mid = lo + (hi - lo) / 2
        if (daysNeeded(weights, mid) <= days) hi = mid else lo = mid + 1
    }
    return lo
}

fun main() {
    println(minEatingSpeed(intArrayOf(3, 6, 7, 11), 8))                  // 4
    println(minCapacity(intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 5))   // 15
    println(minCapacity(intArrayOf(3, 2, 2, 4, 1, 4), 3))                // 6
}
Open in Kotlin Playground ↗

Choosing lo and hi. Set lo to the smallest answer that could possibly work and hi to one that surely works. Then keep hi = mid when mid works (it might be the answer) and lo = mid + 1 when it does not.

Try it: slowest speed that finishes in time

Write minEatingSpeed(piles, h): the smallest whole number of bananas per hour so that all piles are eaten within h hours. She eats from one pile per hour. Return 0 for no piles. Assume h is at least the number of piles. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun minEatingSpeed(piles: IntArray, h: Int): Int {
    TODO("binary search k between 1 and the biggest pile")
}
Open in Kotlin Playground ↗

Hint: For a speed k, a pile of p bananas takes (p + k - 1) / k hours (rounding up). Use a Long for the total hours.

Show a solution
fun minEatingSpeed(piles: IntArray, h: Int): Int {
    if (piles.isEmpty()) return 0
    var lo = 1
    var hi = piles.max()
    while (lo < hi) {
        val mid = lo + (hi - lo) / 2
        var hours = 0L
        for (p in piles) hours += (p.toLong() + mid - 1) / mid
        if (hours <= h) hi = mid else lo = mid + 1
    }
    return lo
}

8. Monotonic stack

A monotonic stack is a stack (Part 3) whose items are always kept in order, for example each item smaller than the one below it. Before you push a new item you pop everything that breaks the order. Each pop is a moment when you learn an answer for the popped item.

The clue

The picture

temps 73 74 75 71 69 72 76 73 days until warmer? stack (indexes) step i=0 73 stack [] push 0 0 i=1 74 74 > 73, pop 0 -> answer[0] = 1; push 1 1 i=2 75 75 > 74, pop 1 -> answer[1] = 1; push 2 2 i=3 71 71 < 75, just push 2 3 i=4 69 just push 2 3 4 i=5 72 pop 4 (ans 1), pop 3 (ans 2); 72 < 75 push 2 5 i=6 76 pop 5 (ans 1), pop 2 (ans 4); push 6 i=7 73 push 6 7 never warmer: 0 Stack holds items still waiting for something bigger. Temperatures on it only go down from bottom to top.
The stack keeps the items that are still waiting for an answer. A bigger arrival answers all the smaller ones on top.

Naive versus pattern

The naive way scans to the right from every item until it finds a bigger one: O(n²) in the worst case (a list that only goes down). With a monotonic stack every item is pushed once and popped at most once, so it is O(n) time and O(n) space. The loop inside a loop is fooled by its own look: the total number of pops can never exceed the number of pushes.

The “largest rectangle in a histogram” idea uses the same stack in the other direction. For each bar, how far can it stretch left and right before meeting a shorter bar? When a bar is popped, the bar below it on the stack is the first shorter bar to its left, and the bar that caused the pop is the first shorter bar to its right. That fixes the width, so the area is height times width.

Run it: next greater, warmer days and the largest rectangle

The stack holds indexes, not values, so we can both read the value and compute distances.

fun nextGreater(a: IntArray): IntArray {
    val result = IntArray(a.size) { -1 }          // -1 means "none"
    val stack = ArrayDeque<Int>()                 // indexes, values decreasing
    for (i in a.indices) {
        while (stack.isNotEmpty() && a[stack.last()] < a[i]) {
            result[stack.removeLast()] = a[i]
        }
        stack.addLast(i)
    }
    return result
}

fun dailyTemperatures(t: IntArray): IntArray {
    val wait = IntArray(t.size)                   // 0 means "never warmer"
    val stack = ArrayDeque<Int>()
    for (i in t.indices) {
        while (stack.isNotEmpty() && t[stack.last()] < t[i]) {
            val j = stack.removeLast()
            wait[j] = i - j
        }
        stack.addLast(i)
    }
    return wait
}

fun largestRectangle(h: IntArray): Int {
    val stack = ArrayDeque<Int>()                 // indexes, heights increasing
    var best = 0
    for (i in 0..h.size) {
        val cur = if (i == h.size) 0 else h[i]    // a final height 0 pops everything
        while (stack.isNotEmpty() && h[stack.last()] >= cur) {
            val height = h[stack.removeLast()]
            val left = if (stack.isEmpty()) -1 else stack.last()
            best = maxOf(best, height * (i - left - 1))
        }
        stack.addLast(i)
    }
    return best
}

fun main() {
    println(nextGreater(intArrayOf(2, 1, 5, 3, 6, 4)).toList())                      // [5, 5, 6, 6, -1, -1]
    println(dailyTemperatures(intArrayOf(73, 74, 75, 71, 69, 72, 76, 73)).toList())  // [1, 1, 4, 2, 1, 1, 0, 0]
    println(largestRectangle(intArrayOf(2, 1, 5, 6, 2, 3)))                          // 10
}
Open in Kotlin Playground ↗

Try it: days until a warmer day

Write dailyTemperatures(t): for each day, how many days until a warmer temperature, or 0 if there never is one. A monotonic stack makes it linear. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun dailyTemperatures(t: IntArray): IntArray {
    TODO("stack of indexes still waiting for a warmer day")
}
Open in Kotlin Playground ↗

Hint: Pop while the top day is colder than today. Each popped day j waited i - j days. Equal temperatures are not warmer, so use a strict <.

Show a solution
fun dailyTemperatures(t: IntArray): IntArray {
    val wait = IntArray(t.size)
    val stack = ArrayDeque<Int>()
    for (i in t.indices) {
        while (stack.isNotEmpty() && t[stack.last()] < t[i]) {
            val j = stack.removeLast()
            wait[j] = i - j
        }
        stack.addLast(i)
    }
    return wait
}

9. Intervals

An interval is a start and an end: a meeting from 9 to 10, a booking from day 3 to day 7. Intervals overlap when they share some time. Interval problems share one big trick: sort by start time, and the overlaps become neighbours.

The clue

The picture

Merge after sorting by start: 1 2 3 4 5 6 7 8 9 10 time A B C merged <- A and B overlap: 1..6 C stays alone: 8..10 -> Meeting rooms: sweep through the sorted starts and sorted ends. intervals (0,30) (5,10) (15,20) starts 0 5 15 ends 10 20 30 a start before the next end -> a new room an end first -> a room frees up at time 5 : 2 rooms busy at time 10: 1 freed, 15 starts: 2 busy again maximum = 2 rooms
After sorting, one pass is enough: each interval either extends the last one or starts a new one.

Naive versus pattern

Without sorting, merging means comparing every interval with every other one, repeatedly: about O(n²). Sorting costs O(n log n) and then the merge is a single O(n) pass, so O(n log n) overall. Counting meeting rooms naively checks every time point against every meeting; the sweep sorts the starts and the ends separately and walks both lists, O(n log n) in total.

If the list is already sorted and non-overlapping, insert interval needs no sort at all: copy the intervals that end before the new one starts, merge all that overlap it, then copy the rest. That is O(n).

Run it: merge, insert and count rooms

Touching intervals such as (1, 4) and (4, 5) are treated as overlapping when merging, but as sharing a room when counting. Decide which you need and test it.

fun mergeIntervals(iv: List<Pair<Int, Int>>): List<Pair<Int, Int>> {
    if (iv.isEmpty()) return emptyList()
    val sorted = iv.sortedBy { it.first }
    val out = mutableListOf(sorted[0])
    for (i in 1 until sorted.size) {
        val last = out.last()
        val cur = sorted[i]
        if (cur.first <= last.second) {
            out[out.size - 1] = last.first to maxOf(last.second, cur.second)   // overlap: extend
        } else {
            out.add(cur)                                                        // gap: new interval
        }
    }
    return out
}

// iv is sorted and non-overlapping already.
fun insertInterval(iv: List<Pair<Int, Int>>, add: Pair<Int, Int>): List<Pair<Int, Int>> {
    val out = mutableListOf<Pair<Int, Int>>()
    var cur = add
    var i = 0
    while (i < iv.size && iv[i].second < cur.first) { out.add(iv[i]); i++ }      // entirely before
    while (i < iv.size && iv[i].first <= cur.second) {                            // overlapping
        cur = minOf(cur.first, iv[i].first) to maxOf(cur.second, iv[i].second)
        i++
    }
    out.add(cur)
    while (i < iv.size) { out.add(iv[i]); i++ }                                   // entirely after
    return out
}

fun minMeetingRooms(iv: List<Pair<Int, Int>>): Int {
    val starts = iv.map { it.first }.sorted()
    val ends = iv.map { it.second }.sorted()
    var i = 0
    var j = 0
    var busy = 0
    var most = 0
    while (i < starts.size) {
        if (starts[i] < ends[j]) { busy++; i++; most = maxOf(most, busy) }   // someone arrives
        else { busy--; j++ }                                                 // someone leaves
    }
    return most
}

fun main() {
    println(mergeIntervals(listOf(1 to 3, 2 to 6, 8 to 10, 15 to 18)))   // [(1, 6), (8, 10), (15, 18)]
    println(insertInterval(listOf(1 to 3, 6 to 9), 2 to 5))              // [(1, 5), (6, 9)]
    println(minMeetingRooms(listOf(0 to 30, 5 to 10, 15 to 20)))         // 2
    println(minMeetingRooms(listOf(1 to 2, 2 to 3)))                     // 1, touching ends reuse the room
}
Open in Kotlin Playground ↗

Try it: merge overlapping intervals

Write mergeIntervals. The input may be in any order. Intervals that overlap or touch become one. Return the result sorted by start. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun mergeIntervals(iv: List<Pair<Int, Int>>): List<Pair<Int, Int>> {
    TODO("sort by start, then extend or append")
}
Open in Kotlin Playground ↗

Hint: Compare each interval with the last one in your result. If it starts at or before the last end, the new end is the larger of the two ends (case: one swallows another).

Show a solution
fun mergeIntervals(iv: List<Pair<Int, Int>>): List<Pair<Int, Int>> {
    if (iv.isEmpty()) return emptyList()
    val sorted = iv.sortedBy { it.first }
    val out = mutableListOf(sorted[0])
    for (i in 1 until sorted.size) {
        val last = out.last()
        val cur = sorted[i]
        if (cur.first <= last.second) {
            out[out.size - 1] = last.first to maxOf(last.second, cur.second)
        } else {
            out.add(cur)
        }
    }
    return out
}

10. Top-k with a heap

You met the heap in Part 4: a structure that always hands you the smallest item (a min-heap) in O(log n). The top-k pattern uses it as a small fixed-size leaderboard.

The clue

The picture

k = 3, numbers arrive: 5 1 9 3 7 8 keep a MIN-heap that never holds more than k items. when it grows past k, throw away the smallest (the weakest on the board). add 5 5 add 1 1 5 add 9 1 5 9 add 3 1 3 5 9 -> too many, drop 1 -> 3 5 9 add 7 3 5 7 9 -> too many, drop 3 -> 5 7 9 add 8 5 7 8 9 -> too many, drop 5 -> 7 8 9 the three largest
For the k largest we use a min-heap: the smallest of the winners sits on top, ready to be kicked out.

Naive versus pattern

The naive way sorts everything, O(n log n), then takes the first k. The heap never holds more than k items, so every push or pop costs O(log k) and the total is O(n log k) with O(k) space. When k is small compared to n, this is a real saving, and it works on streams.

For “k most frequent” first count with a hash map (O(n)), then run the same leaderboard over the counts, ordered by count.

Run it: k largest and k most frequent

In Kotlin you can also write a.sortedDescending().take(k). It is shorter, and fine for small data. The heap version is what scales.

import java.util.PriorityQueue

fun kLargest(a: IntArray, k: Int): List<Int> {
    val heap = PriorityQueue<Int>()          // min-heap
    for (x in a) {
        heap.add(x)
        if (heap.size > k) heap.poll()       // drop the smallest
    }
    return heap.sortedDescending()
}

fun kMostFrequent(words: List<String>, k: Int): List<String> {
    val counts = HashMap<String, Int>()
    for (w in words) counts[w] = (counts[w] ?: 0) + 1

    val heap = PriorityQueue<Pair<String, Int>>(compareBy { it.second })
    for ((word, n) in counts) {
        heap.add(word to n)
        if (heap.size > k) heap.poll()       // drop the least frequent
    }
    return heap.sortedByDescending { it.second }.map { it.first }
}

fun main() {
    println(kLargest(intArrayOf(5, 1, 9, 3, 7, 8), 3))     // [9, 8, 7]
    println(kLargest(intArrayOf(4, 4), 5))                 // [4, 4], fewer than k items is fine
    val words = listOf("a", "b", "a", "c", "a", "b", "d")
    println(kMostFrequent(words, 2))                       // [a, b]
}
Open in Kotlin Playground ↗

11. In-place tricks: Dutch flag and cyclic sort

Sometimes the rules say: do it in the same array, with O(1) extra space. Two small ideas cover a lot of ground.

The Dutch national flag

The clue: the array has only a few distinct values (like 0, 1 and 2) and you must group them together in one pass. Keep three zones with three pointers: everything before lo is 0, everything from hi + 1 on is 2, and mid scans the unknown middle.

0 0 0 1 1 1 ? ? ? ? 2 2 lo mid hi a[mid] == 0 swap with lo, lo++, mid++ a[mid] == 1 mid++ a[mid] == 2 swap with hi, hi-- (mid stays: the swapped-in value is unknown)
Three zones and one scanner. One pass, no extra array.

Cyclic sort

The clue: numbers are in a known range such as 0..n or 1..n, and you must find the missing or duplicate one. Since value v belongs at index v, put each number straight into its home by swapping. Then any index whose value is wrong tells you the answer. Each swap fixes at least one number for good, so it is O(n) time, O(1) space.

Run it: sort colors and find the missing number

Both functions change the array they are given. That is what “in place” means.

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

fun sortColors(a: IntArray) {
    var lo = 0
    var mid = 0
    var hi = a.size - 1
    while (mid <= hi) {
        when (a[mid]) {
            0 -> { swap(a, lo, mid); lo++; mid++ }
            1 -> mid++
            else -> { swap(a, mid, hi); hi-- }
        }
    }
}

// a holds the numbers 0..n with exactly one missing (n = a.size).
fun missingNumber(a: IntArray): Int {
    val n = a.size
    var i = 0
    while (i < n) {
        val v = a[i]
        if (v < n && a[v] != v) swap(a, i, v) else i++    // send v to its home, index v
    }
    for (j in 0 until n) if (a[j] != j) return j
    return n
}

fun main() {
    val colors = intArrayOf(2, 0, 2, 1, 1, 0)
    sortColors(colors)
    println(colors.toList())                                  // [0, 0, 1, 1, 2, 2]

    println(missingNumber(intArrayOf(3, 0, 1)))               // 2
    println(missingNumber(intArrayOf(9, 6, 4, 2, 3, 5, 7, 0, 1)))   // 8
    println(missingNumber(intArrayOf(0, 1)))                  // 2
}
Open in Kotlin Playground ↗

12. Bit manipulation basics

Every integer is stored as bits: ones and zeros. Working directly on bits is fast and sometimes gives surprisingly short answers. You need only a few operations.

OperationKotlinWhat it does
ANDa and b1 where both bits are 1
ORa or b1 where either bit is 1
XORa xor b1 where the bits differ
Shift lefta shl 1moves bits left, doubles the number
Shift righta shr 1moves bits right, halves the number
Read bit i(a shr i) and 1is bit number i set?

The clue

The picture

XOR cancels equal numbers: x xor x = 0 and x xor 0 = x 4 xor 1 xor 2 xor 1 xor 2 = 4 xor (1 xor 1) xor (2 xor 2) = 4 xor 0 xor 0 = 4 4 the single number n and (n - 1) clears the lowest 1-bit: n 0 1 1 0 0 (12) n - 1 0 1 0 1 1 (11) n & n-1 0 1 0 0 0 (8) one 1-bit removed power of two = exactly one 1-bit, so n > 0 and (n and (n-1)) == 0
XOR makes pairs vanish. Subtracting one flips the lowest 1-bit and the zeros below it.

Naive versus pattern

To find the number that appears once, you could count with a hash map: O(n) time and O(n) space. XOR does it in O(n) time and O(1) space. To count set bits you could loop over all 32 positions. The n and (n - 1) trick loops only as many times as there are 1-bits.

Run it: single number, power of two, count bits

The last loop enumerates all 8 subsets of 3 items by counting from 0 to 7. Each bit says “in” or “out” for one item.

fun singleNumber(a: IntArray): Int {
    var x = 0
    for (v in a) x = x xor v
    return x
}

fun isPowerOfTwo(n: Int): Boolean = n > 0 && (n and (n - 1)) == 0

fun countBits(n: Int): Int {
    var x = n
    var count = 0
    while (x != 0) {
        x = x and (x - 1)        // remove the lowest 1-bit
        count++
    }
    return count
}

fun main() {
    println(singleNumber(intArrayOf(4, 1, 2, 1, 2)))     // 4
    println(listOf(0, 1, 2, 3, 16, 18, -8).map { isPowerOfTwo(it) })
    println(listOf(0, 7, 1024, 255).map { countBits(it) })   // [0, 3, 1, 8]

    // Bitmask: list every subset of three items.
    val items = listOf("x", "y", "z")
    for (mask in 0 until (1 shl items.size)) {
        val subset = items.filterIndexed { i, _ -> (mask shr i) and 1 == 1 }
        println("mask ${Integer.toBinaryString(mask).padStart(3, '0')} -> $subset")
    }
}
Open in Kotlin Playground ↗

Try it: power of two and bit count

Fill in both functions using bit operations only (no division, no string conversion). Zero and negative numbers are not powers of two. For countOnes, negative numbers count their 1-bits in 32-bit form. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun isPowerOfTwo(n: Int): Boolean {
    TODO("exactly one 1-bit, and n is positive")
}

fun countOnes(n: Int): Int {
    TODO("remove the lowest 1-bit until nothing is left")
}
Open in Kotlin Playground ↗

Hint: n and (n - 1) removes the lowest 1-bit. A power of two has only one, so removing it leaves 0.

Show a solution
fun isPowerOfTwo(n: Int): Boolean = n > 0 && (n and (n - 1)) == 0

fun countOnes(n: Int): Int {
    var x = n
    var count = 0
    while (x != 0) {
        x = x and (x - 1)
        count++
    }
    return count
}

13. Which pattern? A one-page decision table

Read the problem, find the clue, pick the pattern. Then check the cost matches what you need.

When you see…TryTypical cost
Sorted array, find a pair or triple; reverse; palindromeTwo pointers (opposite ends)O(n) time, O(1) space
Edit an array in place, keep or remove itemsTwo pointers (read and write)O(n), O(1)
Middle of a linked list; cycle; a sequence that may loopFast and slow pointersO(n), O(1)
Contiguous subarray or substring of size kFixed sliding windowO(n)
Longest or shortest stretch satisfying a rule (non-negative data)Variable sliding windowO(n)
Many range sums; subarray sum equals k (negatives allowed)Prefix sums, plus a hash mapO(n) build, O(1) query
Seen before? Count? Group? Pair that sums to a target (unsorted)Hash map or setO(n), O(n) space
Smallest value that still works; a yes/no check that is monotonicBinary search on the answerO(n log M)
Next greater or smaller; how many days until; histogram barsMonotonic stackO(n)
Pairs of (start, end); merge, insert, overlap, roomsSort by start, then sweepO(n log n)
k largest, k smallest, k most frequent, k-th largestHeap of size kO(n log k)
Few distinct values to group; numbers in 1..n with a gap or duplicateDutch flag; cyclic sortO(n), O(1)
Every item twice but one; power of two; set bits; subsets as flagsBit manipulation (XOR, mask)O(n) or O(1)
All combinations, choices, paths; or a best value with repeated sub-problemsRecursion, backtracking, DP (Part 7)depends
Shortest path, connected parts, dependenciesGraph search (Part 5)O(V + E)

A good habit. Always write the naive version first and say its cost. The waste in it (a repeated scan, a recomputed sum, an unsorted list) usually points at the pattern. The clue column is a starting guess, not a rule. If the first pattern does not fit, try the next.

Name that pattern

Without writing code, decide which pattern each problem needs. Open the answer only after you have chosen.

  1. Given a sorted list of prices, find two that add to exactly 50.
  2. Given a text, what is the longest piece that contains at most two different letters?
  3. Given a list of readings that can be negative, how many consecutive stretches sum to zero?
  4. Given the daily stock prices, for each day, how many days to wait for a higher price?
  5. Given a stream of 10 million scores, keep the top 10 at all times.
  6. Given appointment slots, can one person attend all of them?
  7. Given a function that tells whether a time limit of t minutes is enough, find the smallest t.
  8. Given a list where every number appears three times except one that appears once, find it without a map.
Show the answers
  1. Two pointers from both ends.
  2. Variable sliding window with a small map of letter counts.
  3. Prefix sums with a hash map (negatives rule out a plain window).
  4. Monotonic stack (the daily temperatures shape).
  5. A min-heap of size 10 (top-k).
  6. Intervals: sort by start and check that each start is not before the previous end.
  7. Binary search on the answer.
  8. Bit manipulation: count each bit position across all numbers, modulo 3. The XOR trick from this part is the case where pairs appear twice; the idea of reasoning bit by bit extends to triples.

14. Complete pattern catalogue

The sections above teach the most common patterns in depth. To make sure nothing is missing, this catalogue merges two popular quicksheets: the Grokking patterns quicksheet (30 common and 11 advanced patterns) and AlgoArk (29 patterns), plus the Dutch flag from section 11. Duplicates are merged, which leaves 49 patterns.

The ★ Popular mark is my judgment, not a measured statistic: 23 patterns that you will meet again and again in everyday code. Neither source ranks its patterns; Grokking only suggests starting with two pointers, sliding window, hash maps and prefix sums. The Where column links to the section of this series that teaches the pattern. Patterns marked Summary here are not taught in this series, so the table row is the summary: the clue, the idea and the cost. Open a family to see its list; popular patterns come first.

Arrays and strings12 patterns, 6 popular
PatternClue and typical problemsTime; spacePopularWhere
Two pointers
Grokking + AlgoArk
Sorted input, pair or triple, both ends, in-place editsO(n); O(1)★This part §2
Sliding window
Grokking + AlgoArk
Longest/shortest/fixed-size contiguous subarray or substringO(n); O(1) or O(k)★This part §4
Prefix sum
Grokking + AlgoArk
Many range sums; subarray sum equals kO(n) build, O(1) query★This part §5
Hash map counting / grouping
Grokking + AlgoArk
Frequency, seen-before, grouping, anagramsO(n); O(n)★This part §6
Two Sum (complement lookup)
AlgoArk
Unsorted data: store what you have seen, look up target - xO(n); O(n)★This part §6
Kadane's algorithm
AlgoArk
Maximum subarray sum or product: running sum that restarts when it turns negative. cur = max(x, cur + x); best = max(best, cur)O(n); O(1)★Summary here
Cyclic sort
Grokking + AlgoArk
Values in 1..n or 0..n: missing, duplicate or corrupt numbersO(n); O(1)This part §11
Dutch national flag
extra
Group a few distinct values (0/1/2) in one passO(n); O(1)This part §11
Counting
Grokking advanced
Tally or bucket by value to avoid a scan or sortO(n + range)Part 6 §7
Linear-time sorting
Grokking advanced
Small fixed value range: counting or bucket sort beats O(n log n)O(n + k)Part 6 §7
Simulation
Grokking advanced
Follow the described process step by step; the work is in clean state handlingvariesSummary here
Bit manipulation / XOR
Grokking + AlgoArk
Every item twice but one; power of two; set bits; bitmask subsetsO(n) or O(1); O(1)This part §12
Searching, sorting and heaps9 patterns, 4 popular
PatternClue and typical problemsTime; spacePopularWhere
Binary search (and on the answer)
Grokking + AlgoArk
Sorted data, first/last position, smallest value that works (monotonic check)O(log n) or O(n log M)★This part §7
Modified binary search
Grokking
Rotated array, boundaries, peak element: decide which half still holds the answerO(log n); O(1)★Part 6 §3
Merge intervals
Grokking + AlgoArk
Overlaps, insert, meeting rooms: sort by start, then sweepO(n log n); O(n)★This part §9
Top K elements
Grokking + AlgoArk
k largest, smallest, most frequent, closest. Heap of size k, or bucketsO(n log k)★This part §10
Two heaps
Grokking + AlgoArk
Running median, split data into a low half (max-heap) and a high half (min-heap); rebalance sizes after each insertO(log n) per insert; O(n)Summary here
K-way merge
Grokking + AlgoArk
Merge k sorted lists/arrays: a min-heap holds the current head of each list; pop the smallest, push its successorO(N log k); O(k)Summary here
Ordered set / sorted map
Grokking
Sorted order with fast insert, delete, floor/ceiling (Kotlin: java.util.TreeMap/TreeSet)O(log n) per opPart 4 §5
Meet in the middle
Grokking advanced
Search space 2^n too big: solve each half (2^(n/2)) and combineO(2^(n/2))Summary here
Mo's algorithm
Grokking advanced
Many offline range queries on a static array: sort queries by block, move two pointers between themO((n + q) sqrt n)Summary here
Linked lists and stacks5 patterns, 4 popular
PatternClue and typical problemsTime; spacePopularWhere
Fast and slow pointers
Grokking + AlgoArk
Middle of a list, cycle detection, sequences that may loopO(n); O(1)★This part §3
In-place linked list reversal
Grokking + AlgoArk
Reverse a list, a sublist or groups of k by rewiring next pointersO(n); O(1)★Part 3 §3
Stack
Grokking
Valid parentheses, nesting, undo, evaluate expressionsO(n); O(n)★Part 3 §5
Monotonic stack
Grokking + AlgoArk
Next greater/smaller, days until, histogram rectangleO(n); O(n)★This part §8
Monotonic queue
Grokking advanced
Sliding window maximum/minimum: a deque of indexes whose values decrease; drop the front when it leaves the windowO(n); O(k)Summary here
Trees and graphs12 patterns, 4 popular
PatternClue and typical problemsTime; spacePopularWhere
Tree BFS (level order)
Grokking + AlgoArk
Output grouped by level, shortest path in an unweighted graphO(n); O(w)★Part 4 §3
Tree DFS
Grokking + AlgoArk
Root-to-leaf paths, path sum, subtree properties, any traversalO(n); O(h)★Part 4 §2
Graph traversal (BFS and DFS)
Grokking
Connectivity, shortest path in unweighted graphs, componentsO(V + E)★Part 5
Island (matrix traversal)
Grokking
2D grid: count or fill regions with BFS/DFS from each unvisited cellO(rows x cols)★Part 5 §5
Topological sort
Grokking + AlgoArk
Prerequisites, build order, dependencies in a DAGO(V + E)Part 5 §6
Union-find
Grokking + AlgoArk
Same-group checks, merge groups, cycle in an undirected graphnear O(1) per opPart 5 §8
Trie
Grokking + AlgoArk
Prefix search, autocomplete, word searchO(L) per wordPart 4 §8
Serialize and deserialize
Grokking advanced
Turn a tree/graph into a string and back (e.g. pre-order with null markers)O(n)Summary here
Clone
Grokking advanced
Deep copy a graph or a list with random pointers: map old node to new node, copy in two passes or with DFSO(n); O(n)Summary here
Articulation points and bridges
Grokking advanced
Which node or edge disconnects the graph: DFS with discovery time and low-link valuesO(V + E)Summary here
Segment tree
Grokking advanced
Range query (sum/min/max) with updates in betweenO(log n) per opSummary here
Binary indexed tree (Fenwick)
Grokking advanced
Prefix sums with point updates; shorter than a segment treeO(log n) per opSummary here
Recursion, greedy and dynamic programming10 patterns, 5 popular
PatternClue and typical problemsTime; spacePopularWhere
Subsets, permutations, combinations
Grokking
Generate every choice set; branch on include/excludeO(2^n) or O(n!)★Part 7 §3
Backtracking
Grokking + AlgoArk
N-Queens, Sudoku, word search: place, recurse, undo, pruneexponential, pruned★Part 7 §3
Greedy
Grokking + AlgoArk
Scheduling, activity selection, jump game, min coins (when the greedy choice is safe)O(n log n) or O(n)★Part 7 §4
Fibonacci-style DP
Grokking + AlgoArk
Each state depends on the previous one or two: climbing stairs, house robberO(n); O(1)★Part 7 §6
0/1 knapsack
Grokking + AlgoArk
Take or skip each item under a capacity: subset sum, partitionO(n x W); O(W)★Summary here
Unbounded knapsack
AlgoArk
Items reusable without limit: coin change, rod cuttingO(n x W); O(W)Part 7 §7
Longest common subsequence
AlgoArk
Two strings, 2D table over prefixes: LCS, edit distanceO(m x n)Part 7 §8
Palindromic subsequence / substring
Grokking
Longest palindrome: expand around the centre, or DP over [i, j] rangesO(n^2)Summary here
Matrix / grid DP
AlgoArk
Each cell from its neighbours: min path cost, unique paths with obstaclesO(m x n)Part 7 §8
DP on trees
AlgoArk
Post-order DFS where a node combines its children: tree diameter, house robber IIIO(n); O(n)Summary here
Concurrency1 patterns, 0 popular
PatternClue and typical problemsTime; spacePopularWhere
Multi-threaded patterns
Grokking
Alternate output between threads, shared state, producer-consumer. Tools: locks, semaphores, BlockingQueue, coroutinesdependsSummary here

How to use this. Start with the popular rows. If you can name the clue, the idea and the cost of each one without looking, you are ready for most problems. Learn the rest only when a problem shows you the clue.

15. Takeaways

TermIn plain words
PatternA reusable recipe for a family of problems.
Pointer (index)A number that marks a position in an array or string.
In placeChanging the input itself, using only a few extra variables.
Subarray / substringA run of consecutive items of an array or string.
WindowThe stretch of items between a left edge and a right edge.
Prefix sumThe total of all items before a position, stored so range sums are one subtraction.
MonotonicOnly going one way: always rising or always falling.
Monotonic stackA stack kept in sorted order by popping items that break the order.
IntervalA pair (start, end) describing a stretch of time or positions.
SweepWalking once through sorted events and keeping a running count.
Min-heapA structure that always gives back its smallest item first.
BitmaskAn integer used as a row of yes/no flags, one per bit.
XORA bit operation that is 1 where two bits differ; equal values cancel to 0.
← Back to all writing