Learn Java

Lesson 41Phase 4: Collections and algorithms

How fast is it? Big-O in plain words

Counting steps instead of seconds, and the handful of growth rates that tell you whether code will cope with a million items.

By Updated 45 min

  • Big-O
  • O(1)
  • O(log n)
  • O(n)
  • O(n log n)
  • O(n²)

Picture a printed phone book: thousands of names in alphabetical order, each with a phone number. Finding one name among 1,000 takes a moment. Now the book has 1,000,000 names. It is a thousand times bigger, so is finding a name a thousand times slower? That depends on how you search. This lesson gives you a way to answer questions like that before you run anything: count the steps, then describe how the count grows.

By the end of this lesson
  • Count the steps a piece of code takes, and say how that count grows when the input grows.
  • Recognize the common growth rates, from O(1) up to O(2n), by the shape of the code.
  • Work out the Big-O of a method with a few rules of thumb.
  • Explain best, average and worst case, and say how much extra memory a method needs.
  • Pick a Java collection by what its operations cost, and turn an O(n²) check into an O(n) one.
From before
You can write loops over arrays, you know that an ArrayList keeps its values in a backing array, and you have used HashMap and HashSet and linked lists. In recursion you met the naive Fibonacci method, where every call makes two more calls. This lesson asks one new question about all of them: how much work does the code do as the data grows?

Measuring speed by counting steps#

Say you want the number for Sana Patel. There are two sensible ways to look.

  • Start at the first name and read every name in order until you reach hers.
  • Open the book in the middle. If her name comes later in the alphabet, keep the second half; if it comes earlier, keep the first half. Open the half you kept in its middle, and repeat.

With 1,000 names, the first way can take 1,000 looks. The second takes at most 10, because 1,000 halved ten times leaves less than one name. With 1,000,000 names the first way can take 1,000,000 looks, and the second at most 20.

Here are both searches as code. To keep the program small, the book is a row of numbered pages, and we look for the last page, which is the worst place to be for the first method. Each method counts its looks.

PhoneBook.java
1public class Main {
2 // Read the pages from the front until we reach the page we want.
3 static int oneByOne(int pages, int target) {
4 int looks = 0;
5 for (int page = 1; page <= pages; page++) {
6 looks++;
7 if (page == target) {
8 break;
9 }
10 }
11 return looks;
12 }
13
14 // Open the middle, keep the half that must hold the target, repeat.
15 static int halving(int pages, int target) {
16 int looks = 0;
17 int low = 1;
18 int high = pages;
19 while (low <= high) {
20 int middle = (low + high) / 2;
21 looks++;
22 if (middle == target) {
23 break;
24 } else if (middle < target) {
25 low = middle + 1;
26 } else {
27 high = middle - 1;
28 }
29 }
30 return looks;
31 }
32
33 public static void main(String[] args) {
34 int[] sizes = {10, 100, 1000};
35 for (int pages : sizes) {
36 System.out.println(pages + " pages: " + oneByOne(pages, pages)
37 + " looks one by one, " + halving(pages, pages) + " looks by halving");
38 }
39 }
40}
Prints
›10 pages: 10 looks one by one, 4 looks by halving
›100 pages: 100 looks one by one, 7 looks by halving
›1000 pages: 1000 looks one by one, 10 looks by halving

Compare the three lines. One by one, the looks go 10, 100, 1000: ten times the pages means ten times the looks. By halving they go 4, 7, 10, so ten times the pages adds only about three looks.

Why not use a stopwatch?

You could time the two methods instead. The trouble is that a stopwatch measures your computer as much as your code. A faster laptop, another program running in the background, or Java speeding up as it runs (the JVM compiles busy code into faster machine code while the program is running) all change the time. The count of looks is the same on every machine, every time.

So programmers measure speed by counting. A step is any small piece of work that takes the same time however big the input is, such as comparing two values or reading one array slot. The size of the input is called n: the number of names in the book, say, or of items in a list. Then the question becomes: as n grows, how does the number of steps grow?

Key idea

