Asymptotic notations are mathematical tools used to describe how the running time or memory usage of an algorithm grows as the input size becomes very large.
Instead of measuring exact seconds or bytes on a specific machine, asymptotic analysis focuses on the growth rate of the algorithm. This makes it possible to compare solutions fairly, even if they are written in different languages or run on different hardware.
Two algorithms can solve the same problem correctly, but one may become much slower than the other as the input grows. Exact execution time depends on many external factors, such as processor speed, compiler, and system load. Asymptotic notation removes those details and highlights the real efficiency of the algorithm.
Suppose one algorithm performs about n operations and another performs about n^2 operations.
That is why DAA focuses more on how an algorithm grows rather than how fast it looks on a tiny input.
The most commonly used asymptotic notations are:
| Notation | Name | Meaning | What It Represents |
|---|---|---|---|
| O(f(n)) | Big O | Upper bound | Worst-case growth or maximum rate of growth |
| Omega(f(n)) | Big Omega | Lower bound | Best-case growth or minimum rate of growth |
| Theta(f(n)) | Big Theta | Tight bound | Exact asymptotic growth when upper and lower bounds match |
Big O notation gives the upper bound of an algorithm. It tells us how badly the running time can grow in the worst case as the input size increases.
It does not always mean the algorithm will actually take that much time for every input. It means the algorithm will not grow faster than that bound for large enough input sizes.
# O(1) - constant time
def get_first(arr):
return arr[0]
# O(n) - linear time
def find_max(arr):
max_val = arr[0]
for item in arr:
if item > max_val:
max_val = item
return max_val
# O(n^2) - quadratic time
def print_pairs(arr):
for i in range(len(arr)):
for j in range(len(arr)):
print(arr[i], arr[j])
# O(log n) - logarithmic time
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
if arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
Omega notation gives the lower bound of an algorithm. It tells us the minimum amount of work the algorithm must do in the best case or in a lower-bound sense.
Example: in linear search, if the target is the first element, the search finishes immediately. So the best-case time is Omega(1).
For linear search:
Theta notation gives a tight bound. It is used when the upper bound and lower bound are the same order of growth.
If an algorithm always grows like n log n, then we say its complexity is Theta(n log n).
Merge Sort takes Theta(n log n) time because its running time remains in the same growth class for best, average, and worst cases.
| Notation | View | Meaning | Simple Interpretation |
|---|---|---|---|
| O(f(n)) | Upper bound | Algorithm grows at most this fast | Ceiling |
| Omega(f(n)) | Lower bound | Algorithm grows at least this fast | Floor |
| Theta(f(n)) | Tight bound | Algorithm grows exactly at this order | Both floor and ceiling |
| Complexity | Name | Meaning | Example |
|---|---|---|---|
| O(1) | Constant | Does not depend on input size | Array access by index |
| O(log n) | Logarithmic | Problem size reduces quickly | Binary Search |
| O(n) | Linear | Work grows directly with input | Linear Search |
| O(n log n) | Linearithmic | Very efficient for many sorting tasks | Merge Sort, Heap Sort |
| O(n^2) | Quadratic | Often caused by two nested loops | Bubble Sort |
| O(2^n) | Exponential | Doubles rapidly as n grows | Naive Fibonacci recursion |
| O(n!) | Factorial | Extremely expensive growth | Generating all permutations |
For large values of n, the following order is generally true:
O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(2^n) < O(n!)
This is why algorithms with quadratic, exponential, or factorial growth become impractical much more quickly than linear or logarithmic ones.
While writing asymptotic notation, we usually simplify the expression using a few standard rules.
| Rule | Example | Result |
|---|---|---|
| Drop constants | O(5n) | O(n) |
| Drop lower-order terms | O(n^2 + n + 10) | O(n^2) |
| Sequential parts add | O(n) + O(n^2) | O(n^2) |
| Nested loops multiply | O(n) inside O(n) | O(n^2) |
# Example 1: Sequential loops
def example1(arr):
for item in arr:
print(item)
for item in arr:
print(item * 2)
# O(n) + O(n) = O(2n) = O(n)
# Example 2: Nested loops
def example2(n):
for i in range(n):
for j in range(n):
print(i, j)
# O(n) * O(n) = O(n^2)
# Example 3: Logarithmic loop
def example3(n):
i = 1
while i < n:
print(i)
i = i * 2
# 1, 2, 4, 8, ... so complexity is O(log n)
# Example 4: Recursive Fibonacci
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# Very large recursion tree
# Complexity: O(2^n)
The following tl-table gives an approximate idea of how fast different complexity classes grow as input size increases.
| Complexity | n = 10 | n = 100 | n = 1,000 | Main Observation |
|---|---|---|---|---|
| O(1) | 1 | 1 | 1 | Always stable |
| O(log n) | About 3 | About 7 | About 10 | Grows very slowly |
| O(n) | 10 | 100 | 1,000 | Direct proportional growth |
| O(n log n) | About 30 | About 664 | About 9,966 | Still manageable for large data |
| O(n^2) | 100 | 10,000 | 1,000,000 | Becomes expensive quickly |
| O(2^n) | 1,024 | Extremely large | Infeasible | Useful only for very small input |
| O(n!) | 3,628,800 | Impossible to handle | Impossible to handle | Practical only for tiny values of n |
In some advanced analysis, two more notations are also used:
| Notation | Meaning | Use |
|---|---|---|
| o(f(n)) | Strict upper bound | Used when growth is strictly smaller than f(n) |
| omega(f(n)) | Strict lower bound | Used when growth is strictly larger than f(n) |
These are less common in beginner-level DAA, but it is good to know that they exist.
After asymptotic notation, the next natural topic is time complexity. There you will learn how to calculate the running time of loops, recursion, and real algorithmic code more systematically.
Explore 500+ free tutorials across 20+ languages and frameworks.