Requests with keys that all hash to the same bucket, turning O ( 1 ) O(1) O ( 1 ) lookups into O ( n ) O(n) O ( n ) lookups and Causing CPU exhaustion. This is why many languages (Python, Rust, Go) now use hash randomisation.
Amortised analysis gives a tighter bound for a sequence of operations when individual operations may be expensive but the expensive operations are rare enough that the total cost is bounded.
Compute the total cost of n n n operations and divide by n n n .
Dynamic array (e.g., Python listC++ std::vector):
Append is O ( 1 ) O(1) O ( 1 ) when there is capacity, O ( n ) O(n) O ( n ) when resizing is needed Resizing doubles the capacity: after growing from k k k to 2 k 2k 2 k The next k k k appends are O ( 1 ) O(1) O ( 1 ) Total cost for n n n appends: 1 + 1 + ⋯ + 1 + n + 1 + 1 + ⋯ 1 + 1 + \cdots + 1 + n + 1 + 1 + \cdots 1 + 1 + ⋯ + 1 + n + 1 + 1 + ⋯ where the n n n cost occurs at sizes 1 , 2 , 4 , 8 , … 1, 2, 4, 8, \ldots 1 , 2 , 4 , 8 , … Total: n + 1 + 2 + 4 + ⋯ + n = n + 2 n − 1 = 3 n − 1 n + 1 + 2 + 4 + \cdots + n = n + 2n - 1 = 3n - 1 n + 1 + 2 + 4 + ⋯ + n = n + 2 n − 1 = 3 n − 1 Amortised cost per operation: O ( 1 ) O(1) O ( 1 ) Assign an amortised cost to each operation. The amortised cost must be at least the actual cost. The surplus accumulates as credit that pays for future expensive operations.
For dynamic array append:
Assign amortised cost of 3 per append (actual cost is 1 when no resize, k + 1 k+1 k + 1 when resizing from k k k ) When no resize: spend 1, save 2 as credit (1 for the slot, 1 for future resizing) When resizing from k k k to 2 k 2k 2 k : the k k k items already have 1 credit each from previous inserts, providing k k k credit to pay for the k k k copies Credit never goes negative, so the amortised bound is valid Define a potential function Φ \Phi Φ on the data structure state. The amortised cost of operation i i i is:
c ^ i = c i + Φ ( D i ) − Φ ( D i − 1 ) \hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1}) c ^ i = c i + Φ ( D i ) − Φ ( D i − 1 ) Where c i c_i c i is the actual cost and Φ ( D i ) \Phi(D_i) Φ ( D i ) is the potential after the operation.
For a dynamic array with size n n n and capacity m m m :
Φ ( D ) = 2 n − m \Phi(D) = 2n - m Φ ( D ) = 2 n − m After an O ( 1 ) O(1) O ( 1 ) insert (no resize): Φ \Phi Φ increases by 2, amortised cost = 1 + 2 = 3 1 + 2 = 3 1 + 2 = 3 After a resize from m m m to 2 m 2m 2 m : Φ \Phi Φ goes from 2 m − m = m 2m - m = m 2 m − m = m to 2 m − 2 m = 0 2m - 2m = 0 2 m − 2 m = 0 A drop of m m m Amortised cost = m + 0 − m = 0 m + 0 - m = 0 m + 0 − m = 0 (the actual cost of m m m is fully paid by the potential drop) Total amortised cost: O ( 1 ) O(1) O ( 1 ) per operation.
self .data = [ 0 ] * 1 # initial capacity = 1
if self .size == self .capacity:
# Resize: O(capacity) work, but amortised O(1)
new_data = [ 0 ] * ( self .capacity * 2 )
for i in range ( self .size):
new_data[i] = self .data[i]
self .data[ self .size] = value
# Amortised O(1) per append over n operations
# Total: n inserts + sum of resize costs = n + 1 + 2 + 4 + ... + n = 3n
Space complexity measures the additional memory an algorithm uses beyond the input. Like time Complexity, it is expressed in asymptotic notation.
Algorithm Time Space Notes In-place quicksort O ( n log n ) O(n \log n) O ( n log n ) avgO ( log n ) O(\log n) O ( log n ) Stack depth for recursion Merge sort O ( n log n ) O(n \log n) O ( n log n ) O ( n ) O(n) O ( n ) Auxiliary array Heap sort O ( n log n ) O(n \log n) O ( n log n ) O ( 1 ) O(1) O ( 1 ) True in-place DFS O ( V + E ) O(V + E) O ( V + E ) O ( V ) O(V) O ( V ) Recursion stack / explicit stack BFS O ( V + E ) O(V + E) O ( V + E ) O ( V ) O(V) O ( V ) Queue for frontier Dynamic programming (2D) Varies O ( n ⋅ m ) O(n \cdot m) O ( n ⋅ m ) Full table DP with rolling array Same time O ( min ( n , m ) ) O(\min(n, m)) O ( min ( n , m )) Space-optimised
Many algorithms can trade space for time or vice versa:
Memoisation trades O ( n ) O(n) O ( n ) space for exponential-to-polynomial time reductionBloom filters trade a small false positive rate for massive space savings (membership testing)Suffix arrays trade construction time for less space than suffix treesCounting sort trades O ( k ) O(k) O ( k ) space (where k k k is the range of values) for O ( n ) O(n) O ( n ) timeA lower bound is a proof that no algorithm in a given model of computation can do better than a Certain complexity.
Any comparison-based sorting algorithm requires Ω ( n log n ) \Omega(n \log n) Ω ( n log n ) comparisons in the worst case.
Proof sketch (decision tree argument):
A comparison-based sort can be modelled as a binary decision tree Each internal node represents a comparison, each leaf represents a permutation There are n ! n! n ! possible permutations of n n n elements A binary tree of height h h h has at most 2 h 2^h 2 h leaves Therefore: 2 h ≥ n ! 2^h \ge n! 2 h ≥ n ! So h ≥ log 2 ( n ! ) = Ω ( n log n ) h \ge \log_2(n!) = \Omega(n \log n) h ≥ log 2 ( n !) = Ω ( n log n ) (by Stirling’s approximation) This is why non-comparison sorts (counting sort, radix sort) can beat O ( n log n ) O(n \log n) O ( n log n ) , they do not Compare elements pairwise, so the decision tree argument does not apply.
Determining whether all elements in an array are distinct requires Ω ( n log n ) \Omega(n \log n) Ω ( n log n ) time in the Comparison model. This follows from the sorting lower bound (sort, then check adjacent elements).
Unordered array: Ω ( n ) \Omega(n) Ω ( n ) comparisons (must examine every element in the worst case) Sorted array: O ( log n ) O(\log n) O ( log n ) with binary search, and this is optimal for comparison-based search A decision problem is one whose answer is yes or no. Examples: “Does this graph have a Hamiltonian Cycle?” “Is there a subset of these numbers that sums to k k k ?”
Optimisation problems can often be reduced to decision problems: “What is the shortest tour?” Becomes “Is there a tour of length at most k k k ?” (binary search on k k k ).
Class Definition Example Problems P Solvable in polynomial time Sorting, shortest path, MST NP Verifiable in polynomial time SAT, travelling salesman, graph colouring NP-Complete In NP, and every NP problem reduces to it SAT, 3-SAT, vertex cover NP-Hard At least as hard as NP-complete (may not be in NP) Halting problem, TSP optimisation
graph TD
P["P<br/>Polynomial time"]
NP["NP<br/>Verifiable in polynomial time"]
NPC["NP-Complete<br/>Hardest problems in NP"]
NPH["NP-Hard<br/>At least as hard as NP"]
P --> NP
NPC --> NP
NPH -.-> NPC
style P fill:#27ae60,color:#fff
style NP fill:#3498db,color:#fff
style NPC fill:#e74c3c,color:#fff
style NPH fill:#8e44ad,color:#fff A problem A A A reduces to problem B B B (written A ≤ p B A \le_p B A ≤ p B ) if an algorithm for B B B can be used to solve A A A in polynomial time. If A A A is NP-complete and A ≤ p B A \le_p B A ≤ p B Then B B B is also NP-hard. If B B B is also in NP, then B B B is NP-complete.
Cook-Levin Theorem: SAT (Boolean satisfiability) is NP-complete. Every other NP-complete problem Is proven NP-complete by reducing from a known NP-complete problem.
Problem Input Question Practical Significance SAT Boolean formula Is there a satisfying assignment? Basis for all NP-completeness proofs 3-SAT 3-CNF formula Is there a satisfying assignment? Circuit design, scheduling Vertex Cover Graph G G G Integer k k k Is there a vertex cover of size ≤ k \le k ≤ k ? Network monitoring Travelling Salesman Graph with weights, integer k k k Is there a tour of length ≤ k \le k ≤ k ? Logistics, routing Subset Sum Set of integers, target t t t Is there a subset summing to t t t ? Knapsack variants Graph Colouring Graph G G G Integer k k k Can G G G be coloured with k k k colours? Register allocation, scheduling Clique Graph G G G Integer k k k Does G G G contain a clique of size k k k ? Social network analysis
When you encounter an NP-hard problem:
Restrict the input . Many NP-hard problems become polynomial on restricted inputs (e.g., TSP on a tree, graph colouring on a bipartite graph)Approximation algorithms . Find a solution within a guaranteed factor of optimal (e.g., 2-approx for vertex cover, 1.5-approx for metric TSP with Christofides’ algorithm)Heuristics . Greedy algorithms, local search, simulated annealing, genetic algorithms. No guarantees, but often work well in practiceFixed-parameter tractability . If the problem is NP-hard but polynomial for fixed parameter k k k Use FPT algorithms (e.g., vertex cover is O ( 2 k ⋅ n ) O(2^k \cdot n) O ( 2 k ⋅ n ) )SAT solvers . For many combinatorial problems, encoding as SAT and using a modern solver (CDCL-based) is surprisingly effectiveAsymptotic analysis ignores the memory hierarchy. In practice, cache effects dominate:
Sequential access (arrays): prefetcher-friendly, ~1 ns per access from L1 cacheRandom access (linked lists): cache-unfriendly, ~100 ns per miss to main memoryB-trees vs binary trees : B-trees are designed for disk/cache-line-sized blocks, reducing the number of cache misses per operation by a factor of log 2 B \log_2 B log 2 B where B B B is the block sizeA linked list traversal that is O ( n ) O(n) O ( n ) in theory can be 10-100x slower than an array traversal that is also O ( n ) O(n) O ( n ) Because the array has spatial locality.
Big-O hides constant factors. O ( n ) O(n) O ( n ) with a constant of 1000 is slower than O ( n log n ) O(n \log n) O ( n log n ) with a Constant of 1 for n < 2 1000 n \lt 2^{1000} n < 2 1000 . In practice, the constants matter enormously:
Radix sort has O ( n ⋅ k ) O(n \cdot k) O ( n ⋅ k ) time but small constants and excellent cache behaviour, making it faster than comparison sort for integers in practice Insertion sort is O ( n 2 ) O(n^2) O ( n 2 ) but has tiny constants and is adaptive, making it the fastest sort for n < 50 n \lt 50 n < 50 or nearly-sorted data Modern CPUs deeply pipeline instructions and speculate on branch outcomes. A branch that is Unpredictable can cost 15-20 cycles per misprediction. Algorithms with unpredictable branching Patterns (e.g., quicksort on adversarial data, binary search on random data) suffer significantly.
Conditional moves (cmov instructions) and branchless implementations can eliminate misprediction Penalties for small inner loops:
# Branchless max (conceptual, actual implementation uses cmov)
def branchless_max ( a , b ):
# mask = (a - b) >> 31 (sign bit: 1 if a < b, 0 otherwise)
# result = a ^ ((a ^ b) & mask)
return a if a >= b else b
Saying “this algorithm is O ( 1 ) O(1) O ( 1 ) ” when you mean Θ ( 1 ) \Theta(1) Θ ( 1 ) is imprecise. Technically, every Algorithm is O ( 2 n ) O(2^n) O ( 2 n ) because O O O is only an upper bound. If you claim O ( 1 ) O(1) O ( 1 ) You should be Prepared to justify it as a tight bound.
Worst-case analysis is essential for guarantees, but average-case analysis matters for real Performance. Quicksort is O ( n 2 ) O(n^2) O ( n 2 ) worst case but O ( n log n ) O(n \log n) O ( n log n ) average case with a small constant, This is why it is the default sort in most standard libraries (with introsort fallback).
An O ( n ) O(n) O ( n ) time algorithm that uses O ( n 2 ) O(n^2) O ( n 2 ) space is often worse than an O ( n log n ) O(n \log n) O ( n log n ) algorithm that uses O ( 1 ) O(1) O ( 1 ) space. Memory is not infinite, and allocation is not free.
The Master Theorem requires the recurrence to be of the exact form T ( n ) = a T ( n / b ) + f ( n ) T(n) = aT(n/b) + f(n) T ( n ) = a T ( n / b ) + f ( n ) . If your Subproblems are of different sizes (e.g., quicksort’s T ( n ) = T ( k ) + T ( n − k − 1 ) + O ( n ) T(n) = T(k) + T(n-k-1) + O(n) T ( n ) = T ( k ) + T ( n − k − 1 ) + O ( n ) ), you need a Different analysis technique.
The O ( n log n ) O(n \log n) O ( n log n ) sorting lower bound only applies to comparison-based sorts. Counting sort, radix Sort, and bucket sort all beat this bound by using additional information about the input (integer Keys, bounded range, uniform distribution). Similarly, the element uniqueness lower bound is Ω ( n log n ) \Omega(n \log n) Ω ( n log n ) only in the comparison model.
When analysing complexity, focus on the operation that scales with input size. A hash table has O ( 1 ) O(1) O ( 1 ) average-case lookup, but if your keys are strings and the hash function scans each character, The actual cost is O ( k ) O(k) O ( k ) where k k k is the key length. If k k k grows with n n n (e.g., storing all Substrings), the “constant-time” lookup is not actually constant.
Amortised O ( 1 ) O(1) O ( 1 ) means the average over many operations is constant. Individual operations can still be O ( n ) O(n) O ( n ) . In a latency-sensitive system (real-time trading, game loop, audio processing), a single O ( n ) O(n) O ( n ) operation can cause a deadline miss even if the amortised cost is fine. Use data structures with worst-case guarantees (e.g., std::deque instead of std::vector with occasional reallocation) For real-time contexts.
Big-O notation hides constants, but constants matter in practice. An O ( n ) O(n) O ( n ) algorithm with a Constant of 10,000 is slower than an O ( n log n ) O(n \log n) O ( n log n ) algorithm with a constant of 1 for any n n n that Fits in memory. When comparing two algorithms with the same Big-O complexity, benchmark with Realistic data sizes. The constant factors include: number of memory accesses (cache misses Dominate), number of branches (mispredictions cost 15-20 cycles each), and allocation count (heap Allocations are orders of magnitude slower than stack allocations).
When the Master Theorem does not apply (e.g., unequal subproblem sizes), use the recursion tree Method. Draw the recursion tree, compute the work at each level, and sum across all levels.
Example: T ( n ) = T ( n / 3 ) + T ( 2 n / 3 ) + O ( n ) T(n) = T(n/3) + T(2n/3) + O(n) T ( n ) = T ( n /3 ) + T ( 2 n /3 ) + O ( n )
The recursion tree has:
Level 0: work O ( n ) O(n) O ( n ) 1 node of size n n n Level 1: work O ( n / 3 ) + O ( 2 n / 3 ) = O ( n ) O(n/3) + O(2n/3) = O(n) O ( n /3 ) + O ( 2 n /3 ) = O ( n ) 2 nodes Level 2: work O ( n / 9 ) + O ( 2 n / 9 ) + O ( 2 n / 9 ) + O ( 4 n / 9 ) = O ( n ) O(n/9) + O(2n/9) + O(2n/9) + O(4n/9) = O(n) O ( n /9 ) + O ( 2 n /9 ) + O ( 2 n /9 ) + O ( 4 n /9 ) = O ( n ) 4 nodes … Each level does O ( n ) O(n) O ( n ) total work The tree height is log 3 / 2 n \log_{3/2} n log 3/2 n (the longest root-to-leaf path goes by the 2/3 branch) Total: O ( n log n ) O(n \log n) O ( n log n ) The Akra-Bazzi theorem generalises the Master Theorem for recurrences of the form:
T ( x ) = ∑ i = 1 k a i T ( b i x + h i ( x ) ) + f ( x ) T(x) = \sum_{i=1}^{k} a_i T(b_i x + h_i(x)) + f(x) T ( x ) = i = 1 ∑ k a i T ( b i x + h i ( x )) + f ( x ) Where a i > 0 a_i \gt 0 a i > 0 , 0 < b i < 1 0 \lt b_i \lt 1 0 < b i < 1 And h i ( x ) = O ( x / log 2 x ) h_i(x) = O(x / \log^2 x) h i ( x ) = O ( x / log 2 x ) . Find p p p such that ∑ i = 1 k a i b i p = 1 \sum_{i=1}^{k} a_i b_i^p = 1 ∑ i = 1 k a i b i p = 1 . Then:
T ( x ) = Θ ( x p ( 1 + ∫ 1 x f ( u ) u p + 1 d u ) ) T(x) = \Theta\left(x^p \left(1 + \int_1^x \frac{f(u)}{u^{p+1}} du\right)\right) T ( x ) = Θ ( x p ( 1 + ∫ 1 x u p + 1 f ( u ) d u ) ) This handles cases like T ( n ) = T ( n / 3 ) + T ( 2 n / 3 ) + O ( n ) T(n) = T(n/3) + T(2n/3) + O(n) T ( n ) = T ( n /3 ) + T ( 2 n /3 ) + O ( n ) where the subproblem sizes are not equal.
For algorithms whose running time depends on the input distribution (e.g., quicksort), probabilistic Analysis gives expected running time over a random input. Quicksort with random pivot selection has Expected O ( n log n ) O(n \log n) O ( n log n ) comparisons, but the expected number of comparisons can be computed exactly:
E[\mathrm{comparisons] = 2(n+1)H_n - 4n \approx 1.386 n \log_2 n Where H n = ∑ i = 1 n 1 / i H_n = \sum_{i=1}^{n} 1/i H n = ∑ i = 1 n 1/ i is the n n n -th harmonic number. The constant 1.386 1.386 1.386 is about 39% more comparisons than the information-theoretic minimum of n log 2 n n \log_2 n n log 2 n Which is remarkably close To optimal for a comparison sort.
Worst-case analysis can be too pessimistic for algorithms that perform well on typical inputs but Badly on adversarial ones. Smoothed analysis (Spielman and Teng, 2004) measures expected performance under small random perturbations of the input. It explains why the simplex method for linear Programming is efficient in practice despite having exponential worst-case complexity: the Adversarial inputs that trigger exponential behaviour are unstable under small perturbations.
For online algorithms (where future input is unknown), competitive analysis compares the algorithm’s Performance to the optimal offline algorithm. An algorithm is c c c -competitive if its cost is at most c c c times the optimal cost for every input sequence.
Online Problem Algorithm Competitive Ratio Paging (caching) LRU k k k (where k k k = cache size)Paging (caching) FIFO k k k K-server Work function algorithm 2 k − 1 2k - 1 2 k − 1 Load balancing Greedy O ( log n ) O(\log n) O ( log n ) Ski rental Buy after n n n rentals 2
When data does not fit in memory, the cost model changes. The external memory model (Aggarwal and Vitter, 1988) counts:
I/O operations: transferring a block of size B B B between memory and diskMemory size: M M M words available in internal memoryDisk size: N N N words on diskAlgorithm Internal Memory External Memory (I/Os) Scanning O ( N ) O(N) O ( N ) timeO ( N / B ) O(N/B) O ( N / B ) I/OsSorting O ( N log N ) O(N \log N) O ( N log N ) timeO ( ( N / B ) log M / B ( N / B ) ) O((N/B) \log_{M/B}(N/B)) O (( N / B ) log M / B ( N / B )) I/OsBST search O ( log N ) O(\log N) O ( log N ) timeO ( log B N ) O(\log_B N) O ( log B N ) I/OsB-tree search O ( log N ) O(\log N) O ( log N ) timeO ( log B N ) O(\log_B N) O ( log B N ) I/Os
The gap between internal and external memory complexity is why B-trees exist: a binary tree search does O ( log 2 N ) O(\log_2 N) O ( log 2 N ) I/Os (one per level), while a B-tree search does O ( log B N ) O(\log_B N) O ( log B N ) I/Os. For N = 10 9 N = 10^9 N = 1 0 9 and B = 100 B = 100 B = 100 , binary tree needs ~30 I/Os while B-tree needs ~5 I/Os, a 6x improvement.
Splay trees are self-adjusting BSTs with no explicit balance information. Every access is followed by a “splay” operation that moves the accessed node to the root using a sequence of rotations. The Amortised cost of each operation is O ( log n ) O(\log n) O ( log n ) Proven using the potential method.
The potential function for splay trees is:
\Phi(T) = \sum_{v \in T} \log_2(\mathrm{size(v)) Where size(v) is the number of nodes in the subtree rooted at v. The potential is always non-negative and is O ( n log n ) O(n \log n) O ( n log n ) for an n n n -node tree.
Key properties:
No balance information stored, simpler implementation Access pattern adapts to workload, frequently accessed nodes move near the root Static optimality theorem: splay trees perform within a constant factor of the optimal static tree for any access sequence Working set theorem: if an item is accessed t t t times and there are l l l distinct items accessed since its last access, the amortised cost is O ( log l + log t ) O(\log l + \log t) O ( log l + log t )