BACK Mascot image.
← MA0 2 · Introduction to Algorithms and Numerical Analysis

Lesson 4

Data Structures and Graphs

Taught

Data Structures

Every program of the last lesson held a bounded number of values at once: a guess, a counter, a running total, the string being built up. A program that has to keep many values, for example to sort a list of numbers or to remember which candidates have been ruled out, needs somewhere to store them and a way to reach any one of them. A data structure is such a place together with the operations for using it.

We compare algorithms by explicit models of cost, and refine an implementation only once the broad algorithmic choice has been made. For every algorithm there are two things to prove: that it terminates, and that it is correct, meaning that the output it returns is a correct output for its input in the sense of a computational problem.

Functions

From here on an algorithm is usually written as a function: a named piece of code that takes inputs, called its arguments, and returns an output.

def square(x):
    return x * x

print(square(7))       # 49
print(square(-3) + 1)  # 10

The line def square(x): names the function and its parameter x; the indented body is its code. A call square(7) binds the parameter x to the argument 7, runs the body, and evaluates to the value named by return, which ends the call at once. A function whose body finishes without reaching a return returns None. Names bound inside the body are local: they exist only while that call runs and do not disturb names of the same spelling elsewhere.

def smallest(a, b, c):
    target = a
    if b < target:
        target = b
    if c < target:
        target = c
    return target

target = 100
print(smallest(4, -2, 9))   # -2
print(target)              # 100, untouched by the call

A function may return several values at once as a tuple, and the caller may unpack them by multiple assignment: return p, q and then p, q = f(p, q).

In pseudocode we give a function a name and write its arguments in brackets, as in Fact(n), and return has its Python meaning.

Recursion

A function may call itself. The call f(n) then waits while f(n - 1) runs, and that call has its own local names, separate from those of the call that made it. The part of memory that keeps track of this is organised as a stack: each call places a frame holding its local names on top, and returning removes the top frame, so the most recent call is always the first to finish. The number of frames present at once is the depth of the recursion.

Recursion needs a case that does not call the function again, reached after finitely many calls; otherwise the stack grows until Python stops the program with a RecursionError.

def count_down(n):
    if n == 0:
        print('done')
    else:
        print(n)
        count_down(n - 1)

count_down(3)   # 3, 2, 1, done

Problem 4.1.

Write a function digit_sum(n) that returns the sum of the decimal digits of a positive integer nn, once with a while loop and once recursively, using // and % only. State the depth of the recursion in terms of nn.

Induction and Loop Invariants

For a statement P(n)P(n) about every nonnegative integer, proof by induction has two parts: prove P(0)P(0), then assume P(n)P(n) for an arbitrary nn and use that assumption to prove P(n+1)P(n+1). If the statement begins at n=1n = 1, start with P(1)P(1). The base case gives P(0)P(0), and the step then gives P(1)P(1), P(2)P(2) and so on. The last lesson used this pattern for the closed forms of recurrences; we now use it for recursive algorithms and for loops.

Definition 4.1 (Loop invariant).

A loop invariant is a statement about the program state that

  1. holds before the first iteration (initialisation),
  2. is preserved by every iteration (maintenance), and
  3. gives the desired conclusion when the loop stops (use at termination).

An invariant plays the part of the induction hypothesis: initialisation is the base case, and maintenance is the step. It says nothing about whether the loop stops. That is proved separately by a decrementing function, often called a variant in this context.

Theorem 4.2 (Chocolate-bar strategy).

A rectangular chocolate bar has a marked corner square. A move removes a nonempty strip by one horizontal or vertical cut, retaining the piece containing the marked square. The player who receives the 1×11 \times 1 bar loses. The first player has a winning strategy exactly when the starting rectangle is not a square.

Discussion.

Write (p,q)(p, q) for the current numbers of rows and columns. The strategy is to hand the opponent a square every time: from a nonsquare, cut the longer side down to the shorter. The invariant is the pair of facts that the strategy user always receives a nonsquare and always hands over a square; initialisation is the starting position, and maintenance holds because every legal move from a square produces a nonsquare. The product pqpq falls with every move, so it serves as the variant and play reaches (1,1)(1, 1). Since (1,1)(1, 1) is a square, the strategy user never receives it. The two cases of the statement say which player can use the strategy.

Proof.

Write (p,q)(p, q) for the current positive numbers of rows and columns. If p≠qp \neq q, the player to move can cut the larger coordinate down to the smaller one and hand the other player a square. Every legal move from a square changes exactly one coordinate, so it makes the coordinates unequal; the next player can therefore make a square again. The invariant is that the player using this strategy always receives a nonsquare rectangle and always hands over a square.

Every move strictly reduces the positive integer pqpq, so play eventually reaches (1,1)(1, 1). The strategy user cannot receive (1,1)(1, 1), since that state is square. If the initial rectangle is nonsquare, Player One uses the strategy; if it is square, Player Two uses it after Player One’s first cut.

Example 4.3 (A game from (5,3)(5, 3)).

Starting from (5,3)(5, 3), Player One cuts to (3,3)(3, 3). If the opponent cuts to (3,1)(3, 1), Player One cuts to (1,1)(1, 1) and the opponent loses.

One full round can be implemented as a loop: check whether Player One has received (1,1)(1, 1), let Player One move, check whether the opponent has received (1,1)(1, 1), then let the opponent move. Player One’s move is the square-producing cut:

Player One's Move
Input:  the numbers p, q of rows and columns of the retained rectangle.
Output: the rectangle after the move.

    if p > q then p ≝ q
    else if q > p then q ≝ p
    else p ≝ p − 1
    return p, q
def player(p, q):
    if p > q:
        p = q
    elif q > p:
        q = p
    else:
        p = p - 1   # reached only from a square
    return p, q

For a nonsquare starting position, the last branch is never reached under the winning strategy. The opponent may choose any legal cut; the proof above covers all such choices.

Recursive and Iterative Factorials

Recursive and iterative factorial functions show the connection between induction and invariants. Define 0!=10! = 1 and n!=n⋅(n−1)!n! = n \cdot (n-1)! for n>0n > 0.

Recursive Factorial                    Iterative Factorial
Input:  n ∈ N₀.                        Input:  n ∈ N₀.
Output: n!.                            Output: n!.

    Fact(n):                               r ≝ 1
        if n = 0 then return 1             for j ≝ 1 to n do r ≝ r · j
        else return n · Fact(n − 1)        return r

The recursive version returns 11 at n=0n = 0 and otherwise returns n⋅Fact(n−1)n \cdot \mathrm{Fact}(n-1); induction on nn proves it correct, the base case being the first branch and the step the second. The iterative version begins with r=1r = 1 and multiplies rr by jj for j=1,…,nj = 1, \ldots, n. Before iteration jj the invariant is r=(j−1)!r = (j-1)!; after the multiplication it becomes r=j!r = j!, so at exit r=n!r = n!. The variant n−j+1n - j + 1 decreases to zero.

def fact_recursive(n):
    if n == 0:
        return 1
    return n * fact_recursive(n - 1)

def fact_iterative(n):
    r = 1
    for j in range(1, n + 1):
        r = r * j
    return r

print(fact_recursive(10), fact_iterative(10))   # 3628800 3628800

The recursive version has depth n+1n + 1: the call for nn waits on the call for n−1n - 1, down to 00. The iterative version uses a single frame. Python integers have no fixed size, so both return n!n! exactly however large it is. A product of large integers is then not an elementary operation, and costs what the schoolbook bound says.

Problem 4.2.

The loop below is meant to compute ana^n for an integer aa and n∈N0n \in \mathbb{N}_0 with about log⁡2n\log_2 n multiplications.

def power(a, n):
    result, base, e = 1, a, n
    while e > 0:
        if e % 2 == 1:
            result = result * base
        base = base * base
        e = e // 2
    return result

Show that result⋅base e=an\text{result} \cdot \text{base}^{\,e} = a^n is a loop invariant, give a variant, and deduce that power is correct. How many multiplications does it perform, in terms of the binary expansion of nn?

Problem 4.3.

In the chocolate-bar game a move may instead remove a strip of width one or two only. Decide, for each starting rectangle (p,q)(p, q) with 1⩽p,q⩽41 \leqslant p, q \leqslant 4, which player has a winning strategy, and state and prove a rule covering every (p,q)(p, q).

Measuring Cost

A program can spend most of its running time in a small part of its code. Choosing a better algorithm for that part usually does more than rewriting individual instructions, and much of the low-level rewriting is done automatically in any case by the software that translates a program into machine instructions. Costs other than time and memory can matter too, energy and money among them. For example, a “Penny Sort” measure asks how many externally stored records can be sorted for a US cent, after the purchase cost of a fixed machine is spread over an assumed working life. That measure combines hardware cost and algorithm throughput.

Example 4.4 (Which method is faster depends on nn).

Suppose method A uses exactly 100n100n operations on an input of size nn, while method B uses exactly n2n^2. At n=10n = 10, A uses 10001000 operations and B uses 100100; at n=1000n = 1000, A uses 100,000100{,}000 and B uses 1,000,0001{,}000{,}000. The method with fewer operations depends on the input size, and the two cross at n=100n = 100.

The running time of the last lesson counted elementary operations. We also measure memory.

Definition 4.5 (Time cost and auxiliary space).

The time cost of an algorithm counts operations in a stated model. Its auxiliary space, or memory footprint, is the maximum amount of extra storage present at any instant of a run, excluding the input.

Storage may be released and reused later, which is why the footprint is a maximum and not a cumulative total. In a conventional model allocating or touching a memory cell costs at least one operation, so an algorithm’s footprint cannot exceed a constant multiple of its running time. This claim depends on the model, and it does not say that every algorithm uses as much space as time.

The unit-cost assumption has to be stated with care. A comparison of two strings is not necessarily a single operation: it may inspect many characters. A comparison of two integers of bounded size is commonly counted as one operation, but for arbitrarily large integers or variable-length records the cost must be stated.

Lower and Two-Sided Bounds

Landau’s OO from the last lesson is an upper bound. It has a lower counterpart, and the two together give a two-sided bound.

Definition 4.6 (Ω\Omega and Θ\Theta).

Let f,g:N→R⩾0f, g : \mathbb{N} \to \mathbb{R}_{\geqslant 0}. We write

f=Ω(g)if there are c>0 and n0 with c g(n)⩽f(n) for all n⩾n0,f=Θ(g)if f=O(g) and f=Ω(g).\begin{aligned} f &= \Omega(g) &&\text{if there are } c > 0 \text{ and } n_0 \text{ with } c\,g(n) \leqslant f(n) \text{ for all } n \geqslant n_0, \\ f &= \Theta(g) &&\text{if } f = O(g) \text{ and } f = \Omega(g). \end{aligned}

As with OO, the equals sign is shorthand for membership of a set of functions, not equality of functions. The last lesson wrote f=o(g)f = o(g), read ”ff grows strictly more slowly than gg”, when f(n)/g(n)f(n)/g(n) tends to zero. Written out without limits: g(n)>0g(n) > 0 from some point on, and for every ε>0\varepsilon > 0 there is an n0n_0 with f(n)⩽ε g(n)f(n) \leqslant \varepsilon\, g(n) for every n⩾n0n \geqslant n_0. This is stronger than f=O(g)f = O(g), since the constant multiplier can be made as small as we wish by taking nn large enough.

The growth rates met most often are, from slowest to fastest,

log⁡n,n,nlog⁡n,n3/2,n2,n3,2n.\log n, \quad n, \quad n \log n, \quad n^{3/2}, \quad n^2, \quad n^3, \quad 2^n .

Logarithm bases differ only by a constant factor, since log⁡an=log⁡bn/log⁡ba\log_a n = \log_b n / \log_b a by the change of base; so Θ(log⁡2n)\Theta(\log_2 n) and Θ(log⁡10n)\Theta(\log_{10} n) are the same class and the base is usually left off.

Problem 4.4.

Show that 12n2−3n=Θ(n2)\tfrac12 n^2 - 3n = \Theta(n^2) by exhibiting the constants, and that nlog⁡2n=o(n2)n \log_2 n = o(n^2).

Lower Bounds

The comparison model of computation acts on a set of comparable objects. The objects are treated as black boxes supporting only binary tests called comparisons, namely <<, ⩽\leqslant, >>, ⩾\geqslant, == and ≠\neq: each takes two objects and returns True or False according to their relative order. Nothing else about the objects may be inspected, so an algorithm in this model learns about its input only through the answers to comparisons, and its cost is counted as the number of comparisons it makes.

Definition 4.7 (Problem lower bound and optimality).

A problem lower bound is a bound that applies to every algorithm solving the problem in a specified model. An algorithm is asymptotically optimal for a cost measure when its upper bound matches the problem’s lower bound up to constants.

An upper bound is about one algorithm, and a lower bound is about every algorithm for the problem. A lower bound therefore has to fix a model, which lists the operations an algorithm may use.

Proposition 4.8 (Finding a maximum).

Finding the position of the maximum among nn pairwise distinct, otherwise unordered comparable elements requires at least n−1n - 1 comparisons in the comparison model, and n−1n - 1 comparisons suffice.

Discussion.

For the lower bound we count losses: an element can be ruled out as the maximum only once it has been seen to be smaller than something, and one comparison produces exactly one loser. All n−1n - 1 non-maximal elements must be ruled out, so at least n−1n - 1 comparisons are needed. For the upper bound a single left-to-right scan with a running maximum makes one comparison per element after the first, and its invariant says that the stored index is maximal in the prefix scanned so far.

Proof.

Before an element can be ruled out as the maximum, it must lose a comparison against a larger element. One comparison can give a first loss to at most one candidate. All but the true maximum, namely n−1n - 1 candidates, must be ruled out, so at least n−1n - 1 comparisons are necessary.

A scan keeps the index of the largest element seen and compares each of the remaining n−1n - 1 elements with it once; its invariant is that the stored index is maximal in the scanned prefix. Thus it meets the lower bound.

A second proof uses an adversary, an imagined opponent who may change the input as long as every answer already given stays true.

Proof.

Fix an input of distinct values T[0],…,T[n−1]T[0], \ldots, T[n-1] and a nonmaximum element T[j]T[j]. Suppose T[j]T[j] is never compared with an element larger than itself. Change only its value, to one larger than every original value. Every comparison involving T[j]T[j] previously had a smaller other operand, so its outcome stays the same; all other comparisons are unchanged. A deterministic comparison algorithm therefore follows the same sequence of branches and returns the same index. But jj is now the true maximum, a contradiction. Thus each nonmaximum element must lose a comparison, which gives n−1n - 1 losses.

The scan in pseudocode and in Python:

