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

Data Structures and Algorithms, Part 3: Stacks, Queues, Deques and Linked Lists

Khairul Alam·October 2026·~35 min with exercises
Data structuresLinked listStackQueueKotlinFor beginners

Some data structures are defined less by how they store things and more by the rules they enforce. This part covers linked lists, which are built from nodes and pointers, and then stacks, queues and deques, which control the order in which things come out. You will build each one, see its cost, and solve classic problems with them.

This part in 60 seconds
  • A linked list is a chain of nodes; adding at the head is O(1), searching is O(n).
  • Reversing, finding the middle and detecting a cycle all come down to careful pointer moves, often with a slow and a fast pointer.
  • A stack is LIFO (push, pop, peek); a queue is FIFO (enqueue, dequeue); a deque works at both ends. Kotlin’s ArrayDeque covers all three.
  • Arrays usually beat linked lists in practice because of cache and memory, but linked nodes are the base of trees and graphs.
Contents
  1. Linked lists: the idea
  2. Build a singly linked list in Kotlin
  3. Three classic list tricks: reverse, middle, cycle
  4. Doubly linked lists, and lists versus arrays
  5. Stacks: last in, first out
  6. Queues: first in, first out
  7. Deques: both ends
  8. A classic puzzle: a queue from two stacks
  9. Comparing the costs
  10. Practice
  11. Takeaways

1. Linked lists: the idea

In Part 2 you met the array: items stored side by side in one block of memory. That layout makes reading arr[5] instant, but it makes inserting at the front slow, because every other item has to shuffle along.

A linked list takes the opposite deal. Items are not side by side. Each item lives in its own small box called a node, and each node holds two things: the value, and a pointer (also called a reference) to the next node. A pointer is just the address of another object. The last node points to nothing, which we write as null.

Think of a treasure hunt. Each clue tells you what is here and where the next clue is hidden. You cannot jump to clue number 5. You must follow clues 1, 2, 3 and 4 first. But adding a new clue in the middle is easy: change one note to point to the new clue, and let the new clue point onward.

head7valuenext3valuenext9valuenextnull
A singly linked list holding 7, 3, 9. The only thing we keep is head, a pointer to the first node.

Because we only hold head, every other node is reachable only by walking. That one fact explains the whole cost table later in this part.

Inserting at the front

To add a new first item, make a node, point it at the old head, then move head to the new node. Two pointer changes, no shuffling, so it is O(1).

Beforehead739nullStep 1new node [5] points at the old head5739nullStep 2head moves to the new nodeAfterhead5739null
Add-first changes two pointers, whatever the length of the list.

2. Build a singly linked list in Kotlin

We need just one small class. A node has a value and a nullable next. In Kotlin, Node? means “a Node or null”, which is exactly what a pointer that might point to nothing needs.

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

Then a list class that remembers head and the size. Read the comments: each method says what it costs.

Run it: your own linked list

Run it, then try removing the last item, and removing from an empty list. Those edge cases are where linked-list bugs hide.

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

class MyLinkedList {
    var head: Node? = null
    var size = 0
        private set

    // O(1): the new node becomes the head.
    fun addFirst(value: Int) {
        head = Node(value, head)
        size++
    }

    // O(n): we must walk to the end, because we only hold the head.
    fun addLast(value: Int) {
        val node = Node(value)
        val first = head
        if (first == null) {
            head = node
        } else {
            var cur: Node = first
            while (cur.next != null) cur = cur.next!!
            cur.next = node
        }
        size++
    }

    // O(n): walk until we find it (worst case, it is last or missing).
    fun contains(value: Int): Boolean {
        var cur = head
        while (cur != null) {
            if (cur.value == value) return true
            cur = cur.next
        }
        return false
    }

