2.3 Time complexity¶
Runtime gives a direct, accurate picture of an algorithm's efficiency. How might we predict the runtime of a piece of code precisely?
- Specify the execution platform, including hardware, programming language, and system environment, all of which affect performance.
- Determine the time required by each operation: for example, addition
+might take 1 ns, multiplication*10 ns, andprint()5 ns. - Count every operation in the code and sum their execution times to obtain the total runtime.
For example, consider the following code with input size \(n\):
This gives a runtime of \((6n + 12)\) ns:
In practice, calculating runtime this way is neither suitable nor realistic. We do not want estimates tied to one platform, since algorithms run on many platforms. Also, the time taken by each operation is difficult to determine, making accurate prediction very hard.
2.3.1 Measuring growth in runtime¶
Time complexity analysis measures how runtime grows as the input gets larger, rather than the runtime itself.
To make this abstract idea concrete, consider three algorithms, A, B, and C, with input size \(n\):
// Algorithm A: constant time complexity
void algorithm_A(int n) {
cout << 0 << endl;
}
// Algorithm B: linear time complexity
void algorithm_B(int n) {
for (int i = 0; i < n; i++) {
cout << 0 << endl;
}
}
// Algorithm C: constant time complexity
void algorithm_C(int n) {
for (int i = 0; i < 1000000; i++) {
cout << 0 << endl;
}
}
// Algorithm A: constant time complexity
void algorithm_A(int n) {
System.out.println(0);
}
// Algorithm B: linear time complexity
void algorithm_B(int n) {
for (int i = 0; i < n; i++) {
System.out.println(0);
}
}
// Algorithm C: constant time complexity
void algorithm_C(int n) {
for (int i = 0; i < 1000000; i++) {
System.out.println(0);
}
}
// Algorithm A: constant time complexity
void AlgorithmA(int n) {
Console.WriteLine(0);
}
// Algorithm B: linear time complexity
void AlgorithmB(int n) {
for (int i = 0; i < n; i++) {
Console.WriteLine(0);
}
}
// Algorithm C: constant time complexity
void AlgorithmC(int n) {
for (int i = 0; i < 1000000; i++) {
Console.WriteLine(0);
}
}
// Algorithm A: constant time complexity
func algorithm_A(n int) {
fmt.Println(0)
}
// Algorithm B: linear time complexity
func algorithm_B(n int) {
for i := 0; i < n; i++ {
fmt.Println(0)
}
}
// Algorithm C: constant time complexity
func algorithm_C(n int) {
for i := 0; i < 1000000; i++ {
fmt.Println(0)
}
}
// Algorithm A: constant time complexity
function algorithm_A(n) {
console.log(0);
}
// Algorithm B: linear time complexity
function algorithm_B(n) {
for (let i = 0; i < n; i++) {
console.log(0);
}
}
// Algorithm C: constant time complexity
function algorithm_C(n) {
for (let i = 0; i < 1000000; i++) {
console.log(0);
}
}
// Algorithm A: constant time complexity
function algorithm_A(n: number): void {
console.log(0);
}
// Algorithm B: linear time complexity
function algorithm_B(n: number): void {
for (let i = 0; i < n; i++) {
console.log(0);
}
}
// Algorithm C: constant time complexity
function algorithm_C(n: number): void {
for (let i = 0; i < 1000000; i++) {
console.log(0);
}
}
// Algorithm A: constant time complexity
void algorithmA(int n) {
print(0);
}
// Algorithm B: linear time complexity
void algorithmB(int n) {
for (int i = 0; i < n; i++) {
print(0);
}
}
// Algorithm C: constant time complexity
void algorithmC(int n) {
for (int i = 0; i < 1000000; i++) {
print(0);
}
}
// Algorithm A: constant time complexity
fn algorithm_A(n: i32) {
println!("{}", 0);
}
// Algorithm B: linear time complexity
fn algorithm_B(n: i32) {
for _ in 0..n {
println!("{}", 0);
}
}
// Algorithm C: constant time complexity
fn algorithm_C(n: i32) {
for _ in 0..1000000 {
println!("{}", 0);
}
}
// Algorithm A: constant time complexity
void algorithm_A(int n) {
printf("%d", 0);
}
// Algorithm B: linear time complexity
void algorithm_B(int n) {
for (int i = 0; i < n; i++) {
printf("%d", 0);
}
}
// Algorithm C: constant time complexity
void algorithm_C(int n) {
for (int i = 0; i < 1000000; i++) {
printf("%d", 0);
}
}
Figure 2-7 shows their time complexities.
- Algorithm
Aperforms \(1\) print operation. Its runtime does not grow with \(n\), so it has constant time complexity. - Algorithm
Bprints \(n\) times. Its runtime grows linearly with \(n\), so it has linear time complexity. - Algorithm
Cprints \(1000000\) times. It may take a long time, but that time is independent of input size \(n\). Its complexity is therefore constant, likeA.