Position of the Maximum
Input:  a sequence T[0], …, T[n − 1] of comparable elements, n ⩾ 1.
Output: an index k with T[k] maximal.

    k ≝ 0
    for i ≝ 1 to n − 1 do
        if T[i] > T[k] then k ≝ i
    return k
def arg_max(T):
    k = 0
    for i in range(1, len(T)):
        if T[i] > T[k]:
            k = i
    return k

print(arg_max((3, 9, 2, 9, 4)))   # 1

Python’s len(T) gives the length of a tuple, as it does for a string, and T[i] its entry at position i.

Problem 4.5.

Give an algorithm that finds both the maximum and the minimum of nn distinct elements with at most ⌈3n/2⌉−2\lceil 3n/2 \rceil - 2 comparisons. Then use an adversary to show that at least ⌈3n/2⌉−2\lceil 3n/2 \rceil - 2 comparisons are necessary.

The Word-RAM Model

To calculate the resources an algorithm uses we need to say how long a computer takes to perform basic operations. Fixing such a set of operations gives a model of computation, on which the analysis is then based. We use the ww-bit Word-RAM model, which treats a computer as a random-access array of machine words called memory, together with a processor that performs operations on that memory.

Definition 4.9 (Word-RAM).

A machine word is a sequence of ww bits, read as an integer in {0,…,2w−1}\{0, \ldots, 2^w - 1\}. A Word-RAM processor performs each of the following in constant time:

  1. addition, subtraction, multiplication, integer division, remainder, bitwise operations and comparisons of two machine words;
  2. given a word aa, reading or writing the word stored in memory at address aa.

A machine word of ww bits can name at most 2w2^w addresses, so the processor can read and write at most 2w2^w locations of memory. When a problem’s input occupies nn machine words we therefore always assume a word size of w>log⁡2nw > \log_2 n bits, or the machine could not reach all of its input. For comparison, a Word-RAM model of a byte-addressable 6464-bit machine allows inputs of up to about 101010^{10} gigabytes.

Arrays

An array is a fixed number of storage slots in a row, numbered from 00, any of which may be read or written in a single elementary operation. The iith slot of an array pp is written p[i]p[i]. In the Word-RAM an array of nn words is a block of nn consecutive addresses, and reaching p[i]p[i] means reading the address of p[0]p[0] plus ii: one addition and one read, whatever ii is and whatever the length of pp.

Python Lists

Python’s counterpart of an array is a list, written in square brackets. A list is a sequence like a tuple, but its entries may be changed.

p = [3, 1, 4, 1, 5]
print(len(p), p[0], p[4])   # 5 3 5
p[1] = 9
print(p)                    # [3, 9, 4, 1, 5]

q = [True] * 4              # [True, True, True, True]
r = [None] * 3              # [None, None, None]

Positions are numbered from 00, as they are for strings, and p[i] = x writes to position i. The expression [x] * n builds a list of n copies of x. Several further operations will be used below.

  1. p.append(x) adds x at the end, and p.pop() removes the last entry and returns it; p.pop(i) removes and returns the entry at position i.
  2. The slice p[i:j] is a new list holding the entries at positions i up to but not including j, with the same convention as for strings; p[i:] runs to the end and p[:j] starts at the beginning. Building a slice copies its j−ij - i entries.
  3. p + q is a new list holding the entries of p followed by those of q.
  4. The comprehension [f(a) for a in X] builds the list of values f(a) as a runs through X.
  5. x is None tests whether x is the object None.

Unlike a tuple, a list can grow and shrink; how Python does this is described under dynamic arrays below. Used with a fixed length it behaves as an array.

Listing the Primes

Deciding whether one number is prime was a decision problem; listing the primes up to a bound is a general discrete computational problem. It is the first problem here whose best algorithm uses an array, and it has a much faster algorithm than testing each number for primality.

List of Prime Numbers
Input: n ∈ N.
Task:  compute all prime numbers p with p ⩽ n.

Running the trial-division test on each of 2,…,n2, \ldots, n in turn settles it in O(nn)O(n\sqrt{n}) operations. The sieve of Eratosthenes does better by never testing divisibility at all: it writes down every candidate, then crosses out the multiples of each survivor in turn. It uses an array pp indexed by 0,…,n0, \ldots, n.

Example 4.10 (The sieve of Eratosthenes).

The algorithm marks every index as a candidate and then strikes out the multiples of each index that is still marked.

The Sieve of Eratosthenes
Input:  n ∈ N.
Output: all prime numbers less than or equal to n.

    for i ≝ 2 to n do p[i] ≝ "yes"
    for i ≝ 2 to n do
        if p[i] = "yes" then
            output i
            for j ≝ i to ⌊n / i⌋ do p[i · j] ≝ "no"

The inner loop starts at j=ij = i rather than at j=2j = 2: the multiples i⋅2,…,i⋅(i−1)i \cdot 2, \ldots, i \cdot (i-1) have a factor smaller than ii and were struck out already.

The running time depends on the total work of the inner loops, which is a sum of harmonic numbers, which have no closed form. What we need instead is a bound on them. The lower bound is not needed for the sieve; it is used for quicksort in the next lesson.

Proposition 4.11 (Bounds on the harmonic numbers).

For every n∈Nn \in \mathbb{N},

12⌊log⁡2n⌋  ⩽  Hn  ⩽  1+log⁡2n.\tfrac12 \lfloor \log_2 n \rfloor \;\leqslant\; H_n \;\leqslant\; 1 + \log_2 n .

Discussion.

We group the terms into blocks between consecutive powers of two. The block running from k=2tk = 2^t to k=2t+1−1k = 2^{t+1}-1 has 2t2^t terms, each at most 1/2t1/2^t and more than 1/2t+11/2^{t+1}, so the block contributes between 12\tfrac12 and 11 whatever tt is. The number of blocks needed to cover 1,…,n1, \ldots, n is about log⁡2n\log_2 n, and both bounds follow. At the top the last block may be incomplete: for the upper bound we enlarge the sum to the end of that block, and for the lower bound we drop it.

Proof.

Let m=⌊log⁡2n⌋m = \lfloor \log_2 n \rfloor, so that 2m⩽n<2m+12^m \leqslant n < 2^{m+1}. For t⩾0t \geqslant 0 the block of indices 2t⩽k⩽2t+1−12^t \leqslant k \leqslant 2^{t+1} - 1 has 2t+1−2t=2t2^{t+1} - 2^t = 2^t terms, and each has 2t⩽k<2t+12^t \leqslant k < 2^{t+1}, so

12=2t⋅12t+1  ⩽  ∑k=2t2t+1−11k  ⩽  2t⋅12t=1.\frac12 = 2^t \cdot \frac{1}{2^{t+1}} \;\leqslant\; \sum_{k=2^{t}}^{2^{t+1}-1} \frac{1}{k} \;\leqslant\; 2^t \cdot \frac{1}{2^t} = 1 .

Every term is positive, so enlarging the range of summation increases the sum and shrinking it decreases the sum. The blocks t=0,…,mt = 0, \ldots, m cover 1,…,2m+1−1⩾n1, \ldots, 2^{m+1} - 1 \geqslant n, and the blocks t=0,…,m−1t = 0, \ldots, m - 1 cover 1,…,2m−1⩽n1, \ldots, 2^m - 1 \leqslant n. Hence

Hn  ⩽  H2m+1−1=∑t=0m  ∑k=2t2t+1−11k  ⩽  m+1  ⩽  log⁡2n+1,Hn  ⩾  H2m−1=∑t=0m−1  ∑k=2t2t+1−11k  ⩾  m2.H_n \;\leqslant\; H_{2^{m+1}-1} = \sum_{t=0}^{m} \; \sum_{k=2^{t}}^{2^{t+1}-1} \frac{1}{k} \;\leqslant\; m + 1 \;\leqslant\; \log_2 n + 1, \qquad H_n \;\geqslant\; H_{2^{m}-1} = \sum_{t=0}^{m-1} \; \sum_{k=2^{t}}^{2^{t+1}-1} \frac{1}{k} \;\geqslant\; \frac{m}{2} .

Theorem 4.12 (The sieve is correct and runs in O(nlog⁡n)O(n \log n)).

The sieve of Eratosthenes outputs exactly the primes less than or equal to nn, and performs O(nlog⁡n)O(n \log n) elementary operations.

Discussion.

Correctness and running time are proved separately.

Correctness has two directions. No prime is ever struck out, because an entry is written to only as p[i⋅j]p[i \cdot j] with both ii and jj at least 22, and such an index is composite by definition. In the other direction every composite kk must be struck before the outer loop reaches it, and the index that strikes it is its least divisor above 11: that divisor is itself prime, so it still carries “yes” when the outer loop arrives at it, and its partner kk divided by it is large enough to fall in the range the inner loop covers. That the partner is at least the divisor is Proposition 3.14, which is why the inner loop may start at j=ij = i.

For the running time, the outer loop costs O(n)O(n) by itself, and the inner loop belonging to ii runs at most n/in/i times. Summing n/in/i over ii gives nn times a harmonic sum, and the upper bound just proved turns that into nlog⁡2nn\log_2 n.

Proof.

Correctness. An entry of pp is set to “no” only in the inner loop, where the index written to is i⋅ji \cdot j with i⩾2i \geqslant 2 and j⩾i⩾2j \geqslant i \geqslant 2. Such an index is a product of two integers greater than 11 and so is composite; hence no prime is ever struck out, and every prime ⩽n\leqslant n still carries “yes” when the outer loop reaches it and is output.

Conversely let k⩽nk \leqslant n be composite, and let ii be its least divisor with i⩾2i \geqslant 2. Then ii is prime: a divisor dd of ii with 1<d<i1 < d < i would divide kk as well and contradict minimality. By Proposition 3.14, i⩽ki \leqslant \sqrt{k}, so writing j=k/ij = k / i we have

j=ki  ⩾  kk=k  ⩾  i,j=ki⩽ni,j = \frac{k}{i} \;\geqslant\; \frac{k}{\sqrt{k}} = \sqrt{k} \;\geqslant\; i, \qquad j = \frac{k}{i} \leqslant \frac{n}{i},

and jj is an integer, so i⩽j⩽⌊n/i⌋i \leqslant j \leqslant \lfloor n/i \rfloor. Since ii is prime it is not struck out, so when the outer loop reaches ii the test succeeds and the inner loop runs, setting p[i⋅j]=p[k]p[i \cdot j] = p[k] to “no”. Finally i⩽k<ki \leqslant \sqrt{k} < k, so this happens before the outer loop reaches kk, and kk is not output. The algorithm therefore outputs the primes and nothing else.

Running time. The first loop performs n−1n - 1 assignments. In the second loop, each of the n−1n-1 values of ii costs a bounded amount for the test and the output, contributing O(n)O(n) in total. The inner loop belonging to ii runs only when p[i]p[i] is “yes”, and then makes at most ⌊n/i⌋−i+1⩽n/i\lfloor n/i \rfloor - i + 1 \leqslant n/i passes, each of bounded cost. Summing over ii,

∑i=2nni=n(Hn−1)⩽nlog⁡2n\sum_{i=2}^{n} \frac{n}{i} = n\left(H_n - 1\right) \leqslant n \log_2 n

by the previous proposition. Adding the three contributions, the running time is O(n)+O(nlog⁡2n)=O(nlog⁡n)O(n) + O(n\log_2 n) = O(n \log n).

In Python the array pp is a list of n + 1 entries, True for “yes” and False for “no”; positions 00 and 11 are never read.

