1.2.38 · D5Introduction to Programming (Python)
Question bank — Classic recursion — factorial, Fibonacci, binary search
First, three symbols this page uses (earn them before you meet them)
Some later answers say things like (for some growth base we'll pin down) or . If those look like alien runes, read this section once — then the whole page is self-contained.
True or false — justify
A well-defined recursive algorithm must terminate for every valid input, not merely some.
True. "Terminates for some inputs" is not enough — a correct algorithm must reach a base case on all valid inputs. If even one valid input recurses forever, the algorithm is broken; the "smaller" step must provably approach the base every time.
Factorial's base case could be instead of and still work for all .
True for , but it breaks on
factorial(0) — that call would recurse to factorial(-1) and never stop. Using covers one more input safely, which is why it's preferred.fib with only one base case (if n == 0: return 0) still works.
False.
fib(1) would compute fib(0) + fib(-1), and fib(-1) recurses forever below zero. The recurrence reaches back two steps, so it needs two known values.Naive recursive Fibonacci gives wrong answers because it's slow.
False. It gives perfectly correct answers — it is only wasteful, recomputing the same subproblems exponentially. Slowness and incorrectness are different failures; here only speed is wrong.
Binary search returns the smallest index of the target when duplicates exist.
False (as written). The middle-first logic can land on any matching copy; it returns some valid index, not guaranteed the leftmost. Finding the first occurrence needs an extra "keep searching left" tweak.
Every recursive function can be rewritten as a loop.
True — see Recursion vs Iteration. Any recursion can be simulated with an explicit stack and a loop; the machine already does this with its own call stack.
Memoizing factorial gives the same speedup as memoizing Fibonacci.
False. Factorial's calls never repeat a subproblem —
factorial(n) calls factorial(n-1) exactly once each — so caching adds overhead with zero benefit. Fibonacci repeats subproblems, which is why memoization helps it.Binary search on an unsorted list always returns .
False. It may return a wrong index or miss a present element — the "throw away a half" logic is simply invalid on unsorted data, so results are unreliable, not uniformly .
The Fibonacci call tree — why , seen and derived
Why is naive Fibonacci and not ?
Because the call tree is lopsided — one child is
fib(n-1), the other only fib(n-2) — so leaves multiply by per level, not by . The characteristic equation pins the growth base to the golden ratio .Spot the error
def factorial(n):
return n * factorial(n - 1)Why is this wrong?
There is no base case. It recurses forever (below zero too), triggering
RecursionError: maximum recursion depth exceeded. Every recursion needs a floor.def factorial(n):
if n == 0:
return 1
return factorial(n - 1) # forgot the "n *"What does this return for any , and why?
Always
1. It recurses down to the base and returns 1 without ever multiplying by n — the "combine" step of the recursive case is missing, so no actual product is built.def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 1) # both terms n-1Why is this wrong?
The recurrence must be , not . This computes instead of Fibonacci — right shape of recursion, wrong subproblem.
def binary_search(arr, target, lo=0, hi=None):
if hi is None: hi = len(arr) # should be len(arr) - 1
...
mid = (lo + hi) // 2
if arr[mid] == target: return mid
elif arr[mid] < target: return binary_search(arr, target, mid + 1, hi)
else: return binary_search(arr, target, lo, mid - 1)Trace the full control flow: what actually goes wrong when target exceeds every element?
With
hi = len(arr) the range includes the invalid index len(arr). As the search keeps going right, mid climbs toward len(arr); the moment mid == len(arr), arr[mid] reads past the end → IndexError. (If the target is present, it may return the right index by luck first — the bug only bites on the right-edge path, which is exactly why off-by-one range bugs are so sneaky.)def binary_search(arr, target, lo, hi):
mid = (lo + hi) // 2
if arr[mid] < target:
return binary_search(arr, target, mid, hi) # mid, not mid+1
...Why can this loop forever?
When
lo and hi are adjacent, mid == lo, and recursing with lo = mid (not mid + 1) never shrinks the range. The problem must get strictly smaller each call.def fib(n, memo={}):
memo[n] = fib(n-1) + fib(n-2) # base case check missing
return memo[n]Why does memoization not save this version?
The base case check is gone, so it recurses below zero forever before any cache entry helps. Memoization speeds a correct recursion; it can't rescue a missing base case.
Using a mutable default argument def fib(n, memo={}) — what subtle trap does it carry?
The
{} is created once and shared across all calls, even between separate top-level invocations. Cached values persist (usually fine here, but surprising) — a classic Python gotcha worth knowing.Binary search intervals — watch the window shrink
Why questions
Why does the multiplication in factorial(n) happen after the recursive call returns, not before?
Because
n * factorial(n-1) must wait for factorial(n-1) to produce a value first. Each pending multiply sits on the call stack and only fires as the stack unwinds from the base up.Why does memoization turn into ?
Each distinct for is computed exactly once and stored; later requests are lookups. Work becomes proportional to the number of unique subproblems, which is . See Memoization and Dynamic Programming.
Why does binary search need the list sorted, precisely?
Sorted order is what makes "everything left of
mid is smaller, everything right is bigger" true — that single fact is what lets one comparison safely discard a whole half. Remove sortedness and the half you throw away might contain the target.Why is binary search a Divide and Conquer algorithm but factorial is not?
Divide and conquer splits the problem into parts and combines them; binary search halves the search space. Factorial only reduces by one each step (linear chain), so it's plain recursion, not divide-and-conquer.
Why does the recurrence solve to ?
First fix its base case: a problem of size takes a constant amount of work, (one comparison, no further recursion). Now unroll: . The recursion stops when the size hits , i.e. , and there . So total work — you can only halve that many times.
Why can excessive recursion depth crash even a correct recursion?
Every pending call holds a stack frame, and the call stack has a finite limit (Python's default is ~1000). Deep-but-valid recursion (e.g.
factorial(5000)) overflows it → RecursionError.Edge cases
What does correct factorial(0) return, and why is that not "nothing"?
It returns
1. An empty product is defined as 1 — forced by the pattern (see the opening figure) — and it also serves as the base that halts recursion.What do fib(0) and fib(1) return, and why must both be hard-coded?
0 and 1 respectively. They are the two seeds the recurrence reaches back to; without both, fib(1) would try to use the nonexistent fib(-1).Binary search on an empty list [] — what happens?
hi = len(arr) - 1 = -1, so lo (0) > hi (-1) is immediately true → returns -1 (not found). The empty-range base case handles it cleanly.Binary search for a target smaller than every element — trace the shrink.
Each comparison finds
arr[mid] > target, so hi keeps moving left until lo > hi → returns -1. It never spins; the range strictly shrinks to empty.Binary search on a single-element list [5] searching for 5 vs 3.
For
5: lo=hi=0, mid=0, arr[0]==5 → returns 0. For 3: arr[0]=5>3 → hi=-1, then lo>hi → returns -1. Both terminate in one or two steps.What is factorial(-1) with the standard if n == 0 base — safe or not?
Not safe. It never hits
n == 0, recursing forever → RecursionError. Negative inputs are outside the definition and should be rejected up front.For binary search, what if the list has duplicate targets — is the result deterministic?
Yes, deterministic for a fixed list: the same
mid sequence runs every time, returning the same index. But it isn't guaranteed to be the first or last duplicate — just a valid one.What is the smallest where naive fib(n) noticeably lags, and why does it appear so suddenly?
Around
fib(35)–fib(40). Because cost grows as , each extra multiplies work by ~1.6 — the slowdown feels sudden precisely because exponential growth stays cheap then explodes.Recall One-line self-test
Cover the answers above. Can you, for each of the four failure modes — missing base case, wrong subproblem, off-by-one range, unsorted input — name the exact symptom it produces (infinite recursion / RecursionError, a wrong value, an IndexError or endless loop, and unreliable results)? If you can pair every failure with its symptom without peeking, you own this topic.
Connections
- Recursion vs Iteration — every trap here has a loop-based analogue
- Big-O Notation — the vs reasoning and notation
- Memoization and Dynamic Programming — the fix for repeated subproblems
- Divide and Conquer — why binary search halves and factorial doesn't
- The Call Stack and Stack Frames — the source of
RecursionError - Sorting Algorithms — the precondition binary search silently assumes