skip to content

questions

12

Why can a recursive function with a correct base case still recurse forever?

level: juniorimportance: must knowfreq 78%

answer

  1. the stopping condition is only half
  2. what must change on every call
  3. name the quantity that shrinks
  4. strictly decreasing, not merely different
  5. and it has to land on the base

basics

~20 s

A base case only names where to stop; termination also requires every recursive call to move strictly closer to it. If the argument never shrinks, or steps past the base case without matching it, the recursion never ends.

solid answer

~50 s

Termination is a two-part obligation. First there must be a base case: a condition under which the function returns without calling itself. Second, every recursive call must make progress — some measure computed from the arguments must strictly decrease, and it must decrease along a path that actually lands on the base condition. A function that recurses on the same value, or that shrinks the argument on one branch and passes it untouched on another, satisfies the first obligation and still runs forever. The subtler failure is a measure that decreases but skips the base: stepping by two toward an `n == 0` test never matches from an odd start. In review I say the measure out loud — "characters remaining strictly decreases, and every step lands on a value the base tests" — because if you cannot name it, it usually is not there.

go deeper

for a junior

Be ready to state both obligations in one breath: a base case that returns without recursing, and a recursive call on a strictly smaller argument. Expect to be asked to point at both in a short fragment.

for a middle

Explain progress as a named measure that strictly decreases and cannot descend forever, and show how an equality base condition gets stepped over when the measure moves in strides larger than one.

for a senior

Show how you audit this in review: state the measure aloud, ask which inputs actually reach the base, and require the degenerate inputs — empty, single element, extreme value — as tests before approving.

for a principal

Own the standard. Recursive code over untrusted or generated input deserves a written termination argument and degenerate-input tests, because one non-terminating branch discovered in production costs far more than the review minute spent naming the measure.

## Two obligations, not one A recursive function terminates only if two separate things hold, and answers routinely supply just the first. **1. A base case exists.** Somewhere there is a condition under which the function returns a value without calling itself. This is the exit. **2. Every recursive call makes progress toward that exit.** Progress is not vague. It means you can name a *measure* — a quantity computed from the arguments — such that: - it **strictly decreases** on every recursive call (not "usually", not "on most branches"), - it is drawn from a **well-founded** domain, one with no infinite descending chain (the non-negative integers, the number of remaining elements, the number of nodes in a subtree), and - the values it descends through actually **satisfy the base condition** at some point. Miss any of the three and the exit exists but is never taken. ## Why "the argument changes" is not progress Three classic ways a changing argument still fails to terminate: - **It changes without shrinking.** A function that alternates between two states visits a new argument on every call and revisits the same pair forever. - **It shrinks in a domain that is not well-founded.** Halving a positive fractional value gets smaller every time and never reaches zero: 1, 0.5, 0.25, … There is no smallest element to land on, so "it gets smaller" is not a termination argument. - **It shrinks on some paths only.** A branch you forgot passes the argument through unchanged. Typical inputs take the shrinking branch, and the bug hides until an input steers into the other one. ## Stepping over the base Even a strictly decreasing integer measure is not enough when the base case is an **equality** test and the step is larger than one. If the value moves in strides of two and the base tests `n == 0`, an odd start passes through 1 and goes negative, matching nothing. The measure decreased on every call, exactly as promised; the exit condition simply was never on the path. This is why base conditions are safer written as guards over a *region* ("stop while at or below the stride") than as a single point, and why a recursion whose step is bigger than one usually needs more than one base case. ## Measures for structures, not just numbers When the argument is a nested record rather than a counter, pick a numeric measure of it: the number of nodes remaining in the subtree, the size of the region still to process, the count of items not yet consumed. "The child is smaller than the parent" is a real measure because subtree node counts are non-negative integers. Where the data can contain a cycle, no such measure exists until you make one — for example by tracking visited items so the count of unvisited ones strictly decreases. ## Some languages check this for you; most do not This is a genuine design axis. Proof-oriented languages such as Agda and Coq reject a recursive definition outright unless a decreasing argument is evident to the compiler, so a non-terminating recursion cannot be written by accident. Mainstream languages such as Java, Go and Python accept any recursion at all and let it fail at runtime, which puts the entire termination argument in the reviewer's head. When the checker will not do it for you, saying the measure out loud in review is the substitute. ## The cost of getting it wrong Non-termination is a quiet defect. The typical input takes the shrinking path and the function returns fine, so tests pass and the reviewer sees a base case and moves on. It is the degenerate inputs — empty, single element, the extreme value, the odd length — that steer into the unguarded path. That is why the practical habit is worth more than the theory: name the measure, then ask which inputs reach the base, then write the degenerate inputs as tests.

  • What makes a decreasing measure trustworthy rather than hand-waving?
    It has to be a concrete quantity computed from the arguments — remaining elements, subtree node count, magnitude of a counter — that strictly decreases on every recursive call and comes from a domain with no infinite descent, such as the non-negative integers. Naming it turns termination from a feeling into a claim a reviewer can check against each branch.
  • Can the argument differ on every call and the recursion still never end?
    Yes. Alternating between two values changes the argument each time yet revisits the same states forever. Halving a positive fractional value strictly decreases and never reaches zero, because that domain has no least element. Both satisfy "the argument changed"; neither satisfies strict decrease within a well-founded domain that reaches the base.
  • How do you argue termination when the argument is a nested structure rather than a number?
    Map the structure to a number and argue about that: nodes remaining in the subtree, items left to consume, size of the region still open. Show every recursive call reduces it. If the data may contain a cycle, no such measure exists until you add one — for instance a visited set, so the count of unvisited items strictly decreases.