def sieve(n):
    p = [True] * (n + 1)
    primes = []
    for i in range(2, n + 1):
        if p[i]:
            primes.append(i)
            for j in range(i, n // i + 1):
                p[i * j] = False
    return primes

print(sieve(50))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

Problem 4.6.

Count the assignments p[i * j] = False performed by the sieve for n=50n = 50, and compare the count with the bound nlog⁡2nn\log_2 n of the theorem. Then count how many of them write “no” to an entry that was already “no”, and say which composite numbers are struck more than once.

Problem 4.7.

Modify the sieve so that the inner loop starts at j=2j = 2 rather than j=ij = i. Show that the output is unchanged, and say how the count of assignments changes for n=50n = 50.

Problem 4.8.

Show that the sieve may stop its outer loop at i=⌊n⌋i = \lfloor \sqrt{n} \rfloor provided the surviving indices above that point are output afterwards. Which part of the proof of the theorem does this rely on?

Data Structures and Interfaces

A data structure is a way to store a non-constant amount of data, supporting a set of operations to interact with that data. The set of operations a data structure supports is its interface. Many data structures may support the same interface and differ in the cost of each operation, and many problems become easy once the data are stored in a suitable structure.

The most primitive data structure native to the Word-RAM is the static array: a contiguous sequence of words reserved in memory, supporting the static sequence interface.

  1. StaticArray(n): allocate a new static array of size nn, every entry initialised to 00, in Θ(n)\Theta(n) time.
  2. get_at(i): return the word stored at index ii, in Θ(1)\Theta(1) time.
  3. set_at(i, x): write the word xx to index ii, in Θ(1)\Theta(1) time.

The operations get_at(i) and set_at(i, x) run in constant time because every item of the array has the same size, one machine word. To store a larger object at an index, the machine word there is read as the memory address of a larger piece of memory holding the object. A Python tuple is like a static array without set_at(i, x).

Classes

Python writes a data structure as a class: a bundle of stored values, called attributes, together with the functions that act on them, called methods.

class Counter:
    def __init__(self, start):
        self.value = start

    def increment(self):
        self.value = self.value + 1

    def __len__(self):
        return self.value

c = Counter(5)
c.increment()
print(c.value, len(c))   # 6 6

Calling Counter(5) creates a new object of the class and runs __init__ on it, with self bound to the new object and start to 5; the assignment self.value = start creates the attribute value. A call c.increment() runs the method with self bound to c. A few method names are special: len(c) calls c.__len__(), and a for loop over c calls c.__iter__().

Inside a method, yield x hands x to the for loop that is running over the object, then carries on from the same point when the loop asks for the next value; yield from X hands on every value of X in turn. This is how range produces its entries one at a time. The statement raise IndexError stops the program with an error naming a bad index, and assert test stops it with an error if test is False.

A class may be built on another: class B(A): gives B every method of A, and any method B defines with the same name replaces the one from A. Inside B, super().__init__() runs the __init__ of A.

A Static Array in Python

Python has no static array, so we imitate one with a list whose length we never change.

class StaticArray:
    def __init__(self, n):
        self.data = [None] * n

    def get_at(self, i):
        if not (0 <= i < len(self.data)): raise IndexError
        return self.data[i]

    def set_at(self, i, x):
        if not (0 <= i < len(self.data)): raise IndexError
        self.data[i] = x

Birthday Matching

Given the students of a class, each with a name and a birthday, we want two students who share a birthday, or a report that there are none. The algorithm keeps a record of the students seen so far, and checks each new student against it before adding them.

Birthday Match
Input:  n students, each a pair (name, birthday).
Output: the names of two students with the same birthday, or None.

    record ≝ a static array of length n
    for k ≝ 0 to n − 1 do
        (name₁, bday₁) ≝ student k
        for i ≝ 0 to k − 1 do
            (name₂, bday₂) ≝ record[i]
            if bday₁ = bday₂ then return (name₁, name₂)
        record[k] ≝ (name₁, bday₁)
    return None
def birthday_match(students):
    # students: tuple of (name, bday) tuples
    n = len(students)                          # O(1)
    record = StaticArray(n)                    # O(n)
    for k in range(n):                         # n passes
        (name1, bday1) = students[k]           # O(1)
        for i in range(k):                     # k passes: check the record
            (name2, bday2) = record.get_at(i)  # O(1)
            if bday1 == bday2:                 # O(1)
                return (name1, name2)          # O(1)
        record.set_at(k, (name1, bday1))       # O(1)
    return None                                # O(1)

print(birthday_match((('Ada', 'Dec 10'), ('Alan', 'Jun 23'), ('Emmy', 'Mar 23'), ('Kurt', 'Jun 23'))))
# ('Kurt', 'Alan')

We assume that each name and each birthday fits into a constant number of machine words, so that one student’s information can be read and compared in constant time. This allows names and birthdays of O(w)O(w) characters from a fixed alphabet, and since w>log⁡2nw > \log_2 n it still allows every student’s information to be distinct.

Every line then takes constant time except three. Building record takes Θ(n)\Theta(n) time; the outer loop makes at most nn passes; and the inner loop on pass kk runs through the kk entries already in the record. The running time is therefore at most

O(n)+∑k=0n−1(O(1)+k⋅O(1))=O(n)+O(n)+O ⁣(n(n−1)2)=O(n2),O(n) + \sum_{k=0}^{n-1} \bigl( O(1) + k \cdot O(1) \bigr) = O(n) + O(n) + O\!\left(\frac{n(n-1)}{2}\right) = O(n^2),

using the sum of an arithmetic progression. This is quadratic in nn. A different data structure for the record does better, and the hashing section at the end of this chapter gives one.

Problem 4.9.

Suppose birthdays are given as integers 0,…,3650, \ldots, 365 (with 365365 for 29 February). Rewrite birthday_match so that it runs in O(n)O(n) time, using a static array of length 366366 indexed by birthday. Where does your running-time argument use the fact that the number of possible birthdays does not grow with nn?

Sequences and Sets

We use two interfaces, which differ in what decides the order of the stored items.

Sequences maintain a collection of items in an extrinsic order: each stored item has a rank in the sequence, including a first item and a last item. Extrinsic means that the first item is first not because of what the item is, but because some external party put it there. An iterable below is anything a for loop can run over, such as a tuple, a string, a list or a range.

OperationMeaning
Containerbuild(X)given an iterable X, build a sequence from the items of X
len()return the number of stored items
Staticiter_seq()return the stored items one by one in sequence order
get_at(i)return the item of rank i
set_at(i, x)replace the item of rank i with x
Dynamicinsert_at(i, x)add x as the item of rank i
delete_at(i)remove and return the item of rank i
insert_first(x)add x as the first item
delete_first()remove and return the first item
insert_last(x)add x as the last item
delete_last()remove and return the last item

The insert and delete operations change the rank of every item after the one inserted or deleted. Two restricted forms of a sequence have names of their own.

Definition 4.13 (Stack and queue).

A stack is a sequence used only through insert_last and delete_last: the item removed is always the one most recently added, “last in, first out”. A queue is a sequence used only through insert_last and delete_first: the item removed is always the one added longest ago, “first in, first out”.

The call stack of the functions section is a stack in this sense. With a Python list, append and pop() are insert_last and delete_last.

Sets, by contrast, maintain a collection of items based on an intrinsic property of what the items are, usually a unique key x.key attached to each item x. Sets generalise dictionaries and other databases queried by content.

OperationMeaning
Containerbuild(X)given an iterable X, build a set from the items of X
len()return the number of stored items
Staticfind(k)return the stored item with key k
Dynamicinsert(x)add x to the set, replacing the item with key x.key if there is one
delete(k)remove and return the stored item with key k
Orderiter_ord()return the stored items one by one in key order
find_min()return the stored item with smallest key
find_max()return the stored item with largest key
find_next(k)return the stored item with smallest key larger than k
find_prev(k)return the stored item with largest key smaller than k

The find operations return None if no qualifying item exists. In Python an item with a key can be an object of a small class:

class Item:
    def __init__(self, key, value):
        self.key = key
        self.value = value

We now give three data structures for the sequence interface. None of them supports insertion or deletion at an arbitrary rank in less than linear time.

Array Sequences

Computer memory is a finite resource. On a modern computer many running programs share the same main memory, so the operating system assigns a fixed range of memory addresses to each of them. When a program asks to store a variable it must say how much memory, how many bits, the variable needs; the operating system finds that much free memory in the program’s assigned range and reserves it, or allocates it, until it is no longer needed. Python hides memory management from the programmer, but whenever Python is asked to store something it makes such a request, for a fixed amount of memory, behind the scenes.

Now suppose a program wants to store two arrays, each of ten 6464-bit words. It makes two requests, for 640640 bits each, and the operating system might reserve the first ten words of the program’s range for the first array AA and the next ten for the second array BB. Later an eleventh word ww has to be added to AA, and there is no room next to AA: the start of the range is to its left, and BB is to its right. One could shift BB right to make room, but much other data may already be reserved beyond BB and would have to move too. It is better to request eleven new words, copy AA into the start of the new allocation, store ww at the end, and release the old ten words for later requests.

Memory itself is one large fixed-length array, from which the operating system allocates. Implementing a sequence with an array, so that index ii of the array holds the item of rank ii, makes get_at and set_at take O(1)O(1) time by random access. Inserting or deleting, however, means moving items and resizing the array, and these operations take linear time in the worst case.

class Array_Seq:
    def __init__(self):                     # O(1)
        self.A = []
        self.size = 0

    def __len__(self): return self.size     # O(1)
    def __iter__(self): yield from self.A   # O(n) iter_seq

    def build(self, X):                     # O(n)
        self.A = [a for a in X]             # stands in for a static array
        self.size = len(self.A)

    def get_at(self, i): return self.A[i]   # O(1)
    def set_at(self, i, x): self.A[i] = x   # O(1)

    def _copy_forward(self, i, n, A, j):    # O(n)
        for k in range(n):
            A[j + k] = self.A[i + k]

    def _copy_backward(self, i, n, A, j):   # O(n)
        for k in range(n - 1, -1, -1):
            A[j + k] = self.A[i + k]

    def insert_at(self, i, x):              # O(n)
        n = len(self)
        A = [None] * (n + 1)
        self._copy_forward(0, i, A, 0)
        A[i] = x
        self._copy_forward(i, n - i, A, i + 1)
        self.build(A)

    def delete_at(self, i):                 # O(n)
        n = len(self)
        A = [None] * (n - 1)
        self._copy_forward(0, i, A, 0)
        x = self.A[i]
        self._copy_forward(i + 1, n - i - 1, A, i)
        self.build(A)
        return x

    def insert_first(self, x): self.insert_at(0, x)             # O(n)
    def delete_first(self): return self.delete_at(0)            # O(n)
    def insert_last(self, x): self.insert_at(len(self), x)      # O(n)
    def delete_last(self): return self.delete_at(len(self) - 1) # O(n)

_copy_forward(i, n, A, j) copies the nn items starting at index ii into the array A starting at index jj, from left to right; _copy_backward copies the same items from right to left, which matters when source and destination overlap. A method name starting with an underscore is a convention for “used only inside the class”.

In a sorted array, whose items are arranged in the order of their keys, an item can be found far faster than by scanning, by the bisection of the last lesson; binary search in the next lesson makes this precise. Inserting an object while preserving the order is then harder, and costs O(n)O(n) time. Deleting an element, even when its index is known, also costs O(n)O(n) time if the array is to have no gaps and keep the order of the remaining elements.

Problem 4.10.

Trace Array_Seq on build((5, 7, 9)), then insert_at(1, 6), then delete_at(0), giving the list self.A after each operation. Count the item copies each of the two dynamic operations makes, and give that count for insert_at(i, x) on a sequence of length nn.

Linked Lists

In a linked list, inserting or deleting an item does not move the others. Its items are kept in a certain order, as in an array, but they can be stored anywhere in memory, in places independent of one another. With each item we store a reference to the place of the next item, its successor; for the last item this reference is None, which marks the end of the list. One further reference to the first item, the head, is needed to reach the list at all.

Linked lists can be singly or doubly linked. In a doubly linked list each item also stores a reference to its predecessor, and the list keeps a reference to its last item, the tail. This allows an item whose place is known to be deleted with a number of steps bounded by a constant independent of the length of the list: the running time is O(1)O(1).

prevdatanextitem 1prevdatanextitem 2prevdatanextitem 3firstlast
Figure 4.1. A doubly linked list with three items. Every item holds a data entry, a reference to the previous item and a reference to the next; a small open circle is None. The names first and last refer to the two ends. Deleting the accented parts leaves a singly linked list.

The Python below is a singly linked list. A node holds an item and a reference next; later_node(i) walks ii steps along the list.

class Linked_List_Node:
    def __init__(self, x):                  # O(1)
        self.item = x
        self.next = None

    def later_node(self, i):                # O(i)
        if i == 0: return self
        assert self.next
        return self.next.later_node(i - 1)

class Linked_List_Seq:
    def __init__(self):                     # O(1)
        self.head = None
        self.size = 0

    def __len__(self): return self.size     # O(1)

    def __iter__(self):                     # O(n) iter_seq
        node = self.head
        while node:
            yield node.item
            node = node.next

    def build(self, X):                     # O(n)
        for a in reversed(X):
            self.insert_first(a)

    def get_at(self, i):                    # O(i)
        node = self.head.later_node(i)
        return node.item

    def set_at(self, i, x):                 # O(i)
        node = self.head.later_node(i)
        node.item = x

    def insert_first(self, x):              # O(1)
        new_node = Linked_List_Node(x)
        new_node.next = self.head
        self.head = new_node
        self.size += 1

    def delete_first(self):                 # O(1)
        x = self.head.item
        self.head = self.head.next
        self.size -= 1
        return x

    def insert_at(self, i, x):              # O(i)
        if i == 0:
            self.insert_first(x)
            return
        new_node = Linked_List_Node(x)
        node = self.head.later_node(i - 1)
        new_node.next = node.next
        node.next = new_node
        self.size += 1

    def delete_at(self, i):                 # O(i)
        if i == 0:
            return self.delete_first()
        node = self.head.later_node(i - 1)
        x = node.next.item
        node.next = node.next.next
        self.size -= 1
        return x

    def insert_last(self, x): self.insert_at(len(self), x)       # O(n)
    def delete_last(self): return self.delete_at(len(self) - 1)  # O(n)

reversed(X) runs through X from its last entry to its first, so that inserting each at the front leaves them in their original order; a test while node: holds as long as node is not None.

Linked lists have a disadvantage: bisection cannot be applied to them, since reaching the middle item means walking to it. Scanning a linked list is also slower than scanning an array, by a constant factor only, because today’s computers reach consecutive storage places substantially faster than places far apart.

Problem 4.11.

Add a method reverse() to Linked_List_Seq that reverses the order of the items in O(n)O(n) time and O(1)O(1) auxiliary space, by changing the next references rather than the items. State the invariant your loop maintains.

Dynamic Arrays

The array sequence’s dynamic operations take time linear in the length of the array. One way to add items without paying a linear transfer cost every time is to over-allocate: request more space than the array currently needs, so that inserting an item means writing it into the next empty slot. This trades a little extra space for constant-time insertion. Any extra allocation is bounded, however; repeated insertions eventually fill it, and the array must be reallocated and copied again. Extra space reserved also means less space for the rest of the program.

Python does not append to the end of a list in worst-case O(1)O(1) time. Sometimes appending to a Python list requires O(n)O(n) time to transfer the array to a larger allocation, so sometimes appending takes linear time. Allocating extra space in the right way guarantees that any sequence of nn insertions takes O(n)O(n) time in total, because the linear-time transfers happen rarely, so insertion takes O(1)O(1) time per insertion on average over the sequence.

Definition 4.14 (Amortized cost).

An operation has amortized cost T(n)T(n) if every sequence of kk operations, starting from an empty data structure, takes at most k⋅T(n)k \cdot T(n) time in total, where nn is the largest size the structure reaches.

The cost of an expensive operation is amortized, that is spread, across the many cheap ones. To achieve amortized constant-time insertion at the end of an array, the strategy is to allocate extra space in proportion to the size of the array stored. Allocating Θ(n)\Theta(n) extra space ensures that a linear number of insertions must occur before an insertion overflows the allocation. A typical implementation allocates double the space needed for the current array, which is called table doubling; any constant fraction of extra space achieves the same bound. The list implementation of CPython, the standard Python interpreter, has used the rule

new_allocated = newsize // 8 + (3 if newsize < 9 else 6)   # extra slots

when a list must grow to newsize items, translated here from C; recent versions use a rule of the same shape. The extra allocation is modest, about one eighth of the size of the array, but it is still linear in that size, so on average n/8n/8 insertions are performed for every linear-time reallocation: amortized constant time.

Now consider removing items from the end. Popping the last item can be done in constant time by decrementing a stored length, which Python does. But if many items are removed from a large list, the unused allocation can hold a large amount of memory that is not available for other purposes. Once the array is small enough we transfer its contents to a smaller allocation and release the larger one. The new allocation cannot be exactly the size of the array, since an immediate insertion would then trigger another reallocation. For constant amortized time over any sequence of appends and pops, there must remain a linear fraction of unused space whenever we rebuild into a smaller array, which guarantees that Ω(n)\Omega(n) further operations must occur before the next reallocation.

The implementation below does both with table-doubling proportions. When an append would pass the end of the allocation, the contents move to an allocation twice as large. When removals bring the array down to a quarter of its allocation, the contents move to an allocation half as large. Python lists already work this way; the code shows how amortized constant-time append and pop can be implemented.

class Dynamic_Array_Seq(Array_Seq):
    def __init__(self, r = 2):              # O(1)
        super().__init__()
        self.size = 0
        self.r = r
        self._compute_bounds()
        self._resize(0)

    def __len__(self): return self.size     # O(1)

    def __iter__(self):                     # O(n)
        for i in range(len(self)): yield self.A[i]

    def build(self, X):                     # O(n)
        for a in X: self.insert_last(a)

    def _compute_bounds(self):              # O(1)
        self.upper = len(self.A)
        self.lower = len(self.A) // (self.r * self.r)

    def _resize(self, n):                   # O(1) or O(n)
        if (self.lower < n < self.upper): return
        m = max(n, 1) * self.r
        A = [None] * m
        self._copy_forward(0, self.size, A, 0)
        self.A = A
        self._compute_bounds()

    def insert_last(self, x):               # O(1) amortized
        self._resize(self.size + 1)
        self.A[self.size] = x
        self.size += 1

    def delete_last(self):                  # O(1) amortized
        self.A[self.size - 1] = None
        self.size -= 1
        self._resize(self.size)

    def insert_at(self, i, x):              # O(n)
        self.insert_last(None)
        self._copy_backward(i, self.size - (i + 1), self.A, i + 1)
        self.A[i] = x

    def delete_at(self, i):                 # O(n)
        x = self.A[i]
        self._copy_forward(i + 1, self.size - (i + 1), self.A, i)
        self.delete_last()
        return x

    def insert_first(self, x): self.insert_at(0, x)       # O(n)
    def delete_first(self): return self.delete_at(0)      # O(n)

def __init__(self, r = 2) gives the parameter r a default value: Dynamic_Array_Seq() uses r = 2, and Dynamic_Array_Seq(3) uses r = 3. The class inherits get_at, set_at and the two copying methods from Array_Seq.

Proposition 4.15 (Appending is amortized constant time).

Starting from an empty Dynamic_Array_Seq with r=2r = 2, any sequence of nn calls of insert_last takes O(n)O(n) time in total.

Discussion.

Each call does a constant amount of work apart from reallocations, so we bound the total cost of the reallocations. A reallocation at size ss allocates about 2s2s slots and copies ss items, so it costs O(s)O(s). The sizes at which reallocations happen are the points where the allocation fills up, and because the allocation doubles each time these sizes grow geometrically. Their sum is therefore dominated by the last one, which is less than nn, and a geometric sum bounds the total by a constant times nn.

Proof.

Without removals lower never exceeds a quarter of the allocation, so _resize(s + 1) reallocates exactly when s+1s + 1 reaches the current allocation upper. The initial call _resize(0) allocates 22 slots. After a reallocation triggered at size s+1=ms + 1 = m the allocation becomes 2m2m. So the allocations are 2,4,8,…2, 4, 8, \ldots, and the insertions that reallocate are those that bring the size to 2,4,8,…2, 4, 8, \ldots; the one bringing the size to 2t2^t copies 2t−12^t - 1 items and allocates 2t+12^{t+1} slots, at cost at most C 2t+1C\,2^{t+1} for a constant CC.

Over nn insertions these are the tt with 2t⩽n2^t \leqslant n, that is 1⩽t⩽m1 \leqslant t \leqslant m with m=⌊log⁡2n⌋m = \lfloor \log_2 n \rfloor. Their total cost is at most

∑t=1mC 2t+1=4C (2m−1)<4C n\sum_{t=1}^{m} C\, 2^{t+1} = 4C\,(2^m - 1) < 4C\,n

by the geometric sum. Every other part of every call costs O(1)O(1), contributing O(n)O(n). The total is O(n)O(n).

The worst-case costs of the three sequence structures are collected below, with (a) marking an amortized bound.

Data structurebuild(X)get_at(i), set_at(i, x)insert_first(x), delete_first()insert_last(x), delete_last()insert_at(i, x), delete_at(i)
Arraynn11nnnnnn
Linked listnnnn11nnnn
Dynamic arraynn11nn11 (a)nn

Each entry is the O(⋅)O(\cdot) bound as a function of the number nn of stored items.

Problem 4.12.

Take r=2r = 2 and start from an empty Dynamic_Array_Seq. Show that any sequence of nn operations, each an insert_last or a delete_last on a nonempty array, takes O(n)O(n) time in total. Then show that if the array were halved as soon as it fell to half full, rather than a quarter, some sequence of nn operations would take Θ(n2)\Theta(n^2) time.

Problem 4.13.

Extend Linked_List_Seq with a reference to its last node so that insert_last takes O(1)O(1) time, and extend Dynamic_Array_Seq so that insert_first and delete_first take O(1)O(1) amortized time. Which operation cannot be made O(1)O(1) in a singly linked list with a tail reference, and why?

Hashing

The set interface asks for find(k). Stored in an array in no particular order, a set answers find(k) by scanning, in O(n)O(n) time. With comparisons alone we cannot do much better: the lower bound for searching in the next lesson shows that Ω(log⁡n)\Omega(\log n) comparisons are needed for a search among nn items. Reading memory at an address computed from the key is not a comparison, and it reaches any memory cell in one step. Hashing uses this.

Direct Access Arrays

A direct access array is a static array with a meaning attached to each index: an item xx with key kk is stored at index kk. This makes sense only when keys are integers. Anything stored in a computer can be associated with an integer, for example its sequence of bits read as a binary number, or its address in memory, so from now on keys are integers.

Suppose we want to store a set of nn items whose unique integer keys lie in the range 0,…,u−10, \ldots, u - 1. We store them in a direct access array of length uu, whose slot ii holds the item with key ii if there is one. To find the item with key ii, look in slot ii: worst-case constant time. The order operations are slow: the first, last or next item could be in any slot, so they may take uu time.

class DirectAccessArray:
    def __init__(self, u): self.A = [None] * u   # O(u)
    def find(self, k): return self.A[k]          # O(1)
    def insert(self, x): self.A[x.key] = x       # O(1)
    def delete(self, k): self.A[k] = None        # O(1)

    def find_next(self, k):                      # O(u)
        for i in range(k + 1, len(self.A)):
            if self.A[i] is not None:
                return self.A[i]

    def find_max(self):                          # O(u)
        for i in range(len(self.A) - 1, -1, -1):
            if self.A[i] is not None:
                return self.A[i]

    def delete_max(self):                        # O(u)
        for i in range(len(self.A) - 1, -1, -1):
            x = self.A[i]
            if x is not None:
                self.A[i] = None
                return x

A direct access array needs a slot for every possible key in the range. When uu is very large compared with the number of items stored, the array is wasteful, or impossible to store at all. Suppose we wanted find(k) on ten-letter names with a direct access array. There are u=2610≈1.4×1014u = 26^{10} \approx 1.4 \times 10^{14} possible names, and even an array of one bit per name would need 17.617.6 terabytes.

The following counting principle is used below to show that collisions cannot be avoided.

Proposition 4.16 (The pigeonhole principle).

If NN objects are placed in rr boxes, some box contains at least ⌈N/r⌉\lceil N/r \rceil objects.

Discussion.

The argument is by contradiction on the total. If every box held fewer than ⌈N/r⌉\lceil N/r \rceil objects, each would hold at most ⌈N/r⌉−1\lceil N/r \rceil - 1, and the rr boxes together would hold fewer than NN. It remains to check the arithmetic step that ⌈N/r⌉−1<N/r\lceil N/r \rceil - 1 < N/r, which is the defining property of the ceiling.

Proof.

Suppose every box contains at most ⌈N/r⌉−1\lceil N/r \rceil - 1 objects. By the definition of the ceiling, ⌈N/r⌉−1<N/r\lceil N/r \rceil - 1 < N/r, so the total number of objects is at most r(⌈N/r⌉−1)<r⋅N/r=Nr\bigl(\lceil N/r \rceil - 1\bigr) < r \cdot N/r = N, a contradiction.

Hash Functions

To keep fast search while using only O(n)O(n) space when nn is much smaller than uu, we store the items in a smaller direct access array of m=O(n)m = O(n) slots, growing and shrinking it like a dynamic array according to the number of items stored. This needs a way to send each key to one of the mm slots.

Definition 4.17 (Hash function and hash table).

A hash function is a function

h:{0,…,u−1}→{0,…,m−1},h : \{0, \ldots, u - 1\} \to \{0, \ldots, m - 1\} ,

and h(k)h(k) is the hash of the key kk. The smaller direct access array of mm slots in which an item with key kk is stored at slot h(k)h(k) is a hash table. Two keys k1≠k2k_1 \neq k_2 collide if h(k1)=h(k2)h(k_1) = h(k_2).

If hh happens to be injective on the nn keys being stored, so that no two of them collide, the hash table acts as a direct access array over the smaller range {0,…,m−1}\{0, \ldots, m-1\} and supports worst-case constant-time search. When m<um < u, however, the pigeonhole principle puts at least two of the uu possible keys in some slot, and when the keys to be stored are not known in advance it is very unlikely that a chosen hash function avoids collisions among them. (If all the keys are known in advance, a scheme called perfect hashing can be designed to avoid collisions between them.)

A slot can hold one item, so colliding items must be stored somewhere. Either they are stored elsewhere in the same array, which is called open addressing and is how most hash tables are implemented in practice, though it is harder to analyse; or they are stored in a separate structure, which is called chaining and is the strategy we adopt.

Chaining

In chaining each slot of the hash table holds a reference to a chain, a separate data structure supporting the dynamic set operations find(k), insert(x) and delete(k). A chain is usually a linked list or a dynamic array, and any implementation will do provided each operation takes at most linear time in the length of the chain. To insert an item xx, insert it into the chain at slot h(x.key)h(x.\mathrm{key}); to find or delete a key kk, find or delete it in the chain at slot h(k)h(k).

01212473823634table
Figure 4.2. Chaining with m=5m = 5 slots and h(k)=k mod 5h(k) = k \bmod 5. The keys 1212 and 4747 collide in slot 22, and 88, 2323 and 6363 in slot 33; each slot refers to a chain holding the items that hash to it.

A chain in Python can be a list of items searched from the front:

class Chain:
    def __init__(self): self.items = []          # O(1)
    def __iter__(self): yield from self.items    # O(length)

    def find(self, k):                           # O(length)
        for x in self.items:
            if x.key == k: return x
        return None

    def insert(self, x):                         # O(length)
        for i in range(len(self.items)):
            if self.items[i].key == x.key:
                self.items[i] = x                # replace, nothing added
                return False
        self.items.append(x)
        return True

    def delete(self, k):                         # O(length)
        for i in range(len(self.items)):
            if self.items[i].key == k:
                return self.items.pop(i)
        return None

We want chains to be short: if every chain holds a constant number of items, the dynamic set operations run in constant time. If instead the hash function sends every stored key to the same slot, one chain has linear length and the operations can take linear time. A good hash function keeps collisions rare, so that no chain grows long.

Choosing a Hash Function

The simplest map from keys in {0,…,u−1}\{0, \ldots, u-1\} to {0,…,m−1}\{0, \ldots, m-1\} is the division method: h(k)=k mod mh(k) = k \bmod m, in Python k % m. If the keys stored are spread evenly over the range, it spreads them roughly evenly among the slots and the chains stay short. But if all of them happen to leave the same remainder on division by mm, every one lands in one chain. We want performance that does not depend on which keys are stored, and no single hash function gives that.

Remark (Every fixed hash function has bad inputs).

If u>nmu > nm, then every hash function hh from {0,…,u−1}\{0, \ldots, u-1\} to {0,…,m−1}\{0, \ldots, m-1\} sends some nn keys to the same slot. By the pigeonhole principle some slot receives at least ⌈u/m⌉\lceil u/m \rceil keys, and u/m>nu/m > n.

Instead we choose the hash function at random, from a large family, after the keys are fixed. Then no set of keys is bad for most of the family, and we can bound the cost on average over the choice of function. The averages we need are over a finite set of equally likely choices.

Definition 4.18 (Uniform choice from a finite family).

Let H\mathcal{H} be a finite nonempty set, and let hh be chosen from H\mathcal{H} with every member equally likely. For a property PP of members of H\mathcal{H}, and a function X:H→RX : \mathcal{H} \to \mathbb{R}, the probability of PP and the expectation of XX are

Pr⁡h∈H[P(h)]=#{h∈H:P(h)}#H,Eh∈H[X(h)]=1#H∑h∈HX(h).\Pr_{h \in \mathcal{H}}\bigl[P(h)\bigr] = \frac{\#\{h \in \mathcal{H} : P(h)\}}{\#\mathcal{H}}, \qquad \mathbb{E}_{h \in \mathcal{H}}\bigl[X(h)\bigr] = \frac{1}{\#\mathcal{H}} \sum_{h \in \mathcal{H}} X(h) .

Two facts follow from the laws of summation. Expectation is linear: E[X+Y]=E[X]+E[Y]\mathbb{E}[X + Y] = \mathbb{E}[X] + \mathbb{E}[Y] and E[cX]=c E[X]\mathbb{E}[cX] = c\,\mathbb{E}[X], because the sum defining the left side splits into the sums defining the right. And the expectation of an Iverson bracket is a probability: E[[P(h)]]=Pr⁡[P(h)]\mathbb{E}\bigl[[P(h)]\bigr] = \Pr[P(h)], since the bracket contributes 11 for each hh with P(h)P(h) and 00 otherwise. The expectation here is over the choice of hash function, which is made independently of the input. It is not an average over possible input keys.

Definition 4.19 (Universal family).

A finite family H\mathcal{H} of hash functions from {0,…,u−1}\{0, \ldots, u-1\} to {0,…,m−1}\{0, \ldots, m-1\} is universal if for any two keys ki≠kjk_i \neq k_j in {0,…,u−1}\{0, \ldots, u-1\},

Pr⁡h∈H[h(ki)=h(kj)]⩽1m.\Pr_{h \in \mathcal{H}}\bigl[h(k_i) = h(k_j)\bigr] \leqslant \frac{1}{m} .

A family that performs well is

H(m,p)={ hab(k)=((ak+b) mod p) mod m  ∣  a,b∈{0,…,p−1}, a≠0 },\mathcal{H}(m, p) = \bigl\{\, h_{ab}(k) = \bigl((ak + b) \bmod p\bigr) \bmod m \;\bigm|\; a, b \in \{0, \ldots, p-1\},\ a \neq 0 \,\bigr\},

where pp is a prime larger than uu. A single function of the family is specified by choosing concrete values of aa and bb. This family is universal. The proof uses arithmetic modulo a prime, which these notes have not developed, and we take it as given.

Proposition 4.20 (Expected chain length).

Let H\mathcal{H} be a universal family, and let nn distinct keys k0,…,kn−1k_0, \ldots, k_{n-1} be stored in a hash table of mm slots with chaining, using hh chosen uniformly from H\mathcal{H}. For each ii, the expected number of stored keys in the chain at slot h(ki)h(k_i) is at most 1+(n−1)/m1 + (n-1)/m.

Discussion.

The chain holding kik_i contains exactly the stored keys that collide with kik_i, together with kik_i itself. So its length is a sum of Iverson brackets, one for each stored key, and linearity turns the expectation of the sum into a sum of expectations. Each bracket’s expectation is a collision probability: the one for j=ij = i is 11, and each of the n−1n - 1 others is at most 1/m1/m by universality.

Proof.

For each jj let Xij(h)=[ h(ki)=h(kj) ]X_{ij}(h) = [\,h(k_i) = h(k_j)\,], which is 11 if kik_i and kjk_j collide under hh and 00 otherwise. The number of stored keys in the chain at slot h(ki)h(k_i) is Xi=∑jXijX_i = \sum_{j} X_{ij}, and Xii=1X_{ii} = 1 for every hh. By linearity and universality,

Eh∈H[Xi]=∑jE[Xij]=1+∑j≠iPr⁡h∈H[h(ki)=h(kj)]⩽1+∑j≠i1m=1+n−1m.\mathbb{E}_{h \in \mathcal{H}}[X_i] = \sum_{j} \mathbb{E}[X_{ij}] = 1 + \sum_{j \neq i} \Pr_{h \in \mathcal{H}}\bigl[h(k_i) = h(k_j)\bigr] \leqslant 1 + \sum_{j \neq i} \frac{1}{m} = 1 + \frac{n-1}{m} .

If the table is at least linear in the number of items stored, m=Ω(n)m = \Omega(n), the expected length of any chain is 1+(n−1)/Ω(n)=O(1)1 + (n-1)/\Omega(n) = O(1). A hash table with chaining and a hash function chosen at random from a universal family therefore performs the dynamic set operations in expected constant time, the expectation being over the choice of hash function and not over the input keys. To keep m=Θ(n)m = \Theta(n), insertions and deletions may have to rebuild the table at a different size and reinsert every item, as a dynamic array does; this makes the bounds for the dynamic operations amortized as well.

A Hash Table in Python

The statement from random import randint makes the function randint available: randint(a, b) returns an integer chosen uniformly from a,a+1,…,ba, a+1, \ldots, b. The keys are assumed to be integers below the prime p=231−1p = 2^{31} - 1. In [Chain() for _ in range(m)] the name _ is the usual name for a loop variable whose value is not used.

from random import randint

class Hash_Table_Set:
    def __init__(self):                          # O(1)
        self.A = []
        self.size = 0
        self.p = 2**31 - 1                       # a prime larger than every key
        self.a = randint(1, self.p - 1)
        self.b = randint(0, self.p - 1)
        self._compute_bounds()
        self._resize(0)

    def __len__(self): return self.size          # O(1)

    def __iter__(self):                          # O(n)
        for chain in self.A:
            yield from chain

    def build(self, X):                          # O(n) expected
        for x in X: self.insert(x)

    def _hash(self, k, m):                       # O(1)
        return ((self.a * k + self.b) % self.p) % m

    def _compute_bounds(self):                   # O(1)
        self.upper = len(self.A)
        self.lower = len(self.A) // 4

    def _resize(self, n):                        # O(n)
        if self.lower < n < self.upper: return
        m = max(n, 1) * 2
        A = [Chain() for _ in range(m)]
        for x in self:
            A[self._hash(x.key, m)].insert(x)
        self.A = A
        self._compute_bounds()

    def find(self, k):                           # O(1) expected
        h = self._hash(k, len(self.A))
        return self.A[h].find(k)

    def insert(self, x):                         # O(1) amortized expected
        self._resize(self.size + 1)
        h = self._hash(x.key, len(self.A))
        added = self.A[h].insert(x)
        if added: self.size += 1
        return added

    def delete(self, k):                         # O(1) amortized expected
        assert len(self) > 0
        h = self._hash(k, len(self.A))
        x = self.A[h].delete(k)
        if x is not None:
            self.size -= 1
            self._resize(self.size)
        return x

    def find_min(self):                          # O(n)
        out = None
        for x in self:
            if (out is None) or (x.key < out.key):
                out = x
        return out

    def find_max(self):                          # O(n)
        out = None
        for x in self:
            if (out is None) or (x.key > out.key):
                out = x
        return out

    def find_next(self, k):                      # O(n)
        out = None
        for x in self:
            if x.key > k:
                if (out is None) or (x.key < out.key):
                    out = x
        return out

    def find_prev(self, k):                      # O(n)
        out = None
        for x in self:
            if x.key < k:
                if (out is None) or (x.key > out.key):
                    out = x
        return out

    def iter_ord(self):                          # O(n^2)
        x = self.find_min()
        while x:
            yield x
            x = self.find_next(x.key)

The number of items stays strictly between a quarter of the number of slots and the number of slots; when it leaves that range the table is rebuilt with twice as many slots as items, which is the rule of Dynamic_Array_Seq with r=2r = 2. So m=Θ(n)m = \Theta(n) throughout, and by the proposition every chain has expected constant length.

Problem 4.14.

Insert the keys 3,13,23,33,8,183, 13, 23, 33, 8, 18 in that order into a hash table of m=10m = 10 slots with chaining and the division method h(k)=k mod 10h(k) = k \bmod 10, and draw the table. Then do the same with m=7m = 7. Describe every set of keys that makes the division method with m=10m = 10 put all keys into one chain.

Problem 4.15.

Rewrite birthday_match using a Hash_Table_Set keyed by birthday, with birthdays given as integers, so that it runs in expected O(n)O(n) time. Explain why the O(n)O(n) bound is an expectation and what it is taken over.

Graphs

A graph records which pairs of objects are related: towns joined by roads, people who know each other, states of a computation that lead to one another. The objects are the vertices and the related pairs are the edges.

Undirected Graphs

Definition 4.21 (Undirected graph).

An undirected graph is a pair G=(S,A)G = (S, A), where SS is a finite set of vertices and AA is a set of unordered pairs {si,sj}\{s_i, s_j\} of vertices. Such a pair is an edge. Unless stated otherwise, the two vertices of an edge are distinct.

The vertices si,sjs_i, s_j joined by an edge are adjacent, and we write

Adj⁡(si)={ sj∈S:{si,sj}∈A }\operatorname{Adj}(s_i) = \bigl\{\, s_j \in S : \{s_i, s_j\} \in A \,\bigr\}

for the set of vertices adjacent to sis_i. We draw an edge as a line between its endpoints.

156243
Figure 4.3. A graph with S={1,…,6}S = \{1, \ldots, 6\} and four edges. Vertex 44 lies on no edge.

Here S={1,2,3,4,5,6}S = \{1, 2, 3, 4, 5, 6\} and A={{1,2},{1,5},{2,5},{3,6}}A = \bigl\{\{1, 2\}, \{1, 5\}, \{2, 5\}, \{3, 6\}\bigr\}. Vertex 44 is isolated: it belongs to SS but to no edge. For example, Adj⁡(2)={1,5}\operatorname{Adj}(2) = \{1, 5\} and Adj⁡(4)=∅\operatorname{Adj}(4) = \varnothing.

Definition 4.22 (Loops, simple graphs and multigraphs).

A loop joins a vertex to itself. A graph is simple if it has no loops and at most one edge between any two vertices. If repeated edges are allowed, the edge collection is a multiset and the graph is a multigraph. We normally work with simple graphs.

Definition 4.23 (Order and size).

The order of a graph is its number ∣S∣|S| of vertices. Its size is its number ∣A∣|A| of edges, counted with multiplicity for a multigraph.

The graph of Figure 4.3 has order 66 and size 44, even though one vertex is isolated. The pair {1,5}\{1, 5\} is the same unordered edge as {5,1}\{5, 1\}; listing both would not add an edge to a simple graph.

Running times of graph algorithms are functions of the graph, not of a single number, and the notation of the last chapter is extended to cover them. For a graph GG we write S(G)S(G) and A(G)A(G) for its vertex set and edge set.

Definition 4.24 (Landau notation on graphs).

Let G\mathcal{G} be the set of all graphs, and let f,g:G→R⩾0f, g : \mathcal{G} \to \mathbb{R}_{\geqslant 0}. We say that f=O(g)f = O(g) if there exist α>0\alpha > 0 and n0∈Nn_0 \in \mathbb{N} such that

f(G)⩽α⋅g(G)for all G∈G with ∣S(G)∣+∣A(G)∣⩾n0.f(G) \leqslant \alpha \cdot g(G) \qquad \text{for all } G \in \mathcal{G} \text{ with } |S(G)| + |A(G)| \geqslant n_0 .

The notations Ω\Omega and Θ\Theta are extended in the same way.

In other words, if ff is greater than gg, then by at most a constant factor, with exceptions allowed only among graphs with fewer than n0n_0 vertices and edges together. The same definition can be made on any countable set of inputs, with a measure of size in place of ∣S(G)∣+∣A(G)∣|S(G)| + |A(G)|. The function ff usually describes the running time of an algorithm or some memory requirement, and gg often depends only on the numbers of vertices and edges, which we write n=∣S(G)∣n = |S(G)| and m=∣A(G)∣m = |A(G)| throughout. Thus O(n+m)O(n + m) means at most a constant times the number of vertices plus edges.

Directed Graphs

Definition 4.25 (Directed graph).

A directed graph is a pair G=(S,A)G = (S, A) with finite vertex set SS and arc set A⊆S×SA \subseteq S \times S. An arc (si,sj)(s_i, s_j) starts at sis_i and ends at sjs_j, and is drawn si→sjs_i \to s_j. The vertex sjs_j is a successor of sis_i, and sis_i a predecessor of sjs_j.

We write

Succ⁡(si)={ sj:(si,sj)∈A },Pred⁡(si)={ sj:(sj,si)∈A }.\operatorname{Succ}(s_i) = \{\, s_j : (s_i, s_j) \in A \,\}, \qquad \operatorname{Pred}(s_i) = \{\, s_j : (s_j, s_i) \in A \,\}.
124563
Figure 4.4. A directed graph. The two curved arcs between 44 and 55 point in opposite directions, and 44 carries a loop.

In Figure 4.4,

A={(1,2),(2,4),(2,5),(4,1),(4,4),(4,5),(5,4),(6,3)},A = \{(1, 2), (2, 4), (2, 5), (4, 1), (4, 4), (4, 5), (5, 4), (6, 3)\},

so for example Succ⁡(4)={1,4,5}\operatorname{Succ}(4) = \{1, 4, 5\} and Pred⁡(4)={2,4,5}\operatorname{Pred}(4) = \{2, 4, 5\}.

Definition 4.26 (Directed loops and pp-graphs).

A directed loop is an arc (v,v)(v, v). A directed graph without loops is sometimes called elementary. A directed pp-graph permits at most pp parallel arcs with any given ordered endpoints; in a 11-graph the arcs form a set, not a multiset.

The pair (4,5)(4, 5) and the pair (5,4)(5, 4) are different arcs, and removing one changes the graph. Reversing every arc of a drawing exchanges each successor set with the corresponding predecessor set.

Problem 4.16.

Let S={1,…,12}S = \{1, \ldots, 12\} and put an arc (a,b)(a, b) whenever a≠ba \neq b and aa divides bb. List Succ⁡(2)\operatorname{Succ}(2), Pred⁡(12)\operatorname{Pred}(12) and every vertex with no successors, and count the arcs.

Degree

Definition 4.27 (Degree).

In an undirected graph, the degree d(v)d(v) of a vertex vv is the number of edge-ends at vv, a loop counting twice. In a directed graph, the out-degree d+(v)d^+(v) counts the arcs beginning at vv, the in-degree d−(v)d^-(v) counts the arcs ending at vv, and d(v)=d+(v)+d−(v)d(v) = d^+(v) + d^-(v). A directed loop contributes one to each of d+(v)d^+(v) and d−(v)d^-(v).

We write δ(v)\delta(v) for the set of edges with an end at vv in an undirected graph, and δ+(v)\delta^+(v) and δ−(v)\delta^-(v) for the sets of arcs beginning and ending at vv in a directed graph.

In a graph without loops d(v)=∣δ(v)∣d(v) = |\delta(v)|, and in a directed graph d+(v)=∣δ+(v)∣d^+(v) = |\delta^+(v)| and d−(v)=∣δ−(v)∣d^-(v) = |\delta^-(v)|, counting parallel arcs separately. In a simple graph d(v)=∣Adj⁡(v)∣d(v) = |\operatorname{Adj}(v)|, and in a directed 11-graph d+(v)=∣Succ⁡(v)∣d^+(v) = |\operatorname{Succ}(v)| and d−(v)=∣Pred⁡(v)∣d^-(v) = |\operatorname{Pred}(v)|. For multigraphs, arcs are counted with multiplicity rather than by taking the sizes of these sets.

In Figure 4.3 the degrees of the vertices 1,2,3,4,5,61, 2, 3, 4, 5, 6 are 2,2,1,0,2,12, 2, 1, 0, 2, 1. Their sum is 8=2∣A∣8 = 2|A|. In Figure 4.4, d+(4)=3d^+(4) = 3 and d−(4)=3d^-(4) = 3: the loop counts once in each.

Proposition 4.28 (Counting edge-ends).

For an undirected graph and for a directed graph respectively,

∑v∈Sd(v)=2∣A∣,∑v∈Sd+(v)=∑v∈Sd−(v)=∣A∣.\sum_{v \in S} d(v) = 2|A|, \qquad \sum_{v \in S} d^+(v) = \sum_{v \in S} d^-(v) = |A| .

Discussion.

Both sides of each identity count the same thing in two ways. The left side of the first sums, vertex by vertex, the edge-ends at that vertex; the right side counts the same edge-ends edge by edge, and every edge has exactly two ends, a loop included. For a directed graph every arc has one tail and one head, so summing out-degrees counts each arc once by its tail, and summing in-degrees counts it once by its head.

Proof.

Count the pairs (v,e)(v, e) in which vv is an end of the edge ee, a loop at vv giving two such pairs. Grouping by vv gives ∑vd(v)\sum_{v} d(v), by the definition of degree. Grouping by ee gives 22 for every edge, so 2∣A∣2|A|. For a directed graph, count the pairs (v,a)(v, a) in which the arc aa begins at vv: grouping by vv gives ∑vd+(v)\sum_v d^+(v), and grouping by aa gives 11 for every arc, so ∣A∣|A|. Counting the pairs in which aa ends at vv gives ∑vd−(v)=∣A∣\sum_v d^-(v) = |A| in the same way.

Example 4.29 (Odd degrees and repeated degrees).

The degree sum of an undirected graph is 2m2m, an even number. A sum of integers is even exactly when it contains an even number of odd terms: each even degree contributes zero modulo 22, and each odd degree contributes one. Thus the number of vertices of odd degree is even.

Also, among n⩾2n \geqslant 2 vertices of a simple graph, two have the same degree. Every degree lies in {0,1,…,n−1}\{0, 1, \ldots, n-1\}. However, degree n−1n - 1 means a vertex is adjacent to every other vertex, so no vertex can have degree 00 at the same time. At most n−1n - 1 of the nn listed values can occur, and putting nn vertices into at most n−1n - 1 degree classes forces two into one class by the pigeonhole principle, Proposition 4.16 .

Kinds of Graphs

We may attach numbers to edges or arcs, to express distance, cost or capacity.

Definition 4.30 (Weighted graph).

A weighted graph G=(S,A,ν)G = (S, A, \nu) is a graph together with a function ν:A→R\nu : A \to \mathbb{R}, the weight. An undirected weight belongs to the unordered edge {u,v}\{u, v\}.

Definition 4.31 (Complete graph).

A simple undirected graph is complete if every pair of distinct vertices is joined; it is written KnK_n when it has nn vertices. A loop-free directed 11-graph is complete when both (u,v)(u, v) and (v,u)(v, u) are arcs for every u≠vu \neq v.

K3123K512345
Figure 4.5. The complete graphs K3K_3 and K5K_5, with 33 and 1010 edges.

To count the edges of KnK_n, choose two distinct vertices without regard to order. There are nn choices for the first vertex and n−1n - 1 for the second. This counts each unordered pair twice, once in each order, so KnK_n has n(n−1)/2n(n-1)/2 edges. We write

(n2)=n(n−1)2,\binom{n}{2} = \frac{n(n-1)}{2},

read ”nn choose 22”, for this number of unordered pairs among nn objects. In the complete directed graph both directions are arcs, so it has n(n−1)n(n-1) arcs.

Definition 4.32 (Subgraphs).

Let G=(S,A)G = (S, A) be a directed or undirected graph. For S′⊆SS' \subseteq S, the induced subgraph G[S′]G[S'] has vertex set S′S' and contains all edges or arcs of GG whose endpoints lie in S′S'. A subgraph (S′,A′)(S', A') may keep only some of these: A′⊆A∩(S′×S′)A' \subseteq A \cap (S' \times S') in the directed case, with the analogous rule for unordered edges. A spanning subgraph, also called a partial subgraph, keeps every vertex but possibly deletes edges.

