Foundations/Asymptotic Analysis

Lesson 1.33,626 words

Asymptotic Analysis

We measure an algorithm's running time as a function of its input size, then strip away machine-specific constants and lower-order terms to compare algorithms cleanly. This lesson defines the RAM model and the OO, Ω\Omega, Θ\Theta, oo, and ω\omega notations, proves the polynomial theorem, and shows how to rank growth rates with the limit test, L'Hôpital, base substitution, and the logarithm identities the arguments lean on.

╌╌╌╌

In the previous lesson we saw the same algorithm, insertion sort, cost quadratically many comparisons on one input and linearly many on another, all on the same machine. To compare algorithms as algorithms, independent of the hardware and the particular input, we need two things: a model of computation abstract enough to ignore the machine, and a notation coarse enough to ignore constants. This lesson supplies both.

The RAM model of computation

We analyze algorithms against an idealized machine, the random-access machine (RAM). It has the properties CLRS, Skiena, and Erickson all assume, usually tacitly:

  • Instructions execute one at a time, no concurrency.
  • The basic operations, namely arithmetic (, , , ), comparisons, data movement (load, store, copy), and control flow, each take a constant amount of time.
  • Memory is an unbounded array of cells, and accessing any cell by its index costs the same constant (this is what random access means).
  • Each cell holds an integer or float of reasonable size, roughly bits for an input of size , so a single value fits in a machine word.

The RAM is a deliberate fiction. Real multiplication is not truly constant-time for arbitrarily large numbers; real memory has caches that make some accesses far cheaper than others. But the model is predictive: an algorithm that is fast on the RAM is, overwhelmingly, fast in practice. Skiena stresses this engineering payoff;1 CLRS is careful to flag the places, such as bignum arithmetic, where the constant-word assumption breaks down.2

From problem to

It helps to be precise about what we are even measuring. A computational problem is just a function from a set of possible inputs to a set of possible outputs; each element is an instance of . Sorting integer arrays, for example, has . A size function records how big each instance is. For an array we take . An algorithm solves if for every instance .

Now charge = the number of elementary RAM operations performs on input . Inputs of the same size can cost different amounts, so we take the worst one of each size:

When the algorithm is understood, this is the function we denote , the running time as a function of the input size . The size is usually the number of elements, but sometimes the number of bits, or two parameters (e.g. and for a graph); choosing the right size measure is the first decision in any analysis. What we ultimately want is a good, convenient-to-understand upper bound on , which is what the notation below provides.

Worst, average, and best case

For a fixed input size , different inputs of that size may cost different amounts. Insertion sort costs on a sorted array and on a reversed one. So is not one number; it is a range. We summarize it three ways:

  • Worst case : the maximum cost over all inputs of size .
  • Best case: the minimum cost over all inputs of size .
  • Average case: the expected cost over a probability distribution on inputs of size (usually the uniform distribution).
For each fixed size , the cost is a range: best at the bottom edge, worst at the top, average in between. The shaded band is the spread over all inputs of that size. Slicing at one chosen size pins the three cases to three points on the vertical — the runtime of any single input of size lands somewhere on that segment.

We almost always report the worst case. It is a guarantee: the algorithm never does worse, no matter how adversarial the input. The best case is nearly useless as a promise, since any algorithm looks good on its luckiest input. The average case is the most honest predictor of typical performance but requires us to commit to a distribution, and the analysis is usually harder (it often needs the probabilistic tools of a later module). CLRS develops all three; Skiena argues that for design purposes the worst case is what you should plan for.

Why we drop constants and lower-order terms

Suppose careful counting gives the running time of some algorithm as

Two facts make most of this expression noise:

  1. The leading term dominates. As grows, swamps . At the quadratic term is and the rest is , under . The growth rate is governed entirely by .
  2. The constants are machine artifacts. The depends on how many RAM operations our particular pseudocode spends per iteration; recompile on a different machine and it changes. It says nothing about the algorithm's intrinsic scaling.

So we throw both away and say the running time is order . This is the right level of abstraction. An algorithm with a tiny constant still loses eventually to an algorithm with a large one, and eventually is what asymptotic analysis captures. The notation below makes order precise.

