Context:FIT2004_MOC Β· the lecture-2 spine β pseudocode β recurrence β complexity β plus 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, 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.
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) and fold into c.
Scope: the argument must SHRINK β assessed recurrences decrease the search space each call (n-1, n//2, n//3); recursions whose argument grows are out of scope.
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 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).
An iterative rewrite kills the stack term β same time, O(1) auxiliary β why 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): 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 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 but the call tree does not β T(n/3)+c telescopes to Ξ(logn) (one chain), whereas 2T(n/3)+c is leaf-dominated Ξ(nlog3β2)βΞ(n0.63). Count invocations in the source, never 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) 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. Depth and node count are separate questions.
The leaf-count argument β a binary tree of height h holds at most 2h+1β1 nodes βΉ Ξ(2log2βp)=Ξ(p) calls of O(1) work, via the exponent swapalogbβn=nlogbβa β Solving Recurrences (Telescoping).
The transferable repair β two recursive calls with identical arguments are a common subexpression β bind and reuse. One y = takes a from 2 to 1 and Ξ(p) to Ξ(logp); eliminating such duplicates across the whole tree is what memoisation turns dynamic programming into when the subproblems overlap.
Two independent routes to Ξ(logp) β power_fast squares the result; power_better squares the base. Both make a=1; mixing them 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 pushed.
βοΈ 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; 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, which is why Linear Search 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 β scratch arrays at different levels are not simultaneously live; 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.
π 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.
π§ 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 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, summing geometrically to Ξ(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 and the binary tree of height log2βp holds Ξ(p) nodes. Bind y = power(x, p//2) once and return y*y βΉ T(p)=T(p/2)+c=Ξ(logp).
Why:Halving bounds the depth, branching fills it β with a=2,b=2 against a Ξ(1) combine the level totals are leaf-dominated. Deleting one duplicate call is a change of 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)βs entire subtree pops off the stack beforefibonacci(n-2) is pushed, so the two subtrees never coexist in memory though both are paid for in time.
Why does Merge Sort end up Ξ(N) auxiliary rather than Ξ(NlogN), given every one of its logN levels allocates?
Hint: Two different sums β total allocated over time, and 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.
Why:Shrinking allocations sum, they do not multiply β by the r=21βGeometric Series bound the chain costs less than twice the top level, so the logN levels contribute a constant factor, not a log factor.