Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA) Module 1 – Foundations: problems, correctness and complexity

Big-O, Big-Omega and Big-Theta notation

The formal definitions of O, Omega and Theta with explicit constants, how to cut a running time down to its dominant term, and the misuses to avoid.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot

What you will learn

  • Apply the formal definitions of O, Omega and Theta with explicit constants
  • Simplify running-time expressions to their dominant term
  • Avoid common misuses such as reading O as the worst case or dropping a second input
  • Compare two growth rates with the limit of their ratio

Before you start

On this page

Counting gives exact answers such as 3n² + 10n + 7 steps. For large n most of that expression stops mattering: at n = 1,000,000 the 3n² part is 3 × 10¹², while 10n + 7 adds about ten million more, a few millionths of the total. Asymptotic notation keeps the part that decides how the cost grows and drops the rest, including the constant factors that depend on the computer.

The three definitions

Running times are never negative, so the definitions below are written for functions f and g that take non-negative values. Each one says how f compares with a constant multiple of g from some point n₀ onwards; what happens for small n does not count.

  • Big-O is an upper bound. f(n) = O(g(n)) when there are constants c > 0 and n₀ with f(n)≤c⋅g(n)f(n) \le c \cdot g(n) for every n ≥ n₀: eventually f is at most a constant times g.
  • Big-Omega is a lower bound. f(n) = Ω(g(n)) when there are constants c > 0 and n₀ with f(n)≥c⋅g(n)f(n) \ge c \cdot g(n) for every n ≥ n₀: eventually f is at least a constant times g.
  • Big-Theta is both at once. f(n) = Θ(g(n)) when f(n) = O(g(n)) and f(n) = Ω(g(n)), so there are constants c₁, c₂ > 0 and n₀ with c1⋅g(n)≤f(n)≤c2⋅g(n)c_1 \cdot g(n) \le f(n) \le c_2 \cdot g(n) for every n ≥ n₀: the two grow at the same rate.

The constants c and n₀ must be fixed numbers: they may depend on f and g, but not on n. Donald Knuth proposed Ω and Θ with these meanings for the analysis of algorithms, in “Big Omicron and big Omega and big Theta” (ACM SIGACT News, volume 8, number 2, pages 18–24); NIST’s Dictionary of Algorithms and Data Structures lists that paper on its pages for both symbols. The dictionary also notes that this Ω asks more than the older definition used in mathematics, which needed the inequality only for an endless sequence of values of n, not for every n from some point on.

The “=” in f(n) = O(g(n)) is a one-way statement, best read as “f is in O(g)”. So n = O(n²) is true, because n ≤ n² for every n ≥ 1, while n² = O(n) is false, and you cannot swap the two sides as you would in an equation.

A proof with explicit constants

Take f(n) = 3n² + 10n + 7 and prove that f(n) = Θ(n²).

  • Upper bound. For n ≥ 1, 10n ≤ 10n² and 7 ≤ 7n². Adding these to 3n² gives f(n) ≤ 20n². So c = 20 and n₀ = 1 show that f(n) = O(n²).
  • Lower bound. 10n + 7 is positive, so f(n) ≥ 3n² for every n. So c = 3 and n₀ = 1 show that f(n) = Ω(n²).
  • Both hold, so f(n) = Θ(n²).

The constants are not unique. A smaller c needs a larger n₀: c = 4 works only once n² has outgrown 10n + 7, which first happens at n = 11. This program checks both bounds for every n up to a million, finds the first n₀ for three values of c, and prints how close f(n)/n² comes to 3:

Checking the constants of a Θ proof Python · theta_check.py
def f(n):
    """A running time worked out by counting: 3n² + 10n + 7 steps."""
    return 3 * n * n + 10 * n + 7


N = 1_000_000
assert all(3 * n * n <= f(n) <= 20 * n * n for n in range(1, N + 1))
print(f"3n² <= f(n) <= 20n² for every n from 1 to {N:,}")

