Big O Notation Guide
The definitive guide to algorithm complexity analysis. Understand Big O, Big Omega, Big Theta, and how to analyze your code's performance.
Growth Rate Comparison
O(1) - Constant
ExcellentArray access, hash table lookup, arithmetic operations
O(log n) - Logarithmic
ExcellentBinary search, balanced tree operations, binary heap
O(n) - Linear
GoodArray traversal, linear search, counting elements
O(n log n) - Linearithmic
FairMerge sort, quick sort (average), heap sort
O(n²) - Quadratic
PoorBubble sort, nested loops, comparing all pairs
O(2ⁿ) - Exponential
TerribleNaive Fibonacci, power set, some brute force
Rules for Big O Analysis
Mastering Big O Notation
Big O notation is the language of algorithm efficiency. It provides a standardized way to describe how algorithms scale with input size, enabling meaningful comparisons independent of hardware, programming language, or implementation details. Understanding Big O is essential for every software developer—it guides algorithm selection, helps identify bottlenecks, and enables informed trade-off decisions.
This guide covers Big O comprehensively: the mathematical foundation, common complexity classes, analysis techniques, and practical applications. Whether you are preparing for technical interviews or optimizing production code, mastering Big O is a crucial skill.
The Three Notations
While "Big O" is commonly used as a general term, there are actually three related notations that each describe different bounds:
Big O (O) describes the upper bound—the worst case. When we say an algorithm is O(n²), we mean it will never take more than some constant times n² steps. Big O is most commonly used because we often care about guaranteeing performance limits.
Big Omega (Ω) describes the lower bound—the best case. An algorithm that is Ω(n) will take at least linear time. Binary search is Ω(1) because the target might be the first element checked. Lower bounds help establish what cannot theoretically be improved.
Big Theta (Θ) describes the tight bound—when upper and lower bounds match. If an algorithm is Θ(n log n), it takes exactly that order of magnitude, neither more nor less. Θ provides the most precise characterization but requires more analysis.
Understanding Growth Rates
Big O captures how running time grows with input size, not the absolute time. An O(n) algorithm doubles in time when input doubles. An O(n²) algorithm quadruples. An O(log n) algorithm adds a constant amount. This growth rate, not constant factors, dominates for large inputs.
Consider concrete numbers: for n=1000, O(1) takes ~1 operation, O(log n) takes ~10, O(n) takes ~1000, O(n log n) takes ~10,000, O(n²) takes ~1,000,000, and O(2ⁿ) is astronomically large—more than atoms in the universe.
The dramatic differences become clearer at scale. At n=1,000,000: O(n) is a million operations; O(n²) is a trillion. An O(n²) algorithm that takes 1 second at n=1000 would take over 11 days at n=1,000,000. This is why complexity class matters more than constant factors for large data.
Common Complexity Classes Explained
O(1) Constant: The runtime does not change with input size. Hash table lookups (amortized), array access by index, simple arithmetic—these are O(1). The most efficient possible complexity.
O(log n) Logarithmic: Each step eliminates a constant fraction of the remaining work. Binary search halves the search space each comparison. Balanced tree operations maintain O(log n) by ensuring the tree height grows logarithmically with nodes.
O(n) Linear: Time grows proportionally with input. Finding the maximum in an unsorted array must examine every element—O(n). Any algorithm that must touch every element at least once is at minimum O(n).
O(n log n) Linearithmic: Comparison-based sorting cannot be faster than this (proven lower bound). Merge sort and heap sort achieve this bound. It is the efficiency sweet spot for sorting and many divide-and-conquer algorithms.
O(n²) Quadratic: Typically from nested loops over the data. Comparing every pair, naive sorting algorithms, and many graph algorithms on dense graphs are O(n²). Acceptable for small inputs but becomes problematic at scale.
O(2ⁿ) Exponential: Time doubles with each additional input element. Generating all subsets, naive recursive Fibonacci, some brute-force optimization problems. Only practical for very small inputs (n less than 30-40).
O(n!) Factorial: Generating all permutations, brute-force traveling salesman. Grows faster than exponential—20! is already billions of billions. Effective algorithms for such problems use dynamic programming or approximations.
Analyzing Code: A Systematic Approach
Step 1: Identify input variables. What changes size? An array of n elements? Two arrays of sizes n and m? A tree with n nodes and height h?
Step 2: Count primitive operations in terms of input. A loop running n times contributes O(n). A loop running n times containing an O(log n) operation contributes O(n log n).
Step 3: Apply the rules. Drop constants (O(3n) → O(n)). Drop lower-order terms (O(n² + n log n + n) → O(n²)). Add sequential parts, multiply nested parts.
Step 4: Consider best, average, and worst cases. Quick sort is O(n²) worst case but O(n log n) average. Hash tables are O(n) worst case but O(1) average. State which case you are analyzing.
Space Complexity
Space complexity measures memory usage as a function of input size. It follows the same notation as time complexity. An algorithm using a fixed number of variables regardless of input is O(1) space. One that copies the input array uses O(n) space.
In-place algorithms use O(1) extra space (not counting the input itself). Quick sort is nearly in-place (O(log n) for the recursion stack). Merge sort requires O(n) auxiliary space for merging.
Time-space trade-offs are common. Memoization trades O(n) space for dramatic time improvements. Hash tables trade O(n) space for O(1) lookup. Dynamic programming often builds O(n) or O(n²) tables to avoid exponential recomputation.
Amortized Analysis
Some operations are occasionally expensive but average out over many operations. A dynamic array doubles when full—that one doubling is O(n), but the amortized cost per insertion is O(1) because doublings happen exponentially less frequently.
Hash table insertions are amortized O(1) because resizing (O(n)) happens rarely enough that the average remains constant. Understanding amortization prevents incorrectly pessimistic analysis of common data structures.
Practice Problems
Analyze these code patterns: A loop from 1 to n → O(n). A loop from 1 to n inside another loop from 1 to n → O(n²). A loop that halves i each iteration (i = n; i greater than 0; i /= 2) → O(log n). Two sequential loops of n each → O(n) + O(n) = O(n). A loop of n with an inner loop of m → O(nm).
Watch for hidden complexity: string concatenation in a loop (each concat may be O(n), making the loop O(n²)), library function calls (sort is O(n log n), set operations vary), and recursive calls (trace the recursion tree).
From Theory to Practice
Big O ignores constants, but constants matter in practice. An O(n) algorithm with a large constant might be slower than O(n log n) with a tiny constant for realistic input sizes. Always profile real code on realistic data.
Cache effects, memory locality, branch prediction, and parallelism all affect real performance but are not captured by Big O. Two O(n) algorithms can differ by 10x in wall-clock time due to these factors.
Use Big O to guide algorithm selection, but validate with benchmarks. Choose the algorithm with the right complexity class first, then optimize constants if necessary. Premature optimization of O(n²) code is wasted effort when switching to O(n log n) would solve the problem.