    // O(n) to find it, O(1) to unlink it.
    fun remove(value: Int): Boolean {
        val first = head ?: return false
        if (first.value == value) {          // removing the head is a special case
            head = first.next
            size--
            return true
        }
        var prev: Node = first
        while (true) {
            val cur = prev.next ?: return false
            if (cur.value == value) {
                prev.next = cur.next          // skip over it: one pointer change
                size--
                return true
            }
            prev = cur
        }
    }

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

fun main() {
    val list = MyLinkedList()
    list.addFirst(3)
    list.addFirst(7)      // 7 -> 3
    list.addLast(9)       // 7 -> 3 -> 9
    println(list)
    println("contains 3? ${list.contains(3)}   contains 4? ${list.contains(4)}")
    println("remove 3: ${list.remove(3)}  -> $list")
    println("remove 7: ${list.remove(7)}  -> $list   (that was the head)")
    println("remove 42: ${list.remove(42)}  size=${list.size}")
}
Open in Kotlin Playground ↗

Notice how remove works. To delete a node we need the node before it, so that we can make that earlier node point past the victim. That is why the loop tracks prev.

remove 3 fromhead739nullprev = [7], cur = [3]prev.next = cur.nexthead739nullprevcur[3] is no longer reachable(the garbage collector frees it)
Unlinking a node: one pointer is redirected.
OperationCostWhy
Add at headO(1)Two pointer changes.
Add at tail (only head kept)O(n)Walk to the end first.
Find a valueO(n)Follow pointers one by one.
Get the i-th itemO(n)No jumping; walk i steps.
Remove the headO(1)Move head forward.
Remove a valueO(n)Find it, then unlink in O(1).

Tail pointer. If the list also keeps a pointer to the last node, adding at the tail becomes O(1). Removing the tail is still O(n) in a singly linked list, because you need the node before it.

3. Three classic list tricks: reverse, middle, cycle

These three small problems appear in almost every course, because they teach you to think in pointers. Draw them on paper before you code them.

Reversing a list

Walk down the list and flip each pointer to face backwards. You need three variables: prev (the already-reversed part), cur (the node you are fixing) and next (saved first, so you do not lose the rest of the list).

Startprev = null, cur = [1] --> [2] --> [3] --> nullnullnull123prevcurStep 1save next = [2]; point [1] back at prevnullnull123prevcurStep 2save next = [3]; point [2] back at [1]nullnull123prevcurStep 3point [3] back at [2]; cur becomes nullnullnull123prevcurprev is the new head
Reversal in place. It visits each node once: O(n) time, O(1) extra space.

Finding the middle

Use two pointers that start together. The fast pointer moves two nodes per step, the slow pointer moves one. When fast reaches the end, slow is halfway. One pass, no need to know the length.

Run it: reverse a list and find its middle

For an even number of nodes there are two middles; this version returns the second one.

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

fun fromList(items: List<Int>): Node? {
    var head: Node? = null
    for (x in items.reversed()) head = Node(x, head)
    return head
}

fun toList(head: Node?): List<Int> {
    val out = ArrayList<Int>()
    var cur = head
    while (cur != null) { out.add(cur.value); cur = cur.next }
    return out
}

fun reverse(head: Node?): Node? {
    var prev: Node? = null
    var cur = head
    while (cur != null) {
        val next = cur.next     // 1. remember the rest
        cur.next = prev         // 2. flip the pointer
        prev = cur              // 3. move both forward
        cur = next
    }
    return prev                 // prev ends on the old last node: the new head
}

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

fun main() {
    val list = fromList(listOf(1, 2, 3, 4, 5))
    println("middle of 1..5 = ${middle(list)?.value}")                       // 3
    println("middle of 1..4 = ${middle(fromList(listOf(1, 2, 3, 4)))?.value}") // 3 (second of two)
    println("reversed: ${toList(reverse(list))}")
    println("reverse empty: ${toList(reverse(null))}")
}
Open in Kotlin Playground ↗

Detecting a cycle

Normally the last node points to null. If a bug makes some node point back to an earlier one, the list becomes a loop, and walking it would never end. How can you tell without walking forever?

The two-speed trick again, known as Floyd’s tortoise and hare. Slow moves one step, fast moves two. If there is no cycle, fast falls off the end. If there is a cycle, both run round it, and the fast one gains one step per lap on the slow one until they stand on the same node.

Two runners on a circular track. The faster runner eventually laps the slower one, so they must meet. On a straight road with an end, the fast runner just finishes first.

123456slow: 1 2 3 4 5 6 3 ...fast: 1 3 5 3 5 3 ...they meet inside the looplast node points back to [3]loop: 3 → 4 → 5 → 6 → 3
A list whose last node points back to node 3. Time O(n), extra space O(1).

Run it: tortoise and hare

Note ===. We care whether two references are the same node, not whether two nodes hold equal values.

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

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

fun main() {
    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("straight list: ${hasCycle(a)}")   // false
    d.next = b                                  // close a loop: 4 points back to 2
    println("with a loop:   ${hasCycle(a)}")   // true
    println("empty list:    ${hasCycle(null)}") // false
}
Open in Kotlin Playground ↗

Why not just remember visited nodes? A hash set of visited nodes also works, in O(n) time, but it costs O(n) memory. Two pointers need only O(1). Another example of trading space for time, in reverse.

4. Doubly linked lists, and lists versus arrays

A doubly linked list gives each node two pointers: next and prev. You can walk both ways, and if you already hold a node, you can remove it in O(1) without hunting for its predecessor.

null739nullheadtail
Doubly linked: every link goes both ways. Keeping both head and tail makes both ends O(1).

The price is one extra pointer per node, and a bit more care when changing links (four pointer updates instead of two). Java’s LinkedList is a doubly linked list. Real uses include an editor’s list of lines, a browser’s back and forward history, and the least-recently-used cache.

class DNode(var value: Int) {
    var prev: DNode? = null
    var next: DNode? = null
}

// Insert `node` right after `at`: four pointer updates.
fun insertAfter(at: DNode, node: DNode) {
    node.prev = at
    node.next = at.next
    at.next?.prev = node
    at.next = node
}

// Unlink a node we already hold: O(1).
fun unlink(node: DNode) {
    node.prev?.next = node.next
    node.next?.prev = node.prev
}

So, lists or arrays?

On paper, linked lists win at inserting and deleting in the middle (once you hold the spot). Arrays win at reading by position. In practice, arrays usually win overall, for two reasons:

Use a…when…
Array / ArrayListYou read by position, loop often, or just want a sensible default.
Linked listYou add and remove at the ends or at spots you already hold, you never need position lookup, and you want no big resize copies.

Be honest about the default. In most real Kotlin code you will reach for ArrayList or ArrayDeque, not a linked list. We build linked lists to learn pointers, which trees (Part 4) and graphs (Part 5) are made of.

5. Stacks: last in, first out

A stack allows work at one end only, called the top. The last item you put in is the first one to come out. This rule is called LIFO: last in, first out.

A pile of plates. You put a clean plate on top and you take a plate from the top. Nobody pulls one from the bottom.

A stack has three core operations:

push(1)1← toppush(2)12← toppush(3)123← toppop() → 312← top3pop() → 21← top2
Items enter and leave at the top. Every operation is O(1).

Implementing a stack

You do not need to write a node class. Kotlin’s ArrayDeque is a resizable array that is fast at both ends, so a stack is just addLast to push and removeLast to pop. Wrapping it in a small class makes the intent obvious.

Run it: a stack, and an undo button

Undo works because the most recent change is the first one you want to reverse. That is exactly LIFO.

class Stack<T> {
    private val items = ArrayDeque<T>()
    fun push(x: T) = items.addLast(x)                 // O(1) on average
    fun pop(): T = items.removeLast()                 // O(1); throws if empty
    fun peek(): T? = items.lastOrNull()               // O(1); null if empty
    fun isEmpty() = items.isEmpty()
    val size get() = items.size
}

fun main() {
    val s = Stack<Int>()
    s.push(1); s.push(2); s.push(3)
    println("peek=${s.peek()} size=${s.size}")
    println("pop=${s.pop()} pop=${s.pop()} left=${s.size}")

    // Undo: remember each edit on a stack, undo by popping the latest.
    var text = ""
    val history = Stack<String>()
    for (word in listOf("Hello", " there", " world")) {
        history.push(text)       // save the state BEFORE the change
        text += word
        println("typed -> \"$text\"")
    }
    while (!history.isEmpty()) {
        text = history.pop()
        println("undo  -> \"$text\"")
    }
}
Open in Kotlin Playground ↗

Where stacks show up

Balanced brackets

Is {[()]} well formed? Is ([)]? Scan left to right. Push every opening bracket. On a closing bracket, the top of the stack must be its matching opener; pop it. At the end the stack must be empty. The most recent unclosed opener is always the one that must close first, which is LIFO again.

Run it: balanced brackets

Time O(n), one pass. Space O(n) in the worst case, when everything is an opener.

fun isBalanced(s: String): Boolean {
    val pairs = mapOf(')' to '(', ']' to '[', '}' to '{')
    val stack = ArrayDeque<Char>()
    for (c in s) {
        when {
            c in pairs.values -> stack.addLast(c)                       // opener: push
            c in pairs.keys -> {                                         // closer: must match top
                if (stack.isEmpty() || stack.removeLast() != pairs[c]) return false
            }
            // any other character is ignored
        }
    }
    return stack.isEmpty()      // leftover openers mean it is not balanced
}

fun main() {
    for (s in listOf("{[()]}", "([)]", "((", "", "a(b)c")) {
        println("%-8s -> %s".format("\"$s\"", isBalanced(s)))
    }
}
Open in Kotlin Playground ↗

Evaluating postfix

We usually write maths infix: (3 + 4) * 2. Postfix (also called reverse Polish notation) puts the operator after its operands: 3 4 + 2 *. It needs no brackets and no precedence rules, and a stack evaluates it in one pass: push numbers; on an operator, pop two numbers, apply it, push the result.

tokens:34+2*tokenactionstack (top at right)3push34push34+pop 4, pop 33+4 = 7, push72push72*pop 2, pop 77*2 = 14, push14answer = the one item left: 14
Watch the order: the first number popped is the right-hand operand.

Run it: evaluate postfix

Try "2 3 4 * +" and check it by hand.

fun evalPostfix(tokens: List<String>): Int {
    val stack = ArrayDeque<Int>()
    for (t in tokens) {
        when (t) {
            "+", "-", "*", "/" -> {
                val right = stack.removeLast()     // popped first: the RIGHT operand
                val left = stack.removeLast()
                stack.addLast(when (t) {
                    "+" -> left + right
                    "-" -> left - right
                    "*" -> left * right
                    else -> left / right
                })
            }
            else -> stack.addLast(t.toInt())
        }
    }
    return stack.removeLast()
}

fun main() {
    println(evalPostfix("3 4 + 2 *".split(" ")))        // (3 + 4) * 2 = 14
    println(evalPostfix("5 1 2 + 4 * + 3 -".split(" ")))  // 5 + (1+2)*4 - 3 = 14
    println(evalPostfix("10 2 -".split(" ")))             // 8, not -8: order matters
}
Open in Kotlin Playground ↗

6. Queues: first in, first out

A queue is the fair line at a shop. Items join at the back and leave from the front. The first one to arrive is the first one served: FIFO, first in, first out.

ABCDdequeuefrontleaves firstenqueuebackjoins last
A queue. Items join at one end and leave from the other.

With ArrayDeque a queue is addLast to enqueue and removeFirst to dequeue. Both are O(1) on average.

Warning: do not build a queue on a plain ArrayList by calling removeAt(0). Removing the first item shifts all the others, so each dequeue costs O(n).

A tiny circular buffer

How can an array give a fast queue? Keep a head index (where the front is) and a count. When an index runs off the end, wrap around to 0 using the remainder operator %. No shuffling is ever needed, so the array behaves like a ring. This is what ArrayDeque does inside, with extra code to grow when it is full.

capacity 5, head = 2, size = 3 (items C D E), slots 0 and 1 are free012C3D4EFheadlastlast item at (head+size-1) % 5 = 4enqueue F goes to (head+size) % 5 = 0 → wraps to the start
A circular buffer. The array is fixed, but the queue slides around it.

Run it: a fixed-size circular queue

Every operation is O(1). The cost is a fixed capacity; a growable version copies into a bigger array when full.

class RingQueue(private val capacity: Int) {
    private val data = IntArray(capacity)
    private var head = 0          // index of the front item
    var size = 0
        private set

