Big-Theta Notation -- Professional Level¶
Table of Contents¶
- Formal Proof Techniques
- The Sandwich Theorem
- Theta from Limits
- Proving Theta(n log n) for Merge Sort
- Master Theorem and Theta
- Theta for Common Recurrences
- Advanced Proof Examples
- Theta and Polynomial Hierarchy
- Code: Verifying Proofs Computationally
- Summary
Formal Proof Techniques¶
There are three main approaches to proving f(n) = Theta(g(n)):
Approach 1: Direct (Find c1, c2, n0)¶
Prove the definition directly by exhibiting constants.
Template: 1. Show f(n) <= c2 * g(n) for n >= n0 (upper bound) 2. Show f(n) >= c1 * g(n) for n >= n0 (lower bound) 3. Conclude f(n) = Theta(g(n))
Approach 2: Prove O and Omega Separately¶
Since Theta(g(n)) = O(g(n)) AND Omega(g(n)): 1. Prove f(n) = O(g(n)) 2. Prove f(n) = Omega(g(n)) 3. Conclude f(n) = Theta(g(n))
Approach 3: Limit Method¶
Use limits to determine the relationship: - If lim(n->inf) f(n)/g(n) = c where 0 < c < infinity, then f(n) = Theta(g(n))
The Sandwich Theorem¶
The Sandwich Theorem (also called the Squeeze Theorem in calculus) is the theoretical foundation of Big-Theta.
Statement¶
If there exist functions h1(n) and h2(n) and a function f(n) such that: - h1(n) <= f(n) <= h2(n) for all sufficiently large n - h1(n) = Theta(g(n)) - h2(n) = Theta(g(n))
Then f(n) = Theta(g(n)).
Application¶
This is useful when f(n) is complex but can be bounded by simpler functions.
Example: Prove that f(n) = n^2 + n*sin(n) is Theta(n^2).
Since -1 <= sin(n) <= 1: - Lower: n^2 + n(-1) = n^2 - n <= f(n) - Upper: f(n) <= n^2 + n(1) = n^2 + n
For n >= 2: - n^2 - n >= n^2 - n^2/2 = n^2/2 (since n <= n^2/2 for n >= 2) - n^2 + n <= n^2 + n^2 = 2n^2
Therefore: (1/2)n^2 <= f(n) <= 2n^2 for n >= 2. With c1 = 1/2, c2 = 2, n0 = 2: f(n) = Theta(n^2). QED.
Theta from Limits¶
The Limit Theorem for Theta¶
Theorem: If lim(n -> infinity) f(n) / g(n) = L, then:
| Value of L | Conclusion |
|---|---|
| L = 0 | f(n) = o(g(n)), f(n) is NOT Theta(g(n)) |
| 0 < L < infinity | f(n) = Theta(g(n)) |
| L = infinity | g(n) = o(f(n)), f(n) is NOT Theta(g(n)) |
Why This Works¶
If lim f(n)/g(n) = L where 0 < L < infinity, then for any epsilon > 0, there exists N such that for all n >= N:
Choose epsilon = L/2 (so it stays positive):
Multiply by g(n):
This gives c1 = L/2, c2 = 3L/2, proving Theta(g(n)).
Examples Using Limits¶
Example 1: f(n) = 5n^2 + 3n + 7
Since 0 < 5 < infinity: f(n) = Theta(n^2). The constants are approximately c1 = 5/2 = 2.5 and c2 = 15/2 = 7.5.
Example 2: f(n) = n*log(n) + 100n
Since 0 < 1 < infinity: f(n) = Theta(n log n).
Example 3: f(n) = 2^n, g(n) = 3^n
Since L = 0: 2^n is NOT Theta(3^n). (It is o(3^n).)
Example 4: f(n) = n!, g(n) = 2^n
Since L = infinity: n! is NOT Theta(2^n). (n! grows much faster.)
Proving Theta for Merge Sort¶
The Recurrence¶
Merge sort satisfies the recurrence:
where c is the cost per element during merging, and d is the base case cost.
Proving T(n) = Theta(n log n)¶
Upper Bound (O(n log n)):
Claim: T(n) <= anlog2(n) for some constant a and all n >= 2.
Proof by induction: - Base: T(2) = 2T(1) + 2c = 2d + 2c. Need 2d + 2c <= a21. Choose a >= d + c. - Inductive step: Assume T(k) <= aklog2(k) for all k < n.
T(n) = 2T(n/2) + cn
<= 2 * a*(n/2)*log2(n/2) + cn
= a*n*(log2(n) - 1) + cn
= a*n*log2(n) - a*n + cn
<= a*n*log2(n) (when a >= c)
Choose a = max(d + c, c). Then T(n) <= anlog2(n). QED.
Lower Bound (Omega(n log n)):
Claim: T(n) >= bnlog2(n) for some constant b and all n >= 2.
Proof by induction: - Base: T(2) = 2d + 2c >= b21. Choose b <= d + c. - Inductive step: Assume T(k) >= bklog2(k) for all k < n.
T(n) = 2T(n/2) + cn
>= 2 * b*(n/2)*log2(n/2) + cn
= b*n*(log2(n) - 1) + cn
= b*n*log2(n) - b*n + cn
>= b*n*log2(n) (when b <= c)
Choose b = min(d + c, c). Then T(n) >= bnlog2(n). QED.
Conclusion: With constants a and b as defined, bnlog2(n) <= T(n) <= anlog2(n) for all n >= 2. Therefore T(n) = Theta(n log n). QED.
Master Theorem and Theta¶
The Master Theorem provides Theta bounds for recurrences of the form:
where a >= 1, b > 1, and f(n) is asymptotically positive.
Three Cases¶
Let c_crit = log_b(a).
Case 1: If f(n) = O(n^(c_crit - epsilon)) for some epsilon > 0:
Case 2: If f(n) = Theta(n^c_crit * log^k(n)) for k >= 0:
Case 3: If f(n) = Omega(n^(c_crit + epsilon)) for some epsilon > 0, and af(n/b) <= deltaf(n) for some delta < 1 and large n:
Applying Master Theorem¶
Merge Sort: T(n) = 2T(n/2) + Theta(n) - a = 2, b = 2, f(n) = n - c_crit = log_2(2) = 1 - f(n) = n = Theta(n^1 * log^0(n)), so Case 2 with k=0 - T(n) = Theta(n * log(n))
Binary Search: T(n) = T(n/2) + Theta(1) - a = 1, b = 2, f(n) = 1 - c_crit = log_2(1) = 0 - f(n) = 1 = Theta(n^0 * log^0(n)), so Case 2 with k=0 - T(n) = Theta(log(n))
Strassen Matrix Multiply: T(n) = 7T(n/2) + Theta(n^2) - a = 7, b = 2, f(n) = n^2 - c_crit = log_2(7) = 2.807... - f(n) = n^2 = O(n^(2.807 - 0.8)), so Case 1 with epsilon = 0.8 - T(n) = Theta(n^(log_2 7)) = Theta(n^2.807...)
Karatsuba Multiplication: T(n) = 3T(n/2) + Theta(n) - a = 3, b = 2, f(n) = n - c_crit = log_2(3) = 1.585... - f(n) = n = O(n^(1.585 - 0.5)), so Case 1 with epsilon = 0.5 - T(n) = Theta(n^(log_2 3)) = Theta(n^1.585...)
Theta for Common Recurrences¶
| Recurrence | Result | Method |
|---|---|---|
| T(n) = T(n/2) + 1 | Theta(log n) | Master Case 2 |
| T(n) = T(n/2) + n | Theta(n) | Master Case 3 |
| T(n) = 2T(n/2) + 1 | Theta(n) | Master Case 1 |
| T(n) = 2T(n/2) + n | Theta(n log n) | Master Case 2 |
| T(n) = 2T(n/2) + n^2 | Theta(n^2) | Master Case 3 |
| T(n) = 4T(n/2) + n | Theta(n^2) | Master Case 1 |
| T(n) = 4T(n/2) + n^2 | Theta(n^2 log n) | Master Case 2 |
| T(n) = T(n-1) + 1 | Theta(n) | Unrolling |
| T(n) = T(n-1) + n | Theta(n^2) | Unrolling |
| T(n) = 2T(n-1) + 1 | Theta(2^n) | Unrolling |
Advanced Proof Examples¶
Proving Theta(n^2) for Insertion Sort (Worst Case)¶
Worst case: array sorted in descending order. For each element at position i, we compare with all i-1 previous elements.
Upper bound: n(n-1)/2 = (n^2 - n)/2 <= n^2/2. So O(n^2) with c2 = 1/2.
Lower bound: n(n-1)/2 = (n^2 - n)/2 >= n^2/4 for n >= 2 (since n >= n/2 implies n^2 - n >= n^2/2 is not quite right; more carefully: n(n-1)/2 >= n^2/4 when n >= 2, since n-1 >= n/2).
With c1 = 1/4, c2 = 1/2, n0 = 2: worst case is Theta(n^2). QED.
Proving log(n!) = Theta(n log n)¶
This is important because it shows sorting comparison lower bounds.
Upper bound: log(n!) = log(12...n) <= log(n^n) = nlog(n). So O(n log n).
Lower bound: log(n!) = sum of log(i) for i=1 to n
= sum of log(i) for i = n/2 to n (drop the first half) = (n/2) * log(n/2) = (n/2) * (log(n) - 1) = (n/4) * log(n) for n >= 4.
With c1 = 1/4, c2 = 1, n0 = 4: log(n!) = Theta(n log n). QED.
(Stirling's approximation n! ~ sqrt(2pin)(n/e)^n gives log(n!) ~ nlog(n) - n*log(e) + O(log n), confirming Theta(n log n).)
Theta and Polynomial Hierarchy¶
Theorem: Polynomial Dominance¶
For any polynomial p(n) = a_k * n^k + a_{k-1} * n^{k-1} + ... + a_0, where a_k > 0:
Proof sketch using limits:
Since 0 < a_k < infinity, p(n) = Theta(n^k) by the limit theorem.
Hierarchy of Common Theta Classes¶
Theta(1) < Theta(log n) < Theta(sqrt(n)) < Theta(n) < Theta(n log n) < Theta(n^2) < Theta(n^3) < Theta(2^n) < Theta(n!)
The "<" here means: for any f in the left class and g in the right class, lim f(n)/g(n) = 0, so f = o(g) (strictly smaller).
Code: Verifying Proofs Computationally¶
Go:
package main
import (
"fmt"
"math"
)
// verifyTheta checks if f(n)/g(n) converges to a positive constant
func verifyTheta(fName, gName string, f, g func(float64) float64) {
fmt.Printf("\nVerifying %s = Theta(%s):\n", fName, gName)
fmt.Printf("%-12s %-20s %-20s %-15s\n", "n", "f(n)", "g(n)", "f(n)/g(n)")
for _, n := range []float64{100, 1000, 10000, 100000, 1000000} {
fn := f(n)
gn := g(n)
ratio := fn / gn
fmt.Printf("%-12.0f %-20.2f %-20.2f %-15.6f\n", n, fn, gn, ratio)
}
// If ratio converges to c where 0 < c < inf, then Theta is confirmed
}
func main() {
// Verify: 3n^2 + 5n + 2 = Theta(n^2)
verifyTheta(
"3n^2+5n+2", "n^2",
func(n float64) float64 { return 3*n*n + 5*n + 2 },
func(n float64) float64 { return n * n },
)
// Verify: n*log(n) + 100n = Theta(n*log(n))
verifyTheta(
"n*ln(n)+100n", "n*ln(n)",
func(n float64) float64 { return n*math.Log(n) + 100*n },
func(n float64) float64 { return n * math.Log(n) },
)
// Verify: log(n!) = Theta(n*log(n)) using Stirling
verifyTheta(
"log(n!)", "n*log(n)",
func(n float64) float64 {
lgamma, _ := math.Lgamma(n + 1)
return lgamma
},
func(n float64) float64 { return n * math.Log(n) },
)
// Verify NOT Theta: 2^n vs 3^n
verifyTheta(
"2^n", "3^n",
func(n float64) float64 { return math.Pow(2, n) },
func(n float64) float64 { return math.Pow(3, n) },
)
}
Java:
public class ThetaVerifier {
interface MathFunc {
double apply(double n);
}
static void verifyTheta(String fName, String gName, MathFunc f, MathFunc g) {
System.out.printf("%nVerifying %s = Theta(%s):%n", fName, gName);
System.out.printf("%-12s %-20s %-20s %-15s%n", "n", "f(n)", "g(n)", "f(n)/g(n)");
double[] sizes = {100, 1000, 10000, 100000, 1000000};
for (double n : sizes) {
double fn = f.apply(n);
double gn = g.apply(n);
double ratio = fn / gn;
System.out.printf("%-12.0f %-20.2f %-20.2f %-15.6f%n", n, fn, gn, ratio);
}
}
static double logFactorial(double n) {
double result = 0;
for (int i = 2; i <= (int) n; i++) {
result += Math.log(i);
}
return result;
}
public static void main(String[] args) {
verifyTheta("3n^2+5n+2", "n^2",
n -> 3 * n * n + 5 * n + 2,
n -> n * n);
verifyTheta("n*ln(n)+100n", "n*ln(n)",
n -> n * Math.log(n) + 100 * n,
n -> n * Math.log(n));
verifyTheta("log(n!)", "n*log(n)",
ThetaVerifier::logFactorial,
n -> n * Math.log(n));
}
}
Python:
import math
def verify_theta(f_name: str, g_name: str, f, g):
print(f"\nVerifying {f_name} = Theta({g_name}):")
print(f"{'n':<12} {'f(n)':<20} {'g(n)':<20} {'f(n)/g(n)':<15}")
for n in [100, 1000, 10_000, 100_000, 1_000_000]:
fn = f(n)
gn = g(n)
ratio = fn / gn if gn != 0 else float('inf')
print(f"{n:<12} {fn:<20.2f} {gn:<20.2f} {ratio:<15.6f}")
if __name__ == "__main__":
# Verify: 3n^2 + 5n + 2 = Theta(n^2)
verify_theta(
"3n^2+5n+2", "n^2",
lambda n: 3 * n**2 + 5 * n + 2,
lambda n: n**2,
)
# Verify: n*log(n) + 100n = Theta(n*log(n))
verify_theta(
"n*ln(n)+100n", "n*ln(n)",
lambda n: n * math.log(n) + 100 * n,
lambda n: n * math.log(n),
)
# Verify: log(n!) = Theta(n*log(n))
verify_theta(
"log(n!)", "n*log(n)",
lambda n: sum(math.log(i) for i in range(2, n + 1)),
lambda n: n * math.log(n),
)
# Verify NOT Theta: 2^n vs 3^n (ratio goes to 0)
verify_theta(
"2^n", "3^n",
lambda n: 2**n,
lambda n: 3**n,
)
Summary¶
-
Three proof approaches: Direct constants, separate O+Omega, limits.
-
Limit method: lim f(n)/g(n) = c with 0 < c < infinity implies Theta. This is often the fastest approach.
-
Sandwich theorem: If f is squeezed between two Theta(g) functions, then f is also Theta(g).
-
Merge sort proof: By induction on the recurrence T(n) = 2T(n/2) + cn, both upper and lower bounds give n log n, confirming Theta(n log n).
-
Master Theorem: Gives direct Theta results for divide-and-conquer recurrences T(n) = aT(n/b) + f(n).
-
Polynomial functions are always Theta of their leading term.
-
log(n!) = Theta(n log n): A key result for comparison sorting lower bounds.
-
Computational verification: Check that f(n)/g(n) converges to a positive constant for increasing n.
Next: Continue to the Interview Guide for interview preparation.