Growth of functions
For a cost function f(n):
| Notation | Meaning | Read as |
|---|---|---|
f(n) = O(g(n)) | There are constants c, n₀ with f(n) ≤ c·g(n) for all n ≥ n₀ | Upper bound: grows no faster than g |
f(n) = Ω(g(n)) | f(n) ≥ c·g(n) for all n ≥ n₀ | Lower bound: grows at least as fast |
f(n) = Θ(g(n)) | Both of the above | Tight bound: grows exactly like g |
Interviews say “O” when they mean “Θ”: “binary search is O(log n)” means its worst case grows like log n.
Rules that do most of the work:
- Drop constants and lower-order terms:
3n² + 10n + 7 = Θ(n²). - The log base doesn’t matter:
log₂ nandlog₁₀ ndiffer by a constant factor. - Sequential steps add, nested steps multiply: a loop of n containing a
log nsearch isΘ(n log n). - Count what dominates: for recursion, count calls times work per call (or use a recurrence, below).
| Growth | Example | n = 10⁶ (rough operations) |
|---|---|---|
O(1) | Hash map lookup, array index | 1 |
O(log n) | Binary search, fast power | 20 |
O(n) | One pass over the input | 10⁶ |
O(n log n) | Merge sort, sort then scan | 2 × 10⁷ |
O(n²) | All pairs | 10¹² (too slow) |
O(2ⁿ) | All subsets | Hopeless past n ≈ 40 |
A useful rule of thumb: about 10⁸ simple operations per second in C++ and roughly 10⁷ in Python, so read
complexity off the constraints. With n up to 10⁵, an O(n²) solution won’t pass but O(n log n) will.
Recurrences and the Master Theorem
A divide-and-conquer algorithm that splits a problem of size n into a subproblems of size n/b, plus f(n) work to split and combine, costs:
T(n) = a · T(n / b) + f(n)The recursion tree has log_b n levels; level i has aⁱ nodes. Compare the combine work f(n) with
n^(log_b a), the number of leaves:
| Case | Condition | Result | Intuition |
|---|---|---|---|
| 1 | f(n) grows polynomially slower than n^(log_b a) | Θ(n^(log_b a)) | The leaves dominate |
| 2 | f(n) = Θ(n^(log_b a)) | Θ(n^(log_b a) · log n) | Every level costs the same |
| 3 | f(n) grows polynomially faster, and a·f(n/b) ≤ k·f(n) for some k < 1 | Θ(f(n)) | The root dominates |
| Recurrence | Algorithm | a, b, n^(log_b a) | Case | Solution |
|---|---|---|---|---|
T(n) = T(n/2) + O(1) | Binary search, fast power | 1, 2, 1 | 2 | Θ(log n) |
T(n) = 2T(n/2) + O(n) | Merge sort | 2, 2, n | 2 | Θ(n log n) |
T(n) = 2T(n/2) + O(1) | Tree traversal, max of an array by halves | 2, 2, n | 1 | Θ(n) |
T(n) = 3T(n/2) + O(n) | Karatsuba multiplication | 3, 2, n^1.585 | 1 | Θ(n^1.585) |
T(n) = 2T(n/2) + O(n²) | Quadratic combine step | 2, 2, n | 3 | Θ(n²) |
It doesn’t cover everything. Subtract-and-conquer recurrences like T(n) = T(n − 1) + O(n) (selection sort,
naive recursion that peels one element) unroll to n + (n − 1) + … = Θ(n²), and uneven splits need the recursion
tree or the Akra–Bazzi method.
50. Pow(x, n)
LeetCode 50. Compute xⁿ for a float x and an integer n that can be negative and as large as 2³¹ in magnitude.
| Approach | Time | Space |
|---|---|---|
Brute force: multiply x by itself abs(n) times | O(n) | O(1) |
| Better: recursive fast power (square the half) | O(log n) | O(log n) call stack |
| Optimal: iterative binary exponentiation | O(log n) | O(1) |
def my_pow_brute(x, n): result = 1.0 for _ in range(abs(n)): result *= x return result if n >= 0 else 1 / resultdef my_pow_recursive(x, n): if n < 0: return 1 / my_pow_recursive(x, -n) if n == 0: return 1.0 half = my_pow_recursive(x, n // 2) # T(n) = T(n/2) + O(1) return half * half * (x if n % 2 else 1)def my_pow(x, n): if n < 0: x, n = 1 / x, -n result = 1.0 while n: if n & 1: # this bit of n is set: include the current power of x result *= x x *= x # x, x², x⁴, x⁸, ... n >>= 1 return resultKey insight: x¹³ = x⁸ · x⁴ · x¹ because 13 is 1101 in binary. Squaring produces each power of two in one
multiplication, so only log₂ n steps are needed. With n up to 2³¹, the brute force would take two billion
multiplications.
69. Sqrt(x)
LeetCode 69. Return ⌊√x⌋ for a non-negative integer x, without using a built-in power or square-root function.
| Approach | Time | Space |
|---|---|---|
| Brute force: try 1, 2, 3, … until the square passes x | O(√x) | O(1) |
| Optimal: binary search on the answer | O(log x) | O(1) |
| Optimal, fewer steps: integer Newton’s method | O(log x) worst case, quadratic convergence near the root | O(1) |
def my_sqrt_brute(x): r = 0 while (r + 1) * (r + 1) <= x: r += 1 return rdef my_sqrt(x): lo, hi = 0, x while lo <= hi: mid = (lo + hi) // 2 if mid * mid <= x: lo = mid + 1 # mid works; look for something bigger else: hi = mid - 1 # mid is too big return hi # the largest value whose square is <= xdef my_sqrt_newton(x): r = x while r * r > x: r = (r + x // r) // 2 # average r and x/r; converges from above return rKey insight: “the largest r with r² ≤ x” is a monotone yes/no question over a sorted range, which is exactly what binary search answers. Binary search on the answer (not on an array) shows up again in capacity, rate, and “minimum maximum” problems.
Merge sort and its recurrence
Self-assigned: implement merge sort and derive its running time.
| Approach | Time | Space |
|---|---|---|
| Brute force: insertion sort | O(n²) worst case | O(1) |
| Optimal comparison sort: merge sort | O(n log n) in every case | O(n) |
def insertion_sort(a): a = list(a) for i in range(1, len(a)): x, j = a[i], i - 1 while j >= 0 and a[j] > x: # shift bigger elements right a[j + 1] = a[j] j -= 1 a[j + 1] = x return adef merge_sort(a): if len(a) <= 1: return list(a) mid = len(a) // 2 left, right = merge_sort(a[:mid]), merge_sort(a[mid:]) # 2 · T(n/2) merged, i, j = [], 0, 0 while i < len(left) and j < len(right): # O(n) merge if left[i] <= right[j]: # <= keeps equal elements in order (stable) merged.append(left[i]) i += 1 else: merged.append(right[j]) j += 1 return merged + left[i:] + right[j:]The recurrence is T(n) = 2T(n/2) + Θ(n): Master Theorem case 2, so Θ(n log n). The recursion tree shows why:
every level does n total work in its merges, and halving gives log₂ n levels.
flowchart TD A[n] --> B[n/2] A --> C[n/2] B --> D[n/4] B --> E[n/4] C --> F[n/4] C --> G[n/4] D --> H[...] G --> I[...]
Key insight: insertion sort is O(n) on nearly sorted input and has tiny constants, which is why real sorts
(Python’s Timsort) use it for short runs and merge the runs merge-sort style. No comparison sort can beat
Ω(n log n) in the worst case: there are n! possible orders and each comparison at best halves them, so you need
log₂(n!) = Θ(n log n) comparisons.
Patterns to remember
- Halving the problem each step gives
log n; halving it but touching everything at each level givesn log n. - Write the recurrence first, then match it to a Master Theorem case.
- Binary search works on any monotone yes/no question, not only on sorted arrays.