Skip to content

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.

def f(n):
i = 0
while i < n:
j = 0
while j < n:
print(i, j)
j += 1
i += 1

First, group the lines by how often they run, and give each group a constant for one run:

  • Line 2 (i = 0): c1c_1, run once.
  • Lines 3, 4, and 8 (the outer loop test, j = 0, and i += 1): c2c_2, run once per outer iteration, so nn times.
  • Lines 5, 6, and 7 (the inner loop test, the print, and j += 1): c3c_3, run nn times per outer iteration, and the outer loop iterates nn times, so n2n^2 times.

The total running time is c1+c2n+c3n2c_1 + c_2 n + c_3 n^2. Keeping the highest-order term and dropping its constant gives Θ(n2)\Theta(n^2).

The sections are the same as in Problem 1: line 2 costs c1c_1 and runs once; lines 3, 4, and 8 cost c2c_2 and run nn times. But lines 5, 6, and 7 now run 1000 times per outer iteration, however large nn is, so they run 1000n1000n times. The total is

c1+c2n+1000 c3n,c_1 + c_2 n + 1000\, c_3 n,

which is Θ(n)\Theta(n). The 1000 is large, but it is a constant, and it doesn’t grow with nn.

In f, line 6 (i = n) costs c1c_1 and runs once. Lines 7 and 9 (the loop test and i -= 1) cost c2c_2 and run nn 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 xx times, at constant cost c3c_3 each, so g(x) takes time c3xc_3 x, and each call g(n) costs c3nc_3 n. Line 8 runs nn times, for a total of c3n⋅nc_3 n \cdot n.

The total is c1+c2n+c3n2c_1 + c_2 n + c_3 n^2, which is Θ(n2)\Theta(n^2).

Line 2 costs c1c_1 and runs once. Lines 3, 4, and 5 cost c2c_2 per iteration. Because i doubles on every iteration, starting at 1, the loop stops once i reaches nn, after about log⁡2n\log_2 n iterations. The total is c1+c2log⁡2nc_1 + c_2 \log_2 n, which is Θ(log⁡n)\Theta(\log n).

Line 4 (i = 1) and line 9 (the final print) cost c1c_1 together and run once. Lines 5 through 8 (the loop test, the if and its print, and i += 1) cost at most c2c_2 per iteration, and the loop runs while i is at most n\sqrt n: ⌊n⌋\lfloor \sqrt n \rfloor times. The total is c1+c2nc_1 + c_2 \sqrt n, which is Θ(n)\Theta(\sqrt n).

Let the list have nn items, and so n+1n + 1 nodes, counting the sentinel. The +1+1 doesn’t matter in big-Θ terms.

  • __init__, insert_after, prepend, append, and deleting a given node each take Θ(1)\Theta(1) time: each looks at and relinks only a constant number of nodes. prepend and append reach their positions directly, through sentinel.next and sentinel.prev.
  • get(i) has to walk ii nodes from the sentinel, since a linked list cannot jump to a position. In the worst case, ii is near the end of the list: Θ(n)\Theta(n). (Walking from whichever end is closer halves the worst case, but it is still Θ(n)\Theta(n).)
  • length(), written to walk the list and count, visits every node once in every case: Θ(n)\Theta(n), best and worst. (Keeping a running count, updated on every insertion and deletion, makes it Θ(1)\Theta(1).)

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 nn appends, the resizes copy

10+20+⋯+(n−10)=10(1+2+⋯+(n10−1))=10⋅(n10−1)n102=n(n−10)2010 + 20 + \cdots + (n - 10) = 10\left(1 + 2 + \cdots + \left(\frac{n}{10} - 1\right)\right) = 10 \cdot \frac{\left(\frac{n}{10} - 1\right)\frac{n}{10}}{2} = \frac{n(n - 10)}{20}

items in total, using the arithmetic-series formula. That is Θ(n2)\Theta(n^2) copies for nn appends, or Θ(n)\Theta(n) 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 1+2+4+⋯1 + 2 + 4 + \cdots, up to the largest power of 2 below nn, a total of less than 2n2n. That is Θ(n)\Theta(n) copies for nn appends, or Θ(1)\Theta(1) per append on average, as the notes’ argument about one block at a time showed.

Line 2 (count = 0) costs c1c_1 and runs once. Lines 3, 4, and 5 cost c2c_2 per iteration. Each iteration removes one decimal digit from n, so the loop runs once per digit of the original nn: ⌊log⁡10n⌋+1\lfloor \log_{10} n \rfloor + 1 times. The final loop test and the return cost c3c_3. The total is