For a simple undirected graph, the possible edges of a subgraph on S′S' are the unordered pairs of distinct vertices of S′S' that were already edges of GG. Figure 4.6 shows both operations on a four-vertex directed graph.

G1234G′1234G[{1, 2, 4}]124
Figure 4.6. A directed graph GG (left), the spanning subgraph G′G' obtained by deleting (2,2)(2, 2), (3,2)(3, 2) and (3,4)(3, 4) (middle), and the induced subgraph G[{1,2,4}]G[\{1, 2, 4\}] (right).

Here G′G' is the spanning subgraph obtained by deleting (2,2)(2, 2), (3,2)(3, 2) and (3,4)(3, 4). In contrast, the induced subgraph G[{1,2,4}]G[\{1, 2, 4\}] retains the arcs (2,1)(2, 1), (4,1)(4, 1), (4,2)(4, 2) and (2,2)(2, 2): vertex 33 disappears, while every arc of GG joining the remaining vertices stays. An arc between retained vertices cannot be omitted from an induced subgraph.

Definition 4.33 (Clique and independent set).

In a simple undirected graph, a clique is a vertex set CC for which G[C]G[C] is complete, and an independent set, also called a stable set, is a vertex set II for which G[I]G[I] has no edges. For a loop-free directed 11-graph, a directed clique contains both arcs between each pair of its vertices, while a directed stable set has no arcs at all.

