CMPUT-272 · Lesson 8 of 12 · 32 min
Functions and cardinality
A function in mathematics is a rule that assigns to each input exactly one output. Programming functions are named after this idea, though they differ in important ways. This lesson defines functions precisely, classifies them as injective, surjective or bijective, introduces composition and inverses, proves the pigeonhole principle, and then uses functions to compare the sizes of sets, including infinite ones. The surprising result at the end, that some infinite sets are larger than others, underlies the limits of computation studied in CMPUT-474.
Functions as sets of pairs
A function \(f\) from a set \(A\) to a set \(B\), written \(f: A \to B\), assigns to each element \(a \in A\) exactly one element \(f(a) \in B\). The set \(A\) is the domain, \(B\) is the codomain, and the set of values actually taken, \(\{f(a) \mid a \in A\}\), is the range or image. Formally, a function is a subset of \(A \times B\) in which every \(a \in A\) appears as the first coordinate of exactly one pair. Two conditions are packed into that definition: every input has an output (the function is total), and no input has two outputs (the function is well defined).
A Python dictionary is a finite function: its keys are the domain and each key maps to exactly one value. A Python function that always returns a value and has no side effects also behaves like a mathematical function. Programming functions can break both conditions: a function that raises an exception for some inputs is not total, and one that reads a clock or a random generator can return different outputs for the same input, so it is not a function in the mathematical sense.
| Rule | Function from ℝ to ℝ? | Reason |
|---|---|---|
| f(x) = x² | Yes | Every real has exactly one square |
| f(x) = 1/x | No | No value at x = 0 (not total); it is a function on ℝ − {0} |
| f(x) = ±√x | No | Two outputs for x > 0 and none for x < 0 |
| f(x) = ⌊x⌋ | Yes, into ℤ | The floor of x is the greatest integer at most x |
| f(x) = the current time plus x | No | The output changes even for the same x |
Injective, surjective and bijective
A function \(f: A \to B\) is injective (one-to-one) if different inputs give different outputs: \(f(a_1) = f(a_2) \to a_1 = a_2\). It is surjective (onto) if every element of the codomain is hit: \(\forall b \in B\, \exists a \in A\, (f(a) = b)\). It is bijective if it is both. A bijection pairs the elements of \(A\) and \(B\) perfectly, with nothing left over on either side.
def injective(f, A):
images = [f(a) for a in A]
return len(images) == len(set(images))
def surjective(f, A, B):
return {f(a) for a in A} == set(B)
A = range(6)
B = range(6)
tests = {
"x -> 2x mod 6": lambda x: (2 * x) % 6,
"x -> x + 1 mod 6": lambda x: (x + 1) % 6,
"x -> 5x mod 6": lambda x: (5 * x) % 6,
"x -> x*x mod 6": lambda x: (x * x) % 6,
}
for name, f in tests.items():
print(f"{name:16s} injective={injective(f, A)!s:5s} surjective={surjective(f, A, B)}")x -> 2x mod 6 injective=False surjective=False x -> x + 1 mod 6 injective=True surjective=True x -> 5x mod 6 injective=True surjective=True x -> x*x mod 6 injective=False surjective=False
Proofs of these properties follow the definitions directly. To prove injectivity, assume \(f(a_1) = f(a_2)\) and deduce \(a_1 = a_2\). To prove surjectivity, take an arbitrary \(b\) in the codomain and find an \(a\) with \(f(a) = b\), usually by solving the equation. To disprove either, give a counterexample.
Worked problem
Proving a function is a bijection
Prove that f: ℝ → ℝ, f(x) = 3x − 7, is a bijection.
Show solution
- Injective: suppose 3a − 7 = 3b − 7. Adding 7 and dividing by 3 gives a = b.
- Surjective: let y ∈ ℝ. Solve 3x − 7 = y to get x = (y + 7)/3, which is a real number, and f((y + 7)/3) = y.
- Both properties hold, so f is a bijection.
- The inverse is f⁻¹(y) = (y + 7)/3.
Answer: f is bijective with inverse (y + 7)/3. Over the integers, the same rule would not be surjective: no integer x gives 3x − 7 = 0.
The domain and codomain matter, as the last remark shows. The squaring function is not injective on \(\mathbb{R}\), since \((-2)^2 = 2^2\), but it is injective on the non-negative reals. It is not surjective onto \(\mathbb{R}\), since nothing squares to \(-1\), but it is surjective onto the non-negative reals.
Composition and inverses
If \(f: A \to B\) and \(g: B \to C\), the composition \(g \circ f: A \to C\) is defined by \((g \circ f)(a) = g(f(a))\): apply \(f\) first, then \(g\). Composition is how programs are built from smaller functions, and it is associative but not commutative in general. If \(f(x) = x + 1\) and \(g(x) = 2x\), then \(g(f(3)) = 8\) but \(f(g(3)) = 7\).
A function \(f: A \to B\) has an inverse \(f^{-1}: B \to A\) with \(f^{-1}(f(a)) = a\) and \(f(f^{-1}(b)) = b\) exactly when \(f\) is a bijection. Injectivity ensures each output came from only one input, so the inverse is well defined; surjectivity ensures every \(b\) has some input to return, so the inverse is total. Encryption and decryption, encoding and decoding, and compression and decompression are inverse pairs, and lossless compression works only because the compression function is injective on the files it handles.
# A Caesar shift is a bijection on the 26 letters; its inverse shifts back.
import string
L = string.ascii_lowercase
shift = lambda k: (lambda c: L[(L.index(c) + k) % 26])
enc, dec = shift(3), shift(-3)
msg = "discrete"
c = "".join(enc(ch) for ch in msg)
print(msg, "->", c, "->", "".join(dec(ch) for ch in c))
print("enc is injective on letters:", len({enc(ch) for ch in L}) == 26)discrete -> glvfuhwh -> discrete enc is injective on letters: True
The pigeonhole principle
If more than \(n\) objects are placed into \(n\) boxes, some box contains at least two objects. Stated with functions: if \(|A| > |B|\) and both are finite, no function \(f: A \to B\) is injective. The principle sounds trivial, but it proves surprising facts. The generalized form says that if \(N\) objects go into \(k\) boxes, some box contains at least \(\lceil N/k \rceil\) objects.
Worked problem
Pigeonhole in a hash table
A hash function maps 1,000 keys into a table of 365 buckets. Show that some bucket receives at least 3 keys.
Show solution
- Objects: 1,000 keys. Boxes: 365 buckets.
- Generalized pigeonhole: some bucket has at least ⌈1000/365⌉ keys.
- 1000/365 ≈ 2.74, so the ceiling is 3.
- No hash function, however clever, can avoid this; collisions are a mathematical certainty once keys outnumber buckets.
Answer: Some bucket holds at least 3 keys. This is why every hash table needs a collision strategy.
Worked problem
A less obvious pigeonhole argument
Prove that among any five integers chosen from 1 to 8, two of them sum to 9.
Show solution
- Group 1 to 8 into four boxes of pairs summing to 9: {1, 8}, {2, 7}, {3, 6}, {4, 5}.
- Five chosen integers go into four boxes.
- By pigeonhole, some box contains two of the chosen integers.
- Those two sum to 9.
Answer: Proved. The creative step is choosing the boxes; once chosen, the principle does the rest.
The pigeonhole principle also explains why no lossless compressor can shrink every file. There are \(2^n\) files of exactly \(n\) bits, but only \(2^n - 1\) files of fewer than \(n\) bits (counting the empty file). A compressor that made every \(n\)-bit file shorter would map \(2^n\) inputs into \(2^n - 1\) outputs, so two different files would compress to the same output and could not both be recovered.
import random
random.seed(1)
buckets = 365
keys = [random.getrandbits(32) for _ in range(1000)]
counts = [0] * buckets
for k in keys:
counts[k % buckets] += 1
print("largest bucket:", max(counts), " guaranteed at least:", -(-1000 // buckets))
# birthday-style: how many random keys until the first collision?
seen, n = set(), 0
while True:
n += 1
h = random.getrandbits(32) % buckets
if h in seen:
break
seen.add(h)
print("first collision after", n, "keys (pigeonhole guarantees one by key 366)")largest bucket: 10 guaranteed at least: 3 first collision after 9 keys (pigeonhole guarantees one by key 366)
Cardinality and infinite sets
For finite sets, the cardinality \(|A|\) is the number of elements. Two finite sets have the same size exactly when there is a bijection between them. Georg Cantor took that as the definition for all sets: \(A\) and \(B\) have the same cardinality if there is a bijection between them. For infinite sets this leads to results that defy intuition.
A set is countably infinite if it has the same cardinality as \(\mathbb{N}\), meaning its elements can be listed in a sequence \(a_0, a_1, a_2, \ldots\) that eventually reaches every element. The even natural numbers are countable via \(n \mapsto 2n\), even though they seem to be half of \(\mathbb{N}\). The integers are countable via the listing \(0, 1, -1, 2, -2, 3, -3, \ldots\). Even the rational numbers are countable: arrange the fractions \(a/b\) in a grid and walk along the diagonals, skipping repeats.
from math import gcd
def rationals():
"""Yield every positive rational exactly once by walking diagonals a + b = s."""
s = 2
while True:
for a in range(1, s):
b = s - a
if gcd(a, b) == 1:
yield f"{a}/{b}"
s += 1
gen = rationals()
print([next(gen) for _ in range(20)])
z = lambda n: (n + 1) // 2 if n % 2 else -(n // 2) # bijection N -> Z
print([z(n) for n in range(11)])['1/1', '1/2', '2/1', '1/3', '3/1', '1/4', '2/3', '3/2', '4/1', '1/5', '5/1', '1/6', '2/5', '3/4', '4/3', '5/2', '6/1', '1/7', '3/5', '5/3'] [0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5]
Cantor's diagonal argument
Are all infinite sets countable? Cantor proved in 1891 that they are not. The set of all infinite binary sequences, equivalently the real numbers between 0 and 1 written in binary, is uncountable. The proof is by contradiction and is one of the most influential arguments in mathematics.
Worked problem
The diagonal argument
Prove that the set S of all infinite sequences of 0s and 1s is not countable.
Show solution
- Assume, for contradiction, that S is countable, so its elements can be listed s₀, s₁, s₂, ..., where sᵢ(j) is the j-th bit of sequence sᵢ.
- Build a new sequence d by flipping the diagonal: d(i) = 1 − sᵢ(i).
- d is an infinite binary sequence, so d ∈ S, and d must appear in the list as some s_k.
- But d(k) = 1 − s_k(k) ≠ s_k(k), so d differs from s_k in position k. Contradiction.
- No list can contain every sequence, so S is uncountable.
Answer: S is uncountable. The same argument shows the real numbers are uncountable.
The diagonal argument has a direct consequence for computing. Every program is a finite string over a finite alphabet, so the set of all programs is countable: list them by length, then alphabetically. But the set of all functions from \(\mathbb{N}\) to \(\{0, 1\}\), that is, all yes-or-no problems about natural numbers, is uncountable by the diagonal argument. Therefore some problems have no program that solves them. Alan Turing found a specific one, the halting problem, using a diagonal argument of the same shape; you will study it in CMPUT-474.
| Set | Cardinality | Reason |
|---|---|---|
| {0, 1, ..., 9} | 10 (finite) | Count the elements |
| ℕ, ℤ, even numbers | Countably infinite | Explicit bijections with ℕ |
| ℚ | Countably infinite | Diagonal listing of fractions |
| All Python programs | Countably infinite | Finite strings, listed by length |
| ℝ, all infinite bit sequences | Uncountable | Cantor's diagonal argument |
| All yes-or-no problems on ℕ | Uncountable | Same as infinite bit sequences |
Common mistakes
MistakeCalling a rule a function when some inputs have no output or two outputs.
InsteadCheck totality (every input has an output) and well-definedness (exactly one output).
MistakeIgnoring the codomain when testing surjectivity.
InsteadSurjectivity depends on the codomain: x² is onto [0, ∞) but not onto ℝ.
MistakeProving injectivity by checking a few pairs.
InsteadAssume f(a₁) = f(a₂) for arbitrary a₁, a₂ and derive a₁ = a₂.
MistakeWriting (g ∘ f)(x) = f(g(x)).
Insteadg ∘ f applies f first: (g ∘ f)(x) = g(f(x)).
MistakeThinking infinite sets all have the same size.
Insteadℕ and ℚ are countable, but ℝ is strictly larger.
MistakeUsing the pigeonhole principle with boxes that do not fit the problem.
InsteadChoose boxes so that two objects in the same box give the desired conclusion.
Summary and key terms
- A function f: A → B assigns exactly one output in B to every input in A.
- Injective: different inputs give different outputs. Surjective: every codomain element is hit. Bijective: both.
- g ∘ f means apply f, then g; a function has an inverse exactly when it is a bijection.
- Pigeonhole: N objects in k boxes force some box to hold at least ⌈N/k⌉.
- Sets have the same cardinality when there is a bijection between them.
- ℕ, ℤ and ℚ are countably infinite; ℝ and the set of infinite bit sequences are uncountable.
- Programs are countable but problems are not, so some problems cannot be solved by any program.
- Function
- A rule assigning exactly one element of the codomain to each element of the domain.
- Codomain
- The set B in f: A → B, which contains all possible outputs.
- Range
- The set of outputs actually taken by a function.
- Injective
- One-to-one: distinct inputs have distinct outputs.
- Surjective
- Onto: every element of the codomain is an output.
- Bijection
- A function that is both injective and surjective.
- Composition
- The function g ∘ f defined by (g ∘ f)(x) = g(f(x)).
- Pigeonhole principle
- If more than n objects go into n boxes, some box has at least two.
- Countably infinite
- Having a bijection with the natural numbers.
- Uncountable
- Infinite but not countable, like the real numbers.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Is f: ℤ → ℤ, f(n) = n + 5, a bijection?
Show answer
Yes; its inverse is n − 5.
Is f: ℤ → ℤ, f(n) = 2n, surjective?
Show answer
No; odd integers are not hit.
Is f: ℝ → ℝ, f(x) = x³, injective?
Show answer
Yes; x³ = y³ implies x = y for reals.
If f(x) = x² and g(x) = x + 1, find (f ∘ g)(2) and (g ∘ f)(2).
Show answer
f(g(2)) = 9; g(f(2)) = 5.
How many people must be in a room to guarantee two share a birth month?
Show answer
13.
Show that in any group of 6 integers, two have the same remainder mod 5.
Show answer
There are 5 possible remainders, so by pigeonhole two of the 6 share one.
Give a bijection from ℕ to the odd natural numbers.
Show answer
n ↦ 2n + 1.
Why is the set of all finite strings over {a, b} countable?
Show answer
List them by length, then alphabetically within each length.
In the diagonal argument, why must d differ from every sᵢ?
Show answer
d differs from sᵢ in position i by construction.
A function from a 10-element set to a 7-element set: can it be injective? Surjective?
Show answer
Not injective by pigeonhole; it can be surjective.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.