What quadratic time means
An algorithm is quadratic, O(n²), when the work grows with the square of the input size. Double the input and the work quadruples. Multiply the input by ten and the work grows a hundredfold. The usual source is a loop nested inside another loop, where both depend on n.
fn count_steps(n: usize) -> u64 {
let mut count = 0;
for _ in 0..n {
for _ in 0..n {
count += 1;
}
}
count
}
The inner loop does n iterations, and it runs once for each of the n iterations of the outer loop, so the body runs n × n times:
n = 10 -> 100
n = 100 -> 10000
n = 1000 -> 1000000
Ten times more input gives a hundred times more steps. For n = 100000 that is ten billion steps, which is the point where a quadratic solution stops being usable.
Counting pairs
Many quadratic algorithms compare every element with every other element. With ten numbers, the double loop below visits all pairs of different positions:
let n = v.len();
for i in 0..n {
for j in 0..n {
if i != j {
// compare v[i] and v[j]
}
}
}
That visits 90 ordered pairs when v has 10 elements, which is n × (n - 1). Often the order of a pair does not matter, so the inner loop can start after i:
for i in 0..n {
for j in (i + 1)..n {
// compare v[i] and v[j]
}
}
Now each pair is visited once. The inner loop runs n - 1 times, then n - 2, and so on, so the total is (n - 1) + (n - 2) + ... + 1 = n(n - 1) / 2. For 10 elements that is 45, and for 100 elements it is 4950. This looks like an improvement, but half of n² is still proportional to n², and Big O drops the constant 1/2. The algorithm is O(n²) either way.
Hidden quadratic loops
A quadratic algorithm does not need two visible for loops. One loop that calls a linear function in its body is just as quadratic:
- A loop over
numsthat callsseen.contains(&x)on aVec: up toncomparisons per iteration. - A loop that calls
v.insert(0, x)each time: each call shifts all existing elements. Inserting 100 items at the front one after another moves 0 + 1 + ... + 99 = 4950 elements in total. - A loop that calls
remove(0),clone()oriter().sum()on the whole collection. - A loop that builds a string with
s = format!("{}{}", s, piece), copying everything written so far on every iteration. - A loop that calls
s.chars().nth(i)for each positioni, where each call walks the string from the start.
The fix is usually a better data structure or a single pass: a HashSet for membership tests, a VecDeque for queues, an in-place append for strings, or a Vec<char> for indexing by character.
Not every nested loop is quadratic
Check what bounds each loop:
- If the inner loop runs a fixed number of times, such as
for _ in 0..3, the total is3n, which isO(n). - If the two loops are over different inputs of sizes
aandb, the cost isO(a × b), notO(n²). - If the inner loop halves its range each time, the total is
O(n log n). - Two loops one after the other, not nested, add:
O(n) + O(n)isO(n).
Quadratic time in practice
Quadratic algorithms are often the first correct solution to a problem: compare every pair, check every start and end of a subarray. They are perfectly fine for small inputs. The signal to look for something better is the input limit. If n can reach a hundred thousand or more, an O(n²) solution will be too slow, and you should look for sorting, a hash map or two pointers to bring it down to O(n log n) or O(n).
Common mistakes
- Multiplying the counts of unrelated loops. Two separate loops add; only a nested loop multiplies.
- Reporting
O(n²/2). Drop the constant and writeO(n²). - Missing the loop hidden inside a method call.
- Using the same name
nfor two different inputs.
Key takeaways
- Quadratic means the work grows with
n²: ten times the input is a hundred times the work. - Two nested loops that both depend on
nareO(n²), even if the inner loop starts ati + 1. - A linear method call inside a loop over the same data is also quadratic.
- Fix it with a hash set or map, a better algorithm, or a single pass.