c1+c2(⌊log⁡10n⌋+1)+c3,c_1 + c_2 (\lfloor \log_{10} n \rfloor + 1) + c_3,

which is Θ(log⁡n)\Theta(\log n). Dividing by 10 instead of 2 changes only the base of the logarithm, which is a constant factor.

The outer loop’s own step costs c1c_1 and runs nn times. On outer iteration ii, the inner loop runs n−in - i times: nn times when i=0i = 0, n−1n - 1 times when i=1i = 1, and so on down to once. So the print runs

n+(n−1)+⋯+1=n(n+1)2n + (n - 1) + \cdots + 1 = \frac{n(n+1)}{2}

times, at c2c_2 each, for a total of c1n+c2n(n+1)2c_1 n + c_2 \frac{n(n+1)}{2}, which is Θ(n2)\Theta(n^2). The inner loop is shorter on later iterations, but on average it still runs about n/2n/2 times, and a constant factor of 1/21/2 doesn’t change the growth rate.

Line 2 costs c1c_1 and runs once. The outer loop’s step costs c2c_2 per iteration. The inner loop’s test and comparison cost c3c_3 per iteration. The final return False costs c4c_4.

  • Best case: the first two items are equal. The outer loop runs once, the inner loop runs once and returns True, for c1+c2+c3c_1 + c_2 + c_3, which is Θ(1)\Theta(1). 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 ii, the inner loop runs n−1−in - 1 - i times, for (n−1)+(n−2)+⋯+0=n(n−1)2(n-1) + (n-2) + \cdots + 0 = \frac{n(n-1)}{2} comparisons in all. The total is c1+c2n+c3n(n−1)2+c4c_1 + c_2 n + c_3 \frac{n(n-1)}{2} + c_4, which is Θ(n2)\Theta(n^2).

The loop’s own step costs c1c_1 and runs nn times. The call result.insert(0, i) is not constant time: when result holds ii values, it shifts all ii of them, at a cost of c2c_2 per value. Over the whole loop, the shifts cost

c2(0+1+⋯+(n−1))=c2n(n−1)2.c_2 (0 + 1 + \cdots + (n-1)) = c_2 \frac{n(n-1)}{2}.

(The occasional resize of the array adds only Θ(n)\Theta(n) in total, as in the amortized analysis of append.) The total is Θ(n2)\Theta(n^2). Appending instead, result.append(i), takes Θ(1)\Theta(1) amortized time per value, and then reversing the list once, or building it with range(n - 1, -1, -1) to begin with, takes Θ(n)\Theta(n) time in all.

The outer loop’s step, together with j = 1, costs c1c_1 and runs nn times. The inner loop doubles j from 1 until it reaches nn, so it runs ⌈log⁡2n⌉\lceil \log_2 n \rceil times for each outer iteration, at c2c_2 each. The total is

c1n+c2n⌈log⁡2n⌉,c_1 n + c_2 n \lceil \log_2 n \rceil,

which is Θ(nlog⁡n)\Theta(n \log n). It is the same on every input of size nn.

  1. True. The worst case is Θ(n2)\Theta(n^2), which is at least a constant times nn for large nn. It is a lower bound, but not a tight one.
  2. False. The best case, with the first two items equal, is Θ(1)\Theta(1): it does not grow with nn at all.
  3. True. No input takes longer than the worst case, and the worst case is O(n2)O(n^2).
  4. False. A list whose first two items are equal takes constant time. Ω(n2)\Omega(n^2) holds for the worst case, not for every input.
  5. True. For each nn, 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.
  6. True. For each nn, 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.
  1. Alternate appends and pops. The first append finds the block full, so it copies mm values into a block of capacity 2m2m and then holds m+1m + 1 values. The next pop leaves mm values in a block of capacity 2m2m, exactly half full, so it copies mm values into a block of capacity mm, which is full again. The next append doubles again, the next pop shrinks again, and so on: every operation copies mm values. A sequence of kk such operations costs Θ(km)\Theta(km), or Θ(m)\Theta(m) per operation, so appends and pops are no longer Θ(1)\Theta(1) 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.

  2. After the append doubles the block to capacity 2m2m, it holds m+1m + 1 values, just over half full. It won’t shrink until it is a quarter full, at m/2m/2 values, which takes about m/2m/2 pops, and it won’t grow again until it is full, which takes about mm 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 Θ(1)\Theta(1) amortized.