← Back to Community
@senpai

Understanding Time Complexity: A Beginner's Guide to Big O Notation

2026-07-01 · 12 min read · 1,692 words
When you write code, you want it to be fast. But how do you measure "fast"? You could use a stopwatch, but that depends on your computer's hardware, what else is running, and the specific input you chose. What you really need is a way to describe how your algorithm's runtime grows as the input size increases — independent of hardware. That's exactly what time complexity and Big O Notation give you.

What is Time Complexity?

Time complexity is a mathematical function that describes the relationship between the size of the input and the number of operations an algorithm performs. Think of it this way: if you give your algorithm twice as much data, does it take twice as long? Four times as long? The same amount of time? Time complexity answers this question. Time complexity is not about wall-clock time. It's about the rate of growth of operations as the input grows. An algorithm that takes 5 seconds on your laptop might take 2 seconds on a faster machine, but its time complexity — the shape of its growth curve — remains the same. Time complexity is almost always expressed in terms of n, where n represents the size of the input. For an array, n is the number of elements. For a string, n is the number of characters. For a matrix, n might be the number of rows or the total number of cells.

Big O Notation: The Language of Algorithm Analysis

Big O Notation is the standard way we express time complexity. It describes the upper bound — the worst-case scenario for how many operations an algorithm will need relative to the input size. The formal mathematical definition says that f(n) = O(g(n)) if there exist positive constants c and n₀ such that 0 ≤ f(n) ≤ c × g(n) for all n ≥ n₀. In simpler terms: beyond a certain input size, the algorithm's runtime will not exceed some constant multiple of g(n). But you don't need math to understand Big O in practice. Think of it as a label that tells you roughly how the algorithm scales.

The Most Common Big O Complexities

O(1) — Constant Time

An O(1) algorithm takes the same amount of time regardless of input size. The operation count does not depend on n at all.
def get_first_element(arr):
    return arr[0]  # Always one operation

Whether the array has 10 elements or 10 million, accessing arr[0] takes the same amount of time. Array indexing is O(1).

Other examples of O(1) operations include hash map lookups, stack push/pop, and checking if a number is even with the modulo operator.

O(1) is the holy grail of algorithm design. When you see it, you know the algorithm scales perfectly.

O(log n) — Logarithmic Time

An O(log n) algorithm becomes proportionally slower as n grows, but only by the logarithm of n. Each operation divides the problem space in some way, typically in half.
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

Binary search is the classic O(log n) algorithm. Each comparison eliminates half the remaining elements. If you double the array size, you only need one more comparison. That's remarkably efficient.

To appreciate how good O(log n) is: if you have a billion sorted elements, binary search finds any target in at most 30 comparisons. Thirty operations to search a billion items.

Other O(log n) examples include operations on balanced binary search trees (like AVL or Red-Black trees), heap insertion and deletion, and finding an element in a skip list.

O(n) — Linear Time

An O(n) algorithm scales directly with input size. If the input doubles, the operations double. If the input grows by 10x, operations grow by 10x.
def find_max(arr):
    max_val = arr[0]
    for i in range(1, len(arr)):
        if arr[i] > max_val:
            max_val = arr[i]
    return max_val

This function visits every element exactly once. A 100-element array takes 100 steps; a 10,000-element array takes 10,000 steps. The growth is directly proportional.

Linear time is very common and usually acceptable. Most problems require at least looking at every input element once — which is already O(n). Examples include finding the maximum or minimum, summing an array, linear search, counting occurrences, and traversing a linked list.

O(n log n) — Linearithmic Time

O(n log n) is slightly worse than linear but still very efficient. It appears in algorithms that do logarithmic work for each element, or that repeatedly divide the problem and recombine the results.
# Merge Sort is O(n log n)
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

Merge Sort divides the array in half (log n levels of recursion) and at each level, it does O(n) work merging. The total is O(n log n).

