Skip to content
DSA in Rust · Lesson 16

Recursion and backtracking in Rust

Functions that call themselves, and a search that retraces its steps.

Contents
Part 1 of 7 · What it is

Lost in a maze

Try a path, hit a wall, step back.

You are in a maze. At the first fork, you take the left path. It leads to a dead end. You do not give up and you do not start over. You walk back to the fork, and take the right path instead. At the next fork, you do the same thing: pick a road, and if it fails, return and pick another.

That habit is backtracking: try a choice, see where it leads, and if it goes wrong, undo it and try the next choice. It is how you solve a puzzle that has too many possibilities to see at once, such as every way to seat guests, or every set of items that fit in a bag.

Backtracking is built on recursion, which is a function that solves a big problem by calling itself on a smaller one. Think of a set of nesting dolls. To find the smallest doll, open the doll, and ask the same question of the doll inside.

Reach for recursion when a problem is made of smaller copies of itself: a folder with folders inside, a tree (lesson 7), or "choose, then solve the rest". Reach for backtracking when you must list or count all the ways to do something.

Part 2 of 7 · The idea

Every call gets its own page

A stack of unfinished jobs.

The take-or-skip tree for the items 1, 2 and 3Every fork is a choice: skip the item (left) or take it (right). Each of the eight leaves is one finished subset. The outlined road takes 1, skips 2 and takes 3. After each leaf, the search steps back up to the nearest fork that still has a road untried, which is backtracking.ITEM 1ITEM 2ITEM 3skiptake{ }{3}{2}{2,3}{1}{1,3}{1,2}{1,2,3}
Every fork is a choice: skip the item (left) or take it (right). Each of the eight leaves is one finished subset. The outlined road takes 1, skips 2 and takes 3. After each leaf, the search steps back up to the nearest fork that still has a road untried, which is backtracking.

Start with plain recursion, and a small example. The factorial of 4, written 4!, is 4 × 3 × 2 × 1. Notice that 4! is just 4 × 3!, and 3! is 3 × 2!. Each answer asks for a smaller copy of the same question.

Here is what the computer does. It calls factorial(4), which needs factorial(3) before it can finish. So it pauses the first job and starts the second. That one needs factorial(2), so it pauses and starts a third. Then factorial(1), which is simple: the answer is 1, with no more asking. Now the pile unwinds. factorial(2) gets 1 and finishes with 2. factorial(3) finishes with 6. factorial(4) finishes with 24.

The pile of paused jobs is the call stack. It is a stack in exactly the sense of lesson 4: the newest job is on top, and only the top job runs. Each paused job keeps its own copy of its variables, like its own page of rough work.

Every recursive function needs two things.

  • A base case: a question so simple that it is answered directly, with no more calls. factorial(1) was one. Without it, the calls never stop.
  • A smaller step: each call must move closer to the base case. Here the number drops by one each time.

Remember: a recursive function needs a base case that stops, and a step that gets closer to it.

Now backtracking. Take the items 1, 2 and 3, and list every way to choose some of them. The picture is the answer. At each item, there is a fork with two roads: skip it, or take it. Three items means three forks, so 2 × 2 × 2 = 8 finished choices. These are the subsets: nothing, just 1, just 2, and so on up to all three.

The search walks the tree from the top. It goes left everywhere first and reaches the empty subset. Then it steps back up one fork, takes the other road, and reaches {3}. The key is the step back. When you take an item, you add it to the list you are building. Before you try the next road, you must undo that, or the leftover item would sneak into later answers.

The pattern has three beats, and you will see it in every backtracking problem: choose, explore, un-choose.

Part 3 of 7 · Build it

Choose, explore, undo

The same shape, every time.

Start with plain recursion. These two are the smallest examples.