    fun enqueue(x: Int): Boolean {
        if (size == capacity) return false          // full
        data[(head + size) % capacity] = x          // wrap around with %
        size++
        return true
    }

    fun dequeue(): Int? {
        if (size == 0) return null                  // empty
        val x = data[head]
        head = (head + 1) % capacity
        size--
        return x
    }
}

fun main() {
    val q = RingQueue(3)
    println(listOf(q.enqueue(1), q.enqueue(2), q.enqueue(3), q.enqueue(4)))  // last is false: full
    println("dequeue -> ${q.dequeue()}")      // 1
    println("enqueue 5 -> ${q.enqueue(5)}")    // true: it reuses the freed slot
    val out = ArrayList<Int?>()
    repeat(4) { out.add(q.dequeue()) }
    println(out)                               // [2, 3, 5, null]
}
Open in Kotlin Playground ↗

Where queues show up

Run it: a preview of breadth-first search

Because the queue is FIFO, everything one step from A is processed before anything two steps away.

fun main() {
    // Who is connected to whom. Start at A and visit nearest first.
    val friends = 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()
    )
    val queue = ArrayDeque<String>()
    val seen = HashSet<String>()
    queue.addLast("A"); seen.add("A")
    val order = ArrayList<String>()
    while (queue.isNotEmpty()) {
        val node = queue.removeFirst()           // the one that has waited longest
        order.add(node)
        for (next in friends.getValue(node)) {
            if (seen.add(next)) queue.addLast(next)   // join the back, once only
        }
    }
    println(order)     // [A, B, C, D, E, F]: A, then its neighbours, then theirs
}
Open in Kotlin Playground ↗

