Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Section 2.9 compared the efficiency of functions by measuring how long they took for many different inputs and graphing the results. The resulting graphs had a few characteristic shapes: a straight line for exp, a curve that flattens out for exp_fast, and a curve that bends upward for overlap. This appendix introduces the terminology that computer scientists use to describe these shapes precisely using upper and lower bounds.

Exactly determining just how much space or time will be used when calling a function is a very difficult task that depends upon many factors. A useful way to analyze a process is to categorize it along with a group of processes that all have similar requirements. A useful categorization is the order of growth of a process, which expresses in simple terms how the resource requirements of a process grow as a function of the input.

As an introduction to orders of growth, we will analyze the function count_factors below, which counts the number of positive integers that evenly divide an input n. The function attempts to divide n by every integer less than or equal to its square root. The implementation takes advantage of the fact that if kk divides nn and k<nk < \sqrt{n}, then there is another factor j=n/kj = n / k such that j>nj > \sqrt{n}.

Loading...

How much time is required to evaluate count_factors? The exact answer will vary on different machines, but we can make some useful general observations about the amount of computation involved. The total number of times this process executes the body of the while statement is the greatest integer less than n\sqrt{n}. The statements before and after this while statement are executed exactly once. So, the total number of statements executed is w×n+vw \times \sqrt{n} + v, where ww is the number of statements in the while body and vv is the number of statements outside of the while statement. Although it isn’t exact, this formula generally characterizes how much time will be required to evaluate count_factors as a function of the input n.

A more exact description is difficult to obtain. The constants ww and vv are not constant at all, because the assignment statements to factors are sometimes executed but sometimes not. An order of growth analysis allows us to gloss over such details and instead focus on the general shape of growth. In particular, the order of growth for count_factors expresses in precise terms that the amount of time required to compute count_factors(n) scales at the rate n\sqrt{n}, within a margin of some constant factors.

Theta Notation. Let nn be a parameter that measures the size of the input to some process, and let R(n)R(n) be the amount of some resource that the process requires for an input of size nn. In our previous examples we took nn to be the number for which a given function is to be computed, but there are other possibilities. For instance, if our goal is to compute an approximation to the square root of a number, we might take nn to be the number of digits of accuracy required.

R(n)R(n) might measure the amount of memory used, the number of elementary machine steps performed, and so on. In computers that do only a fixed number of steps at a time, the time required to evaluate an expression will be proportional to the number of elementary steps performed in the process of evaluation.

We say that R(n)R(n) has order of growth Θ(f(n))\Theta(f(n)), written R(n)=Θ(f(n))R(n) = \Theta(f(n)) (pronounced “theta of f(n)f(n)”), if there are positive constants k1k_1 and k2k_2 independent of nn such that

k1×f(n)≤R(n)≤k2×f(n)k_1 \times f(n) \leq R(n) \leq k_2 \times f(n)

for any value of nn larger than some minimum mm. In other words, for large nn, the value R(n)R(n) is always sandwiched between two values that both scale with f(n)f(n):

We can apply this definition to show that the number of steps required to evaluate count_factors(n) grows as Θ(n)\Theta(\sqrt{n}) by inspecting the function body.

First, we choose k1=1k_1=1 and m=0m=0, so that the lower bound states that count_factors(n) requires at least 1×n1 \times \sqrt{n} steps for any n>0n>0. There are at least 4 lines executed outside of the while statement, each of which takes at least 1 step to execute. There are at least two lines executed within the while body, along with the while header itself. All of these require at least one step. The while body is evaluated at least n−1\sqrt{n}-1 times. Composing these lower bounds, we see that the process requires at least 4+3×(n−1)4 + 3 \times (\sqrt{n}-1) steps, which is always larger than k1×nk_1 \times \sqrt{n}.

Second, we can verify the upper bound. We assume that any single line in the body of count_factors requires at most pp steps. This assumption isn’t true for every line of Python, but does hold in this case. Then, evaluating count_factors(n) can require at most p×(6+4×n)p \times (6 + 4 \times \sqrt{n}) steps, because there are 5 lines outside of the while statement and 4 within (including the header), and the header is evaluated one more time than the body. This upper bound holds even if every if header evaluates to true. Finally, if we choose k2=10×pk_2=10 \times p, then the steps required are always smaller than k2×nk_2 \times \sqrt{n} for any n≥1n \geq 1.

Therefore, the number of steps is always bounded below by 1×n1 \times \sqrt{n} and above by 10×p×n10 \times p \times \sqrt{n}, and so the number of steps of count_factors(n) grows as Θ(n)\Theta(\sqrt{n}).

Example: Exponentiation

Section 2.9 introduced two functions for computing bnb^n. The first, exp, follows the recursive definition bn=b×bn−1b^n = b \times b^{n-1} with b0=1b^0 = 1.

>>> def exp(b, n):
        if n == 0:
            return 1
        else:
            return b * exp(b, n-1)

This is a linear recursive process that requires Θ(n)\Theta(n) steps and Θ(n)\Theta(n) space. Just as with factorial, we can readily formulate an equivalent linear iteration that requires a similar number of steps but constant space.

>>> def exp_iter(b, n):
        result = 1
        for _ in range(n):
            result = result * b
        return result

The second, exp_fast, uses successive squaring: bn=(b12n)2b^n = (b^{\frac{1}{2} n})^2 when nn is even.

