Free Handbook · Every example compiled & verified

Data Structures & Algorithms

Stacks, queues, linked lists, trees, heaps and graphs in Kotlin — built by hand, then with the stdlib — plus binary search, sorting and DP.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 13 · what you'll be able to do

  • Choose a Kotlin collection from its cost table instead of by habit
  • Build a stack, linked list, binary search tree and min-heap by hand, then swap in ArrayDeque, TreeMap and PriorityQueue
  • Count, group and de-duplicate with mutableMapOf, getOrPut, groupingBy and HashSet
  • Walk a graph with BFS and DFS and write binary search without an off-by-one
  • Sort with sortedBy and sortedWith(compareBy … thenBy) and turn slow recursion into dynamic programming
01

The cost table, and lists

A data structure is a promise about which operations are cheap. Kotlin gives you most of them ready-made — some from the Kotlin standard library (listOf, mutableMapOf, ArrayDeque) and some straight from the JDK (java.util.PriorityQueue, java.util.TreeMap), because Kotlin on the JVM can use every Java class directly. Knowing the cost of each operation is what lets you pick the right one, and it is exactly what interviewers probe.

Average-case costs. "Amortised" means an occasional expensive resize is spread across many cheap appends.
Structure (Kotlin type)AccessSearchInsertDelete
MutableList / ArrayListO(1) by indexO(n)O(1) amortised at end, O(n) at frontO(1) at end, O(n) at front
ArrayDeque (stack or queue)O(1) either endO(n)O(1) either endO(1) either end
Linked list (hand-built)O(n)O(n)O(1) at head / known nodeO(1) at head / known node
HashMap / HashSet / mutableMapOf—O(1) averageO(1) averageO(1) average
TreeMap / sortedSetOf—O(log n)O(log n)O(log n)
Binary search tree (hand-built)—O(log n) balanced, O(n) worstO(log n) balancedO(log n) balanced
PriorityQueue (binary heap)O(1) min onlyO(n)O(log n)O(log n) remove min
Graph (adjacency list)—O(V + E) with BFS/DFSO(1) add edgeO(degree) remove edge

listOf gives a read-only List; mutableListOf gives a MutableList backed by a JVM ArrayList — a resizable array. Reading by index is instant, appending at the end is cheap, but inserting at index 0 shifts every element one slot to the right. arrayOf / IntArray are fixed-size JVM arrays: use IntArray for large numeric work because it stores raw ints instead of boxed Integer objects.

kotlinMain.kt
fun main() {
    val nums = mutableListOf(5, 3, 8)
    nums.add(1)          // append: O(1) amortised
    nums.add(0, 9)       // insert at front: shifts everything, O(n)
    println(nums)
    println(nums[2])     // index access: O(1)
    nums.removeAt(0)
    println(nums)
    println(nums.size)

    val squares = IntArray(5) { i -> i * i }   // fixed size, unboxed ints
    println(squares.joinToString())
    println(squares.sum())
}
Outputcompiled & run with real Kotlin
[9, 5, 3, 8, 1]
3
[5, 3, 8, 1]
4
0, 1, 4, 9, 16
30
Your turn

Time the difference yourself: add 100,000 numbers with add(x), then another 100,000 with add(0, x), measuring each loop with kotlin.system.measureTimeMillis.

02

Hash maps and sets

A hash map turns a key into an array slot with hashCode(), so lookup, insert and delete are O(1) on average. In Kotlin, mutableMapOf() returns a LinkedHashMap, which also remembers insertion order — printing it is predictable. HashMap() skips that bookkeeping and iterates in hash order, which you should never rely on. map[key] returns V?: a missing key is null, not an exception.

kotlinMain.kt
fun main() {
    val text = "the cat and the hat and the bat"
    val counts = mutableMapOf<String, Int>()   // LinkedHashMap: insertion order
    for (word in text.split(" ")) {
        counts[word] = counts.getOrDefault(word, 0) + 1
    }
    println(counts)
    println(counts["the"])
    println(counts["dog"])
    println("cat" in counts)

    val seen = HashSet<Int>()
    val dupes = mutableListOf<Int>()
    for (n in listOf(4, 1, 4, 7, 1, 9)) {
        if (!seen.add(n)) dupes.add(n)    // add() returns false if already present
    }
    println(dupes)
    println(seen.sorted())
}
Outputcompiled & run with real Kotlin
{the=3, cat=1, and=2, hat=1, bat=1}
3
null
true
[4, 1]
[1, 4, 7, 9]

