DSAAsymptotic Notations (Ω, Θ, O)

Asymptotic Notations (Ω, Θ, O)

When we analyze algorithms, we want to describe how their running time (or space usage) grows as the input size grows — without getting bogged down in exact machine-level constants. Asymptotic notation gives us a precise mathematical language to do exactly that.

Note
The word "asymptotic" means "as n approaches infinity." We care about large inputs because that is where algorithm choice matters most. For n = 10, almost any algorithm is fast. For n = 1,000,000, the difference between O(n) and O(n²) can be the difference between milliseconds and hours.
The Three Core Notations

There are three notations you will encounter in every algorithms textbook and interview:

Notation

Name

Meaning

Analogy

O(f(n))

Big O

Upper bound — at most this fast

Speed limit on a highway

Ω(f(n))

Big Omega

Lower bound — at least this slow

Minimum time a flight takes

Θ(f(n))

Big Theta

Tight bound — exactly this order

Rush-hour commute: always 30-40 min

Big O — Upper Bound

Big O is the most widely used notation. We say T(n) = O(f(n)) if there exist positive constants c and n₀ such that T(n) ≤ c · f(n) for all n ≥ n₀. In plain English: beyond some input size, T(n) never exceeds a constant multiple of f(n). Big O describes the worst case ceiling.

Tip
Big O does NOT automatically mean worst case. It means upper bound. You can use Big O to describe best, average, or worst case — it only says "the function grows no faster than this." In practice, people loosely say "Big O of an algorithm" and mean the worst-case upper bound, which is the most useful combination.

Example — Linear Search: In the worst case you scan all n elements. T(n) = n, so T(n) = O(n). We could also say T(n) = O(n²) (n is certainly no worse than n²), but that is a loose bound and not useful.

Python
def linear_search(arr, target):
    for i in range(len(arr)):   # runs at most n times
        if arr[i] == target:
            return i            # could return early (best case O(1))
    return -1

# Worst case: target not found → O(n)
# Best case:  target is arr[0]  → O(1)
# Both are valid Big O statements about DIFFERENT scenarios
Common Big O Classes (fastest to slowest)

Notation

Name

n = 100 ops

Example

O(1)

Constant

1

Array index access, hash map lookup

O(log n)

Logarithmic

~7

Binary search, balanced BST lookup

O(n)

Linear

100

Linear scan, single loop

O(n log n)

Linearithmic

~700

Merge sort, heap sort

O(n²)

Quadratic

10,000

Bubble sort, nested loops

O(n³)

Cubic

1,000,000

Naive matrix multiplication

O(2ⁿ)

Exponential

~1.27 × 10^30

Brute-force subsets

O(n!)

Factorial

astronomical

Brute-force permutations

Big Omega (Ω) — Lower Bound

Big Omega gives a lower bound. We say T(n) = Ω(f(n)) if there exist positive constants c and n₀ such that T(n) ≥ c · f(n) for all n ≥ n₀. This means T(n) grows at least as fast as f(n). It is the floor, not the ceiling.

Example — Comparison-based sorting: Any algorithm that sorts by comparisons must make at least Ω(n log n) comparisons in the worst case. No comparison sort can beat this lower bound — it is a fundamental limit, not a limitation of any specific algorithm.

Python
def find_min(arr):
    min_val = arr[0]
    for x in arr:        # must look at every element
        if x < min_val:
            min_val = x
    return min_val

# T(n) = Ω(n)  — you MUST examine all n elements
# No shortcut exists without extra structure like a sorted array
Big Theta (Θ) — Tight Bound

Big Theta gives a tight (exact) bound. T(n) = Θ(f(n)) if and only if: T(n) = O(f(n)) AND T(n) = Ω(f(n)). In other words, f(n) is both an upper and lower bound (up to constants). Theta is the most precise — it pins down exactly which complexity class the algorithm belongs to.

Python
def sum_array(arr):
    total = 0
    for x in arr:   # exactly n iterations, always
        total += x
    return total

# Every call touches exactly n elements.
# T(n) = Θ(n)  — tight bound (not O(1), not O(n²))

# Merge sort:
# Best case:    O(n log n)
# Worst case:   O(n log n)
# Average case: O(n log n)
# → T(n) = Θ(n log n)  because best == worst (same order)
Note
Θ is the most informative statement. When you can prove both O and Ω with the same function, you have a tight bound. Many algorithms have the same Big O for best and worst case, making Theta easy to state. Others (like quicksort) have different best and worst cases, so you separate them.
Little o and Little ω

Beyond the big three, there are two more notations that mean strictly less than or greater than (not equal up to constants):

Notation

Strict meaning

Example

o(f(n))

T(n) grows strictly slower than f(n)

n = o(n²) — n is strictly less than n²

ω(f(n))

T(n) grows strictly faster than f(n)

n² = ω(n) — n² is strictly more than n

The formal definition of little-o uses a limit: lim(n→∞) T(n) / f(n) = 0 This ratio going to zero means T(n) is dominated by f(n) — T(n) is negligible relative to f(n) for large n. You rarely use little-o in practice, but it appears in advanced analysis and some interview discussions.

