Lesson 3
Introduction to Python
Taught
Imperative Knowledge and Computation
There are two kinds of knowledge a mathematical text can record. Declarative knowledge is a statement of fact: the square root of a positive real is the positive with . Imperative knowledge is a recipe: a sequence of instructions which, followed to the letter, produces it. A machine can carry out imperative knowledge, but not declarative knowledge.
At its lowest level a computer does two things: it performs arithmetic, and it stores the results. It does billions of operations a second and can store a great deal.
An algorithm is a finite sequence of unambiguous instructions which, given an initial state and a set of inputs, passes through well-defined successive states and after finitely many steps produces an output and stops.
Finite rules out an infinite list of instructions. Unambiguous rules out a step whose meaning depends on who is reading it. After finitely many steps rules out a recipe that never returns an answer; of the three conditions it is the hardest to check.
Writing an algorithm down needs a notation for assignment, which has no counterpart in an equation. We write
for the instruction “replace the current value of by the value of the expression ”. It is an instruction, not a statement about . The instruction makes sense, while the equation has no solutions.
Here is Heron of Alexandria’s method for the square root of a real .
- Choose any guess .
- If is close enough to , stop and return .
- Otherwise perform .
- Go back to step 2.
The procedure has the two ingredients of every algorithm: a list of operations, and control flow, the rule deciding which operation comes next. Control flow depends on tests that answer yes or no, and it lets a fixed piece of text describe a computation whose length is not fixed.
Let and , and read “close enough” as . Compute the first three values of produced by Heron’s method. Does the method stop within those three steps?
Computability
Early machines were fixed-program computers, wired to solve one problem. The stored-program computer keeps the instructions and the data they act on in the same memory, so that a single interpreter can execute any legal instruction sequence handed to it. A program counter walks the interpreter through the instructions in order, deviating only where control flow says to jump. Since the output of a computation may itself be a sequence of instructions, a machine of this kind can write its own programs.
Remark (The Church–Turing thesis).
Turing’s 1936 model, the Universal Turing Machine, is the standard formalisation of what a stored-program machine can do. The Church–Turing thesis asserts that a function is computable by any effective procedure exactly when some Turing machine computes it. It is not a theorem: “effective procedure” is an informal notion, and the thesis says that the formal model captures it.
A language is Turing complete if it can simulate a Universal Turing Machine. Python is, and so is every language in ordinary use, which is why any algorithm expressible in one of them is expressible in all of them.
There is, however, no algorithm which, given an arbitrary program and its input, decides in finitely many steps whether that program eventually stops or runs forever. This is the halting problem. Because it is unsolvable, the clause “stops after finitely many steps” in the definition of an algorithm has to be proved for each algorithm separately; it cannot be checked mechanically.
Syntax and Semantics
A program is a piece of text, and like a mathematical formula it has a meaning only if it obeys three kinds of rule.
- Primitives. The atoms of the language: numeric literals such as
3.2, strings of text, and operators such as+and*. - Syntax. The rules saying which arrangements of primitives are well formed.
3.2 + 3.2is well formed;3.2 3.2is not. Violations are caught before a single instruction runs. - Static semantics. The rules saying which well-formed arrangements have a meaning. Adding a number to a piece of text has the right shape, operand-operator-operand, and still means nothing.
A program that obeys all three has a semantics: exactly one meaning, fixed by the language and not by the reader. An English sentence, by contrast, can have several.
So when a program misbehaves the machine has not misunderstood it: the error is in what was written. It shows up in one of three ways: the program stops with an error, it runs forever, or it finishes and returns the wrong answer. The third gives no sign that anything is wrong, which is why a program needs a proof of correctness and not only a run that looked right.
Classify each of the following as a syntax error, a static semantic error, or a program that runs to completion and returns the wrong answer. Justify each answer in a sentence.
x = 5 + * 3x = "hello" + 7- An implementation of Heron’s method that stops as soon as rather than when is small.
Problems, Words and Languages
To compare algorithms for a problem, the problem has to be stated in a fixed form. A machine reads and writes finite strings of characters, so problems are stated in terms of strings.
Definition 3.2 (Alphabet, word and language).
Let be a non-empty finite set, called an alphabet. For let denote the set of functions . Such a function is written as the sequence
and is called a word, or string, of length over . The set has a single element, the empty word, of length . Writing
for the set of all words over , a language over is a subset of .
A set is finite if there is an injection for some , and infinite otherwise; the number of elements of a finite set is written . A set admitting an injection into is countable, and the infinite ones among them are the countably infinite sets of the last lesson.
Definition 3.3 (Computational problem).
A computational problem is a relation such that every has at least one with . The elements of are the instances of , and is a correct output for the instance whenever .
The problem is unique if is a function, so that every instance has exactly one correct output. It is discrete if and are languages over a finite alphabet, and numerical if and for some . A unique problem with is a decision problem.
A decision problem is one whose answer is yes or no, and we state such a problem by naming its instances and asking the question.
Example 3.4 (Primality as a decision problem).
Taking as a language over the alphabet , the relation
is a decision problem, which we write as
Primality
Input: n ∈ N.
Question: is n prime? A machine works directly only on discrete problems. A numerical problem must have its instances and its answers written as finite strings, and no finite string names an arbitrary real; rounding error is the difference between a real number and the string used for it.
Pseudocode
An algorithm does not depend on a programming language, and is usually written in pseudocode: the control flow and the assignments written out, without the details a particular language would require. An algorithm in pseudocode names its input and its output, and uses for assignment and output for the result.
Example 3.5 (The square of an integer).
Squaring a natural number takes a single instruction, and the algorithm written out in full reads
The Square of an Integer
Input: x ∈ N.
Output: the square of x.
result ≝ x · x
output resultThe variables here are the input x and the working variable result; the assignment puts the value of into result, and output yields what result holds.
We use pseudocode to describe an algorithm and Python to run it.
Objects and Types
A Python program is a sequence of definitions and commands, evaluated in order by an interpreter. A command is called a statement, and it instructs the interpreter to do something. A call to the built-in print writes to the screen: it takes any number of arguments, separates them by spaces, and follows them by a newline unless told otherwise.
print("Algorithm")
print("terminated.")
# several arguments, and a different ending
print("Algorithm", "terminated", end=".\n")
Writing an f before the opening quote of a string makes it an f-string, in which any expression placed in braces is evaluated and its value substituted:
n = 7
print(f"{n} squared is {n * n}") # 7 squared is 49
In mathematics the legality of an operation depends on what it is applied to: addition is defined for numbers, composition for maps. In Python each value is an object, and every object has a type, which fixes the operations allowed on it. The interpreter uses types to enforce static semantics.
Types divide into scalar, meaning indivisible, and non-scalar, meaning having internal structure. Python has four primitive scalar types.
int, the integers, written as usual:5,-12.float, an approximation to the reals, written with a decimal point (3.0,-28.72) or in scientific notation (1.6e-19). Memory is finite and the reals are not, so afloatholds one of finitely many values andfloatarithmetic is not the arithmetic of : the expression0.1 + 0.2 == 0.3evaluates toFalse.bool, inhabited by exactly two values,TrueandFalse.None, inhabited by one value, used for the absence of a result.
Expressions and Relational Operators
Objects combined with operators according to the syntax form an expression, and every expression evaluates to a single object of some type. The built-in type reports which type an object has:
type(5) # int
type(5.0) # float
Control flow needs tests that come out True or False, and these are built with the relational operators <, <=, >, >=, == and !=. Every one of them evaluates to a bool.
Python’s notation differs from the pseudocode here. Mathematical assignment is written in Python with a single equals sign, g = e. Testing whether two expressions have the same value is ==, and testing that they do not is !=.
3.0 + 2.0 # the float 5.0
3 != 2 # the bool True
Give the value of type(4 == 4) and of type(4.0). Then say what happens if the test in step 2 of Heron’s method is written with = in place of ==, and which of the three kinds of rule of the previous section it breaks.
Arithmetic Operators
Addition, subtraction and multiplication are +, - and *, and exponentiation is **. Division splits into three operators, two of which are the floor and the remainder of the last lesson.
/always evaluates to afloat:5 / 3gives1.6666666666666667.//is floor division, the largest integer not exceeding the quotient. So5 // 3is1and-4 // 3is-2.%is the remainder:5 % 3is2. A modern processor carries out%and//in a few clock cycles, so we count each of them as a single elementary operation.
Remark (Floors and remainders in Python).
For integers a and b > 0, a // b is and a % b is as defined in Definition 2.19. The identity therefore reads
a == b * (a // b) + a % band holds for every integer a. It holds for negative a because // rounds down, not towards zero.
Verify that a % b and a - (a // b) * b are equal, first for and , and then in general.
Suppose integer division were defined by truncation towards zero instead, so that divided by gave . What must the remainder then be for the identity to survive, and what happens to the guarantee ?
Compound expressions are read by precedence: ** binds most tightly, then *, /, // and %, then + and -.
print(2 + 3 * 4) # 14, not 20
print(5 + 4 % 3) # 6, not 0 (% binds as tightly as * and /)
print(2 ** 3 * 4) # 32, not 4096
Operators of equal precedence are read by associativity. Most associate to the left; exponentiation associates to the right.
print(5 - 4 - 3) # -2, not 4
print(4 ** 3 ** 2) # 4 ** 9 = 262144, not 64 ** 2 = 4096
print((4 ** 3) ** 2) # 4096
Logical Operators
The three logical connectives, negation, conjunction and disjunction, are implemented as the keywords not, and and or. They act on bool values and are fixed by these tables, in which T and F abbreviate True and False.
a | not a |
|---|---|
| T | F |
| F | T |
a | b | a and b | a or b |
|---|---|---|---|
| T | T | T | T |
| T | F | F | T |
| F | T | F | T |
| F | F | F | F |
So a and b holds exactly when both do, a or b when at least one does, and not a reverses the value. All three are reserved words: they are part of the syntax and cannot be reassigned or used as names.
Both and and or stop as soon as the answer is settled: in a and b the expression b is never evaluated when a is False, and in a or b it is never evaluated when a is True.
(3 != 2) and (5 % 3 == 0) # False, since 5 % 3 is 2
The Operators in One Place
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, //, **, %, unary -, unary + |
| Relational | <, <=, >, >=, ==, != |
| Assignment | =, +=, -=, *=, /=, //=, **=, %=, <<=, >>= |
| Logical | and, or, not |
The compound assignments abbreviate an update in place: total += x means total = total + x.
The bitwise operators (<<, >>, &, |, ^, ~, &=, |=, ^=), and with them <<= and >>=, are not used in this lesson.
Variables and State
An equation such as ties to permanently: change and changes with it. Assignment does not. It creates a binding, at one moment in time, between a name and one object in memory, and the binding stands until it is replaced.
pi = 3.14159
radius = 11.0
area = pi * (radius ** 2)
radius = 14.0
The third statement evaluates pi * (radius ** 2) to the single float 380.13239 and binds area to it. The fourth rebinds radius, and area is untouched: it is bound to a number, not to a formula. This lets Heron’s method overwrite its guess at every step while the input stays fixed.
Names and Comments
A program has to be checked by a reader, and a correct program with opaque names is hard to check. The # symbol begins a comment, which the interpreter ignores.
a = 3.14159
b = 11.2
c = a * (b ** 2)
pi = 3.14159
diameter = 11.2
area = pi * ((diameter / 2.0) ** 2)
The two blocks compute different numbers, and only the second makes it visible which one is the area of a circle: the first squares a diameter where it should square a radius, and its names hide the fact.
Multiple Assignment
Several names may be bound at once. Every expression on the right of the = is evaluated in full before any name on the left is rebound, so the two sides do not interfere.
x, y = 2, 3
x, y = y, x
print("x is", x) # x is 3
print("y is", y) # y is 2
Exchanging two values therefore needs no third name to hold one of them.
Start from a, b = 1, 1 and perform a, b = b, a + b three times. Give the values of a and b after each of the three steps, and name the sequence they run through. Then say what a, b = b, a + b would produce if the right-hand side were evaluated one name at a time, left to right.
Numbers in Other Bases
The string is a representation of a number rather than the number itself. Each digit occupies a position whose value is a power of ten:
The rightmost digit sits in the units place, , and each position to its left is worth ten times the one before, so the leftmost of stands for two thousand while the rightmost stands for two. This is a positional numeral system: what a digit means depends on where it sits.
Base ten comes from counting on fingers, not from mathematics. Ten is divisible by only four numbers, and , which limits how easily fractions can be handled; twelve, divisible by and , would serve better. A digital circuit holds two voltage levels, high and low, so machines work in base two, where arithmetic is carried out directly by sequences of logic gates.
That every natural number has exactly one representation of this shape in any base is Theorem 2.21 of the last lesson, and the notation is fixed by its corollary. Read as a statement about digit strings, it says that the base- expansion of is the string satisfying three conditions:
- ;
- every digit satisfies ;
- if then the leading digit is not , and the expansion of is the string in every base.
The first condition says that the digits record how many copies of each power of are wanted; in decimal the places from the right are units, tens, hundreds, and in binary they are . The second restricts the available digits to , and without it uniqueness fails: if were a decimal digit standing for ten, then and would both represent one hundred and two. The third bans leading zeros, without which and would be different strings for one number.
Following the corollary we write
for the number with this expansion, and a string carrying no subscript is decimal.
Remark (Names of the small bases).
The base-, , , and expansions are called binary, ternary, octal, decimal and hexadecimal. Bases past ten need more than the ten digits, and the convention is to carry on with letters: , , and so on up to .
Example 3.6 (One number in six bases).
The decimal expansion of is itself. Since , every power of two from to occurs exactly once and the binary expansion is ten ones. For base thirty-six, , and is while is . Altogether
Python writes the three bases a machine uses with bin, oct and hex, each returning a string carrying a prefix that names the base:
n = 1023
bin(n) # '0b1111111111'
oct(n) # '0o1777'
hex(n) # '0x3ff'
The same prefixes may be typed directly as literals, so the conversions can be checked against each other:
a = 0b1111111111
b = 0o1777
c = 0x3FF
a == b == c == 1023 # True
For any other base, int takes the base as a second argument and reads a string written in it:
int('1101220', 3) # 1023
int('sf', 36) # 1023
Find the binary, ternary, octal, hexadecimal and base- expansions of by hand, using to as the extra hexadecimal digits and to for base thirty-six. Check each against int, and against bin, oct and hex.
A number has octal expansion . Give its decimal value and its hexadecimal expansion.
Let . Write down, in terms of , the smallest and the largest number whose base- expansion has exactly three digits.
Branching
Everything written so far is a straight-line program: the statements run in the order they appear, each exactly once. Such a program is easy to reason about, and limited. If one atomic operation takes one unit of time, a straight-line program of lines takes at most units however large its input, because no line ever runs twice.
The first way to let a program do more is branching: a test is evaluated, and the block of statements that runs next depends on the answer.
Conditional Statements
The basic branching construct is if–else. It consists of a test evaluating to True or False, a block run when the test is True, and an optional else block run when it is False.
Python marks the extent of a block by indentation. Where other languages use braces or an end keyword, Python uses the whitespace itself, so the layout of a program shows its structure.
x = 14
if x % 2 == 0:
print("The integer is even.")
else:
print("The integer is odd.")
print("Branching complete.")
The test uses x % 2, the remainder on division by two, which is the last digit of the binary expansion of x. It is 0 for even x and 1 for odd. The final print is not indented, so it belongs to neither block and runs either way; indenting it by four spaces would make it part of the else and suppress it for even x.
Because indentation carries meaning, a long expression cannot be broken across lines anywhere. A backslash at the end of a line continues it explicitly, and a line inside unclosed brackets continues implicitly.
# explicit continuation
alpha = 1.61803398875 + \
2.71828182845 + \
3.14159265359
# implicit continuation, inside parentheses
beta = (1.61803398875 +
2.71828182845 +
3.14159265359)
Nested Conditionals
A block inside a branch may itself branch, which builds a decision tree.
Where the cases are mutually exclusive, a chain of else blocks each containing one if grows an extra level of indentation per case. The keyword elif collapses the chain: its test is evaluated exactly when every test above it has come out False.
n = 15
if n % 2 == 0:
if n % 3 == 0:
print("n is a multiple of 6.")
else:
print("n is even, but not divisible by 3.")
elif n % 3 == 0:
print("n is odd and divisible by 3.")
else:
print("n is not divisible by 2 or 3.")
Compound Tests and Updating State
Tests may be combined with and, or and not. How they are combined changes how much work the program does.
Take the problem of returning the largest positive number among , and , or, if none of them is positive, the smallest of the three. Each of the three is positive or not, so there are cases, and a program with one branch per case tests the same three comparisons over and over.
Binding a provisional answer and updating it only when a later value beats it replaces the eight cases by three independent tests:
a, b, c = -5, 12, 7
# if none is positive the answer is the smallest, so start there
target = min(a, b, c)
if a > 0:
target = a
if b > 0 and b > target:
target = b
if c > 0 and c > target:
target = c
print(target) # 12
The built-in min returns the smallest of its arguments, and max the largest. Each of the three variables has its sign examined once.
Conditional Expressions
For a choice between two values rather than between two blocks of work, Python supplies the conditional expression, which packs an if–else into a single expression:
expression_if_true if test else expression_if_false
A variable may therefore be bound according to a test without a multi-line block. The absolute value of Definition 1.65 is given by cases, so there are several ways to compute it. The most direct is a standard if block:
x = -10
if x < 0:
x = -x
For a body consisting of a single short statement, Python permits writing the block on the same line as the if:
if x < 0: x = -x # only for very short statements
Branching can also be avoided entirely by using the fact that True and False behave as and in arithmetic:
y = (x < 0) * (-x) + (x >= 0) * x # works, but hard to read
This works, but it is hard to read, it evaluates both branches every time, and it hides the case split inside a multiplication. The conditional expression is clearer:
y = x if x >= 0 else -x
Python also provides the built-in abs for this purpose, so abs(-10) returns 10.
Using conditional expressions and no if statements, write single-line definitions of
- the sign of , which is , or according as is negative, zero or positive;
- the larger of and , without using
max; - the distance between two reals, without using
abs.
Constant Time
Branching does not remove the limit on straight-line programs. Each block is entered at most once on a run, so if one atomic operation takes one unit of time, a branching program of lines can never exceed units, and its maximum running time is hard-bounded by a constant .
Definition 3.7 (Constant time).
An algorithm runs in constant time if there is a constant , depending on the algorithm alone, such that the algorithm performs at most atomic operations on every input, whatever the size or magnitude of that input.
Every straight-line program runs in constant time, and so does every branching program, with the number of lines.
Remark (Beyond constant time).
Constant time is a strong restriction. Consider computing the factorial of an integer . It requires multiplications, and is not bounded, so no fixed number of multiplication statements serves for every ; a program with one hard-coded branch per value of would need infinitely many branches and would violate the finiteness in the definition of an algorithm. The same holds for reading the base- digits of a number, whose count grows like . Computations whose length grows with the input need control flow that can return to an earlier instruction.
Let a, b and c be the coefficients of . Write a program that computes the discriminant and reports whether the polynomial has two distinct real roots, one repeated real root, or none. Treat separately, where the expression is linear rather than quadratic, and say what your program should report when .
Consider the rule which replaces an integer by when is odd and by when is even. Using conditional expressions, write a program that applies the rule three times in succession to a given positive integer. Check that starting from it produces , and find a starting value below from which three applications return the starting value itself.
Iteration
Branching programs are bound by constant time: each instruction is executed at most once, so the whole computation is capped by the static length of the source. The base expansions of the first chapter show the limit. Every step applied the same pair of operations, // and %, to the current quotient, and with no way of repeating those operations automatically we performed each step by hand. Computing the factorial of , testing whether a number is prime, extracting the digits of an arbitrarily large base expansion: these tasks take longer on larger inputs, and no branching program can perform them.
Iteration sends execution back to an instruction already passed.
An iteration, or loop, is a control structure that sends execution back to an instruction it has already passed, so that a block of statements runs repeatedly. How many times it runs is decided by the state of the computation while it runs, not by the length of the program.
The While Loop
Python’s while statement has the syntax of if: a test, a colon, and an indented body. The test is evaluated; if it is True the body runs in full and control returns to the test; if it is False the body is skipped and execution continues after the loop.
The following program computes by adding to a running total times.
x = 3
ans = 0
num_iterations = 0
while num_iterations != x:
ans = ans + x
num_iterations = num_iterations + 1
print(f"{x} squared is {ans}")
Recording the values of the variables each time the test is reached, as though one were the interpreter, is called hand simulation, and it is how we check what a loop does.
| Test evaluation | x | ans | num_iterations | Test |
|---|---|---|---|---|
| 1st | 3 | 0 | 0 | True |
| 2nd | 3 | 3 | 1 | True |
| 3rd | 3 | 6 | 2 | True |
| 4th | 3 | 9 | 3 | False |
On the fourth evaluation num_iterations has reached x, and the program prints 3 squared is 9.
Whether it stops at all depends on x, and there are three cases. If , the test fails at once, the body never runs, and 0 squared is 0 is printed. If , the counter starts below x and rises by exactly one per pass, so after passes it equals x and the loop ends with the right answer. If , the counter runs through and never equals a negative number: the test is never False, and the program runs forever. This is an infinite loop.
Weakening the test to num_iterations < abs(x) stops the loop after abs(x) passes, but each pass still adds the negative number x, so the program would announce that . The body has to be corrected too:
x = -3
ans = 0
num_iterations = 0
while num_iterations < abs(x):
ans = ans + abs(x)
num_iterations = num_iterations + 1
print(f"{x} squared is {ans}") # -3 squared is 9
Example 3.9 (The leading digit).
Floor division by ten discards the last decimal digit, so applying it until one digit is left leaves the first digit. How many times it must be applied is not known before the loop starts, which is what a while loop is for.
n = 72658489290098
n = abs(n)
while n >= 10:
n = n // 10
print(n) # 7 Two further pieces of Python are needed for the problems below. Writing + between two strings joins them end to end, so 'X' + 'X' is 'XX'. And input(prompt) prints its prompt, waits for the user to type a line, and returns what was typed as a string; int converts a string of digits to the integer it denotes, so int(input('n? ')) reads a number.
The program below should print the letter X a given number of times. Replace the comment by a while loop that appends 'X' to to_print exactly num_x times, and say what your loop does when the user enters or a negative number.
num_x = int(input('How many times should I print the letter X? '))
to_print = ''
# append X to to_print num_x times
print(to_print) Leaving a Loop Early
A break statement ends the loop containing it at once and passes control to the first statement after it, without returning to the test.
# the smallest positive integer divisible by both 11 and 12
x = 1
while True:
if x % 11 == 0 and x % 12 == 0:
break
x = x + 1
print(x, 'is divisible by 11 and 12') # 132 is divisible by 11 and 12
The test while True never fails, so the break is the only way out, and the proof that the loop stops is about the break. This arrangement suits a loop whose exit condition is natural to check partway through the body rather than at the top.
Where one loop sits inside another, a break ends only the loop that immediately contains it; the outer loop carries on.
Write a program that reads ten integers, one at a time, and then prints the largest odd number among them, or a message saying that none was odd. Do not use max.
The For Loop
The loops above share a pattern: a counter is set up, tested at the top, and advanced at the bottom of the body. The for statement does that bookkeeping itself. Its form is
for variable in sequence:
body
The variable is bound to the first entry of the sequence and the body runs; then to the second, and the body runs again; and so on until the sequence is exhausted or a break intervenes.
total = 0
for num in (77, 11, 3):
total = total + num
print(total) # 91
The object (77, 11, 3) is a tuple, an ordered finite sequence written in parentheses.
The Range Function
The sequence is most often produced by range, which generates a progression of integers and takes one, two or three arguments.
With one argument, range(stop) gives .
for i in range(4):
print(i) # 0, then 1, then 2, then 3
With two, range(start, stop) gives . The lower end is included and the upper end is not.
total = 0
for x in range(5, 11):
total = total + x
print(total == 5 + 6 + 7 + 8 + 9 + 10) # True
With three, range(start, stop, step) gives , and stops before reaching stop. For positive step the last entry is the largest below stop; a negative step descends instead, so range(40, 5, -10) gives .
total = 0
for x in range(10, 3, -1):
if x % 2 == 1:
total = total + x
print(total) # 9 + 7 + 5 = 21
Choosing the step to land on the right numbers is often more trouble than testing them inside the body. The following sums the odd numbers between m and n whatever the parity of m:
m, n = 4, 10
total = 0
for x in range(m, n + 1):
if x % 2 == 1:
total = total + x
print(total) # 5 + 7 + 9 = 21
The two-argument form covers the one-argument form: range(0, 3) produces the same sequence as range(3). The entries are produced one at a time as the loop asks for them rather than stored all at once, so range(1000000) costs no more memory than range(3).
Remark (Which loop to use).
A for loop is the right choice when the number of passes is settled before the loop begins, since range then states that number and no counter can be mismanaged. A while loop is the right choice when it is not: the leading-digit program above stops when the number falls below ten, and how many divisions that takes is a fact about the input rather than about the program.
Example 3.10 (Squaring by repeated addition again).
The squaring program loses its counter entirely when written with for:
x = -3
ans = 0
for num_iterations in range(abs(x)):
ans = ans + abs(x)
print(f"{x} squared is {ans}") # -3 squared is 9There is no explicit test and no explicit increment; range supplies both.
Reassigning the Loop Variable
Assigning to the loop variable inside the body does not disturb the loop.
for i in range(2):
print(i)
i = 0
print(i)
This prints 0, 0, 1, 0 and stops. The sequence is fixed when the for statement is first reached, and at the start of each pass the variable is rebound to the next entry of it, whatever happened to the variable in between. The loop is equivalent to
index = 0
last_index = 1
while index <= last_index:
i = index
print(i)
i = 0
print(i)
index = index + 1
For the same reason, changing a variable that was used in the range call has no effect, because the call is evaluated once:
x = 1
for i in range(x):
print(i)
x = 4
# prints 0, and nothing else
An inner for is a different matter: its range call is reached afresh on every pass of the outer loop and so is evaluated again.
x = 3
for j in range(x):
print('Outer')
for i in range(x):
print(' Inner')
x = 2
The outer range(x) is evaluated once, with , so the outer loop makes three passes. The inner range(x) sees on the first pass and afterwards, giving inner passes in total.
Iterating Over a String
A string is a sequence of characters. Its positions are numbered from , so a string of length occupies positions : len(s) gives the length, s[i] gives the character at position i, and the slice s[i:j] gives the characters at positions i up to but not including j. That upper end is excluded for the same reason it is excluded from range, and the two conventions agree: s[0:len(s)] is the whole of s, and the positions it covers are exactly those produced by range(len(s)).
Combined with in, the for statement walks a string directly, binding the variable to one character at a time and dispensing with the positions altogether.
total = 0
for c in '12345678':
total = total + int(c)
print(total) # 36
Write a program that computes with a for loop, and check the result against the closed form for an arithmetic progression obtained in the last lesson. Then do the same for , once by choosing the step of the range and once by testing inside the body.
Nested Loops
A loop inside a loop runs the inner loop to completion on every pass of the outer one.
An block of asterisks needs one loop for the rows and one for the columns:
n = 5
for row in range(n):
for col in range(n):
print('*', end='')
print()*****
*****
*****
*****
*****The bare print() after the inner loop ends the line. Letting the inner range depend on the outer variable changes the shape:
n = 5
for row in range(n):
print(row, end=' ')
for col in range(row):
print('*', end=' ')
print()0
1 *
2 * *
3 * * *
4 * * * *Row carries asterisks, so the block becomes a triangle.
Continue and Pass
Two keywords sit alongside break. A continue abandons the rest of the current pass and goes straight to the next one: back to the test for a while, on to the next entry for a for. A pass does nothing, and exists because Python’s syntax requires a statement in places where no action is wanted.
for n in range(200):
if n % 3 == 0:
continue
elif n == 8:
break
else:
pass
print(n, end=' ')
# 1 2 4 5 7
The multiples of three never reach the print, the loop ends when n reaches , and the remaining values pass through the else branch and are printed.
Divisibility and Primality
Definition 3.12 (Divisibility).
Let . We say divides , written , if for some ; equivalently, for , if . In Python this is the test n % d == 0.
Definition 3.13 (Prime and composite).
An integer is prime if its only positive divisors are and , and composite otherwise. The integers and and the negative integers are neither.
Checking that is composite takes a single operation once the divisor is known, since 91 % 7 == 0 settles it; finding the divisor is where the work lies. Read as an instruction, the definition says to test every candidate between and and to stop at the first one that divides.
n = 91
is_prime = n >= 2
for factor in range(2, n):
if n % factor == 0:
is_prime = False
break
if is_prime:
print(f'{n} is prime')
else:
print(f'{n} is not prime')
# 91 is not prime
One divisor settles the question, so the loop stops at the first one it finds. Wrapping the whole thing in a second loop lists the primes below a bound:
for n in range(2, 100):
is_prime = True
for factor in range(2, n):
if n % factor == 0:
is_prime = False
break
if is_prime:
print(n, end=' ')
# 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
Trial Division to the Square Root
Testing every candidate up to is more than necessary.
Proposition 3.14 (A composite number has a small divisor).
Let . Then is composite if and only if some integer with divides .
Discussion.
Divisors come in pairs: if then and are both divisors and their product is . A product of two numbers both exceeding exceeds , so the two members of a pair cannot both lie above , and one of them is at or below it. The proof takes the least divisor above ; its partner is then the larger of the two, and the inequality can be squared. The converse direction is immediate: a divisor in that range is neither nor , because once .
Proof.
Suppose is composite. The set contains , so it is non-empty; let be its least element and write with , so that and .
We first rule out . If then . But is composite, so it has a divisor with ; that lies in and is smaller than , contradicting the minimality of . Hence , so and therefore . Consequently
so .
Conversely, suppose and . From we get , so , and by hypothesis. Thus has a positive divisor other than and , so it is composite.
Only the integers up to need be tested, and can be dealt with separately so that the loop skips the even candidates:
n = 1010809
is_prime = n >= 2
if n > 2 and n % 2 == 0:
is_prime = False
else:
max_factor = round(n ** 0.5)
for factor in range(3, max_factor + 1, 2):
if n % factor == 0:
is_prime = False
break
print(f'{n} is prime: {is_prime}') # 1010809 is prime: True
round(n ** 0.5) returns either or one more, never less, so the loop may test one candidate too many and never one too few.
The first program tests up to candidates and the second about : for , about a million tests against about five hundred. Python’s time module measures it. The statement import time makes the module’s tools available, and time.time() returns the current time in seconds. A colon inside the braces of an f-string says how to format the value: :.2f prints it with two digits after the decimal point.
import time
n = 1010809
time0 = time.time()
is_prime_basic = True
for factor in range(2, n):
if n % factor == 0:
is_prime_basic = False
break
time1 = time.time()
print(f'Basic: {(time1 - time0) * 1000:.2f} ms')
time0 = time.time()
is_prime_fast = n >= 2
if n > 2 and n % 2 == 0:
is_prime_fast = False
else:
max_factor = round(n ** 0.5)
for factor in range(3, max_factor + 1, 2):
if n % factor == 0:
is_prime_fast = False
break
time1 = time.time()
print(f'Optimised: {(time1 - time0) * 1000:.2f} ms')
print(is_prime_basic == is_prime_fast) # True
The two agree, and on an ordinary machine the first takes tens of milliseconds where the second takes a small fraction of one.
Searching for the th Object
Finding the th number with a given property is a different kind of problem: the answer is not known in advance, so there is no range to run over. A counter of successes and a candidate that advances by one at a time turn it into a while loop.
target = 5
found = 0
guess = 0
while found <= target:
guess = guess + 1
if guess % 4 == 0 or guess % 7 == 0:
found = found + 1
print(f'Counting from zero, entry {target} of the multiples of 4 or 7 is {guess}')
# entry 5 is 16
The multiples of four or seven begin , and counting from zero the entry numbered is . Substituting the primality test for the divisibility test finds the th prime:
target = 10
found = 0
guess = 1
while found <= target:
guess = guess + 1
is_prime = guess >= 2
if guess > 2 and guess % 2 == 0:
is_prime = False
else:
max_factor = round(guess ** 0.5)
for factor in range(3, max_factor + 1, 2):
if guess % factor == 0:
is_prime = False
break
if is_prime:
found = found + 1
print(f'Counting from zero, prime {target} is {guess}') # prime 10 is 31
The primes are , and counting from zero the one numbered is .
Write a program that prints the sum of the primes strictly between and , using a primality test nested inside a loop over the odd integers from to . Then say how many candidate divisors your program tests in total, and how many the version without the square-root bound would test.
Digits in an Arbitrary Base
In the first chapter we read off base- expansions by hand, dividing by , recording the remainder as the next digit from the right, and continuing with the quotient until it reached zero. Automating that needs a way of repeating instructions until a condition is met, which the while loop provides. The number of repetitions is the number of digits, and it is not known before the divisions are done.
Proposition 3.15 (Digit extraction).
Let and . Define and . Then for some , the least such is , and for
where is the base- expansion of .
Discussion.
One pass of the loop performs the split , which removes the last digit. The proof identifies explicitly, as the number whose digits are the top digits of , and proves this by induction on . The step is the split applied to , and the remainder is the digit by the uniqueness of quotient and remainder, as in the uniqueness half of Theorem 2.21. The number of steps then follows: the sum defining is empty exactly when , and is at least before that, because its leading digit is not zero.
Proof.
By Corollary 2.22 the number has a unique expansion
We claim that
For this is the expansion itself. Assume it for some and split off the term :
The sum on the right is an integer and , so this is the division of by with quotient and remainder, and by the uniqueness of that division
which is the claim at together with the stated identity for the digits.
At the sum is empty, so . For every term is non-negative and the term is , so . Hence is the least index at which the sequence vanishes.
The loop below carries out the proposition. Digits are produced from the last to the first, so each new one is put in front of what has been built so far, and digits above nine are turned into letters: str(d) turns the number d into the string of its decimal digits, ord(c) gives the code number of the character c, chr(k) gives the character with code k, and the codes of A to Z run consecutively.
n = 12345
b = 17
digits = ''
while n > 0:
remainder = n % b
if remainder < 10:
digits = str(remainder) + digits
else:
digits = chr(ord('A') + remainder - 10) + digits
n = n // b
print(digits) # 28C3
Example 3.16 (Checking the expansion).
Reading back gives
as the proposition says.
The loop above prints nothing when . Say why, in terms of the hypotheses of the proposition, and repair it so that it prints '0'. Then use it to find the base- and hexadecimal expansions of , and check them against the by-hand method of the first chapter.
Write a program that repeatedly asks the user for a string and prints it back, stopping when the user enters 'done'. It should then print 'Bye!' followed by the number of strings entered, not counting 'done'.
Running Time
Timing the two primality tests measured one machine at one time. The number of instructions a program performs depends on the compiler and on the hardware, and we do not in any case know it exactly. We want a statement about the algorithm instead: how the amount of work grows with the input.
Definition 3.17 (Elementary operation and running time).
An elementary operation is one the machine performs in a bounded number of clock cycles independently of the values involved: an arithmetic operation or a comparison on numbers of bounded size, a read or a write of one stored value. Both a // b and a % b are elementary in this sense.
The running time of an algorithm is the number of elementary operations it performs, counted as a function of its input.
Two things remain to be fixed: how the size of an input is measured, and how precisely we count.
Landau Notation
Let . Then is the set of functions for which there exist and with
The constant must not depend on ; were it allowed to, every function would lie in and the definition would say nothing. Nor does the inequality have to hold everywhere: it may fail for finitely many , so changing at the first million values leaves the statement untouched.
Instead of one usually writes
read ” is big-Oh of ”. The equals sign here is not the symmetric one, since is a set and is a member of it; the notation is standard nonetheless.
Take and . For we have and , so
and with meets the definition. Hence .
Take and . For we have and , so
and with serves. Hence .
Here the bound grows at the same rate as itself, where the previous example bounded a cubic by a quartic. Both statements are true, but the cubic-by-quartic one loses information, since grows strictly faster than ; the same argument with gives the sharper . A function lies in for many different , and the useful statement uses the slowest-growing available.
Example 3.21 (When the definition fails).
Not every pair of functions is related this way: is not . Suppose it were, so that for some and all . Dividing by , which is positive, gives for all . But
satisfies and , which contradicts it. So no constant serves, and the direction of an statement is not reversible: holds while does not.
The argument of the first two examples works for any polynomial.
Proposition 3.22 (Polynomials).
Let with every and . Then .
Discussion.
The proof uses one inequality: for and we have , because raising a number at least to a larger exponent cannot decrease it. Applying it to each term replaces every power by the top power, and what is left is the sum of the coefficients, which serves as the constant of the definition. Non-negative coefficients let each term be bounded separately; with negative coefficients the same bound holds after taking absolute values of the coefficients.
Proof.
Let . For each with we have , so since . Adding these inequalities,
Taking , which is a positive constant unless every is , and , the definition is satisfied.
Remark (The limit form).
For a reader who has met limits there is a shorter route to most statements. Suppose from some point on and the ratio tends to a finite limit,
Then : beyond some the ratio stays below , so meets the definition. The cubic example is settled in one line this way, since , and so is the tight bound, since .
The converse fails only because the ratio need not converge at all, and replacing the limit by the limit superior repairs it: holds exactly when
A limit of says more than does. It says that is negligible against rather than merely bounded by a multiple of it, and that stronger relation is written ; so , while is not .
Remark (What $O$ does and does not see).
The notation is insensitive to scaling: if then for any non-zero constants and . It is equally insensitive to any finite initial stretch of the two functions. What it describes is therefore the asymptotic behaviour of and , their behaviour for large inputs rather than at any one input. Constant factors depend on the machine and the compiler, which we do not model, so this is the level of precision we want; for the same reason an bound alone does not decide which of two programs to run on a given machine.
The Cost of Schoolbook Arithmetic
We all learned at school how to add and multiply natural numbers written in decimal: once and are known for the single digits , sums and products of arbitrary numbers follow by working column by column and carrying. These schoolbook methods are algorithms in the sense of the first chapter, and we can count their cost.
Remark (The base does not matter).
There is nothing special about ten here. The same algorithms work, with minor changes, in any base, binary included. Binary is convenient for multiplication, because the table of single-digit products that has to be known in advance is very much smaller.
Throughout this section the size of the input is the number of digits, which for written in base is by Corollary 2.22.
Proposition 3.23 (Cost of schoolbook addition).
Fix a base . Computing by the schoolbook algorithm takes elementary operations, where is the larger of the numbers of base- digits of and of .
Discussion.
The algorithm treats one column at a time, and each column costs at most a fixed amount, however large the numbers are. A single column adds three quantities: the digit of there, the digit of there, and the carry coming in from the column to its right. The first two lie in and the third is or , so there are at most possible columns to deal with, a number fixed once is fixed and independent of . So each column costs at most some constant , there are columns and at most one extra step to write a final carry, and the count is . Padding the shorter number with leading zeros makes both numbers digits long.
Proof.
Write and in base , padding the shorter expansion with leading zeros so that both have digits.
The algorithm works through the columns from right to left. At each column it adds three quantities: the digit of in that column, the digit of in that column, and the carry from the preceding column. The two digits lie in and the carry is or , so there are finitely many possible single-column computations, their number depending on alone. Since is fixed, there is a constant such that every single-column computation takes at most elementary operations.
The algorithm performs one such computation for each of the columns and at most one further step to write a final carry, so its running time is at most . By the proposition on polynomials this is .
No algorithm does better than here, since the output has about digits and writing it down takes that long. Schoolbook multiplication costs more.
Proposition 3.24 (Cost of schoolbook multiplication).
Fix a base . Computing by the schoolbook algorithm takes elementary operations, with as above.
Discussion.
The algorithm has two stages, bounded separately. In the first it forms one partial product for each digit of : multiplying the whole of by a single digit is a column-by-column pass of the kind already costed, so it is , and there are digits of , giving . Shifting a partial product left only decides where its digits are written. In the second stage the partial products are added, and each addition involves numbers of at most digits, so by the previous proposition each costs and the of them cost . Two stages of make .
Proof.
Write and in base , padded to digits each.
The algorithm forms partial products, one for each digit of . Forming the one belonging to means multiplying by each of the digits of , one column at a time and carrying where needed, and then shifting the result places to the left to obtain . Each single-digit product with a carry is one of finitely many computations depending on alone, so each partial product costs and all of them cost .
It then adds the partial products. Each is at most digits long, so by the previous proposition each addition costs , and of them cost .
Both stages are , so there are constants bounding each by a multiple of beyond some point; adding the two bounds gives a constant bounding the total, and the running time is .
Remark (Faster multiplication).
Multiplication algorithms asymptotically better than the schoolbook one do exist, though they are a great deal more complicated. The first was found by Karatsuba in 1960 and multiplies two -digit numbers in operations with . More recently Harvey and van der Hoeven gave an algorithm, which is believed to be the best possible.
This last result also shows a limitation of the notation. The running time is bounded by for some constant , but is very large: the algorithm beats its rivals only on enormous inputs, and at the sizes that arise in practice methods with worse asymptotic behaviour and smaller constants are faster.
The Cost of Trial Division
Remark (The cost of the obvious method).
The definition of primality suggests testing, for a given , every pair with to see whether . That is up to multiplications. Today’s machines perform several billion operations a second, and even so an eight-digit input on this method would occupy one of them for several hours.
Trial division does much better.
Proposition 3.25 (Cost of trial division).
Deciding whether is prime by testing every candidate divisor from to takes elementary operations.
Discussion.
Each pass of the loop performs a fixed amount of work, one remainder and one comparison, both elementary, so the running time is a constant multiple of the number of passes. The loop runs over the integers from to and may stop early at a divisor, so the count is at most ; the floor is at most , and the constants are absorbed by the . That stopping at rather than at is correct is the proposition on small divisors.
Proof.
By the proposition on small divisors, is composite exactly when some with divides it, so the loop over decides the question. It makes at most passes, and each pass computes one remainder and one comparison, which is a bounded number of elementary operations. The total is at most , which is .
Remark (Avoiding the square root).
It is not obvious that can itself be computed with an elementary operation, and the algorithm need not compute it. Increasing the candidate by and stopping as soon as tests exactly the same candidates and uses only multiplication and comparison. Every step of the algorithm is then well defined for every admissible input, the algorithm stops after finitely many steps, and it returns the right answer, so it computes the function taking the value yes exactly at the primes. We write or for the two values as well.
Remark (Fast in $n$, slow in the input).
An bound looks good, but is not the size of the input. The instance handed to the algorithm is the digit string of , whose length is , so is about and is about . Measured against the length of its input, trial division takes exponentially many steps, so a three-hundred-digit number is out of its reach at any realistic speed.
Give the running time of each of the following in notation, as a function of , and justify each answer.
- Summing the integers from to with a loop.
- Summing the integers from to with the closed form.
- Printing every pair with .
- Extracting the base- digits of by repeated division.
A product may be computed by adding to a running total times, using no multiplication at all. Let and have digits in base .
- Give the running time of this method as a function of and .
- Evaluate that count and the of the schoolbook algorithm at for , and . On a machine performing operations a second, say for which of the three the repeated-addition method is still usable.
- Both methods perform additions of -digit numbers, and only one of them is out of reach for a twenty-digit input. Say which quantity in your answer to the first part is responsible, and why measuring the input by rather than by is what makes the difference visible.
Two programs settle the same problem on inputs of size . The first performs elementary operations, the second .
- Find every at which the second is the faster, and the size at which the first overtakes it.
- Give the running time of each in notation, and say what those two statements do and do not tell you about which program to run.
- A machine performs operations a second and no input ever exceeds . Which program should be run, and how long does it take?
Search and Approximation
The loops of the previous chapters mostly checked something: whether any candidate divides , whether every character of a string is a digit. The loops of this chapter search for a value: they try candidates until one works.
Exhaustive Enumeration
The simplest search strategy is the one the primality test already used: try every candidate in some collection until one works. When the collection is finite, or can be made finite by bounding the range, this is exhaustive enumeration, and it finds a solution whenever one exists.
Cube Roots
Suppose we want the integer cube root of a perfect cube. Given an integer , we seek an integer with , or a report that no such integer exists.
The strategy is direct: test in order until either , a success, or , a failure, the latter being conclusive because implies for non-negative integers. For negative the cube root is the negative of the cube root of .
n = 27
x = 0
while x ** 3 < abs(n):
x = x + 1
if x ** 3 != abs(n):
print(f'{n} is not a perfect cube')
else:
if n < 0:
x = -x
print(f'Cube root of {n} is {x}')
# Cube root of 27 is 3
Hand simulating for :
| Test evaluation | x | x ** 3 | x ** 3 < 27 |
|---|---|---|---|
| 1st | 0 | 0 | True |
| 2nd | 1 | 1 | True |
| 3rd | 2 | 8 | True |
| 4th | 3 | 27 | False |
The loop stops with , and since the program reports a cube.
Trace the program above for , and . In each case build the hand-simulation table, state the value of x when the loop stops, and say whether the program reports a perfect cube.
Example 3.26 (The speed of exhaustive search).
Take . Its cube root is , so the loop makes passes and finishes at once. Take instead , whose cube root is : the loop makes nearly two million passes and still finishes in well under a second. At billions of instructions a second, a million passes take a fraction of a second.
Termination and Decrementing Functions
Every loop in a correct program must stop, unless it is deliberately endless. To prove that one does, we exhibit a quantity that strictly decreases at every pass and cannot go below zero.
Definition 3.27 (Decrementing function).
A decrementing function for a loop is an integer-valued expression in the program’s variables such that
- whenever the loop test holds, and
- strictly decreases at every pass of the body.
A loop admitting a decrementing function stops after at most passes, where is the initial value of .
With it, “the loop eventually stops” can be proved. For the cube-root search a suitable choice is , written with the ceiling of the last lesson, which may also be had from the floor as .
Theorem 3.28 (Termination of the cube-root search).
The exhaustive cube-root search stops for every integer .
Discussion.
The variable increases rather than decreases, so it is not itself a decrementing function; what decreases is the distance from to the value at which the loop stops, and the loop stops once reaches , that is, once reaches . Since is an integer this value is rounded up, which is where the ceiling enters. The two conditions of the definition then have to be checked separately: non-negativity comes from the loop test, since the test holding means has not yet reached the target, and the strict decrease comes from the body, since the body adds exactly to and does not change the target.
Proof.
Put . Initially , so .
Suppose the loop test holds. Then , and both sides being integers gives , so in particular .
Each pass replaces by and changes nothing else, so falls by exactly . Being a non-negative integer that falls by each pass, can do so at most times, and the loop stops after at most that many passes.
Note (Decrementing functions as a diagnostic).
When a program appears to run forever, the definition suggests where to look. Identify the quantity that ought to be decreasing; if there is none, the loop has no decrementing function, which suggests that the loop may never end. Otherwise print the candidate at every pass, and if the printed values fail to fall, the fault is in the body. Deleting x = x + 1 from the cube-root search, for instance, leaves at every pass, constant instead of decreasing.
Remark (The cost of exhaustive enumeration).
Exhaustive enumeration is correct and simple, but it can be slow. The cube-root search tests candidates, so its running time is . For that is passes, which is quick; for it is passes, minutes or hours. Bisection search needs about a hundred steps for the same .
Approximate Solutions
The cube-root search demands an exact integer answer, so it works only on perfect cubes. Many numerical problems have no exact answer in the integers, or even in the rationals: is irrational, and no finite program returns it. We ask instead for an approximate answer, within a stated tolerance.
Definition 3.29 (-approximation).
Let be a function, a target value and a prescribed tolerance. An -approximation to a solution of is a value with
The tolerance is chosen by whoever writes the program. A smaller demands a more accurate answer, and can take much more computation.
Exhaustive Search for Square Roots
Exhaustive enumeration adapts to the approximate setting. Rather than the integers we test the values for a small step , and accept the first with .
n = 25
epsilon = 0.01
step = 0.0001
guess = 0.0
num_guesses = 0
while abs(guess ** 2 - n) >= epsilon and guess <= n:
guess = guess + step
num_guesses = num_guesses + 1
if abs(guess ** 2 - n) >= epsilon:
print(f'Failed to find sqrt({n})')
else:
print(f'{guess} is close to sqrt({n})')
print(f'Number of guesses: {num_guesses}')
# 4.999000000001688 is close to sqrt(25)
# Number of guesses: 49990
Roughly candidates are tested before the neighbourhood of is reached, here of them. The answer is not : is within of , which is all that was asked.
Example 3.30 (When the search space misses the answer).
Run the same program with :
n = 0.25
epsilon = 0.01
step = 0.0001
guess = 0.0
while abs(guess ** 2 - n) >= epsilon and guess <= n:
guess = guess + step
if abs(guess ** 2 - n) >= epsilon:
print(f'Failed to find sqrt({n})')
# Failed to find sqrt(0.25)The search fails because , while the guard guess <= n stops it at . The guard was written for , where ; for we have and the upper bound has to be raised.
Example 3.31 (When the step is too large).
Now take with the same . The program runs a long time and then reports failure: the step carries it over every value within of without ever landing on one. Shrinking to repairs that and obliges the program to test some candidates. Starting nearer the answer would help, and presumes we already know roughly where the answer is.
The step controls both the accuracy and the running time, in opposite directions: a smaller step is more accurate and slower.
Bisection Search
Exhaustive enumeration does not use whether was too small or too large. Bisection uses that comparison to discard half the remaining candidates at every step.
Looking up a word in a dictionary works the same way: open it near the middle, and if the word comes later than the page shown, discard the first half and open the second half near its middle.
We state the algorithm on an interval, in the notation of the first lesson.
Definition 3.32 (Bisection search).
Let preserve order on , so that implies throughout. Bisection search solves by maintaining the invariant that a solution lies in and halving the interval:
- compute the midpoint ;
- if is too large, replace by ; if is too small, replace by ;
- repeat until .
After steps the interval has width , where is its initial width.
Implementation
n = 25
epsilon = 0.01
low = 0.0
high = max(1.0, n)
guess = (low + high) / 2.0
num_guesses = 0
while abs(guess ** 2 - n) >= epsilon:
num_guesses = num_guesses + 1
if guess ** 2 < n:
low = guess
else:
high = guess
guess = (low + high) / 2.0
print(f'{guess} is close to sqrt({n})')
print(f'Number of guesses: {num_guesses}')
# 5.00030517578125 is close to sqrt(25)
# Number of guesses: 13
Exhaustive enumeration needed about fifty thousand guesses; bisection needs thirteen. The upper end is max(1.0, n) rather than n, so that lies in whether or , which repairs the failure on .
Example 3.33 (Hand simulating the bisection).
The first four steps for on :
| Step | low | high | guess | guess ** 2 | Action |
|---|---|---|---|---|---|
| 0 | 0.0 | 25.0 | 12.5 | 156.25 | too high, high = 12.5 |
| 1 | 0.0 | 12.5 | 6.25 | 39.0625 | too high, high = 6.25 |
| 2 | 0.0 | 6.25 | 3.125 | 9.765625 | too low, low = 3.125 |
| 3 | 3.125 | 6.25 | 4.6875 | 21.972656 | too low, low = 4.6875 |
Four steps have taken the interval from width to width , and eight more bring it below .
Example 3.34 (Bisection on a larger input).
Exhaustive approximation failed on because no step served: too large and it skipped the root, too small and it needed hundreds of millions of guesses. Bisection starts from and, by the theorem below, needs at most guesses to narrow the interval that far. Run, it stops after , the excess coming from the test being on rather than on the width of the interval.
Convergence
Bisection produces guesses , and each step halves the interval holding the root, so the distance from the guess to the root falls by a factor of two every time. The bound this gives is stated with the logarithm to base , written : the power to which must be raised to give , so that and , and for an that is not a power of two.
Theorem 3.35 (Convergence of bisection search).
Let be the initial interval and the tolerance. Bisection search narrows the interval below after at most steps.
Discussion.
The proof follows the width of the interval. One step replaces the interval by one of its two halves, so the width is halved whichever half survives, and after steps it is the initial width divided by . It remains to find the least for which this is below , by taking of both sides; the ceiling appears because counts steps and must be an integer. The argument resembles the one for decrementing functions, with a width halved at each step in place of a counter decreased by one, and so the number of steps is logarithmic rather than linear.
Proof.
Each step replaces by either or with the midpoint, so the new width is half the old one. After steps the width is therefore , and since the midpoint of an interval of width is within of every point of it, the guess is within of the root.
We need , that is , that is
The least integer meeting this is .
Example 3.36 (Checking the bound).
For on with the theorem gives
and the program used : the test is on rather than on the width, and floating-point rounding can add a step.
Remark (Logarithmic against linear).
Exhaustive enumeration with step tests about candidates; bisection tests about . For and that is of the order of guesses against roughly . Doubling the range doubles the work of the first and adds a single step to the second.
Adapt the bisection program to approximate to within . How many guesses does it need? Compare that with the number exhaustive enumeration with step would need, and with the bound of the theorem.
Machine Arithmetic
The methods above assume that arithmetic on reals is exact: that is , that halving an interval halves it, and that the only error is the tolerance we chose. On a machine none of these holds exactly.
Binary Fractions
A computer stores numbers in binary. Just as the decimal system writes fractions with negative powers of ten, so that , the binary system uses negative powers of two:
Example 3.37 (Exact binary fractions).
The decimal has an exact binary form:
Likewise and . These are exact because their denominators are powers of two: , and .
Not every decimal fraction has a finite binary form.
Theorem 3.38 (One tenth is not a finite binary fraction).
The number has no finite binary representation.
Discussion.
A finite binary fraction has a power of two as its denominator, and the proof compares that with the factor in . Suppose the representation existed with bits. Multiplying it through by clears every denominator at once and leaves an integer on the right, so the supposed identity becomes for an integer . The right-hand side is divisible by and the left is a power of two, and no power of two has among its factors. The same argument gives the general case: a fraction in lowest terms is a finite binary fraction exactly when its denominator is a power of two.
Proof.
Suppose for some bits . Then
Cross-multiplying gives , so divides . But is a product of factors of , and is a prime different from , so divides no power of . The supposition is therefore false.
The same holds in general: a rational in lowest terms has a finite binary expansion exactly when is a power of two, and since , the fraction needs an infinite repeating binary expansion, just as repeats for ever in decimal.
What This Costs in Practice
Python’s float uses the IEEE 754 double-precision format: bits carrying a sign, an -bit exponent and a -bit significand with one further bit implied. That is about to significant decimal digits.
The consequence is that a decimal constant which looks exact in the source is silently rounded to the nearest representable binary fraction. Putting the format specifier :.20f inside an f-string prints twenty digits after the point and exposes it:
print(0.1) # 0.1 (the display is rounded)
print(f'{0.1:.20f}') # 0.10000000000000000555
print(0.1 + 0.1 + 0.1 == 0.3) # False
print(f'{0.1 + 0.1 + 0.1:.20f}') # 0.30000000000000004441
print(f'{0.3:.20f}') # 0.29999999999999998890
The sum overshoots by about , while the literal 0.3 is itself rounded downwards from three tenths. The two errors go opposite ways, so == returns False.
Example 3.39 (A loop that never lands).
Counting from to in steps of a tenth:
x = 0.0
count = 0
while x != 1.0:
x = x + 0.1
count = count + 1
if count > 20:
print('Gave up')
breakThe loop never stops of its own accord. After ten additions x is 0.9999999999999999, not 1.0; the eleventh takes it to 1.0999999999999999, and it has stepped over 1.0 without touching it. Replacing != by < 1.0, or by abs(x - 1.0) >= epsilon, repairs it.
Remark (Never compare floats with $==$).
Do not test floating-point numbers for exact equality. The expression x == 0.3 is almost certainly wrong even when x was computed by a formula mathematically equal to three tenths. Test instead that the difference is small:
x = 0.1 + 0.1 + 0.1
print(abs(x - 0.3) < 1e-9) # TrueThis is an -approximation applied to equality; the tolerance should match the precision of the computation, not of the display.
Example 3.40 (Rounding error accumulates).
Adding a tenth a thousand times:
total = 0.0
for i in range(1000):
total = total + 0.1
print(f'{total:.20f}') # 99.99999999999859312538
print(total == 100.0) # False
print(abs(total - 100.0)) # about 1.4e-12Each addition contributes a rounding error of the order of , and over a thousand additions they accumulate to about : small, but enough to make an exact test fail.
Remark (When it matters).
For the bisection search this error is harmless: an approximate answer was wanted, and is much larger than the rounding. The problem arises in code that assumes exact arithmetic, by testing x == 0.0 where it should test abs(x) < epsilon, or by expecting a counter incremented by 0.1 to arrive exactly at 1.0 after ten steps.
The theorem above rules out , and the paragraph following it makes the general claim: a rational in lowest terms has a finite binary expansion exactly when is a power of two. Prove both directions, following the argument of the theorem. Then say how many bits the expansion of needs when is odd, and give the expansions of and as far as each can be written.
The loop of the example above adds a thousand times and lands about short of .
- Write a second program that adds the integer a thousand times and divides by ten at the end, and compare its result with
100.0using==. - Say why the second is exact where the first is not, in terms of which numbers have a finite binary expansion.
- A sum of money is to be accumulated over many transactions, each an exact number of pounds and pence. Say which of the two arrangements should be used, and what the other would cost after a million transactions.
Exercises on Types and Expressions
Give the type and the value of each expression.
7 / 2;7 // 2;7 % 2;7.0 // 2;2 ** 0.5;1 == 1.0.
State the value of 2 ** 3 ** 2, (2 ** 3) ** 2, -2 ** 2 and (-2) ** 2, and say which rule of precedence or associativity settles each one.
Predict the value of 0.1 + 0.2 == 0.3 and of 0.5 + 0.25 == 0.75, then check both. Explain why the two come out differently, and give another pair of float values whose sum can be tested exactly.
Let n be a positive integer.
- Write an expression for its last two decimal digits.
- Write an expression for the digit in its hundreds place.
- Write an expression that is
Trueexactly whennis a multiple of but not of .
Give the value of int('101', b) for and , and find the base for which it equals .
Explain why x != 0 and 100 % x == 0 may be evaluated for any integer x, while 100 % x == 0 and x != 0 may not.
Explain what a, b = b, a does, and why performing a = b and then b = a does not do the same. Say what the second pair leaves in a and b.
Exercises on Branching
Write a program that reads three integers and prints them in increasing order, using conditional statements only and no built-in sorting.
A year is a leap year when it is divisible by , except that centuries are not, except that those divisible by are. Write a program that reads a year and reports whether it is a leap year, and check it on , , and .
Write a program that reads a real number x and prints which of the intervals , , and contains it.
Write a program that reads three numbers a, b and c and prints how many of them are strictly positive. Use branching only, with no loop and no arithmetic on the results of the tests.
The expressions 1/x if x != 0 else 0 and (x != 0) * (1/x) agree for every non-zero x. Say what each does when x is 0, and explain the difference in terms of which arms of a case split get evaluated.
Explain why a branching program of statements performs at most atomic operations on any input, and give a three-statement program whose count of operations depends on its input.
Write a program that reads three positive reals and reports whether they can be the side lengths of a triangle, that is, whether each of them is smaller than the sum of the other two. Use conditional statements only.
Exercises on Iteration
Write a program that computes for a given with a for loop, and a second that does the same with a while loop. State how many multiplications each performs, and say what each returns for .
Write a program that counts the digits of in base with a loop, for and . Check its count against Corollary 2.22 for several and .
Write a program that sums the decimal digits of a positive integer using // and % only, with no strings. Extend it to repeat the process on the result until a single digit is left, and compare that digit with .
Write a program that prints the first Fibonacci numbers, using a single multiple assignment inside the loop to advance the pair.
Write a program that takes positive integers and and repeatedly replaces the pair by the smaller number and the remainder of the larger on division by it, stopping when the remainder is . Hand simulate the loop on , and identify the surviving number as the largest integer dividing both and .
Write a program that prints every pair with and , for a given . Say how many divisibility tests it performs as a function of .
Modify the trial-division program so that, when is composite, it also prints the least divisor of above . Run it on three numbers near one million of your own choosing, and say which of them are prime.
Write a program that converts a string of base- digits to an integer with a loop, for a given , using Horner’s scheme rather than forming any power of . Count the multiplications, and check the result against int.
Write a program that runs the loop replacing by when is even and by when is odd, stopping when reaches , and counts its passes. Run it on each starting value from to , and report which takes the most.
Exercises on Running Time
Show directly from the definition that , giving an explicit and , and that as well, so that each of the two is of the other. Then do both again with the limit form.
Decide which of the following hold, with a proof or a counterexample in each case.
- ;
- ;
- ;
- .
Suppose and . Prove that and that for every constant . Then give functions with and for which fails, so that is closed under sums and constant multiples but not under products.
An input of decimal digits denotes a number of size about . Express the running time of trial division as a function of the number of digits of its input rather than of the number itself, and say how many digits a number may have before an algorithm performing operations a second needs more than an hour.
Count the elementary operations performed by the schoolbook algorithms on two -digit numbers exactly, rather than up to : give the number of single-column additions performed by the addition, and the number of single-digit multiplications performed by the multiplication.
Exercises on Search and Approximation
Give a decrementing function for each of the following loops and state the bound on the number of passes it yields.
while n >= 10: n = n // 10, for ;while a != b: a, b = (a - b, b) if a > b else (a, b - a), for positive integers and ;- the exhaustive square-root search of this chapter, whose body is
guess = guess + step.
The positions of a string are numbered from . Write a program that prints the characters of a string my_str at the even positions, so that 'abcdefg' produces aceg. Write it twice, once with range and indexing, and once with a for loop over the characters and a counter.
Let be an integer with . Write a program that finds by bisection search, printing the number of guesses it needed and the value found. When the midpoint of the current interval falls between two integers, take the smaller.
A positive integer is a perfect power if for integers and . Write a program that reads and prints integers root and pwr with and , or reports that no such pair exists. Test it on , which admits , and , on , and on .
Write a program that approximates for a positive real to within by bisection. It must first find an interval containing : note that , that grows without bound, and that becomes arbitrarily small, so may have to be negative. Test it on , and .
Run the exhaustive square-root search and the bisection search on the same for and , recording the number of guesses each makes. Say which of the two counts grows with and which with , and check the readings against the two bounds.
Find two float values a and b for which (a + b) + c and a + (b + c) differ for some c, and explain the difference in terms of rounding. What does this say about summing a list of numbers in different orders?
Which of the decimals , , , and are exact as float values? Predict the answer from the theorem on binary fractions before checking each with :.20f.
An Applied Exercise
A band puts tickets on sale for one night and prices them dynamically. The first ticket costs . The tickets are sold in blocks of , and after each block the price is raised by of the current price. The band will not charge more than a cap of : once a rise would take the price above the cap, the price is set to the cap and stays there for every remaining block.
- Write a program that sells all tickets under this rule and prints the price of each block together with the total revenue. Report the revenue.
- Give, in terms of , and , the number of rises after which the uncapped price would first exceed the cap, and hence the number of the first block sold at the cap. Check your formula against the output of your program.
- Consider the sequence of price increases between consecutive blocks. Show that before the cap binds these form a geometric sequence and give its ratio. Exactly one increase belongs to neither the geometric stretch nor the capped stretch: identify it, give its value, and say what it would have been without the cap.
- Show that once the cap binds the total revenue is a linear function of the number of tickets sold, and give its slope. Say what the revenue would look like as a function of if the cap were removed.
- Give the running time of your program in notation as a function of and , and say which of the two the running time really depends on.
- Prices in pounds are not exact
floatvalues. Rewrite the simulation in integer pence, rounding each new price down to the nearest penny, and compare the two totals. The two disagree for two separate reasons; say which reason accounts for most of the gap, and which of the two totals the band should quote.
Check Yourself
Fresh questions on the whole lesson — none of them is worked out above. Work each one out on paper or in your head 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.
What is the type of 5 % 2 == 1?
What is the value of 7 // -2?
What is the value of -7 % 3?
What is the value of 9 // 2 ** 2?
After total = 0 and then total += 5 twice, what is total?
What is the value of 2 + 3 * 4 ** 2?
After x, y = 1, 2 and then x, y = y, x + y, what is y?
After x = 5, y = x and x = 7, what is y?
How many values does range(5, 20, 4) produce?
What is the last value produced by range(10, 3, -2)?
In a for i in range(3) loop whose body is a for j in range(i) loop, how many times does the inner body run in total?
Starting from n = 4096, how many times does the body of while n >= 10: n = n // 10 run?
Testing for primality by trial division, what is the largest candidate divisor that has to be tried?
What is the value of int('ff', 16)?
What are the binary digits of ?
Testing whether is prime by trial division up to has which running time?
Multiplying two -digit numbers by the schoolbook algorithm has which running time?
How many bisection steps are needed to narrow to a width below ?
Starting from x = 0, how many times does the body of while x ** 3 < 64: x = x + 1 run?
What is the value of (3 > 2) or (1 / 0 == 0)?