Algorithm Complexity
About 2563 wordsAbout 9 min
AlgorithmsMath
2025-07-25
Definition: Time Complexity
The time complexity describes how the running time of the algorithm grows as the size of the input increases. It can be expressed using different notations given varied purposes, which provides an upper bound, lower bound or average performance of the algorithm.
Mathematically, time complexity can be defined as a function T:A×n→F(n), where A is the set of all algorithms[1], n is the input size, and F(n) is the set of possible functions with n as input, normally, F(n)={1,nk,kn,logn(x),ln(x),nlogn(x),n!}, where k∈N, n is the input size of the algorithm. We have time complexity T(a)=f(n) for a∈A and f(n)∈F(n) when the input size of a is n.
This could be a bit abstract, but this is not really the common time complexity definition we use in practice. In real practice, we add another layer of abstraction to the time complexity, benchmarking algorithms in different aspects, such as the worst-case, best-case, and average-case time complexity. We will discuss them in the following sections.
Let's consider the example of linear search, a simple algorithm that searches for a target element in an array. The pseudocode of the linear search algorithm is shown below. We simply designate a target element and traverse the array from the beginning to the end, comparing each element with the target. If the target is found, we return the index of the element; otherwise, we return -1 to indicate that the target is not in the array. Now we will analyze the time complexity of the linear search algorithm.
Algorithm: Linear Search
for i = 0 to n - 1:
if arr[i] == target then:
return i // Target found at index i
end
end
return -1 // Target not found in the array
Example: To figure out the time complexity of the linear search, we first need to determine which metadata decides the input size of the algorithm. Obviously, in this algorithm we only traverse the array linearly, which means the input size of the algorithm is the length of the array. Therefore, we denote the input size as n, and the time complexity of the linear search algorithm is n, where n is the length of the array.
Now we look at a slightly different example, which is also related to Linear Search.
def linear_search(arr: list[int], target: int) -> int:
"""
Linear search algorithm to find the target element in the array.
"""
for i in range(len(arr)):
if arr[i] == target:
return i # Target found at index i
return -1 # Target not found in the array
target = 5
target_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(linear_search(target_list, target)) # Output: 4
Problem: What is the time complexity of the linear search algorithm in this call?
The answer is constant function, f(n)=1, but not linear function, f(n)=n. The reason is that the length of the array is fixed here, instead of being a variable. Therefore, the time complexity of the linear search algorithm in this call is constant. The same goes for a given array of length 100,000, because the length of the array is fixed. This example may confuse some people, but no worries, we will explicitly discuss the worst-case, best-case, and average-case time complexity in the following sections using linear search as an example.
Worst-case Time Complexity (Big-O Notation)
As we said, when we evaluate the time complexity of an algorithm, we usually take certain considerations, and the most common one is how bad the algorithm can perform. This is called the worst-case time complexity, which provides an upper bound on the running time of the algorithm. We use big-O notation to represent the worst-case time complexity, for example, O(n), O(n2), O(logn), etc.
Definition: Big-O Notation
By assuming the input of an algorithm is of variable size n, we use big-O notation to analyse the asymptotic upper bound of the time complexity when n→∞. We state that, in the worst case, the time complexity T(n) of an algorithm with input size n has T(n)=O(g(n)), if there exist constants c>0 and r∈N such that for all n>r, 0≤f(n)≤c⋅g(n). where f(n) is the exact time complexity of the algorithm, and g(n) is the upper bound of the time complexity.
In natural language, we say that the time complexity of the algorithm is O(g(n)) if there exists a constant c and a positive integer r such that, for all input sizes n greater than r, the time complexity of the algorithm is bounded above by c⋅g(n).
Still, we just use linear search as an example, though it's quite special, because its actual worst-case time complexity is exactly the same as the big-O notation, while this does not always happen in other algorithms. We will see more examples in the future.
Example: For the linear search algorithm, the worst-case time complexity is O(n), where n is the length of the array. This is because, we have f(n)=n, and g(n)=n, when c=1, ∀n∈N, 0≤f(n)≤1⋅g(n).
In this case, we have more than what we need to know about the worst-case time complexity, because we need only ∀n>r, 0≤f(n)≤c⋅g(n), however we have ∀n∈N, 0≤f(n)≤c⋅g(n). This is decided by the nature of the linear search algorithm, which is quite simple and straightforward. However, again, this is not always the case, and we will see more examples in the future.
Best-case Time Complexity (Big-Omega Notation)
Opposite to the worst-case time complexity, we have the best-case time complexity, which provides a lower bound on the running time of the algorithm. We use big-Omega notation to represent the best-case time complexity, for example, Ω(n), Ω(n2), Ω(logn), etc.
Definition: Big-Omega Notation
By assuming the input of an algorithm is of variable size n, we use big-Omega notation to analyse the asymptotic lower bound of the time complexity when n→∞. We state that, in the best case, the time complexity T(n) of an algorithm with input size n has T(n)=Ω(g(n)), if there exist constants c>0 and r∈N such that for all n>r, f(n)≥c⋅g(n). where f(n) is the exact time complexity of the algorithm, and g(n) is the lower bound of the time complexity.
In natural language, we say that the time complexity of the algorithm is Ω(g(n)) if there exists a constant c and a positive integer r such that, for all input sizes n greater than r, the time complexity of the algorithm is bounded below by c⋅g(n).
Again, we use linear search as an example to illustrate the best-case time complexity.
Example: For the linear search algorithm, the best-case time complexity is Ω(1), which means that the best-case time complexity is constant. This is because, we have f(n)=1, and g(n)=1, when c=1, ∀n∈N, f(n)≥1⋅g(n).
Average-case Time Complexity (Big-Theta Notation)
Definition: Average Time Complexity
Let I(n) be the set of all input instances of size n, and let T(n,x) denote the running time of an algorithm on input x in I(n). Suppose there is a probability distribution P(x) over these inputs for example, the uniform distribution, meaning that for all x∈I(n) we have P(x)=1/(∣I(n)∣). Then the average running time is defined as Tavg(n)=∑x∈I(n)P(x)⋅T(n,x). In the case where every input is equally likely, this expression simplifies to Tavg(n)=1/(∣I(n)∣)∑x∈I(n)T(n,x).
We say that an algorithm has an average-case time complexity of Θ(g(n)) if there exist positive constants c1 and c2 and a function f(n) such that, for all sufficiently large n, the average running time of the algorithm on inputs of size n is bounded above and below by c1⋅g(n) and c2⋅g(n), respectively. In other words, the average-case time complexity is Θ(g(n)) if there exist constants c1, c2, and n0 such that, for all n≥n0, we have c1⋅g(n)≤Tavg(n)≤c2⋅g(n).
Average-case analysis is a more realistic measure of an algorithm's performance, yet it is also more complex to determine than the worst-case or best-case time complexity. It requires knowledge of the probability distribution of inputs and the expected running time of the algorithm on each input.
To make it easier to understand, we can say that average time complexity, in probability theory, is the expected value of the time complexity of the algorithm, given a probability distribution of inputs, which is usually uniform by default.
Now we will discuss the average-case time complexity of the linear search algorithm, as an example for average-case analysis.
Example: For the linear search algorithm, assuming the target element is equally likely to be at any position in the array or not present at all, the average-case time complexity is Θ(n). If the target is present, the average number of comparisons is (1+2+...+n)/n=(n+1)/2. If the target is not present, it takes n comparisons. Assuming each of these n+1 scenarios (target at index 0 to n-1, or not present) is equally likely, the average number of comparisons is [n(n+1)/2+n2]/[n(n+1)], which simplifies to a value that is proportional to n. Thus, Tavg(n)∈Θ(n).
Space Complexity
Space complexity is a measure of the amount of working storage an algorithm needs. That is, how much memory, in the worst case, is required at any point in the algorithm. As with time complexity, we are typically concerned with how the space requirements grow as the input size grows, and we use asymptotic notation to describe this growth.
Let S(n) be the space complexity of an algorithm, where n is the input size. This includes space for input variables, auxiliary variables, and output variables.
- Auxiliary Space: This refers to the temporary space used by the algorithm, not including the space taken by the input. For many algorithms, the auxiliary space complexity is more interesting than the total space complexity, especially if the input is given and cannot be modified.
Example: Linear Search Space Complexity The linear search algorithm, as presented, has a space complexity of S(n)∈Θ(1). This is because it only uses a fixed number of variables (e.g., i
, target
, and the input array itself, which is considered given). The space required does not grow with the size of the input array n.
Example: Recursive Algorithms and Space Complexity For recursive algorithms, space complexity must account for the space used by the call stack. Consider a recursive implementation of calculating the n-th Fibonacci number:
def fib(n):
if n <= 1:
return n
else:
return fib(n-1) + fib(n-2)
While its time complexity is high, its space complexity is determined by the maximum depth of the recursion tree, which is n. Therefore, S(n)∈Θ(n).
Algorithm Complexity Rigorously Explained[2]
For readers with a background in mathematical analysis and set theory, we can formalize the concepts of algorithm complexity further.
Formalizing Time and Space
Let A be a fixed algorithm. Let I be the set of all possible inputs to A. For each input x∈I, let ∣x∣ denote its size.
A cost function C:I→R+ can represent either time (number of operations) or space (number of memory cells used).
The complexity function for a specific case (worst, best, average) is a function F:N→R+.
- Worst-case complexity: Fworst(n)=supx∈I∣x∣=nC(x)
- Best-case complexity: Fbest(n)=infx∈I∣x∣=nC(x)
- Average-case complexity: Requires a probability distribution μn on the set of inputs {x∈I:∣x∣=n}. Then, Favg(n)=EX∼μn[C(X)]=∫{x:∣x∣=n}C(x)dμn(x). For a discrete uniform distribution over mn possible inputs of size n, this becomes Favg(n)=mn1∑x∈I∣x∣=nC(x).
Asymptotic Notation as Set Relations
The notations O, Ω, and Θ describe relationships between sets of functions. Let G be the set of all functions g:N→R+.
- O(g(n))={f(n)∣∃c>0,n0∈N s.t. ∀n≥n0,0≤f(n)≤c⋅g(n)}
- Ω(g(n))={f(n)∣∃c>0,n0∈N s.t. ∀n≥n0,0≤c⋅g(n)≤f(n)}
- Θ(g(n))=O(g(n))∩Ω(g(n))
When we state F(n)∈O(g(n)), we mean that the function F (which could be Fworst, Fbest, etc.) is an element of the set O(g(n)).
The Role of the Computational Model
The exact cost function C(x) depends on the underlying computational model (e.g., Random Access Machine (RAM), Turing Machine). For instance:
- On a RAM, arithmetic operations might take O(1) time.
- On a multi-tape Turing Machine, they might take O(logn) time if numbers can be arbitrarily large.
However, for most algorithmic analysis in computer science, the RAM model is implicitly assumed, and the focus is on the number of operations relative to the input size, abstracting away constant factors and lower-order terms. This is why asymptotic analysis is so powerful—it provides a machine-independent way to classify algorithms by their fundamental growth rates.