72851634
Figure 4.7. The accented triangle {1,5,7}\{1, 5, 7\} is a clique of maximum size; {2,4,5}\{2, 4, 5\} is an independent set of maximum size.

In Figure 4.7, {1,5,7}\{1, 5, 7\} is a clique of maximum size and {2,4,5}\{2, 4, 5\} is an independent set of maximum size. To test whether {1,5,7}\{1, 5, 7\} is a clique, check the three pairs {1,5}\{1, 5\}, {1,7}\{1, 7\}, {5,7}\{5, 7\}; to test whether {2,4,5}\{2, 4, 5\} is independent, check that none of its three pairs is an edge. Checking a particular set requires only pairwise tests. Proving that it is of maximum size also requires ruling out every larger set, and finding maximum cliques in arbitrary graphs is much harder than checking whether a proposed set is a clique.

Definition 4.34 (Bipartite and regular graphs).

An undirected graph is bipartite if its vertices can be divided into disjoint sets S1,S2S_1, S_2 so that every edge has one endpoint in each. It is complete bipartite, written Kn1,n2K_{n_1, n_2}, if ∣Si∣=ni|S_i| = n_i and every pair with one vertex in each set is an edge. A graph is kk-regular if every vertex has degree kk.

Example 4.35 (K2,3K_{2,3} and the four-cycle).

