Data Structures, Algorithms and Programming Patterns · Part 8 of 9
Data Structures and Algorithms, Part 8: Programming Patterns for Solving Problems
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.
- 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 intoO(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
kand 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
- Why patterns? Recognise the shape, reuse the recipe
- Two pointers
- Fast and slow pointers
- Sliding window
- Prefix sums
- Hash map as memory (a recap)
- Binary search on the answer
- Monotonic stack
- Intervals
- Top-k with a heap
- In-place tricks: Dutch flag and cyclic sort
- Bit manipulation basics
- Which pattern? A one-page decision table
- Complete pattern catalogue
- 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:
- The clue. The words in a problem that suggest the pattern.
- A picture. What the pointers, window or stack look like.
- The naive way and its cost. Usually brute force from Part 1.
- The pattern and its cost. Often
O(n²)down toO(n). - 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 data is sorted, or you are free to sort it, and you want a pair or a triple.
- You need to work from both ends toward the middle (reverse, palindrome).
- You must change an array in place, with no extra array (remove duplicates, move zeros).
The picture
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.
| Problem | Naive | Two pointers |
|---|---|---|
| Pair with sum (sorted) | O(n²) | O(n), space O(1) |
| Reverse in place | copy into a new array, O(n) space | O(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 water | O(n²) pairs | O(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
- “Find the middle of a linked list” in one pass.
- “Does this list or sequence contain a cycle (loop)?”
- A value is repeatedly replaced by a function of itself, and you wonder if it ends or loops (the “happy number” idea).
The picture
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 words contiguous, subarray or substring.
- “The best (longest, shortest, largest) stretch such that…”
- A fixed size
k(fixed window), or a rule the stretch must satisfy (variable window).
The picture
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
- Fixed size: build the first window, then slide it one step at a time.
- Variable size: a
forloop moves the right edge. After each step, if the window breaks the rule, move the left edge until it is fine again (“shrink”), or if it satisfies the rule, record the answer and shrink to look for a better one.
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
- Many range sum questions on data that does not change.
- “How many subarrays have sum equal to
k?” (especially when numbers can be negative, so a sliding window will not work). - A grid and many rectangle-sum questions (the 2D version).
The picture
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.
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
- You catch yourself scanning the same data again and again to look something up.
- Questions about counting, duplicates, pairs or grouping.
- Order does not matter, only membership.
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.
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 question asks for the smallest or largest value that still works: minimum capacity, slowest speed, smallest time.
- You can write a quick yes/no check: “does this value work?”
- The check is monotonic: if a value works, every bigger value also works (for “smallest” problems). So the answers look like
no no no yes yes yes.
The picture
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
- “For each item, find the next greater (or smaller) item to the right or left.”
- “How many days until…”, “how far until a bigger value”.
- Bars or buildings, and the biggest rectangle, or the span an item controls.
The picture
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
- Pairs of
(start, end): meetings, bookings, ranges, schedules. - “Merge overlapping…”, “insert a new…”, “how many at the same time?”, “do any conflict?”
The picture
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 k largest / smallest…”, “the k most frequent…”, “the k-th largest”.
- You do not need everything sorted, only the best few.
- The data may be a stream, too big to keep all of it.
The picture
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.
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.
| Operation | Kotlin | What it does |
|---|---|---|
| AND | a and b | 1 where both bits are 1 |
| OR | a or b | 1 where either bit is 1 |
| XOR | a xor b | 1 where the bits differ |
| Shift left | a shl 1 | moves bits left, doubles the number |
| Shift right | a shr 1 | moves bits right, halves the number |
| Read bit i | (a shr i) and 1 | is bit number i set? |
The clue
- “Every item appears twice except one”: XOR cancels pairs.
- “Is it a power of two?” or “count the set bits”.
- A small set of yes/no options, stored as one number (a bitmask), for example all subsets of 10 items as the numbers 0 to 1023.
The picture
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… | Try | Typical cost |
|---|---|---|
| Sorted array, find a pair or triple; reverse; palindrome | Two pointers (opposite ends) | O(n) time, O(1) space |
| Edit an array in place, keep or remove items | Two pointers (read and write) | O(n), O(1) |
| Middle of a linked list; cycle; a sequence that may loop | Fast and slow pointers | O(n), O(1) |
Contiguous subarray or substring of size k | Fixed sliding window | O(n) |
| Longest or shortest stretch satisfying a rule (non-negative data) | Variable sliding window | O(n) |
Many range sums; subarray sum equals k (negatives allowed) | Prefix sums, plus a hash map | O(n) build, O(1) query |
| Seen before? Count? Group? Pair that sums to a target (unsorted) | Hash map or set | O(n), O(n) space |
| Smallest value that still works; a yes/no check that is monotonic | Binary search on the answer | O(n log M) |
| Next greater or smaller; how many days until; histogram bars | Monotonic stack | O(n) |
| Pairs of (start, end); merge, insert, overlap, rooms | Sort by start, then sweep | O(n log n) |
| k largest, k smallest, k most frequent, k-th largest | Heap of size k | O(n log k) |
Few distinct values to group; numbers in 1..n with a gap or duplicate | Dutch flag; cyclic sort | O(n), O(1) |
| Every item twice but one; power of two; set bits; subsets as flags | Bit manipulation (XOR, mask) | O(n) or O(1) |
| All combinations, choices, paths; or a best value with repeated sub-problems | Recursion, backtracking, DP (Part 7) | depends |
| Shortest path, connected parts, dependencies | Graph 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.
- Given a sorted list of prices, find two that add to exactly 50.
- Given a text, what is the longest piece that contains at most two different letters?
- Given a list of readings that can be negative, how many consecutive stretches sum to zero?
- Given the daily stock prices, for each day, how many days to wait for a higher price?
- Given a stream of 10 million scores, keep the top 10 at all times.
- Given appointment slots, can one person attend all of them?
- Given a function that tells whether a time limit of
tminutes is enough, find the smallestt. - Given a list where every number appears three times except one that appears once, find it without a map.
Show the answers
- Two pointers from both ends.
- Variable sliding window with a small map of letter counts.
- Prefix sums with a hash map (negatives rule out a plain window).
- Monotonic stack (the daily temperatures shape).
- A min-heap of size 10 (top-k).
- Intervals: sort by start and check that each start is not before the previous end.
- Binary search on the answer.
- 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
| Pattern | Clue and typical problems | Time; space | Popular | Where |
|---|---|---|---|---|
| Two pointers Grokking + AlgoArk | Sorted input, pair or triple, both ends, in-place edits | O(n); O(1) | ★ | This part §2 |
| Sliding window Grokking + AlgoArk | Longest/shortest/fixed-size contiguous subarray or substring | O(n); O(1) or O(k) | ★ | This part §4 |
| Prefix sum Grokking + AlgoArk | Many range sums; subarray sum equals k | O(n) build, O(1) query | ★ | This part §5 |
| Hash map counting / grouping Grokking + AlgoArk | Frequency, seen-before, grouping, anagrams | O(n); O(n) | ★ | This part §6 |
| Two Sum (complement lookup) AlgoArk | Unsorted data: store what you have seen, look up target - x | O(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 numbers | O(n); O(1) | This part §11 | |
| Dutch national flag extra | Group a few distinct values (0/1/2) in one pass | O(n); O(1) | This part §11 | |
| Counting Grokking advanced | Tally or bucket by value to avoid a scan or sort | O(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 handling | varies | Summary here | |
| Bit manipulation / XOR Grokking + AlgoArk | Every item twice but one; power of two; set bits; bitmask subsets | O(n) or O(1); O(1) | This part §12 |
Searching, sorting and heaps9 patterns, 4 popular
| Pattern | Clue and typical problems | Time; space | Popular | Where |
|---|---|---|---|---|
| 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 answer | O(log n); O(1) | ★ | Part 6 §3 |
| Merge intervals Grokking + AlgoArk | Overlaps, insert, meeting rooms: sort by start, then sweep | O(n log n); O(n) | ★ | This part §9 |
| Top K elements Grokking + AlgoArk | k largest, smallest, most frequent, closest. Heap of size k, or buckets | O(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 insert | O(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 successor | O(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 op | Part 4 §5 | |
| Meet in the middle Grokking advanced | Search space 2^n too big: solve each half (2^(n/2)) and combine | O(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 them | O((n + q) sqrt n) | Summary here |
Linked lists and stacks5 patterns, 4 popular
| Pattern | Clue and typical problems | Time; space | Popular | Where |
|---|---|---|---|---|
| Fast and slow pointers Grokking + AlgoArk | Middle of a list, cycle detection, sequences that may loop | O(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 pointers | O(n); O(1) | ★ | Part 3 §3 |
| Stack Grokking | Valid parentheses, nesting, undo, evaluate expressions | O(n); O(n) | ★ | Part 3 §5 |
| Monotonic stack Grokking + AlgoArk | Next greater/smaller, days until, histogram rectangle | O(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 window | O(n); O(k) | Summary here |
Trees and graphs12 patterns, 4 popular
| Pattern | Clue and typical problems | Time; space | Popular | Where |
|---|---|---|---|---|
| Tree BFS (level order) Grokking + AlgoArk | Output grouped by level, shortest path in an unweighted graph | O(n); O(w) | ★ | Part 4 §3 |
| Tree DFS Grokking + AlgoArk | Root-to-leaf paths, path sum, subtree properties, any traversal | O(n); O(h) | ★ | Part 4 §2 |
| Graph traversal (BFS and DFS) Grokking | Connectivity, shortest path in unweighted graphs, components | O(V + E) | ★ | Part 5 |
| Island (matrix traversal) Grokking | 2D grid: count or fill regions with BFS/DFS from each unvisited cell | O(rows x cols) | ★ | Part 5 §5 |
| Topological sort Grokking + AlgoArk | Prerequisites, build order, dependencies in a DAG | O(V + E) | Part 5 §6 | |
| Union-find Grokking + AlgoArk | Same-group checks, merge groups, cycle in an undirected graph | near O(1) per op | Part 5 §8 | |
| Trie Grokking + AlgoArk | Prefix search, autocomplete, word search | O(L) per word | Part 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 DFS | O(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 values | O(V + E) | Summary here | |
| Segment tree Grokking advanced | Range query (sum/min/max) with updates in between | O(log n) per op | Summary here | |
| Binary indexed tree (Fenwick) Grokking advanced | Prefix sums with point updates; shorter than a segment tree | O(log n) per op | Summary here |
Recursion, greedy and dynamic programming10 patterns, 5 popular
| Pattern | Clue and typical problems | Time; space | Popular | Where |
|---|---|---|---|---|
| Subsets, permutations, combinations Grokking | Generate every choice set; branch on include/exclude | O(2^n) or O(n!) | ★ | Part 7 §3 |
| Backtracking Grokking + AlgoArk | N-Queens, Sudoku, word search: place, recurse, undo, prune | exponential, 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 robber | O(n); O(1) | ★ | Part 7 §6 |
| 0/1 knapsack Grokking + AlgoArk | Take or skip each item under a capacity: subset sum, partition | O(n x W); O(W) | ★ | Summary here |
| Unbounded knapsack AlgoArk | Items reusable without limit: coin change, rod cutting | O(n x W); O(W) | Part 7 §7 | |
| Longest common subsequence AlgoArk | Two strings, 2D table over prefixes: LCS, edit distance | O(m x n) | Part 7 §8 | |
| Palindromic subsequence / substring Grokking | Longest palindrome: expand around the centre, or DP over [i, j] ranges | O(n^2) | Summary here | |
| Matrix / grid DP AlgoArk | Each cell from its neighbours: min path cost, unique paths with obstacles | O(m x n) | Part 7 §8 | |
| DP on trees AlgoArk | Post-order DFS where a node combines its children: tree diameter, house robber III | O(n); O(n) | Summary here |
Concurrency1 patterns, 0 popular
| Pattern | Clue and typical problems | Time; space | Popular | Where |
|---|---|---|---|---|
| Multi-threaded patterns Grokking | Alternate output between threads, shared state, producer-consumer. Tools: locks, semaphores, BlockingQueue, coroutines | depends | Summary 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
- Learn to read the clue: sorted and pair, contiguous stretch, range sum, smallest value that works, next greater, start and end pairs, best k.
- Write the naive version first; the repeated work in it usually names the pattern.
- Two edges that only move forward (two pointers, windows, stacks) give linear time even when a loop sits inside a loop.
- Spend memory to save time: prefix arrays, hash maps and stacks all do this.
- Test the edges: empty input, one item, all items equal, and no valid answer.
- Next up: Part 9 puts everything together into a study path and a quicksheet.
| Term | In plain words |
|---|---|
| Pattern | A reusable recipe for a family of problems. |
| Pointer (index) | A number that marks a position in an array or string. |
| In place | Changing the input itself, using only a few extra variables. |
| Subarray / substring | A run of consecutive items of an array or string. |
| Window | The stretch of items between a left edge and a right edge. |
| Prefix sum | The total of all items before a position, stored so range sums are one subtraction. |
| Monotonic | Only going one way: always rising or always falling. |
| Monotonic stack | A stack kept in sorted order by popping items that break the order. |
| Interval | A pair (start, end) describing a stretch of time or positions. |
| Sweep | Walking once through sorted events and keeping a running count. |
| Min-heap | A structure that always gives back its smallest item first. |
| Bitmask | An integer used as a row of yes/no flags, one per bit. |
| XOR | A bit operation that is 1 where two bits differ; equal values cancel to 0. |