The asymptotic notations

Let and be functions from the positive integers to the nonnegative reals. The notations describe how behaves relative to for all sufficiently large .

Big-O: asymptotic upper bound

State it as a clean existential:

The constant lets us ignore multiplicative factors; the threshold lets us ignore small inputs where lower-order terms might still dominate. (CLRS phrases as a set of functions and adds to keep things nonnegative; the two readings agree.)

The picture to keep in mind is the for large sketch: the scaled curve rises above once passes the threshold , and stays above forever after. What happens to the left of is irrelevant.

Curve rises above once passes the threshold .

The wanted inequality method. Proofs of -bounds follow a fixed recipe: write down the inequality you want to hold, then reverse-engineer constants and that make it true. To prove , we want . For each lower term is at most , so ; thus , work. The same move handles any polynomial; see the theorem below.

Big-Omega: asymptotic lower bound

Mirror the quantifiers, flip the inequality:

It is the mirror image of : if and only if .

Big-Theta: asymptotic tight bound

pins between two constant multiples of : it grows exactly as fast as , up to constants. The fundamental link is

When we say insertion sort is in the worst case, we mean its worst-case cost is sandwiched between and , a precise, two-sided claim. When we only have an upper bound we say ; this is why people loosely write even where holds. But the distinction matters, as the next two results show.

The polynomial theorem

The single most useful fact for everyday analysis collapses every polynomial to its leading power:

polynomial_growth.pypython
from typing import NamedTuple, Sequence

class BoundWitness(NamedTuple):
  """
    A constant `c` and threshold `n0` certifying f(n) <= c * n^degree for\n
    every n >= n0 — the pair an O-bound proof must exhibit.\n
  """
  constant: float
  threshold: int

class Polynomial:
  """
    A polynomial stored by its coefficients, lowest power first.\n
    `coefficients[index]` is the multiplier on n^index, so\n
    [200, 50, 3] represents 3 n^2 + 50 n + 200.\n
  """

  def __init__(self, coefficients: Sequence[float]) -> None:
    if not coefficients:
      raise ValueError("a polynomial needs at least one coefficient")
    self.coefficients: list[float] = list(coefficients)

  def degree(self) -> int:
    """
      The highest power with a non-zero coefficient (0 for a constant).\n
    """
    # walk powers high-to-low for the first non-zero coefficient.
    for power in range(len(self.coefficients) - 1, -1, -1):
      if self.coefficients[power] != 0:
        return power
    return 0

  def leading_coefficient(self) -> float:
    """
      The coefficient on the highest non-zero power — the `a_k` the\n
      theorem requires to be positive for the Theta(n^k) conclusion.\n
    """
    return self.coefficients[self.degree()]

  def evaluate(self, size: int) -> float:
    """
      The value of the polynomial at n = `size`, by Horner's rule.\n
    """
    # fold high power down, each step multiplying by size and adding a_i.
    total: float = 0.0
    for coefficient in reversed(self.coefficients):
      total = total * size + coefficient

    return total

  def growth_exponent(self) -> int:
    """
      The exponent k such that this polynomial is Theta(n^k).\n
      That is exactly its degree, provided the leading coefficient is\n
      positive (otherwise the "Theta(n^k)" conclusion does not apply).\n
    """
    if self.leading_coefficient() <= 0:
      raise ValueError("Theta(n^k) needs a positive leading coefficient")
    return self.degree()

  def upper_bound_witness(self) -> BoundWitness:
    """
      A (c, n0) witness for f(n) = O(n^degree), built by the lesson's\n
      method: for n >= 1 every lower power is at most n^degree, so summing\n
      the absolute values of all coefficients bounds f(n) by\n
      (sum |a_i|) * n^degree. Hence c = sum of |a_i| and n0 = 1.\n
    """
    constant: float = sum(abs(coefficient) for coefficient in self.coefficients)
    return BoundWitness(constant=constant, threshold=1)

is an upper bound, not a promise of tightness

Consider a worked cautionary case. Exchange sort compares with for every pair , so its running time satisfies

by the polynomial theorem. But also implies the true but useless statement : a correct upper bound need not be tight. So how loose can we go? Not below . The number of pairs is itself a lower bound on the work:

This forces ; indeed for any . No constant can keep under once is large enough, because whenever . The moral: alone tells you the cost is no worse than something; only matching it with (i.e. proving ) certifies you have found the true growth rate.

Picture the exponents on a line. Exchange sort's cost sits at . Every with is a valid upper bound, but only is tight; the side forbids any upper bound with , walling off the left. Where the two bounds meet is .

Valid bounds for exchange sort's cost, by exponent. Every with holds but only is tight; rules out the whole region below . The bounds pinch shut exactly at .

Little-o and little-omega: strict bounds

and allow and to grow at the same rate. The lowercase versions forbid that; they assert a strict gap.

So means becomes negligible compared to (, ), and means dominates (, ) — mirror images, since . A useful analogy from CLRS: are to functions as are to numbers.

Each asymptotic notation mirrors a comparison on numbers: line up with . The little-o and little-omega ends are strict (they forbid equal growth), exactly as and are strict. One example per column: , , , , .

Strict implies loose, never the reverse

Little-o strengthens . If , then the inequality holds in particular for past some threshold, and with that threshold witnesses . So , and by the mirror argument . The containment is proper, and the counterexample is as small as they come:

The picture to hold: is the part of that keeps a widening gap below , and is the part that tracks exactly. The two cannot overlap: if then eventually drops below for every candidate lower constant , so no bound, and hence no bound, can hold. The sets , , and are pairwise disjoint, which is precisely what the strict/tight/strict labels in the figure record.

One warning before leaning on the number analogy too hard. Real numbers obey trichotomy: for any , exactly one of , , holds. Functions do not. CLRS's example is versus : the exponent oscillates between and forever, so the second function is neither nor , and the pair cannot be ranked at all.3 Asymptotic comparison is a partial order, not a total one. In practice the running times we meet are comparable, but proofs should never assume two functions can be ordered.

asymptotic_relation.pypython
import math
from enum import Enum, auto
from typing import Callable

# A growth function maps an input size to a non-negative cost.
GrowthFunction = Callable[[int], float]

class Relation(Enum):
  """
    The strict verdict of comparing f to g by the limit of the ratio.\n
    LITTLE_O is f = o(g) (so also f = O(g)); THETA is f = Theta(g);\n
    LITTLE_OMEGA is f = omega(g) (so also f = Omega(g)). These mirror\n
    <, =, > on numbers.\n
  """
  LITTLE_O = auto()
  THETA = auto()
  LITTLE_OMEGA = auto()

def limit_of_ratio(
  numerator: GrowthFunction,
  denominator: GrowthFunction,
  largest_size: int = 1 << 14,
) -> float:
  """
    Estimate lim_{n->inf} numerator(n) / denominator(n) by probing the\n
    ratio at geometrically growing sizes up to `largest_size`. Returns\n
    math.inf when the ratio is diverging and 0.0 when it is vanishing.\n
    The verdict comes from the *trend* of the last two probes, not their\n
    raw magnitude: a ratio still shrinking by a factor of two each\n
    doubling is heading to 0, one still growing is heading to infinity,\n
    and one that has settled is a finite constant. `denominator` must be\n
    eventually positive.\n
  """
  # probe the ratio at doubling sizes, keeping the last two readings.
  size: int = 1
  previous: float = 0.0
  current: float = 0.0
  while size <= largest_size:
    bottom: float = denominator(size)
    if bottom > 0:
      previous = current
      current = numerator(size) / bottom
    size *= 2

  # an outright vanishing or diverging final ratio settles it on its own.
  if current == 0.0:
    return 0.0
  if math.isinf(current):
    return math.inf

  # read the trailing trend: flat -> constant, shrinking -> 0, growing -> inf.
  if previous <= 0.0:
    return current
  trend: float = current / previous
  if trend < 0.95:
    return 0.0
  if trend > 1.05:
    return math.inf

  return current