for c in (4, 5, 20):
    n0 = 1
    while f(n0) > c * n0 * n0:  # the first n where f(n) <= c·n² holds
        n0 += 1
    assert all(f(n) <= c * n * n for n in range(n0, N + 1))  # ... and it keeps holding up to N
    print(f"c = {c:>2}: f(n) <= {c}n² from n0 = {n0}")

for n in (10, 100, 1_000, 1_000_000):
    print(f"f({n:,}) / {n:,}² = {f(n) / (n * n):.6f}")

Output

3n² <= f(n) <= 20n² for every n from 1 to 1,000,000
c =  4: f(n) <= 4n² from n0 = 11
c =  5: f(n) <= 5n² from n0 = 6
c = 20: f(n) <= 20n² from n0 = 1
f(10) / 10² = 4.070000
f(100) / 100² = 3.100700
f(1,000) / 1,000² = 3.010007
f(1,000,000) / 1,000,000² = 3.000010

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 theta_check.py

A check up to a million is evidence, not proof; the algebra above is the proof, and it covers every n. The ratio f(n)/n² falls towards 3, the constant in front of the n² term: another sign that n² is the right comparison.

Graphing Calculator Plot y = 3x^2 + 10x + 7 and y = 4x^2 together and find where the second overtakes the first: just before x = 11.

Keep the dominant term

Proofs like the one above all follow a few rules, which let you simplify a running time in one line:

  1. Drop constant factors. 5n² = Θ(n²) and n/2 = Θ(n).
  2. Drop lower-order terms. n² + 1000n = Θ(n²), because 1000n ≤ n² once n ≥ 1000.
  3. A polynomial is Θ of its highest power. 4n³ − 2n² + 9 = Θ(n³).
  4. The base of a logarithm does not matter. log₂ n and log₁₀ n differ by the constant factor log₂ 10, so both are Θ(log n), and the base is left out (the logarithm calculator changes base for you when a count needs an actual number).
  5. The base of an exponential does matter. 3ⁿ / 2ⁿ = 1.5ⁿ grows without limit, so 3ⁿ is not O(2ⁿ).
Logarithm bases differ by a constant, exponential bases do not Python · log_bases.py
import math

print("log2(n) / log10(n), for n = 10, 1,000 and 10**9:")
print(" ", ", ".join(f"{math.log2(n) / math.log10(n):.5f}" for n in (10, 1_000, 10**9)))

print("3**n / 2**n, for n = 10, 100 and 1,000:")
print(" ", ", ".join(f"{3**n / 2**n:.4g}" for n in (10, 100, 1_000)))

Output

log2(n) / log10(n), for n = 10, 1,000 and 10**9:
  3.32193, 3.32193, 3.32193
3**n / 2**n, for n = 10, 100 and 1,000:
  57.67, 4.066e+17, 1.234e+176

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 log_bases.py

The first line is the same for every n: the two logarithms are always the same multiple of each other. The second line keeps growing, which is why 2ⁿ and 3ⁿ belong to different classes even though both are called exponential.

From slowest to fastest, the growth rates you will meet most often in this track are:

1<log⁡n<n<n<nlog⁡n1 < \log n < \sqrt{n} < n < n \log n nlog⁡n<n2<n3<2n<n!n \log n < n^2 < n^3 < 2^n < n!

Each one is o(the next), in the sense defined at the end of this lesson.

Misuses to avoid

“Big-O means the worst case”

It does not. Big-O bounds a function, and the best case, the worst case and the average case of an algorithm are three different functions of n. Each can be described with O, Ω or Θ. Insertion sort shows the difference:

Comparisons made by insertion sort on three kinds of input Python · insertion_cases.py
import random


def insertion_sort(items):
    """Sort a copy of items, and count the comparisons between two items."""
    a = list(items)
    comparisons = 0
    for i in range(1, len(a)):
        x = a[i]
        j = i - 1
        while j >= 0:
            comparisons += 1
            if a[j] <= x:
                break
            a[j + 1] = a[j]  # shift the larger item one place right
            j -= 1
        a[j + 1] = x
    assert a == sorted(items)
    return comparisons