Count the steps, then ask how the count grows when n grows. That growth tells you whether code will cope with a million items.

Big-O: a name for the growth#

Big-O notation is a short way to write down how the count grows. Reading every page is O(n), said “big O of n” or “order n”: the work grows in proportion to n, so twice the pages means twice the looks. Halving is O(log n): the work grows with the number of times you can halve n. How a method's running time grows with the size of its input is its time complexity, and Big-O is the usual way to write it down.

Big-O deliberately throws detail away. It ignores the exact number of steps and keeps only the shape of the growth. A method that takes 3n + 2 steps and a method that takes n steps are both O(n), because when n doubles, both roughly double. Big-O answers one question: if the input gets bigger, how much more work is there?

What log n means here
In this course log n means the number of times you can halve n before you get down to 1. Starting from 8 you halve to 4, then 2, then 1, so log 8 is 3. log 1,000 is about 10, and log 1,000,000 is about 20. It grows very slowly: squaring n only doubles log n. (Mathematicians call this the logarithm base 2.)

The growth rates you will meet#

Most code falls into one of a handful of growth rates, and each comes with a shape of code you can learn to spot. Here they are from fastest to slowest.

O(1): constant time

The work stays the same however big the input gets. Reading scores[3] is one step whether the array holds 6 numbers or 6 million. Java knows where the array starts and how big each slot is, so it works out where slot 3 lives and goes straight there, the way a room number takes you to room 304 without knocking on the 303 doors before it. Calling get(3) on an ArrayList is O(1) too, because the list keeps its values in an array.

Finding a value when you do not know its position is a different job. Step through this program and compare its two halves.

Lookup.java
1public class Main {
2 public static void main(String[] args) {
3 int[] scores = {72, 95, 61, 88, 79, 90};
4
5 // By position: go straight to slot 3.
6 int fourth = scores[3];
7 System.out.println("Slot 3 holds " + fourth);
8
9 // By value: walk along until we meet 90.
10 int looks = 0;
11 int slot = -1;
12 for (int i = 0; i < scores.length; i++) {
13 looks++;
14 if (scores[i] == 90) {
15 slot = i;
16 break;
17 }
18 }
19 System.out.println("90 is in slot " + slot + ", found after " + looks + " looks");
20 }
21}
Prints
›Slot 3 holds 88
›90 is in slot 5, found after 6 looks

Reading slot 3 is a single line with no loop. Finding 90 took six looks, one for each slot up to the match, and a longer array would take more. When you know where a value is, reaching it costs O(1); when you have to search for it, you pay for the search.

O(log n): logarithmic time

Here the code cuts the problem in half each time around a loop. The phone book's halving search has this shape, and so does this loop, which counts how many times n can be halved before it reaches 1.

Halving.java
1public class Main {
2 static int halvings(int n) {
3 int count = 0;
4 while (n > 1) {
5 n = n / 2;
6 count++;
7 }
8 return count;
9 }
10
11 public static void main(String[] args) {
12 int[] sizes = {1_000, 1_000_000, 1_000_000_000};
13 for (int n : sizes) {
14 System.out.println(n + " items: " + halvings(n) + " halvings");
15 }
16 }
17}
Prints
›1000 items: 9 halvings
›1000000 items: 19 halvings
›1000000000 items: 29 halvings

It prints 9, 19 and 29. Going from a thousand items to a million costs only 10 more halvings, and going on to a billion costs 10 more again. Each doubling of n adds just one step. (The underscores in 1_000_000 are only there to make the number easy to read; Java ignores them.)

That is why the halving search through a sorted list, called binary search, is so fast. You will write it yourself in searching and sorting.

O(n): linear time

Here the code makes one pass over the input, doing a fixed amount of work for each item. Reading the phone book page by page is O(n), and so are finding 90 above, adding up an array and finding the largest value in a list. In each of them, doubling the number of items doubles the work.

O(n log n): a loop around a halving job