In K2,3K_{2,3}, each of the two vertices of S1S_1 is joined to each of the three vertices of S2S_2, giving 2⋅3=62 \cdot 3 = 6 edges. Its degrees are 33 on S1S_1 and 22 on S2S_2, so it is not regular. The graph with vertices 1,2,3,41, 2, 3, 4 and edges {1,2},{2,3},{3,4},{4,1}\{1,2\}, \{2,3\}, \{3,4\}, \{4,1\}, the cycle on four vertices, is 22-regular and bipartite, with classes {1,3}\{1, 3\} and {2,4}\{2, 4\}.

In general Kn1,n2K_{n_1, n_2} has n1n2n_1 n_2 edges. Fix a vertex of S1S_1; it is joined to each of the n2n_2 vertices of S2S_2. There are n1n_1 such vertices, and every edge has exactly one endpoint in S1S_1, so this counts each edge exactly once.

Example 4.36 (Counting the same thing twice).

Let a bipartite graph with parts S1,S2S_1, S_2 be kk-regular, where k>0k > 0, and let it have mm edges. Sum the degrees of the vertices in S1S_1. Every edge has exactly one endpoint there, so this sum counts every edge once and equals mm. Each of the ∣S1∣|S_1| vertices has degree kk, so the same sum is k∣S1∣k|S_1|. Therefore m=k∣S1∣m = k|S_1|. Repeating the count over S2S_2 gives m=k∣S2∣m = k|S_2|. The two expressions for mm are equal, and dividing by the positive number kk gives ∣S1∣=∣S2∣|S_1| = |S_2|.

Example 4.37 (A monochromatic triangle).

Colour each edge of K6K_6 red or blue. Then there is a triangle whose three edges have the same colour. Pick a vertex xx. Five edges leave xx, so by the pigeonhole principle at least ⌈5/2⌉=3\lceil 5/2 \rceil = 3 of them, say xy1,xy2,xy3xy_1, xy_2, xy_3, share a colour; suppose it is red. If any edge among y1,y2,y3y_1, y_2, y_3 is red, that edge and the two red edges to xx make a red triangle. If none is red, all three edges among y1,y2,y3y_1, y_2, y_3 are blue, making a blue triangle. These cases cover every colouring, so the argument works for all of them.

Problem 4.17.

Show that K5K_5 has a red and blue colouring of its edges with no triangle all of one colour, so that 66 in the last example cannot be replaced by 55.

Problem 4.18.

Show that a graph is bipartite if it has no cycle of odd length, in the sense of the next section, by colouring each vertex according to the parity of its distance from a fixed vertex in its component. Show conversely that a bipartite graph has no cycle of odd length.

Walks, Paths, Cycles and Distance

Definition 4.38 (Walks and paths in a directed graph).

In a directed graph, a walk from uu to vv is a sequence ⟨s0,s1,…,sk⟩\langle s_0, s_1, \ldots, s_k \rangle of vertices with s0=us_0 = u, sk=vs_k = v and (si−1,si)∈A(s_{i-1}, s_i) \in A for every 1⩽i⩽k1 \leqslant i \leqslant k. Its length is kk. The vertex vv is reachable from uu if such a walk exists. A simple path is a walk with no repeated vertex. A closed walk has s0=sks_0 = s_k and k⩾1k \geqslant 1; a simple directed cycle is a closed walk with no further repeated vertices. A loop is a cycle of length one.

A walk may repeat vertices; a simple path may not. We allow the walk ⟨u⟩\langle u \rangle of length zero from uu to itself, so that every vertex is reachable from itself. This convention is used for matrix powers below. When a positive number of arcs is required we say positive-length reachability explicitly.

123456
Figure 4.8. The accented arcs form the simple directed cycle ⟨1,2,5,4,1⟩\langle 1, 2, 5, 4, 1 \rangle.

In Figure 4.8, ⟨1,4,2,5⟩\langle 1, 4, 2, 5 \rangle is a simple path, ⟨3,6,6,6⟩\langle 3, 6, 6, 6 \rangle is a walk with the repeated vertex 66, ⟨1,2,5,4,1⟩\langle 1, 2, 5, 4, 1 \rangle is a simple directed cycle, and ⟨1,2,5,4,2,5,4,1⟩\langle 1, 2, 5, 4, 2, 5, 4, 1 \rangle is a closed walk that is not a simple cycle.

Definition 4.39 (Walks, trails and cycles in an undirected graph).

In an undirected graph, consecutive vertices of a walk are joined by an edge, and walks, length, reachability and simple paths are as in the directed case. A trail is a walk that uses no edge twice. A cycle is a closed trail of positive length; a simple cycle additionally has no repeated vertices except its first and last. A graph with no cycles is acyclic. In a simple undirected graph a simple cycle has length at least three.

Proposition 4.40 (A walk contains a simple path).

If there is a walk from uu to vv, there is a simple path from uu to vv.

Discussion.

If a walk visits some vertex twice, the part between the two visits can be cut out, leaving a shorter walk between the same endpoints. So a walk with the fewest edges can have no repeated vertex. The proof takes such a shortest walk, which exists because walk lengths are nonnegative integers and at least one walk exists.

Proof.

Among all walks from uu to vv, choose one, ⟨s0,…,sk⟩\langle s_0, \ldots, s_k \rangle, with the fewest edges. If it repeated a vertex, say si=sjs_i = s_j with i<ji < j, then ⟨s0,…,si,sj+1,…,sk⟩\langle s_0, \ldots, s_i, s_{j+1}, \ldots, s_k \rangle would be a walk from uu to vv with k−(j−i)<kk - (j - i) < k edges, since (si,sj+1)=(sj,sj+1)(s_i, s_{j+1}) = (s_j, s_{j+1}) is an arc, or edge, of the graph. Hence no vertex repeats.

Definition 4.41 (Distance and diameter).

The distance d(u,v)d(u, v) is the length of a shortest walk from uu to vv, or +∞+\infty if vv is not reachable from uu. The diameter of a nonempty graph is max⁡u,v∈Sd(u,v)\max_{u, v \in S} d(u, v), which may be +∞+\infty. In directed graphs, d(u,v)d(u, v) and d(v,u)d(v, u) may differ.

The two-argument d(u,v)d(u, v) is a distance and the one-argument d(v)d(v) a degree; the number of arguments tells them apart. A shortest walk to a different vertex is a simple path, by the proof of the last proposition, so it has at most n−1n - 1 arcs.

In Figure 4.8, d(1,5)=2d(1, 5) = 2 via 1→2→51 \to 2 \to 5, and d(5,1)=2d(5, 1) = 2 via 5→4→15 \to 4 \to 1. The arc 3→63 \to 6 gives d(3,6)=1d(3, 6) = 1, but there is no directed route from 66 to 33, so d(6,3)=∞d(6, 3) = \infty.

Problem 4.19.

Give the distance d(u,v)d(u, v) for every ordered pair of vertices of Figure 4.8, as a 6×66 \times 6 table, and find the diameter. Which vertices can reach every other vertex?

Representing a Graph

To run an algorithm on a graph we must store it. Let G=(S,A)G = (S, A) have its vertices numbered 1,…,n1, \ldots, n.

Adjacency Lists

An adjacency list T[i]T[i] lists the vertices jj for which (i,j)(i, j) is an arc, or {i,j}\{i, j\} is an edge. For an undirected edge {i,j}\{i, j\} with i≠ji \neq j, jj appears in T[i]T[i] and ii appears in T[j]T[j]. The order within a list is arbitrary.

More generally, an adjacency list representation keeps for every vertex vv a list of all the edges incident with it, the set δ(v)\delta(v); for simple graphs a list of the adjacent vertices, as above, is often enough. For a directed graph one keeps two lists for each vertex, one of the outgoing arcs δ+(v)\delta^+(v) and one of the incoming arcs δ−(v)\delta^-(v).

Example 4.42 (Adjacency lists of a directed graph).

The directed graph GG of Figure 4.6 has

T[1]=(),T[2]=(2,1),T[3]=(1,4,3,2),T[4]=(1,2,3).T[1] = (), \qquad T[2] = (2, 1), \qquad T[3] = (1, 4, 3, 2), \qquad T[4] = (1, 2, 3).

Its arc set is exactly the nine ordered pairs described by these lists. The loop at 22 contributes the entry 22 to T[2]T[2], and the loop at 33 contributes the entry 33 to T[3]T[3]. There are 0+2+4+3=90 + 2 + 4 + 3 = 9 entries, hence nine arcs; counting list entries is a direct way to check the arc count.

Storage. There are nn list headers. The total number of entries is mm for a directed graph with mm arcs and 2m2m for an undirected graph with mm edges, a loop also taking two entries if loops are allowed. Thus storage is O(n+m)O(n + m).

Operations. Reading T[i]T[i] gives the successors and the out-degree of ii quickly. Testing for one particular arc (i,j)(i, j) may require scanning T[i]T[i]. Finding all predecessors of jj requires scanning all the lists, unless reverse lists are stored as well. Visiting all arcs takes O(n+m)O(n + m) time, not merely O(m)O(m), when isolated vertices must also be visited.

Adjacency Lists in Two Arrays

We can store the adjacency lists one after another in a single array Succ[1..m]\mathrm{Succ}[1..m]. A second array Head[1..n]\mathrm{Head}[1..n] gives the final position of each list, and Head[0]=0\mathrm{Head}[0] = 0 gives the starting boundary of the first. The successors of vv then occupy