def classify(
  numerator: GrowthFunction,
  denominator: GrowthFunction,
  largest_size: int = 1 << 14,
) -> Relation:
  """
    The strict relation of `numerator` to `denominator` from the limit of\n
    the ratio: 0 -> LITTLE_O, a finite positive constant -> THETA,\n
    infinity -> LITTLE_OMEGA.\n
  """
  ratio: float = limit_of_ratio(numerator, denominator, largest_size)
  if ratio == 0.0:
    return Relation.LITTLE_O
  if math.isinf(ratio):
    return Relation.LITTLE_OMEGA
  return Relation.THETA

def is_big_o(
  numerator: GrowthFunction,
  denominator: GrowthFunction,
  largest_size: int = 1 << 14,
) -> bool:
  """
    Whether numerator = O(denominator): the loose upper bound, true when\n
    the relation is LITTLE_O or THETA (since o and Theta both imply O).\n
  """
  return classify(numerator, denominator, largest_size) in (
    Relation.LITTLE_O,
    Relation.THETA,
  )

def is_big_omega(
  numerator: GrowthFunction,
  denominator: GrowthFunction,
  largest_size: int = 1 << 14,
) -> bool:
  """
    Whether numerator = Omega(denominator): the loose lower bound, true\n
    when the relation is LITTLE_OMEGA or THETA.\n
  """
  return classify(numerator, denominator, largest_size) in (
    Relation.LITTLE_OMEGA,
    Relation.THETA,
  )

def is_big_theta(
  numerator: GrowthFunction,
  denominator: GrowthFunction,
  largest_size: int = 1 << 14,
) -> bool:
  """
    Whether numerator = Theta(denominator) — a tight, two-sided bound,\n
    equivalently both O and Omega.\n
  """
  return classify(numerator, denominator, largest_size) is Relation.THETA

def witnesses_big_o(
  numerator: GrowthFunction,
  denominator: GrowthFunction,
  constant: float,
  threshold: int,
  largest_size: int = 1 << 14,
) -> bool:
  """
    Check a concrete O-bound witness against the definition: whether\n
    numerator(n) <= constant * denominator(n) holds for every probed\n
    n >= `threshold`. This is the exact inequality an O proof claims, with\n
    the candidate `constant` (c) and `threshold` (n0) supplied.\n
  """
  if constant <= 0 or threshold <= 0:
    raise ValueError("a witness needs c > 0 and n0 > 0")

  # test the inequality from n0 up, densely near n0 then doubling.
  size: int = threshold
  while size <= largest_size:
    if numerator(size) > constant * denominator(size):
      return False
    size += 1 if size < threshold + 64 else size

  return True

Comparing functions with limits

The limit of the ratio is how you rank growth rates:

The three outcomes of the ratio test. Plot against : a ratio sinking to certifies ; a ratio settling at a constant certifies ; a ratio climbing without bound certifies .

One caution. The test is sufficient, not necessary: the limit may fail to exist even when a -bound holds. The function hops between and forever, so has no limit, yet with and . When the ratio oscillates, fall back on the quantifier definitions; they are the ground truth, and the limit forms are a convenience layered on top.4

The ratio test, worked three times

The method: form the ratio, simplify it until its limit is readable, and apply the case table above. Three comparisons cover the moves that recur in practice.

1. vs : L'Hôpital on the leftover. Divide out the shared factor of first:

Numerator and denominator both tend to infinity, and both are differentiable as functions of a real variable , so L'Hôpital's rule applies (switch to ; the base costs only a constant factor):

So : any polynomial exponent strictly above , however slightly, eventually outgrows . The crossover is remote, though. The ratio only drops below once , which first happens around , on the order of . For every input you will ever benchmark, looks bigger; the limit says the polynomial wins anyway.

2. Polylogs vs polynomials: substitute . Claim: for all constants ,

so every polynomial beats every polylogarithm.5 The ratio mixes a log and a power awkwardly; the fix is a change of variable. Set (so , and exactly when ):

The substitution turns an awkward polylog-vs-polynomial race in into a familiar polynomial-vs-exponential race in : vs becomes vs , and the exponential wins.

This is now a polynomial in against an exponential in . Take of the ratio:

because (the , exponent- case of comparison 1). A quantity whose logarithm tends to tends to , so the ratio vanishes and the claim holds. Erickson's slogan for the general principle: take logs until the comparison becomes one you already know.

3. vs : take the log of the ratio. The same move settles polynomials against exponentials directly:

