Definition.f(n)=O(g(n)) if there exist constants c>0 and n0 such that f(n)≤c⋅g(n) for all n≥n0.
Definition.f(n)=Ω(g(n)) if there exist constants c>0 and n0 such that f(n)≥c⋅g(n) for all n≥n0.
Definition.f(n)=Θ(g(n)) if f(n)=O(g(n)) and f(n)=Ω(g(n)).
Definition.f(n)=o(g(n)) if limn→∞f(n)/g(n)=0.
Definition.f(n)=ω(g(n)) if limn→∞g(n)/f(n)=0.
Theorem 1.1.f(n)=O(g(n)) if and only if g(n)=Ω(f(n)).
Proof. Suppose f(n)=O(g(n)). Then there exist c,n0 such that f(n)≤c⋅g(n) for all n≥n0 Hence g(n)≥(1/c)⋅f(n) for all n≥n0 So g(n)=Ω(f(n)). The converse follows by symmetry. ■
Theorem 1.2.f(n)=Θ(g(n)) if and only if there exist constants c1,c2>0 and n0 such that c1⋅g(n)≤f(n)≤c2⋅g(n) for all n≥n0.
Proof. By definition, f(n)=Θ(g(n)) means f(n)=O(g(n)) and f(n)=Ω(g(n)). The former gives f(n)≤c2⋅g(n) for some c2>0 And the latter gives f(n)≥c1⋅g(n) for some c1>0. Combining yields the stated inequality. ■
Theorem 1.3 (Limit Rule). If limn→∞f(n)/g(n)=c where 0<c<∞ Then f(n)=Θ(g(n)). If c=0 Then f(n)=O(g(n)). If c=∞ Then g(n)=O(f(n)).
Proof. If c=0 Then for any ε>0 There exists n0 such that f(n)/g(n)<ε for all n≥n0 So f(n)≤ε⋅g(n) Establishing f(n)=O(g(n)). If 0<c<∞ Take ε=c/2; then (c/2)⋅g(n)≤f(n)≤(3c/2)⋅g(n) for sufficiently large n Giving Θ. The c=∞ case is symmetric. ■
Proposition 1.4. Asymptotic notation is transitive: if f=O(g) and g=O(h) Then f=O(h).
Proof. There exist c1,n1 with f(n)≤c1g(n) for n≥n1 And c2,n2 with g(n)≤c2h(n) for n≥n2. Then f(n)≤c1c2h(n) for n≥max(n1,n2). ■
Proposition 1.5.O-notation is reflexive: f=O(f) for all f.
Proof. Take c=1 and n0=1. Then f(n)≤1⋅f(n) for all n≥1. ■
Worked Example: Proving $n^2 + 3n + 1 = O(n^2)$
We must find c>0 and n0 such that n2+3n+1≤c⋅n2 for all n≥n0.
Note that n2+3n+1≤n2+3n2+n2=5n2 for all n≥1 (since n≥1 implies 3n≤3n2 and 1≤n2).
So take c=5 and n0=1.
To show tightness, note n2+3n+1≥n2 for all n≥0 So n2+3n+1=Θ(n2).
Worked Example: Proving $2^n \neq O(n^k)$ for any constant $k$
By the limit rule: limn→∞2n/nk=∞ for any fixed k (this follows from repeated application of L”Hôpital’s rule, or from the fact that log(2n)=nlog2 grows faster than log(nk)=klogn). Therefore 2n=ω(nk) for all k And in particular 2n=O(nk).
Worked Example: Sum of Harmonic Series and Its Use in Analysis
The k-th harmonic number is Hk=∑i=1k1/i. We show Hk=Θ(logk).
Therefore Hk=Θ(logk). This is used extensively in analysing algorithms like binary search on unbounded domains and the expected depth of elements in a random BST.
Worked Example: Analysing a Nested Loop Algorithm
Consider the algorithm:
for i = 1 to n:
for j = i to n:
constant-time work
The total number of iterations is ∑i=1n(n−i+1)=∑k=1nk=n(n+1)/2=Θ(n2).
Now consider:
for i = 1 to n:
j = 1
while j < n:
j = j * 2
constant-time work
The inner loop runs ⌊log2n⌋+1=O(logn) times, and the outer loop runs n times. Total: O(nlogn).
Now consider the triple loop:
for i = 1 to n:
for j = 1 to i:
for k = 1 to j:
constant-time work
Total iterations: ∑i=1n∑j=1ij=∑i=1n2i(i+1)=Θ(n3).
Many divide-and-conquer algorithms yield recurrences of the form:
T(n)=aT(n/b)+f(n)
Where a≥1 is the number of subproblems, b>1 is the factor by which the input is divided, and f(n) is the cost of dividing and combining.
Theorem 1.7 (Master Theorem). Let a≥1 and b>1 be constants, let f(n) be a function, and let T(n) be defined on the nonnegative integers by the recurrence T(n)=aT(n/b)+f(n) Where we interpret n/b to mean either ⌊n/b⌋ or ⌈n/b⌉. Let c=logba. Then:
If f(n)=O(nc−ε) for some ε>0 Then T(n)=Θ(nc).
If f(n)=Θ(nclogkn) for some k≥0 Then T(n)=Θ(nclogk+1n).
If f(n)=Ω(nc+ε) for some ε>0 And if af(n/b)≤qf(n) for some constant q<1 and all sufficiently large n (the regularity condition), then T(n)=Θ(f(n)).
Proof (Case 1 sketch). The recursion tree has depth logbn. At level i There are ai nodes, each costing f(n/bi). Since f(n)=O(nc−ε) The cost at level i is ai⋅(n/bi)c−ε=nc−ε⋅(a/bc−ε)i. The total cost is dominated by the leaves (level logbn), which contribute alogbn=nc. The internal levels contribute a geometric series with ratio a/bc−ε=bε>1 So the leaf level dominates. ■
Worked Example: Merge Sort Recurrence
Merge sort divides into 2 subproblems of size n/2 and combines in O(n) time.
T(n)=2T(n/2)+Θ(n)
Here a=2, b=2 So c=log22=1. We have f(n)=Θ(n)=Θ(nclog0n) Which is Case 2 with k=0.
Therefore T(n)=Θ(n1log1n)=Θ(nlogn).
Worked Example: Binary Search RecurrenceT(n)=T(n/2)+O(1)
Here a=1, b=2 So c=log21=0. We have f(n)=O(1)=O(n0). This matches Case 2 with k=0.
Therefore T(n)=Θ(logn).
Worked Example: Strassen's Matrix Multiplication
Strassen’s algorithm divides into 7 subproblems of size n/2 and combines in O(n2) time.
T(n)=7T(n/2)+O(n2)
Here a=7, b=2 So c=log27≈2.807. We have f(n)=O(n2)=O(nc−ε) with ε=c−2≈0.807 Which is Case 1.
Amortised analysis gives the average cost per operation over a sequence of operations, even when Individual operations may be expensive.
Methods:
Aggregate analysis: Compute total cost of n operations, divide by n.
Accounting method: Assign an amortised cost to each operation; the sum of amortised costs must be an upper bound on the total actual cost.
Potential method: Define a potential function Φ; the amortised cost of the i-th operation is c^i=ci+Φ(Di)−Φ(Di−1).
Theorem 1.8 (Potential Method). If Φ(Di)≥Φ(D0) for all i≥1 Then the total amortised cost ∑i=1nc^i is an upper bound on the total actual cost ∑i=1nci.
Example (Dynamic Array). A dynamic array doubles in size when full. Insertion is O(1) Amortised: most insertions cost O(1); occasional resizing costs O(n) But is paid for by the Surplus from previous O(1) insertions.
Worked Example: Dynamic Array Amortised Analysis (Accounting Method)
Assign an amortised cost of 3 per insertion. The actual cost of insertion without resizing is 1. We store the surplus of 2 as “credit” on each element in the array.
When the array of capacity k is full and we must resize to capacity 2k:
The resizing cost is k (copy all k elements).
We have 2k credits stored (2 per element).
We spend k credits on the copy, leaving k credits.
The new array has k elements, so we need 2k more credits, but we already have k.
Subsequent insertions build up credits again.
Since the total credits are always non-negative, the amortised cost of 3 per insertion covers all actual costs.
Worked Example: Dynamic Array Amortised Analysis (Potential Method)
Define the potential function Φ(D)=2n−m where n is the number of elements and m is the capacity. We require m≥n So Φ≥0 (since 2n−n=n≥0).
Case 1: No resize. c^=1+(2(n+1)−m)−(2n−m)=1+2=3.
Case 2: Resize from m=n to m=2n. c^=(1+n)+(2(n+1)−2n)−(2n−n)=(n+1)+2−n=3.
Linked List. Each node stores data and a pointer to the next node. O(1) insertion/deletion at Head (given pointer), O(n) access by position.
Doubly Linked List. Each node has pointers to both next and previous nodes. O(1) insertion And deletion at any position (given pointers to the node and its neighbours).
Worked Example: Reversing a Singly Linked List
To reverse a singly linked list in O(n) time and O(1) space:
prev = null
curr = head
while curr != null:
next = curr.next
curr.next = prev
prev = curr
curr = next
head = prev
At each iteration, we move one node to the front of the reversed list. After n iterations, all n nodes are reversed.
Stack. Last-in, first-out (LIFO). Operations: push, pop, peek, all O(1).
Queue. First-in, first-out (FIFO). Operations: enqueue, dequeue, peek, all O(1).
Implementation. Both can be implemented with arrays (with circular buffer) or linked lists.
Worked Example: Implementing a Queue with Two Stacks
A queue can be implemented using two stacks in_stack and out_stack.
enqueue(x): push x onto in_stack. O(1).
dequeue(): if out_stack is empty, pop all elements from in_stack and push onto out_stack. Then pop from out_stack.
The dequeue operation is O(1) amortised: each element is moved from in_stack to out_stack at most once across all operations. Over n operations, the total number of element moves is at most n Giving O(1) amortised per dequeue.
A hash table maps keys to values using a hash function h:K→0,1,…,m−1.
Collision resolution:
Chaining: Each bucket is a linked list. Average case: O(1+α) where α=n/m is the load factor.
Open addressing (linear probing): Insert into the next available slot. Average case: O(1/(1−α)).
Theorem 2.1 (Uniform Hashing). Under the assumption of simple uniform hashing, the expected Length of a chain in a hash table with chaining is α=n/m.
Proof. Under simple uniform hashing, each of the n keys is equally likely to hash to any of the m slots. For any given key k The expected number of other keys that hash to the same slot as k is (n−1)/m<α. Including k itself, the expected chain length is 1+(n−1)/m≤1+α. ■
Theorem 2.2 (Successful Search with Chaining). Under simple uniform hashing, the expected time for a successful search in a hash table with chaining is O(1+α/2).
Proof. The expected length of the list containing the searched element is 1+(n−1)/m Since n−1 other elements are distributed uniformly. On average, the searched element is halfway through its list, giving (1+(n−1)/m)/2+1/2 comparisons (we examine elements before the target plus the target itself). This equals 1+α/2−1/(2m)=O(1+α). ■
Double Hashing. Uses two hash functions: h(k,i)=(h1(k)+i⋅h2(k))modm Where h2(k) is relatively prime to m. This avoids the clustering problems of linear probing.
Worked Example: Choosing a Good Hash Function
For integer keys, a common choice is the multiplication method: h(k)=⌊m⋅(k⋅Amod1)⌋ where A=(5−1)/2≈0.618 (the inverse golden ratio).
For string keys, the polynomial rolling hash: h(s)=(∑i=0k−1s[i]⋅pk−1−i)modm where p is a prime (e.g., 31 or 127).
Both avoid clustering better than simple modulo hashing when the input distribution is adversarial.
Binary Search Tree (BST). For each node: left subtree values < node value, right subtree values > node value.
Search, insert, delete: O(h) where h is the height.
For a balanced BST, h=O(logn).
Theorem 2.3. The expected height of a randomly built BST with n distinct keys is O(logn).
Proof. Let Xn be the height of a BST built from n random keys. Let Yn=2Xn. Then E[Yn]≤41∑i=0n−1(in)E[Yi]E[Yn−1−i]/n. Using the indicator random variable technique, E[Yn]≤n3/2cn+1∑i=0n−1cicn−1−ii3/2(n−1−i)3/2≤c⋅n3/2 for some constant c. Taking logs gives E[Xn]=E[logYn]≤logE[Yn]=O(logn) by Jensen’s inequality. ■
Definition. An AVL tree is a BST where for every node, the heights of its left and right subtrees differ by at most 1. The balance factor of a node is the height of its left subtree minus the height of its right subtree; valid balance factors are −1,0,1.
Rotations. After insertion or deletion, the balance factor may become ±2. This is fixed by rotations:
Right rotation (LL case): Balance factor +2 at node A Balance factor +1 at left child B.
A B
/ \ / \
B C → D A
/ \ / \
D E E C
Left rotation (RR case): Mirror of right rotation.
Left-Right rotation (LR case): Balance factor +2 at A Balance factor −1 at left child B. First left-rotate B Then right-rotate A.
Right-Left rotation (RL case): Mirror of LR case.
Theorem 2.4. An AVL tree with n nodes has height h≤1.4404⋅log2(n+2)−1.3277.
Proof. Let N(h) be the minimum number of nodes in an AVL tree of height h. We have N(0)=1, N(1)=2 And N(h)=1+N(h−1)+N(h−2) for h≥2. This is the Fibonacci recurrence, giving N(h)=Fh+3−1. Using Fh=5ϕh−ϕ^h where ϕ=21+5 We get N(h)>ϕh/5−1 So h<logϕ(5(n+1))≈1.4404log2(n+1). ■
Corollary 2.5. All AVL tree operations (search, insert, delete) run in O(logn) time.
Proof. Each operation visits O(h)=O(logn) nodes along a root-to-leaf path, and each rotation is O(1). Since at most O(logn) rotations are needed (at most 2 per level), the total cost is O(logn). ■
For every node, all simple paths from the node to descendant leaves contain the same number of black nodes (the black-height).
Theorem 2.6. A red-black tree with n internal nodes has height h≤2log2(n+1).
Proof. Let r be the root. By property 5, at least half the nodes on any root-to-leaf path are black (by property 4, no two reds are adjacent), so the black-height bh(r)≥h/2. Let S(h,bh) be the minimum number of internal nodes in a subtree of height h and black-height bh. Then S(h,bh)≥2bh−1 (proved by induction on bh Using the fact that each child has black-height at least bh−1 and the root is black). Since bh(r)≥h/2 and n≥2bh(r)−1≥2h/2−1 We get h≤2log2(n+1). ■
Insertion. Insert as in a standard BST (colour the new node red), then fix violations by recolouring and rotating up to O(logn) times.
Deletion. More complex than insertion. When a black node is removed, we may need to fix the black-height property by recolouring and rotating. The fix-up procedure takes O(logn) time.
Definition. A B-tree of minimum degree t≥2 is a rooted tree satisfying:
Every node has at most 2t−1 keys.
Every non-root node has at least t−1 keys (hence at least t children).
The root has at least 1 key.
All leaves appear at the same depth.
Theorem 2.7. A B-tree with n keys and minimum degree t has height h≤logt2n+1.
Proof. The root has at least 1 key and 2 children. At depth 1, each node has at least t−1 keys and t children. At depth d≥1 The number of nodes is at least 2td−1 Each with at least t−1 keys. So the total number of keys is at least 1+2(t−1)∑i=0h−1ti=1+2(t−1)⋅t−1th−1=2th−1. Setting n≥2th−1 gives h≤logt((n+1)/2). ■
Worked Example: Red-Black Tree Insertion Trace
Insert keys 7, 3, 18, 10, 22, 8, 11, 26 into an initially empty red-black tree.
Insert 7: Root is coloured black. T=[7(B)]
Insert 3: Left child of 7, coloured red. T=[7(B)←3(R)]. Valid (root is black, no red-red violation).
Insert 18: Right child of 7, coloured red. T=[3(R)←7(B)→18(R)]. Valid.
Insert 10: Insert as left child of 18, coloured red. Now 18 has red child 10. Uncle of 10 is 3(R), Case 1 (uncle is red): recolour uncle 3 black, parent 18 black, grandparent 7 red. Grandparent 7 is root, so recolour 7 black.
Result: T=[3(B)←7(B)→18(B)] with 10(R) under 18.
Insert 22: Right child of 18, coloured red. Uncle of 22 is 10(R), Case 1 again: recolour 10 black, 22 black, 18 red. Parent 18 is not root, grandparent is 7(B). Now 7 has right child 18(R). Check 7’s left child: 3(B). No violation.
Insert 8: Left child of 10, coloured red. Uncle of 8 is 22(R), Case 1: recolour 8 black, 22 black, 10 red. Now 10(R) is left child of 18(B). Check grandparent 7: right child 18(B) with left child 10(R). No red-red violation.
Insert 11: Right child of 10, coloured red. Now 10(R) has both children red, wait, 10 is red and 11 is red: violation. Uncle of 11 is 8(R), Case 1: recolour 8 black, 11 black, 10 red. Grandparent 18(B): 10(R) is left child, 22(B) is right child. No violation.
Insert 26: Right child of 22, coloured red. Uncle of 26 is 10(R), Case 1: recolour 10 black, 26 black, 22 red. Now 22(R) is right child of 18(B). Check left child of 18: 10(B). Valid.
A skip list is a probabilistic data structure that provides O(logn) expected-time search, insertion, and deletion. It consists of multiple levels of linked lists.
Structure:
Level 0 is a standard linked list containing all elements in sorted order.
Each element is promoted to level 1 with probability p ( p=1/2).
Each level-i element is promoted to level i+1 independently with probability p.
A sentinel head node links to the first element at every level.
Theorem 2.8. The expected number of levels in a skip list with n elements is O(logn).
Proof. An element reaches level i with probability pi. The expected number of elements at level i is npi. The highest non-empty level is approximately log1/pn. ■
Theorem 2.9. Search, insert, and delete in a skip list take O(logn) expected time.
Proof. The search path at each level i moves right to find an element whose successor at level i is greater than the target, then drops to level i−1. At each level, the expected number of rightward steps is at most 1/p (a geometric distribution argument). With O(log1/pn) levels, the total expected work is O(logn/p)=O(logn). ■
Worked Example: Skip List Search
Search for key 25 in a skip list containing keys [3,7,12,19,25,31,42] with the following level structure (probability p=1/2):
A Bloom filter is a space-efficient probabilistic data structure for set membership testing. It may produce false positives but never false negatives.
Structure: A bit array of m bits (initially all 0) and k independent hash functions h1,…,hk.
Operations:
Insertx: set bits h1(x),h2(x),…,hk(x) to 1.
Queryx: return true if all h1(x),…,hk(x) are set to 1.
Theorem 2.10. After inserting n elements into a Bloom filter of size m with k hash functions, the probability of a false positive is approximately (1−e−kn/m)k.
Proof. After inserting n elements, each of the m bits is set to 0 with probability (1−1/m)kn≈e−kn/m. The probability that all k bits for a query element are set is (1−e−kn/m)k. ■
Optimal number of hash functions. The false positive rate is minimised when k=(m/n)ln2 Giving a minimum rate of approximately (1/2)k=0.6185m/n.
Worked Example: Bloom Filter Configuration
We want to store n=10000 URLs with a false positive rate of at most 1%.
From the formula: (1−e−kn/m)k≤0.01.
Optimal k=(m/n)ln2. At the optimal point, the false positive rate is (1/2)k≈0.6185m/n.
We need 0.6185m/n≤0.01 So m/n≥ln(0.01)/ln(0.6185)≈9.585.
Take m=95850 bits ≈11.5 KB. Then k=(95850/10000)ln2≈6.64 So use k=7 hash functions.
This requires only 11.5 KB of memory versus approximately 600 KB for storing the URLs as 60-byte strings.
Worked Example: Bloom Filter Operations Trace
A Bloom filter has m=10 bits, k=2 hash functions: h1(x)=xmod10, h2(x)=(x⋅3)mod10.
Insert 15: h1(15)=5, h2(15)=5⋅3mod10=5. Set bit 5. Bit array: 0000010000
Insert 22: h1(22)=2, h2(22)=22⋅3mod10=6. Set bits 2, 6. Bit array: 0010010100
Insert 37: h1(37)=7, h2(37)=37⋅3mod10=1. Set bits 1, 7. Bit array: 0110010110
Query 22: h1(22)=2 (set), h2(22)=6 (set). Result: possibly in set (true positive).
Query 42: h1(42)=2 (set), h2(42)=42⋅3mod10=6 (set). Result: possibly in set (false positive!).
Query 50: h1(50)=0 (not set). Result: definitely not in set (true negative).
Theorem 2.11 (Build-Heap).buildHeap on an array of n elements runs in O(n) time.
Proof.buildHeap calls heapify on each of the ⌊n/2⌋ non-leaf nodes. The cost of heapify at a node of height h is O(h). The total cost is ∑h=0⌊logn⌋⌈n/2h+1⌉⋅O(h)=O(n∑h=0∞h/2h)=O(n)Since ∑h=0∞h/2h=2. ■
Worked Example: Build-Heap Step by Step
Build a max-heap from the array [4,10,3,5,1,8,2].
The array represents the tree:
4
/ \
10 3
/ \ / \
5 1 8 2
Non-leaf nodes (indices ⌊7/2⌋−1=2 down to 0): nodes 3, 10, 4.
Heapify index 2 (value 3): Children are 8 and 2. Swap 3 with 8. Array: [4,10,8,5,1,3,2].
4
/ \
10 8
/ \ / \
5 1 3 2
Heapify index 1 (value 10): Children are 5 and 1. 10 is largest. No swap.
Heapify index 0 (value 4): Children are 10 and 8. Swap with 10. Array: [10,4,8,5,1,3,2].
10
/ \
4 8
/ \ / \
5 1 3 2
Now heapify index 1 (value 4): Children are 5 and 1. Swap with 5. Array: [10,5,8,4,1,3,2].
10
/ \
5 8
/ \ / \
4 1 3 2
This is a valid max-heap. Total operations: 3 swaps = O(n).
The Union-Find data structure maintains a partition of a set into disjoint subsets, supporting:
MakeSet(x): Create a singleton set x.
Find(x): Return the representative of the set containing x.
Union(x,y): Merge the sets containing x and y.
Implementation with path compression and union by rank:
Each set is represented as a rooted tree.
Find follows parent pointers and compresses the path (makes every node on the path point directly to the root).
Union attaches the shorter tree under the root of the taller tree (by rank/size).
Theorem 2.12. With both path compression and union by rank, the amortised time per operation is O(α(n)) Where α(n) is the inverse Ackermann function.
Proof (outline). The inverse Ackermann function α(n) grows so slowly that α(n)≤4 for all practical values of n (n≤22265536). The proof uses a potential function argument on the levels of the tree. Each node is assigned a level based on its rank. The key insight is that path compression prevents the number of nodes at high levels from growing. The total number of Find operations that can charge to any level-ℓ node is bounded by O(n⋅F−1(ℓ)) where F is a fast-growing function. Summing over all levels gives O(nα(n)) total, hence O(α(n)) amortised per operation. ■
Worked Example: Union-Find for Connected Components
Given an undirected graph with n vertices and m edges, determine the connected components.
for each vertex v:
MakeSet(v)
for each edge (u, v):
if Find(u) != Find(v):
Union(u, v)
After processing all edges, vertices in the same connected component have the same representative. Total time: O(n+mα(n))≈O(n+m).
Algorithm. Divide the array in half, recursively sort each half, then merge.
MergeSort(A, l, r):
if l < r:
m = (l + r) / 2
MergeSort(A, l, m)
MergeSort(A, m+1, r)
Merge(A, l, m, r)
Theorem 3.1. Merge sort runs in O(nlogn) time in all cases (best, average, worst).
Proof. The recurrence is T(n)=2T(n/2)+O(n). By the Master theorem (case 2): a=2, b=2, f(n)=O(n)=O(nlogba) So T(n)=O(nlogn). ■
Theorem 3.2. Merge sort is stable and requires O(n) auxiliary space.
Proof of stability. In Merge. When elements from the left and right halves are equal, we always take from the left half first. This preserves the relative order of equal elements. ■
Theorem 3.3 (Correctness of Merge). The Merge procedure produces a sorted array from two sorted subarrays.
Proof. Let A[l..m] and A[m+1..r] be sorted subarrays. The merge procedure maintains two indices i and j pointing to the next unmerged element in each subarray. At each step, the minimum of A[i] and A[j] is appended to the output. By induction on the number of elements placed: if k elements have been placed and they are the k smallest among the remaining elements, then the (k+1)-th element placed is the minimum of the remaining elements. Since each subarray is sorted, the smallest remaining element is min(A[i],A[j]). ■
Theorem 3.4 (Optimality of Merge Sort). Merge sort achieves the optimal O(nlogn) comparison-based sorting bound.
Proof. By Theorem 3.7, no comparison sort can do better than Ω(nlogn). Merge sort achieves O(nlogn) in all cases, so it is asymptotically optimal among comparison sorts. ■
Worked Example: Merge Sort Trace
Sort the array [38,27,43,3,9,82,10]:
Split: [38,27,43,3] and [9,82,10]
Split: [38,27] and [43,3] Split: [38] and [27] → Merge: [27,38] Split: [43] and [3] → Merge: [3,43] Merge: [3,27,38,43]
Split: [9,82] and [10] Split: [9] and [82] → Merge: [9,82] Merge: [9,10,82]
Algorithm. Choose a pivot, partition the array into elements ≤ pivot and > pivot, recursively Sort each partition.
QuickSort(A, lo, hi):
if lo < hi:
p = Partition(A, lo, hi)
QuickSort(A, lo, p - 1)
QuickSort(A, p + 1, hi)
Partition (Lomuto): Select the last element as pivot. Iterate through the array, maintaining that elements A[lo..i] are ≤ pivot and A[i+1..j−1] are > pivot.
Theorem 3.5. Quicksort runs in O(nlogn) expected time and O(n2) worst-case time.
Proof (expected case). If the pivot is chosen uniformly at random, the expected number of Comparisons is O(nlogn) by an indicator random variable argument.
Let Xij be the indicator random variable that zi and zj are compared, where z1,…,zn are the sorted elements. Since elements are compared only if one is an ancestor of the other in the recursion tree, and the pivot is chosen uniformly at random:
E[Xij]=Pr(ziandzjarecompared)=j−i+12
The total number of comparisons is X=∑i<jXij So:
Worst case occurs when the pivot is always the smallest or largest element (e.g., already sorted Array with first-element pivot): T(n)=T(n−1)+O(n)=O(n2). ■
Theorem 3.6. Quicksort is not stable but uses O(logn) expected stack space.
Worked Example: Quicksort with Median-of-Three Pivot
Sort [3,7,8,5,2,1,9,5,4] using median-of-three (first, middle, last element).
Pivot selection: elements at positions 0, 4, 8 are 3,2,4. Median is 3.
Partition around 3: [2,1,3,8,5,7,9,5,4]
Recurse on [2,1] (pivot 2): [1,2] Recurse on [8,5,7,9,5,4] (median of 8,7,4 is 7): [4,5,5,7,9,8]
Recurse on [4,5,5] (pivot 5): [4,5,5] Recurse on [9,8] (pivot 9): [8,9]
Algorithm. Build a max-heap in O(n) time, then repeatedly extract the maximum.
Theorem 3.7. Heapsort runs in O(nlogn) time in all cases, uses O(1) auxiliary space, and Is not stable.
Proof. Building the heap takes O(n) by Theorem 2.11. Each of the n extract-max operations takes O(logn) For a total of O(nlogn). Space is O(1) since the heap is stored in-place. ■
Theorem 3.8. Any comparison-based sorting algorithm requires Ω(nlogn) comparisons in the Worst case.
Proof. A comparison-based sort can be modelled as a decision tree. The tree must have at least n! Leaves (one for each permutation). A binary tree with n! leaves has height at least log2(n!)≥log2((n/2)n/2)=(n/2)log2(n/2)=Ω(nlogn).
More precisely, using Stirling’s approximation: log2(n!)=nlog2n−nlog2e+O(logn)=Ω(nlogn). ■
Counting sort sorts integers in O(n+k) time where k is the range of values.
Algorithm:
Count occurrences: C[i]= number of elements equal to i.
Compute prefix sums: C[i]= number of elements ≤i.
Place each element in its correct position (iterating backwards for stability).
Theorem 3.9. Counting sort runs in O(n+k) time and O(n+k) space, and is stable.
Proof. Step 1 takes O(n) time. Step 2 takes O(k) time. Step 3 takes O(n) time. The output array uses O(n) space and the count array uses O(k) space. Stability follows from iterating backwards in step 3: the last occurrence of each value is placed first, preserving the order of equal elements. ■
Radix sort processes digits from least significant to most significant (LSD radix sort) using a stable sort (e.g., counting sort) as a subroutine.
Theorem 3.10. LSD radix sort sorts n integers with d digits in base b in O(d(n+b)) time.
Proof. We perform d passes of counting sort, each taking O(n+b) time. After the i-th pass, the array is sorted by the i least significant digits. By induction on i After d passes the array is fully sorted. ■
Corollary 3.11. For d-digit integers where d=O(1) (e.g., 32-bit integers), radix sort runs in O(n) time.
Worked Example: Counting Sort Trace
Sort the array [4,2,2,8,3,3,1] using counting sort.
Range of values: [1,8] So k=8.
Step 1, Count:C=[0,1,2,2,1,0,0,0] (indices 1 through 8).
Step 2, Prefix sums:C=[0,1,3,5,6,6,6,6].
Step 3, Place (iterate backwards):
A[6]=1: C[1]=1 Place at position 0. C[1]=0.
A[5]=3: C[3]=5 Place at position 4. C[3]=4.
A[4]=3: C[3]=4 Place at position 3. C[3]=3.
A[3]=8: C[8]=6 Place at position 5. C[8]=5.
A[2]=2: C[2]=3 Place at position 2. C[2]=2.
A[1]=2: C[2]=2 Place at position 1. C[2]=1.
A[0]=4: C[4]=6 Place at position 5. C[4]=5.
Result: [1,2,2,3,3,4,8].
Worked Example: Radix Sort Trace
Sort the array [170,45,75,90,802,24,2,66] using LSD radix sort with base 10.
Pass 1 (ones digit): Sort by 0,5,5,0,2,4,2,6. After stable counting sort: [170,90,802,2,24,45,75,66]
Pass 2 (tens digit): Sort by 7,9,0,0,2,4,7,6. After stable counting sort: [802,2,24,45,66,170,75,90]
Pass 3 (hundreds digit): Sort by 8,0,0,0,0,1,0,0. After stable counting sort: [2,24,45,66,75,90,170,802]
Total: 3 passes, each O(n+10)=O(n). Total: O(3n)=O(n).
Problem. Sort data that is too large to fit in main memory.
Algorithm (External Merge Sort):
Read chunks that fit in memory, sort each in memory, write sorted runs to disk.
Repeatedly merge runs using a k-way merge, reading/writing from disk.
Theorem 3.12. External merge sort with M bytes of memory and N bytes of data performs O(MNlogk(MN)) passes, where k is the merge fan-in.
Proof. The initial pass creates N/M sorted runs of size M. Each merge pass combines k runs into 1, reducing the number of runs by a factor of k. After logk(N/M) passes, a single sorted run remains. Each pass reads and writes all N bytes, so total I/O is O(Nlogk(N/M)). ■
BFS explores the graph level by level from a source vertex.
BFS(G, s):
for each v in V:
d[v] = INF
pi[v] = NIL
d[s] = 0
Q = {s}
while Q is not empty:
u = Q.dequeue()
for each v in G.adj[u]:
if d[v] == INF:
d[v] = d[u] + 1
pi[v] = u
Q.enqueue(v)
Theorem 4.1. BFS runs in O(V+E) time and discovers shortest paths (in terms of number of Edges) from the source to all reachable vertices.
Proof.Time: Each vertex is enqueued at most once and dequeued at most once: O(V). Each edge is examined at most twice (once from each endpoint): O(E). Total: O(V+E).
Correctness: We prove by induction on the length of shortest paths. Let v be a vertex discovered via edge (u,v). At the time v is discovered, d[u] equals the shortest-path distance from s to u (induction hypothesis). Since BFS processes vertices in non-decreasing order of distance, d[v]=d[u]+1 is the shortest distance to v. Any path to v must go through some vertex at distance at most d[u] (the predecessor on the path), and all such vertices have already been processed. ■
Worked Example: BFS on a Weighted Path Problem
Find the shortest path (fewest edges) from A to all other vertices in the graph with edges: (A,B),(A,C),(B,D),(C,D),(C,E),(D,F),(E,F).
Starting from A:
Level 0: A (distance 0)
Level 1: B,C (distance 1)
Level 2: D,E (distance 2)
Level 3: F (distance 3)
Shortest path from A to F: A→C→E→F (or A→B→D→F or A→C→D→F), all of length 3.
DFS explores as far as possible along each branch before backtracking.
DFS(G):
for each v in V:
colour[v] = WHITE
pi[v] = NIL
time = 0
for each v in V:
if colour[v] == WHITE:
DFS-Visit(G, v)
DFS-Visit(G, u):
colour[u] = GREY
time += 1
d[u] = time
for each v in G.adj[u]:
if colour[v] == WHITE:
pi[v] = u
DFS-Visit(G, v)
colour[u] = BLACK
time += 1
f[u] = time
Theorem 4.2. DFS runs in O(V+E) time and can be used to detect cycles, find connected Components, compute topological orderings, and identify strongly connected components.
Proof (time). Each vertex is coloured exactly once (from WHITE to GREY) and finished exactly once (from GREY to BLACK): O(V). Each edge is examined at most twice (once in each direction for undirected, once for directed): O(E). Total: O(V+E). ■
Theorem 4.3 (Parenthesis Theorem). In any DFS, for any two vertices u and v Exactly one of the following holds: (1) d[u]<d[v]<f[v]<f[u] (interval nesting), (2) d[v]<d[u]<f[u]<f[v] (interval nesting), or (3) the intervals [d[u],f[u]] and [d[v],f[v]] are disjoint.
Proof. The DFS call stack forms a nesting of intervals. When we start visiting v from u We must finish v before finishing u Giving nesting. If u and v are in different DFS trees, their intervals are disjoint. ■
Theorem 4.4 (White-Path Theorem).v is a descendant of u in the DFS forest if and only if, at the time d[u] is discovered, there is a path from u to v consisting entirely of white vertices.
Proof. (⇒) By induction on the depth of v in the DFS tree. If v is a child of u Then v was white when discovered from u. If v is a deeper descendant, the path goes through intermediate white vertices.
(⇐) Suppose there is a white path from u to v at time d[u]. Let w be the first vertex on this path discovered after u. All vertices before w on the path are still white (they can only be discovered after w), so w will be discovered from the path. By induction, v is a descendant of w Hence of u. ■
A topological ordering of a DAG is a linear ordering of vertices such that for every directed edge (u,v), u appears before v.
Algorithm: Run DFS on the DAG. Output vertices in reverse order of finishing times.
Theorem 4.5. The reverse post-order of a DFS on a DAG produces a valid topological ordering.
Proof. Suppose there is an edge (u,v) but u appears after v in the ordering (i.e., f[u]<f[v]). Since (u,v) is an edge, when u is being explored (coloured GREY), if v is WHITE, then v is discovered as a descendant of u So f[v]<f[u] Contradiction. If v is GREY, we have a back edge, implying a cycle, contradicting that the graph is acyclic. If v is BLACK, then f[v]<d[u]<f[u] Contradicting f[u]<f[v]. ■
Two vertices u and v are strongly connected if there is a path from u to v and from v to u. A strongly connected component (SCC) is a maximal set of strongly connected vertices.
Kosaraju’s Algorithm:
Run DFS on G Recording finishing times.
Compute GT (transpose of G: reverse all edges).
Run DFS on GT in decreasing order of finishing times. Each DFS tree is an SCC.
Theorem 4.6. Kosaraju’s algorithm correctly identifies all SCCs in O(V+E) time.
Proof. Let C be the SCC containing the vertex s with the highest finishing time in the first DFS. We claim that in GT No vertex in C can reach a vertex outside C (otherwise s would have a path to and from that vertex, placing it in C). In the second DFS, starting from s in GT We discover exactly the vertices in C. Removing C and repeating the argument gives the remaining SCCs. ■
Tarjan’s Algorithm finds SCCs in a single DFS pass using a stack and low-link values, also in O(V+E) time.
Problem. Find shortest paths from a single source in a weighted graph with non-negative edge Weights.
Algorithm. Maintain a priority queue of vertices keyed by their current shortest-path estimate. Repeatedly extract the vertex with minimum distance and relax its edges.
Dijkstra(G, w, s):
for each v in V:
d[v] = INF
pi[v] = NIL
d[s] = 0
S = {} // processed vertices
Q = V // priority queue keyed by d[]
while Q is not empty:
u = ExtractMin(Q)
S = S ∪ {u}
for each v in G.adj[u]:
if d[v] > d[u] + w(u, v):
d[v] = d[u] + w(u, v)
pi[v] = u
DecreaseKey(Q, v, d[v])
Theorem 4.7. Dijkstra’s algorithm correctly computes shortest paths from the source.
Proof. We prove by induction on ∣S∣ that when a vertex u is added to S, d[u]=δ(s,u) (the true shortest-path distance).
Base case: s is the first vertex added, and d[s]=0=δ(s,s).
Inductive step: Suppose all vertices in S have correct distances. Let u be the next vertex extracted from Q. Suppose for contradiction that d[u]>δ(s,u). Consider a shortest path P from s to u And let (x,y) be the first edge on P where x∈S and y∈/S. Then δ(s,y)=δ(s,x)+w(x,y)=d[x]+w(x,y) (by induction). When x was added to S The edge (x,y) was relaxed, so d[y]≤d[x]+w(x,y)=δ(s,y). Since edge weights are non-negative, δ(s,y)≤δ(s,u). But d[y]≤δ(s,y)≤δ(s,u)<d[u] And y is in Q Contradicting that u has the minimum d-value in Q. ■
Theorem 4.8. Dijkstra’s algorithm with a binary heap runs in O((V+E)logV) time. With a Fibonacci heap: O(VlogV+E).
Proof. Each vertex is extracted from the priority queue at most once: O(VlogV). Each edge is Relaxed at most once, and each relaxation may cause a DecreaseKey: O(ElogV). Total: O((V+E)logV). With a Fibonacci heap, DecreaseKey is O(1) amortised, giving O(VlogV+E). ■
Problem. Find shortest paths from a single source, allowing negative edge weights (but no negative Cycles).
BellmanFord(G, w, s):
for each v in V:
d[v] = INF
pi[v] = NIL
d[s] = 0
for i = 1 to |V| - 1:
for each edge (u, v) in E:
if d[v] > d[u] + w(u, v):
d[v] = d[u] + w(u, v)
pi[v] = u
for each edge (u, v) in E:
if d[v] > d[u] + w(u, v):
return "negative cycle detected"
Theorem 4.9. Bellman-Ford runs in O(VE) time. It correctly detects negative-weight cycles.
Proof. After i iterations of the relaxation loop, d[v] is at most the weight of the shortest path from s to v using at most i edges. This is proved by induction on i.
Base case (i=0): Only d[s]=0 is finite, which is the weight of the empty path.
Inductive step: A shortest path from s to v using at most i edges either uses at most i−1 edges (handled by induction) or uses exactly i edges, ending with edge (u,v) where the shortest path to u uses at most i−1 edges. The relaxation of (u,v) in iteration i handles this case.
After V−1 iterations, all shortest paths (which have at most V−1 edges) are correct. A V-th iteration that updates any distance indicates a path of length V with strictly decreasing weight, which contains a cycle of negative weight. ■
Worked Example: Bellman-Ford with Negative Edge Weights
Find shortest paths from A in the graph:
A6B, A7C
B5C, B−4D, B8E
C−3D, C9E
D2E, D7F
E−5F
Initial: d=[∞,∞,∞,∞,∞,∞] for [A,B,C,D,E,F]. d[A]=0.
Algorithm. For k=1,…,V: for each pair (i,j) Check if going through vertex k Improves the path.
dij(k)=min(dij(k−1),dik(k−1)+dkj(k−1))
Derivation. Define dij(k) as the shortest-path distance from i to j using only intermediate vertices from 1,2,…,k. Then:
dij(0)=w(i,j) (the weight of edge (i,j) Or ∞ if no edge).
For k≥1: The shortest path from i to j through vertices 1,…,k either does not use vertex k (giving dij(k−1)) or uses vertex k (giving dik(k−1)+dkj(k−1)).
Theorem 4.10. Floyd-Warshall runs in O(V3) time and O(V2) space.
Proof. The triple nested loop (k, i, j) executes V3 iterations, each doing O(1) work. The distance matrix requires V2 space. Note that dij(k) can overwrite dij(k−1) in place because dik(k)=dik(k−1) and dkj(k)=dkj(k−1) (paths from i to k and k to j using vertices up to k cannot be improved by going through k again without a negative cycle). ■
Worked Example: Floyd-Warshall on 4 Vertices
Find all-pairs shortest paths for the graph with vertices 1,2,3,4 and edges: w(1,2)=3, w(1,3)=8, w(1,4)=−4w(2,1)=5, w(2,3)=7, w(2,4)=2w(3,1)=2, w(3,4)=−1w(4,1)=6, w(4,3)=9.
Kruskal’s Algorithm. Sort edges by weight, add edges to the MST as long as they don’t create a Cycle (using Union-Find). O(ElogE).
Prim’s Algorithm. Start from any vertex, repeatedly add the minimum-weight edge connecting the Tree to a non-tree vertex (using a priority queue). O((V+E)logV).
Theorem 4.11 (Cut Property). For any cut of the graph, the minimum-weight edge crossing the cut Belongs to some MST.
Proof. Let (S,V∖S) be a cut and e=(u,v) be the minimum-weight crossing edge with u∈S, v∈/S. Let T be an MST. If e∈T We are done. Otherwise, adding e to T creates a cycle. This cycle must cross the cut at least once more (it goes from u to v via some other path). Let e′ be another crossing edge on this cycle. Since e is the minimum-weight crossing edge, w(e)≤w(e′). Replacing e′ with e in T gives a spanning tree of weight no greater than T Hence an MST containing e. ■
Theorem 4.12 (Cycle Property). For any cycle, the maximum-weight edge on the cycle does not belong To any MST.
Proof. Let C be a cycle and e be the maximum-weight edge on C. Let T be an MST. If e∈/T We are done. Otherwise, removing e from T disconnects it into two components. The rest of cycle C must contain an edge e′=e crossing this cut. Since w(e′)<w(e) (if w(e′)=w(e) We can replace either), replacing e with e′ gives a spanning tree of strictly smaller weight, contradicting the optimality of T. ■
Theorem 4.13. Kruskal’s algorithm produces a minimum spanning tree.
Proof. We prove by induction on the number of edges added. At each step, the algorithm adds the minimum-weight edge that does not create a cycle. Consider the cut formed by the two connected components that the new edge connects. By the cut property, this minimum-weight crossing edge belongs to some MST. Since all previously added edges are in every MST (by induction), the edge set maintained by the algorithm is always a subset of some MST. ■
Theorem 4.14. Prim’s algorithm produces a minimum spanning tree.
Proof. By induction on the size of the tree. At each step, Prim’s adds the minimum-weight edge connecting the current tree T to a vertex outside T. Consider the cut (T,V∖T). By the cut property, the minimum-weight crossing edge belongs to some MST. ■
Worked Example: Kruskal's Algorithm
Find the MST of the graph with edges (sorted by weight): (D,E,2), (C,E,3), (A,B,4), (B,C,5), (B,E,6), (A,E,7), (A,D,8), (C,D,9).
Show that an optimal solution to the problem includes an optimal solution to a subproblem.
Proved by contradiction: if the optimal solution contained a suboptimal sub-solution, replacing it with an optimal one would improve the overall solution.
Example (Shortest Path). If p is a shortest path from u to v and w is an intermediate vertex on p Then the subpath of p from u to w is a shortest path from u to w.
Proof. If not, there exists a shorter path p′ from u to w. Then p′ concatenated with the subpath of p from w to v would be shorter than p Contradicting that p is a shortest path. ■
Problem. Given n items with weights w1,…,wn and values v1,…,vn And a knapsack of capacity W Maximise the total value without exceeding the capacity.
Time:O(nW). Space:O(nW) (can be reduced to O(W) with 1D array).
Proof of correctness. For each item i Either we don’t include it (value dp[i−1][c]) or we include it (value vi+dp[i−1][c−wi]). The optimal choice is the maximum. The base cases are correct. ■
Problem. Given matrices A1,A2,…,An where Ai has dimensions pi−1×pi Find the parenthesisation that minimises the total number of scalar multiplications.
Proof of correctness. The optimal parenthesisation of Ai⋯Aj splits at some position k: (Ai⋯Ak)(Ak+1⋯Aj). The cost is the cost of the left subproduct plus the cost of the right subproduct plus the cost of multiplying the resulting matrices (pi−1⋅pk⋅pj scalar multiplications). The optimal k minimises this total. ■
Time:O(mn). Space:O(mn) (can be reduced to O(min(m,n)) for the length only).
Proof of correctness. If xi=yj Any LCS of X[1..i] and Y[1..j] must include xi So LCS=1+LCS(X[1..i−1],Y[1..j−1]). If xi=yj The LCS either Excludes xi or excludes yj Giving the max of the two subproblems. ■
Problem. Given coin denominations d1,…,dn and a target amount M Find the minimum number of coins needed.
Recurrence:
dp[c]={0mini:di≤c(dp[c−di]+1)ifc=0ifc>0
Time:O(nM). Space:O(M).
Proof of correctness. To make change for amount c>0 The last coin used must be some di≤c. The remaining amount is c−di And the optimal solution for c uses 1+dp[c−di] coins. Taking the minimum over all valid di gives the optimal solution. ■
Worked Example: Coin Change
Denominations: 1,5,10,25. Target: M=63.
Bottom-up computation:
dp[0]=0
dp[1..4]=dp[c−1]+1=c (use pennies)
dp[5]=min(dp[4]+1,dp[0]+1)=1 (use a nickel)
dp[10]=min(dp[9]+1,dp[5]+1,dp[0]+1)=1 (use a dime)
…
dp[25]=1 (use a quarter)
dp[50]=2 (use two quarters)
dp[63]=min(dp[62]+1,dp[58]+1,dp[53]+1,dp[38]+1)
Working backwards: dp[63]=dp[38]+1=dp[13]+2=dp[3]+3=6.
Patience sorting approach (O(nlogn)): Maintain piles. For each card, place on the leftmost pile whose top card is ≥ the current card, or start a new pile on the right if no such pile exists. The number of piles equals the LIS length.
P: The class of decision problems solvable in polynomial time by a deterministic Turing machine.
NP: The class of decision problems solvable in polynomial time by a non-deterministic Turing Machine. Equivalently, problems whose “yes” instances have polynomial-time verifiable certificates.
NP-hard: A problem A is NP-hard if every problem in NP can be reduced to A in polynomial Time.
NP-complete: A problem is NP-complete if it is in NP and NP-hard.
Theorem 6.1. If any NP-complete problem is in P, then P = NP.
A polynomial-time reduction from problem A to problem B is a polynomial-time algorithm that Transforms instances of A into instances of B Preserving the answer.
Lemma 6.1. If A≤pB and B∈P Then A∈P.
Lemma 6.2. If A≤pB and A is NP-hard, then B is NP-hard.
Theorem 6.2 (Cook-Levin, 1971). SAT is NP-complete.
Proof sketch. We show that every problem in NP reduces to SAT. Let L∈NP. There exists a polynomial-time non-deterministic Turing machine M that decides L Running in time p(n) on inputs of length n. For an input x We construct a Boolean formula ϕx that is satisfiable if and only if M accepts x.
The formula encodes:
Tableau variables:T[i,j,σ]=1 iff cell (i,j) of the computation tableau contains symbol σ. The tableau has p(n)+1 rows and p(n) columns.
Initial configuration: Row 0 encodes the initial state of M on input x.
Transition constraints: Each 2×3 window of the tableau corresponds to a valid transition of M.
Accepting configuration: Some cell in the last row contains the accept state.
Each of these constraints can be expressed as a polynomial-size CNF formula. The total formula ϕx has size polynomial in n and is satisfiable iff M accepts x. ■
Show B∈NP (polynomial-time verifiable certificate).
Show a known NP-complete problem A reduces to B: A≤pB.
Example. 3-SAT ≤p Vertex Cover: construct a graph from the 3-SAT formula where each Variable and each clause become vertices, and edges enforce the constraint that a satisfying Assignment corresponds to a vertex cover.
Worked Example: 3-SAT $\leq_p$ Vertex Cover Reduction
Reduce 3-SAT formula ϕ=(x1∨xˉ2∨x3)∧(xˉ1∨x2∨x3)∧(x1∨x2∨xˉ3) to a vertex cover instance.
For each variable xi Create two vertices xi and xˉi connected by an edge (the “literal edge”).
For each clause Cj Create a triangle of 3 vertices cj1,cj2,cj3.
For each clause vertex cjk Connect it to the literal vertex corresponding to the k-th literal of clause j.
Claim:ϕ is satisfiable iff the graph has a vertex cover of size k+2m where k is the number of variables and m is the number of clauses.
(⇒) If ϕ is satisfiable, include in the cover: for each variable, the literal vertex matching the truth assignment (e.g., x1 if x1=true, xˉ1 if x1=false). This covers all literal edges (k vertices). For each clause triangle, at least one literal in the clause is true, so the corresponding literal vertex covers one of the three edges from the triangle. Include the other two vertices of the triangle (2m vertices total).
(⇐) A vertex cover of size k+2m must include exactly one endpoint of each literal edge (otherwise the triangle requires 3 vertices). Set each variable according to which literal vertex is in the cover. Each clause triangle has exactly one uncovered vertex, whose edge to a literal vertex is covered by that literal vertex, meaning the clause is satisfied.
The reduction takes polynomial time (number of vertices and edges is polynomial in the formula size).