rng = random.Random(11)
print(f"{'n':>6} {'sorted input':>13} {'random input':>13} {'reversed input':>15}")
for n in (10, 100, 1_000):
    values = list(range(n))
    shuffled = rng.sample(values, n)
    counts = insertion_sort(values), insertion_sort(shuffled), insertion_sort(values[::-1])
    print(f"{n:>6,} {counts[0]:>13,} {counts[1]:>13,} {counts[2]:>15,}")

Output

     n  sorted input  random input  reversed input
    10             9            38              45
   100            99         2,436           4,950
 1,000           999       245,288         499,500

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 insertion_cases.py

On sorted input each new item needs one comparison, n − 1 in all; on reversed input it is compared with everything before it, n(n − 1)/2 in all; random input lands near n²/4. So the precise statement is: insertion sort’s worst case is Θ(n²) and its best case is Θ(n). “Insertion sort is O(n²)” is true, because no input needs more than quadratic time, but it is not the same claim as “the worst case is Θ(n²)”, and “insertion sort is Θ(n²)” is false for sorted input.

Dropping a second input

When the cost depends on two sizes, keep both. Checking every name of a list a against a list b with in:

Two lists, two sizes Python · two_inputs.py
def common_names(a, b):
    """Names that are in both lists, and how many comparisons the `in` tests made."""
    comparisons = 0
    both = []
    for name in a:
        for other in b:  # what `name in b` does on a list
            comparisons += 1
            if name == other:
                both.append(name)
                break
    return both, comparisons


for n, m in ((1_000, 10), (10, 1_000), (1_000, 1_000)):
    a = [f"guest{i}" for i in range(n)]
    b = [f"member{i}" for i in range(m)]  # no names in common: every `in` checks all of b
    _, steps = common_names(a, b)
    print(f"n = {n:>5,}, m = {m:>5,}: {steps:>9,} comparisons = n × m")

Output

n = 1,000, m =    10:    10,000 comparisons = n × m
n =    10, m = 1,000:    10,000 comparisons = n × m
n = 1,000, m = 1,000: 1,000,000 comparisons = n × m

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_inputs.py

The cost is O(n · m) for n = len(a) and m = len(b), not O(n²): the first two lines make the same 10,000 comparisons with very different n. In the same way, a graph algorithm that looks at every vertex and every edge once is O(V + E), and comparing n strings of length L each is O(n · L).

A true bound that says too little

3n + 4 = O(n²) is true, and NIST’s dictionary uses it as an example, but it hides the fact that the cost is linear. Give the tightest bound you can prove, and use Θ when you have both directions.

Forgetting the constants exist

Big-O hides constant factors, and for small inputs they can decide. An algorithm that takes 100n steps is slower than one that takes n² steps for every n below 100, although Θ(n) beats Θ(n²) in the long run. The first lesson of this track measured such a constant factor.

Little-o, little-omega and the limit test

The strict versions say that one function grows strictly slower or faster than another:

  • f(n) = o(g(n)) when, for every constant c > 0, f(n) < c·g(n) from some n₀ on (n₀ may depend on c). f becomes negligible next to g.
  • f(n) = ω(g(n)) when g(n) = o(f(n)).

The CS2023 curriculum lists both among the complexity topics beyond Big-O, Big-Omega and Big-Theta. The quickest way to compare two functions is often the limit of their ratio: work out L=limn→∞⁡f(n)/g(n)L = \lim_{n \to \infty} f(n) / g(n) and read off the answer.

  • L = 0: f(n) = o(g(n)), so f grows strictly slower.
  • L is a constant greater than 0: f(n) = Θ(g(n)), so they grow at the same rate.
  • L is infinite: f(n) = ω(g(n)), so f grows strictly faster.

For f(n) = 3n² + 10n + 7 and g(n) = n² the ratio tends to 3, as the program above showed, so f(n) = Θ(n²). For n log n against n², the ratio is (log n)/n, which tends to 0, so n log n = o(n²). If the ratio has no limit at all, the test says nothing, and you are back to the definitions.

