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 , where is the set of all algorithms1, is the input size, and is the set of possible functions with as input, normally, , where , is the input size of the algorithm. We have time complexity for and when the input size of is .
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 1 Linear Search
Require: array , target
Ensure: index of or
1:for to do
2:if then
3:return // target found at index
4:end if
5:end for
6:return // target not found
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 , and the time complexity of the linear search algorithm is , where 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 = 5target_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]print(linear_search(target_list, target)) # Output: 4What is the time complexity of the linear search algorithm in this call?
The answer is constant function, , but not linear function, . 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, , , , etc.
By assuming the input of an algorithm is of variable size , we use big-O notation to analyse the asymptotic upper bound of the time complexity when . We state that, in the worst case, the time complexity of an algorithm with input size has , if there exist constants and such that for all , . where is the exact time complexity of the algorithm, and is the upper bound of the time complexity.
In natural language, we say that the time complexity of the algorithm is if there exists a constant and a positive integer such that, for all input sizes greater than , the time complexity of the algorithm is bounded above by .
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.
For the linear search algorithm, the worst-case time complexity is , where is the length of the array. This is because, we have , and , when , , .
In this case, we have more than what we need to know about the worst-case time complexity, because we need only , , however we have , . 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, , , , etc.
By assuming the input of an algorithm is of variable size , we use big-Omega notation to analyse the asymptotic lower bound of the time complexity when . We state that, in the best case, the time complexity of an algorithm with input size has , if there exist constants and such that for all , . where is the exact time complexity of the algorithm, and is the lower bound of the time complexity.
In natural language, we say that the time complexity of the algorithm is if there exists a constant and a positive integer such that, for all input sizes greater than , the time complexity of the algorithm is bounded below by .
Again, we use linear search as an example to illustrate the best-case time complexity.
For the linear search algorithm, the best-case time complexity is , which means that the best-case time complexity is constant. This is because, we have , and , when , , .
Average-case Time Complexity (Big-Theta Notation)
Let be the set of all input instances of size , and let denote the running time of an algorithm on input in . Suppose there is a probability distribution over these inputs for example, the uniform distribution, meaning that for all we have Then the average running time is defined as In the case where every input is equally likely, this expression simplifies to
We say that an algorithm has an average-case time complexity of if there exist positive constants and and a function such that, for all sufficiently large , the average running time of the algorithm on inputs of size is bounded above and below by and , respectively. In other words, the average-case time complexity is if there exist constants , , and such that, for all , we have .
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.
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 . If the target is present, the average number of comparisons is . If the target is not present, it takes comparisons. Assuming each of these scenarios (target at index 0 to n-1, or not present) is equally likely, the average number of comparisons is , which simplifies to a value that is proportional to . Thus, .
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 be the space complexity of an algorithm, where 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.
The linear search algorithm, as presented, has a space complexity of . 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 .
For recursive algorithms, space complexity must account for the space used by the call stack. Consider a recursive implementation of calculating the -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 . Therefore, .
Algorithm Complexity Rigorously Explained2
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 be a fixed algorithm. Let be the set of all possible inputs to . For each input , let denote its size.
-
A cost function 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 .
- Worst-case complexity:
- Best-case complexity:
- Average-case complexity: Requires a probability distribution on the set of inputs . Then, . For a discrete uniform distribution over possible inputs of size , this becomes .
Asymptotic Notation as Set Relations
The notations , , and describe relationships between sets of functions. Let be the set of all functions .
When we state , we mean that the function (which could be , , etc.) is an element of the set .
The Role of the Computational Model
The exact cost function depends on the underlying computational model (e.g., Random Access Machine (RAM), Turing Machine). For instance:
- On a RAM, arithmetic operations might take time.
- On a multi-tape Turing Machine, they might take 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.
Discussion