A base case is the exit door. Progress is walking toward it. A room with a door you never step toward holds you just as well as a room with no door at all.

saying these in an interview costs you the question

  • Claims a base case by itself guarantees termination
  • Counts any change of argument as progress
  • Says the input gets smaller eventually without naming a measure
  • Assumes an equality base case is hit by any decreasing step
  • Believes a non-terminating branch would always show up in testing

context

open as a page

Why is a recursive function that allocates no data structures still not O(1) space?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Every unfinished call keeps an activation record on the call stack — parameters, locals, return address. A recursion descending n levels holds n frames at once, so it costs O(n) space even when it allocates nothing.

open as a page

Tracing the call tree of a naive recursive Fibonacci, why does fib(6) take far more than 6 calls?

level: juniorimportance: must knowfreq 78%

basics

~10 s

Every call spawns two more calls, so the calls form a branching tree rather than a straight chain. fib(6) expands into 25 invocations, and most of them recompute values the other branch already computed.

open as a page

A chunking recursion consumes two characters per call and bases only on n == 0 — which inputs never terminate?

level: middleimportance: must knowfreq 60%

basics

~20 s

Every odd-length input. The remaining count steps by two, so from an odd start it runs 5, 3, 1, -1 and never equals zero. A stride of two needs two base cases: one at n == 0 and one at n == 1.

open as a page

Why is a function making two recursive calls on n-1 exponential rather than O(n^2)?

level: middleimportance: must knowfreq 70%

basics

~20 s

Two calls per invocation multiply the width of the tree at every level, so the node count doubles level by level and reaches about 2^n over n levels. Multiplying once per level is exponential; O(n^2) would need the work to merely add up.

open as a page

A recursive count of comments in a thread returns 0 for every thread — what base-case mistake explains it?

level: middleimportance: should knowfreq 48%

basics

~20 s

The base case returns 0 for a childless comment, so no call ever counts the node it was handed. A childless comment is one comment — the base must return 1, matching the contract the recursive branch relies on.

open as a page

What determines how deep recursion can go before the call stack overflows?

level: middleimportance: should knowfreq 58%

basics

~20 s

The ceiling is a byte budget, not a call count: a fixed-size stack region divided by the size of each frame. Fatter frames from more parameters or bigger locals overflow sooner, so identical recursion shapes fail at very different depths.

open as a page

In a game-move tree with 3 legal moves per position, what does searching one ply deeper cost?

level: middleimportance: should knowfreq 48%

basics

~20 s

One extra ply multiplies the whole search by about three: each position at the current frontier fans out into three more. Depth-limited search cost is dominated by the deepest level, so deepening is a multiplication, never an increment.

open as a page

Why can a digit-stripping recursion with an n == 0 base case never terminate on negative input?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Because the termination argument silently assumes integer division rounds toward zero. Under rounding toward negative infinity the value sticks at -1 and never equals 0. Negating the input first is not a safe fix: the most-negative fixed-width value has no positive counterpart.

open as a page

Your recursive log parser passed every test, then died at 3 a.m. on a million-record chain — why was that inevitable?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Recursion depth tracked input size, so thousand-record fixtures held a thousand frames while the production chain demanded a thousand times more than the stack region could hold. Stack exhaustion has no warning curve: it works, then it aborts.

open as a page

In a drawn game-search tree, how do you tell that different move orders re-explore the same position?

level: seniorimportance: should knowfreq 56%

basics

~20 s

Label every node with the arguments the invocation received — the position and the remaining depth — instead of with the move that led there. Repeated labels are repeated work: identical labels have identical subtrees beneath them.

open as a page

Recursion overflows on deeply skewed input: how do you choose between an explicit stack, reshaping the data, or capping depth?

level: principalimportance: should knowfreq 38%

basics

~20 s

Decide by what removes the failure class versus what the team can maintain. Reshaping the data bounds depth permanently but touches producers; an explicit stack keeps the algorithm and relocates linear state; caps buy time with a documented rejection.

open as a page