Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Analyzing Algorithms

In Chapter 1 we saw that two algorithms for the same problem — trial division versus the Sieve of Eratosthenes — can differ enormously in performance. In this chapter we develop the mathematical framework for making such comparisons precise. We introduce asymptotic notation, which lets us describe how an algorithm's resource usage grows with input size, and we study several techniques for analyzing running time: best-, worst-, and average-case analysis, amortized analysis, and recurrence relations.

Why analyze algorithms?

Suppose you have two sorting algorithms, and , and you want to know which one is faster. The most direct approach is to run both on the same input and measure the wall-clock time. This is called benchmarking, and it has an important place in software engineering. However, benchmarking has limitations:

  • Hardware dependence. Algorithm might be faster on your laptop but slower on a different machine with a different CPU, cache hierarchy, or memory bandwidth.
  • Input dependence. Algorithm might be faster on the particular test data you chose, but slower on inputs that arise in practice.
  • Implementation effects. A clever implementation of a theoretically slower algorithm can outperform a naive implementation of a theoretically faster one.

What we want is a way to compare algorithms independently of these factors — a way to reason about the inherent efficiency of an algorithm rather than the efficiency of a particular implementation on a particular machine with a particular input. This is what asymptotic analysis provides.

The idea is to count the number of "basic operations" an algorithm performs as a function of the input size , and then focus on how that function grows as becomes large. We ignore constant factors (which depend on the hardware and implementation) and lower-order terms (which become negligible for large ). The result is a concise characterization of an algorithm's scalability.

Measuring input size and running time

Before we can analyze an algorithm, we need to agree on two things: what counts as the input size, and what counts as a basic operation.

Input size is usually the most natural measure of how much data the algorithm must process:

  • For an array of numbers, the input size is the number of elements .
  • For a graph, the input size is often specified as a pair — the number of vertices and edges.
  • For a number-theoretic algorithm like the Sieve of Eratosthenes, the input size is the number itself.

Basic operations are the elementary steps we count. Common choices include comparisons, arithmetic operations, assignments, or array accesses. The specific choice rarely matters for asymptotic analysis, because changing which operation we count changes the total by at most a constant factor.


Definition 2.1 --- Running time

The running time of an algorithm on a given input is the number of basic operations it performs when executed on that input.


We are usually interested in expressing the running time as a function of the input size .

Example 2.1: Running time of max.

Recall the max function from Chapter 1:

export function max(elements: number[]): number | undefined {
  let result: number | undefined;

  for (const element of elements) {
    if (result === undefined || element > result) {
      result = element;
    }
  }
  return result;
}

If we count comparisons as our basic operation, the loop performs exactly one comparison per element (the element > result check; the undefined check is bookkeeping). For an array of elements, the running time is .

Asymptotic notation

Rather than stating that an algorithm takes exactly operations, we want to capture the growth rate — the fact that the dominant behavior is quadratic. Asymptotic notation gives us a precise way to do this.

Big-O: upper bounds


Definition 2.2 --- Big-O notation

Let and be functions from the non-negative integers to the non-negative reals. We write

if there exist constants and such that

In words: grows no faster than , up to a constant factor, for sufficiently large .


Example 2.2. Let . We claim .

Proof. For , we have and , so

Choosing and satisfies Definition 2.2.

Note that is also technically true — is bounded above by — but it is less informative. By convention, we always state the tightest bound we can prove.

Big-Omega: lower bounds


Definition 2.3 --- Big-Omega notation

We write if there exist constants and such that

In words: grows at least as fast as , up to a constant factor.


Example 2.3. .

Proof. For all , . Choose and .

Big-Omega is especially useful for expressing lower bounds on problems: "any algorithm that solves this problem must take at least time."

Big-Theta: tight bounds


Definition 2.4 --- Big-Theta notation

We write if and .

Equivalently, there exist constants and such that

In words: and grow at the same rate, up to constant factors.


Example 2.4. From Examples 2.2 and 2.3, we have .

Big-Theta is the most precise statement: it says the function grows exactly like , within constant factors. When we can determine a Big-Theta bound for an algorithm, we have characterized its running time completely (in the asymptotic sense).

Summary of notation

NotationMeaningAnalogy
grows no faster than
grows at least as fast as
and grow at the same rate

