Data Structures, Algorithms and Programming Patterns · Part 1 of 9
Data Structures and Algorithms, Part 1: Start Here and Learn to Think in Cost
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.
- 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
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.
| Part | What you learn |
|---|---|
| 1. Start here | How to measure the cost of code (Big-O) and a method for solving problems. |
| 2. Arrays and hashing | Arrays, strings, hash maps and sets, the workhorses of everyday code. |
| 3. Stacks and queues | Stacks, queues, deques and linked lists. |
| 4. Trees and heaps | Binary trees, search trees, heaps and tries. |
| 5. Graphs | Modelling networks, breadth-first and depth-first search, shortest paths, union-find. |
| 6. Sorting and searching | Classic sorts, binary search and when to use which. |
| 7. Recursion and DP | Recursion, backtracking, greedy choices and dynamic programming. |
| 8. Patterns | Two pointers, sliding window, prefix sums, monotonic stack and more. |
| 9. Study path | A 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”.
| Class | Name | Example | Doubling n does… |
|---|---|---|---|
| O(1) | constant | read list[5] | nothing |
| O(log n) | logarithmic | binary search | adds one step |
| O(n) | linear | find the maximum | doubles the work |
| O(n log n) | linearithmic | good sorting | a little more than doubles |
| O(n²) | quadratic | compare every pair | quadruples the work |
| O(2ⁿ) | exponential | try every subset | squares the work |
Reading Big-O from code
- A statement that runs once is
O(1). - A loop over
nitems isO(n). - A loop inside a loop, both over
n, isO(n²). - A loop that halves the problem each time is
O(log n). - Steps one after another add; steps inside steps multiply. Then keep the biggest term.
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:
| Idea | Kotlin type | Python twin | Java twin |
|---|---|---|---|
| Fixed-size array | IntArray, Array<T> | list | int[] |
| Growable list | MutableList / ArrayList | list | ArrayList |
| Key to value | HashMap, mutableMapOf | dict | HashMap |
| Unique values | HashSet, mutableSetOf | set | HashSet |
| Stack, queue, deque | ArrayDeque | collections.deque | ArrayDeque |
| Smallest first | PriorityQueue | heapq | PriorityQueue |
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.
- Restate it. Say the problem back in your own words. What goes in? What must come out?
- Try small examples by hand. Include an empty input, a single item and a tricky case.
- 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.
- 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.)
- Write the better version and run your examples through it.
- 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
- Count steps, not seconds, and describe growth with Big-O.
- Loops multiply, sequences add, halving gives a logarithm.
- Extra memory is often the price of speed.
- Always write the simple version first; it is your test oracle.
- Next up: arrays, strings, hash maps and sets, where the first big speed-ups live.
| Term | In plain words |
|---|---|
| Data structure | A way of organising data so some operations are cheap. |
| Algorithm | A finite list of steps that solves a problem. |
| Big-O | A way to describe how the cost of code grows with the input size. |
| Worst case | The input that makes the code do the most work. |
| Time complexity | How many steps the code takes as n grows. |
| Space complexity | How much extra memory the code needs as n grows. |
| Brute force | Trying every possibility; simple, correct, often slow. |