7. Deques: both ends

A deque (say “deck”, short for double-ended queue) lets you add and remove at both ends in O(1). It is more general than a stack or a queue, which is why Kotlin’s one ArrayDeque class can act as either.

addFirstABCDaddLastremoveFirstfrontremoveLastback
Four O(1) operations: addFirst, addLast, removeFirst, removeLast.
You want a…Use these calls
StackaddLast and removeLast
QueueaddLast and removeFirst
Dequeall four

Run it: a deque as a palindrome checker

Compare the front and the back, then shrink inwards. Only a structure with cheap access at both ends makes this natural.

fun isPalindrome(text: String): Boolean {
    val dq = ArrayDeque<Char>()
    for (c in text) if (c.isLetterOrDigit()) dq.addLast(c.lowercaseChar())
    while (dq.size > 1) {
        if (dq.removeFirst() != dq.removeLast()) return false   // compare the two ends
    }
    return true
}

fun main() {
    println(isPalindrome("A man, a plan, a canal: Panama"))   // true
    println(isPalindrome("kotlin"))                            // false
    println(isPalindrome(""))                                  // true
}
Open in Kotlin Playground ↗

Deques also power a few clever tricks, such as sliding-window maximum, which Part 8 covers. For now, remember: if you need both ends, reach for ArrayDeque.