The analogy to , , is informal but helpful for intuition. Formally, all three notations suppress constant factors and describe behavior only for sufficiently large .

Common growth rates

The following table lists growth rates that appear throughout this book, ordered from slowest to fastest:

Growth rateNameExample
ConstantArray index access
LogarithmicBinary search
LinearFinding the maximum
LinearithmicMerge sort, heap sort
QuadraticInsertion sort (worst case)
CubicFloyd-Warshall all-pairs shortest paths
ExponentialBrute-force subset enumeration
FactorialBrute-force permutation enumeration

To appreciate the practical impact, consider an algorithm that performs operations on a computer executing operations per second:

1010 ns33 ns100 ns1 μs1 μs
100100 ns664 ns10 μs1 ms years
1,0001 μs10 μs1 ms1 s
1 ms20 ms17 min31.7 years
1 s30 s31.7 years

The table makes a powerful point: the gap between and is large for a million elements, and the jump to is catastrophic even for modest inputs.

Best case, worst case, and average case

The running time of an algorithm usually depends on the specific input, not just its size. Consider insertion sort.

Insertion sort as a running example

Recall the insertion sort implementation from Chapter 4 (we discuss it fully there, but introduce it here as an analysis example):

export function insertionSort<T>(
  elements: T[],
  comparator: Comparator<T> = numberComparator as Comparator<T>,
): T[] {
  const copy = elements.slice(0);

  for (let i = 1; i < copy.length; i++) {
    const toInsert = copy[i]!;
    let insertIndex = i - 1;

    while (insertIndex >= 0 && comparator(toInsert, copy[insertIndex]!) < 0) {
      copy[insertIndex + 1] = copy[insertIndex]!;
      insertIndex--;
    }
    insertIndex++;
    copy[insertIndex] = toInsert;
  }
  return copy;
}

The outer loop runs iterations (for ). For each iteration, the inner while loop shifts elements to the right until it finds the correct insertion point. The number of shifts depends on the input.

Worst-case analysis


Definition 2.5 --- Worst-case running time

The worst-case running time is the maximum running time over all inputs of size :


For insertion sort, the worst case occurs when the array is sorted in reverse order: . In this case, every new element must be shifted past all previously sorted elements. The inner loop performs comparisons in iteration , so the total number of comparisons is:

Best-case analysis


Definition 2.6 --- Best-case running time

The best-case running time is the minimum running time over all inputs of size :


For insertion sort, the best case occurs when the array is already sorted. Each new element is already in its correct position, so the inner loop performs zero shifts — just one comparison to discover that no shifting is needed. The total is:

This is remarkable: insertion sort runs in linear time on already-sorted input, matching the theoretical minimum for any comparison-based algorithm that must verify sortedness.

Average-case analysis


Definition 2.7 --- Average-case running time

The average-case running time is the expected running time over some distribution of inputs. For a uniform distribution over all permutations of elements:


For insertion sort, consider iteration : the element being inserted has an equal probability of belonging at any of the positions in the sorted prefix. On average, it must be shifted past half of the sorted elements, so the expected number of comparisons in iteration is roughly . The total expected comparisons are:

The average case is still — the same order of growth as the worst case. The constant factor is half as large, but asymptotically the behavior is the same.

Which case matters?

In practice, worst-case analysis is the most commonly used, for several reasons:

  1. Guarantees. The worst case gives an upper bound that holds for every input. This is crucial in real-time systems, web servers, and other contexts where performance must be predictable.
  2. Average case can be misleading. The "average" depends on the input distribution, which we may not know. If the actual inputs differ from our assumption, the average-case analysis may not apply.
  3. Worst case is often typical. For many algorithms, the worst case and average case have the same asymptotic growth rate (as we just saw with insertion sort).

We will occasionally discuss best-case and average-case bounds when they provide useful insight, but unless otherwise stated, all complexity bounds in this book refer to the worst case.

Amortized analysis

Sometimes an operation is expensive occasionally but cheap most of the time. Amortized analysis gives us a way to average the cost over a sequence of operations, providing a tighter bound than the worst-case cost per operation.

The dynamic array example

