Counting operations is the main way to estimate an algorithm's complexity without running it. What matters is not seconds but how the number of basic operations grows as the input size n grows. A handful of simple rules gives you the Big-O of most functions in seconds.
The core idea: drop the constants
Assignment, comparison, arithmetic and indexing count as O(1). We drop constant factors and lower-order terms: 3n + 7 is O(n), and n^2/2 + 5n is O(n^2).
Rule 1: sequential blocks add up
Parts that run one after another add their costs, and the largest one survives. A loop of n steps followed by a loop of n * n steps costs n + n^2, which is O(n^2).
def steps_sequential(n):
ops = 0
for _ in range(n):
ops += 1
for _ in range(n):
for _ in range(n):
ops += 1
return ops
for n in (10, 100, 1000):
print(n, steps_sequential(n))