8. A classic puzzle: a queue from two stacks

Can you build a FIFO queue when the only tool you have is two LIFO stacks? Yes, and it teaches something deep: reversing the order twice gives back the original order.

Keep an in stack and an out stack.

enqueue 1, 2, 3321inoutdequeue (out is empty: pour in → out)in123out← pop gives 1: the oldest
Pouring one stack into another reverses it.

Pouring costs O(n) once, but each item is poured exactly once in its life. So over many operations the average cost per call is O(1). We call that amortised O(1): expensive occasionally, cheap on average.

Run it: queue built from two stacks

Interleaving enqueues and dequeues is the tricky case: never pour while outbox still has items, or you will break the order.

class QueueFromStacks {
    private val inbox = ArrayDeque<Int>()    // used only as a stack
    private val outbox = ArrayDeque<Int>()   // used only as a stack

    fun enqueue(x: Int) { inbox.addLast(x) }

    fun dequeue(): Int {
        if (outbox.isEmpty()) {
            while (inbox.isNotEmpty()) outbox.addLast(inbox.removeLast())   // pour: reverses order
        }
        return outbox.removeLast()
    }

    fun isEmpty() = inbox.isEmpty() && outbox.isEmpty()
}

fun main() {
    val q = QueueFromStacks()
    q.enqueue(1); q.enqueue(2); q.enqueue(3)
    println(q.dequeue())      // 1
    q.enqueue(4)
    println(q.dequeue())      // 2  (still in the outbox from the earlier pour)
    println(q.dequeue())      // 3
    println(q.dequeue())      // 4
    println(q.isEmpty())      // true
}
Open in Kotlin Playground ↗