Figure 2-7 Runtime growth of algorithms A, B, and C
How does time complexity analysis differ from calculating runtime directly?
- Time complexity evaluates efficiency effectively. Algorithm
Bgrows linearly, becoming slower thanAwhen \(n > 1\) and slower thanCwhen \(n > 1000000\). With sufficiently large input size \(n\), a constant-time algorithm will outperform a linear-time one. This is what a growth trend tells us. - Time complexity is easier to derive. The execution platform and kinds of elementary operations do not determine the growth trend. We can treat each elementary operation as taking the same unit of time, reducing runtime estimation to operation counting and greatly simplifying analysis.
- Time complexity has limitations. Algorithms
AandChave the same complexity but very different runtimes. AlthoughBhas higher complexity thanC, it is much faster for small \(n\). Complexity alone may not settle such comparisons. Even so, it remains the most effective and widely used way to evaluate algorithm efficiency.
2.3.2 Asymptotic upper bounds¶
Consider this function with input size \(n\):
Let \(T(n)\) be the operation count as a function of input size \(n\). For the function above:
Since \(T(n)\) is a first-degree function, runtime grows linearly, giving linear time complexity.
We write linear time complexity as \(O(n)\). This is called big-\(O\) notation, and it gives an asymptotic upper bound on \(T(n)\).
Time complexity analysis essentially finds an asymptotic upper bound on the operation count \(T(n)\). It has a precise mathematical definition.
Asymptotic upper bound
If a positive real constant \(c\) and a real number \(n_0\) exist such that \(T(n) \leq c \cdot f(n)\) for all \(n > n_0\), then \(f(n)\) is an asymptotic upper bound on \(T(n)\), written \(T(n) = O(f(n))\).
As shown in Figure 2-8, we seek a function \(f(n)\) such that, as \(n\) tends to infinity, \(T(n)\) and \(f(n)\) have the same order of growth, differing only by a constant factor \(c\).

Figure 2-8 Asymptotic upper bound of a function
2.3.3 How to derive time complexity¶
The mathematics of asymptotic bounds may feel unfamiliar. Do not worry if it is not fully clear yet: learn the practical method first, and its meaning will become clearer with practice.
Once we identify \(f(n)\), the time complexity is \(O(f(n))\). Finding the asymptotic upper bound \(f(n)\) takes two steps: count operations, then determine the bound.
1. Step 1: Count operations¶
We can count code operations line by line. Since the constant factor \(c\) in \(c \cdot f(n)\) may be arbitrarily large, coefficients and constant terms in \(T(n)\) can be ignored. This gives three useful shortcuts.
- Ignore constant terms in \(T(n)\). They are independent of \(n\) and do not affect complexity.
- Drop coefficients. For example, \(2n\) or \(5n + 1\) iterations can be counted simply as \(n\), because the coefficient of \(n\) does not affect complexity.
- Multiply for nested loops. Multiply the outer and inner iteration counts, applying shortcuts
1.and2.to each level.
We can apply these shortcuts to the following function:
The formulas below compare full and simplified counts. Both give time complexity \(O(n^2)\).
2. Step 2: Determine the asymptotic upper bound¶
The highest-order term in \(T(n)\) determines time complexity. As \(n\) tends to infinity, that term dominates, and the others become negligible.
Table 2-2 includes deliberately extreme coefficients to emphasize that coefficients cannot overcome growth order. As \(n\) tends to infinity, those constants become insignificant.
Table 2-2 Operation counts and their time complexities
| Operation count \(T(n)\) | Time complexity \(O(f(n))\) |
|---|---|
| \(100000\) | \(O(1)\) |
| \(3n + 2\) | \(O(n)\) |
| \(2n^2 + 3n + 2\) | \(O(n^2)\) |
| \(n^3 + 10000n^2\) | \(O(n^3)\) |
| \(2^n + 10000n^{10000}\) | \(O(2^n)\) |
2.3.4 Common complexity classes¶
For input size \(n\), common time complexities are shown below in increasing order of growth.

