Data Structures and Algorithms

Recursion, Activation Records and Backtracking Foundations

PGCP-AC

Recursion is a method of solving a problem by reducing it to one or more smaller instances of the same problem. A recursive function participates in its own call chain, either by calling itself directly or by calling other functions that eventually call it again. Recursion is especially natural when the data or problem has a nested structure, as in trees, divide-and-conquer algorithms and searches through a space of choices.

A recursive definition has two essential parts. A base case gives a direct answer for an instance that needs no further recursion. A recursive case transforms the current instance into smaller instances and combines their answers. Correct code also needs measurable progress: every recursive path must move toward a base case. A base case that exists syntactically is insufficient if some inputs can never reach it.

Understanding a Recursive Definition

Factorial illustrates the pattern. For a nonnegative integer n:

0! = 1
n! = n × (n - 1)!  for n > 0

The first equation is the base case. The second reduces n by one. A direct implementation returns 1 when n == 0; otherwise it returns n * factorial(n - 1). Evaluating factorial(4) creates the pending multiplication 4 * factorial(3), then 3 * factorial(2) and so on. When factorial(0) returns 1, the calls finish in reverse order, yielding 1, 2, 6 and finally 24.

The input contract matters. If the function accepts negative integers but only tests n == 0, calls move to increasingly negative values and never reach the base case. A robust design rejects values outside the mathematical domain or defines explicit behavior for them.

To reason about termination, identify a variant: a nonnegative measure that decreases on every recursive step. In factorial, the variant is n. In binary search, it is the length of the remaining interval. In tree traversal, it may be the number of nodes in the current subtree. Well-founded decrease prevents an infinite descending call sequence.

Direct, Indirect and Multiple Recursion

Direct recursion occurs when function f calls f. Indirect recursion occurs through a cycle of functions, such as f calling g and g later calling f. Indirect recursion needs the same termination reasoning, although progress can be harder to see because it is distributed across several functions.

A function is linear recursive if each invocation makes at most one recursive call. Factorial and a simple linked-list traversal are examples. Binary recursion makes two recursive calls, as a tree traversal does for left and right subtrees. More generally, multiple recursion creates several subproblems per call. The number of calls, rather than the syntax alone, determines running time.

Structural recursion follows the shape of recursive data. A binary tree is either empty or consists of a root and two smaller trees, so processing the root and recursively processing both subtrees mirrors the definition. Divide-and-conquer recursion splits an input, solves independent pieces and combines them. Backtracking recursion explores alternative choices and abandons branches that cannot succeed.

Activation Records and Local State

Every active function call needs its own execution state. The runtime stores this state in an activation record or stack frame. A frame may contain parameter values, local variables, temporary results, a return address and saved machine state. When a call begins, a frame is created logically on the call stack. When the call returns, its frame is removed and execution resumes in its caller.

Recursive calls therefore have separate local variables even though they execute the same function body. In factorial(4), one frame holds n = 4, another holds n = 3 and so forth. Modifying a nonstatic local in one invocation does not change the corresponding local in another invocation.

Objects referenced by those locals may still be shared. If every frame receives a reference to the same mutable list, a change through one reference is visible through the others. The frame separates reference variables; it does not automatically copy the objects they designate. Static fields and global variables are also shared and can make recursive logic harder to reason about or unsafe across concurrent invocations.

The call stack stores only currently active calls. If a recursion tree contains many calls overall, frames from completed branches have already been removed before later branches run. Maximum auxiliary call-stack space is determined by the longest active root-to-leaf chain, not the total number of calls.

Time and Space Analysis

A recursive algorithm is often described with a recurrence. Straightforward factorial performs constant work and makes one call on n - 1:

T(n) = T(n - 1) + O(1)

Expanding this recurrence gives O(n) time. Its maximum active depth is n + 1 including the base call, so call-stack space is O(n).

The naive Fibonacci definition calls both fib(n - 1) and fib(n - 2). The recursion tree repeats the same arguments in many branches. Its running time grows exponentially, while maximum depth is only O(n). This distinction shows why total call count and active stack depth must be analyzed separately.

Memoization stores results for arguments already solved. Once fib(k) has been computed, later calls return the cached value instead of expanding another subtree. This changes Fibonacci computation to O(n) time with O(n) cached values. A bottom-up dynamic program can compute the same sequence iteratively and may reduce extra space to O(1) when only the previous two values are needed.