The reverse puzzle, a stack from queues, also works: after each push, rotate the queue so the new item comes to the front. It makes push cost O(n). Try it on paper; it is a good check of your understanding of FIFO.

9. Comparing the costs

Here is everything side by side. “Avg” for the array’s append means amortised: the occasional resize is spread over many appends.

OperationArray / ArrayListSingly linked listStackQueue (ArrayDeque)
Read by positionO(1)O(n)not offerednot offered
Search for a valueO(n)O(n)O(n)O(n)
Add at the frontO(n)O(1)n/aO(1) (deque)
Add at the backO(1) avgO(n), or O(1) with a tailpush: O(1)enqueue: O(1) avg
Remove from the frontO(n)O(1)n/adequeue: O(1)
Remove from the backO(1)O(n)pop: O(1)n/a (deque: O(1))
Insert in the middleO(n)O(1) once you hold the spot, O(n) to find itnot offerednot offered

The pattern to take away: a stack or queue gives you fewer operations on purpose. That restriction is a feature. When code uses a stack, the reader instantly knows the order is LIFO, and the structure guarantees nothing else can reach the middle.

Choosing quickly
  • Need to undo, backtrack, match nested things, or follow a call chain? Stack.
  • Need fairness or nearest-first order? Queue.
  • Need both ends? Deque.
  • Need position lookup? Array.
  • Learning pointers, or building trees and graphs? Linked nodes.

10. Practice

Five exercises, from pointer wiring to stack tricks. Draw the picture on paper first, then write the code. The edge cases (empty, one item) are in the tests on purpose.

Try it: reverse a linked list

The Node class and two helpers are done for you. Fill in reverse so it flips the pointers in place and returns the new head. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

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

fun fromList(items: List<Int>): Node? {
    var head: Node? = null
    for (x in items.reversed()) head = Node(x, head)
    return head
}

fun toList(head: Node?): List<Int> {
    val out = ArrayList<Int>()
    var cur = head
    while (cur != null) { out.add(cur.value); cur = cur.next }
    return out
}

fun reverse(head: Node?): Node? {
    TODO("flip each next pointer; keep prev, cur and a saved next")
}
Open in Kotlin Playground ↗

Hint: Save cur.next in a variable before you overwrite it. Return prev.

Show a solution
class Node(var value: Int, var next: Node? = null)

fun fromList(items: List<Int>): Node? {
    var head: Node? = null
    for (x in items.reversed()) head = Node(x, head)
    return head
}

fun toList(head: Node?): List<Int> {
    val out = ArrayList<Int>()
    var cur = head
    while (cur != null) { out.add(cur.value); cur = cur.next }
    return out
}

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

Try it: detect a cycle

Write hasCycle using two pointers, so it uses O(1) extra memory. The tests build loops by hand. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

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

fun hasCycle(head: Node?): Boolean {
    TODO("slow moves 1, fast moves 2; do they ever meet?")
}
Open in Kotlin Playground ↗

Hint: Loop while fast and fast.next are not null. Compare nodes with ===, not by value.

Show a solution
class Node(var value: Int, var next: Node? = null)

fun hasCycle(head: Node?): Boolean {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow!!.next
        fast = fast.next!!.next
        if (slow === fast) return true
    }
    return false
}

Try it: balanced brackets, with a twist

