Big O notation is a mathematical notation used to describe how the time or space requirements of an algorithm grow as its input size increases. It provides a standard way to discuss algorithm efficiency without depending on a specific computer, programming language, or execution time.
In data structures and algorithms, Big O notation helps developers compare different approaches to the same problem and understand how they behave when working with larger inputs.
For example, searching an unsorted array from beginning to end may require checking every element, giving O(n) time complexity. A binary search on a sorted array can repeatedly divide the search space, giving O(log n) time complexity.
Why Big O Notation Is Important
An algorithm should not be evaluated only by how quickly it runs for a small input. Its behavior as the input becomes larger is often much more important.
- Compare algorithms that solve the same problem.
- Understand how an algorithm scales with input size.
- Identify expensive operations and inefficient approaches.
- Choose appropriate data structures and algorithms.
- Estimate performance before processing large datasets.
- Communicate algorithm efficiency during technical interviews.
What Does Big O Describe?
Big O is commonly used to describe the growth of an algorithm's time complexity or space complexity.
Time complexity describes how the amount of computational work grows, while space complexity describes how additional memory requirements grow.
function findMaximum(numbers) {
let maximum = numbers[0];
for (const number of numbers) {
if (number > maximum) {
maximum = number;
}
}
return maximum;
}
The function scans the array once, so its time complexity is O(n). It uses only a fixed number of additional variables, so its auxiliary space complexity is O(1).
Understanding Input Size
Big O expressions normally use a variable such as n
to represent input size.
The meaning of n depends on the problem. It may represent the number of elements in an array, characters in a string, nodes in a tree, or vertices in a graph.
const numbers = [10, 20, 30, 40, 50];
// Here, n = 5
For graph algorithms, multiple input-size variables may be needed. For example,
V can represent vertices and
E can represent edges.
O(1) — Constant Time
O(1) means that the number of operations does not grow with the size of the input.
function getFirstElement(numbers) {
return numbers[0];
}
console.log(getFirstElement([10, 20, 30, 40]));
Accessing a known array position requires a constant number of operations. The array can contain a small or very large number of elements without changing the complexity of this particular operation.
O(log n) — Logarithmic Time
O(log n) occurs when an algorithm repeatedly reduces the size of the remaining problem by a constant factor. Binary search is a common example.
function binarySearch(numbers, target) {
let left = 0;
let right = numbers.length - 1;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
if (numbers[middle] === target) {
return middle;
}
if (numbers[middle] < target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return -1;
}
Each iteration eliminates approximately half of the remaining search space. Therefore, the number of iterations grows logarithmically with n.
O(n) — Linear Time
O(n) means the amount of work grows approximately in proportion to the input size.
function printAll(numbers) {
for (const number of numbers) {
console.log(number);
}
}
If the input contains n elements, the loop processes each element once, resulting in O(n) time complexity.
O(n log n) — Linearithmic Time
O(n log n) is commonly found in efficient sorting algorithms such as merge sort and heap sort.
These algorithms typically combine work across multiple levels of a divide-and-conquer or heap-based process. O(n log n) generally grows faster than O(n) but much slower than O(n²).
O(n²) — Quadratic Time
O(n²) occurs when the amount of work grows approximately with the square of the input size. Nested loops are a common pattern.
function printPairs(numbers) {
for (let i = 0; i < numbers.length; i++) {
for (let j = 0; j < numbers.length; j++) {
console.log(numbers[i], numbers[j]);
}
}
}
The outer loop runs n times and the inner loop can also run n times for each outer iteration, producing approximately n² operations.
O(2ⁿ) — Exponential Time
O(2ⁿ) describes exponential growth. It can occur when an algorithm repeatedly branches into two or more independent possibilities.
function countWays(n) {
if (n <= 1) {
return 1;
}
return countWays(n - 1) + countWays(n - 2);
}
This recursive Fibonacci-style implementation repeatedly recomputes overlapping subproblems, resulting in exponential growth in its straightforward form. Memoization or dynamic programming can reduce this complexity significantly.
O(n!) — Factorial Time
O(n!) grows extremely quickly and commonly appears when an algorithm explores every possible ordering of n elements.
Generating all permutations of n distinct elements is a classic example. Factorial complexity becomes impractical for relatively small input sizes, so algorithms that explore permutations often require pruning, dynamic programming, or other optimizations.
Big O Complexity Chart
The following complexity classes are commonly encountered when studying data structures and algorithms.
| Big O | Name | Typical Example |
|---|---|---|
| O(1) | Constant | Array index access |
| O(log n) | Logarithmic | Binary search |
| O(n) | Linear | Linear search |
| O(n log n) | Linearithmic | Merge sort |
| O(n²) | Quadratic | Simple nested loops |
| O(2ⁿ) | Exponential | Some recursive branching algorithms |
| O(n!) | Factorial | Generating permutations |
How Big O Ignores Constants
Big O focuses on the growth rate of an algorithm. Constant multipliers are normally ignored when expressing asymptotic complexity.
5n + 20
// Big O: O(n)
The expression 5n + 20 contains a linear term and a constant. As n becomes large, the linear term determines the growth category.
Similarly, an expression such as 4n² + 3n + 10 is classified as O(n²), because the quadratic term dominates the growth.
Dominant Terms in Big O
When an algorithm contains multiple terms, the term with the fastest growth usually determines the final asymptotic classification.
T(n) = n² + n + 50
// Dominant term: n²
// Big O: O(n²)
This simplification makes it easier to compare algorithms based on their behavior for large inputs.
Sequential Loops and Big O
Consider two loops that execute one after another.
for (let i = 0; i < n; i++) {
console.log(i);
}
for (let j = 0; j < n; j++) {
console.log(j);
}
The total work is approximately n + n = 2n. After dropping the constant factor, the complexity is O(n), not O(n²).
Nested Loops and Big O
Nested loops require more careful analysis because the number of executions of an inner loop may depend on the outer loop.
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
console.log(i, j);
}
}
Here the inner loop executes n times for each of the n outer iterations, resulting in O(n²).
Nested Loops Do Not Always Mean O(n²)
A common mistake is assuming every nested loop has quadratic complexity. The actual complexity depends on how the loop variables change.
let j = 0;
for (let i = 0; i < n; i++) {
while (j < n) {
j++;
}
}
Although a loop appears inside another loop, the while loop increments
j only n times in total. The combined complexity
is O(n), not automatically O(n²).
Big O and Conditional Statements
Conditional branches do not automatically add their complexities together. Usually, the branch with the greatest growth is considered when determining the upper bound.
function process(numbers, enabled) {
if (enabled) {
for (const number of numbers) {
console.log(number);
}
} else {
console.log("Disabled");
}
}
The first branch is O(n), while the second branch is O(1). The overall upper-bound complexity is O(n).
Big O and Recursion
Recursive algorithms can have different complexity patterns depending on how many recursive calls they make and how the input changes between calls.
function countdown(n) {
if (n <= 0) {
return;
}
console.log(n);
countdown(n - 1);
}
There is one recursive call for each decrement of n, so the time complexity is O(n). The active call stack also grows to O(n).
Big O for Binary Search
Binary search demonstrates why reducing the problem size can produce logarithmic complexity.
function binarySearch(numbers, target) {
let left = 0;
let right = numbers.length - 1;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
if (numbers[middle] === target) {
return middle;
}
if (numbers[middle] < target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return -1;
}
Because each iteration removes roughly half of the remaining elements, the worst-case time complexity is O(log n).
Big O and Data Structure Selection
Big O is particularly useful when choosing between data structures. Different structures provide different performance characteristics for insertion, deletion, searching, and access.
| Structure | Typical Operation | Typical Complexity |
|---|---|---|
| Array | Index access | O(1) |
| Array | Linear search | O(n) |
| Hash table | Lookup | O(1) average |
| Balanced BST | Search | O(log n) |
| Heap | Insert | O(log n) |
| Linked List | Search | O(n) |
These are typical complexity characteristics and can depend on the exact operation, implementation, and assumptions about the data structure.
Best Case, Average Case, and Worst Case
An algorithm can have different performance depending on the input. Complexity analysis may therefore describe best-case, average-case, or worst-case behavior.
function linearSearch(numbers, target) {
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] === target) {
return i;
}
}
return -1;
}
- Best case: The target is the first element, so the search can finish in O(1).
- Worst case: The target is last or absent, requiring O(n).
- Average case: The expected work depends on assumptions about where the target occurs.
Big O vs Big Omega vs Big Theta
Big O is not the only asymptotic notation used in algorithm analysis. Big Omega and Big Theta provide different forms of growth bounds.
| Notation | General Meaning |
|---|---|
| O(f(n)) | Asymptotic upper-bound notation |
| Ω(f(n)) | Asymptotic lower-bound notation |
| Θ(f(n)) | Asymptotically tight bound |
In programming discussions, Big O is frequently used to communicate an algorithm's growth rate, particularly its worst-case upper bound when that is the relevant analysis.
How to Find Big O Notation Step by Step
- Identify what represents the input size.
- Find the operations whose number of executions changes with the input.
- Analyze loops and nested loops.
- Check whether each iteration reduces or expands the problem.
- Analyze recursive calls and their branching structure.
- Combine sequential operations.
- Keep the dominant growth term.
- Remove constant factors and lower-order terms.
- Express the result using Big O notation.
Worked Example: Find the Big O
function processNumbers(numbers) {
for (const number of numbers) {
console.log(number);
}
for (let i = 0; i < numbers.length; i++) {
for (let j = 0; j < numbers.length; j++) {
console.log(numbers[i], numbers[j]);
}
}
}
The first loop contributes O(n). The nested loops contribute O(n²).
Therefore, the combined complexity is O(n + n²). Since n² grows faster than n, the dominant term is n² and the final Big O complexity is O(n²).
Common Big O Mistakes
- Thinking Big O represents exact execution time in seconds.
- Keeping constant factors such as 2n or 5n in the final simplified expression.
- Assuming every nested loop is automatically O(n²).
- Ignoring recursive calls when analyzing complexity.
- Confusing time complexity with space complexity.
- Ignoring the cost of operations performed inside loops.
- Assuming a data structure always has the same complexity for every operation.
- Forgetting that sorted input may enable more efficient algorithms such as binary search.
- Using only the best-case behavior when worst-case analysis is required.
Big O in Real-World Applications
Big O analysis is not limited to coding interview exercises. It can help developers reason about real application features such as searching records, processing files, filtering data, handling requests, and working with large collections.
For example, repeatedly searching a large list for matching records may become expensive. Introducing an appropriate hash-based lookup structure can change the typical lookup cost substantially, although the trade-off includes additional memory and data preparation.
Key Takeaways
- Big O notation describes how algorithm resource requirements grow with input size.
- It can be used to describe both time and space complexity.
- O(1) represents constant growth.
- O(log n) commonly occurs when the search space is repeatedly reduced.
- O(n) represents linear growth.
- O(n log n) is common in efficient sorting algorithms.
- O(n²) commonly occurs when two input-dependent loops are nested.
- O(2ⁿ) and O(n!) grow very quickly and often require optimization for larger inputs.
- Constant factors and lower-order terms are normally removed when simplifying Big O expressions.
- The dominant term determines the asymptotic growth category.
- Understanding Big O helps developers choose algorithms and data structures more effectively.