Do an O(log n) job once for each of n items and you get n × log n steps. A loop over n items with a halving loop inside it has this shape. In practice you meet O(n log n) whenever you sort: Arrays.sort, List.sort and Collections.sort all take O(n log n) time. It grows a little faster than O(n) and far slower than O(n²), which is why sorting even a million items is quick. How a sort manages it is a story for the next lesson.

O(n²): quadratic time

Here the code has a loop inside a loop, and both run over the input. A common case is checking a list for repeats by comparing every pair of values. This version counts its comparisons, and the arrays hold no repeats, so it has to try every pair.

Pairs.java
1public class Main {
2 static int comparisons = 0;
3
4 static boolean hasDuplicate(int[] values) {
5 for (int i = 0; i < values.length; i++) {
6 for (int j = i + 1; j < values.length; j++) {
7 comparisons++;
8 if (values[i] == values[j]) {
9 return true;
10 }
11 }
12 }
13 return false;
14 }
15
16 public static void main(String[] args) {
17 int[] ten = {31, 4, 15, 9, 26, 53, 58, 97, 93, 23};
18 int[] twenty = {31, 4, 15, 9, 26, 53, 58, 97, 93, 23,
19 84, 62, 64, 33, 83, 27, 95, 2, 88, 41};
20
21 System.out.println("10 values: " + hasDuplicate(ten) + " after " + comparisons + " comparisons");
22 comparisons = 0;
23 System.out.println("20 values: " + hasDuplicate(twenty) + " after " + comparisons + " comparisons");
24 }
25}
Prints
›10 values: false after 45 comparisons
›20 values: false after 190 comparisons

10 values need 45 comparisons; 20 values need 190. Doubling n roughly quadruples the work. The exact count is n * (n - 1) / 2: the first value is compared with the 9 after it, the second with the 8 after it, and so on down to 1, and 9 + 8 + ... + 1 = 45. That is about half of n², and a half makes no difference to the shape, so the method is O(n²).

Quadratic code hides inside method calls
Two nested loops are easy to see. A loop that calls a method with a loop of its own is harder to spot. contains(x) on an ArrayList checks the values one by one, so calling it inside a loop over the same list is O(n²) as well. You will see what each collection method costs later in this lesson.

O(2n): exponential time

Each step up in n doubles the work, or close to it. The example you already know is the naive Fibonacci method from recursion, where every call makes two more calls. Here it is again with a counter.

Fib.java
1public class Main {
2 static int calls = 0;
3
4 static int fib(int n) {
5 calls++;
6 if (n <= 1) {
7 return n;
8 }
9 return fib(n - 1) + fib(n - 2);
10 }
11
12 public static void main(String[] args) {
13 for (int n = 4; n <= 12; n += 2) {
14 calls = 0;
15 int result = fib(n);
16 System.out.println("fib(" + n + ") = " + result + " took " + calls + " calls");
17 }
18 }
19}
Prints
›fib(4) = 3 took 9 calls
›fib(6) = 8 took 25 calls
›fib(8) = 21 took 67 calls
›fib(10) = 55 took 177 calls
›fib(12) = 144 took 465 calls

Each time n goes up by 2, the number of calls grows about 2.6 times: 9, 25, 67, 177, 465. Keep going and it gets out of hand fast. fib(40) makes over 330 million calls, and fib(50) over 40 billion.

Four of them side by side

This program counts three loop shapes at once, doubling n each time. The first column is n itself, which is also what a single loop over n items counts.

Growth.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("n\tlog n\tn log n\tn*n");
4 for (int n = 2; n <= 16; n = n * 2) {
5 int logSteps = 0;
6 for (int i = n; i > 1; i = i / 2) {
7 logSteps++;
8 }
9
10 int nLogSteps = 0;
11 for (int i = 0; i < n; i++) {
12 for (int j = n; j > 1; j = j / 2) {
13 nLogSteps++;
14 }
15 }
16
17 int squareSteps = 0;
18 for (int i = 0; i < n; i++) {
19 for (int j = 0; j < n; j++) {
20 squareSteps++;
21 }
22 }
23
24 System.out.println(n + "\t" + logSteps + "\t" + nLogSteps + "\t" + squareSteps);
25 }
26 }
27}
Prints
›n log n n log n n*n
›2 1 2 4
›4 2 8 16
›8 3 24 64
›16 4 64 256

