Big-O Notation
One short symbol for a growth story: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ).
- read and write Big-O notation for the six patterns that cover most code;
- translate a step-count formula into a Big-O class in one line;
- say what each class means in plain words and in real time;
- read the class straight off a function, line by line, without measuring;
- distinguish best, worst and average case, and say which one to quote;
- tell time cost from space cost, and quote both;
- revise everything from weeks 1–7 for the midterm.
8.1The notation
Last week you learned to reduce a step count to its dominant term with the constants stripped off. Big-O is just the standard way of writing that result down:
T(n) = 5n² + 200n + 3000 → O(n²)
Read it aloud as "oh of n squared" or "order n squared". It means: as the input grows, the work grows in proportion to n². It is a statement about shape, not about speed. O(n²) code can be quick on ten items and hopeless on ten thousand; Big-O tells you which of those two facts will still be true next year.
The multiplier, the smaller terms, your CPU, your programming language, and whether n is small. It answers exactly one question: how does the cost respond to more data? Every other question needs the stopwatch as well.
8.2The six you need
| Notation | Name | In plain words | Everyday example | n doubles ⇒ time |
|---|---|---|---|---|
O(1) | constant | Cost does not depend on the input size at all | Reading data[5]; a formula | unchanged |
O(log n) | logarithmic | Each doubling of the data adds one step | The dictionary halving trick | +1 step |
O(n) | linear | One visit per item | Summing a list; searching for a missing item | ×2 |
O(n log n) | linearithmic | A pass over the data, repeated log n times | Good sorting: sorted() | slightly more than ×2 |
O(n²) | quadratic | Every item meets every item | Comparing all pairs; bubble sort | ×4 |
O(2ⁿ) | exponential | Each extra item doubles the total work | Trying every combination | squared — hopeless |
The order above is from best to worst, and the gaps between the rows are enormous. Drag the slider and read the right-hand column as clock time, on a computer doing ten million steps a second:
At n = 1 000 000 the difference between O(n) and O(n²) is the difference between "a tenth of a second" and "the rest of the week". No hardware purchase closes that gap. Choosing the right approach does.
8.3Why we drop constants and lower terms
This is the step people take on faith at first, so it is worth doing once with real numbers. Take a made-up step count with a fat constant and a big linear term bolted on:
T(n) = 3n² + 500n + 8000
Watch which term actually decides the total as n grows. The table shows what each term contributes and, in the last column, the share the n² term takes:
| n | 3n² | 500n | 8000 | share from 3n² |
|---|---|---|---|---|
| 10 | 300 | 5 000 | 8 000 | 2% |
| 100 | 30 000 | 50 000 | 8 000 | 34% |
| 1 000 | 3 000 000 | 500 000 | 8 000 | 85% |
| 10 000 | 300 000 000 | 5 000 000 | 8 000 | 98% |
| 1 000 000 | 3 × 10¹² | 5 × 10⁸ | 8 000 | 99.98% |
At small n the constant 8000 is the biggest thing on the row and the "slow" n² term is invisible. But every column except the first eventually surrenders to n²: the constant never moves, the linear term is left in the dust, and by n = 1 000 000 the squared term is the answer to two decimal places. Big-O reports the term that wins this race, because that is the term that decides what happens when the data grows — which is the only question Big-O is asked.
And the multiplier? Drop it because it does not change the shape. Double the
input on any n² curve — whether it is n², 3n² or
1000n² — and the time goes up four-fold. The 3 sets the height of the
curve; the n² sets its bend. Big-O is a claim about the bend.
To go from T(n) to Big-O: keep the fastest-growing term, throw away its
multiplier. 7n + 3 → O(n). n²/2 − n → O(n²).
4n log n + 90n → O(n log n). Everything else on the line is noise once
n is large.
8.4Reading code and naming its class
Four questions, in order. They resolve most everyday code:
- Is there a loop over the data at all? No →
O(1). - One loop over n? →
O(n). - A loop inside a loop, both over n? →
O(n²). - Does the problem halve each round? →
O(log n), orO(n log n)if that halving happens inside a pass over the data.
# A
def first_item(data):
return data[0]
# B
def total(data):
result = 0
for x in data:
result += x
return result
# C
def any_duplicate(data):
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] == data[j]:
return True
return False
# D
def halvings(n):
count = 0
while n > 1:
n = n // 2
count += 1
return countAnswers
A is O(1): one shelf lookup, whatever the length. B is O(n):
one visit each. C is O(n²): every pair, in the worst case — note that
"return True early" helps only when duplicates exist, and Big-O quotes the worst case.
D is O(log n): the value halves every round.
Some innocent-looking lines contain a loop inside Python itself. x in my_list
is O(n). sorted(data) is O(n log n).
data.insert(0, x) is O(n). Put any of those inside your own
loop and you have quietly written nested loops. This is the single most common way
beginners produce accidentally quadratic code, and weeks 10–11 are devoted to it.
8.5Reading Big-O off code, line by line
When the shape is not obvious at a glance, fall back on two mechanical rules and add the costs up like a shopping receipt:
- Statements in sequence add. A + B + C → keep the biggest. O(n) then O(n) then O(1) is O(n).
- Nested loops multiply. A loop that runs n times, doing O(n) work each time, is O(n) × O(n) = O(n²).
Here is a function built to exercise both rules. Price it one region at a time:
def report(data): # n = len(data)
n = len(data) # O(1) — length is stored
total = 0
for x in data: # O(n) — one pass
total += x
biggest_gap = 0
for i in range(n): # outer runs n times…
for j in range(n): # …inner runs n times each → O(n²)
gap = abs(data[i] - data[j])
if gap > biggest_gap:
biggest_gap = gap
top10 = sorted(data)[-10:] # O(n log n) sort, then O(1) slice
return total, biggest_gap, top10
Reading top to bottom, the receipt is:
O(1) + O(n) + O(n²) + O(n log n).
Sequential parts add, so we keep the biggest — and n² beats
n log n beats n beats 1. The whole function is
O(n²), and the culprit is the one nested loop. Everything else is along
for the ride.
The first loop and the double loop are written one after another, so they
add: O(n) + O(n²), which is still O(n²). Only loops written
inside each other multiply. It is worth pointing at the indentation and
saying which rule applies — that habit alone prevents most misreadings.
What if the inner loop were for j in range(10):?
Then the inner loop is a fixed ten steps, not n, so the double loop is
O(n) × O(10) = O(10n) = O(n). The whole function would drop to
O(n log n), decided by the sort. A loop bound that does not grow with the
data is a constant, and constants leave.
8.6Best, worst and average case
Searching a list of n items for a value:
- Best case — it is the first item. One step.
O(1). - Worst case — it is last, or absent. n steps.
O(n). - Average case — roughly n/2 steps, which is still
O(n)once constants go.
Unqualified Big-O means worst case. That is the honest default: it is the promise you can make to a user without knowing their data. Quote a best case only when you say the words "best case", or you are selling something.
def find(data, target):
for i, x in enumerate(data):
if x == target:
return i # stops the moment it hits
return -1
nums = list(range(1000))
print(find(nums, 0)) # best case: 1 comparison
print(find(nums, 999)) # worst case: 1000 comparisons
print(find(nums, -1)) # worst case: 1000 comparisons, absentSame function, three wildly different amounts of work, and the input alone decides which you get. When you report "linear search is O(n)", you are quoting the middle and third lines — the promise that holds even for the unlucky user.
Systems fail at their worst moments, not their average ones. A search that is normally instant but occasionally scans ten million rows will be discovered by your users at the busiest hour of the year.
8.7A space-complexity teaser
Big-O is not only about time. The same notation describes how much extra memory a method needs as the input grows — its space complexity. When you quote a cost, it is fair to ask which one you mean, because a method can be cheap in one and dear in the other.
"Extra" is the key word: it means memory beyond the input you were handed.
def reverse_in_place(data):
i, j = 0, len(data) - 1
while i < j: # swaps ends inward
data[i], data[j] = data[j], data[i]
i += 1
j -= 1
# uses two index variables, whatever n is → O(1) extra space
def reverse_copy(data):
result = []
for x in data:
result = [x] + result # builds a brand-new list of n items
return result # → O(n) extra spaceBoth return a reversed sequence. The first rearranges the list you gave it and borrows only two counters, so its extra space is O(1). The second grows a second list the size of the first, so its extra space is O(n). Neither is "wrong" — an in-place reverse is leaner, a copy leaves the original untouched — but you should be able to say which you are paying for.
Every time your code builds a new list, string or dictionary whose size follows n,
that is O(n) space. The list comprehension [(x, y) for x in data for y in
data] from earlier is O(n²) in both time and space — it constructs n²
pairs and has to hold them all. Week 9's counting solution spends O(1) extra space
(two fixed 26-slot lists) to buy O(n) time; that is the trade named in full next
week.
8.8Name that complexity — a gallery
Cover the answers and call each one before you read on. These are the shapes you will meet again and again; the goal is to name them on sight.
# 1
def middle(data):
return data[len(data) // 2]
# 2
def pair_sums(data):
out = []
for x in data:
for y in data:
out.append(x + y)
return out
# 3
def count_down(n):
while n > 1:
n = n // 2
print(n)
# 4
def has_common(a, b):
for x in a: # len(a) = n
if x in b: # 'in' on a list = O(len(b))
return True
return False
# 5
def sort_then_scan(data):
data = sorted(data) # O(n log n)
for x in data: # O(n)
print(x)
# 6
def first_three(data):
return data[0], data[1], data[2]Answers
1 — O(1): one division, one lookup, length is free.
2 — O(n²) in time and O(n²) in space; it stores every pair.
3 — O(log n): the value halves each round.
4 — O(n × m), and if both lists are length n that is O(n²): the
hidden loop inside in nests under the visible loop.
5 — O(n log n): sort dominates the later linear scan.
6 — O(1): three fixed lookups, no dependence on length.
8.9Common mistakes
| Mistake | Why it is wrong |
|---|---|
| "O(2n) is worse than O(n)" | They are the same class. Constants are dropped. |
| "O(n²) is always slower than O(n)" | Only beyond the crossover point. For small n a fat constant on the O(n) side can lose. |
| "Nested loops are always O(n²)" | Only if both loops grow with n. An inner loop of fixed length 10 is a constant factor. |
| "Two loops in a row is O(n²)" | Sequential loops add, not multiply: O(n) + O(n) = O(n). Only nested loops multiply. |
| "Big-O tells me how many seconds" | It tells you the shape of the curve, not a point on it. You still need the stopwatch. |
| "My code has no loops, so it is O(1)" | Check the built-in calls: in, sorted, max, sum, slicing and copying all walk the data. |
| "O(n) time means O(n) memory" | Different questions. A pass that keeps one running total is O(n) time but O(1) space. |
8.10Try it yourself
Give the Big-O class of each, in terms of the length of data:
a = len(data)
b = data[len(data) // 2]
c = max(data)
d = sorted(data)
e = [x * 2 for x in data]
f = [(x, y) for x in data for y in data]Answers
a O(1) — Python stores the length. b O(1) — arithmetic plus
one lookup. c O(n) — must inspect every item. d O(n log n).
e O(n). f O(n²) — and it builds a list of n² pairs, so it
eats memory at the same rate.
Take the four ratio columns you measured in week 6 and write the Big-O class each one implies. Then look at the code and confirm the class from its shape. Any disagreement is a finding worth a paragraph.
An O(n²) function takes 1.0 second at n = 10 000. Predict its time at n = 40 000, then write and run it to check. Do the same for an O(n) function.
Answers
Four times the data on a quadratic curve is sixteen times the work: about 16 seconds. The linear one goes to about 4 seconds. Expect real measurements a bit above the prediction — memory effects usually work against the bigger run.
Give the overall time class of each function, showing the per-region receipt you added up. Then say the space class of the second one.
def one(data):
s = sum(data) # ?
data.sort() # ?
return s, data[0]
def two(data):
seen = []
for x in data: # ?
if x not in seen: # ?
seen.append(x)
return seenAnswers
one: sum is O(n), sort is O(n log n), the
return is O(1). Add and keep the biggest → O(n log n).
two: the loop runs n times, and not in seen is itself an
O(n) scan, so it is O(n) × O(n) = O(n²) time. Its space is
O(n) because seen can grow to the size of the input.
(Week 11's set turns that membership test into O(1) and the whole thing into O(n).)
For the find function in §8.6, and a list of 1 000 items, give the exact
number of comparisons in the best case, the worst case, and the average case when the
target is present and equally likely to be anywhere. Then say the Big-O of each.
Answers
Best: 1 comparison, O(1). Worst: 1 000 comparisons, O(n). Average: the target is at position 1 to 1 000 with equal chance, so the mean is (1 + 1 000) / 2 ≈ 500 comparisons — still O(n) once the ½ constant is dropped.
8.11Self-check
T(n) = 7n + 300. In Big-O this is:
Which grows fastest as n becomes large?
You write for item in queries: and inside it if item in big_list:. If both hold n items, the class is:
Unqualified, "this search is O(n)" refers to which case?
A function does one O(n) loop, then a separate O(n²) double loop, then returns. Its class is:
A function walks a list once keeping a single running maximum. Its time and space are:
An inner loop written as for j in range(10): sits inside a loop over n items. The pair is:
8.12Midterm revision checklist
- Define "algorithm" and give the four properties (week 1).
- Write a loop, a nested loop and a halving loop from memory (weeks 3–4).
- Add a step counter to any function and predict what it will print (week 3).
- Time a function correctly, with repeats, and say what could distort the result (week 5).
- Run a doubling experiment and name the pattern from the ratio column (week 6).
- Write T(n) for a short snippet and simplify it to a dominant term (week 7).
- Convert T(n) to Big-O, and explain the class in plain words (week 8).
- Read the class off a function by adding sequential parts and multiplying nested loops (week 8).
- Explain best / worst / average case for linear search (weeks 4, 8).
- Tell time cost from space cost, and quote both for a short function (week 8).
- Explain the crossover between 100n and n², with a number (week 7).
The midterm gives you short code snippets and measurement tables. You will count steps, name classes, and justify each answer in one sentence. Nothing needs to be memorised that is not in this checklist.
8.13Homework
- Create
AA_Week08.ipynband complete Tasks 1–5. - Build a personal reference table of the six classes: notation, plain-words meaning, a code example you wrote yourself, and a measured timing from your own notebooks.
- Take the
reportfunction from §8.5, type it into Colab, and add a comment above every region naming its class. Then change the inner loop bound fromnto10and write one sentence on how the overall class changes and why. - Find one example of a hidden loop in code you wrote in an earlier week. Name its true class and describe how you would test that claim.
- Pick any function you have written and state both its time class and its extra-space class, with a sentence of justification for each.
- Write five sentences answering: "Why do we say O(n²) is worse than O(n) even though a particular O(n²) program might run faster today?"
8.14Words from this week
| Term | Meaning in plain words |
|---|---|
| Big-O | A short label for how cost grows with input size. |
| constant / logarithmic / linear | O(1) / O(log n) / O(n). |
| linearithmic | O(n log n) — the speed of good sorting. |
| quadratic / exponential | O(n²) — every pair. O(2ⁿ) — every combination; unusable beyond tiny n. |
| dominant term | The fastest-growing part of T(n); the one Big-O keeps. |
| worst case | The most work the method can be forced to do; the default meaning of a Big-O claim. |
| space complexity | How the extra memory a method needs grows with the input. |
| hidden loop | A built-in operation that walks the data even though you wrote no loop. |
8.15Three cousins of Big-O: Ω, Θ, o, ω in plain words
So far O has done all the work, and for everyday code it is all you need. But the textbook — and the exercises below — use four relatives of O, written with Greek letters. They sound intimidating and mean something very simple. Big-O only ever says "grows no faster than" — it is a ceiling. The cousins fill in the other directions: a floor, an exact match, and two strict versions.
| Symbol | Say it | In plain words | Everyday analogy |
|---|---|---|---|
O(g) | "big-oh" | grows no faster than g — an upper bound, a ceiling | a speed limit: you never go above it |
Ω(g) | "big-omega" | grows no slower than g — a lower bound, a floor | a minimum wage: you never earn below it |
Θ(g) | "big-theta" | grows at exactly the same rate as g — ceiling and floor at once | two runners who stay side by side the whole race |
o(g) | "little-oh" | grows strictly slower than g — falls hopelessly behind | a pedestrian next to a car: the gap only widens |
ω(g) | "little-omega" | grows strictly faster than g — pulls away without limit | the car, seen from the pedestrian's side |
Two of these you already understand. O is the ceiling you have used all
week; Θ (theta) is the honest one — it says "same shape, up to a
constant", which is what you mean when you say a function "really is" quadratic. The
difference between the big and little versions is the word strictly:
n = O(n) is true (n grows no faster than itself), but
n = o(n) is false (n does not grow strictly slower than itself). Little-o
and little-ω are for when one function leaves the other in the dust for good.
Think of comparing two numbers. O is like ≤ (at most),
Ω is ≥ (at least), Θ is = (equal),
o is < (strictly less), and ω is > (strictly
greater) — but applied to growth rates as n heads to infinity, ignoring
constant multipliers. If f = Θ(g) then automatically
f = O(g) and f = Ω(g), exactly as "equal" implies
both "at most" and "at least".
Every exercise below is really the same skill: line the two functions up against a
fixed ladder of growth rates and see which rung each one lands on. Here is that ladder,
slowest on the left, and it is worth memorising — ≪ here means "grows
strictly slower than":
1 ≪ log n ≪ √n ≪ n ≪ n log n ≪ n² ≪ n³ ≪ 2ⁿ ≪ 3ⁿ ≪ n!
Read left to right, each item is little-o of everything to its right and little-ω of everything to its left. Anything at the same rung (only differing by a constant factor) is Θ of its neighbour on that rung. Almost every "which grows faster?" question is answered by pointing at this line. Two facts do most of the heavy lifting: any polynomial beats any logarithm, and any exponential beats any polynomial.
8.16Chapter problem set — Skiena 2.10 · Big-O
These are the Big-O exercises from Chapter 2 of Steven Skiena's The Algorithm
Design Manual, worked here at first-year level. Skiena writes lg for
the base-2 logarithm and ln for the natural one; for Big-O purposes the
base does not matter, because changing base only multiplies by a constant — so we will
just write "log" and lean on the growth ladder from §8.15. Where a problem asks
for a formal proof we give an honest but light argument: pick a constant, show a few
values, and name the reason. Cover each answer and try it first.
How many times is r incremented, and what is the running time in Big-O?
r = 0
for i in range(1, n + 1):
for j in range(1, i + 1):
for k in range(j, i + j + 1):
for l in range(1, (i + j - k) + 1):
r += 1Worked solution
Do not panic at the messy bounds. Count the loops: there are four, nested one inside the other, and each one runs a number of times that is at worst proportional to n. Every extra nested loop that ranges up to about n multiplies the total count by another factor of about n, so four of them give roughly n × n × n × n = n⁴. The exact starting points shift the constant in front but not the shape.
The clean test is to count the increments and double n. If it is really n⁴, doubling n should multiply the count by about 2⁴ = 16:
def prestiferous(n):
r = 0
for i in range(1, n + 1):
for j in range(1, i + 1):
for k in range(j, i + j + 1):
for l in range(1, (i + j - k) + 1):
r += 1
return r
prev = None
for n in [5, 10, 20, 40, 80]:
r = prestiferous(n)
ratio = "-" if prev is None else f"{r / prev:.2f}x"
print(f"n={n:>3} r={r:>12,} ratio={ratio}")
prev = rThe ratio climbs steadily towards 16 — the unmistakable fingerprint of n⁴. (It approaches 16 from below rather than sitting on it exactly because of the lower-order terms, just as the doubling harness behaved in weeks 5–7.)
Optional challenge: the exact closed form can be worked out with the summation techniques of §8.16's later problems, but the marks are for the class, not the formula.
Answer: peeling the loops from the inside gives the exact value r = n(n+1)(n+2)(3n+1)/24, so the running time is Θ(n⁴). (The loop depth only hints at the class; summing the loops proves it.)
How many times is r incremented, and what is the worst-case running time?
r = 0
for i in range(1, n + 1):
for j in range(i + 1, n + 1):
for k in range(i + j - 1, n + 1):
r += 1Worked solution
Again, ignore the awkward starting points and count the loops: three,
nested, each ranging over a stretch that is at worst about n long. Three nested loops
over roughly n give about n³. The inner bounds depending on i and
j only shrink the count by a constant fraction — they cannot change
the power of n. If it is cubic, doubling n should multiply the count by about
2³ = 8:
def conundrum(n):
r = 0
for i in range(1, n + 1):
for j in range(i + 1, n + 1):
for k in range(i + j - 1, n + 1):
r += 1
return r
prev = None
for n in [10, 20, 40, 80, 160]:
r = conundrum(n)
ratio = "-" if prev is None else f"{r / prev:.2f}x"
print(f"n={n:>3} r={r:>10,} ratio={ratio}")
prev = rThe ratio settles towards 8 — cubic confirmed by arithmetic, no stopwatch needed.
Answer: the moving start points empty many iterations, leaving the exact count r = Σi=1⌊n/2⌋ (n−2i+1)(n−2i+2)/2 (a closed form m(m+1)(4m−1)/6 for even n = 2m), so the worst-case running time is Θ(n³). Depth alone would not prove this — the ranges must be summed.
(a) Is 2ⁿ⁺¹ = O(2ⁿ)? (b) Is 2²ⁿ = O(2ⁿ)?
Worked solution
(a) True. 2ⁿ⁺¹ = 2 · 2ⁿ —
that is just 2ⁿ with a constant factor of 2 bolted on, and Big-O
ignores constant factors. Take c = 2 and 2ⁿ⁺¹
≤ 2 · 2ⁿ holds for every n.
(b) False. 2²ⁿ = (2ⁿ)² = 4ⁿ,
which is a completely different, faster exponential. The ratio
4ⁿ / 2ⁿ = 2ⁿ shoots off to infinity, so no fixed
constant c can ever keep 4ⁿ ≤ c · 2ⁿ.
Doubling the exponent is not a constant factor — it changes the base.
Answer: (a) true; (b) false.
For each pair, is f = O(g), f = Ω(g), or f = Θ(g)? (When both O and Ω hold, the honest answer is Θ.)
Worked solution
Read each pair straight off the ladder.
| f | g | Verdict | Why | |
|---|---|---|---|---|
| (a) | log(n²) | log n + 5 | Θ | log n² = 2 log n; both are log n up to a constant |
| (b) | √n | log(n²) = 2 log n | Ω | √n beats any logarithm |
| (c) | (log n)² | log n | Ω | squaring a growing thing makes it grow faster |
| (d) | n | (log n)² | Ω | a polynomial beats any power of a logarithm |
| (e) | n log n + n | log n | Ω | n log n is far above log n |
| (f) | 10 | log 10 | Θ | both are fixed constants — neither grows |
| (g) | 2ⁿ | 10 n² | Ω | an exponential beats any polynomial |
| (h) | 2ⁿ | 3ⁿ | O | 2ⁿ grows slower than 3ⁿ (ratio (2/3)ⁿ→0) |
Answer: (a) Θ · (b) Ω · (c) Ω · (d) Ω · (e) Ω · (f) Θ · (g) Ω · (h) O.
For each pair decide whether f = O(g), g = O(f), or both (which is Θ).
Worked solution
| f | g | Verdict | Why | |
|---|---|---|---|---|
| (a) | (n² − n)/2 | 6n | g = O(f) | f is quadratic (≈ n²/2), g is only linear |
| (b) | n + 2√n | n² | f = O(g) | f ≈ n, well below n² |
| (c) | n log n | n√n / 2 | f = O(g) | √n grows faster than log n, so n√n beats n log n |
| (d) | n + log n | √n | g = O(f) | f ≈ n, above √n |
| (e) | 2 · (log n)² | log n + 1 | g = O(f) | (log n)² grows faster than log n |
| (f) | 4n log n + n | (n² − n)/2 | f = O(g) | n log n is below n² |
None of these pairs is a tie, so Θ never applies here. Answer: (a) g=O(f) · (b) f=O(g) · (c) f=O(g) · (d) g=O(f) · (e) g=O(f) · (f) f=O(g).
Show that n³ − 3n² − n + 1 = Θ(n³).
Worked solution
Θ(n³) means "sandwiched between two constant multiples of n³ once n is large". The clean trick is to divide the whole expression by n³ and watch what happens as n grows:
(n³ − 3n² − n + 1) / n³ = 1 − 3/n − 1/n² + 1/n³
Each of the trailing fractions heads to 0 as n grows, so the whole ratio heads to
1. That means for large enough n the expression is close to n³
— certainly between, say, 0.5 n³ and 1 · n³.
A quick check: at n = 10 the value is 1000 − 300 − 10 + 1 = 691, and
indeed 0.5(1000) = 500 ≤ 691 ≤ 1000. Two constant multiples of n³ trap it,
which is exactly the definition of Θ(n³).
Answer: Θ(n³).
Show that n² = O(2ⁿ).
Worked solution
This is the headline fact of the ladder: an exponential eventually overtakes any
polynomial and never looks back. To prove n² = O(2ⁿ) we just
need one constant c and a starting point n₀
such that n² ≤ c · 2ⁿ for all n ≥
n₀. Watch the two race:
| n | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 |
|---|---|---|---|---|---|---|---|---|
n² | 1 | 4 | 9 | 16 | 25 | 36 | 64 | 100 |
2ⁿ | 2 | 4 | 8 | 16 | 32 | 64 | 256 | 1024 |
At n = 3 the polynomial is briefly ahead (9 > 8), but from n = 4
onward 2ⁿ catches up and then pulls away for good — at n = 10
it is already ten times larger, and the gap only widens. So with c = 1
and n₀ = 4 we have n² ≤ 2ⁿ for all
n ≥ 4, which is exactly what O demands.
Answer: n² = O(2ⁿ), witnessed by c = 1, n₀ = 4.
For each pair, find a positive constant c with f(n) ≤ c · g(n) for all n > 1.
Worked solution
The tactic each time: bound every term of f by a copy of the leading term, then compare with g.
(a) f = n² + n + 1, g = 2n³. For n > 1
each of n and 1 is at most n², so
f ≤ 3n². And 3n² ≤ 4n³ = 2 · 2n³
for n > 1. So c = 2 works comfortably.
(b) f = n√n + n² = n1.5 + n², g = n².
For n > 1, n1.5 ≤ n², so
f ≤ n² + n² = 2n². So c = 2.
(c) f = n² − n + 1, g = n²/2. Since
−n + 1 ≤ 0 for n ≥ 1, we have
f ≤ n² = 2 · (n²/2). So c = 2.
Answer: c = 2 works in all three (many other constants work too — any single witness proves the bound).
Suppose f₁ = O(g₁) and f₂ = O(g₂). Show
(2-13) f₁ + f₂ = O(g₁ + g₂); (2-15)
f₁ · f₂ = O(g₁ · g₂); and (2-14) the same
two rules hold with Ω in place of O.
Worked solution
These are the rules that let us analyse code one piece at a time — price each
region, then add or multiply — so they are worth seeing once. "f = O(g)"
just means there is a constant c with f ≤ c · g for
large n. So write f₁ ≤ c₁ g₁ and f₂ ≤
c₂ g₂.
Sum (2-13). Add the two inequalities:
f₁ + f₂ ≤ c₁ g₁ + c₂ g₂. Let
c be the bigger of c₁ and c₂; then the
right side is at most c(g₁ + g₂). So
f₁ + f₂ = O(g₁ + g₂). This is why sequential blocks
of code add up, and keeping the biggest is just simplifying the sum.
Product (2-15). Multiply the two inequalities (all quantities
positive): f₁ · f₂ ≤ c₁ c₂ · g₁
· g₂. The single constant c₁ c₂ does the
job, so f₁ f₂ = O(g₁ g₂). This is why a loop that
runs O(g₁) times doing O(g₂) work each time costs the product.
Ω versions (2-14). Identical argument with the inequalities
flipped: f ≥ c · g. Adding lower bounds gives a lower bound on
the sum; multiplying lower bounds gives a lower bound on the product. So both rules
hold for Ω too — and since Θ is just O and Ω together, they
hold for Θ as well.
Answer: both rules hold; they are the formal licence for the "add sequential parts, multiply nested loops" recipe from §8.5.
Prove that a polynomial ak nk + ak−1 nk−1 + … + a1 n + a0 of degree k is O(nk).
Worked solution
This is the formal version of "keep the highest power". Every term ai ni with i ≤ k satisfies |ai ni| ≤ |ai| nk once n ≥ 1, because a lower power of n is no larger than the top power. Add the terms up:
|ak nk + … + a0| ≤ (|ak| + … + |a0|) · nk
The bracket c = |ak| + … + |a0| is a fixed constant — it does not depend on n. So the whole polynomial is at most c · nk, which is precisely O(nk).
Answer: O(nk), with the constant being the sum of the absolute values of the coefficients.
Show that (n + a)ᵇ = Θ(nᵇ) for any constants a and b > 0.
Worked solution
Intuition first: when n is large, adding a fixed number a barely nudges
it — 1 000 000 + 7 is, for growth purposes, still a million. So
(n + a) and n grow at the same rate, and raising both to
the same power b keeps them at the same rate. Formally, look at the ratio:
(n + a)ᵇ / nᵇ = (1 + a/n)ᵇ → 1b = 1 as n → ∞
Because that ratio approaches 1, for large n it stays trapped between two fixed
constants (say between ½ and 2). That traps (n + a)ᵇ between
½ nᵇ and 2 nᵇ — the sandwich that
defines Θ.
Answer: (n + a)ᵇ = Θ(nᵇ).
List these from slowest- to fastest-growing, marking ties (Skiena's own set):
n 2n n lg n ln n n−n3+7n5 lg n √n en n2+lg n n2 2n−1 lg lg n n3 (lg n)2 n! n1+ε (0<ε<1)
Worked solution
Tidy each into its shape first: n−n3+7n5 → n5; n2+lg n → n2 (ties with n2); ln n ties lg n; 2n−1 = ½·2n ties 2n; en is above 2n (since e>2); and n1+ε sits between n lg n and n2. Then walk the ladder — remembering any real power of n beats any power of a logarithm.
Answer (braces = ties): lg lg n < {ln n = lg n} < (lg n)2 < √n < n < n lg n < n1+ε < {n2 = n2+lg n} < n3 < n5 < {2n−1 = 2n} < en < n!
Same task, with a function that shrinks and a constant hidden in the list:
√n n 2n n log n n−n3+7n5 n2+log n n2 n3 log n n1/3+log n (log n)2 n! ln n n/log n log log n (1/3)n (3/2)n 6
Worked solution
The traps: (1/3)n shrinks to 0 (smallest of all); 6 is a constant; and the sneaky pair (log n)2 vs n1/3 — a squared logarithm is still a logarithm, and any real power of n (even 1/3) eventually wins, so (log n)2 < n1/3. Also n/log n sits just below n, and (3/2)n < 2n.
Answer (braces = ties): (1/3)n < 6 < log log n < {log n = ln n} < (log n)2 < n1/3 < √n < n/log n < n < n log n < {n2 = n2+log n} < n3 < n5 < (3/2)n < 2n < n!
Give functions f and g satisfying each condition, or argue that none exist.
Worked solution
(a) f = o(g) and f ≠ Θ(g). Easy:
take f = n, g = n². n grows strictly slower than n²
(so little-o holds), and they are not the same rate (so not Θ). In fact little-o
always rules out Θ, so any little-o example works.
(b) f = Θ(g) and f = o(g). Impossible.
Θ says "same rate"; little-o says "strictly slower". A function cannot be both
exactly as fast and strictly slower than another. No such pair exists.
(c) f = Θ(g) and f ≠ O(g). Impossible.
Θ is O and Ω together, so f = Θ(g) already forces
f = O(g). You cannot have the first without the second.
(d) f = Ω(g) and f ≠ O(g). Easy:
take f = n², g = n. n² grows at least as fast as n
(Ω holds), but it is not bounded above by n (so not O). This is the usual
picture of a strict lower bound.
Answer: (a) e.g. n, n² · (b) none · (c) none · (d) e.g. n², n.
Decide each, giving a one-line reason from the ladder.
Worked solution
| Claim | Verdict | Why | |
|---|---|---|---|
| (a) | 2n² + 1 = O(n²) | True | a constant factor plus a lower term; still n² |
| (b) | √n = O(log n) | False | √n grows faster than log n, not slower |
| (c) | log n = O(√n) | True | a logarithm is below any power of n |
| (d) | n²(1 + √n) = O(n² log n) | False | the left side is ≈ n²·√n = n2.5, above n² log n |
| (e) | 3n² + √n = O(n²) | True | √n is a lower term; n² dominates |
| (f) | √n log n = O(n) | True | √n·log n grows slower than √n·√n = n |
| (g) | log n = O(n−1/2) | False | log n → ∞ while n−1/2 = 1/√n → 0 |
Answer: (a) T · (b) F · (c) T · (d) F · (e) T · (f) T · (g) F.
State the tightest relationship of f to g.
Worked solution
(a) f = n² + 3n + 4, g = 6n + 7. f is quadratic, g is
linear, so f grows faster: f = Ω(g) (and definitely not O(g)).
(b) f = n√n = n1.5, g = n² − n.
g is ≈ n², above n1.5, so f is the smaller one:
f = O(g).
(c) f = 2ⁿ − n², g = n⁴ + n². The
2ⁿ term swamps the −n² (which is a rounding
error beside it), so f grows like 2ⁿ — an exponential, above
the polynomial g: f = Ω(g).
Answer: (a) Ω · (b) O · (c) Ω.
Answer yes or no, with a one-line reason.
Worked solution
The key idea: O is only a ceiling on the worst case — it
never forbids an algorithm from being faster. Θ on the worst case
is stronger: it says the worst case really does reach that height.
(a) Worst case O(n²) — can it be O(n) on some
inputs? Yes. Individual inputs (like a best case) can finish far
faster; the ceiling only caps the slowest.
(b) Worst case O(n²) — can it be O(n) on
all inputs? Yes. O(n²) is just an upper
bound; an algorithm that is actually linear everywhere is still (loosely) O(n²).
O does not claim the bound is tight.
(c) Worst case Θ(n²) — can it be O(n) on
some inputs? Yes. Θ pins the worst case at
quadratic, but the best case (some inputs) can still be linear or better.
(d) Worst case Θ(n²) — can it be O(n) on
all inputs? No. Θ(n²) worst case means some
inputs genuinely force quadratic work, so not every input can be linear.
(e) Is f(n) = Θ(n²) where f = 100n² for even
n and f = 20n² − n log² n for odd n? Yes.
Every value, even or odd, sits between constant multiples of n² (roughly between
19 n² and 100 n² for large n), so the whole
function is Θ(n²) regardless of which branch it takes.
Answer: (a) yes · (b) yes · (c) yes · (d) no · (e) yes.
For each, answer with a reason. Recall log(aⁿ) = n log a.
Worked solution
(a) 3ⁿ = O(2ⁿ)? No. The ratio
3ⁿ / 2ⁿ = (3/2)ⁿ = 1.5ⁿ races off to infinity, so
no constant can cap it. Different bases are not a constant factor apart.
(b) log 3ⁿ = O(log 2ⁿ)? Yes. Take logs:
log 3ⁿ = n log 3 and log 2ⁿ = n log 2. Their
ratio is the fixed constant log 3 / log 2 ≈ 1.585 — a
constant factor — so each is O of the other. (Taking the log tamed the
exponential into a linear function of n.)
(c) 3ⁿ = Ω(2ⁿ)? Yes. Ω asks
whether 3ⁿ grows at least as fast as 2ⁿ — and it grows much
faster, so certainly at least as fast.
(d) log 3ⁿ = Ω(log 2ⁿ)? Yes. Same
constant-ratio picture as (b): n log 3 versus n log 2. Since
they are a constant factor apart, each is both O and Ω of the other.
Answer: (a) no · (b) yes · (c) yes · (d) yes.
Give the growth class of each sum for i running from 1 to n.
Worked solution
(a) ∑ 1/i. This is the harmonic number, and it is famously
≈ ln n — add the terms up and they creep towards the natural
log. So it is Θ(log n). A quick numerical check confirms the
gap Hₙ − ln n settles at a constant (about 0.577, Euler's
constant):
import math
for n in [10, 100, 1000, 10000]:
H = sum(1.0 / i for i in range(1, n + 1))
print(f"n={n:>6} H={H:.4f} ln n={math.log(n):.4f} gap={H - math.log(n):.4f}")
The gap stops moving, which means H and ln n stay a
constant apart — the signature of Θ(log n).
(b) ∑ ⌈1/i⌉. For every i ≥ 1, the value
1/i lies in (0, 1], so its ceiling ⌈1/i⌉
is exactly 1. Adding 1 a total of n times gives n. So the sum is Θ(n).
(c) ∑ log i. Summing logs is the log of the product:
∑ log i = log(1 · 2 · … · n) = log(n!),
which is Θ(n log n) (see part d).
(d) log(n!). Half of the n factors are at least n/2, so
n! is at least (n/2)n/2, giving
log(n!) ≥ (n/2) log(n/2) — on the order of n log n.
And n! is at most nn, giving
log(n!) ≤ n log n. Trapped both sides: Θ(n log n).
Answer: (a) Θ(log n) · (b) Θ(n) · (c) Θ(n log n) · (d) Θ(n log n).
For each sum over i = 1 … n, give a simple g with the sum = Θ(g).
Worked solution
Two facts do all the work. A sum of n terms whose largest is a power iᵇ
behaves like Θ(nᵇ⁺¹) (n copies of the top term,
roughly). And a geometric sum — each term a fixed multiple of the
last, like 4ⁱ — is dominated by its final term, so it is
Θ of that last term.
(a) ∑ (3i⁴ + 2i³ − 19i + 20). The top piece is
i⁴, and ∑ i⁴ = Θ(n⁵). So
Θ(n⁵).
(b) ∑ (3 · 4ⁱ + 2 · 3ⁱ − i¹⁹ + 20).
The geometric 4ⁱ term outgrows everything, and a geometric sum is
Θ of its last term 4ⁿ. So Θ(4ⁿ).
(c) ∑ (5i + 3 · 2ⁱ). The geometric 2ⁱ
term dominates the linear one; the sum is Θ of its last term. So
Θ(2ⁿ).
Answer: (a) Θ(n⁵) · (b) Θ(4ⁿ) · (c) Θ(2ⁿ).
Let S = ∑i=1..n 3ⁱ. Which are true?
(a) S = Θ(3n−1) ·
(b) S = Θ(3ⁿ) ·
(c) S = Θ(3n+1).
Worked solution
A geometric series sums to S = 3 + 3² + … + 3ⁿ =
(3n+1 − 3) / 2, which is about 3n+1/2.
The whole thing is dominated by its last term, so S = Θ(3ⁿ).
Now the trick: 3n−1, 3ⁿ and
3n+1 differ only by a factor of 3 each way — and a
constant factor of 3 is invisible to Θ. So Θ of any one of
them is Θ of all of them. All three are true.
Answer: (a), (b) and (c) are all correct — they name the same growth class up to a constant.
For each f, give a simple g with f = Θ(g).
Worked solution
The rule is the same one from §8.3: throw away everything except the fastest-growing term (and drop its constant).
(a) f = 1000 · 2ⁿ + 4ⁿ. 4ⁿ = (2ⁿ)²
dwarfs 2ⁿ no matter how big the constant 1000 is. So
Θ(4ⁿ).
(b) f = n + n log n + √n. Of the three, n log n grows
fastest. So Θ(n log n).
(c) f = log(n²⁰) + (log n)¹⁰. The first term is
20 log n (using log n²⁰ = 20 log n) — just a
constant times log n. The second, (log n)¹⁰, is a much higher
power of log n and wins. So Θ((log n)¹⁰).
(d) f = (0.99)ⁿ + n¹⁰⁰. A base below 1 means
(0.99)ⁿ shrinks towards 0 as n grows, so it contributes
nothing; n¹⁰⁰ is the whole story. So
Θ(n¹⁰⁰).
Answer: (a) Θ(4ⁿ) · (b) Θ(n log n) · (c) Θ((log n)¹⁰) · (d) Θ(n¹⁰⁰).
For each pair (A, B), list every relationship that holds. (Remember: if A is strictly slower than B, then both A = O(B) and the stronger A = o(B) hold.)
Worked solution
(a) A = n¹⁰⁰, B = 2ⁿ. A polynomial is
strictly slower than an exponential, so A = O(B) and
A = o(B).
(b) A = (lg n)¹², B = √n. Any power of a
logarithm is strictly slower than any power of n, so A = O(B) and
A = o(B).
(c) A = √n, B = ncos(πn/8). The exponent
cos(πn/8) oscillates forever between −1 and +1, so B keeps
swinging from 1/n up to n and back. Sometimes B is far above
A, sometimes far below — no single relationship holds for all large n.
None of O, o, Ω, ω, Θ apply (a genuine
"can't compare" case).
(d) A = 10ⁿ, B = 100ⁿ. Different bases with
100 > 10: the ratio (10/100)ⁿ = 0.1ⁿ → 0,
so A is strictly slower: A = O(B) and A = o(B).
(e) A = nlg n, B = (lg n)ⁿ. Compare their
logs: lg A = (lg n)², while lg B = n · lg lg n.
The second grows far faster (it has a factor of n), so B outgrows A:
A = O(B) and A = o(B).
(f) A = lg(n!), B = n lg n. From Problem 2-25,
lg(n!) = Θ(n lg n) — same rate. So A = Θ(B)
(and therefore also A = O(B) and A = Ω(B), but not the
strict little versions).
Answer: (a) O, o · (b) O, o · (c) none · (d) O, o · (e) O, o · (f) Θ (with O and Ω).
Not one of these problems needed a limit computed from scratch or an epsilon chased. Every single one came down to: place both functions on the growth ladder, strip constants, and read off which is higher — with two facts on speed-dial (a polynomial beats any logarithm; an exponential beats any polynomial), one trick for taming exponentials (take logs), and one for sums (a sum is Θ of n copies of its top term, or of its last term when it is geometric). That is the whole of asymptotic reasoning at this level.
8.17Two more ordering puzzles (Skiena 2-26, 2-27)
These finish the ordering problems from the chapter. They look scary because sums and odd exponents appear — but each piece collapses to something on the growth ladder from §8.15. The trick is always the same: replace each expression by the simplest thing it grows like, then line them up.
Order these from slowest- to fastest-growing:
f1 = n² log₂ nf2 = n (log₂ n)²f3 = 2⁰ + 2¹ + … + 2ⁿ(a sum of powers of two)f4 = log₂(2⁰ + 2¹ + … + 2ⁿ)
Worked solution
First simplify the two that hide a sum:
f3is a geometric sum:1 + 2 + 4 + … + 2ⁿ = 2ⁿ⁺¹ − 1, which grows like 2ⁿ (exponential).f4is the log of that, andlog₂(2ⁿ⁺¹) = n + 1, which grows like n (linear).
Now compare the four simplified shapes: n, n(log n)², n² log n, 2ⁿ.
To compare f2 = n(log n)² with f1 = n² log n, divide both by n log n: you get log n versus n — and log n is far smaller, so f2 < f1.
Answer: f4 (≈ n) < f2 (n log² n) < f1 (n² log n) < f3 (2ⁿ).
Order these from slowest- to fastest-growing, and say if any are the same order:
f1 = √1 + √2 + … + √n(a sum of square roots)f2 = (√n)log nf3 = n√(log n)f4 = 12·n3/2 + 4n
Worked solution
Simplify each:
f1: adding up√ifori = 1..nis like the area under√x, which grows like n3/2 (about⅔·n3/2).f4 = 12 n3/2 + 4n— keep the top term: also n3/2. Sof1andf4are the same order.f2 = (√n)log n = n(log₂ n)/2— the exponent itself grows withn, so this beats every fixed power ofn.f3 = n√(log n)— the exponent√(log n)also grows, so this too beats every fixed power — but more slowly thanf2, because(log n)/2eventually dwarfs√(log n).
Answer: f1 = f4 (both n3/2) < f3 (n√log n) < f2 (n(log n)/2). The key idea: a fixed power like n3/2 is always beaten by n raised to a power that itself keeps growing.
You can read a Big-O class off code. Week 9 puts it to work: one problem, solved four ways, landing in four completely different classes.