How to Calculate Complexity? -- Junior Level¶
Table of Contents¶
- Introduction
- What Does "Calculating Complexity" Mean?
- Step 1: Count the Operations
- Step 2: Identify the Dominant Term
- Step 3: Drop Constants and Lower-Order Terms
- Analyzing Loops
- Single Loop -- O(n)
- Nested Loops -- O(n^2)
- Halving Loop -- O(log n)
- Sequential Loops
- Step-by-Step Calculation Method
- Common Patterns Cheat Sheet
- Practice Examples
- Key Takeaways
Introduction¶
Understanding how to calculate the time complexity of code is one of the most fundamental skills in computer science. It is not enough to memorize that "sorting is O(n log n)" -- you need to be able to look at any piece of code and derive its complexity from first principles.
This guide teaches you the systematic method for analyzing code, step by step.
What Does "Calculating Complexity" Mean?¶
When we calculate complexity, we are answering: How does the number of operations grow as the input size n increases?
We do not care about: - The exact number of operations (e.g., "47 operations") - Constants (e.g., "3n" vs "5n" -- both are O(n)) - Hardware speed
We do care about: - The growth rate as n gets large - The worst-case scenario (unless stated otherwise)
Step 1: Count the Operations¶
The first step is to go through the code line by line and count how many times each line executes.
Go¶
func sumArray(arr []int) int {
total := 0 // 1 time
for i := 0; i < len(arr); i++ { // n times
total += arr[i] // n times
}
return total // 1 time
}
// Total operations: 1 + n + n + 1 = 2n + 2
Java¶
public static int sumArray(int[] arr) {
int total = 0; // 1 time
for (int i = 0; i < arr.length; i++) { // n times
total += arr[i]; // n times
}
return total; // 1 time
}
// Total operations: 1 + n + n + 1 = 2n + 2
Python¶
def sum_array(arr):
total = 0 # 1 time
for x in arr: # n times
total += x # n times
return total # 1 time
# Total operations: 1 + n + n + 1 = 2n + 2
In all three cases the operation count is 2n + 2. In Big-O notation this becomes O(n).
Step 2: Identify the Dominant Term¶
When you have an expression like 3n^2 + 5n + 100, the dominant term is the one that grows fastest as n increases.
| Expression | Dominant Term | Big-O |
|---|---|---|
| 2n + 2 | n | O(n) |
| 3n^2 + 5n + 100 | n^2 | O(n^2) |
| 4n^3 + 2n^2 + n | n^3 | O(n^3) |
| 5 * 2^n + n^3 | 2^n | O(2^n) |
| 10 log n + n | n | O(n) |
Rule: The term with the highest growth rate dominates. The growth rate hierarchy is:
Step 3: Drop Constants and Lower-Order Terms¶
Big-O notation ignores: - Multiplicative constants: O(3n) = O(n) - Additive constants: O(n + 5) = O(n) - Lower-order terms: O(n^2 + n) = O(n^2)
Why?¶
Because for large n, these do not matter:
| n | n^2 | n^2 + 1000n |
|---|---|---|
| 10 | 100 | 10,100 |
| 1,000 | 1,000,000 | 2,000,000 |
| 1,000,000 | 10^12 | 1.001 * 10^12 |
At n = 1,000,000, the "+1000n" term contributes less than 0.1% to the total.
Analyzing Loops¶
Loops are the primary source of complexity in most code. Let us analyze each common loop pattern.
Single Loop -- O(n)¶
A loop that iterates once for each element in the input runs n times.
Go¶
// O(n) -- linear scan
func findMax(arr []int) int {
max := arr[0] // 1 operation
for i := 1; i < len(arr); i++ { // runs n-1 times
if arr[i] > max { // 1 comparison per iteration
max = arr[i] // at most 1 assignment per iteration
}
}
return max // 1 operation
}
// Count: 1 + (n-1)*2 + 1 = 2n, so O(n)
Java¶
// O(n) -- linear scan
public static int findMax(int[] arr) {
int max = arr[0]; // 1 operation
for (int i = 1; i < arr.length; i++) { // runs n-1 times
if (arr[i] > max) { // 1 comparison per iteration
max = arr[i]; // at most 1 assignment per iteration
}
}
return max; // 1 operation
}
// Count: 1 + (n-1)*2 + 1 = 2n, so O(n)
Python¶
# O(n) -- linear scan
def find_max(arr):
max_val = arr[0] # 1 operation
for x in arr[1:]: # runs n-1 times
if x > max_val: # 1 comparison per iteration
max_val = x # at most 1 assignment per iteration
return max_val # 1 operation
# Count: 1 + (n-1)*2 + 1 = 2n, so O(n)
Key insight: If a loop runs proportional to n, the body executes n times. Multiply the body cost by n.
Nested Loops -- O(n^2)¶
When one loop is inside another, multiply the iteration counts.
Go¶
// O(n^2) -- checking all pairs
func hasDuplicate(arr []int) bool {
n := len(arr)
for i := 0; i < n; i++ { // outer: n times
for j := i + 1; j < n; j++ { // inner: n-i-1 times on average
if arr[i] == arr[j] { // 1 comparison
return true
}
}
}
return false
}
// Inner loop runs: (n-1) + (n-2) + ... + 1 + 0 = n(n-1)/2
// That is O(n^2)
Java¶
// O(n^2) -- checking all pairs
public static boolean hasDuplicate(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) { // outer: n times
for (int j = i + 1; j < n; j++) { // inner: n-i-1 times on average
if (arr[i] == arr[j]) { // 1 comparison
return true;
}
}
}
return false;
}
// Inner loop runs: (n-1) + (n-2) + ... + 1 + 0 = n(n-1)/2
// That is O(n^2)
Python¶
# O(n^2) -- checking all pairs
def has_duplicate(arr):
n = len(arr)
for i in range(n): # outer: n times
for j in range(i + 1, n): # inner: n-i-1 times on average
if arr[i] == arr[j]: # 1 comparison
return True
return False
# Inner loop runs: (n-1) + (n-2) + ... + 1 + 0 = n(n-1)/2
# That is O(n^2)
Mathematical proof that n(n-1)/2 = O(n^2):
Drop constants and lower-order terms: O(n^2).Halving Loop -- O(log n)¶
A loop where the variable is halved (or multiplied) each iteration runs O(log n) times.
Go¶
// O(log n) -- binary search
func binarySearch(arr []int, target int) int {
low, high := 0, len(arr)-1
for low <= high { // runs log2(n) times
mid := low + (high-low)/2 // 1 operation
if arr[mid] == target {
return mid
} else if arr[mid] < target {
low = mid + 1 // eliminate half
} else {
high = mid - 1 // eliminate half
}
}
return -1
}
// Each iteration halves the search space: n -> n/2 -> n/4 -> ... -> 1
// Number of steps: log2(n), so O(log n)
Java¶
// O(log n) -- binary search
public static int binarySearch(int[] arr, int target) {
int low = 0, high = arr.length - 1;
while (low <= high) { // runs log2(n) times
int mid = low + (high - low) / 2; // 1 operation
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
low = mid + 1; // eliminate half
} else {
high = mid - 1; // eliminate half
}
}
return -1;
}
// Each iteration halves the search space: n -> n/2 -> n/4 -> ... -> 1
// Number of steps: log2(n), so O(log n)
Python¶
# O(log n) -- binary search
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high: # runs log2(n) times
mid = low + (high - low) // 2 # 1 operation
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1 # eliminate half
else:
high = mid - 1 # eliminate half
return -1
# Each iteration halves the search space: n -> n/2 -> n/4 -> ... -> 1
# Number of steps: log2(n), so O(log n)
Why log n? If you start with n and keep dividing by 2:
How many steps k does it take? Solve n / 2^k = 1, which gives k = log2(n).Sequential Loops¶
When loops are not nested (they come one after another), add their complexities.
Go¶
func processData(arr []int) {
// Loop 1: O(n)
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
// Loop 2: O(n)
for i := 0; i < len(arr); i++ {
arr[i] *= 2
}
// Loop 3: O(n^2)
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr); j++ {
fmt.Println(arr[i] + arr[j])
}
}
}
// Total: O(n) + O(n) + O(n^2) = O(n^2)
// The dominant term wins
Java¶
public static void processData(int[] arr) {
// Loop 1: O(n)
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// Loop 2: O(n)
for (int i = 0; i < arr.length; i++) {
arr[i] *= 2;
}
// Loop 3: O(n^2)
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
System.out.println(arr[i] + arr[j]);
}
}
}
// Total: O(n) + O(n) + O(n^2) = O(n^2)
Python¶
def process_data(arr):
# Loop 1: O(n)
for x in arr:
print(x)
# Loop 2: O(n)
arr = [x * 2 for x in arr]
# Loop 3: O(n^2)
for i in range(len(arr)):
for j in range(len(arr)):
print(arr[i] + arr[j])
# Total: O(n) + O(n) + O(n^2) = O(n^2)
Rule: For sequential sections, take the maximum.
Step-by-Step Calculation Method¶
Follow these five steps for any code:
Step 1: Identify Input Size(s)¶
What is n? It could be: - Length of an array - Number of nodes in a tree - Number of characters in a string - Multiple inputs (n and m)
Step 2: Go Line by Line¶
For each statement, determine: - How many times does this line execute? - What is the cost of each execution?
Step 3: Sum Up the Counts¶
Write the total as a mathematical expression.
Step 4: Simplify¶
- Drop constants
- Drop lower-order terms
- Keep only the dominant term
Step 5: Express in Big-O¶
Write the final answer as O(f(n)).
Full Worked Example¶
Go¶
func mystery(n int) int {
result := 0 // Step 2: executes 1 time
for i := 0; i < n; i++ { // Step 2: loop runs n times
for j := 0; j < n; j++ { // Step 2: inner loop runs n times per outer iteration
result += i * j // Step 2: runs n*n = n^2 times total
}
}
for k := 0; k < n; k++ { // Step 2: loop runs n times
result += k // Step 2: runs n times
}
return result // Step 2: executes 1 time
}
// Step 3: Total = 1 + n^2 + n + 1 = n^2 + n + 2
// Step 4: Drop constants and lower terms -> n^2
// Step 5: O(n^2)
Java¶
public static int mystery(int n) {
int result = 0; // 1 time
for (int i = 0; i < n; i++) { // n times
for (int j = 0; j < n; j++) { // n times per outer
result += i * j; // n^2 times total
}
}
for (int k = 0; k < n; k++) { // n times
result += k; // n times
}
return result; // 1 time
}
// Total = n^2 + n + 2 -> O(n^2)
Python¶
def mystery(n):
result = 0 # 1 time
for i in range(n): # n times
for j in range(n): # n times per outer
result += i * j # n^2 times total
for k in range(n): # n times
result += k # n times
return result # 1 time
# Total = n^2 + n + 2 -> O(n^2)
Common Patterns Cheat Sheet¶
| Code Pattern | Example | Complexity |
|---|---|---|
| Single statement | x = x + 1 | O(1) |
| Single loop 0..n | for i in range(n) | O(n) |
| Nested loop n*n | Two nested loops over n | O(n^2) |
| Triple nested loop | Three nested loops over n | O(n^3) |
| Halving loop | while n > 0: n //= 2 | O(log n) |
| Loop * halving | Outer 0..n, inner halves | O(n log n) |
| Two sequential loops | O(n) then O(n) | O(n) |
| Loop over n then n^2 | O(n) then O(n^2) | O(n^2) |
Additional Patterns¶
Loop where variable doubles each time:
Loop with inner loop depending on outer variable:
// Sum: 1 + 2 + 3 + ... + n = n(n+1)/2 = O(n^2)
for i := 0; i < n; i++ {
for j := 0; j < i; j++ {
// constant work
}
}
Two nested independent loops with different sizes:
// O(n * m) -- not O(n^2) unless n == m
for i := 0; i < n; i++ {
for j := 0; j < m; j++ {
// constant work
}
}
Practice Examples¶
Example 1: What is the complexity?¶
Go¶
func example1(arr []int) int {
n := len(arr)
sum := 0
for i := 0; i < n; i += 2 {
sum += arr[i]
}
return sum
}
Analysis: The loop runs n/2 times. Each iteration does O(1) work. Total: n/2 = O(n). Dropping the constant 1/2 still gives O(n).
Example 2: What is the complexity?¶
Go¶
Analysis: i takes values 1, 3, 9, 27, ... until i >= n. The number of steps is log3(n). Since O(log3(n)) = O(log n) (base does not matter in Big-O), this is O(log n).
Example 3: What is the complexity?¶
Python¶
def example3(matrix):
n = len(matrix)
total = 0
for i in range(n):
for j in range(n):
for k in range(10):
total += matrix[i][j]
return total
Analysis: The outer two loops give n^2 iterations. The innermost loop runs exactly 10 times (a constant, not dependent on n). Total: 10 * n^2 = O(n^2). The constant 10 is dropped.
Example 4: What is the complexity?¶
Java¶
public static void example4(int n) {
for (int i = 0; i < n; i++) {
for (int j = 1; j < n; j *= 2) {
System.out.println(i + ", " + j);
}
}
}
Analysis: Outer loop runs n times. Inner loop doubles j each time, running log2(n) times. Total: O(n log n).
Key Takeaways¶
- Count operations line by line, tracking how many times each executes.
- Identify the dominant term -- the one that grows fastest.
- Drop constants and lower-order terms to get the Big-O class.
- Loops are the key: single loop = O(n), nested = O(n^2), halving = O(log n).
- Sequential blocks: add their complexities, then take the maximum.
- Nested blocks: multiply their complexities.
- Constants in loop bounds (like
for k < 10) do not add to complexity. - Practice on real code -- the more you analyze, the faster you get.
Next: Middle Level -- Recurrence relations, Master Theorem, and amortized analysis.