RustRun in Rust Playground (opens in a new tab)
/// 5! is 5 * 4 * 3 * 2 * 1. A function that solves its job by asking a
/// smaller copy of itself.
pub fn factorial(n: u64) -> u64 {
    // Base case: the answer is known, so nothing more is asked.
    if n <= 1 {
        return 1;
    }
    n * factorial(n - 1)
}

The line if n <= 1 is the base case. After it, n * factorial(n - 1) is the smaller step. The answer to factorial(20) is the biggest that fits in a u64, so the demo stays small.

RustRun in Rust Playground (opens in a new tab)
/// Adds up a slice by taking off the first item and asking for the rest.
pub fn sum_list(nums: &[i32]) -> i32 {
    match nums.split_first() {
        None => 0, // an empty list adds up to nothing
        Some((first, rest)) => first + sum_list(rest),
    }
}

This one adds a slice. split_first takes the first item off and hands back the rest, or None for an empty slice. The base case is the empty list, which sums to 0. Otherwise, the answer is the first item plus the sum of the rest.

Now the subsets, using the choose, explore, un-choose shape.

RustRun in Rust Playground (opens in a new tab)
/// Every way to choose some of the items (including none, and all).
pub fn subsets(items: &[i32]) -> Vec<Vec<i32>> {
    let mut all = Vec::new();
    let mut current = Vec::new();
    explore(items, 0, &mut current, &mut all);
    all
}

/// At each item, there are two roads: skip it, or take it.
fn explore(items: &[i32], index: usize, current: &mut Vec<i32>, all: &mut Vec<Vec<i32>>) {
    if index == items.len() {
        // Every item has been decided. Save a copy, because `current` changes.
        all.push(current.clone());
        return;
    }

    explore(items, index + 1, current, all); // road 1: skip it

    current.push(items[index]); // road 2: take it
    explore(items, index + 1, current, all);
    current.pop(); // step back, so the caller finds `current` as it left it
}

Go through it in order.

  • subsets sets up three things: the all list of answers, the current subset being built, and a call to explore.
  • explore is the recursion. index says which item is being decided. When index reaches the end, every item has been decided, so that is a finished subset. It pushes a copy with clone(), because current will keep changing as the search goes on.
  • Otherwise it takes the first road, skipping the item, and recurses.
  • Then the second road. It pushes the item (choose), recurses (explore), and pops it off again (un-choose). That pop is the step back from the picture.

Both current and all are passed as &mut, a borrow the function may change. That way every call shares the same two lists, instead of copying them at each step.

Remember: whatever you add before the recursive call, you remove after it.

Now permutations, the ways to put items in a row. Three items give 3 × 2 × 1 = 6 orders. The shape is the same, with a loop for the roads.

RustRun in Rust Playground (opens in a new tab)
/// Every way to put the items in a row, each item used once.
pub fn permutations(items: &[i32]) -> Vec<Vec<i32>> {
    let mut all = Vec::new();
    let mut current = Vec::new();
    let mut used = vec![false; items.len()];
    arrange(items, &mut used, &mut current, &mut all);
    all
}

fn arrange(items: &[i32], used: &mut [bool], current: &mut Vec<i32>, all: &mut Vec<Vec<i32>>) {
    if current.len() == items.len() {
        all.push(current.clone());
        return;
    }
    for i in 0..items.len() {
        if used[i] {
            continue; // already placed earlier in this row
        }
        used[i] = true;
        current.push(items[i]);
        arrange(items, used, current, all);
        // Undo both changes before trying the next item in this spot.
        current.pop();
        used[i] = false;
    }
}

At each place in the row, the loop tries every item that has not been used yet. used[i] remembers which are already placed. After trying an item, both changes are undone together: pop takes it off the row, and used[i] = false makes it available again.

Part 4 of 7 · Standard library

Not much in the library

Rust has no backtracking helper.

