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

Data Structures and Algorithms, Part 1: Start Here and Learn to Think in Cost

Khairul Alam·October 2026·~25 min with exercises
Data structuresAlgorithmsBig-OKotlinFor beginners

Before any list, tree or graph, you need two skills: knowing roughly how expensive a piece of code is, and having a repeatable way to attack a problem. This first part gives you both, with small programs you can run and change.

This part in 60 seconds
  • Big-O counts steps as the input grows; it ignores constants and keeps the biggest term.
  • The common classes, from fast to slow: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ).
  • You can often trade space for time, for example with a hash set.
  • Solve in six steps: restate, try examples, brute force, find the waste, improve, state the cost.
Contents
  1. What this series is, and who it is for
  2. Big-O: counting steps instead of seconds
  3. Time and space
  4. The Kotlin toolkit used in this series
  5. A method for solving any problem
  6. Practice
  7. Takeaways

1. What this series is, and who it is for

A data structure is a way of arranging data in memory so some jobs become easy. An algorithm is a recipe of steps that solves a problem. Together they are the toolbox that every program is built from, whether it is a phone app, a game or a database.

This series is a gentle, hands-on guide for computer science students and self-taught programmers who are starting out. It assumes you know the basics of any programming language: variables, if, loops and functions. Everything else is explained in plain words, with a picture, a short Kotlin program, and exercises with tests that you run in the free Kotlin Playground.

Think of a kitchen. A data structure is how you store things: a spice rack, a fridge, a pile of plates. An algorithm is the recipe. A messy kitchen still lets you cook, but it takes much longer, and it gets much worse when you cook for a hundred people instead of two.

Here is the map of the nine parts. Each one stands on its own, but they are written to be read in order.

PartWhat you learn
1. Start hereHow to measure the cost of code (Big-O) and a method for solving problems.
2. Arrays and hashingArrays, strings, hash maps and sets, the workhorses of everyday code.
3. Stacks and queuesStacks, queues, deques and linked lists.
4. Trees and heapsBinary trees, search trees, heaps and tries.
5. GraphsModelling networks, breadth-first and depth-first search, shortest paths, union-find.
6. Sorting and searchingClassic sorts, binary search and when to use which.
7. Recursion and DPRecursion, backtracking, greedy choices and dynamic programming.
8. PatternsTwo pointers, sliding window, prefix sums, monotonic stack and more.
9. Study pathA quicksheet, a practice routine and where to go next.

How to use the code

Every program in the series is complete: copy it or press the green or blue Open in Kotlin Playground button, press Run, and change things to see what happens. The Try it boxes contain tests that print PASS or FAIL; they fail at first on purpose. Nothing is sent anywhere until you click a button.

2. Big-O: counting steps instead of seconds

How fast is a piece of code? Seconds depend on your laptop, the language and what else is running. Computer scientists count something steadier: how many basic steps the work takes as the input grows. We call the input size n.

Big-O notation keeps only the part that matters when n gets large. It drops constants and smaller terms, because 3n + 10 and n grow the same way: in a straight line. We write that as O(n), read “order n”.

stepsn (input size) O(1) O(log n) O(n) O(n log n) O(n²)
The shapes of the common growth rates. Small inputs hide the differences; large inputs make them huge.
ClassNameExampleDoubling n does…
O(1)constantread list[5]nothing
O(log n)logarithmicbinary searchadds one step
O(n)linearfind the maximumdoubles the work
O(n log n)linearithmicgood sortinga little more than doubles
O(n²)quadraticcompare every pairquadruples the work
O(2ⁿ)exponentialtry every subsetsquares the work

Reading Big-O from code

Run it: count the steps yourself

Instead of timing, this program counts steps. Run it and watch how each column grows when n is multiplied by ten.

fun constant(n: Int): Int {
    var steps = 0
    steps++                      // one thing, however big n is
    return steps
}

fun linear(n: Int): Int {
    var steps = 0
    for (i in 1..n) steps++      // n things
    return steps
}

