Call search([4, 8, 15, 16, 23, 42], 16) - a new frame is pushed onto the call stack.
primitives are shown in Variables.
Real execution, step by step. Press play or scrub with the controls - every variable and array index updates live.
Start here: what this lesson is for
Welcome to the first lesson of the course. Before anything technical, here is what you are about to learn and why it comes first.
By the end of this page you will be able to look at a piece of code - code you have never seen before, written by someone else - and say roughly how expensive it is. Not by running it. Not by timing it. Just by reading it and counting.
That single skill is the foundation of everything else in this course. Every later chapter - sorting, searching, trees, graphs - is really a collection of tricks for doing less work. If you cannot measure work, none of those tricks will feel like anything more than trivia to memorise. Once you can measure it, they start to feel obvious.
You do not need any prior knowledge. If you can read a basic loop, you have enough to follow every word below.
- What you need first: how to read a basic Java loop. That is all.
- What you will be able to do: estimate the cost of unfamiliar code by reading it.
- How long: about 16 minutes, plus the two problems at the end.
- algorithm
- - a fixed recipe of steps that turns an input into an answer
Fast code isn't a fast computer
Here's the first surprise of this whole course, and it catches almost everyone: whether code is 'fast' or 'slow' has almost nothing to do with how expensive your computer is. A brilliant algorithm on a cheap phone will crush a clumsy one running on a supercomputer, every time, as soon as the amount of data grows.
That sounds like an exaggeration, so let's make it concrete. Imagine two programs that both search a list of names. The first checks names one at a time. The second uses a smarter method you will meet in lesson three. On a list of ten names, both finish instantly and you would never tell them apart. On a list of ten million names, the first takes several seconds and the second still finishes instantly - on the same machine, in the same language.
So buying a machine twice as fast makes your code twice as fast, which sounds good until you realise the data you are handling might grow ten times. Then you have lost, badly. The method scales; the hardware does not.
This is why professional engineers talk about algorithms rather than hardware, and it is why this lesson exists before any data structure does.
Imagine it like this: Two people looking for a word in a dictionary. One reads from page 1. The other opens the middle and halves the search each time. Give the first person a faster brain and they will still lose - the method beats the machine.
- input
- - the data your code is given to work with - a list, a number, a piece of text
- step
- - one unit of work: one comparison, one calculation, one item examined
What exactly counts as one step?
If we are going to count steps, we need to agree on what a step is. The good news is that the definition is deliberately loose, and that looseness is a feature rather than sloppiness.
A step is any single small action that takes roughly the same amount of time no matter how big your data is. Comparing two numbers is a step. Adding two numbers is a step. Reading one item out of an array is a step. Assigning a value to a variable is a step.
What is not one step is anything that secretly loops. Reading one item from an array is one step; reading every item is one step per item. Calling a method that itself walks a list is not one step - it is however many steps that method takes. This is the most common early mistake: treating a method call as free because it is a single line of code.
Notice we are not measuring seconds. One step might be a nanosecond on your laptop and three nanoseconds on a cheap phone, and we simply do not care. We are counting how many steps, because that number is a property of the algorithm itself and stays true on any machine, in any language, in any year.
A stopwatch measures your laptop, today, with that browser tab open. A step count measures the algorithm itself. Run the same code tomorrow on a different machine and the seconds change; the step count does not. That is why every serious engineer thinks in steps.
- constant time
- - work that takes the same time regardless of input size - one step
Watch it count
Scroll back up to the animation for a second - it's a real Java method, a linear search, hunting through a row of numbers for the value 16. 'Linear' just means it walks in a straight line, one box after another, left to right. Press play and physically count the checks. It looks at 4, then 8, then 15, then 16 - found it, on the 4th look.
Now let's look at the code that produced it. Do not worry if Java is not your language; read it as instructions in English. The loop starts at the first box, and each time round it asks one question: is this the number I want?
Here's that same array as a picture. The search starts at index 0 and steps right until the value it's holding matches the target. Each box it opens is one step.
int linearSearch(int[] numbers, int target) {
for (int i = 0; i < numbers.length; i++) { // one step per box opened
if (numbers[i] == target) {
return i; // found it - stop early
}
}
return -1; // walked the whole row, no match
}
int[] numbers = {4, 8, 15, 16, 23, 42};
System.out.println(linearSearch(numbers, 16));3
- linear search
- - checking items one after another, from the start
- index
- - the position of an item in an array, counting from 0
Best case, worst case
Our search found 16 in four steps. But that number depends entirely on what we searched for. Ask for the very first value, 4, and it finishes in a single step - the best case. Ask for 42, the last one, and it must open every box. Ask for a value that isn't there at all, say 99, and it still has to check all six before it can honestly say 'not found'.
That last situation - the input that forces the most work - is the worst case, and it is the one professionals care about most. Anyone can look fast on a lucky input. We judge an algorithm by how it behaves when the luck runs out, because that's the guarantee you can actually build on.
There is a third measure, the average case, and it is exactly what it sounds like: the cost over typical inputs rather than the luckiest or unluckiest. It is genuinely useful, but it is also harder to reason about, because it requires you to know what 'typical' means for your data. When people quote a single cost without saying which they mean, they almost always mean the worst case - and so will we, unless we say otherwise.
For a list of n items, a linear search does at most n steps in the worst case. Double the list, and you double the work. Make it ten times longer, and you do ten times the work. The steps grow in a perfectly straight line with the size of the input - and that straight-line growth has a name.
Don't take our word for it - make the code count for you. Add a counter, run the same search three times, and read the three numbers:
Before you scroll: our array has 6 items. Searching for 42 (the last item) costs 6 steps. How many steps does searching for 99 - a value that is NOT in the array - cost?
int stepsToFind(int[] numbers, int target) {
int steps = 0;
for (int i = 0; i < numbers.length; i++) {
steps++; // count every box we open
if (numbers[i] == target) return steps;
}
return steps; // opened them all, found nothing
}
int[] numbers = {4, 8, 15, 16, 23, 42};
System.out.println(stepsToFind(numbers, 4)); // best case
System.out.println(stepsToFind(numbers, 42)); // worst case
System.out.println(stepsToFind(numbers, 99)); // not there at all1 6 6
- Best case: the answer is first - 1 step
- Worst case: the answer is last or missing - n steps
- Average case: the cost over typical inputs, between the two
- We design and judge for the worst case, never the lucky one
- n
- - the size of the input - how many items you're working with
- worst case
- - the input that forces the most steps; the honest measure of cost
- average case
- - the cost over typical inputs rather than the extremes
Say hello to O(n)
Engineers write that straight-line growth as O(n), said out loud as 'oh of n' or 'order n'. It is not as intimidating as it looks. The n is the size of your input, and the whole expression is a short way of saying: the work grows in step with the input.
The capital O is doing one specific job - it means 'grows no faster than'. It deliberately throws away detail you should not be relying on, and knowing what it throws away is what makes it usable.
It throws away constants. Code that does 3 steps per item is O(n), and so is code that does 1 step per item. The first is three times slower, and Big-O does not care, because tripling is a fixed penalty while growth is unbounded.
It throws away smaller terms. Code that does n steps and then another 50 steps is still O(n). Once n is a million, the 50 has stopped mattering.
This feels like cheating the first time you meet it. It is not: it is a deliberate choice to describe the shape of the growth rather than its exact value, because shape is what still holds true when your data is a thousand times bigger.
// all three of these are O(n) - the SHAPE is the same
for (int x : items) sum += x; // n steps
for (int x : items) { sum += x; max = big(x); min = small(x); } // 3n steps
for (int x : items) sum += x; // n + 50 steps
for (int i = 0; i < 50; i++) warmUpCache();n = 1,000 n steps 1,000 3n steps 3,000 n+50 steps 1,050
n = 1,000,000 n steps 1.0M 3n steps 3.0M n+50 steps 1.000050M
as n grows, the 3x stays 3x - and the +50 disappears- O(n)
- - linear growth - double the input, double the work
- Big-O
- - notation describing how cost grows as the input grows
The other shapes you will meet
O(n) is one shape among a handful you will meet constantly. You do not need to memorise them today - every one gets its own lesson later - but seeing the family now makes the rest of the course feel like a map rather than a maze.
O(1), constant time. The work does not depend on the input size at all. Reading the fifth item of an array takes the same time whether the array holds ten items or ten million. This is the best possible shape.
O(log n), logarithmic. Every step throws away half of what is left. A million items takes about twenty steps. This is nearly as good as O(1), and lesson three is entirely about it.
O(n), linear. What we have been doing. Look at everything once.
O(n log n), linearithmic. The cost of a good sort. Slower than linear, but close enough that it is considered efficient.
O(n squared), quadratic. Usually a loop inside a loop - for every item, do something with every other item. Fine at small sizes, catastrophic at large ones, and the subject of lesson two.
One thousand items, and the number of steps each shape needs. The bottom of the ladder is instant. The top is a million steps for the same thousand items - and at ten thousand items it becomes a hundred million.
Do not memorise the numbers - notice the gaps. Between O(n) and O(n squared) there is a factor of a thousand at n=1000, and it gets worse as n grows. Most of this course is about moving a solution one rung down that ladder, and this figure is why that is worth so much effort.
- O(1)
- - constant time - cost does not depend on input size
- O(log n)
- - halving each step - a million items in about twenty steps
- O(n squared)
- - a loop inside a loop - doubling the input quadruples the work
Counting a loop, step by step
Let's turn this into something you can actually do, because reading about counting is not the same as counting. Here is a method that works on any piece of code you meet.
First, find the loops. Code with no loops and no method calls that hide loops is O(1) - it does a fixed amount of work.
Second, for each loop, ask how many times it runs in terms of n. A loop from 0 to n runs n times. A loop that doubles its counter each round - 1, 2, 4, 8 - runs about log n times.
Third, if one loop sits inside another, multiply their counts. If they sit one after the other, add them and keep only the bigger, because addition of different shapes is dominated by the larger one.
That is the whole technique. Everything else is practice.
// A: no loop at all
int first(int[] a) { return a[0]; }
// B: one loop over n
int sum(int[] a) {
int total = 0;
for (int x : a) total += x;
return total;
}
// C: a loop INSIDE a loop - multiply
boolean anyDuplicate(int[] a) {
for (int i = 0; i < a.length; i++)
for (int j = i + 1; j < a.length; j++)
if (a[i] == a[j]) return true;
return false;
}
// D: two loops one AFTER the other - add, then keep the bigger
int sumThenMax(int[] a) {
int total = 0, max = a[0];
for (int x : a) total += x; // n
for (int x : a) max = Math.max(max, x); // n
return total + max; // n + n = 2n, still O(n)
}A no loops -> O(1) B one loop over n -> O(n) C loop inside a loop -> O(n squared) D two loops in sequence -> O(n) + O(n) = O(n)
Counting lines of code instead of steps executed. A single line that asks a list whether it contains a value looks like one step and is really n of them, because it searches the whole list. Whenever you call something, ask what it does inside - the cost belongs to your loop too.
Why this is the lesson everything rests on
You now have the one tool the rest of this course keeps using. Every algorithm you meet from here on is an answer to the same question: can we get this same result while doing less work?
Lesson two takes the loop-inside-a-loop from example C and shows exactly why it becomes unusable, using numbers you can feel. Lesson three shows the opposite - an algorithm that throws away half the problem at every step, and finds one item among a million in about twenty looks.
Before that, one honest caveat. Big-O is about growth, not about speed on a particular day. An O(n squared) algorithm can genuinely beat an O(n log n) one on twenty items, because the constants Big-O discards are still real. That is not a flaw in the theory; it is a reminder that these are two different questions. Big-O tells you what happens as your data grows - and data almost always grows.
If you take one sentence from this lesson, take this: you can predict performance by counting, before writing a line of code. That is the difference between guessing and engineering.
- Cost = steps, not seconds - it holds on any machine
- Judge by the worst case; anyone looks fast on a lucky input
- O(n) means the work grows in a straight line with the input
- Loops nested = multiply; loops in sequence = add and keep the bigger
- Watch for method calls that hide a loop inside one innocent line
Your turn
Reading is not learning. Open each one in NeonFlow and watch your own code run, step by step.
Count the steps yourself
Add a counter to this method so it reports how many comparisons it does, then run it with a target at the start, at the end, and one that is missing. Predict all three numbers before you press run - being wrong is the most useful thing that can happen here.
public class CountSteps {
static int steps = 0;
static int find(int[] a, int target) {
for (int i = 0; i < a.length; i++) {
// TODO: count this comparison
if (a[i] == target) return i;
}
return -1;
}
public static void main(String[] args) {
int[] a = {4, 8, 15, 16, 23, 42};
steps = 0; find(a, 4); System.out.println("first: " + steps);
steps = 0; find(a, 42); System.out.println("last: " + steps);
steps = 0; find(a, 99); System.out.println("missing: " + steps);
}
}Which shape is this?
Three methods, three different shapes. Work out whether each is O(1), O(n) or O(n squared) before running anything, then run it and check the printed counts against your answer.
public class WhichShape {
// TODO: O(?) - write your guess above each method
static int a(int[] xs) {
return xs[0] + xs[xs.length - 1];
}
// TODO: O(?)
static int b(int[] xs) {
int total = 0;
for (int x : xs) total += x;
return total;
}
// TODO: O(?)
static int c(int[] xs) {
int pairs = 0;
for (int i = 0; i < xs.length; i++)
for (int j = i + 1; j < xs.length; j++)
pairs++;
return pairs;
}
public static void main(String[] args) {
int[] xs = new int[8];
System.out.println("a touches 2 items");
System.out.println("b touches " + xs.length + " items");
System.out.println("c counts " + c(xs) + " pairs");
}
}