Week 08 · Phase 3 · The language of growth

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

Big question: What is the shortest honest way to describe how an algorithm scales?big-Omidterm review≈3 hours
By the end of this week you can
  • 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.

What Big-O deliberately ignores

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

NotationNameIn plain wordsEveryday examplen doubles ⇒ time
O(1)constantCost does not depend on the input size at allReading data[5]; a formulaunchanged
O(log n)logarithmicEach doubling of the data adds one stepThe dictionary halving trick+1 step
O(n)linearOne visit per itemSumming a list; searching for a missing item×2
O(n log n)linearithmicA pass over the data, repeated log n timesGood sorting: sorted()slightly more than ×2
O(n²)quadraticEvery item meets every itemComparing all pairs; bubble sort×4
O(2ⁿ)exponentialEach extra item doubles the total workTrying every combinationsquared — 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:

n3n²500n8000share from 3n²
103005 0008 0002%
10030 00050 0008 00034%
1 0003 000 000500 0008 00085%
10 000300 000 0005 000 0008 00098%
1 000 0003 × 10¹²5 × 10⁸8 00099.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.

A one-line rule you can trust

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:

  1. Is there a loop over the data at all? No → O(1).
  2. One loop over n? → O(n).
  3. A loop inside a loop, both over n? → O(n²).
  4. Does the problem halve each round? → O(log n), or O(n log n) if that halving happens inside a pass over the data.
name each one
# 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 count
Answers

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.

Hidden loops

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:

Here is a function built to exercise both rules. Price it one region at a time:

price_each_region.py
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.

Two loops side by side are not nested

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:

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.

the three cases, on one function
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, absent
0 999 -1

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

Why worst case is the professional default

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.

same job, different space
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 space

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

A cheap watch for space

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.

gallery.py
# 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

MistakeWhy 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

Task 1 — classify

Give the Big-O class of each, in terms of the length of data:

classify these
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.

Task 2 — from measurement to notation

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.

Task 3 — predict in seconds

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.

Task 4 — price the receipt

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.

receipts.py
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 seen
Answers

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

Task 5 — best, worst, average by hand

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:

Drop the constant multiplier and the lower-order term: the shape is linear.

Which grows fastest as n becomes large?

Order from best to worst: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ).

You write for item in queries: and inside it if item in big_list:. If both hold n items, the class is:

This is the hidden-loop trap. One visible loop plus one hidden loop is still nesting, and week 11 shows the one-line fix.

Unqualified, "this search is O(n)" refers to which case?

Worst case is the professional default because it is the only promise that holds for data you have not seen.

A function does one O(n) loop, then a separate O(n²) double loop, then returns. Its class is:

Statements in sequence add; adding O(n) and O(n²) keeps the dominant O(n²). Only nested loops multiply.

A function walks a list once keeping a single running maximum. Its time and space are:

One pass is O(n) time; storing a single number regardless of n is O(1) extra space. Time and space are separate questions.

An inner loop written as for j in range(10): sits inside a loop over n items. The pair is:

A loop whose length does not grow with the data is a constant factor. O(n) × O(10) = O(n).

8.12Midterm revision checklist

Weeks 1–8 in one list
  • 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

Due before week 9
  1. Create AA_Week08.ipynb and complete Tasks 1–5.
  2. 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.
  3. Take the report function from §8.5, type it into Colab, and add a comment above every region naming its class. Then change the inner loop bound from n to 10 and write one sentence on how the overall class changes and why.
  4. 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.
  5. Pick any function you have written and state both its time class and its extra-space class, with a sentence of justification for each.
  6. 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

TermMeaning in plain words
Big-OA short label for how cost grows with input size.
constant / logarithmic / linearO(1) / O(log n) / O(n).
linearithmicO(n log n) — the speed of good sorting.
quadratic / exponentialO(n²) — every pair. O(2ⁿ) — every combination; unusable beyond tiny n.
dominant termThe fastest-growing part of T(n); the one Big-O keeps.
worst caseThe most work the method can be forced to do; the default meaning of a Big-O claim.
space complexityHow the extra memory a method needs grows with the input.
hidden loopA 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.

SymbolSay itIn plain wordsEveryday analogy
O(g)"big-oh"grows no faster than g — an upper bound, a ceilinga speed limit: you never go above it
Ω(g)"big-omega"grows no slower than g — a lower bound, a floora minimum wage: you never earn below it
Θ(g)"big-theta"grows at exactly the same rate as g — ceiling and floor at oncetwo runners who stay side by side the whole race
o(g)"little-oh"grows strictly slower than g — falls hopelessly behinda pedestrian next to a car: the gap only widens
ω(g)"little-omega"grows strictly faster than g — pulls away without limitthe 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.

A one-line way to hold all five

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.

Problem 2-3 · count the loops

How many times is r incremented, and what is the running time in Big-O?

prestiferous.py
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
Worked 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:

step_counter_2_3.py
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 = r
n= 5 r= 140 ratio=- n= 10 r= 1,705 ratio=12.18x n= 20 r= 23,485 ratio=13.77x n= 40 r= 347,270 ratio=14.79x n= 80 r= 5,335,740 ratio=15.36x

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

Problem 2-4 · count the loops

How many times is r incremented, and what is the worst-case running time?

conundrum.py
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
Worked 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:

step_counter_2_4.py
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 = r
n= 10 r= 95 ratio=- n= 20 r= 715 ratio=7.53x n= 40 r= 5,530 ratio=7.73x n= 80 r= 43,460 ratio=7.86x n=160 r= 344,520 ratio=7.93x

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

Problem 2-7 · true or false

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

Problem 2-8 · O, Ω or Θ?

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.

fgVerdictWhy
(a)log(n²)log n + 5Θlog n² = 2 log n; both are log n up to a constant
(b)√nlog(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 + nlog nΩn log n is far above log n
(f)10log 10Θboth are fixed constants — neither grows
(g)2ⁿ10 n²Ωan exponential beats any polynomial
(h)2ⁿ3ⁿO2ⁿ grows slower than 3ⁿ (ratio (2/3)ⁿ→0)

Answer: (a) Θ · (b) Ω · (c) Ω · (d) Ω · (e) Ω · (f) Θ · (g) Ω · (h) O.

Problem 2-9 · which is O of which?

For each pair decide whether f = O(g), g = O(f), or both (which is Θ).

Worked solution
fgVerdictWhy
(a)(n² − n)/26ng = O(f)f is quadratic (≈ n²/2), g is only linear
(b)n + 2√nn²f = O(g)f ≈ n, well below n²
(c)n log nn√n / 2f = O(g)√n grows faster than log n, so n√n beats n log n
(d)n + log n√ng = O(f)f ≈ n, above √n
(e)2 · (log n)²log n + 1g = O(f)(log n)² grows faster than log n
(f)4n log n + n(n² − n)/2f = 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).

Problem 2-10 · prove a Θ

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

Problem 2-11 · prove an O

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:

n123456810
n²14916253664100
2ⁿ2481632642561024

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.

Problem 2-12 · find a constant

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

Problem 2-13, 2-14, 2-15 · the sum and product rules

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.

Problem 2-16 · any polynomial is O(nk)

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.

Problem 2-17 · shifting by a constant does not matter

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

Problem 2-18 · order Skiena's 16 functions

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!

Problem 2-19 · order Skiena's trickier 18 functions

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!

Problem 2-20 · find functions, or explain why you cannot

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.

Problem 2-21 · true or false

Decide each, giving a one-line reason from the ladder.

Worked solution
ClaimVerdictWhy
(a)2n² + 1 = O(n²)Truea 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)Truea logarithm is below any power of n
(d)n²(1 + √n) = O(n² log n)Falsethe 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)Falselog n → ∞ while n−1/2 = 1/√n → 0

Answer: (a) T · (b) F · (c) T · (d) F · (e) T · (f) T · (g) F.

Problem 2-22 · O, Ω, Θ or none?

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

Problem 2-23 · what Big-O does and does not forbid

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.

Problem 2-24 · yes, no, or can't tell?

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.

Problem 2-25 · summation asymptotics

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

harmonic_check.py
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}")
n= 10 H=2.9290 ln n=2.3026 gap=0.6264 n= 100 H=5.1874 ln n=4.6052 gap=0.5822 n= 1000 H=7.4855 ln n=6.9078 gap=0.5777 n= 10000 H=9.7876 ln n=9.2103 gap=0.5773

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

Problem 2-28 · simplify each sum to a Θ

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

Problem 2-29 · a geometric sum

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.

Problem 2-30 · keep the fastest-growing term

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

Problem 2-31 · which of O, o, Ω, ω, Θ hold?

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

The one habit behind every answer above

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.

Problem 2-26 · put four functions in increasing order

Order these from slowest- to fastest-growing:

  • f1 = n² log₂ n
  • f2 = 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:

  • f3 is a geometric sum: 1 + 2 + 4 + … + 2ⁿ = 2ⁿ⁺¹ − 1, which grows like 2ⁿ (exponential).
  • f4 is the log of that, and log₂(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ⁿ).

Problem 2-27 · order four functions, noting any ties

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 n
  • f3 = n√(log n)
  • f4 = 12·n3/2 + 4n
Worked solution

Simplify each:

  • f1: adding up √i for i = 1..n is 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. So f1 and f4 are the same order.
  • f2 = (√n)log n = n(log₂ n)/2 — the exponent itself grows with n, so this beats every fixed power of n.
  • f3 = n√(log n) — the exponent √(log n) also grows, so this too beats every fixed power — but more slowly than f2, because (log n)/2 eventually 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.

Where this leads

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.