fun quadratic(n: Int): Int {
    var steps = 0
    for (i in 1..n) for (j in 1..n) steps++   // n * n things
    return steps
}

fun logarithmic(n: Int): Int {
    var steps = 0
    var x = n
    while (x > 1) { x /= 2; steps++ }          // halve until one is left
    return steps
}

fun main() {
    println("n      O(1)  O(log n)  O(n)    O(n^2)")
    for (n in listOf(10, 100, 1000, 10000)) {
        println("%-6d %-5d %-9d %-7d %d".format(n, constant(n), logarithmic(n), linear(n), quadratic(n)))
    }
}
Open in Kotlin Playground ↗

Look at the last row. At n = 10,000 the linear code takes ten thousand steps and the quadratic code takes a hundred million. That gap is why we care.

Best, average, worst. Big-O usually describes the worst case: the most steps the input could force. Searching a list for a value might find it on the first try (best case) or the last (worst case, O(n)). We plan for the worst. Part 9 adds the other two symbols, Ω and Θ, and a full quicksheet.

3. Time and space

Code costs two things: time (steps) and space (extra memory). Big-O describes both. A loop that only keeps a counter uses O(1) extra space. A program that builds a copy of its input uses O(n).

The two often trade against each other. A common deal is to spend memory to save time: keep a lookup table so you never repeat work. You will see this again and again, in hash maps (Part 2) and dynamic programming (Part 7).

Run it: spend memory to save time

Same question, two answers. Timings vary by machine, but the fast version should win by a wide margin.

// Does the list contain a duplicate?

// Slow, small: compare every pair. Time O(n^2), extra space O(1).
fun hasDuplicateSlow(nums: List<Int>): Boolean {
    for (i in nums.indices) for (j in i + 1 until nums.size) {
        if (nums[i] == nums[j]) return true
    }
    return false
}

// Fast, bigger: remember what we have seen. Time O(n), extra space O(n).
fun hasDuplicateFast(nums: List<Int>): Boolean {
    val seen = HashSet<Int>()
    for (x in nums) {
        if (!seen.add(x)) return true   // add returns false if it was already there
    }
    return false
}

fun main() {
    val nums = List(20000) { it }       // 0..19999, no duplicates: the worst case
    var t = System.nanoTime()
    println("slow: ${hasDuplicateSlow(nums)}  in ${(System.nanoTime() - t) / 1_000_000} ms")
    t = System.nanoTime()
    println("fast: ${hasDuplicateFast(nums)}  in ${(System.nanoTime() - t) / 1_000_000} ms")
}
Open in Kotlin Playground ↗

4. The Kotlin toolkit used in this series

All examples use Kotlin because it is short to read and runs in the browser. You do not need to know it well. The ideas carry over to Java, Python, C++ or JavaScript, and each Kotlin type used here has a twin in those languages. This is the whole toolkit:

IdeaKotlin typePython twinJava twin
Fixed-size arrayIntArray, Array<T>listint[]
Growable listMutableList / ArrayListlistArrayList
Key to valueHashMap, mutableMapOfdictHashMap
Unique valuesHashSet, mutableSetOfsetHashSet
Stack, queue, dequeArrayDequecollections.dequeArrayDeque
Smallest firstPriorityQueueheapqPriorityQueue

Run it: a five-minute tour

Run it once and read each line next to its output. You will meet every one of these again.

import java.util.PriorityQueue

fun main() {
    // Array and list
    val arr = intArrayOf(5, 3, 8)
    val list = mutableListOf(5, 3, 8)
    list.add(1)
    println("arr[0]=${arr[0]} list=$list size=${list.size}")

    // Map: count letters
    val counts = HashMap<Char, Int>()
    for (c in "banana") counts[c] = (counts[c] ?: 0) + 1
    println("counts=$counts")

    // Set: unique values
    val unique = hashSetOf(1, 2, 2, 3, 3, 3)
    println("unique=$unique")

    // ArrayDeque works as a stack and as a queue
    val dq = ArrayDeque<Int>()
    dq.addLast(1); dq.addLast(2); dq.addLast(3)
    println("removeLast=${dq.removeLast()} removeFirst=${dq.removeFirst()} left=$dq")

    // Priority queue: smallest comes out first
    val pq = PriorityQueue<Int>()
    pq.add(7); pq.add(2); pq.add(5)
    println("poll=${pq.poll()} then ${pq.poll()}")
}
Open in Kotlin Playground ↗