Most efficient comparison-based sorting algorithms are O(n log n): Merge Sort, Quick Sort (average case), Heap Sort, and Timsort (Python's built-in sort). Many divide-and-conquer algorithms like the Fast Fourier Transform also run in O(n log n).

O(n²) — Quadratic Time

An O(n²) algorithm's operations grow with the square of the input size. Doubling the input quadruples the runtime. This quickly becomes impractical for large datasets.
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

The nested loop is the signature of O(n²). For each of n elements, we loop over roughly n other elements, giving n × n = n² operations.

Nested loops don't always mean O(n²) — it depends on what the inner loop iterates over. If the inner loop iterates over a fixed-size collection, it's still O(n). But when both loops iterate over the input, it's quadratic.

Examples include Bubble Sort, Insertion Sort, Selection Sort, checking all pairs in an array, and naive matrix multiplication.

O(2ⁿ) — Exponential Time

Exponential algorithms double their runtime with each additional input element. An input of size 20 requires about a million operations; size 30 requires about a billion. These algorithms become unusable extremely quickly.
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

The naive recursive Fibonacci makes two recursive calls for each call, creating a tree of height n with roughly 2ⁿ nodes. This is why fib(50) would take years to compute with this approach.

Exponential time often appears in brute-force recursive solutions to combinatorial problems: generating all subsets (there are 2ⁿ of them), the naive solution to the Traveling Salesman Problem, and recursive solutions without memoization. When you encounter O(2ⁿ), it's time to look for dynamic programming, greedy algorithms, or heuristics.

How to Analyze Time Complexity

Here's a practical approach to analyzing any algorithm:

Step 1: Identify the input

What is n? For most problems, n is the size of the array, the length of the string, or the number of nodes in a graph.

Step 2: Count the dominant operations

Look for loops. Each loop that iterates over the input adds a factor of n. Nested loops multiply.

Step 3: Drop constants and lower-order terms

Big O describes growth rates, not exact counts. O(2n) is just O(n). O(n² + n) is just O(n²). As n approaches infinity, the dominant term completely overshadows the rest.

Step 4: Consider the worst case

Unless specified otherwise, Big O represents the worst-case scenario. A hash table lookup is O(1) average but O(n) worst case when all keys collide. Here are some examples:
  • Single loop over array: O(n)
  • Two independent loops over the same array: O(n + n) = O(2n) = O(n)
  • Nested loop where inner depends on outer: O(n²)
  • Loop that divides problem in half each iteration: O(log n)
  • Recursive function with one recursive call on half the data: O(log n)
  • Recursive function with two recursive calls on half the data: O(n)

Space Complexity: The Other Half of the Equation

We've focused on time, but Big O also applies to space — how much extra memory an algorithm uses. An algorithm that creates a copy of the input array has O(n) space complexity. An algorithm that only uses a few variables has O(1) space complexity.
# O(n) space — creates a new array
def double_values(arr):
    return [x * 2 for x in arr]

# O(1) space — modifies in place
def double_values_in_place(arr):
    for i in range(len(arr)):
        arr[i] *= 2

You often trade space for time. Hash maps give O(1) lookups at the cost of O(n) extra space. Memoization speeds up recursive algorithms by caching results but uses O(n) extra memory.

Practical Takeaways for Coding Interviews

If you're preparing for technical interviews, here's what you need to remember:
  • Know the complexity of common operations: Array access O(1), hash map lookup O(1), binary search O(log n), sorting O(n log n), nested loops over the same input O(n²).
  • Look at the constraints: If n ≤ 10⁶, you need O(n) or O(n log n). If n ≤ 10³, O(n²) is fine. If n ≤ 20, even O(2ⁿ) might work.
  • Always mention both time and space complexity when discussing your solution. Interviewers expect it.
  • Optimize step by step: Start with a brute-force solution (often O(n²) or O(2ⁿ)), then optimize. Even if you don't find the optimal solution, showing your thought process matters.
  • Watch for hidden costs: String concatenation in a loop is O(n²) in languages with immutable strings. Recursion without memoization can be exponential. Hash table operations degrade to O(n) with poor hash functions.

Further Learning

Mastering time complexity is a journey. Once you're comfortable with Big O basics, explore amortized analysis (explaining why dynamic arrays are O(1) amortized for appends), master theorem for divide-and-conquer recurrences, and NP-completeness for understanding which problems are inherently hard. The PixoQuest DSA course includes dedicated modules on algorithm analysis with interactive quizzes and mini-games to help you internalize these concepts through active practice. Sign in and start the Complexity Analysis chapter to test your understanding.
DSAalgorithmsbeginnerstime complexityBig O