What 'fast' really means

Speed isn't about a fast laptop. It's about how much WORK your code does. Learn to count the work - and watch a search do it, one step at a time.

16 min read

Watch it run
LinearSearch.java
1 / 12

Call search([4, 8, 15, 16, 23, 42], 16) - a new frame is pushed onto the call stack.

step 1 / 12
Panels
Data structures
No arrays or objects in scope yet -
primitives are shown in Variables.
Legendcurrent linejust swapped / movedbeing comparedijpointers slide to the index they point atactive function framefilled DP cell

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.

Remember these
  • 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.
Words you just learned
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.

Words you just learned
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.

Why count instead of time?

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.

Words you just learned
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.

LinearSearch.java
java
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));
What it prints
3
The loop body runs once per box. Searching for 16 runs it four times - indexes 0, 1, 2, 3 - and returns 3. That count, not the clock, is what we mean by cost.
4
0
8
1
15
2
found
16
3
23
4
42
5
Searching for 16: four boxes opened (0, 1, 2, 3) before a match. Four steps. The index numbers below each box are the addresses the code counts up through.
Words you just learned
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?

CountingSteps.java
java
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 all
What it prints
1
6
6
Same method, same array, three very different costs. Notice the last two match: a missing value costs exactly as much as one hiding at the end, because both force every box open. Six items, six steps - that is n.
Remember these
  • 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
Words you just learned
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.

SameShape.java
java
// 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();
What it prints
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
Look at the bottom row. The constant 3 is still 3, but the +50 has become invisible. That is precisely why Big-O keeps the first kind of difference in mind informally and discards the second entirely.
Words you just learned
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.

Steps to finishfor n = 1,000
O(1)
1
O(log n)
10
O(n)
1,000
O(n log n)
9,966
O(n²)
~1 million

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.

How to read the ladder

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.

Words you just learned
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.

CountThese.java
java
// 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)
}
What it prints
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)
C is the one to stare at. Its inner loop does not run the full n each time - it starts at i+1, so it shrinks. The total is about n squared / 2 comparisons, and Big-O drops the /2, leaving O(n squared). Shrinking does not save you from the shape.
The mistake nearly everyone makes first

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.

Remember these
  • 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.

Problem 1

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.

Starter
java
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);
    }
}
Problem 2

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.

Starter
java
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");
    }
}