Each time n doubles, the halving count goes up by just one, while the loop in a loop multiplies by 4. The n log n count sits in between: going from 8 items to 16 takes it from 24 to 64, a little under 3 times as many. The table sums up all six shapes.

Growth rateShape of the codeExamples
O(1)No loop over the inputscores[i], get(i) on an ArrayList
O(log n)A loop that halves (or doubles) each timeBinary search in a sorted array
O(n)One loop over the inputAdding up an array, searching an unsorted list
O(n log n)A halving job done once per itemArrays.sort, List.sort
O(n²)A loop over the input inside another oneComparing every pair of values
O(2n)Each call makes two more calls on a job only slightly smallerNaive fib(n)
The fastest code is at the top and the slowest at the bottom.

Feel the difference

Numbers like 256 do not look frightening. The gap opens up as n grows. Drag n in the chart below and watch the curves pull apart, then read the table under it, which turns each count into time by pretending every step takes one microsecond, a millionth of a second.

Growth rates side by siden = 100

n, the number of items

100
11,000,000
Chart height
GrowthSteps at n = 100Time at 1 µs a step
11 µs
77 µs
100100 µs
664664 µs
10,00010 ms
about 1.27 × 1030longer than the age of the universe
Steps are each formula worked out at n and rounded, with log meaning log base 2. The time pretends one step takes one microsecond (µs), a millionth of a second; a millisecond (ms) is a thousandth. The small tick on the slider is n = 1,000, the chart's right edge. Tap a row to pick out its curve.

Slide all the way to n = 1,000,000. O(log n) needs about 20 microseconds, O(n) one second and O(n log n) about 20 seconds. O(n²) needs about 11.6 days. O(2n) is hopeless long before that: at n = 30 it already takes about 18 minutes, and by n = 80 it takes longer than the universe has existed.

Faster computers do not close the gap
A real processor does far more than a million simple steps a second, but a faster computer divides every time in the table by the same amount. On a machine a thousand times faster, the O(n²) job drops from 11.6 days to about 17 minutes, while the O(n log n) one finishes in 20 milliseconds. The O(n²) job is still about 50,000 times slower, the same gap as before.
Key idea

For big inputs, the growth rate matters far more than the speed of the computer. O(n²) work on a million items takes days at a microsecond a step; O(n log n) work takes seconds.

Working out the Big-O of code#

You rarely need to count every step. A few rules of thumb get you to the answer.

Drop the constants

A method that walks an array twice, once to find the smallest value and once to find the largest, takes about 2n steps. Big-O calls that O(n). A constant multiplier like the 2 depends on details such as what you count as one step, and it does not change the shape: double n and 2n doubles too. Fixed extra work goes the same way, so 3n + 5 is O(n) and a method that always does 7 steps is O(1).

Keep the biggest term

When a count has several parts, the fastest-growing part wins. n² + n is O(n²): at n = 1,000 the n² part is 1,000,000 and the n part only 1,000, a tenth of one percent of the total. The bigger n gets, the less the smaller parts matter.

Steps one after another add

If a method runs one loop and then another, add their costs. An O(n) loop followed by another O(n) loop is O(n + n), which is O(2n), which is O(n). An O(n) loop followed by an O(n²) loop is O(n + n²), which is O(n²) once you keep the biggest term.

Loops inside loops multiply

A loop that runs n times, with a loop inside that also runs n times, does n × n work: O(n²). If the inner loop halves instead, the total is n × log n. When the two loops run over different things, keep both sizes: a loop over m customers with a loop over n products inside it does m × n work, O(m × n), and calling both sizes n would hide that.

A loop that halves or doubles is logarithmic

If the loop variable is divided by 2, or multiplied by 2, each time around, the loop runs about log n times. Dividing by 3 or by 10 instead is still O(log n). Logs to different bases differ only by a constant multiplier, and constants are dropped.

