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).

Three counting rules: sequential blocks add, nested loops multiply, dependent bounds give a triangle
Three counting rules: sequential blocks add, nested loops multiply, dependent bounds give a triangle

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).

python
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))