Building Intuition for Big-O by Tracing Code Line by Line
Complexity analysis becomes intuitive when you count operations by actually tracing through examples. Practice output prediction problems at PyCodeIt.
Most Big-O explanations are abstract. This one is not.
Complexity analysis is just counting operations. Tracing code to count operations is the most reliable way to build genuine intuition for why an algorithm has the complexity it has.
Counting Operations in a Simple Loop
def find_max(lst):
max_val = lst[0] # 1 operation
for item in lst: # n iterations
if item > max_val: # 1 comparison per iteration
max_val = item # at most 1 assignment per iteration
return max_val # 1 operation
Total operations: 1 + n*2 + 1 = 2n + 2
As n grows large, the constant 2 and the coefficient 2 become irrelevant compared to n. We keep only the dominant term and drop constants: O(n).
Counting a Nested Loop
def has_duplicate(lst):
for i in range(len(lst)): # n iterations
for j in range(len(lst)): # n iterations for each i
if i != j and lst[i] == lst[j]:
return True
return False
The outer loop runs n times. For each outer iteration, the inner loop runs n times. Total comparisons: n * n = n squared.
Time complexity: O(n squared).
Space complexity: O(1) — no additional data structure is created regardless of input size.
Why Dictionary Lookup Is O(1)
A list lookup lst[i] is O(1) because the index is an offset from the start of the array in memory.
A dictionary lookup d[key] is O(1) on average because Python computes hash(key) to find the bucket where the value is stored, then checks equality. This is a fixed number of operations regardless of dictionary size.
def count_words(text):
counts = {}
for word in text.split(): # n iterations
if word in counts: # O(1) dictionary lookup
counts[word] += 1 # O(1) update
else:
counts[word] = 1 # O(1) insert
return counts
Total: n * O(1) = O(n)
Compare to a list-based approach:
def count_words_slow(text):
words = text.split()
counts = []
for word in words: # n iterations
found = False
for entry in counts: # up to n iterations
if entry[0] == word:
entry[1] += 1
found = True
break
if not found:
counts.append([word, 1])
return counts
Total: n * n = O(n squared)
The dictionary version is faster not because of clever algorithm design but because of the O(1) hash table lookup replacing an O(n) linear scan.
Recognizing Complexity From Structure
Once you have traced enough examples, you start recognizing complexity from structure without counting every operation:
Single loop over input: O(n)
Two nested loops over input: O(n squared)
Loop that halves input each iteration: O(log n)
Loop over input with a dictionary operation inside: O(n)
Recursive function that makes two calls with smaller input: O(2 to the n) if no memoization, O(n) with memoization
The Space Complexity Question
def get_evens(lst):
return [x for x in lst if x % 2 == 0]
Time: O(n) —> iterates over every element once Space: O(n) — creates a new list that could be up to size n
def has_even(lst):
return any(x % 2 == 0 for x in lst)
Time: O(n) worst case — might iterate over every element Space: O(1) , the generator expression produces values one at a time without storing them
Interview questions about space complexity often involve identifying whether a solution creates data structures proportional to input size or operates with constant extra space.
Top comments (0)