Put the lines in orderDoubling.java
Put the lines in order so the program counts how many times 1 has to be doubled to reach at least 100, and prints 7 doublings to reach 128.

Tap a line, then tap where it should go. Or use the arrows.

Now use the rules on three counters at once. Before you run it, decide which counter grows like n, which like log n, and which like n².

Predict the outputCounters.java
1public class Main {
2 public static void main(String[] args) {
3 int n = 16;
4
5 int a = 0;
6 for (int i = 0; i < n; i++) {
7 a++;
8 }
9
10 int b = 0;
11 for (int i = n; i > 1; i = i / 2) {
12 b++;
13 }
14
15 int c = 0;
16 for (int i = 0; i < n; i++) {
17 for (int j = 0; j < n; j++) {
18 c++;
19 }
20 }
21
22 System.out.println(a + " " + b + " " + c);
23 }
24}

With n = 16, what does the program print?

a counts one loop: n, so 16. b halves from 16 to 8 to 4 to 2 and stops before 1: log n, so 4. c counts a loop in a loop: n × n, so 256.

Quick check
What is the Big-O of for (int i = 0; i < n; i += 2) { total += prices[i]; }?
Quick check
A method runs one loop over an array of n prices to add them up, and then a second, separate loop over the same array to find the highest. What is its Big-O?
Key idea

Drop constants, keep the biggest term, add what runs in sequence, multiply what is nested, and count a halving loop as log n. Those rules give you the Big-O of most code by reading it.

Best, average and worst case#

The same method can take very different numbers of steps on inputs of the same size. Searching a guest list for a name finishes at once if the name is first, and checks every name if it is last or missing.

Search.java
1public class Main {
2 static int comparisons = 0;
3
4 static int indexOf(String[] names, String target) {
5 for (int i = 0; i < names.length; i++) {
6 comparisons++;
7 if (names[i].equals(target)) {
8 return i;
9 }
10 }
11 return -1;
12 }
13
14 public static void main(String[] args) {
15 String[] guests = {"Ana", "Ben", "Chen", "Dev", "Eli", "Fay", "Gus", "Hana"};
16 String[] wanted = {"Ana", "Dev", "Hana", "Zoe"};
17
18 for (String name : wanted) {
19 comparisons = 0;
20 int at = indexOf(guests, name);
21 System.out.println(name + ": index " + at + ", comparisons: " + comparisons);
22 }
23 }
24}
Prints
›Ana: index 0, comparisons: 1
›Dev: index 3, comparisons: 4
›Hana: index 7, comparisons: 8
›Zoe: index -1, comparisons: 8

There are eight guests. Ana is found after 1 comparison: that is the best case, the luckiest input of this size. Hana, at the end, and Zoe, who is not on the list at all, both need all 8: the worst case, the input that makes the method work hardest. If every guest is equally likely to be the one you want, you check about half the list, 4.5 comparisons here: the average case.

So searching an unsorted list is O(1) in the best case and O(n) in the average and worst cases, since n / 2 is still O(n). When someone gives one Big-O for a method without saying which case, they usually mean the worst case, because it is a promise: the method never does worse than that. The big exception is hashing. The famous O(1) of a HashMap is an average, as the next section explains.

Test with the worst case on purpose
When you check how your code copes with big inputs, feed it the input that makes it work hardest, such as a name that is not in the list or an array with no repeats. A lucky best case tells you very little about how slow the code can get.

What Java's collections cost#

Every collection makes trade-offs, and knowing what each operation costs lets you pick the right one. The Java API documentation states most of these costs in each class's description. These are the ones you will use most.