The standard library has no function for subsets or permutations. (The itertools crate does, if you ever want one in a project.) What it gives you are the small pieces used above.

  • split_first and split_at cut a slice without copying it, which is what makes the recursion on a slice cheap.
  • clone() copies the list being built, so a saved answer does not change later.
  • Vec::push and Vec::pop are the choose and un-choose, and they are both O(1).
  • Closures and nested fn items keep a helper close to the function that uses it. The solutions to the practice problems tuck their helper inside.

If a recursion is slow because it solves the same smaller problem again and again, that is the sign for lesson 19, where you remember the answers instead.

Part 5 of 7 · Cost

What the search costs

n is how many items there are.

ProblemTimeSpaceWhy
FactorialO(n)O(n)n calls, each waiting on the stack
Sum of a listO(n)O(n)One call per item
All subsetsO(2ⁿ × n)O(n) deep, plus the answersTwo roads at each of n forks, and copying each result
All permutationsO(n! × n)O(n) deep, plus the answersn choices, then n − 1, and so on
Counting subsets that hit a sumO(2ⁿ)O(n)The same tree, with nothing saved
Fast powerO(log n)O(log n)The exponent halves each call

The honest news: the number of answers explodes. Twenty items have more than a million subsets. Backtracking is fine for small inputs, and very slow for large ones. Pruning helps, which means giving up on a road early when you can already tell it cannot work. You will do that in the practice problems.

Part 6 of 7 · Practise

Three problems, one shape

Halve it, count it, place it.

Each starter has a todo!(), a placeholder that panics if it runs. Replace it with your code.

Problem 1

Power in a few calls

Write fast_pow, which takes a base and an exponent exp and returns base raised to the power exp, using recursion that halves the exponent. fast_pow(3, 13) gives 1594323. Assume the answer fits in a u64.

Start here

RustRun in Rust Playground (opens in a new tab)
pub fn fast_pow(base: u64, exp: u32) -> u64 {
    todo!("what if you halve the exponent?")
}

Stuck?

Show a solution
RustRun in Rust Playground (opens in a new tab)
/// `base` to the power `exp`, in about log(exp) calls instead of exp calls.
pub fn fast_pow(base: u64, exp: u32) -> u64 {
    if exp == 0 {
        return 1;
    }
    let half = fast_pow(base, exp / 2);
    if exp % 2 == 0 {
        half * half
    } else {
        half * half * base // an odd exponent has one left over
    }
}

Halving the exponent turns n calls into about log n calls. The important detail is that fast_pow(base, exp / 2) is called once and its answer stored in half, then used twice. If you wrote the call twice, you would solve the same sub-problem two times, and all the savings would be lost. For exp = 13, the exponents go 13, 6, 3, 1, 0, only five calls. It takes O(log exp) time and O(log exp) stack space.

Problem 2

Count the subsets that add up

Write count_subset_sums, which takes a slice of non-negative whole numbers and a target, and returns how many subsets add up to exactly the target. The empty subset counts as adding up to 0. [2, 3, 5, 6, 8] with target 11 gives 3.

Start here

RustRun in Rust Playground (opens in a new tab)
pub fn count_subset_sums(nums: &[u32], target: u32) -> usize {
    todo!("take it or skip it, and stop early when you go over")
}

Stuck?

Show a solution
RustRun in Rust Playground (opens in a new tab)
/// How many subsets of `nums` add up to exactly `target`.
pub fn count_subset_sums(nums: &[u32], target: u32) -> usize {
    // `left` is how much is still needed from the items from `index` on.
    fn count(nums: &[u32], index: usize, left: u32) -> usize {
        if index == nums.len() {
            return usize::from(left == 0); // 1 if we hit the target exactly
        }
        let skip = count(nums, index + 1, left);
        // Prune: an item bigger than what is left can never be taken.
        let take = if nums[index] <= left {
            count(nums, index + 1, left - nums[index])
        } else {
            0
        };
        skip + take
    }
    count(nums, 0, target)
}

