Big O notation describes how an algorithm’s resource requirements grow as the amount of input increases. It is one of the main tools computer scientists use to compare algorithms, predict performance, and understand whether a solution will remain practical as a problem becomes larger.
An algorithm that processes 100 records in a fraction of a second might take considerably longer to process a million. Big O notation helps explain why some algorithms scale well while others become impractical, even when both produce the correct result.
Rather than measuring an algorithm’s exact running time, Big O describes its growth rate. It focuses on how the work changes with input size, not on the speed of a particular computer or the details of a programming language. The result is a way to reason about efficiency that applies across different hardware, software environments, and problem sizes.
What Big O notation measures
An algorithm is a defined sequence of steps for solving a problem. Sorting a list, searching a database, finding the shortest route between two locations, and compressing a file are all tasks that algorithms can perform.
Two algorithms may solve the same problem but require very different amounts of work. One might examine every item in a collection, while another might eliminate large portions of the collection at each step. Their performance can look similar for small inputs but diverge dramatically as the input grows.
Big O notation expresses this difference mathematically. It describes an upper bound on how quickly an algorithm’s resource requirements grow, usually in terms of the number of input elements, represented by nnn.
For example, an algorithm with a running time of O(n)O(n)O(n) has linear growth: doubling the input size generally doubles the amount of work, assuming the same type of operation and comparable input conditions. An algorithm with a running time of O(n2)O(n^2)O(n2) has quadratic growth: doubling the input can increase the work by roughly a factor of four.
These descriptions do not promise exact running times. An O(n)O(n)O(n) algorithm does not necessarily finish in one second, and an O(n2)O(n^2)O(n2) algorithm does not necessarily take four times as long whenever its input doubles. Big O describes the mathematical growth pattern, not an exact stopwatch measurement.
The notation also abstracts away constant factors. An algorithm that performs 5n5n5n operations and another that performs 100n100n100n operations are both O(n)O(n)O(n), because both grow linearly. They may still have substantially different running times in practice.
This distinction makes Big O useful for understanding scalability: how well an algorithm continues to perform as the problem becomes larger.
How Big O notation works mathematically
Suppose an algorithm processes nnn elements and performs approximately 3n+103n+103n+10 operations. The term 3n3n3n represents work that grows with the input, while the constant 10 represents additional work that does not depend on input size.
Big O simplifies this expression to O(n)O(n)O(n). It ignores constant multipliers and lower-order terms because they become less significant in the growth-rate comparison as nnn increases.
Consider a second algorithm with an estimated operation count of 2n2+5n+202n^2+5n+202n2+5n+20. Its Big O classification is O(n2)O(n^2)O(n2). As the input grows, the quadratic term eventually dominates the linear and constant terms.
This simplification follows a general principle: identify the term that grows fastest as the input becomes large, then express that growth rate using Big O notation.
Strictly speaking, Big O is a mathematical upper bound. If an algorithm’s operation count is f(n)f(n)f(n), saying f(n)=O(g(n))f(n)=O(g(n))f(n)=O(g(n)) means that, beyond some input size, f(n)f(n)f(n) is bounded above by a constant multiple of g(n)g(n)g(n). The bound does not need to be the tightest possible one. For example, a linear function is also O(n2)O(n^2)O(n2), although O(n)O(n)O(n) gives a more informative description of its growth.
In everyday algorithm analysis, programmers generally use Big O to communicate a useful asymptotic upper bound, often the tightest familiar bound for the behavior being discussed. This convention makes comparisons easier, provided the assumptions behind each classification are clear.
The most common Big O time complexities
Different growth rates lead to very different performance as input sizes increase. The most familiar complexity classes range from constant time to exponential time.
| Big O notation | Common name | Typical pattern |
|---|---|---|
| O(1)O(1)O(1) | Constant | Work stays bounded as input grows |
| O(logn)O(\log n)O(logn) | Logarithmic | Work grows by a fixed amount when input size is multiplied by a constant |
| O(n)O(n)O(n) | Linear | Work grows in proportion to input size |
| O(nlogn)O(n\log n)O(nlogn) | Linearithmic | Work combines linear processing with logarithmic levels |
| O(n2)O(n^2)O(n2) | Quadratic | Work grows approximately with the square of input size |
| O(n3)O(n^3)O(n3) | Cubic | Work grows approximately with the cube of input size |
| O(2n)O(2^n)O(2n) | Exponential | Work can roughly double when one input unit is added |
| O(n!)O(n!)O(n!) | Factorial | Work can grow by a factor proportional to the new input size at each step |
These classes are useful reference points, but the actual performance depends on the algorithm, the input, and the cost of its individual operations.
Constant time: O(1)O(1)O(1)
An operation has constant time complexity when its cost does not grow with the size of the input.
For example, retrieving an element from an array by its index generally takes constant time, assuming ordinary array access and excluding unusual implementation details. Accessing the thousandth element does not require examining all the elements that come before it.
Constant time does not mean zero time or necessarily a very short time. It means the operation’s running time is bounded independently of the input size under the stated assumptions.
Logarithmic time: O(logn)O(\log n)O(logn)
Logarithmic algorithms reduce the remaining problem by a constant factor at each step. Binary search is a classic example.
Binary search works on a sorted collection. It examines the middle element, determines which half could contain the target, and discards the other half. It repeats this process until it finds the target or determines that the target is absent.
If a sorted collection contains 1,024 elements, each halving step reduces the remaining search space to 512, then 256, then 128, and so on. About 10 halvings reduce the search space to one element because 210=1,0242^{10}=1,024210=1,024.
If the collection grows to roughly twice its original size, binary search needs only about one additional halving step. That slow growth is what makes logarithmic algorithms especially effective for large inputs.
The method depends on the collection being sorted and on the ability to access its middle element efficiently. Sorting the collection first has a cost of its own, which should be included when evaluating the full task.
Linear time: O(n)O(n)O(n)
Linear-time algorithms perform work proportional to the number of input elements.
Finding the largest number in an unsorted list is a straightforward example. The algorithm examines each element, remembers the largest value seen so far, and updates that value whenever it encounters a larger one.
For a list of 100 numbers, it examines 100 numbers. For a list of 1 million numbers, it examines 1 million. The number of comparisons grows directly with the list’s length.
Linear time is often a practical target because every element may need to be examined at least once. If the input is an unsorted list and any element could contain the maximum value, an exact algorithm generally cannot determine the answer without checking every element.
Linearithmic time: O(nlogn)O(n\log n)O(nlogn)
Linearithmic growth commonly appears in efficient general-purpose sorting algorithms, including merge sort and average-case analysis of some other sorting methods.
Merge sort divides a list into smaller parts, sorts those parts recursively, and combines them. Dividing the list creates about logn\log nlogn levels, while the total work across each level is proportional to nnn. Multiplying the work per level by the number of levels gives O(nlogn)O(n\log n)O(nlogn).
This growth is faster than linear but substantially slower than quadratic as the input becomes large. For that reason, O(nlogn)O(n\log n)O(nlogn) is an important benchmark for comparison-based sorting.
Quadratic time: O(n2)O(n^2)O(n2)
Quadratic algorithms often arise when an algorithm compares each element with many or all other elements.
Imagine a program that checks every possible pair in a list. With nnn elements, the number of ordered pairs is n2n^2n2. If the program compares only distinct pairs without regard to order, the count is n(n−1)/2n(n-1)/2n(n−1)/2. Both expressions have quadratic growth.
An input of 100 elements produces about 10,000 ordered pairs. An input of 1,000 elements produces about 1 million. Increasing the input by a factor of 10 increases the number of ordered pairs by a factor of 100.
Some simple sorting algorithms, such as bubble sort and insertion sort, have quadratic worst-case time complexity. They can still be useful for small collections, and insertion sort can perform well on data that is already nearly sorted. Big O describes growth under specified conditions; it does not make every quadratic algorithm useless.
Exponential and factorial time: O(2n)O(2^n)O(2n) and O(n!)O(n!)O(n!)
Exponential and factorial growth can make algorithms impractical even for moderately sized inputs.
An algorithm with O(2n)O(2^n)O(2n) complexity may examine every possible subset of a set containing nnn elements. Each element can either be included or excluded, producing 2n2^n2n possible subsets.
Factorial complexity can arise when an algorithm examines every possible ordering of nnn distinct items. There are n!n!n! such orderings: nnn choices for the first position, n−1n-1n−1 for the second, and progressively fewer choices for each remaining position.
These patterns appear in exhaustive searches for certain difficult optimization problems. They are not unavoidable for every problem in those areas, since specialized algorithms, pruning, approximations, or additional structure may reduce the work substantially. However, they illustrate why a seemingly modest increase in input size can overwhelm a naive approach.
How to calculate an algorithm’s Big O complexity
To analyze an algorithm, identify the input size, determine how the main operations depend on that size, and simplify the resulting growth expression.
A useful starting point is to count operations that have a predictable relationship to the input, such as comparisons, array accesses, or arithmetic operations. Exact counting is often unnecessary. The goal is to identify the dominant growth pattern.
Consider this pseudocode:
Assume the list contains nnn elements and is not empty. Initializing the maximum takes constant time. The loop visits each element, performs a comparison, and may update the stored value. The loop therefore performs work proportional to nnn.
The overall running time is O(n)O(n)O(n). The algorithm uses a constant amount of additional storage because it keeps only a few variables, so its auxiliary space complexity is O(1)O(1)O(1).
Now consider nested loops:
The outer loop executes nnn times. For each outer iteration, the inner loop also executes nnn times. The total number of comparisons is n×n=n2n \times n=n^2n×n=n2, giving O(n2)O(n^2)O(n2) time.
Nested loops do not automatically imply quadratic complexity. If the inner loop runs a fixed number of times, the overall growth may remain linear. If one loop processes nnn elements and another independently processes mmm elements, the combined work may be O(n+m)O(n+m)O(n+m), while fully nested loops over both collections may require O(nm)O(nm)O(nm).
The relationship between loop bounds matters more than the number of loops alone.
How to simplify Big O expressions
Several rules make algorithm analysis easier.
Ignore constant factors. An operation count of 7n7n7n simplifies to O(n)O(n)O(n), just as 100n100n100n does. Constant factors can still matter in real programs, but they do not change the asymptotic growth class.
Keep the fastest-growing term. An expression such as n2+3n+50n^2+3n+50n2+3n+50 simplifies to O(n2)O(n^2)O(n2). For large nnn, the quadratic term eventually dominates the linear and constant terms.
Add sequential work. If one section of an algorithm takes O(n)O(n)O(n) time and a later section takes O(n2)O(n^2)O(n2), the total is O(n+n2)O(n+n^2)O(n+n2), which simplifies to O(n2)O(n^2)O(n2). The larger growth rate dominates the combined expression.
Multiply the costs of nested work when appropriate. If an outer loop runs nnn times and an inner operation takes O(n)O(n)O(n) time on every iteration, the combined work is O(n2)O(n^2)O(n2). This rule applies when the inner cost is actually incurred across those iterations.
Account for changing input sizes. An inner loop that repeatedly halves its remaining work may contribute a logarithmic factor rather than a linear one. For example, a loop that halves nnn until it reaches one executes about log2n\log_2 nlog2n times, producing O(logn)O(\log n)O(logn) work if each iteration does constant work.
These rules are simplifications, not substitutes for understanding the algorithm. Recursion, early termination, data-dependent branches, and operations whose costs depend on the input can require more careful analysis.
Why Big O notation is not the same as actual running time
Big O is a theoretical measure of growth, while actual running time is an observed property of a particular implementation running under particular conditions.
Consider two algorithms that both have O(n)O(n)O(n) time complexity. One might perform a single comparison per element, while the other performs many arithmetic operations and makes several function calls for each element. Both grow linearly, but one can be much slower.
Hardware also matters. Processor speed, memory bandwidth, cache behavior, storage access, and the programming language’s runtime can all influence performance. Two implementations with identical Big O classifications may behave quite differently because of these factors.
For small inputs, an algorithm with a higher asymptotic complexity can sometimes outperform one with a lower complexity. A theoretically efficient method may require additional setup, allocate memory, or perform more work per element. Those costs can outweigh its growth-rate advantage until the input becomes sufficiently large.
Big O becomes particularly informative when input sizes increase enough for growth patterns to dominate. It helps identify whether an algorithm is likely to remain practical, but it cannot determine an exact execution time without additional information.
For performance-sensitive software, theoretical analysis and measurement serve complementary purposes. Complexity analysis helps explain how an algorithm should scale; benchmarking reveals how a particular implementation performs on real workloads.
Best-case, average-case, and worst-case complexity
An algorithm’s running time may depend not only on input size but also on the arrangement or values of the input. Complexity analysis therefore needs to specify which behavior is being described.
The best case is the most favorable input arrangement for a given size. The worst case is the least favorable arrangement. The average case describes expected performance under a specified distribution of possible inputs.
Consider linear search, which examines elements until it finds a target or reaches the end of a list. If the target is the first element, the search takes constant time, giving a best-case bound of O(1)O(1)O(1). If the target is last or absent, the algorithm may inspect every element, giving worst-case time of O(n)O(n)O(n).
The average case depends on assumptions about where the target is likely to appear and whether it is present. Under a simple model in which the target is equally likely to occupy any position, a successful search examines about half the elements on average. That still produces O(n)O(n)O(n) average-case time because a constant fraction of nnn grows linearly.
Different cases can produce different complexity classes, especially in algorithms that change their behavior based on the data. Quick sort, for example, has average-case time of O(nlogn)O(n\log n)O(nlogn) under standard assumptions but can degrade to O(n2)O(n^2)O(n2) when its partitions are consistently poor.
When discussing an algorithm’s complexity, it is therefore important to distinguish the case being analyzed. A statement such as “this algorithm is O(nlogn)O(n\log n)O(nlogn)” may be incomplete if the algorithm has substantially worse behavior for certain inputs and the intended bound is unclear.
Space complexity: measuring memory use
Algorithm efficiency is not limited to running time. An algorithm also consumes memory, and space complexity describes how its memory requirements grow with input size.
Space analysis often distinguishes between the memory needed to store the input and the auxiliary space used by the algorithm in addition to that input. This distinction matters because an algorithm may need to process a large dataset without creating another equally large structure.
For example, the maximum-finding algorithm described earlier uses O(1)O(1)O(1) auxiliary space. It stores a running maximum and a few other variables regardless of how many elements the list contains.
An algorithm that creates a new array containing one additional value for each input element may use O(n)O(n)O(n) auxiliary space. If it constructs an nnn-by-nnn table, its memory use may be O(n2)O(n^2)O(n2).
Recursion also consumes memory. Each recursive call may add a frame to the program’s call stack, which stores information such as local variables and where execution should resume. A recursive algorithm with a maximum of nnn simultaneously active calls may require O(n)O(n)O(n) stack space, even if each individual call uses only a constant amount of memory.
The total space requirement depends on what is counted. An analysis that includes the input itself can give a different result from one that reports auxiliary space alone. Both approaches are useful, but the distinction should be stated clearly when it affects the comparison.
How to choose between algorithms with different complexities
The most efficient algorithm is not always the one with the smallest-looking Big O expression. The appropriate choice depends on the input size, the required result, available memory, implementation costs, and the structure of the data.
For searching, binary search is usually preferable to linear search when the data is sorted, random access is efficient, and repeated searches justify the conditions required by the method. If the data is unsorted and only one search is needed, sorting it first may cost more than simply scanning the collection.
For sorting, an O(nlogn)O(n\log n)O(nlogn) algorithm generally scales better than a quadratic algorithm on large, arbitrary inputs. Yet a simpler method may be reasonable for a small list or data that already has useful structure. Some algorithms also offer practical benefits beyond their time complexity, such as stable ordering, low memory use, or predictable worst-case performance.
Real applications frequently involve trade-offs. An algorithm might use extra memory to reduce running time, preprocess data to make future queries faster, or accept an approximate answer when an exact calculation would be too expensive. Big O helps clarify these choices by describing how the costs change as the problem grows.
It is also important to analyze the complete operation rather than one isolated step. A fast lookup method may depend on an expensive preprocessing stage. A database query may involve network communication and disk access that dominate the computational work. A program may spend more time moving data than executing the operations counted in a simple model.
The practical value of Big O notation lies in recognizing the shape of an algorithm’s costs. It reveals whether an approach grows steadily, becomes increasingly expensive, or can explode in complexity as the input expands. Combined with a clear understanding of the algorithm and measurements of real performance, that insight provides a strong foundation for designing software that remains efficient as its demands increase.