OperationCostWhy
Array: a[i], to read or writeO(1)Java works out where slot i is and goes straight there.
ArrayList: get(i), set(i, x)O(1)The values sit in a backing array.
ArrayList: add(x) at the endO(1) amortizedUsually x goes into a spare slot. Now and then the list copies everything into a bigger array, rarely enough to average out.
ArrayList: add(0, x) or remove(0) at the frontO(n)Every value after the front shifts one place.
LinkedList: get(i)O(n)It walks node by node from whichever end is nearer.
LinkedList: addFirst, addLast, removeFirstO(1)Only the links at one end change.
HashMap: get, put, containsKeyO(1) on averageThe key's hash code leads straight to its bucket.
TreeMap: get, put, remove, containsKeyO(log n), guaranteedIt walks down a balanced tree that stays about log n levels deep.
contains(x) on an ArrayList or LinkedListO(n)It checks the values one by one.
contains(x) on a HashSetO(1) on averageA HashSet keeps its values in a HashMap.
contains(x) on a TreeSetO(log n)A TreeSet keeps its values in a TreeMap.
ArrayDeque: push, pop, offer, pollO(1) amortizedIt works at the two ends of an array that grows when it fills up.
PriorityQueue: offer, pollO(log n)It moves one item up or down a tree about log n levels deep to keep the smallest in front. peek is O(1).
Arrays.sort, List.sort, Collections.sortO(n log n)Java's sorts are tuned versions of quicksort and merge sort.
Based on the class descriptions in the Java SE 25 API documentation. n is the number of items in the collection.

Amortized: cheap when averaged over many adds

In the ArrayList lesson you watched an ArrayList run out of spare slots and copy every value into an array half as big again. That copy costs O(n), so now and then a single add is slow. But after each copy, many cheap adds follow before the next one, and the bigger the list gets, the longer the gap between copies. Spread over a long run of adds, the cost per add stays constant. Averaging the cost over a whole sequence of operations like this is called amortized cost. The ArrayList documentation puts it in one line: adding n elements requires O(n) time. You will see why the sums work out when you build your own ArrayList.

Average O(1): what a HashMap promises

A HashMap's O(1) depends on its keys' hashCode spreading them out across the buckets, as you saw in HashMap and HashSet. If many keys land in the same bucket, a lookup has to search through that bucket. With well-behaved keys such as String and Integer that does not happen in practice, and get and put stay fast however many entries the map holds.

Quick check
invited is an ArrayList of n names, and arrived is another list of n names. What is the Big-O of for (String guest : arrived) { if (invited.contains(guest)) { count++; } }?
Pick the collection for the job you do most
If your code keeps asking “is this value in here?”, use a HashSet, or a HashMap when each value has data attached; a List has to search for the answer. If it mostly adds to the end and reads by position, an ArrayList is the right fit.

Making a slow check fast#

Back to the duplicate check from the O(n²) section. Comparing every pair works, but its 190 comparisons for 20 values become about 500 billion for a million values. There are two good ways to do better.

Sort first: O(n log n)

After sorting, equal values end up next to each other, so one pass that compares each value with the one before it finds any repeat. Arrays.sort(sorted) sorts an array in place, and Arrays.copyOf(values, values.length)makes a copy first, so the caller's array keeps its order.

SortFirst.java
1import java.util.Arrays;
2
3public class Main {
4 // Returns true if any value appears more than once.
5 static boolean hasDuplicate(int[] values) {
6 int[] sorted = Arrays.copyOf(values, values.length);
7 Arrays.sort(sorted);
8 for (int i = 1; i < sorted.length; i++) {
9 if (sorted[i] == sorted[i - 1]) {
10 return true;
11 }
12 }
13 return false;
14 }
15
16 public static void main(String[] args) {
17 System.out.println(hasDuplicate(new int[] {42, 7, 19, 7, 3}));
18 System.out.println(hasDuplicate(new int[] {5, 1, 4, 2}));
19 }
20}
Prints
›true
›false

It prints true, then false. Now cost it with the rules. Copying is O(n), sorting is O(n log n), and the pass is O(n). They run one after another, so they add: O(n + n log n + n). Keeping the biggest term leaves O(n log n). For a million values that is roughly 20 million steps instead of 500 billion.

Remember what you have seen: O(n)

A HashSet answers “have I seen this value before?” in O(1) on average. So walk the array once. For each value, ask the set whether it is already there; if it is, you have found a repeat, and if not, add it. That is n rounds of O(1) work: O(n) on average, with no sorting at all.

