Context:FIT2004_MOC Β· the Week 1 lecture-2 spine β pseudocode β recurrence β complexity β and the half Solving Recurrences (Telescoping) does not own: reading auxiliary space off the same recursion
Parent Framework:Recursion
Quick Revision
π― Objective: one recursive function yields two answers β solve the recurrence for time, measure the deepest live frame chain for auxiliary space.
π¦ Core Components:read a, shrink, work off the code β recurrence | telescope β time | max depth β space.
β‘ Key Constraint: auxiliary space is Ξ(maxΒ depth), neverΞ(totalΒ calls) β siblings run sequentially, so naive Fibonacci is O(2N) time but only Ξ(N) space.
π How It Works
1. Stage 1 β code β recurrence
Read three things β a = how many recursive calls the body makes Β· the shrink on the argument (Nβ1 vs N/2) Β· the non-recursive work per call (c constant, or cN if the body scans).
a counts call SITES, not multipliers β a scalar on the returned value is Ξ(1) arithmetic and folds into c; only a second invocation raises a. 2 * f(n//3) βΉ a=1; f(n//3) + f(n//3) βΉ a=2 β worked below.
Write it piecewise with symbolic constants β base T(n)=a for n<k, general T(n)=β―+c for nβ₯k, where k is the guard threshold read off the if β not a reflexive T(1)=b. Without the base line Stage 2 cannot fix the depth.
Sum, donβt nest β two calls in one body give T(Nβ1)+T(Nβ2), an additive branching term β not a product.
Guards are Ξ(1) β if N == 0 / if index > N fold into c; they never change the growth class.
Scope: the argument must SHRINK β assessed recurrences decrease the search space each call (n-1, n//2, n//3); recursions whose argument grows in value are out of assessment scope.
2. Stage 2 β recurrence β time
Owned elsewhere β the solving itself, in the mandated Steps 0β6b exam format (levels β substitute β general form β base β closed form β complexity β verify) β Solving Recurrences (Telescoping).
The one diagnostic β subtracting from the argument gives depth Ξ(N); dividing gives depth Ξ(logN).
3. Stage 3 β recursion β auxiliary space
What the stack costs β each live call holds a frame (parameters, locals, return address) βΉ auxiliary space =Ξ(maxΒ depth)ΓframeΒ size.
Depth reuses Stage 2βs number β the k that solved the recurrence is the depth: shrink-by-one βΉ Ξ(N) frames Β· halving βΉ Ξ(logN) frames.
Only one root-to-leaf path is live β a sibling call cannot start until the previous one has returned and popped, so a branching recursion pays for its height, not its node count.
Add, donβt max, when a data buffer exists β Merge Sort allocates Ξ(N) scratch and carries Ξ(logN) frames βΉ Ξ(N+logN)=Ξ(N); the lecture writes both terms, the tightest quote collapses them.
An iterative rewrite kills the stack term β same recurrence-derived time, O(1) auxiliary β the reason Binary Search is written with a while loop.
βοΈ Core Implementation
πΉ Reading a off the code β the multiplier trap
same shrink, same constant work, different a
def bla_one(n): # n is an integer if n < 3: return n # base: T(n) = a, n < 3 return 2 * bla_one(n // 3) + 4 # ONE call; the *2 and +4 are O(1) # general: T(n) = T(n/3) + c, n >= 3def bla_two(n): if n < 3: return n # base: T(n) = a, n < 3 return bla_two(n // 3) + bla_two(n // 3) + 4 # TWO calls, same size # general: T(n) = 2 T(n/3) + c, n >= 3
π‘ Common Mistake:Writing 2T(n/3) for 2 * bla(n//3) β the algebra looks identical on the page but the call tree does not β T(n/3)+c telescopes to Ξ(logn) (a/b=31β, one chain), whereas 2T(n/3)+c is leaf-dominated Ξ(nlog3β2)βΞ(n0.63). Count the invocations in the source, never the coefficients.
πΉ Linear vs halving recursion β the same shape, two classes
power (Ξ(N)) vs power_better (Ξ(logN))
def power(x, n): # T(n) = T(n-1) + c if n == 0: return 1 if n == 1: return x return x * power(x, n - 1) # depth n-1 -> Theta(n) time, Theta(n) framesdef power_better(x, n): # T(n) = T(n/2) + c if n == 0: return 1 if n == 1: return x if n % 2 == 0: return power_better(x * x, n // 2) # square the base, halve the exponent return x * power_better(x * x, n // 2) # odd -> peel one factor first# depth floor(log2 n)+1 -> Theta(log n) time, Theta(log n) frames
π‘ Common Mistake:Squaring the base is what buys the halving β power_better(x, n//2) (base left alone) computes xn/2, not xn; the recursion must pass x*x because xn=(x2)n/2.
πΉ Duplicate calls β the halving that buys nothing (Applied 2 P4)
power_naive (2T(p/2)+c=Ξ(p)) vs the one-line repair (Ξ(logp))
def power_naive(x, p): # T(p) = 2 T(p/2) + c if p == 0: return 1 if p == 1: return x if p % 2 == 0: return power_naive(x, p // 2) * power_naive(x, p // 2) # SAME argument, twice return power_naive(x, p // 2) * power_naive(x, p // 2) * x# halving depth log2(p), but a BINARY call tree -> Theta(p) calls -> Theta(p) timedef power_fast(x, p): # T(p) = T(p/2) + c if p == 0: return 1 y = power_fast(x, p // 2) # compute ONCE, bind it if p % 2 == 0: return y * y return y * y * x# one call per level -> Theta(log p) time, Theta(log p) frames
π‘ Common Mistake:Assuming βit halves, so itβs Ξ(logp)β β halving fixes only the treeβs height; two calls per node fill that height with Ξ(2log2βp)=Ξ(p) nodes, so power_naive is no faster than the Ξ(p) decrement version. Depth and node count are separate questions.
The leaf-count argument, stated as the sheet does β the call tree is binary of height log2βp; a binary tree of height h (root at level 0) holds at most 2h+1β1 nodes βΉ Ξ(2log2βp)=Ξ(p) calls of O(1) work each. The collapse uses the exponent swapalogbβn=nlogbβa β Solving Recurrences (Telescoping).
The transferable repair β two recursive calls with identical arguments are a common subexpression β bind the result to a variable and reuse it. One y = deletes an entire branch of the tree, taking a from 2 to 1 and Ξ(p) to Ξ(logp).
It is the seed of memoisation β eliminating duplicate calls within one frame is the trivial case; eliminating them across the whole tree is what dynamic programming does when the subproblems overlap.
Two independent routes to Ξ(logp) β power_fast keeps the base and squares the result; power_better above squares the base and passes x*x. Both make a=1; neither is better, and mixing them (squaring both) computes x2p.
πΉ Branching recursion β exponential time, linear space
naive fibonacci β the space trap
def fibonacci(n): # T(n) = T(n-1) + T(n-2) + c if n == 0: return 0 if n == 1: return 1 return fibonacci(n - 1) + fibonacci(n - 2)# calls ~ Theta(phi^n) -> O(2^n) time# frames ~ n -> Theta(n) auxiliary space
π‘ Common Mistake:Counting calls as frames β the call tree has Ξ(Οn) nodes but its height is only n; fibonacci(n-1) fully returns and pops before fibonacci(n-2) is entered, so the stack never holds more than n frames.
βοΈ Complexity
(Every function in the lecture-2 deck, both deliverables side by side.)
When It Flips: the two columns decouple whenever the recursion branches. With one call per body, time and space share the depth (Ξ(N)/Ξ(N), Ξ(logN)/Ξ(logN)); with two or more, time counts nodes and space counts height, and the gap can be exponential.
π Exam Execution Trace
Manual Execution Trace
power_better(2, 13) β the live stack, deepest frame at the bottom:
Problem:linear_search_recursive(array[1..N], target, index=1) returns False if index > N, the index on a match, else recurses with index+1. Give worst-case time and auxiliary space, and say what the iterative version changes.
Final Extracted Output:Ξ(N) time, Ξ(N) auxiliary. The iterative scan keeps Ξ(N) time but drops to O(1) auxiliary β the recursion buys nothing here, which is why the Linear Search reference implementation is a loop.
β οΈ Common Mistakes
π‘ βIn-placeβ does not survive recursion silently β a recursive algorithm that allocates nothing still pays Ξ(depth) for frames; quote it or lose the space mark.
π‘ Quoting Ξ(NlogN) space for Merge Sort β the scratch arrays at different levels are not simultaneously live; sizes N,2Nβ,4Nβ,β¦ sum to <2N by the r=21β bound in Geometric Series.
π‘ Forgetting the base case in Stage 1 β the general form in k is unsolvable without it; a recurrence written as T(N)=T(Nβ1)+c alone earns no marks.
π Beyond the lecture(not in the slides) β CPython performs no tail-call elimination, so even a tail-recursive linear_search_recursive really does hold N frames and raises RecursionError near depth 1000. The Ξ(depth) space cost is a runtime fact here, not just an accounting convention.
π§ Active Recall
return 2 * f(n//3) + 4 and return f(n//3) + f(n//3) + 4 sit in otherwise identical functions. Write both recurrences and both complexities.
Hint:a is a count of invocations; everything else in the return line is Ξ(1).
Answer
Short answer: Both share the base T(n)=a for n<3. The first is T(n)=T(n/3)+cβΞ(logn); the second is T(n)=2T(n/3)+cβΞ(nlog3β2)βΞ(n0.63).
Why:Coefficients donβt branch β multiplying one returned value by 2 is a single Ξ(1) op inside one frame, so the call tree stays a chain of depth log3βn. Writing the call twice makes level i hold 2i frames, and with a Ξ(1) combine the level totals sum geometrically to Ξ(2log3βn)=Ξ(nlog3β2) β leaf-dominated, polynomial instead of logarithmic.
A power function halves its exponent every call yet runs in Ξ(p), not Ξ(logp). Diagnose it, and repair it in one line.
Hint: Height and node count are different measurements of the same tree.
Answer
Short answer: it calls power(x, p/2)twice with the same argument, so T(p)=2T(p/2)+c; the tree is binary of height log2βp and therefore holds Ξ(2log2βp)=Ξ(p) nodes. Bind y = power(x, p//2) once and return y*y, giving T(p)=T(p/2)+c=Ξ(logp).
Why:Halving bounds the depth, branching fills it β a is what multiplies the work per level, and with a=2,b=2 against a Ξ(1) combine the level totals are leaf-dominated βΉ Ξ(plog2β2)=Ξ(p). Two recursive calls on identical arguments are a common subexpression, and deleting one is a Ξ(p)βΞ(logp) change in the growth class, not a micro-optimisation.
Naive fibonacci(n) makes exponentially many calls yet uses only Ξ(n) auxiliary space. Reconcile the two.
Hint: Ask what is live at one instant, not what happens over the whole run.
Answer
Short answer: Time counts every node of the call tree (Ξ(Οn), bounded as O(2n)); space counts only the treeβs height (n), because at most one root-to-leaf chain of frames exists at a time.
Why:Siblings are sequential β fibonacci(n-1) runs to completion and its entire subtree pops off the stack beforefibonacci(n-2) is pushed, so the two subtrees never coexist in memory even though both are paid for in time.
Two functions both recurse once per call and do Ξ(1) work. Why is one Ξ(N) and the other Ξ(logN) in both time and space?
Hint: How the argument shrinks fixes the depth, and the depth fixes both columns.
Answer
Short answer: Subtracting (T(N)=T(Nβ1)+c) needs N steps to reach the base; halving (T(N)=T(N/2)+c) needs log2βN. With one call per body, that depth is both the step count and the frame count.
Why:Single-chain recursion β the call tree degenerates to a path, so nodes = height; only branching (aβ₯2) separates the time and space answers.
Why does Merge Sort end up Ξ(N) auxiliary rather than Ξ(NlogN), given every one of its logN levels allocates?
Hint: Two different sums β the total allocated over time, and the peak live at once.
Answer
Short answer: Peak live memory is what is quoted. Frames along one path hold scratch of sizes N,2Nβ,4Nβ,β¦, summing to <2N=Ξ(N); the Ξ(logN) stack is dominated and vanishes.
Why:Shrinking allocations sum, they do not multiply β by the r=21βGeometric Series bound the whole chain costs less than twice the top level, so the logN levels contribute a constant factor, not a log factor.