since by comparison 2. Hence , i.e. for every fixed ; even is eventually dominated by . Read backwards, : no polynomial upper bound of any degree can hold for an exponential.

Small inputs lie

Comparison 2 comes with the same caveat as comparison 1: the crossover can sit far beyond any table of test values. Take , , that is, against . The two are equal exactly when for , and gives on both sides. So the polylog stays above the polynomial for every up to , and near it is ahead by a factor of about . An empirical plot stopping at would rank the two backwards; the limit gets it right.

Small inputs lie. The vertical axis is the ratio of to on a log scale, so the horizontal axis is the ratio- line. The polylog runs ahead (ratio above , peaking near around ) all the way to the crossover at — only then does the polynomial pull ahead for good.

Why we write with no base. Inside asymptotic notation the base is irrelevant, because changing base only multiplies by a constant:

So , , and all live in the same -class, and means the same thing whichever base you had in mind. (The constant is the multiplicative factor and are designed to absorb.)

The logarithm identities analysis leans on

Base-changing is one of a small kit of identities that carry most asymptotic arguments involving logs:6

Each has a specific role. The product rule turns products into sums, which is how one analyzes anything defined multiplicatively; the log of becomes the sum , handled below. The power rule pulls exponents out front, so : the logarithm of any polynomial in is just , degree notwithstanding. And base-change is the license to write bare .

One less familiar identity swaps a base against an exponent:

Its use is cosmetic but constant: it rewrites an exponential in as a plain polynomial. For instance , a shape that will fall out of divide-and-conquer recurrences and would otherwise be hard to place in the hierarchy.

Finally, the sum promised above:

Stirling's approximation sharpens the same statement to ,7 but the half-the-terms trick above is the version worth internalizing; it reappears whenever a sum needs a quick lower bound. This bound is also why comparison sorts that do work are optimal in a sense we will prove later.

With the notations defined and the ranking machinery in hand, the next lesson puts them to work: it lays out the standard growth hierarchy, proves the orderings between its rungs, and shows how to read the running time of a loop nest straight off the page. This continues in Growth Rates and Loop Analysis.

Takeaways

  • The RAM model charges constant time per primitive operation and lets us measure running time as a function of input size, machine-independently.
  • Report the worst case by default: it is a guarantee. Best case promises nothing; average case is honest but needs a distribution.
  • Drop constants and lower-order terms. They are machine artifacts and noise; the leading term's growth rate is what scales.
  • is an upper bound, a lower bound, a tight (two-sided) bound; and are their strict versions. and .
  • alone certifies only that the cost is no worse than something; matching it with (proving ) is what pins the true growth rate. Comparison is a partial order — some function pairs, like and , cannot be ranked at all.
  • The limit ranks two functions: , a constant, or give , , or . When the ratio is hard to evaluate, take its logarithm or substitute until the comparison becomes one you know.
  • Small inputs lie. exceeds for every up to , and looks larger than out past ; only the limit verdict is final.
  • Logarithms: the base never matters inside ; turns products into sums; turns exponentials in into plain polynomials; and by the half-the-terms trick.

Footnotes

  1. Skiena, §2 — Algorithm Analysis: the RAM model as a machine-independent abstraction whose engineering payoff is predicting real-world speed.
  2. CLRS, Ch. 3 — Characterizing Running Times: the RAM model's constant-word assumption and where it breaks down (e.g. bignum arithmetic).
  3. CLRS, Ch. 3 — Characterizing Running Times: asymptotic comparability is not total; and cannot be ranked.
  4. CLRS, Ch. 3 — Characterizing Running Times: the limit characterizations of and ; the quantifier definitions remain the ground truth when the limit does not exist.
  5. Erickson, Algorithms, Appendix — Solving Recurrences (analysis throughout): ranking growth rates by the limit of the ratio, so every polynomial dominates every polylogarithm.
  6. Skiena, §2 — Algorithm Analysis: logarithm identities and their consequences for analysis, including why the base is irrelevant inside asymptotic notation.
  7. CLRS, Ch. 3 — Characterizing Running Times: via Stirling's approximation.
Practice

╌╌ END ╌╌