What does Big O notation actually mean, and how do you work out the bound for a given piece of code?
Big O notation is a formal way of describing how the cost of an algorithm grows as its input grows, discarding constant factors and lower-order terms. Writing O(f(n)) says the work is bounded above by some constant multiple of f(n) once n is large enough. It is a promise about a family of inputs, not a measurement of one run, and it never claims the algorithm is exactly that fast.
The derivation rules are few. Sequential statements add their costs, so the total is dominated by the largest term. A loop running n times with a constant-cost body is O(n). A loop inside a loop multiplies, giving O(n times m), while two loops that run one after the other add, so O(n) followed by O(n) stays O(n).
def count_operations(n, nested):
ops = 0
for i in range(n):
ops += 1
if nested:
for j in range(n):
ops += 1
return ops
for n in (10, 20, 40):
print(n, count_operations(n, False), count_operations(n, True))The printed counts show the shape. The flat version performs exactly n operations, so doubling n doubles the work. The nested version performs n plus n squared operations, and the numbers confirm that doubling n roughly quadruples the work. That steep curve is what the quadratic label describes.
Here is the part interviewers press on: Big O is only an upper bound, so saying an algorithm is O(n squared) does not rule out that it is also O(n). When you know more, say Theta for a tight bound where the cost really grows like that function, or Omega for a lower bound. Saying O(n) when you mean Theta(n) is a habit worth dropping. Also name your variables, because a function over a list of length n and a text of length m may depend on both, which is why a bound is often written in terms of several inputs.