Notice that no list is built at all. A count is enough, so each call returns a number and the two roads are added together. That removes the choose and un-choose steps, because there is no shared list to repair. The prune nums[index] <= left is safe because the numbers are not negative, so a too-big item can never be made right later. It saves work but not the worst case, which is still O(2ⁿ) time. The recursion is n levels deep, so the space is O(n). Equal numbers count as different items, which is why [4, 4, 4] with target 8 gives 3.

Problem 3

Queens that do not clash

Write n_queens, which takes n and returns how many ways there are to place n queens on an n by n board so that no two attack each other. n_queens(6) gives 4, and n_queens(8) gives 92.

Start here

RustRun in Rust Playground (opens in a new tab)
pub fn n_queens(n: usize) -> usize {
    todo!("one queen per row; which columns are still safe?")
}

Stuck?

Show a solution
RustRun in Rust Playground (opens in a new tab)
/// How many ways to put `n` queens on an `n` by `n` board so that none
/// can capture another.
pub fn n_queens(n: usize) -> usize {
    // Which columns and which two kinds of diagonal already hold a queen.
    let mut cols = vec![false; n];
    let mut slash = vec![false; 2 * n];
    let mut backslash = vec![false; 2 * n];

    fn place(
        row: usize,
        n: usize,
        cols: &mut [bool],
        slash: &mut [bool],
        backslash: &mut [bool],
    ) -> usize {
        if row == n {
            return 1; // every row has a queen
        }
        let mut ways = 0;
        for col in 0..n {
            // Squares on one diagonal share `row + col`, and on the other
            // they share `row - col`, shifted up so it is never negative.
            let (a, b) = (row + col, row + n - 1 - col);
            if cols[col] || slash[a] || backslash[b] {
                continue; // this square is attacked
            }
            cols[col] = true;
            slash[a] = true;
            backslash[b] = true;
            ways += place(row + 1, n, cols, slash, backslash);
            cols[col] = false; // backtrack
            slash[a] = false;
            backslash[b] = false;
        }
        ways
    }
    place(0, n, &mut cols, &mut slash, &mut backslash)
}

This is backtracking with strong pruning. A square that is already attacked is skipped at once, so whole branches of the tree are never visited. Placing one queen per row means there are at most n choices per level and n levels, but pruning cuts that down enormously. The three boolean lists answer 'is this square attacked?' in O(1). Setting them back to false after the recursive call is the un-choose step: forget it and later branches see queens that are no longer there. Even so, the work grows quickly with n, so it is meant for small boards. A board of 8 has 92 solutions.

Part 7 of 7 · Interview notes

Backtracking in the interview

What to ask, and what to say.

Recursion questions test whether you can trust a function to solve a smaller copy of itself. Backtracking questions add the habit of undoing.

  • Spot the signals. "All ways", "every combination", "all arrangements", "generate", "place without conflict", and "does any path exist" point to backtracking. A tree or a nested structure points to plain recursion.
  • Name the base case first. Say what the simplest input is and what its answer is. Then say how each call gets closer to it.
  • Draw the tree. Sketch two levels of choices for a tiny input, like the picture in this lesson. It shows the interviewer your plan, and it exposes the cost.
  • Use the three beats. Say "choose, explore, un-choose" out loud, and make sure every choice has a matching undo.
  • Say what you copy. A saved answer must be a copy, as in current.clone(). Pushing the list you are still changing is the classic bug.
  • Prune early. Name any condition that lets you give up on a road: a sum gone over, a square attacked, a letter already used. Pruning turns a hopeless search into a workable one.
  • State the cost honestly. Subsets are O(2ⁿ) and permutations are O(n!). Say that this is fine for small n, and that a repeating sub-problem is the cue to remember answers, as in dynamic programming.
  • Mention stack depth. Recursion depth equals stack space. If the depth could be huge, offer an explicit stack with a Vec instead.

Updated 2026-10-17

Made by VeraStack Labs