Write isBalanced for the three kinds (), [] and {}. Ignore all other characters. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun isBalanced(s: String): Boolean {
    TODO("push openers, match closers against the top")
}
Open in Kotlin Playground ↗

Hint: Check stack.isEmpty() before popping when you see a closer, and check it again at the very end.

Show a solution
fun isBalanced(s: String): Boolean {
    val pairs = mapOf(')' to '(', ']' to '[', '}' to '{')
    val stack = ArrayDeque<Char>()
    for (c in s) {
        if (c in pairs.values) stack.addLast(c)
        else if (c in pairs.keys) {
            if (stack.isEmpty() || stack.removeLast() != pairs[c]) return false
        }
    }
    return stack.isEmpty()
}

Try it: evaluate postfix

Write evalPostfix for + - * / over whole numbers. Remember that the first value you pop is the right operand. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun evalPostfix(tokens: List<String>): Int {
    TODO("push numbers; on an operator pop right, pop left, push the result")
}
Open in Kotlin Playground ↗

Hint: A token like "-3" is a number, not the minus operator, so test for the four exact operator strings only.

Show a solution
fun evalPostfix(tokens: List<String>): Int {
    val stack = ArrayDeque<Int>()
    for (t in tokens) {
        when (t) {
            "+", "-", "*", "/" -> {
                val right = stack.removeLast()
                val left = stack.removeLast()
                stack.addLast(when (t) {
                    "+" -> left + right
                    "-" -> left - right
                    "*" -> left * right
                    else -> left / right
                })
            }
            else -> stack.addLast(t.toInt())
        }
    }
    return stack.removeLast()
}

Try it: a queue from two stacks

Complete MyQueue using only the two stacks provided (use addLast and removeLast on them, nothing else). dequeue and peek return null when empty. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

class MyQueue {
    private val inbox = ArrayDeque<Int>()
    private val outbox = ArrayDeque<Int>()

    fun enqueue(x: Int) {
        TODO("push onto inbox")
    }

    private fun refill() {
        TODO("if outbox is empty, pour all of inbox into it")
    }

    fun dequeue(): Int? {
        TODO("refill, then pop from outbox")
    }

    fun peek(): Int? {
        TODO("refill, then look at the top of outbox")
    }

    fun isEmpty(): Boolean {
        TODO("both stacks empty")
    }
}
Open in Kotlin Playground ↗

Hint: Only pour when outbox is empty. Pouring earlier mixes old and new items and breaks the order.

Show a solution
class MyQueue {
    private val inbox = ArrayDeque<Int>()
    private val outbox = ArrayDeque<Int>()

    fun enqueue(x: Int) {
        inbox.addLast(x)
    }

    private fun refill() {
        if (outbox.isEmpty()) {
            while (inbox.isNotEmpty()) outbox.addLast(inbox.removeLast())
        }
    }

    fun dequeue(): Int? {
        refill()
        return outbox.removeLastOrNull()
    }

    fun peek(): Int? {
        refill()
        return outbox.lastOrNull()
    }

    fun isEmpty(): Boolean = inbox.isEmpty() && outbox.isEmpty()
}

Going further. Try adding removeLast to the linked list, writing a doubly linked list with a tail pointer, or building a stack on top of the linked list so that push and pop work at the head. All three are O(1) at the head.

11. Takeaways

TermIn plain words
NodeA small object holding a value and one or more pointers to other nodes.
Pointer / referenceThe address of another object, or null for nothing.
Head / tailThe first / last node of a list.
Singly / doubly linkedNodes have a pointer to the next only / to both next and previous.
LIFOLast in, first out: the stack rule.
FIFOFirst in, first out: the queue rule.
Push / pop / peekAdd to the top / remove from the top / look at the top of a stack.
Enqueue / dequeueAdd at the back / remove from the front of a queue.
DequeA double-ended queue: add and remove at both ends.
Circular bufferA fixed array used as a ring by wrapping indexes with %.
Call stackThe stack of active function calls the computer keeps while a program runs.
AmortisedAveraged over many operations, so an occasional expensive step is spread out.
← Back to all writing