Solutions: Analysis of data structures
Solutions to the practice problems at the end of Analysis of data structures. Line numbers count from the first line of each problem’s code.
Problem 1: Nested loops
Section titled “Problem 1: Nested loops”def f(n): i = 0 while i < n: j = 0 while j < n: print(i, j) j += 1 i += 1First, group the lines by how often they run, and give each group a constant for one run:
- Line 2 (
i = 0): , run once. - Lines 3, 4, and 8 (the outer loop test,
j = 0, andi += 1): , run once per outer iteration, so times. - Lines 5, 6, and 7 (the inner loop test, the
print, andj += 1): , run times per outer iteration, and the outer loop iterates times, so times.
The total running time is . Keeping the highest-order term and dropping its constant gives .
Problem 2: A fixed inner loop
Section titled “Problem 2: A fixed inner loop”The sections are the same as in Problem 1: line 2 costs and runs once; lines 3, 4, and 8 cost and run times. But lines 5, 6, and 7 now run 1000 times per outer iteration, however large is, so they run times. The total is
which is . The 1000 is large, but it is a constant, and it doesn’t grow with .
Problem 3: A loop that calls a function
Section titled “Problem 3: A loop that calls a function”In f, line 6 (i = n) costs and runs once. Lines 7 and 9 (the loop test and i -= 1) cost and run times.
Line 8, g(n), is not a constant-time line: it calls g, whose running time depends on its argument. So analyze g first. Its loop runs times, at constant cost each, so g(x) takes time , and each call g(n) costs . Line 8 runs times, for a total of .
The total is , which is .
Problem 4: A doubling loop variable
Section titled “Problem 4: A doubling loop variable”Line 2 costs and runs once. Lines 3, 4, and 5 cost per iteration. Because i doubles on every iteration, starting at 1, the loop stops once i reaches , after about iterations. The total is , which is .
Problem 5: Factors up to the square root
Section titled “Problem 5: Factors up to the square root”Line 4 (i = 1) and line 9 (the final print) cost together and run once. Lines 5 through 8 (the loop test, the if and its print, and i += 1) cost at most per iteration, and the loop runs while i is at most : times. The total is , which is .
Problem 6: Linked-list operations
Section titled “Problem 6: Linked-list operations”Let the list have items, and so nodes, counting the sentinel. The doesn’t matter in big-Θ terms.
__init__,insert_after,prepend,append, and deleting a given node each take time: each looks at and relinks only a constant number of nodes.prependandappendreach their positions directly, throughsentinel.nextandsentinel.prev.get(i)has to walk nodes from the sentinel, since a linked list cannot jump to a position. In the worst case, is near the end of the list: . (Walking from whichever end is closer halves the worst case, but it is still .)length(), written to walk the list and count, visits every node once in every case: , best and worst. (Keeping a running count, updated on every insertion and deletion, makes it .)
Problem 7: Growing by a constant
Section titled “Problem 7: Growing by a constant”The block starts with capacity 10, so the first 10 appends need no copying. The 11th append finds the block full and copies 10 items into a block of capacity 20; the 21st copies 20 items; the 31st copies 30; and so on. Over appends, the resizes copy
items in total, using the arithmetic-series formula. That is copies for appends, or per append on average. Growing by 10 slots instead of 1 makes the resizes ten times rarer, but that is only a constant factor.
With doubling, the copies are , up to the largest power of 2 below , a total of less than . That is copies for appends, or per append on average, as the notes’ argument about one block at a time showed.
Problem 8: Counting digits
Section titled “Problem 8: Counting digits”Line 2 (count = 0) costs and runs once. Lines 3, 4, and 5 cost per iteration. Each iteration removes one decimal digit from n, so the loop runs once per digit of the original : times. The final loop test and the return cost . The total is
which is . Dividing by 10 instead of 2 changes only the base of the logarithm, which is a constant factor.
Problem 9: An inner loop that starts at i
Section titled “Problem 9: An inner loop that starts at i”The outer loop’s own step costs and runs times. On outer iteration , the inner loop runs times: times when , times when , and so on down to once. So the print runs
times, at each, for a total of , which is . The inner loop is shorter on later iterations, but on average it still runs about times, and a constant factor of doesn’t change the growth rate.
Problem 10: Looking for a repeated value
Section titled “Problem 10: Looking for a repeated value”Line 2 costs and runs once. The outer loop’s step costs per iteration. The inner loop’s test and comparison cost per iteration. The final return False costs .
- Best case: the first two items are equal. The outer loop runs once, the inner loop runs once and returns
True, for , which is . This is a list of any length whose first two items match, not a short list. - Worst case: no value repeats, so every pair is compared. On outer iteration , the inner loop runs times, for comparisons in all. The total is , which is .
Problem 11: Building a list at the front
Section titled “Problem 11: Building a list at the front”The loop’s own step costs and runs times. The call result.insert(0, i) is not constant time: when result holds values, it shifts all of them, at a cost of per value. Over the whole loop, the shifts cost
(The occasional resize of the array adds only in total, as in the amortized analysis of append.) The total is . Appending instead, result.append(i), takes amortized time per value, and then reversing the list once, or building it with range(n - 1, -1, -1) to begin with, takes time in all.
Problem 12: A doubling inner loop
Section titled “Problem 12: A doubling inner loop”The outer loop’s step, together with j = 1, costs and runs times. The inner loop doubles j from 1 until it reaches , so it runs times for each outer iteration, at each. The total is
which is . It is the same on every input of size .
Problem 13: Cases and bounds
Section titled “Problem 13: Cases and bounds”- True. The worst case is , which is at least a constant times for large . It is a lower bound, but not a tight one.
- False. The best case, with the first two items equal, is : it does not grow with at all.
- True. No input takes longer than the worst case, and the worst case is .
- False. A list whose first two items are equal takes constant time. holds for the worst case, not for every input.
- True. For each , the best case is no slower than the worst case, so any upper bound on the worst case is also an upper bound on the best case.
- True. For each , every input takes at least as long as the best case, so any lower bound on the best case holds for every input. Statements 5 and 6 are why upper bounds are usually stated for the worst case and lower bounds for the best case: each then applies to every input.
Problem 14: Shrinking a growing array
Section titled “Problem 14: Shrinking a growing array”-
Alternate appends and pops. The first append finds the block full, so it copies values into a block of capacity and then holds values. The next pop leaves values in a block of capacity , exactly half full, so it copies values into a block of capacity , which is full again. The next append doubles again, the next pop shrinks again, and so on: every operation copies values. A sequence of such operations costs , or per operation, so appends and pops are no longer amortized. The trouble is that each copy leaves the block either completely full or exactly at the shrinking threshold, so no cheap operations come between one copy and the next to pay for it.
-
After the append doubles the block to capacity , it holds values, just over half full. It won’t shrink until it is a quarter full, at values, which takes about pops, and it won’t grow again until it is full, which takes about appends. In general, right after any resize the block is half full, so it takes at least about a quarter of its capacity in operations to reach either threshold. Those cheap operations pay for the next copy, whose size is proportional to the capacity, so appends and pops are again amortized.