5. A method for solving any problem

The hardest part of a coding problem is usually not the code, it is starting. Use the same six steps every time. They work on exercises, on assignments and on real bugs.

  1. Restate it. Say the problem back in your own words. What goes in? What must come out?
  2. Try small examples by hand. Include an empty input, a single item and a tricky case.
  3. Write the simple, slow solution first. Brute force is fine. A correct slow answer beats a wrong fast one, and it gives you something to test against.
  4. Find the waste. Where do you repeat work? Can a different data structure remove it? (Often the answer is a hash map, a sorted order, or a stack.)
  5. Write the better version and run your examples through it.
  6. State the cost. Say the time and space in Big-O, out loud.

Here is the method on a small problem. Given a list of numbers and a target, find two numbers that add up to the target.

Run it: brute force, then better

// Step 3: brute force. Check every pair. O(n^2) time, O(1) space.
fun twoSumSlow(nums: IntArray, target: Int): Pair<Int, Int>? {
    for (i in nums.indices) for (j in i + 1 until nums.size) {
        if (nums[i] + nums[j] == target) return i to j
    }
    return null
}

// Step 4: the waste is searching for the partner again and again.
// Remember each number's position, then ask "have I seen target - x?". O(n) time, O(n) space.
fun twoSumFast(nums: IntArray, target: Int): Pair<Int, Int>? {
    val seenAt = HashMap<Int, Int>()
    for ((i, x) in nums.withIndex()) {
        val need = target - x
        seenAt[need]?.let { return it to i }
        seenAt[x] = i
    }
    return null
}

fun main() {
    val nums = intArrayOf(2, 7, 11, 15)
    println(twoSumSlow(nums, 9))     // (0, 1)
    println(twoSumFast(nums, 9))     // (0, 1)
    println(twoSumFast(nums, 100))   // null
}
Open in Kotlin Playground ↗

Notice the shape of the improvement: we traded a little memory for a big gain in speed. That is the story of most of this series.

6. Practice

Two short exercises. The first rewards a better data structure, the second makes Big-O feel real.

Try it: find a duplicate fast

Fill in containsDuplicate so it runs in O(n) time. The last test uses 200,000 numbers, so the slow pairwise version will time out. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun containsDuplicate(nums: List<Int>): Boolean {
    TODO("remember what you have seen")
}
Open in Kotlin Playground ↗

Hint: HashSet.add returns false when the value was already inside.

Show a solution
fun containsDuplicate(nums: List<Int>): Boolean {
    val seen = HashSet<Int>()
    for (x in nums) if (!seen.add(x)) return true
    return false
}

Try it: count the halvings

Write stepsToOne(n): how many times can you halve n (integer division) before it reaches 1? This is the O(log n) loop from above. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.

fun stepsToOne(n: Int): Int {
    TODO("halve n until it is 1, counting the steps")
}
Open in Kotlin Playground ↗

Hint: Use a while loop and a counter, like logarithmic earlier.

Show a solution
fun stepsToOne(n: Int): Int {
    var x = n
    var steps = 0
    while (x > 1) { x /= 2; steps++ }
    return steps
}

Stuck? Read the hint, then the solution, then close it and write it again from memory. Understanding it once is not the same as being able to produce it.

7. Takeaways

TermIn plain words
Data structureA way of organising data so some operations are cheap.
AlgorithmA finite list of steps that solves a problem.
Big-OA way to describe how the cost of code grows with the input size.
Worst caseThe input that makes the code do the most work.
Time complexityHow many steps the code takes as n grows.
Space complexityHow much extra memory the code needs as n grows.
Brute forceTrying every possibility; simple, correct, often slow.
← Back to all writing