Computational complexity is the study of how much time and memory resources are required to solve different types of problems using computers. It provides a mathematical framework for classifying problems based on how difficult they are …
When we analyze how long an algorithm takes, we don't count seconds or milliseconds because those depend on the specific computer. Instead, we count fundamental operations like comparisons, additions, or array accesses. A sorting algorithm that compares pairs of numbers will perform different numbers of comparisons depending on how many items need sorting.
The key insight is examining how runtime changes when input size increases. If sorting 10 numbers takes 45 comparisons but sorting 100 numbers takes 4,950 comparisons, we observe a quadratic relationship: doubling the input roughly quadruples the work. We express this using "Big O notation"—this quadratic algorithm would be O(n²), meaning time grows proportional to the square of input size n.
Different algorithms exhibit different growth patterns. Linear time O(n) means doubling input doubles work, like reading every item in a list once. Logarithmic time O(log n) appears in binary search, where doubling input size adds just one more step because we eliminate half the possibilities each time. Exponential time O(2ⁿ) means adding one more input item doubles the total work, making large problems quickly unsolvable.
Space complexity measures how much memory an algorithm needs beyond the input itself. Some algorithms work "in place" using only a few extra variables, achieving O(1) constant space. Others must allocate auxiliary arrays, hash tables, or other data structures that grow with input size, consuming O(n) or more space.
Recursive algorithms present a special case because each recursive call adds a frame to the call stack, storing local variables and return addresses. Computing factorial recursively uses O(n) space despite the simple calculation, since all n recursive calls remain active on the stack until reaching the base case. This stack depth becomes a practical limitation for very large inputs.
Space-time tradeoffs are fundamental in algorithm design. Dynamic programming solves complex problems by storing previously computed results in tables, trading O(n) or O(n²) memory for dramatic time savings. Conversely, some problems can be solved with minimal memory but require recalculating values repeatedly, spending more time to save space.
Complexity classes organize computational problems into categories sharing similar resource requirements. Class P contains all problems solvable in polynomial time—where runtime is bounded by some polynomial like n³ or n¹⁰⁰. These problems are considered "tractable" because solutions exist that scale reasonably with input size, even if large exponents make them slow in practice.
Class NP includes problems where proposed solutions can be verified quickly in polynomial time, even if finding solutions seems much harder. Sudoku exemplifies this: checking whether a completed grid is valid takes moments, but finding the solution might require extensive search. The famous unsolved question "P versus NP" asks whether every problem with quickly-verifiable solutions also has a quick method for finding those solutions.
Beyond P and NP lie harder classes. NP-complete problems are the "hardest" problems in NP—if any one could be solved efficiently, all NP problems could be. EXPTIME contains problems requiring exponential time, where even small inputs might need more computation than exists in the universe. These hierarchies help us understand which problems are fundamentally difficult versus merely lacking clever algorithms.
Reduction is a transformation technique that takes any instance of problem A and converts it into an equivalent instance of problem B in polynomial time. If we can reduce A to B, then B is at least as hard as A—any efficient algorithm for B automatically gives us an efficient algorithm for A by first performing the reduction. This relationship establishes a hierarchy of problem difficulty.
Reductions proved that thousands of diverse problems are actually NP-complete by showing they're all equivalent in difficulty. The first NP-complete problem, Boolean satisfiability (SAT), asks whether logical formulas can be made true. Researchers then reduced SAT to graph coloring, graph coloring to scheduling, and scheduling to countless other problems, building a web of equivalences. Now if anyone solves one NP-complete problem efficiently, all thousands would become solvable.
Reductions also serve practical purposes beyond theory. When facing a new problem, finding a reduction to a well-studied problem lets us apply existing optimized algorithms and software libraries. This approach is ubiquitous in practice: many real-world optimization tasks get reduced to linear programming or integer programming, where mature solvers can tackle them efficiently.
Lower bounds demonstrate that certain problems require at least a specified amount of time or space, regardless of algorithmic cleverness. Comparison-based sorting has a proven lower bound of O(n log n)—any algorithm that sorts by comparing elements must perform at least that many comparisons in the worst case. This isn't because we haven't found better algorithms; it's mathematically impossible to do better with comparisons alone.
These impossibility results rely on adversarial arguments and information-theoretic reasoning. For sorting, there are n! possible orderings of n items, and each comparison can at best distinguish between two possibilities. Since log₂(n!) grows as n log n, that many questions must be asked to identify which ordering we have. Non-comparison sorts like counting sort can beat this bound by exploiting additional structure in the input.
The halting problem represents a more fundamental limit: no algorithm can determine whether arbitrary programs will eventually stop or run forever. Alan Turing proved this through diagonalization, showing that any supposed solution could be used to construct a paradoxical program. Such undecidability results reveal absolute boundaries—problems that no amount of computational resources can solve, establishing the outer edges of what computation can achieve.