Measuring Algorithm Efficiency
Growth Rates and Why Constants Eventually Don't Matter
Read aloud is unavailable in this browser.
The Tortoise, the Hare, and the Exponential Monster
Imagine two algorithms for the same problem:
- Algorithm A: Takes 1,000,000n operations (yes, a million operations per element)
- Algorithm B: Takes n² operations
Which is faster?
On small inputs, Algorithm B crushes Algorithm A. At n=10, A does 10 million operations while B does just 100. At n=100, A does 100 million operations while B does only 10,000.
But watch what happens as n grows:
| n | Algorithm A (1,000,000n) | Algorithm B (n²) | Winner |
|---|---|---|---|
| 100 | 100,000,000 | 10,000 | B by 10,000x |
| 1,000 | 1,000,000,000 | 1,000,000 | B by 1,000x |
| 10,000 | 10,000,000,000 | 100,000,000 | B by 100x |
| 100,000 | 100,000,000,000 | 10,000,000,000 | B by 10x |
| 1,000,000 | 1,000,000,000,000 | 1,000,000,000,000 | Tie |
| 10,000,000 | 10,000,000,000,000 | 100,000,000,000,000 | A by 10x |
| 100,000,000 | 100,000,000,000,000 | 10,000,000,000,000,000 | A by 100x |
At n = 1,000,000, they're equal. Beyond that, A—despite its enormous constant factor—is faster. And the gap grows forever.
This is why growth rates matter more than constants. Eventually, always, the growth rate dominates.
By the end of this page, you will deeply understand why growth rates dominate constant factors at scale, how to compare functions by their asymptotic behavior, and why algorithm analysis focuses on growth class rather than exact operation counts. This understanding prepares you for formal Big-O notation.
Understanding Growth Rates
A growth rate describes how a function increases as its input increases. In algorithm analysis, we characterize algorithms by the growth rate of their operation count as a function of input size.
The key insight is that not all growth rates are equal. Some functions grow slowly; others explode. The relative ordering of growth rates is absolute and unchanging—no matter how much you scale or shift the functions.
From slowest to fastest growing: O(1) < O(log n) < O(√n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!) < O(nⁿ). Each function in this sequence eventually surpasses all functions to its left, no matter how large their constant factors.
Why the hierarchy is absolute:
Consider f(n) = n and g(n) = n². No matter what constant you multiply n by, eventually n² will be larger:
- Is 1000n > n²? Yes, when n < 1000. But not when n ≥ 1000.
- Is 1,000,000n > n²? Yes, when n < 1,000,000. But not when n ≥ 1,000,000.
- Is 10^100 × n > n²? Yes, when n < 10^100. But not when n ≥ 10^100.
For any constant C, there exists an N beyond which n² > Cn. The quadratic always wins eventually.
This is the essence of asymptotic analysis: we care about what happens as n → ∞, where growth rates are the only thing that matters.
| Growth Rate | n=10 | n=100 | n=1,000 | n=10,000 | Character |
|---|---|---|---|---|---|
| O(1) | 1 | 1 | 1 | 1 | Constant—doesn't grow |
| O(log n) | 3.3 | 6.6 | 10 | 13.3 | Extremely slow growth |
| O(n) | 10 | 100 | 1,000 | 10,000 | Linear—proportional |
| O(n log n) | 33 | 664 | 10,000 | 132,877 | Log factor overhead |
| O(n²) | 100 | 10,000 | 1,000,000 | 100,000,000 | Quadratic explosion |
| O(2ⁿ) | 1,024 | 10³⁰ | 10³⁰¹ | — | Exponential catastrophe |
The Crossover Point — When Better Growth Wins
Every pair of algorithms with different growth rates has a crossover point—the input size where the algorithm with better growth overtakes the one with worse growth (regardless of constants).
This crossover point determines when asymptotic analysis predictions become actionable.
Finding the crossover:
Given:
- Algorithm A: f(n) = 100n log n
- Algorithm B: g(n) = n²
Crossover occurs when: 100n log n = n²
Simplifying: 100 log n = n
Solving numerically: n ≈ 1,000
For n < 1,000, Algorithm B is faster despite its worse growth rate. For n > 1,000, Algorithm A is faster, and the gap widens with scale.
Practical implication: If your inputs are always small (n < 1,000), you might prefer B for its simplicity or lower constant. If your inputs can be large, A is the only sensible choice.
Real-World Crossover Examples
Practical cases where crossover points matter:
Example 1
Insertion Sort vs Merge SortInsertion sort (O(n²)) beats merge sort (O(n log n)) for n < ~50Explanation
That's why production sort implementations (TimSort, IntroSort) switch to insertion sort for small subarrays.
Example 2
Linear Search vs Hash TableLinear search (O(n)) beats hash table (O(1)) for n < ~10Explanation
Hash computation overhead exceeds the cost of a small linear scan. For tiny collections, arrays beat hash tables.
Example 3
Matrix Multiplication: Strassen vs StandardStrassen's O(n^2.8) beats standard O(n³) only for n > ~100Explanation
Strassen has larger constants. It's only used for large matrices in practice.
Near the crossover point, constants dominate. Far beyond it, growth rates dominate. Most analysis focuses on growth rates because (1) data often grows, and (2) predicting the crossover precisely requires implementation details. Better to choose the right growth rate and optimize constants afterward.
Why Constants Are Ignored in Asymptotic Analysis
When we say an algorithm is O(n²), we're deliberately ignoring constants. The algorithm might be n² or 5n² or 0.001n²—they're all O(n²). Why do we make this choice?
- •Constants vary by implementation — The same algorithm coded in C vs Python vs JavaScript will have different constant factors. We want to analyze the algorithm, not the implementation.
- •Constants vary by hardware — A 3 GHz CPU runs 3x faster than a 1 GHz CPU, but both execute the same algorithm. The constant factor changes; the growth rate doesn't.
- •Constants are dwarfed at scale — For sufficiently large n, the growth rate dominates so completely that constants become rounding errors.
- •Constants are hard to measure — Precisely counting operations is tedious and error-prone. Determining the growth rate is usually straightforward.
- •Constants are refineable later — After choosing the right algorithm (right growth rate), you optimize constants. But fixing the wrong growth rate requires starting over.
The mathematical justification:
When we write f(n) = O(g(n)), we're asserting that for sufficiently large n, f grows no faster than g (up to a constant factor). Formally:
f(n) = O(g(n)) means there exist constants c > 0 and n₀ such that for all n > n₀: f(n) ≤ c × g(n)
This definition explicitly absorbs constants into c and ignores behavior for small n (below n₀). It's designed to capture asymptotic growth while discarding implementation-specific details.
Asymptotic analysis sacrifices precision for generality. We don't know the exact constant, but we know the growth class. This tells us how performance scales, which is usually more important than how fast it is at one specific size.
When Constants Actually Matter
Despite the focus on growth rates, there are situations where constants can't be ignored:
1. Input sizes are bounded and small
If your data is always under 100 elements, crossing over from O(n²) to O(n log n) might never happen. The algorithm with smaller constants wins regardless of growth rate.
2. Comparing algorithms in the same complexity class
If two algorithms are both O(n log n), the only difference is constants. Merge sort and heapsort have the same complexity; their constant factors and cache behavior determine real-world performance.
3. Tight latency requirements
When you have hard deadlines (microseconds for trading, milliseconds for games), the exact constant—not just the growth rate—determines if you meet the requirement.
4. Very large constants can change the story
"O(n)" sounds fast, but an algorithm with 10^9 n operations is effectively useless for all practical n. The hidden constant makes theoretical efficiency practically irrelevant.
| Situation | Constants Matter? | Recommended Approach |
|---|---|---|
| Data size grows indefinitely | Less important | Focus on growth rate; optimize constants later |
| Data size is always small (<100) | Very important | Benchmark implementations; choose lower constants |
| Comparing same complexity class | Crucial | Benchmark extensively; constants are the only difference |
| Hard latency requirements | Important | Calculate expected time from constant × growth at target n |
| Extreme constants (10^6 per element) | Critical | These algorithms may be unusable despite good growth |
Some theoretically efficient algorithms have such enormous constants that they're only faster for absurdly large inputs—larger than atoms in the universe. These 'galactic algorithms' are theoretically interesting but practically useless. Matrix multiplication has several such algorithms with good asymptotic complexity but impractical constants.
Visualizing Growth Rate Dominance
Numbers in tables are precise, but visualization makes growth rate dominance visceral. Consider these operation counts for various algorithms on increasing input sizes:
At n = 1,000:
| Complexity | Operations | If 1 op = 1µs, time = |
|---|---|---|
| O(1) | 1 | 1 microsecond |
| O(log n) | 10 | 10 microseconds |
| O(n) | 1,000 | 1 millisecond |
| O(n log n) | 10,000 | 10 milliseconds |
| O(n²) | 1,000,000 | 1 second |
| O(n³) | 1,000,000,000 | 16.7 minutes |
| O(2ⁿ) | 10^301 | Longer than universe age |
Now scale to n = 1,000,000:
| Complexity | Operations | If 1 op = 1µs, time = |
|---|---|---|
| O(1) | 1 | 1 microsecond |
| O(log n) | 20 | 20 microseconds |
| O(n) | 1,000,000 | 1 second |
| O(n log n) | 20,000,000 | 20 seconds |
| O(n²) | 10^12 | 11.6 days |
| O(n³) | 10^18 | 31.7 million years |
| O(2ⁿ) | — | Cannot be computed |
The pattern is unmistakable:
Moving from n=1,000 to n=1,000,000 (1000x increase):
- O(n) went from 1 millisecond to 1 second (1000x)
- O(n²) went from 1 second to 11.6 days (1,000,000x)
- O(2ⁿ) went from unthinkable to even more unthinkable
The gap between complexity classes grows with scale. This is why, for large-scale systems, choosing the right complexity class is non-negotiable.
Modern computers can do roughly 10^8 to 10^9 simple operations per second. If your algorithm requires more than ~10^9 operations, it will take multiple seconds—noticeable and often unacceptable. Use this as a quick check: will my complexity × expected input size exceed 10^9?
Lower-Order Terms and Their Fate
When we analyze an algorithm and find it performs 3n² + 5n + 12 operations, we reduce this to O(n²). What happens to the 5n and 12?
They become negligible.
Consider 3n² + 5n + 12 for various n:
| n | 3n² | 5n | 12 | Total | % from n² term |
|---|---|---|---|---|---|
| 10 | 300 | 50 | 12 | 362 | 83% |
| 100 | 30,000 | 500 | 12 | 30,512 | 98.3% |
| 1,000 | 3,000,000 | 5,000 | 12 | 3,005,012 | 99.8% |
| 10,000 | 300,000,000 | 50,000 | 12 | 300,050,012 | 99.98% |
By n=1,000, the lower-order terms contribute less than 0.2% of the total. By n=10,000, they're barely a rounding error.
Why lower-order terms eventually vanish:
When one term grows faster than another, it eventually dominates completely. n² grows from n to n² (factor of n). n grows from n to n (factor of 1). The ratio between them is n, which goes to infinity.
Mathematically:
lim (n→∞) (5n) / (3n²) = lim (n→∞) 5/(3n) = 0
The linear term becomes zero percent of the quadratic as n grows.
The simplification principle:
When expressing complexity:
- Keep only the highest-order term
- Drop constant coefficients
- The result is the complexity class
3n² + 5n + 12 → n² → O(n²)
100n log n + 50n → n log n → O(n log n)
2ⁿ + n^100 → 2ⁿ → O(2ⁿ) [exponential beats any polynomial]
For small n, lower-order terms can be significant (in our table, they're 17% at n=10). For precise benchmarking or tight latency requirements with small inputs, you might care. But for algorithm selection and scalability analysis, the dominant term is what matters.
The Major Complexity Families
Algorithm complexities cluster into recognizable families. Understanding these families—their character and typical causes—builds intuition for recognizing complexity in code.
- •O(1) — Constant: Fixed work regardless of input. Array access, hash lookup, stack push/pop. The ideal.
- •O(log n) — Logarithmic: Work barely grows with input. Binary search, balanced tree operations. Adding data more than twice as much data adds only one more step.
- •O(n) — Linear: Work proportional to input. Single pass through data. Finding max, computing sum. Often optimal—you can't do better than looking at each element once.
- •O(n log n) — Linearithmic: The 'sorting threshold.' Merge sort, heapsort, many divide-and-conquer algorithms. Often optimal for comparison-based ordering.
- •O(n²) — Quadratic: Every pair of elements. Bubble sort, naive substring search, some dynamic programming. Acceptable for n < 10,000; problematic beyond.
- •O(n³) — Cubic: Matrix multiplication (naive), all triples. Acceptable for n < 1,000; rare in production code.
- •O(2ⁿ) — Exponential: All subsets. Brute-force solutions to NP problems. Acceptable only for n < 30 or so.
- •O(n!) — Factorial: All permutations. Traveling salesman brute force. Acceptable for n < 12; intractable beyond.
The complexity spectrum:
Feasible Infeasible
| |
O(1) O(log n) O(n) O(n log n) O(n²) O(n³) O(2ⁿ) O(n!)
↑ ↑ ↑
Sweet spot Warning zone Desperation only
Algorithms left of O(n²) are generally scalable. At O(n²), you start needing to think carefully about input size. Right of O(n²), you're in trouble for anything but small inputs.
The exponential wall:
The jump from O(n³) to O(2ⁿ) isn't just a big step—it's a qualitative change. Polynomials (n, n², n³, n^100) are all "feasible" in theory. Exponentials are not. This distinction—polynomial vs exponential—is the dividing line in complexity theory between tractable and intractable problems.
Quick reference for maximum feasible n (assuming ~10⁸ operations/second and 1 second budget): O(n) → n ≤ 10⁸. O(n log n) → n ≤ 10⁷. O(n²) → n ≤ 10,000. O(n³) → n ≤ 500. O(2ⁿ) → n ≤ 25. O(n!) → n ≤ 10. Commit these rough limits to memory.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.