ExerciseRepeats.java
Finish hasDuplicate so it makes a single pass over values with the HashSet seen. For each value, return true if seen already contains it; otherwise add it to seen. If the loop ends without finding a repeat, return false.

For a million values, that is about a million set operations, against roughly 20 million steps for sorting first and 500 billion comparisons for checking every pair.

Key idea

The biggest speed-ups come from changing the algorithm or the data structure. Swapping a pair-by-pair search for a HashSet took this check from O(n²) to O(n).

Space complexity: memory counts too#

Time is one cost of running code, and memory is the other. Space complexity describes how much extra memory a method needs as n grows, written with Big-O in the same way. The input itself is usually left out of the count; what matters is what the method adds.

The duplicate checks make a good comparison. The pair-by-pair version needs two index variables whatever the size of the array: O(1) extra space. The sort-first version copies the array: O(n). The HashSet version may end up storing every value: O(n). The HashSet bought its speed with memory, and trading memory for time like this is one of the most common moves in programming.

Recursion has a memory cost that is easy to miss. Every call that has not returned yet keeps a frame on the call stack, as you saw in recursion. Step through both sums below and watch the call stack.

Sum.java
1public class Main {
2 static int sumLoop(int n) {
3 int total = 0;
4 for (int i = 1; i <= n; i++) {
5 total += i;
6 }
7 return total;
8 }
9
10 static int sumRecursive(int n) {
11 if (n == 0) {
12 return 0;
13 }
14 return n + sumRecursive(n - 1);
15 }
16
17 public static void main(String[] args) {
18 System.out.println(sumLoop(4));
19 System.out.println(sumRecursive(4));
20 }
21}
Prints
›10
›10

Both print 10. The loop does its work inside one frame with a few variables, whatever n is: O(1) extra space. The recursive version stacks up a frame for every call, from sumRecursive(4) down to sumRecursive(0), so five frames for n = 4 and n + 1 frames in general: O(n) space. Make n large enough and the stack runs out, which is the StackOverflowError you met in the recursion lesson.

Next: searching and sorting
You now have the words to judge an algorithm. In searching and sorting you build binary search, which is O(log n), and several sorts, from simple O(n²) ones to merge sort, which reaches O(n log n).

Try it

Answer each one in your head or on paper first, then open the answer.

1
What is the Big-O of int last = values[values.length - 1];? Does the length of the array change the answer?
2
Write each step count in Big-O: 4n + 10, n² + 50n, 7, and n + log n.
3
This loop multiplies i by 3 each time. What does it print for n = 1000, and what is its Big-O?
4
The inner loop here runs up to i, not up to n. What does it print for n = 10, and is it still O(n²)?
5
An O(n²) method takes 2 seconds on 10,000 items. Roughly how long does it take on 20,000 items? How long would an O(n) method that also took 2 seconds take?
6
A spell checker checks thousands of words against a dictionary of 100,000 words. Should the dictionary be an ArrayList or a HashSet, and why?

Practice

Work through these on your own. Each one builds on this page.

1
Write int digits(int n), which counts the digits of a positive number by dividing it by 10 until it reaches 0. Try it on 7, 1234 and 1000000000. What is its Big-O, where n is the number itself?
2
The seats in a small theater are numbered 0 to 99. Write boolean hasDuplicate(int[] seats) in O(n) without a HashSet, using a boolean[] taken = new boolean[100] that marks each seat as you see it. How much extra space does it need, and does that grow with the number of tickets?
3
Building a string of n characters with s += "x"; in a loop copies the whole string so far on every pass: 1 character, then 2, then 3, up to n. Add those up and give the Big-O of the whole loop. Then explain what changes with a StringBuilder and its append.
4
On your own machine with a JDK installed, time the pair-by-pair hasDuplicate with System.nanoTime() on arrays of 10,000, 20,000 and 40,000 different values. Does the time roughly multiply by 4 each time n doubles? Then time the HashSet version on the same arrays.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email