Key takeaways

  • f(n) = O(g(n)) means f(n) ≤ c·g(n) for all n ≥ n₀, for some fixed constants c and n₀; Ω is the matching lower bound and Θ is both.
  • A Θ proof names its constants: 3n² ≤ 3n² + 10n + 7 ≤ 20n² for every n ≥ 1.
  • Simplify by dropping constant factors and lower-order terms; logarithm bases do not matter, exponential bases do.
  • O is a bound, not a case: state which case (best, worst, average) you are bounding, and keep every input size that matters.
  • If f(n)/g(n) tends to 0, f is o(g); to a positive constant, Θ(g); to infinity, ω(g).

Exercise

Exercise · Easy · Python, JavaScript

Find the smallest n0 for a Big-O constant

To show that f(n) = 3n² + 10n + 7 is O(n²), you pick a constant c and find an n0 such that f(n) ≤ c·n² for every n ≥ n0. Write smallest_n0(c) (JavaScript: smallestN0(c)), which returns the smallest whole number n0 ≥ 1 that works for the given c.

  • For c = 4 it returns 11: f(10) = 407 is more than 4 × 100 = 400, but from n = 11 on the inequality holds.
  • When no n0 exists it returns None (JavaScript: null). That happens when c ≤ 3, because f(n) − 3n² = 10n + 7 is always positive.
  • c may be a fraction, such as 3.5.

For c greater than 3, the inequality says (c − 3)·n² − 10n − 7 ≥ 0. That expression reaches 0 or more at some n and stays there for every larger n, so the first n where the inequality holds is the answer. The sample tests try seven values of c, and check that your n0 works and that n0 − 1 does not.

Python · Starter code · witness.py

def f(n):
    return 3 * n * n + 10 * n + 7


def smallest_n0(c):
    """The smallest whole n0 >= 1 with f(n) <= c * n * n for every n >= n0, or None when there is none."""
    # Replace this line with your code.
    return None
The sample tests · test_witness.py
from witness import f, smallest_n0


def works_from(c, n0):
    """f(n) <= c·n² for n0 and the next 2,000 values of n, and not for n0 - 1."""
    holds = all(f(n) <= c * n * n for n in range(n0, n0 + 2001))
    starts_there = n0 == 1 or f(n0 - 1) > c * (n0 - 1) ** 2
    return holds and starts_there


def test_c_4():
    """c = 4 gives n0 = 11"""
    assert smallest_n0(4) == 11


def test_c_5_and_20():
    """c = 5 gives 6, and c = 20 works from n = 1"""
    assert [smallest_n0(5), smallest_n0(20)] == [6, 1]


def test_fractions():
    """fractional constants: c = 3.5 and c = 3.01"""
    assert [smallest_n0(3.5), smallest_n0(3.01)] == [21, 1001]


def test_no_constant():
    """c = 3 and c = 2 have no n0"""
    assert [smallest_n0(3), smallest_n0(2)] == [None, None]


def test_answers_are_smallest():
    """each answer works, and one less does not"""
    for c in (4, 5, 20, 3.5, 3.01, 13, 100):
        assert works_from(c, smallest_n0(c)), c

JavaScript · Starter code · witness.mjs

export const f = (n) => 3 * n * n + 10 * n + 7;

/** The smallest whole n0 >= 1 with f(n) <= c * n * n for every n >= n0, or null when there is none. */
export function smallestN0(c) {
  // Replace this line with your code.
  return null;
}
The sample tests · witness.test.mjs
import { test, assert } from 'toolverse:test';
import { f, smallestN0 } from './witness.mjs';

/** f(n) <= c·n² for n0 and the next 2,000 values of n, and not for n0 - 1. */
function worksFrom(c, n0) {
  for (let n = n0; n <= n0 + 2000; n++) if (f(n) > c * n * n) return false;
  return n0 === 1 || f(n0 - 1) > c * (n0 - 1) ** 2;
}