Consider a dynamic array (like JavaScript's Array or std::vector in C++) that supports an append operation. The array maintains an internal buffer of some capacity. When the buffer is full and a new element is appended, the array allocates a new buffer of double the capacity and copies all existing elements over. This resize operation costs , where is the current number of elements.

At first glance, this seems concerning: a single append can cost . But resizes happen infrequently — only when the size reaches a power of 2. Let us analyze the cost of consecutive appends starting from an empty array.

The resize operations happen at sizes 1, 2, 4, 8, , , where . The total copying cost across all resizes is:

Adding the non-resize operations (cost 1 each), the total cost of appends is less than . Therefore the amortized cost per append is:

Each individual append may cost in the worst case, but averaged over a sequence of operations, the cost is per operation.

Amortized vs. average case

It is important to distinguish amortized analysis from average-case analysis:

  • Average case averages over random inputs: we assume a probability distribution on the inputs and compute the expected running time.
  • Amortized analysis averages over a sequence of operations on a worst-case input: no probability is involved. The guarantee holds deterministically.

Amortized analysis says: "no matter what sequence of operations you perform, the total cost is at most , so the amortized cost per operation is ." This is a worst-case guarantee about the total, not a probabilistic statement.

We will see amortized analysis again in Chapter 7 (dynamic arrays), Chapter 11 (binary heaps), and Chapter 18 (union-find).

Recurrence relations

When an algorithm calls itself recursively, its running time is naturally expressed as a recurrence relation: a formula that expresses in terms of applied to smaller values.

Setting up a recurrence

Example 2.5: Binary search. Binary search (discussed in Chapter 3) repeatedly halves the search space:

  1. Compare the target with the middle element.
  2. If they match, return the index.
  3. Otherwise, recurse on the left or right half.

The running time satisfies the recurrence:

The term accounts for the recursive call on half the array, and the term accounts for the comparison and index computation.

Example 2.6: Merge sort. Merge sort (discussed in Chapter 5) divides the array in half, recursively sorts both halves, and merges the results:

The two recursive calls each process half the array (), and the merge step takes time.

Solving recurrences by expansion

One way to solve a recurrence is to expand it repeatedly until a pattern emerges.

Example 2.7: Solving the binary search recurrence.

Expanding:

The recursion bottoms out when , i.e., . Therefore:

Example 2.8: Solving the merge sort recurrence.

Expanding:

At level : . Setting :

The recursion tree method

A recursion tree is a visual tool for solving recurrences. Each node represents the cost at one level of recursion, and the total cost is the sum over all nodes.

For merge sort with :

Level 0:              cn                    → cost cn
                    /    \
Level 1:        cn/2      cn/2              → cost cn
                / \       / \
Level 2:    cn/4  cn/4  cn/4  cn/4          → cost cn
              ...         ...
Level k:   c  c  c  ...  c  c  c           → cost cn
           (n leaves)

There are levels, each contributing work, so the total is .

The Master Theorem

The Master Theorem provides a general solution for recurrences of a common form.


Definition 2.8 --- The Master Theorem

Let and be constants, let be a function, and let be defined by the recurrence

Then can be bounded asymptotically as follows:

  1. If for some constant , then .

  2. If , then .

  3. If for some constant , and if for some constant and sufficiently large , then .


The three cases correspond to three scenarios:

  • Case 1: The cost is dominated by the leaves of the recursion tree. The recursive calls do most of the work.
  • Case 2: The cost is evenly distributed across all levels of the tree. Each level contributes roughly equally.
  • Case 3: The cost is dominated by the root. The non-recursive work dominates.

Let us apply the Master Theorem to our earlier examples.

Example 2.9: Binary search. .

Here , , . We have . Since , Case 2 applies:

Example 2.10: Merge sort. .

Here , , . We have . Since , Case 2 applies:

Example 2.11: Strassen's matrix multiplication. .

Here , , . We have . Since with , Case 1 applies:

This is better than the naive matrix multiplication.

Limitations of the Master Theorem

The Master Theorem does not cover all recurrences. It requires that the subproblems be of equal size and that fall into one of the three cases. Recurrences like (which arises in randomized quicksort analysis) do not fit the template directly. For such cases, the recursion-tree method or the Akra–Bazzi theorem can be used.

Space complexity

So far we have focused on time complexity, but algorithms also consume memory. Space complexity measures the amount of additional memory an algorithm uses beyond the input.


Definition 2.9 --- Space complexity

The space complexity of an algorithm is the maximum amount of memory it uses at any point during execution, measured as a function of the input size.


We distinguish between:

  • Auxiliary space: the extra memory used beyond the input itself.
  • Total space: auxiliary space plus the space for the input.

Unless stated otherwise, when we refer to "space complexity" in this book, we mean auxiliary space.

Example 2.12: Space complexity of max.

The max function from Chapter 1 uses a single variable result. Its auxiliary space is .

Example 2.13: Space complexity of merge sort.

Merge sort requires a temporary array of size for the merge step, plus space for the recursion stack. Its auxiliary space is .

Example 2.14: Space complexity of insertion sort.

Our insertion sort implementation copies the input array (space ). An in-place variant that sorts the array directly would use only auxiliary space.

Time–space trade-offs

Often there is a trade-off between time and space. An algorithm can sometimes be made faster by using more memory, or made more memory-efficient at the cost of additional computation. A classic example:

  • Hash table lookup: average time, space.
  • Linear search through an unsorted array: time, space.

Both solve the problem of finding an element in a collection, but they make different trade-offs. Recognizing and navigating these trade-offs is a recurring theme in algorithm design.

Practical considerations

Asymptotic analysis is a powerful framework, but it has limitations that a practicing programmer should keep in mind.

Constant factors matter for moderate

Asymptotic notation hides constant factors. An algorithm with running time is , and an algorithm with running time is . For , the "slower" algorithm is actually faster. In practice, constant factors depend on:

  • The number of operations per step.
  • Cache behavior — algorithms with good spatial locality are faster in practice.
  • Branch prediction — algorithms with predictable control flow benefit from CPU branch predictors.

This is why, for example, insertion sort (which is ) is often used for small arrays (say, ) even inside asymptotically faster algorithms like merge sort. The constant factor is smaller, and for tiny inputs the quadratic term has not yet become dominant.

Lower-order terms

An algorithm that performs operations is , but for , the linear term dominates. Asymptotic analysis describes long-term growth; for small inputs, the actual constants and lower-order terms may be more important.

Choosing the right model

Our analysis assumes a simple model where every basic operation takes the same amount of time. Real computers have caches, pipelines, and memory hierarchies that make some access patterns much faster than others. An algorithm that accesses memory sequentially (like insertion sort) can be significantly faster in practice than one that accesses memory randomly (like binary search on a large array), even if the latter has a better asymptotic bound.

Despite these caveats, asymptotic analysis remains the single most useful tool for comparing algorithms. It correctly predicts which algorithm will win for large enough inputs, and "large enough" usually means "the sizes that actually matter in practice."

Looking ahead

In this chapter we have developed the fundamental tools for analyzing algorithms:

  • Asymptotic notation (, , ) captures growth rates while abstracting away constant factors and hardware details.
  • Worst-case analysis gives reliable upper bounds on running time. Best-case and average-case analyses provide additional insight.
  • Amortized analysis reveals that operations with occasional expensive steps can still be efficient on average.
  • Recurrence relations express the running time of recursive algorithms, and the Master Theorem provides a quick way to solve common recurrences.
  • Space complexity measures memory usage and highlights time–space trade-offs.

Armed with these tools, we are ready to analyze every algorithm in this book rigorously. In the next chapter, we explore recursion and the divide-and-conquer strategy — one of the most powerful algorithm design techniques — and apply our analytical framework to algorithms like binary search and the closest pair of points.

Exercises

Exercise 2.1. Rank the following functions by asymptotic growth rate, from slowest to fastest. For each consecutive pair, state whether , , or .

Exercise 2.2. Prove or disprove: if and , then . (In other words, is Big-O transitive?)

Exercise 2.3. For each of the following recurrences, use the Master Theorem to determine the asymptotic bound, or explain why the Master Theorem does not apply.

(a)

(b)

(c)

(d)

Exercise 2.4. Consider a dynamic array that triples (instead of doubles) its capacity when full. Prove that the amortized cost of an append operation is still . How does the constant factor compare to the doubling strategy?

Exercise 2.5. An algorithm processes an array of elements as follows: for each element, it performs a binary search over the preceding elements. What is the overall time complexity? Express your answer in Big-Theta notation.