How to Determine the Bound of Your Algorithm
  1. Identify the dominant operation (the one that runs the most)

  2. Count how many times it runs as a function of n

  3. Drop lower-order terms (n² + n → n²)

  4. Drop constant coefficients (3n → n)

  5. State the result with the appropriate notation

Python
def example(arr):
    n = len(arr)

    result = 0              # O(1)

    for x in arr:           # O(n)
        result += x

    for i in range(n):      # O(n²) — nested loop
        for j in range(n):
            result += arr[i] * arr[j]

    return result

# Dominant term: n²
# T(n) = O(n²)  — the O(n) and O(1) parts are absorbed

# Full breakdown: c1 + c2*n + c3*n² = O(n²)
Drop Constants — Why It Works

Big O hides constants because we are describing a growth rate class, not an exact formula. Two algorithms both running in 2n and 1000n steps are in the same class — O(n) — because at scale, the constant factor matters far less than the shape of the curve.

Python
# Both are O(n) — constant factors are absorbed:
def fast_scan(arr):
    return sum(arr)          # ~1 operation per element

def slow_scan(arr):
    total = 0
    for x in arr:
        for _ in range(1000):   # constant inner loop
            total += x / 1000
    return total

# At n = 1,000,000:
# fast_scan: ~1,000,000 ops
# slow_scan: ~1,000,000,000 ops
# 1000x difference, BUT both are O(n).
# In practice constants matter — Big O tells you the category.
Multi-Variable Complexity

Sometimes complexity depends on multiple input variables. You cannot reduce these — both must appear in the notation.

Python
def print_grid(rows, cols):
    for r in range(rows):      # 'rows' iterations
        for c in range(cols):  # 'cols' iterations
            print(r, c)

# T = O(rows × cols)  — cannot simplify to O(n²) unless rows == cols

# BFS/DFS on a graph:
# T = O(V + E)
# V = number of vertices, E = number of edges
# These are independent variables — both stay in the notation
Common Mistakes
Mistake 1: Confusing Big O with worst case
Big O is an upper bound, not automatically the worst case. You can say "best case is O(1)" — that is using Big O to bound the best case scenario. The notation and the scenario are separate concepts.
Mistake 2: Not dropping lower-order terms
Writing O(n² + n) instead of O(n²) is technically correct but sloppy. Always simplify to the dominant term.
Mistake 3: Keeping coefficients
Writing O(3n) instead of O(n). Constants are always dropped in asymptotic notation. O(3n) = O(n).
Mistake 4: Equating Big O with Theta
Saying "merge sort is O(n log n)" is a true statement (it is an upper bound), but "merge sort is Θ(n log n)" is more precise and informative. Use Theta when you know both bounds are the same.
Quick Reference: Identifying Complexity Patterns

Code Pattern

Complexity

Single loop from 0 to n

O(n)

Loop halving each step (i /= 2)

O(log n)

Two nested loops 0..n

O(n²)

Three nested loops 0..n

O(n³)

Recursive halving (binary search)

O(log n)

Recursive call on two halves + O(n) work

O(n log n)

Recursive call on each subset

O(2ⁿ)

Nested loop where inner starts at outer

O(n²) — triangular

Interview Application

In interviews, you will be expected to:

  1. State the Big O of your solution immediately after describing it

  2. Distinguish between time complexity and space complexity

  3. Know whether you are describing worst, average, or best case

  4. Recognize when Theta applies (when best and worst case are identical)

  5. Argue why a lower bound (Omega) proves your algorithm is optimal

Python
def two_sum(nums, target):
    seen = {}
    for i, num in enumerate(nums):          # O(n) — one pass
        complement = target - num
        if complement in seen:              # O(1) — hash lookup
            return [seen[complement], i]
        seen[num] = i                       # O(1) — hash insert
    return []

# Time complexity:  O(n) — one loop, constant-time ops inside
# Space complexity: O(n) — hash map stores up to n entries
#
# Is this optimal?
# Lower bound: Ω(n) — must read all elements at least once
# Upper bound: O(n) — our algorithm achieves this
# → Θ(n) tight bound. This solution is asymptotically optimal!
Tip
A key interview skill is arguing optimality. If you can show the lower bound matches your algorithm's upper bound, you have proven your solution cannot be meaningfully improved in terms of asymptotic complexity.
Summary

Notation

Bound type

Formal condition

When to use

O(f(n))

Upper

T(n) ≤ c·f(n) for large n

Describing worst case or ceiling

Ω(f(n))

Lower

T(n) ≥ c·f(n) for large n

Proving a problem is hard / optimality arguments

Θ(f(n))

Tight

Both O and Ω with same f

When best and worst case are the same order

o(f(n))

Strict upper

T(n)/f(n) → 0

T(n) is dominated by f(n)

ω(f(n))

Strict lower

T(n)/f(n) → ∞

T(n) dominates f(n)