test('c = 4 gives n0 = 11', () => assert.equal(smallestN0(4), 11));
test('c = 5 gives 6, and c = 20 works from n = 1', () => assert.deepEqual([smallestN0(5), smallestN0(20)], [6, 1]));
test('fractional constants: c = 3.5 and c = 3.01', () => assert.deepEqual([smallestN0(3.5), smallestN0(3.01)], [21, 1001]));
test('c = 3 and c = 2 have no n0', () => assert.deepEqual([smallestN0(3), smallestN0(2)], [null, null]));
test('each answer works, and one less does not', () => {
  for (const c of [4, 5, 20, 3.5, 3.01, 13, 100]) assert.ok(worksFrom(c, smallestN0(c)), String(c));
});
A hint

Handle c ≤ 3 first and return None straight away, or a search would never stop. Otherwise start at n = 1 and add 1 while 3 * n * n + 10 * n + 7 > c * n * n; the first n where the loop stops is n0. Solving the quadratic (c − 3)·n² − 10n − 7 = 0 and rounding its positive root up gives the same answer without a loop.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

8 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 8 What is the tightest bound on the number of times work() is called?

    Read the code, then choose one answer.

    for i in range(n):
        j = n
        while j > 1:
            j //= 2
            work()
    Show the answer to question 1

    Answer: Θ(n log n)

    The outer loop runs n times and the inner loop halves j each time, about log₂ n passes, so the total is about n log₂ n calls.

  2. Question 2 of 8 a has n items and b has m items. What is the tightest bound on the work?

    Read the code, then choose one answer.

    for x in a:
        work(x)
    for y in b:
        work(y)
    Show the answer to question 2

    Answer: Θ(n + m)

    The loops run one after the other, so their counts add: n + m. Writing Θ(n) would drop the second input, which can be far larger than the first.

  3. Question 3 of 8 What is the tightest bound on the number of passes?

    Read the code, then choose one answer.

    i = 1
    while i * i <= n:
        i += 1
    Show the answer to question 3

    Answer: Θ(√n)

    The loop goes on while i² ≤ n, that is while i ≤ √n, so it makes ⌊√n⌋ passes. Θ(n / 2) would be the same class as Θ(n), and neither is right here.

  4. Question 4 of 8 What is the tightest bound on the number of times work() is called?

    Read the code, then choose one answer.

    for i in range(n):
        for j in range(5):
            work()
    Show the answer to question 4

    Answer: Θ(n)

    The inner loop always runs 5 times, a constant, so the total is 5n calls, and constant factors are dropped: Θ(n). A nested loop is not automatically quadratic.

  5. Question 5 of 8 For f(n) = 3n² + 10n + 7 and c = 13, what is the smallest n0 ≥ 1 with f(n) ≤ 13n² for every n ≥ n0?

    Type a number.

    Show the answer to question 5

    Answer: 2

    f(n) ≤ 13n² means 10n² ≥ 10n + 7. At n = 1 that is 10 ≥ 17, false; at n = 2 it is 40 ≥ 27, true, and the gap only grows after that. So n0 = 2.

  6. Question 6 of 8 Which of these statements are true?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • log₂ n = Θ(log₁₀ n)
    • n = O(n²)
    • 3n² + 10n + 7 = Ω(n²)

    n ≤ n² for n ≥ 1, the two logarithms differ by the constant factor log₂ 10, and 3n² + 10n + 7 ≥ 3n². But n² is not at most c·n for any fixed c once n > c, and 3ⁿ / 2ⁿ = 1.5ⁿ grows without limit.

  7. Question 7 of 8 Which sentence about insertion sort is correct?

    Choose one answer.

    Show the answer to question 7

    Answer: Its worst case is Θ(n²) and its best case is Θ(n)

    Reversed input needs n(n − 1)/2 comparisons and sorted input n − 1, so the two cases are different functions with different tight bounds. Sorted input still needs n − 1 comparisons to find out that it is sorted.

  8. Question 8 of 8 The ratio (n log n) / n² tends to 0 as n grows. What does that show?

    Choose one answer.

    Show the answer to question 8

    Answer: n log n = o(n²)

    A ratio that tends to 0 means the top grows strictly slower than the bottom, which is what little-o says. A positive constant limit would mean Θ, and an infinite one ω.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.