Succ[Head[v−1]+1],…,Succ[Head[v]],\mathrm{Succ}\bigl[\mathrm{Head}[v-1] + 1\bigr], \ldots, \mathrm{Succ}\bigl[\mathrm{Head}[v]\bigr],

and an empty list has equal consecutive boundaries. For the graph of the last example,

(Head[1],…,Head[4])=(0,2,6,9),Succ=(2,1,1,4,3,2,1,2,3).\bigl(\mathrm{Head}[1], \ldots, \mathrm{Head}[4]\bigr) = (0, 2, 6, 9), \qquad \mathrm{Succ} = (2, 1, 1, 4, 3, 2, 1, 2, 3).

The empty first list is encoded by Head[1]=0\mathrm{Head}[1] = 0; the second list ends at position 22, and so on. These boundaries are cumulative counts: if d+(v)d^+(v) is the length of list vv, then Head[v]=d+(1)+⋯+d+(v)\mathrm{Head}[v] = d^+(1) + \cdots + d^+(v).

To find the predecessors of 22, scan each list and record its owner vv whenever an entry equals 22; the result is 2,3,42, 3, 4. This costs O(n+m)O(n + m) even though only three names are recorded. The same cumulative-count idea produces all the predecessor lists in O(n+m)O(n + m): count how many times each vertex occurs in Succ\mathrm{Succ}, which gives the in-degrees; take cumulative sums to allocate a contiguous block to each vertex; then scan the arcs once more to fill the blocks.

Predecessor Lists
Input:  n, and the arrays Head[0..n] and Succ[1..m] of a directed graph.
Output: arrays PHead[0..n] and Pred[1..m] storing the predecessor lists
        in the same way.

    for v ≝ 1 to n do c[v] ≝ 0
    for i ≝ 1 to m do c[Succ[i]] ≝ c[Succ[i]] + 1
    PHead[0] ≝ 0
    for v ≝ 1 to n do
        PHead[v] ≝ PHead[v − 1] + c[v]
        free[v] ≝ PHead[v − 1] + 1
    for u ≝ 1 to n do
        for i ≝ Head[u − 1] + 1 to Head[u] do
            w ≝ Succ[i]
            Pred[free[w]] ≝ u
            free[w] ≝ free[w] + 1

Python numbers list positions from 00, so the successors of vv are the slice Succ[Head[v - 1]:Head[v]] and the positions of the blocks shift down by one.

def predecessor_lists(n, Head, Succ):
    count = [0] * (n + 1)
    for w in Succ:
        count[w] += 1
    PHead = [0] * (n + 1)
    free = [0] * (n + 1)
    for v in range(1, n + 1):
        PHead[v] = PHead[v - 1] + count[v]
        free[v] = PHead[v - 1]
    Pred = [None] * len(Succ)
    for u in range(1, n + 1):
        for w in Succ[Head[u - 1]:Head[u]]:
            Pred[free[w]] = u
            free[w] += 1
    return PHead, Pred

PHead, Pred = predecessor_lists(4, [0, 0, 2, 6, 9], [2, 1, 1, 4, 3, 2, 1, 2, 3])
print(PHead, Pred)           # [0, 3, 6, 8, 9] [2, 3, 4, 2, 3, 4, 3, 4, 3]
print(Pred[PHead[1]:PHead[2]])   # [2, 3, 4], the predecessors of 2

Each of the loops runs over the vertices or over the arcs once, so the running time is O(n+m)O(n + m).

Matrices

Definition 4.43 (Matrix).

An r×sr \times s matrix is a table of numbers with rr rows and ss columns. Its entry in row ii and column jj is written MijM_{ij}; rows run across and columns down. An n×nn \times n matrix is square, and the identity matrix InI_n is the square matrix with 11 on its diagonal and 00 elsewhere.

Two matrices of the same shape are added entry by entry. If BB is r×sr \times s and CC is s×ts \times t, their product BCBC is the r×tr \times t matrix with

(BC)ij=∑ℓ=1sBiℓ Cℓj,(BC)_{ij} = \sum_{\ell=1}^{s} B_{i\ell}\, C_{\ell j},

and the powers of a square matrix are M0=InM^0 = I_n and Mk+1=MkMM^{k+1} = M^k M. This is a definition, not ordinary entrywise multiplication. For example,

(1101)(1101)=(1201),\begin{pmatrix} 1 & 1 \\ 0 & 1 \end{pmatrix} \begin{pmatrix} 1 & 1 \\ 0 & 1 \end{pmatrix} = \begin{pmatrix} 1 & 2 \\ 0 & 1 \end{pmatrix},

because the top-right entry is 1⋅1+1⋅1=21 \cdot 1 + 1 \cdot 1 = 2. We use only this rule and ordinary arithmetic below. Computing one entry of the product of two n×nn \times n matrices takes nn multiplications, so the whole product takes O(n3)O(n^3) arithmetic operations.

Adjacency Matrices

For a directed 11-graph with vertices 1,…,n1, \ldots, n, the adjacency matrix MM is the n×nn \times n matrix with

Mij={1,(i,j)∈A,0,(i,j)∉A.M_{ij} = \begin{cases} 1, & (i, j) \in A, \\ 0, & (i, j) \notin A . \end{cases}

For a simple undirected graph we put Mij=1M_{ij} = 1 when {i,j}\{i, j\} is an edge. Then Mij=MjiM_{ij} = M_{ji}, so MM is symmetric, and one can store only the entries on and above the diagonal and recover the rest by reflection; this uses about half as many entries, but still Θ(n2)\Theta(n^2) space. For a directed multigraph one may instead put the number of parallel arcs into MijM_{ij}.

Example 4.44 (An adjacency matrix).

The graph GG of Figure 4.6 has

M=(0000110011111110).M = \begin{pmatrix} 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 0 \end{pmatrix}.

The rows match the adjacency lists: the third row has four ones, one for each successor of 33.

A matrix stores n2n^2 entries. Testing whether an arc exists takes one entry lookup; scanning the successors of a vertex takes a row scan of nn entries; and scanning all arcs takes O(n2)O(n^2) time, however few arcs there are.

A second matrix records which vertices lie on which edges.

Definition 4.45 (Incidence matrix).

Let GG be a graph without loops, with vertices 1,…,n1, \ldots, n and edges e1,…,eme_1, \ldots, e_m. Its incidence matrix is the n×mn \times m matrix NN with, for an undirected graph,

Nij={1,i is an end of ej,0,otherwise,N_{ij} = \begin{cases} 1, & i \text{ is an end of } e_j, \\ 0, & \text{otherwise}, \end{cases}

and for a directed graph Nij=−1N_{ij} = -1 if the arc eje_j begins at ii, Nij=1N_{ij} = 1 if it ends at ii, and Nij=0N_{ij} = 0 otherwise.

Example 4.46 (An incidence matrix).

Number the edges of the graph of Figure 4.3 as e1={1,2}e_1 = \{1, 2\}, e2={1,5}e_2 = \{1, 5\}, e3={2,5}e_3 = \{2, 5\}, e4={3,6}e_4 = \{3, 6\}. Its incidence matrix, with rows for the vertices 1,…,61, \ldots, 6, is

N=(110010100001000001100001).N = \begin{pmatrix} 1 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}.

Every column has exactly two ones, the two ends of its edge, and the sum of the entries of row ii is the degree d(i)d(i). Adding all the entries both ways is the count of edge-ends.

The memory requirements of the adjacency matrix and the incidence matrix are therefore Θ(n2)\Theta(n^2) and Θ(nm)\Theta(nm). For graphs with Θ(n)\Theta(n) edges, which is often the case, this is far more than required: adjacency lists need memory proportional to n+mn + m.

For most purposes an adjacency list is the preferred data structure. It allows δ(v)\delta(v), or δ+(v)\delta^+(v) and δ−(v)\delta^-(v), to be scanned for every vertex vv in time linear in their size, and its memory requirement is proportional to the number of vertices and edges, if we assume, as usual, that references, vertex numbers and edge numbers each need only a constant amount of memory. In the Word-RAM this is the assumption that one machine word holds any of them, which w>log⁡2nw > \log_2 n allows for vertex numbers. Graph algorithms are therefore stated for this representation, and moving to the next edge in a list of edges, or reading an endpoint of an edge, is counted as an elementary operation. An adjacency matrix remains the better choice for a graph with very many edges, or when many single-arc tests are needed.

Proposition 4.47 (Powers of the adjacency matrix count walks).

Let MM be the adjacency matrix of a directed 11-graph, and k∈N0k \in \mathbb{N}_0. Then (Mk)ij(M^k)_{ij} is the number of walks of length kk from ii to jj.

Discussion.

The definition of the power is a recursion on kk, so the proof is an induction on kk. For the step, a walk of length k+1k+1 from ii to jj is a walk of length kk from ii to some vertex ℓ\ell followed by one arc from ℓ\ell to jj, and ℓ\ell, the second-to-last vertex, is determined by the walk. So the walks can be sorted by ℓ\ell, and the number through ℓ\ell is the number of kk-walks from ii to ℓ\ell times MℓjM_{\ell j}, which is 11 or 00 according as the last arc exists. Summing over ℓ\ell is exactly the formula for the entry of MkMM^k M. Walks, not paths, are counted: vertices may repeat.

Proof.

For k=0k = 0, InI_n has 11 in position (i,i)(i, i) and 00 elsewhere, and there is exactly one walk of length 00 from each vertex to itself and none to any other vertex.

Suppose the claim holds for kk. Every walk ⟨i=v0,…,vk,vk+1=j⟩\langle i = v_0, \ldots, v_k, v_{k+1} = j \rangle of length k+1k + 1 has a unique second-to-last vertex ℓ=vk\ell = v_k, and consists of a walk of length kk from ii to ℓ\ell followed by the arc (ℓ,j)(\ell, j). For fixed ℓ\ell there are (Mk)iℓ(M^k)_{i\ell} choices of the first part, by the hypothesis, and MℓjM_{\ell j} choices of the last arc, so (Mk)iℓMℓj(M^k)_{i\ell} M_{\ell j} walks pass through ℓ\ell last. Summing over ℓ\ell,

#{walks of length k+1 from i to j}=∑ℓ=1n(Mk)iℓ Mℓj=(MkM)ij=(Mk+1)ij.\#\{\text{walks of length } k+1 \text{ from } i \text{ to } j\} = \sum_{\ell=1}^{n} (M^k)_{i\ell}\, M_{\ell j} = (M^k M)_{ij} = (M^{k+1})_{ij} .

The directed graph 1→2→11 \to 2 \to 1 has adjacency matrix (0110)\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}. Its square is I2I_2: there is one walk of length two from each vertex back to itself, and none to the other vertex.

Reachability from Matrix Powers

To turn walk counts into yes-or-no answers, let sgn⁡(x)=0\operatorname{sgn}(x) = 0 when x=0x = 0 and 11 when x>0x > 0, applied to every entry of a matrix.

Definition 4.48 (Transitive closure).

For a directed graph on nn vertices with adjacency matrix MM, the reflexive transitive closure is

R=sgn⁡(I+M+M2+⋯+Mn−1),R = \operatorname{sgn}\bigl(I + M + M^2 + \cdots + M^{n-1}\bigr),

and the transitive closure is

T=sgn⁡(M+M2+⋯+Mn).T = \operatorname{sgn}\bigl(M + M^2 + \cdots + M^{n}\bigr).

By the proposition, Rij=1R_{ij} = 1 exactly when jj is reachable from ii, the walk of length zero included: a shortest walk to a different vertex is a simple path and uses at most n−1n - 1 arcs. Likewise Tij=1T_{ij} = 1 exactly when there is a walk of positive length from ii to jj, and T=sgn⁡(MR)T = \operatorname{sgn}(MR). The power MnM^n matters on the diagonal, since a shortest closed walk of positive length may be a cycle of length nn. Thus TiiT_{ii} need not be 11, while Rii=1R_{ii} = 1 always.

In the two-vertex graph 1→21 \to 2 with no return arc,

M=(0100),R=(1101),T=(0100),M = \begin{pmatrix} 0 & 1 \\ 0 & 0 \end{pmatrix}, \qquad R = \begin{pmatrix} 1 & 1 \\ 0 & 1 \end{pmatrix}, \qquad T = \begin{pmatrix} 0 & 1 \\ 0 & 0 \end{pmatrix},

and the difference on the diagonal is exactly the zero-length walk.

Computing RR from its definition takes n−2n - 2 matrix products; repeated squaring needs far fewer.

Proposition 4.49 (Closure by repeated squaring).

For every q∈Nq \in \mathbb{N},

∏j=0q−1(I+M2j)=I+M+M2+⋯+M2q−1.\prod_{j=0}^{q-1} \bigl(I + M^{2^j}\bigr) = I + M + M^2 + \cdots + M^{2^q - 1} .

Discussion.

Multiplying out the product, each term chooses from every factor either II or M2jM^{2^j}, and the chosen powers multiply to MM raised to a sum of distinct powers of two. The claim is that every exponent 0,…,2q−10, \ldots, 2^q - 1 arises exactly once, which is the existence and uniqueness of the binary expansion of a number below 2q2^q. The proof is an induction on qq: multiplying the sum up to M2q−1M^{2^q - 1} by I+M2qI + M^{2^q} keeps the sum and adds a copy shifted up by 2q2^q, which fills in the exponents 2q,…,2q+1−12^q, \ldots, 2^{q+1} - 1.

Proof.

For q=1q = 1 both sides are I+MI + M. Suppose the identity holds for qq. Since powers of MM commute with one another,

∏j=0q(I+M2j)=(∑e=02q−1Me)(I+M2q)=∑e=02q−1Me+∑e=02q−1Me+2q=∑e=02q+1−1Me.\prod_{j=0}^{q} \bigl(I + M^{2^j}\bigr) = \Bigl(\sum_{e=0}^{2^q - 1} M^{e}\Bigr)\bigl(I + M^{2^q}\bigr) = \sum_{e=0}^{2^q - 1} M^{e} + \sum_{e=0}^{2^q - 1} M^{e + 2^q} = \sum_{e=0}^{2^{q+1} - 1} M^{e} .