Figure 2-9 Common time complexity classes
1. Constant time \(O(1)\)¶
A constant operation count is independent of input size \(n\): changing \(n\) does not change the count.
In the following function, size may be large, but it is independent of input size \(n\), so the time complexity remains \(O(1)\):
Visualize execution
2. Linear time \(O(n)\)¶
A linear operation count grows proportionally to input size \(n\). It commonly appears in a single loop:
Visualize execution
Traversing an array or linked list takes \(O(n)\) time, where \(n\) is its length:
Visualize execution
The meaning of input size \(n\) depends on the input type. In the first example, it is the variable \(n\); in the second, it is the array length \(n\).
3. Quadratic time \(O(n^2)\)¶
A quadratic operation count grows with the square of input size \(n\). It commonly appears in nested loops where both levels take \(O(n)\) time, giving \(O(n^2)\) overall:
Visualize execution
Figure 2-10 compares constant, linear, and quadratic time complexity.

Figure 2-10 Constant, linear, and quadratic time complexity
In bubble sort, for example, the outer loop runs \(n - 1\) times, while the inner loop runs \(n-1\), \(n-2\), \(\dots\), \(2\), and \(1\) times, averaging \(n / 2\). The resulting complexity is \(O((n - 1) n / 2) = O(n^2)\):
def bubble_sort(nums: list[int]) -> int:
"""Quadratic complexity (bubble sort)"""
count = 0 # Counter
# Outer loop: the unsorted range is [0, i]
for i in range(len(nums) - 1, 0, -1):
# Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for j in range(i):
if nums[j] > nums[j + 1]:
# Swap nums[j] and nums[j + 1]
tmp: int = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
count += 3 # Swapping two elements takes 3 elementary operations
return count
/* Quadratic complexity (bubble sort) */
int bubbleSort(vector<int> &nums) {
int count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
int bubbleSort(int[] nums) {
int count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (int i = nums.length - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
int BubbleSort(int[] nums) {
int count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (int i = nums.Length - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
func bubbleSort(nums []int) int {
count := 0 // Counter
// Outer loop: the unsorted range is [0, i]
for i := len(nums) - 1; i > 0; i-- {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for j := 0; j < i; j++ {
if nums[j] > nums[j+1] {
// Swap nums[j] and nums[j + 1]
tmp := nums[j]
nums[j] = nums[j+1]
nums[j+1] = tmp
count += 3 // Swapping two elements takes 3 elementary operations
}
}
}
return count
}
/* Quadratic complexity (bubble sort) */
func bubbleSort(nums: inout [Int]) -> Int {
var count = 0 // Counter
// Outer loop: the unsorted range is [0, i]
for i in nums.indices.dropFirst().reversed() {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for j in 0 ..< i {
if nums[j] > nums[j + 1] {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
count += 3 // Swapping two elements takes 3 elementary operations
}
}
}
return count
}
/* Quadratic complexity (bubble sort) */
function bubbleSort(nums) {
let count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
function bubbleSort(nums: number[]): number {
let count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (let i = nums.length - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (let j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
int bubbleSort(List<int> nums) {
int count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (var i = nums.length - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (var j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
fn bubble_sort(nums: &mut [i32]) -> i32 {
let mut count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for i in (1..nums.len()).rev() {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for j in 0..i {
if nums[j] > nums[j + 1] {
// Swap nums[j] and nums[j + 1]
let tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
count
}
/* Quadratic complexity (bubble sort) */
int bubbleSort(int *nums, int n) {
int count = 0; // Counter
// Outer loop: the unsorted range is [0, i]
for (int i = n - 1; i > 0; i--) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Swapping two elements takes 3 elementary operations
}
}
}
return count;
}
/* Quadratic complexity (bubble sort) */
fun bubbleSort(nums: IntArray): Int {
var count = 0 // Counter
// Outer loop: the unsorted range is [0, i]
for (i in nums.size - 1 downTo 1) {
// Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for (j in 0..<i) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
val temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
count += 3 // Swapping two elements takes 3 elementary operations
}
}
}
return count
}
### Quadratic complexity (bubble sort)###
def bubble_sort(nums)
count = 0 # Counter
# Outer loop: the unsorted range is [0, i]
for i in (nums.length - 1).downto(0)
# Inner loop: move the largest element in the unsorted range [0, i] to its right end through swaps
for j in 0...i
if nums[j] > nums[j + 1]
# Swap nums[j] and nums[j + 1]
tmp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
count += 3 # Swapping two elements takes 3 elementary operations
end
end
end
count
end
Visualize execution
4. Exponential time \(O(2^n)\)¶
Cell division is a classic example of exponential growth. Starting with \(1\) cell, one round produces \(2\), two rounds produce \(4\), and after \(n\) rounds there are \(2^n\) cells.
The figure and code below simulate cell division in \(O(2^n)\) time. The input \(n\) is the number of rounds, while the returned count is the total number of individual divisions.
def exponential(n: int) -> int:
"""Exponential complexity (iterative implementation)"""
count = 0
base = 1
# Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for _ in range(n):
for _ in range(base):
count += 1
base *= 2
# count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count
/* Exponential complexity (iterative implementation) */
int exponential(int n) {
int count = 0, base = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (int i = 0; i < n; i++) {
for (int j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation) */
int exponential(int n) {
int count = 0, base = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (int i = 0; i < n; i++) {
for (int j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation) */
int Exponential(int n) {
int count = 0, bas = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (int i = 0; i < n; i++) {
for (int j = 0; j < bas; j++) {
count++;
}
bas *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation)*/
func exponential(n int) int {
count, base := 0, 1
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for i := 0; i < n; i++ {
for j := 0; j < base; j++ {
count++
}
base *= 2
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count
}
/* Exponential complexity (iterative implementation) */
func exponential(n: Int) -> Int {
var count = 0
var base = 1
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for _ in 0 ..< n {
for _ in 0 ..< base {
count += 1
}
base *= 2
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count
}
/* Exponential complexity (iterative implementation) */
function exponential(n) {
let count = 0,
base = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (let i = 0; i < n; i++) {
for (let j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation) */
function exponential(n: number): number {
let count = 0,
base = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (let i = 0; i < n; i++) {
for (let j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation) */
int exponential(int n) {
int count = 0, base = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (var i = 0; i < n; i++) {
for (var j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation) */
fn exponential(n: i32) -> i32 {
let mut count = 0;
let mut base = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for _ in 0..n {
for _ in 0..base {
count += 1
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
count
}
/* Exponential complexity (iterative implementation) */
int exponential(int n) {
int count = 0;
int bas = 1;
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (int i = 0; i < n; i++) {
for (int j = 0; j < bas; j++) {
count++;
}
bas *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
/* Exponential complexity (iterative implementation) */
fun exponential(n: Int): Int {
var count = 0
var base = 1
// Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (i in 0..<n) {
for (j in 0..<base) {
count++
}
base *= 2
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count
}
### Exponential complexity (iterative implementation)###
def exponential(n)
count, base = 0, 1
# Each cell divides into two per round, giving the sequence 1, 2, 4, 8, ..., 2^(n-1)
(0...n).each do
(0...base).each { count += 1 }
base *= 2
end
# count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
count
end
Visualize execution

Figure 2-11 Exponential time complexity
Exponential time often appears in recursive algorithms. The following function branches into two calls at each level, stopping after \(n\) levels:
Visualize execution
Exponential growth is very rapid and common in exhaustive approaches such as brute-force search and backtracking. It is unacceptable for large inputs, which usually require approaches such as dynamic programming or greedy algorithms.
5. Logarithmic time \(O(\log n)\)¶
Logarithmic growth corresponds to repeatedly halving a quantity, the reverse of exponential growth. Starting with input size \(n\), halving each round gives \(\log_2 n\) iterations, the inverse of \(2^n\).
The figure and code below simulate repeated halving, taking \(O(\log_2 n)\) time, usually written \(O(\log n)\):
Visualize execution

Figure 2-12 Logarithmic time complexity
Like exponential growth, logarithmic growth often occurs in recursion. The following code produces a recursion tree of height \(\log_2 n\):
Visualize execution
Logarithmic time often arises in divide-and-conquer algorithms, reflecting the idea of dividing a problem into smaller, simpler parts. It grows slowly and is highly desirable, second only to constant time among these classes.
What is the base of \(O(\log n)\)?
More precisely, reducing the size by a factor of \(m\) gives \(O(\log_m n)\). The change-of-base formula shows that different fixed bases yield the same complexity:
Thus, changing the base \(m\) does not affect the complexity. We usually omit \(m\) and simply write \(O(\log n)\).
6. Linearithmic time \(O(n \log n)\)¶
Linearithmic time often appears when one level of a nested process takes \(O(\log n)\) and another takes \(O(n)\). An example follows:
def linear_log_recur(n: int) -> int:
"""Linearithmic complexity"""
if n <= 1:
return 1
# Split into two subproblems, each half the original size
count = linear_log_recur(n // 2) + linear_log_recur(n // 2)
# The current subproblem includes n operations
for _ in range(n):
count += 1
return count
Visualize execution
Figure 2-13 shows how linearithmic growth arises. Each level of the binary tree performs \(n\) operations, and there are \(\log_2 n + 1\) levels, giving \(O(n \log n)\) time.

Figure 2-13 Linearithmic time complexity
Common sorting algorithms, including quick sort, merge sort, and heap sort, typically take \(O(n \log n)\) time.
7. Factorial time \(O(n!)\)¶
Factorial growth arises when generating every permutation of \(n\) distinct elements. The number of permutations is:
Algorithms with factorial time complexity are usually implemented recursively. As the figure and code below show, each call creates \(n\) branches at the first level, \(n - 1\) at the second, and so on until level \(n\):
Visualize execution

Figure 2-14 Factorial time complexity
Since \(n! > 2^n\) whenever \(n \geq 4\), factorial growth is faster than exponential growth and is also unacceptable for large \(n\).
2.3.5 Worst-case, best-case, and average-case time complexity¶
An algorithm's efficiency often depends on how its input data is arranged. Suppose nums is an array of length \(n\) containing every integer from \(1\) through \(n\) exactly once in a random order. The task is to return the index of \(1\). We can conclude the following.
- If
nums = [?, ?, ..., 1], with \(1\) last, we must traverse the whole array, giving worst-case time complexity \(O(n)\). - If
nums = [1, ?, ?, ...], with \(1\) first, no further traversal is needed regardless of array length, giving best-case time complexity \(\Omega(1)\).
Worst-case time complexity corresponds to an asymptotic upper bound, expressed with big-\(O\) notation. Best-case time complexity corresponds to an asymptotic lower bound, expressed with \(\Omega\):
def random_numbers(n: int) -> list[int]:
"""Generate an array containing 1, 2, ..., n in shuffled order"""
# Generate the array nums = 1, 2, 3, ..., n
nums = [i for i in range(1, n + 1)]
# Randomly shuffle the array elements
random.shuffle(nums)
return nums
def find_one(nums: list[int]) -> int:
"""Find the index of the number 1 in array nums"""
for i in range(len(nums)):
# When 1 is at the start of the array, the best-case time complexity is O(1)
# When 1 is at the end of the array, the worst-case time complexity is O(n)
if nums[i] == 1:
return i
return -1
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
vector<int> randomNumbers(int n) {
vector<int> nums(n);
// Generate the array nums = { 1, 2, 3, ..., n }
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Use the system time to generate a random seed
unsigned seed = chrono::system_clock::now().time_since_epoch().count();
// Randomly shuffle the array elements
shuffle(nums.begin(), nums.end(), default_random_engine(seed));
return nums;
}
/* Find the index of the number 1 in array nums */
int findOne(vector<int> &nums) {
for (int i = 0; i < nums.size(); i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] == 1)
return i;
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
int[] randomNumbers(int n) {
Integer[] nums = new Integer[n];
// Generate the array nums = { 1, 2, 3, ..., n }
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Randomly shuffle the array elements
Collections.shuffle(Arrays.asList(nums));
// Integer[] -> int[]
int[] res = new int[n];
for (int i = 0; i < n; i++) {
res[i] = nums[i];
}
return res;
}
/* Find the index of the number 1 in array nums */
int findOne(int[] nums) {
for (int i = 0; i < nums.length; i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] == 1)
return i;
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
int[] RandomNumbers(int n) {
int[] nums = new int[n];
// Generate the array nums = { 1, 2, 3, ..., n }
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Randomly shuffle the array elements
for (int i = 0; i < nums.Length; i++) {
int index = new Random().Next(i, nums.Length);
(nums[i], nums[index]) = (nums[index], nums[i]);
}
return nums;
}
/* Find the index of the number 1 in array nums */
int FindOne(int[] nums) {
for (int i = 0; i < nums.Length; i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] == 1)
return i;
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
func randomNumbers(n int) []int {
nums := make([]int, n)
// Generate the array nums = { 1, 2, 3, ..., n }
for i := 0; i < n; i++ {
nums[i] = i + 1
}
// Randomly shuffle the array elements
rand.Shuffle(len(nums), func(i, j int) {
nums[i], nums[j] = nums[j], nums[i]
})
return nums
}
/* Find the index of the number 1 in array nums */
func findOne(nums []int) int {
for i := 0; i < len(nums); i++ {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if nums[i] == 1 {
return i
}
}
return -1
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
func randomNumbers(n: Int) -> [Int] {
// Generate the array nums = { 1, 2, 3, ..., n }
var nums = Array(1 ... n)
// Randomly shuffle the array elements
nums.shuffle()
return nums
}
/* Find the index of the number 1 in array nums */
func findOne(nums: [Int]) -> Int {
for i in nums.indices {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if nums[i] == 1 {
return i
}
}
return -1
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
function randomNumbers(n) {
const nums = Array(n);
// Generate the array nums = { 1, 2, 3, ..., n }
for (let i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Randomly shuffle the array elements
for (let i = 0; i < n; i++) {
const r = Math.floor(Math.random() * (i + 1));
const temp = nums[i];
nums[i] = nums[r];
nums[r] = temp;
}
return nums;
}
/* Find the index of the number 1 in array nums */
function findOne(nums) {
for (let i = 0; i < nums.length; i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] === 1) {
return i;
}
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
function randomNumbers(n: number): number[] {
const nums = Array(n);
// Generate the array nums = { 1, 2, 3, ..., n }
for (let i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Randomly shuffle the array elements
for (let i = 0; i < n; i++) {
const r = Math.floor(Math.random() * (i + 1));
const temp = nums[i];
nums[i] = nums[r];
nums[r] = temp;
}
return nums;
}
/* Find the index of the number 1 in array nums */
function findOne(nums: number[]): number {
for (let i = 0; i < nums.length; i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] === 1) {
return i;
}
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
List<int> randomNumbers(int n) {
final nums = List.filled(n, 0);
// Generate the array nums = { 1, 2, 3, ..., n }
for (var i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Randomly shuffle the array elements
nums.shuffle();
return nums;
}
/* Find the index of the number 1 in array nums */
int findOne(List<int> nums) {
for (var i = 0; i < nums.length; i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] == 1) return i;
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
fn random_numbers(n: i32) -> Vec<i32> {
// Generate the array nums = { 1, 2, 3, ..., n }
let mut nums = (1..=n).collect::<Vec<i32>>();
// Randomly shuffle the array elements
nums.shuffle(&mut thread_rng());
nums
}
/* Find the index of the number 1 in array nums */
fn find_one(nums: &[i32]) -> Option<usize> {
for i in 0..nums.len() {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if nums[i] == 1 {
return Some(i);
}
}
None
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
int *randomNumbers(int n) {
// Allocate heap memory for a one-dimensional array of n int elements
int *nums = (int *)malloc(n * sizeof(int));
// Generate the array nums = { 1, 2, 3, ..., n }
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Randomly shuffle the array elements
for (int i = n - 1; i > 0; i--) {
int j = rand() % (i + 1);
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
return nums;
}
/* Find the index of the number 1 in array nums */
int findOne(int *nums, int n) {
for (int i = 0; i < n; i++) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] == 1)
return i;
}
return -1;
}
/* Generate an array containing { 1, 2, ..., n } in shuffled order */
fun randomNumbers(n: Int): Array<Int?> {
val nums = IntArray(n)
// Generate the array nums = { 1, 2, 3, ..., n }
for (i in 0..<n) {
nums[i] = i + 1
}
// Randomly shuffle the array elements
nums.shuffle()
val res = arrayOfNulls<Int>(n)
for (i in 0..<n) {
res[i] = nums[i]
}
return res
}
/* Find the index of the number 1 in array nums */
fun findOne(nums: Array<Int?>): Int {
for (i in nums.indices) {
// When 1 is at the start of the array, the best-case time complexity is O(1)
// When 1 is at the end of the array, the worst-case time complexity is O(n)
if (nums[i] == 1)
return i
}
return -1
}
### Generate an array containing 1, 2, ..., n in shuffled order ###
def random_numbers(n)
# Generate the array nums = 1, 2, 3, ..., n
nums = Array.new(n) { |i| i + 1 }
# Randomly shuffle the array elements
nums.shuffle!
end
### Find the index of the number 1 in array nums ###
def find_one(nums)
for i in 0...nums.length
# When 1 is at the start of the array, the best-case time complexity is O(1)
# When 1 is at the end of the array, the worst-case time complexity is O(n)
return i if nums[i] == 1
end
-1
end
Visualize execution
Best-case complexity is rarely used in practice because it may occur only with low probability and can be misleading. Worst-case complexity is more useful because it gives a safe performance bound, allowing us to use the algorithm with confidence.
In this example, both the worst and best cases occur only for particular arrangements, perhaps with low probability, and may not reflect ordinary performance. Average-case complexity describes performance on random inputs and is expressed with \(\Theta\).
For some algorithms, the average case is easy to derive. In the example above, \(1\) is equally likely to occur at any index in the shuffled array. The average number of iterations is half the array length, \(n / 2\), giving \(\Theta(n / 2) = \Theta(n)\).
For more complex algorithms, average-case complexity can be difficult to calculate because the overall expectation under the input distribution is hard to derive. In such cases, we usually judge efficiency by worst-case complexity.
Why is \(\Theta\) rarely seen?
Perhaps because \(O\) is so familiar, it is often used for average-case complexity too. Strictly speaking, this usage is not standard. In this book and other resources, read a statement such as "average-case time complexity \(O(n)\)" as \(\Theta(n)\).