Numeric limits are independent of recursion. Factorial values overflow fixed-width integer types quickly, even when recursion depth is safe. A correct algorithm still needs a numeric representation large enough for its results or a defined overflow policy.

Tail Recursion and Iteration

A recursive call is in tail position when its returned result becomes the caller’s result without further computation. A tail-recursive factorial carries an accumulator:

factorialTail(n, acc)
    if n == 0, return acc
    return factorialTail(n - 1, n * acc)

Some language implementations reuse one frame for tail calls. Java does not guarantee tail-call optimization, so rewriting a function into tail-recursive form does not guarantee constant stack space on the JVM. An explicit loop is the reliable way to avoid linear call depth there.

Many linear recursive algorithms translate directly into loops. More complex recursive procedures can use an explicit stack whose records hold the pending work that call frames would otherwise remember. An iterative depth-first traversal, for example, pushes nodes still to be visited. This makes memory management visible to the program and may allow different capacity policies, but it does not remove the need to store pending state.

Recursion can express hierarchical algorithms clearly, but each call has overhead and available call-stack space is finite. Iteration is often preferable for very deep linear structures. Recursion remains valuable where the structure of calls closely matches the structure of the problem and realistic depth is controlled.

Backtracking as Systematic Search

Backtracking constructs a candidate solution one decision at a time. At each stage it selects a choice, updates the partial state and recursively explores what follows. If the partial state violates a constraint or cannot lead to a complete answer, that branch is abandoned. Before trying the next choice, any mutable state changed for the abandoned branch is restored.

A general outline is:

search(state)
    if state is a complete solution
        record or return it
    for each allowed choice
        apply choice
        if state remains promising
            search(state)
        undo choice

The apply–explore–undo discipline is central. Suppose a search places a value in a board cell and marks its row as occupied. Both changes must be reversed before the next candidate is tried. If state is copied for every child instead, explicit undo may be unnecessary, but copying can increase time and memory use.

The search space is modeled as a tree. Each node is a partial solution and each outgoing edge is a possible next choice. Leaves represent complete solutions or dead ends. A basic search may visit an exponential number of nodes. Pruning avoids a whole subtree as soon as a constraint proves that no descendant can work.

N-Queens Example

In the N-Queens problem, n queens must be placed on an n × n board so that no two share a row, column or diagonal. A row-by-row backtracking algorithm places one queen in each row. For the current row, it tries each column that is not already attacked, records the placement, recurses to the next row and then removes the queen.

Three sets or Boolean arrays can test attacks efficiently: occupied columns, diagonals identified by row - column and anti-diagonals identified by row + column. When the current row equals n, every queen has been placed and the state is a solution. If no column is legal, the current branch ends.

The order of choices affects when a solution is found but does not affect correctness if every valid choice is eventually considered. The algorithm can stop after the first solution or continue and enumerate all solutions. Those are different contracts and should be stated explicitly.

Other classic backtracking problems include generating permutations, solving Sudoku, coloring graphs, finding maze paths and selecting subsets under constraints. The same framework applies, but effective pruning and compact state representation often determine practical performance.

Correctness of Recursive and Backtracking Algorithms

Recursive correctness can be established by induction. Prove that each base case returns the correct answer. Then assume recursive calls are correct for smaller instances and show that the current function combines them into the correct answer for its instance. Termination is a separate obligation proved by showing progress toward the base case.

Backtracking additionally needs two properties. Soundness means every reported solution satisfies all constraints. Completeness means every valid solution is reachable through some sequence of considered choices and is not removed by incorrect pruning. State restoration ensures one branch cannot contaminate another.

Testing should include base cases, the first recursive case, invalid-domain inputs and cases that create maximum expected depth. For backtracking, test an instance with no solution, one solution and several solutions. Validate every produced solution independently and compare small-instance solution counts with known results. These checks reveal missing choices, unsafe pruning and incomplete undo logic.

Recursion is most useful when it exposes a smaller copy of the same problem. Its safety depends on a reachable base case, measurable progress, bounded depth and correct handling of local and shared state. Backtracking builds on these ideas to explore choices systematically while pruning impossible branches and restoring state between alternatives.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.