Let q=⌈log⁡2n⌉q = \lceil \log_2 n \rceil, so that 2q−1⩾n−12^q - 1 \geqslant n - 1. Powers beyond Mn−1M^{n-1} reveal no new reachable vertex, so R=sgn⁡(∏j<q(I+M2j))R = \operatorname{sgn}\bigl(\prod_{j<q}(I + M^{2^j})\bigr), and we may replace every positive entry by 11 after each product. The same result follows by working throughout with Boolean matrix addition (OR) and multiplication (AND followed by OR). For n⩾2n \geqslant 2 there are q−1q - 1 squarings to form M2,M4,…,M2q−1M^2, M^4, \ldots, M^{2^{q-1}} and q−1q - 1 products to combine the qq factors: at most 2q−22q - 2 matrix products in total. One more multiplication by MM gives TT. For n=1n = 1, R=IR = I and no product is needed. For comparison, MKM^K takes K−1K - 1 products by repeated multiplication when K⩾1K \geqslant 1, and at most ⌊log⁡2K⌋+popcount⁡(K)−1\lfloor \log_2 K \rfloor + \operatorname{popcount}(K) - 1 by squaring, where popcount⁡(K)\operatorname{popcount}(K) is the number of digits 11 in the binary expansion of KK. These counts are of matrix products; each product of two n×nn \times n matrices takes O(n3)O(n^3) arithmetic operations.

Reflexive Transitive Closure
Input:  the adjacency matrix M of a directed graph on n vertices.
Output: the matrix R with R[i][j] = 1 exactly when j is reachable from i.

    R ≝ sgn(I + M);  P ≝ M;  k ≝ 2
    while k < n do
        P ≝ sgn(P · P)
        R ≝ sgn(R · (I + P))
        k ≝ 2k
    return R

At each test of the loop, P=sgn⁡(Mk/2)P = \operatorname{sgn}(M^{k/2}) and R=sgn⁡(I+M+⋯+Mk−1)R = \operatorname{sgn}(I + M + \cdots + M^{k-1}), by the proposition; the loop stops once k⩾nk \geqslant n, when RR covers every power up to Mn−1M^{n-1}. In Python a matrix is a list of rows, each row a list, and M[i][j] is the entry in row i and column j, numbered from 00.

def product(X, Y):              # Boolean product, O(n^3)
    n = len(X)
    Z = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            for l in range(n):
                if X[i][l] == 1 and Y[l][j] == 1:
                    Z[i][j] = 1
    return Z

def plus_identity(X):           # sgn(I + X)
    n = len(X)
    return [[1 if i == j else X[i][j] for j in range(n)] for i in range(n)]

def closure(M):
    n = len(M)
    R = plus_identity(M)
    P = M
    k = 2
    while k < n:
        P = product(P, P)
        R = product(R, plus_identity(P))
        k = 2 * k
    return R

M = [[0, 1, 0], [0, 0, 1], [0, 0, 0]]   # 1 -> 2 -> 3, numbered 0, 1, 2
print(closure(M))                       # [[1, 1, 1], [0, 1, 1], [0, 0, 1]]

The inner [0] * n must be built afresh for each row, which is what the comprehension does: [[0] * n] * n would make nn references to one and the same row.

Weight Matrices

For a weighted directed graph G=(S,A,ν)G = (S, A, \nu) with vertices s1,…,sns_1, \ldots, s_n, the weight matrix is

Wij={ν(si,sj),(si,sj)∈A,+∞,(si,sj)∉A.W_{ij} = \begin{cases} \nu(s_i, s_j), & (s_i, s_j) \in A, \\ +\infty, & (s_i, s_j) \notin A . \end{cases}

The value +∞+\infty means that there is no direct arc; it is a convention of computation, not an arc of the graph, and WW has entries in R∪{+∞}\mathbb{R} \cup \{+\infty\}. A zero-weight arc is present and must not be confused with an absent arc.

AGBCEF−262−34−1289
Figure 4.9. A weighted directed graph. Each arc carries its weight.

The graph of Figure 4.9 has, with rows and columns in the order A,B,C,E,F,GA, B, C, E, F, G,

W=ABCEFGA∞6∞∞∞−2B∞∞∞∞∞2C∞4∞∞−1∞E∞28∞9∞F∞∞∞∞∞∞G∞∞−3∞∞∞W = \begin{array}{c|rrrrrr} & A & B & C & E & F & G \\ \hline A & \infty & 6 & \infty & \infty & \infty & -2 \\ B & \infty & \infty & \infty & \infty & \infty & 2 \\ C & \infty & 4 & \infty & \infty & -1 & \infty \\ E & \infty & 2 & 8 & \infty & 9 & \infty \\ F & \infty & \infty & \infty & \infty & \infty & \infty \\ G & \infty & \infty & -3 & \infty & \infty & \infty \end{array}

Reading row EE: the arcs E→BE \to B, E→CE \to C and E→FE \to F have weights 22, 88 and 99. The entry WEA=∞W_{EA} = \infty says that there is no arc E→AE \to A; it says nothing about longer routes. For an undirected weighted graph the weight matrix is symmetric. A diagonal entry is ∞\infty unless a loop is present; a distance matrix, which puts 00 on the diagonal, is a different object.

Problem 4.20.

For the graph of Figure 4.8, write down the adjacency lists, the arrays Head\mathrm{Head} and Succ\mathrm{Succ}, and the adjacency matrix MM. Compute M2M^2 and M3M^3, and check the entries (M2)15(M^2)_{15} and (M3)11(M^3)_{11} against walks you can list.

Problem 4.21.

In a directed graph a universal sink is a vertex of in-degree n−1n - 1 and out-degree 00. Give an algorithm that decides whether a graph given by its adjacency matrix has a universal sink, using O(n)O(n) matrix lookups.

Exercises on Data Structures

Exercise 4.1.

An increasing subarray of an array of integers is a run of consecutive entries whose values strictly increase. Write a Python function count_long_subarrays(A) which takes a tuple A=(a0,a1,…,an−1)A = (a_0, a_1, \ldots, a_{n-1}) of n>0n > 0 positive integers and returns the number of longest increasing subarrays of AA, that is, the number of increasing subarrays whose length is at least that of every other. For A=(1,3,4,2,7,5,6,9,8)A = (1, 3, 4, 2, 7, 5, 6, 9, 8) it should return 22, since the longest increasing subarrays have length three and there are two of them, (1,3,4)(1, 3, 4) and (5,6,9)(5, 6, 9). Your function should run in O(n)O(n) time.

Exercise 4.2.

Order the following functions so that if faf_a appears before fbf_b then fa=O(fb)f_a = O(f_b), and indicate which pairs satisfy both fa=O(fb)f_a = O(f_b) and fb=O(fa)f_b = O(f_a). Here log⁡\log means log⁡2\log_2.

f1=log⁡(nn),f2=(log⁡n)n,f3=log⁡(n6006),f4=(log⁡n)6006,f5=log⁡log⁡(6006n).f_1 = \log(n^n), \qquad f_2 = (\log n)^n, \qquad f_3 = \log(n^{6006}), \qquad f_4 = (\log n)^{6006}, \qquad f_5 = \log\log(6006n).

Exercise 4.3.

Let ff and gg be functions N→R⩾0\mathbb{N} \to \mathbb{R}_{\geqslant 0}. Using the definition of Θ\Theta, prove that max⁡(f(n),g(n))=Θ(f(n)+g(n))\max\bigl(f(n), g(n)\bigr) = \Theta\bigl(f(n) + g(n)\bigr).

Exercise 4.4.

A data structure DD supports the sequence operations D.build(X) in O(n)O(n) time, and D.insert_at(i, x) and D.delete_at(i) each in O(log⁡n)O(\log n) time, where nn is the number of items stored at the time of the operation. Using only these operations, describe algorithms for the following, each running in O(klog⁡n)O(k \log n) time. Recall that delete_at returns the deleted item.

  1. reverse(D, i, k): reverse the order of the kk items of DD starting at index ii, that is, those at indices ii to i+k−1i + k - 1.
  2. move(D, i, k, j): move the kk items of DD starting at index ii, in order, to be in front of the item at index jj, where i⩽j<i+ki \leqslant j < i + k is false.

Exercise 4.5.

Each node x of a doubly linked list keeps a reference x.prev to the node before it as well as x.next to the node after it, and the list L keeps L.head and L.tail, its first and last nodes. The list does not store its length.

  1. Describe algorithms for insert_first(x), insert_last(x), delete_first() and delete_last(), each in O(1)O(1) time.
  2. Given two nodes x1 and x2 of a list L, with x1 before x2, describe a constant-time algorithm that removes all nodes from x1 to x2 inclusive from L and returns them as a new doubly linked list.
  3. Given a node x of a list L1 and a second list L2, describe a constant-time algorithm that splices L2 into L1 after x, leaving L2 empty.
  4. Implement these operations in Python, in a class built like Linked_List_Seq.

Exercise 4.6.

A student keeps nn pages of notes in a binder, the first at index 00 and the last at index n−1n - 1, and has two bookmarks AA and BB. Describe a data structure supporting the following operations, where nn is the number of pages at the time of the operation. Assume both bookmarks are placed before any shift or move, and that AA is always at a lower index than BB. For each operation, say whether your running time is worst-case or amortized.

OperationEffectTime
build(X)initialise with the pages of the iterable XO(∣X∣)O(\lvert X \rvert)
place_mark(i, m)place bookmark m∈{A,B}m \in \{A, B\} between the pages at indices ii and i+1i+1O(n)O(n)
read_page(i)return the page at index iiO(1)O(1)
shift_mark(m, d)move bookmark mm, in front of the page at index ii, to be in front of the page at index i+di + d, for d∈{−1,1}d \in \{-1, 1\}O(1)O(1)
move_page(m)move the page in front of bookmark mm to be in front of the other bookmarkO(1)O(1)

Exercise 4.7.

  1. Insert the integer keys 47,61,36,52,56,33,9247, 61, 36, 52, 56, 33, 92 in this order into a hash table of size 77 using the hash function h(k)=(10k+4) mod 7h(k) = (10k + 4) \bmod 7. Each slot stores a linked list of the keys hashing to it, later insertions being appended at the end. Draw the table after all keys have been inserted.
  2. Suppose instead h(k)=((10k+4) mod c) mod 7h(k) = \bigl((10k + 4) \bmod c\bigr) \bmod 7 for a positive integer cc. Find the smallest cc for which no collisions occur when inserting these keys.

Exercise 4.8.

A university assigns 2n2n new students to nn rooms, numbered 00 to n−1n - 1, by hashing their IDs. Each ID is a positive integer less than uu, with uu much larger than 2n2n; no two students have the same ID, and students choose their own IDs. The university publishes a family H\mathcal{H} of hash functions before IDs are chosen, and afterwards chooses the rooming function uniformly from H\mathcal{H}. Two students want to be roommates. For each family below, either show that they can choose IDs k1,k2k_1, k_2 that guarantee it, or prove that no choice guarantees it and find the highest probability of being roommates they can achieve.

  1. H={ hab(k)=(ak+b) mod n  :  a,b∈{0,…,n−1}, a≠0 }\mathcal{H} = \bigl\{\, h_{ab}(k) = (ak + b) \bmod n \;:\; a, b \in \{0, \ldots, n-1\},\ a \neq 0 \,\bigr\}.
  2. H={ ha(k)=(⌊kn/u⌋+a) mod n  :  a∈{0,…,u−1} }\mathcal{H} = \bigl\{\, h_{a}(k) = \bigl(\lfloor kn/u \rfloor + a\bigr) \bmod n \;:\; a \in \{0, \ldots, u-1\} \,\bigr\}.

Exercise 4.9.

A wall is lined with nn boxes of paper, box ii standing ii feet from the left end and containing bib_i reams, where the bib_i are distinct positive integers. A pair of boxes (bi,bj)(b_i, b_j) is close if ∣i−j∣<n/10\lvert i - j \rvert < n/10, and it fulfils an order of rr reams if bi+bj=rb_i + b_j = r. Given B=(b0,…,bn−1)B = (b_0, \ldots, b_{n-1}) and rr, describe an algorithm running in expected O(n)O(n) time that decides whether BB contains a close pair fulfilling the order.

Exercise 4.10.

Imagine inserting the keys 0,1,2,…,n0, 1, 2, \ldots, n, in that order, into a hash table of size 77 that resolves collisions by chaining, with h(k)=k mod 7h(k) = k \bmod 7. Draw the table after the insertion of the keys up to n=9n = 9. Explain how the table evolves for arbitrary nn, and derive the worst-case time for the whole operation of inserting the n+1n + 1 keys.

Exercises on Graphs

Exercise 4.11.

Construct a 33-regular graph on 88 vertices. Is there a 33-regular graph on 99 vertices?

Exercise 4.12.

Show that every graph whose average degree is dd has a subgraph in which every vertex has degree at least d/2d/2.

Exercise 4.13.

Let GG be a graph with no loops in which every vertex has the same odd degree kk. Show that the number of edges is a multiple of kk and that the number of vertices is even.

Exercise 4.14.

  1. Can 77 line segments be drawn in the plane so that each intersects exactly 55 others? Prove your answer.
  2. Suppose a simple graph has exactly two vertices of odd degree. Prove that there is a path between them.

Check Yourself

 

Fresh questions on the whole lesson — none of them is worked out above. Work each one out on paper before opening Python; the box only tells you whether you got there.

Answers are checked in your browser, as often as you like. Nothing is sent anywhere and nothing is kept but your own work. A formula may be written with the symbols themselves or with ~ & | -> <-> ^, and \and, \or, \to expand as you type.

Exercise 4.15.

Which class contains 3n2+10 nlog⁡2n3n^2 + 10\, n \log_2 n?

answer one of these

Exercise 4.16.

What is the least number of comparisons that finds the maximum of 1010 distinct numbers in the worst case?

answer one of these

Exercise 4.17.

How many numbers does the sieve of Eratosthenes output for n=30n = 30?

answer one of these

Exercise 4.18.

Starting from an empty Dynamic_Array_Seq with r=2r = 2, how many slots are allocated after 1717 calls of insert_last?

answer one of these

Exercise 4.19.

What is the worst-case cost of get_at(i) in Linked_List_Seq with nn items?

answer one of these

Exercise 4.20.

Twenty-five objects are placed in seven boxes. What is the largest number that is certain to be in some single box?

answer one of these

Exercise 4.21.

Ten distinct keys are stored with chaining in a table of 55 slots, the hash function drawn from a universal family. What bound does the proposition on chain length give for the expected length of the chain holding a given stored key?

answer one of these

Exercise 4.22.

A simple graph has 1212 edges. What is the sum of its vertex degrees?

answer one of these

Exercise 4.23.

How many edges does K7K_7 have?

answer one of these

Exercise 4.24.

How many edges does K3,4K_{3,4} have?

answer one of these

Exercise 4.25.

For the directed graph with arcs 1→21 \to 2 and 2→12 \to 1 and adjacency matrix MM, what is the entry (M3)12(M^3)_{12}?

answer one of these