When loops nest: O(n squared)

Put a loop inside a loop and the work explodes. See why comparing every item to every other item gets slow shockingly fast - and watch it happen.

18 min read

Watch it run
CountPairs.java
1 / 30

Call countPairs([5, 9, 2, 7]) - a new frame is pushed onto the call stack.

step 1 / 30
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.

A loop inside a loop

Last lesson, one loop walked through the list once - a fair O(n). Now watch what happens when we put a loop inside another loop. The method above counts how many pairs of items exist in a list, so for every item it walks through the rest of the list again. One outer step becomes a whole lap of the inner loop.

Press play and keep your eye on the line 'count = count + 1'. For a list of just four numbers, count how many times it actually runs. It isn't 4. It isn't 8. It's 6 - and the reason it grows so fast is the whole point of this lesson.

Every time the outer loop takes one step, the inner loop runs from scratch. So the total number of inner steps is roughly the list size, multiplied by itself. That multiplication is where the trouble hides.

Imagine it like this: A party of n people where everyone must shake hands with everyone else. 4 people is a handful of handshakes. 100 people is thousands. The crowd grew 25 times, but the handshakes grew over 600 times.

Words you just learned
nested loop
- a loop running inside another loop - the inner one restarts on every outer step

Why squaring is so vicious

If the outer loop runs n times and the inner loop runs about n times for each, the total work is n multiplied by n: n squared. We write it O(n squared), read 'order n squared', and it is the danger zone of everyday code.

Here is why it deserves that name. Compare it to the honest O(n) from last lesson, side by side, and watch what happens as the list grows. The two costs start close and then rip apart.

Steps to finishfor n = 1,000
O(n)
1,000
O(n²)
~1 million

At just 1,000 items, an O(n) pass does 1,000 steps while O(n squared) does a million. Grow the list to a million items and O(n squared) becomes a trillion steps - a program that appears to freeze. Same input, a million times the pain.

Remember these
  • One pass over n: about n steps - O(n)
  • A loop inside a loop over n: about n squared steps - O(n squared)
  • Squaring is brutal: 1,000x more data means 1,000,000x more work
Words you just learned
O(n squared)
- quadratic time - work grows with the square of the input; the mark of nested loops

Spotting it, and beating it

You don't need to count every step to smell O(n squared). Train your eye on one shape: a loop over your data, with another loop over the same data inside it. The sentence 'for every item, look at every other item' should make you pause every single time.

The good news, and the promise of this whole course: most O(n squared) solutions can be rewritten to O(n) with a smarter tool - a hash set, a sort, a single clever pass. You can't fix a cost you can't see, though, which is exactly why we make it move on screen first.

Imagine it like this: Finding duplicate names by comparing every student to every other student is O(n squared). Sorting the names first, then scanning for matching neighbours, is far less work - the payoff of a better plan.

The nested-loop trap

Two loops over the same array is the classic O(n squared). Two loops over different arrays (size n and size m) is only O(n times m) - often fine. The danger is looping over the same data twice.

How to spot quadratic code in the wild

Nested loops are easy to see when they are written as two for statements. In real code they hide, and learning the disguises is what makes this lesson practical.

The most common disguise is a method call inside a loop. list.contains(x) looks like one step and walks the entire list; string.indexOf(y) does the same. A loop containing either is quadratic even though only one loop is visible.

The second disguise is string concatenation in a loop. Strings are immutable, so s += x builds a brand new string each time, copying everything accumulated so far. A thousand appends copies roughly half a million characters.

The third is repeated sorting or searching inside a loop - sorting inside a loop over n items is n times n log n, which is worse than quadratic.

HiddenQuadratic.java
java
// looks linear, is quadratic - contains() walks the whole list
for (String name : names) {
    if (seen.contains(name)) duplicates.add(name);   // O(n) inside O(n)
    seen.add(name);
}

// looks linear, is quadratic - each += copies the whole string
String out = "";
for (String part : parts) out += part;

// the fixes
Set<String> seenFast = new HashSet<>();      // contains() is now O(1)
StringBuilder sb = new StringBuilder();      // append() does not copy
What it prints
10,000 items

list.contains in a loop     ~50,000,000 comparisons    ~380 ms
set.contains in a loop           ~10,000 lookups         ~2 ms

string += in a loop         ~50,000,000 chars copied   ~410 ms
StringBuilder.append             ~10,000 appends         ~1 ms
Neither slow version has two visible loops. The second loop is inside the method you called - which is why 'count the steps executed, not the lines written' is the habit that matters.
Words you just learned
immutable
- cannot be changed after creation - every edit builds a new copy

Recap

A loop inside a loop multiplies: n outer passes times n inner steps is n squared, and doubling the input quadruples the work.

That is survivable at small sizes and impossible at large ones, and the crossover arrives faster than people expect - a million items is a trillion steps.

The skill this lesson builds is not spotting two for statements. It is noticing the loop you did not write, hiding inside a method call on the line in front of you.

Remember these
  • Nested loops multiply; sequential loops add
  • Doubling the input quadruples quadratic work
  • contains, indexOf and string += all hide a loop
  • Fix by remembering (a set or map) instead of re-scanning

Your turn

Reading is not learning. Open each one in NeonFlow and watch your own code run, step by step.

Problem 1

Find the hidden quadratic

This method looks like a single loop and is quadratic. Find the hidden loop, then fix it so the counts collapse. Predict the operation count for 1,000 items before running.

Starter
java
import java.util.*;

public class HiddenLoop {
    static int operations = 0;

    static List<Integer> duplicates(int[] a) {
        List<Integer> seen = new ArrayList<>();
        List<Integer> dupes = new ArrayList<>();
        for (int x : a) {
            operations += seen.size();     // what contains() really costs
            if (seen.contains(x)) dupes.add(x);
            else seen.add(x);
        }
        return dupes;
    }

    // TODO: rewrite using a HashSet and count operations as 1 per lookup

    public static void main(String[] args) {
        int[] a = new int[1000];
        for (int i = 0; i < 1000; i++) a[i] = i;
        operations = 0;
        duplicates(a);
        System.out.println("list version: " + operations + " operations");
    }
}
Problem 2

Watch the crossover

Run the same quadratic method at n = 100, 200, 400 and 800, printing the operation count each time. Watch it quadruple when n doubles - that is the shape, seen from the inside.

Starter
java
public class Crossover {
    static long operations = 0;

    static void allPairs(int n) {
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++)
                operations++;
    }

    public static void main(String[] args) {
        for (int n : new int[]{100, 200, 400, 800}) {
            operations = 0;
            allPairs(n);
            // TODO: print n and operations, and the ratio to the previous run
            System.out.println("n=" + n + "  operations=" + operations);
        }
    }
}