>>> def square(x):
        return x * x

>>> def exp_fast(b, n):
        if n == 0:
            return 1
        elif n % 2 == 0:
            return square(exp_fast(b, n//2))
        else:
            return b * exp_fast(b, n-1)

>>> exp_fast(2, 100)
1267650600228229401496703205376

The process evolved by exp_fast grows logarithmically with n in both space and number of steps. Computing b2nb^{2n} using exp_fast requires only one more multiplication than computing bnb^n, so the number of multiplications required for an exponent of n grows about as fast as the logarithm of n base 2. The process has Θ(log⁡n)\Theta(\log n) growth. The difference between Θ(log⁡n)\Theta(\log n) growth and Θ(n)\Theta(n) growth becomes striking as nn becomes large. For example, exp_fast for n of 1000 requires only 15 multiplications instead of 1000.

Growth Categories

Orders of growth are designed to simplify the analysis and comparison of computational processes. Many different processes can all have equivalent orders of growth, which indicates that they scale in similar ways. It is an essential skill of a computer scientist to know and recognize common orders of growth and identify processes of the same order.

Constants. Constant terms do not affect the order of growth of a process. So, for instance, Θ(n)\Theta(n) and Θ(500×n)\Theta(500 \times n) are the same order of growth. This property follows from the definition of theta notation, which allows us to choose arbitrary constants k1k_1 and k2k_2 (such as 1500\frac{1}{500}) for the upper and lower bounds. For simplicity, constants are always omitted from orders of growth.

Logarithms. The base of a logarithm does not affect the order of growth of a process. For instance, log⁡2n\log_2 n and log⁡10n\log_{10} n are the same order of growth. Changing the base of a logarithm is equivalent to multiplying by a constant factor.

Nesting. When an inner computational process is repeated for each step in an outer process, then the order of growth of the entire process is a product of the number of steps in the outer and inner processes.

For example, this version of the overlap function from Section 2.9 uses the in operator to compute the number of items in list a that also appear in list b.

>>> def overlap(a, b):
        count = 0
        for item in a:
            if item in b:
                count += 1
        return count

>>> overlap([1, 3, 2, 2, 5, 1], [5, 4, 2])
3

The in operator for lists requires Θ(n)\Theta(n) time in the worst case, where nn is the length of the list b. It is applied Θ(m)\Theta(m) times, where mm is the length of the list a. The item in b expression is the inner process, and the for item in a loop is the outer process. The total order of growth for this function is therefore Θ(m×n)\Theta(m \times n) in the worst case.

Lower-order terms. As the input to a process grows, the fastest growing part of a computation dominates the total resources used. Theta notation captures this intuition. In a sum, all but the fastest growing term can be dropped without changing the order of growth.

For instance, consider the one_more function that returns how many items of a list a are one more than some other item of a. That is, in the list [3, 14, 15, 9], the item 15 is one more than 14, so one_more will return 1.

>>> def one_more(a):
        return overlap([x-1 for x in a], a)

>>> one_more([3, 14, 15, 9])
1

There are two parts to this computation: the list comprehension and the call to overlap. For a list a of length nn, the list comprehension requires Θ(n)\Theta(n) steps, while the call to overlap requires Θ(n2)\Theta(n^2) steps. The sum of steps is Θ(n+n2)\Theta(n + n^2), but this is not the simplest way of expressing the order of growth.

Θ(n2+k×n)\Theta(n^2 + k \times n) and Θ(n2)\Theta(n^2) are equivalent for any constant kk because the n2n^2 term will eventually dominate the total for any kk. The fact that bounds must hold only for nn greater than some minimum mm establishes this equivalence. For simplicity, lower-order terms are always omitted from orders of growth, and so we will never see a sum within a theta expression.

Common categories. Given these equivalence properties, a small set of common categories emerges to describe most computational processes. The most common are listed below from slowest to fastest growth, along with descriptions of the growth as the input increases.

CategoryTheta NotationGrowth DescriptionExample
ConstantΘ(1)\Theta(1)Growth is independent of the inputabs
LogarithmicΘ(log⁡n)\Theta(\log{n})Multiplying input increments resourcesexp_fast
LinearΘ(n)\Theta(n)Incrementing input increments resourcesexp
QuadraticΘ(n2)\Theta(n^2)Incrementing input adds n resourcesone_more
ExponentialΘ(bn)\Theta(b^n)Incrementing input multiplies resourcesfib

Other categories exist, such as the Θ(n)\Theta(\sqrt{n}) growth of count_factors. However, these categories are particularly common.

Exponential growth describes many different orders of growth, because changing the base bb does affect the order of growth. For instance, the number of steps in our tree-recursive Fibonacci computation fib grows exponentially in its input nn. In particular, one can show that the nth Fibonacci number is the closest integer to

ϕn5\frac{\phi^{n}}{\sqrt{5}}

where ϕ\phi is the golden ratio:

ϕ=1+52≈1.6180\phi = \frac{1 + \sqrt{5}}{2} \approx 1.6180

It turns out that the number of calls made by fib(n) is 2×fib(n+1)−12 \times \text{fib}(n+1)-1, and so the tree-recursive process requires Θ(ϕn)\Theta(\phi^n) steps, a function that grows exponentially with nn.