seen.sorted() is deliberate: a HashSet has no defined order, so sort before you print or compare.

Two idioms replace most hand-written counting and grouping loops. getOrPut(key) { default } returns the existing value or inserts the default first — perfect for "map of lists". groupingBy { … }.eachCount() counts in one line.

kotlinMain.kt
fun main() {
    val words = listOf("eat", "tea", "tan", "ate", "nat", "bat")
    val groups = LinkedHashMap<String, MutableList<String>>()
    for (w in words) {
        val key = w.toCharArray().sorted().joinToString("")
        groups.getOrPut(key) { mutableListOf() }.add(w)
    }
    println(groups)
    println(groups.values.map { it.size })
    println(words.groupingBy { it.first() }.eachCount())
}
Outputcompiled & run with real Kotlin
{aet=[eat, tea, ate], ant=[tan, nat], abt=[bat]}
[3, 2, 1]
{e=1, t=2, a=1, n=1, b=1}
Your turn

Rewrite the anagram grouping with words.groupBy { … } and check you get the same map.

Keys must be stable
A key's hashCode() must not change while it is in the map. Data classes generate hashCode from their constructor properties — if one of those is a var you mutate after inserting, the map can no longer find the entry. Use val properties (or immutable types like String) as keys.
03

Stacks: last in, first out

A stack is a pile: push onto the top, pop from the top. Undo history, the call stack, bracket matching and depth-first search are all stacks. Building one by hand takes a few lines on top of a MutableList — the generic <T> makes it work for any element type.

kotlinMain.kt
class Stack<T> {
    private val items = mutableListOf<T>()

    fun push(item: T) { items.add(item) }

    fun pop(): T {
        check(items.isNotEmpty()) { "pop from empty stack" }
        return items.removeAt(items.lastIndex)
    }

    fun peek(): T? = items.lastOrNull()
    fun isEmpty() = items.isEmpty()
    val size: Int get() = items.size
}

fun main() {
    val s = Stack<Int>()
    s.push(10); s.push(20); s.push(30)
    println("${s.pop()} ${s.size} ${s.peek()}")
    val words = Stack<String>()
    println(words.peek())
    println(words.isEmpty())
}
Outputcompiled & run with real Kotlin
30 2 20
null
true

pop() throws on an empty stack while peek() returns null — a deliberate choice: popping nothing is a bug, peeking at nothing is a normal question.

In real code, use kotlin.collections.ArrayDeque as the stack: addLast to push, removeLast (or removeLastOrNull) to pop, last() to peek. Avoid java.util.Stack — it is a synchronised legacy class from Java 1.0.

kotlinMain.kt
fun isBalanced(s: String): Boolean {
    val pairs = mapOf(')' to '(', ']' to '[', '}' to '{')
    val stack = ArrayDeque<Char>()
    for (c in s) {
        when (c) {
            '(', '[', '{' -> stack.addLast(c)
            ')', ']', '}' -> if (stack.removeLastOrNull() != pairs[c]) return false
        }
    }
    return stack.isEmpty()
}

