Time Complexity
BasisPrerequisites
Asymptotic Notations gave you the language; time complexity is how you apply it to real code. This checkpoint shows you how to count the operations an algorithm performs, reduce that count to its dominant term, and read the result directly from common control-flow patterns — so you can classify any algorithm’s cost without running it.
What you are counting
Time complexity is a function that maps the size of an input to the number of elementary operations the algorithm performs. An elementary operation is one that takes constant time: comparing two numbers, adding, writing to a memory address, following a reference. You count these as if each costs exactly one unit.
The definition of “input size” depends on the problem:
- For a list or array: is the number of elements.
- For a number: is the number of bits (or digits), not the value itself. Computing whether a 1000-digit number is even takes far less work than iterating up to its value.
- For a graph: you commonly use for the number of vertices and for the number of edges, and express complexity in terms of both.
Worst case, best case, and average case
The same algorithm can behave very differently on different inputs of the same size. Consider searching an unsorted array of elements for a target value:
- Best case: the target is the first element — one comparison suffices.
- Worst case: the target is the last element, or absent — you check all elements.
- Average case: assuming the target appears uniformly at random, you check about elements on average.
Unless stated otherwise, time complexity in this series means worst-case time complexity. Worst-case analysis gives you a guarantee: the algorithm will never take longer than steps, no matter what input of size you give it.
Best-case analysis is rarely useful — knowing things can go well does not protect you when they go badly. Average-case analysis is valuable but requires assumptions about the input distribution that are not always justified; it is introduced where it matters.
Reading time complexity from code
The most important practical skill is translating control flow into a complexity expression. The patterns below cover the vast majority of cases you will encounter.
Simple statements
A single statement that does a fixed amount of work — a comparison, an assignment, an arithmetic operation — contributes to the total regardless of .
let mid = lo + (hi - lo) / 2; // O(1)
A single loop
A loop that runs times, with work per iteration, contributes total.
fn sum(arr: &[i64]) -> i64 {
let mut total = 0;
for &x in arr { // n iterations, O(1) work each
total += x;
}
total
}
Nested loops
Two loops where the inner loop runs up to times for each of the outer loop’s iterations give .
fn has_duplicate(arr: &[i64]) -> bool {
for i in 0..arr.len() { // n iterations
for j in (i + 1)..arr.len() { // up to n iterations
if arr[i] == arr[j] {
return true;
}
}
}
false
}
The inner loop runs steps for outer iterations , for a total of comparisons. That is .
Halving loops
When a loop variable is divided (or multiplied) by a constant each iteration instead of incremented by one, the loop runs times.
fn floor_log2(mut n: u64) -> u64 {
let mut count = 0;
while n > 1 {
n /= 2; // n halves each iteration
count += 1;
}
count
}
Starting from and halving until reaching takes steps. The same argument applies to binary search: at each step the search space halves, so the number of steps is .
Sequential blocks
When independent code blocks run one after another, add their complexities, then keep only the dominant term:
// Block 1: O(n)
let total: i64 = arr.iter().sum();
// Block 2: O(n^2) — this dominates
for i in 0..arr.len() {
for j in (i + 1)..arr.len() {
// process pair (i, j)
}
}
The total is .
Nested loops with non-uniform bounds
Not all nested loops are . When the inner bound depends on the outer variable, you must compute the sum explicitly.
for i in 0..n {
for j in 0..i { // inner runs i steps, not n steps
// O(1) work
}
}
Total iterations: . Still quadratic here, but the pattern matters — other non-uniform bounds lead to different sums.
For example, a loop of the form for j in 0..(2*i) gives , while for j in 0..((i as f64).sqrt() as usize) gives .
The dominant-term rule
The most important simplification: drop all but the fastest-growing term, and drop constant factors.
If , then .
Why? For large , grows faster than all other terms combined. Once is large enough, accounts for more than half of , and the rest become negligible.
More formally, for any two growth classes with :
The corollary: when analyzing code, you can identify the “innermost” or “dominant” loop, determine its cost, and that cost is the overall complexity. You do not need to track every constant.
Worked example: binary search
Binary search finds a target value in a sorted array by repeatedly halving the search range.
fn binary_search(arr: &[i64], target: i64) -> Option<usize> {
let mut lo = 0;
let mut hi = arr.len();
while lo < hi { // (1)
let mid = lo + (hi - lo) / 2; // O(1)
match arr[mid].cmp(&target) {
std::cmp::Ordering::Equal => return Some(mid),
std::cmp::Ordering::Less => lo = mid + 1,
std::cmp::Ordering::Greater => hi = mid,
}
}
None
}
At each iteration of the while loop (1), the search range is halved. Starting from a range of size , after iterations the range has size . The loop terminates when the range is empty, which happens after at most iterations. Each iteration does work.
Total: .
Compare this with linear search, which is . For , linear search may perform up to comparisons; binary search performs at most .
A note on space complexity
Time complexity counts operations; space complexity counts memory usage as a function of , using the same asymptotic notation. An algorithm that allocates an auxiliary array of size uses extra space; one that uses only a fixed number of variables uses extra space, which is also called in-place.
The two measures always appear together in algorithm analysis. A fast algorithm that requires memory may be impractical even if its time complexity is acceptable.
Summary
- Time complexity is the number of elementary operations as a function of input size , expressed in asymptotic notation.
- By default, report worst-case time complexity — it is the strongest useful guarantee.
- Key patterns from code: a simple statement is ; a single loop is ; two nested loops over the full range are ; a halving loop is ; sequential independent blocks add, then keep the dominant term.
- For nested loops with non-uniform bounds, compute the sum explicitly rather than assuming .
- The dominant-term rule: drop all but the fastest-growing term, and drop constant factors.
- Space complexity uses the same notation to describe memory usage; in-place means extra space.