fun main() {
    for (s in listOf("([]{})", "([)]", "((", "a(b)c")) {
        println("$s -> ${isBalanced(s)}")
    }
}
Outputcompiled & run with real Kotlin
([]{}) -> true
([)] -> false
(( -> false
a(b)c -> true
Your turn

Make isBalanced also report where it failed: return the index of the first bad character, or -1 when balanced.

VisualizeisBalanced("([)]")Step 1 / 5
val stack = ArrayDeque<Char>()
for (c in s) {
when (c) {
'(', '[', '{' -> stack.addLast(c)
')', ']', '}' -> if (stack.removeLastOrNull() != pairs[c]) return false
}
}
return stack.isEmpty()
Line 1

Start with an empty stack.

Variables now
stack[]
All 5 steps as a table
StepLineWhat happenedVariables now
11Start with an empty stack.stack = []
24c = '(' is an opener — push it.c = ( stack = [(]
34c = '[' is an opener — push it.c = [ stack = [(, []
45c = ')' — pop [, but ) needs (. Mismatch.c = ) popped = [ stack = [(]
55Return false immediately; the last ] is never read.
04

Queues and deques with ArrayDeque

A queue is first in, first out: join at the back, leave from the front — print jobs, message processing, breadth-first search. Using a MutableList and removeAt(0) is a classic performance bug, because every removal shifts the whole list. ArrayDeque is a circular buffer: it adds and removes at both ends in O(1), so the same class is your stack, your queue and your double-ended queue.

kotlinMain.kt
fun main() {
    val queue = ArrayDeque<String>()
    queue.addLast("ana")
    queue.addLast("ben")
    queue.addLast("cy")
    println(queue.removeFirst())   // first in, first out
    queue.addLast("dee")
    println(queue)
    println(queue.first())         // peek at the front

    val deque = ArrayDeque(listOf(2, 3))
    deque.addFirst(1)
    deque.addLast(4)
    println(deque)
    println("${deque.removeFirst()} ${deque.removeLast()} $deque")
}
Outputcompiled & run with real Kotlin
ana
[ben, cy, dee]
ben
[1, 2, 3, 4]
1 4 [2, 3]

Kotlin ArrayDeque

  • kotlin.collections.ArrayDeque — no import needed
  • addFirst/addLast, removeFirst/removeLast, first()/last()
  • removeFirstOrNull() returns null when empty
  • Also a MutableList: indexing works

java.util.ArrayDeque / LinkedList

  • Need import java.util.ArrayDeque — same simple name, easy to mix up
  • offer/poll/peek (Queue) and push/pop (Deque)
  • poll() returns null (a platform type)
  • Seen in Java codebases and older Android code; fine to use, just be consistent
05

A linked list built by hand

A linked list stores each value in a node that points to the next one. Adding at the head is O(1) with no shifting, but reaching the 500th element means following 500 pointers. You will almost never use one in production Kotlin — ArrayDeque beats it on real hardware — but interviewers love them because they test whether you can handle null references without crashing. Kotlin's null safety makes the next: Node<T>? explicit.

kotlinMain.kt
class Node<T>(val value: T, var next: Node<T>? = null)

class LinkedList<T> {
    private var head: Node<T>? = null
    private var tail: Node<T>? = null
    var size = 0
        private set

    fun addFirst(value: T) {
        val node = Node(value, head)
        head = node
        if (tail == null) tail = node
        size++
    }

    fun addLast(value: T) {
        val node = Node(value)
        val t = tail
        if (t == null) head = node else t.next = node
        tail = node
        size++
    }

    fun reverse() {
        tail = head
        var prev: Node<T>? = null
        var cur = head
        while (cur != null) {
            val next = cur.next
            cur.next = prev
            prev = cur
            cur = next
        }
        head = prev
    }

    override fun toString(): String {
        val parts = mutableListOf<String>()
        var cur = head
        while (cur != null) { parts.add(cur.value.toString()); cur = cur.next }
        return parts.joinToString(" -> ", postfix = " -> null")
    }
}

fun main() {
    val list = LinkedList<Int>()
    list.addLast(2); list.addLast(3); list.addFirst(1)
    println(list)
    println(list.size)
    list.reverse()
    println(list)
    list.addLast(0)
    println(list)
}
Outputcompiled & run with real Kotlin
1 -> 2 -> 3 -> null
3
3 -> 2 -> 1 -> null
3 -> 2 -> 1 -> 0 -> null

val t = tail copies the property into a local so the compiler can smart-cast it — a mutable property could change between the null check and the use.

Visualizereverse() on 1 -> 2 -> 3Step 1 / 8
var prev: Node<T>? = null
var cur = head
while (cur != null) {
val next = cur.next
cur.next = prev
prev = cur
cur = next
}
head = prev
Line 2

Start at the head with nothing behind us.

Variables now
prevnull
cur1
All 8 steps as a table
StepLineWhat happenedVariables now
12Start at the head with nothing behind us.prev = null cur = 1
25Save next = 2, point node 1 backwards at null.next = 2 1.next = null
37Step forward: 1 becomes prev, 2 becomes cur.prev = 1 cur = 2
45Save next = 3, point node 2 back at 1.next = 3 2.next = 1
57Step forward.prev = 2 cur = 3
65Save next = null, point node 3 back at 2.next = null 3.next = 2
77Step forward; cur is now null so the loop ends.prev = 3 cur = null
89The old tail is the new head: 3 -> 2 -> 1 -> null.head = 3
06

Recursion and tailrec

A recursive function calls itself on a smaller input until it hits a base case. Every call takes a frame on the JVM stack, so very deep recursion (tens of thousands of levels) throws StackOverflowError. Kotlin's tailrec modifier fixes that for one shape: when the recursive call is the very last thing the function does, the compiler rewrites it into a loop.

kotlinMain.kt
fun factorial(n: Int): Long = if (n <= 1) 1 else n * factorial(n - 1)

tailrec fun gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

// fast power: halve the exponent each step -> O(log n) calls
fun power(base: Long, exp: Int): Long = when {
    exp == 0 -> 1
    exp % 2 == 0 -> { val half = power(base, exp / 2); half * half }
    else -> base * power(base, exp - 1)
}

fun main() {
    println(factorial(5))
    println(factorial(20))
    println(gcd(48, 18))
    println(power(2, 10))
    println(power(3, 13))
}
Outputcompiled & run with real Kotlin
120
2432902008176640000
6
1024
1594323

factorial returns Long because 13! already overflows an Int. factorial is not tail-recursive (the multiply happens after the call returns), so tailrec would be refused with a warning.

Visualizefactorial(4)Step 1 / 5
fun factorial(n: Int): Long = if (n <= 1) 1 else n * factorial(n - 1)
Line 1

factorial(4) needs factorial(3) first — frame waits.

Variables now
n4
All 5 steps as a table
StepLineWhat happenedVariables now
11factorial(4) needs factorial(3) first — frame waits.n = 4
21factorial(3) needs factorial(2).n = 3
31factorial(2) needs factorial(1).n = 2
41Base case: n <= 1, return 1.n = 1 returns = 1
51Unwind: 2 * 1 = 2, then 3 * 2 = 6, then 4 * 6 = 24.returns = 24
07

Binary search trees, then TreeMap

A binary search tree keeps every key in the left subtree smaller than the node and every key in the right subtree larger. Search follows one path from the root, so it is O(log n) when the tree is balanced — and O(n) if you insert already-sorted keys and it degenerates into a linked list. An in-order walk (left, node, right) visits the keys in sorted order.

kotlinMain.kt
class BST {
    private class Node(val key: Int) {
        var left: Node? = null
        var right: Node? = null
    }

    private var root: Node? = null

    fun insert(key: Int) { root = insert(root, key) }

    private fun insert(node: Node?, key: Int): Node {
        if (node == null) return Node(key)
        if (key < node.key) node.left = insert(node.left, key)
        else if (key > node.key) node.right = insert(node.right, key)
        return node   // duplicates are ignored
    }

    fun contains(key: Int): Boolean {
        var cur = root
        while (cur != null) {
            if (key == cur.key) return true
            cur = if (key < cur.key) cur.left else cur.right
        }
        return false
    }

    fun inOrder(): List<Int> {
        val out = mutableListOf<Int>()
        fun walk(n: Node?) {
            if (n == null) return
            walk(n.left); out.add(n.key); walk(n.right)
        }
        walk(root)
        return out
    }

    fun height(): Int {
        fun h(n: Node?): Int = if (n == null) 0 else 1 + maxOf(h(n.left), h(n.right))
        return h(root)
    }
}

fun main() {
    val tree = BST()
    for (k in listOf(50, 30, 70, 20, 40, 60, 80, 30)) tree.insert(k)
    println(tree.inOrder())
    println("${tree.contains(60)} ${tree.contains(65)}")
    println(tree.height())
}
Outputcompiled & run with real Kotlin
[20, 30, 40, 50, 60, 70, 80]
true false
3
Your turn

Insert 1, 2, 3, 4, 5, 6, 7 into a fresh tree and print height(). It prints 7 — the tree has become a linked list. That is why production code uses a self-balancing tree.

That self-balancing tree already exists: java.util.TreeMap (a red-black tree) and sortedSetOf(), which returns a java.util.TreeSet. Both keep keys sorted and answer "closest key" questions — floorKey, ceilingKey, headMap — that a hash map cannot.

kotlinMain.kt
import java.util.TreeMap

fun main() {
    val scores = TreeMap<String, Int>()
    scores["mia"] = 88
    scores["al"] = 72
    scores["zed"] = 95
    scores["kim"] = 81
    println(scores)                    // always sorted by key
    println(scores.firstKey() + " " + scores.lastKey())
    println(scores.headMap("l"))       // keys strictly before "l"
    println(scores.ceilingKey("b"))    // smallest key >= "b"

    val set = sortedSetOf(5, 1, 9, 3)
    println(set)
    println(set.higher(3))             // smallest element > 3
}
Outputcompiled & run with real Kotlin
{al=72, kim=81, mia=88, zed=95}
al zed
{al=72, kim=81}
kim
[1, 3, 5, 9]
5
08

Heaps and PriorityQueue

A min-heap always hands you the smallest element. It is a complete binary tree stored in a plain list: the children of index i sit at 2i + 1 and 2i + 2, the parent at (i - 1) / 2. Push appends and sifts up; pop moves the last element to the root and sifts down. Both are O(log n).

kotlinMain.kt
class MinHeap {
    private val a = mutableListOf<Int>()
    val size: Int get() = a.size

    fun push(x: Int) {
        a.add(x)
        var i = a.lastIndex
        while (i > 0) {
            val parent = (i - 1) / 2
            if (a[parent] <= a[i]) break
            a[parent] = a[i].also { a[i] = a[parent] }   // swap
            i = parent
        }
    }

    fun pop(): Int {
        val top = a[0]
        val last = a.removeAt(a.lastIndex)
        if (a.isNotEmpty()) {
            a[0] = last
            var i = 0
            while (true) {
                val l = 2 * i + 1
                val r = l + 1
                var smallest = i
                if (l < a.size && a[l] < a[smallest]) smallest = l
                if (r < a.size && a[r] < a[smallest]) smallest = r
                if (smallest == i) break
                a[i] = a[smallest].also { a[smallest] = a[i] }
                i = smallest
            }
        }
        return top
    }
}

fun main() {
    val heap = MinHeap()
    for (x in listOf(7, 2, 9, 4, 1)) heap.push(x)
    val out = mutableListOf<Int>()
    while (heap.size > 0) out.add(heap.pop())
    println(out)
}
Outputcompiled & run with real Kotlin
[1, 2, 4, 7, 9]

a[x] = a[y].also { a[y] = a[x] } is the Kotlin swap idiom: the right side is read first, then also overwrites the other slot.

In real code use java.util.PriorityQueue. It is a min-heap by default; pass a comparator for anything else — reverseOrder() for a max-heap, compareBy { … }.thenBy { … } for objects. The classic trick for "top k largest" is a min-heap capped at size k: the smallest of the current top k sits at the root, ready to be evicted.

kotlinMain.kt
import java.util.PriorityQueue

data class Task(val name: String, val priority: Int)

fun main() {
    val minHeap = PriorityQueue(listOf(7, 2, 9, 4, 1))
    println(minHeap.peek())

    val maxHeap = PriorityQueue<Int>(reverseOrder())
    maxHeap.addAll(listOf(7, 2, 9, 4, 1))
    println(maxHeap.poll())

    val tasks = PriorityQueue(compareBy<Task> { it.priority }.thenBy { it.name })
    tasks.add(Task("deploy", 2))
    tasks.add(Task("fix bug", 1))
    tasks.add(Task("docs", 3))
    tasks.add(Task("alert", 1))
    val order = mutableListOf<String>()
    while (tasks.isNotEmpty()) order.add(tasks.poll().name)
    println(order)

    val k = 3
    val top = PriorityQueue<Int>()
    for (x in listOf(5, 12, 3, 20, 8, 15)) {
        top.add(x)
        if (top.size > k) top.poll()   // evict the smallest
    }
    println(top.sortedDescending())
}
Outputcompiled & run with real Kotlin
1
9
[alert, fix bug, deploy, docs]
[20, 15, 12]

Printing a PriorityQueue directly shows its internal array order, not sorted order — always poll or sort before printing.

09

Graphs: BFS and DFS

A graph is nodes plus edges. The everyday representation is an adjacency list: Map<Node, List<Node>>. Breadth-first search uses a queue and visits nodes level by level, so the first time it reaches a node is along a shortest path (in edges). Depth-first search uses a stack — often the call stack via recursion — and dives down one branch before backtracking. Both are O(V + E). Both need a visited set, or a cycle loops forever.

kotlinMain.kt
val graph: Map<String, List<String>> = mapOf(
    "A" to listOf("B", "C"),
    "B" to listOf("D"),
    "C" to listOf("D", "E"),
    "D" to listOf("F"),
    "E" to listOf("F"),
    "F" to emptyList(),
)

fun bfs(start: String): List<String> {
    val visited = mutableSetOf(start)
    val queue = ArrayDeque(listOf(start))
    val order = mutableListOf<String>()
    while (queue.isNotEmpty()) {
        val node = queue.removeFirst()
        order.add(node)
        for (next in graph.getValue(node)) {
            if (visited.add(next)) queue.addLast(next)
        }
    }
    return order
}

fun dfs(node: String, visited: MutableSet<String> = mutableSetOf()): List<String> {
    if (!visited.add(node)) return emptyList()
    val order = mutableListOf(node)
    for (next in graph.getValue(node)) order += dfs(next, visited)
    return order
}

fun main() {
    println("BFS: ${bfs("A")}")
    println("DFS: ${dfs("A")}")
}
Outputcompiled & run with real Kotlin
BFS: [A, B, C, D, E, F]
DFS: [A, B, D, F, C, E]
Your turn

Add a parent map to bfs (set parent[next] = node when you enqueue) and rebuild the shortest path from A to F by walking parents backwards. You should get [A, B, D, F].

Visualizebfs("A")Step 1 / 6
val visited = mutableSetOf(start)
val queue = ArrayDeque(listOf(start))
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
order.add(node)
for (next in graph.getValue(node)) {
if (visited.add(next)) queue.addLast(next)
}
}
Line 2

Seed the queue with the start node and mark it visited.

Variables now
queue[A]
visited{A}
All 6 steps as a table
StepLineWhat happenedVariables now
12Seed the queue with the start node and mark it visited.queue = [A] visited = {A}
27Take A; enqueue its unvisited neighbours B and C.queue = [B, C] order = [A]
37Take B; enqueue D.queue = [C, D] order = [A, B]
47Take C; D is already visited (add returns false), enqueue E.queue = [D, E] order = [A, B, C]
57Take D; enqueue F.queue = [E, F] order = [A, B, C, D]
65Take E (F already visited), then F. Queue empty — done.queue = [] order = [A, B, C, D, E, F]
11

Sorting: merge sort, then sortedBy and sortedWith

Merge sort splits the list in half, sorts each half recursively and merges the two sorted halves — always O(n log n), and stable (equal elements keep their original order). Writing it once teaches divide-and-conquer; after that, use the stdlib.

kotlinMain.kt
fun mergeSort(xs: List<Int>): List<Int> {
    if (xs.size <= 1) return xs
    val mid = xs.size / 2
    val left = mergeSort(xs.subList(0, mid))
    val right = mergeSort(xs.subList(mid, xs.size))
    val out = ArrayList<Int>(xs.size)
    var i = 0
    var j = 0
    while (i < left.size && j < right.size) {
        if (left[i] <= right[j]) out.add(left[i++]) else out.add(right[j++])
    }
    while (i < left.size) out.add(left[i++])
    while (j < right.size) out.add(right[j++])
    return out
}

fun main() {
    println(mergeSort(listOf(38, 27, 43, 3, 9, 82, 10)))
    println(mergeSort(emptyList()))
}
Outputcompiled & run with real Kotlin
[3, 9, 10, 27, 38, 43, 82]
[]

The stdlib splits sorting into two families. sorted(), sortedBy { }, sortedDescending() and sortedWith(comparator) return a new list. sort(), sortBy { } and sortWith() sort a MutableList in place. Build multi-key comparators with compareBy { }.thenBy { }.thenByDescending { }. Object sorts are stable (the JDK uses TimSort).

kotlinMain.kt
data class Dev(val name: String, val lang: String, val years: Int)

fun main() {
    val devs = listOf(
        Dev("Ravi", "Kotlin", 5), Dev("Ana", "Java", 8),
        Dev("Li", "Kotlin", 2), Dev("Sam", "Java", 3), Dev("Zoe", "Kotlin", 5),
    )
    println(listOf(5, 2, 9, 1).sorted())
    println(listOf(5, 2, 9, 1).sortedDescending())
    println(devs.sortedBy { it.years }.map { it.name })
    val byLangThenSenior = compareBy<Dev> { it.lang }.thenByDescending { it.years }.thenBy { it.name }
    println(devs.sortedWith(byLangThenSenior).map { it.name })

    val fruit = mutableListOf("pear", "fig", "banana")
    fruit.sortBy { it.length }     // in place, returns Unit
    println(fruit)
}
Outputcompiled & run with real Kotlin
[1, 2, 5, 9]
[9, 5, 2, 1]
[Li, Sam, Ravi, Zoe, Ana]
[Ana, Sam, Ravi, Zoe, Li]
[fig, pear, banana]

Ravi and Zoe both have 5 years: sortedBy is stable, so Ravi stays ahead of Zoe exactly as in the input.

AlgorithmBestAverageWorstStable?Where you meet it
Bubble / insertion sortO(n)O(n²)O(n²)YesInterviews; insertion sort inside TimSort for tiny runs
Merge sortO(n log n)O(n log n)O(n log n)YesThe idea behind TimSort (sortedBy on objects)
Quicksort (dual-pivot)O(n log n)O(n log n)O(n²)NoIntArray.sort() on primitives
Heap sortO(n log n)O(n log n)O(n log n)NoDraining a PriorityQueue
12

Dynamic programming

Dynamic programming is recursion that remembers. If a problem breaks into sub-problems that overlap — naive fib(50) recomputes fib(2) billions of times — store each answer once. Top-down keeps the recursion and adds a memo map; bottom-up fills a table from the smallest case upward, often in O(1) extra space.

kotlinMain.kt
val memo = HashMap<Int, Long>()

fun fib(n: Int): Long {
    if (n <= 1) return n.toLong()
    return memo.getOrPut(n) { fib(n - 1) + fib(n - 2) }   // top-down
}

fun fibTable(n: Int): Long {                            // bottom-up, O(1) space
    if (n <= 1) return n.toLong()
    var prev = 0L
    var cur = 1L
    repeat(n - 1) {
        val next = prev + cur
        prev = cur
        cur = next
    }
    return cur
}

fun minCoins(coins: IntArray, amount: Int): Int {
    val dp = IntArray(amount + 1) { Int.MAX_VALUE }
    dp[0] = 0
    for (a in 1..amount) {
        for (c in coins) {
            if (c <= a && dp[a - c] != Int.MAX_VALUE) dp[a] = minOf(dp[a], dp[a - c] + 1)
        }
    }
    return if (dp[amount] == Int.MAX_VALUE) -1 else dp[amount]
}

fun main() {
    println(fib(50))
    println(fibTable(90))
    println(minCoins(intArrayOf(1, 5, 10, 25), 63))
    println(minCoins(intArrayOf(2), 3))
}
Outputcompiled & run with real Kotlin
12586269025
2880067194370816120
6
-1
Your turn

Change minCoins to also return which coins it used: keep a second array lastCoin[a] recording the coin that improved dp[a], then walk back from amount.

VisualizefibTable(5)Step 1 / 6
var prev = 0L
var cur = 1L
repeat(n - 1) {
val next = prev + cur
prev = cur
cur = next
}
return cur
Line 2

fib(0) and fib(1).

Variables now
prev0
cur1
All 6 steps as a table
StepLineWhat happenedVariables now
12fib(0) and fib(1).prev = 0 cur = 1
26Round 1: next = 0 + 1.prev = 1 cur = 1
36Round 2: next = 1 + 1.prev = 1 cur = 2
46Round 3: next = 1 + 2.prev = 2 cur = 3
56Round 4: next = 2 + 3.prev = 3 cur = 5
68Four rounds for n = 5; return fib(5).
getOrPut, not computeIfAbsent
Kotlin's getOrPut is an inline "get, else compute and put", so recursing inside its lambda is safe. Java's HashMap.computeIfAbsent forbids modifying the map from inside the function and throws ConcurrentModificationException on recursive memoisation.
13

Cheat sheet

You need…Reach forKey calls
An ordered, growable listmutableListOf()add, removeAt, [i]
Fast numeric arrayIntArray(n), LongArray(n)[i], sum(), sort()
Key → value lookupmutableMapOf() (ordered) / HashMap()getOrPut, getOrDefault, in
Membership / de-dupmutableSetOf() / HashSet()add returns Boolean, in
StackArrayDeque()addLast, removeLast, last()
Queue / dequeArrayDeque()addLast, removeFirst, first()
Smallest / largest firstjava.util.PriorityQueueadd, poll, peek, reverseOrder()
Sorted keys, range queriesjava.util.TreeMap, sortedSetOf()floorKey, ceilingKey, headMap
Counting / groupingstdlib operatorsgroupingBy { }.eachCount(), groupBy { }
Search a sorted listList.binarySearchnegative result = -(insertion) - 1
Sort by one or more keyssortedWithcompareBy { }.thenByDescending { }
Big-O
How the work grows as the input grows, ignoring constants: O(1) constant, O(log n), O(n), O(n log n), O(n²).
Amortised O(1)
Usually O(1), with an occasional O(n) resize whose cost averages out across many operations — ArrayList.add, ArrayDeque.addLast.
ArrayDeque
Kotlin's circular-buffer list with O(1) add/remove at both ends; the default stack and queue.
LinkedHashMap
What mutableMapOf() returns: a hash map that also remembers insertion order.
Binary search tree
A tree where left < node < right for every node; O(log n) when balanced. TreeMap is a self-balancing (red-black) one.
Heap
A complete binary tree in an array where each parent is <= its children; java.util.PriorityQueue.
BFS / DFS
Graph walks with a queue (level by level, shortest paths) or a stack/recursion (deep first).
Stable sort
Equal elements keep their input order. sortedBy / sortedWith on objects are stable.
tailrec
Kotlin modifier that turns a self-call in tail position into a loop, avoiding StackOverflowError.
Memoisation
Caching each sub-problem's answer so overlapping recursion runs once per input — top-down DP.
Quick check

You need a queue for BFS in Kotlin. Which choice is both idiomatic and O(1) for removing from the front?

Quick check

listOf(3, 8, 15, 23).binarySearch(10) returns what?

Frequently asked questions

Should I use Kotlin's ArrayDeque or java.util.ArrayDeque?
Prefer kotlin.collections.ArrayDeque in Kotlin code: it needs no import, is also a MutableList, and has null-safe helpers like removeFirstOrNull(). java.util.ArrayDeque is fine in mixed Java/Kotlin codebases — just do not import it by accident when you meant the Kotlin one.
Does Kotlin have a built-in priority queue or tree map?
Not in the common stdlib, but on the JVM (and Android) you use java.util.PriorityQueue and java.util.TreeMap directly — Kotlin calls Java classes with no wrappers. sortedSetOf() and sortedMapOf() return a TreeSet and TreeMap.
Is it worth implementing data structures by hand if the stdlib has them?
Once each, yes. Building a stack, linked list, BST and heap is what makes the cost table stick, and interviews still ask for them. In production code, use the stdlib and JDK versions — they are tested and faster.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.