Recursion
Algorithm Design
Programming Challenges
Computational Limitations
Coding Techniques

Trouble designing recursion with limited results

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Designing recursion that limits the number of results — such as "find the first N solutions" or "generate combinations up to a maximum count" — requires a different approach than standard recursion. Without a limiting mechanism, a recursive function explores the entire search space. To limit results, you can pass a counter through recursive calls, use a shared mutable collection with an early termination check, yield results with a generator, or use backtracking with pruning. The key challenge is propagating the "stop" signal through the recursion stack efficiently.

The Problem: Unlimited Recursion

python
1# Standard recursion: finds ALL subsets (2^n results)
2def all_subsets(nums, index=0, current=None):
3    if current is None:
4        current = []
5    if index == len(nums):
6        return [current[:]]  # Copy of current subset
7    # Include nums[index]
8    current.append(nums[index])
9    with_item = all_subsets(nums, index + 1, current)
10    current.pop()
11    # Exclude nums[index]
12    without_item = all_subsets(nums, index + 1, current)
13    return with_item + without_item
14
15result = all_subsets([1, 2, 3, 4, 5])
16print(len(result))  # 32 — all 2^5 subsets

For large inputs, this generates millions of results. You only need the first N.

Solution 1: Pass a Counter and Check Early

python
1def limited_subsets(nums, limit, index=0, current=None, results=None):
2    if current is None:
3        current = []
4    if results is None:
5        results = []
6
7    if len(results) >= limit:
8        return results  # Stop collecting
9
10    if index == len(nums):
11        results.append(current[:])
12        return results
13
14    # Include
15    current.append(nums[index])
16    limited_subsets(nums, limit, index + 1, current, results)
17    current.pop()
18
19    # Exclude (only if we haven't hit the limit)
20    if len(results) < limit:
21        limited_subsets(nums, limit, index + 1, current, results)
22
23    return results
24
25result = limited_subsets([1, 2, 3, 4, 5], limit=5)
26print(result)  # First 5 subsets only
27print(len(result))  # 5

The check if len(results) < limit prunes branches early, avoiding unnecessary computation.

Solution 2: Generator with yield (Most Pythonic)

python
1def subset_generator(nums, index=0, current=None):
2    if current is None:
3        current = []
4
5    if index == len(nums):
6        yield current[:]
7        return
8
9    # Include
10    current.append(nums[index])
11    yield from subset_generator(nums, index + 1, current)
12    current.pop()
13
14    # Exclude
15    yield from subset_generator(nums, index + 1, current)
16
17
18# Take only the first 5 results
19from itertools import islice
20
21results = list(islice(subset_generator([1, 2, 3, 4, 5, 6, 7, 8]), 5))
22print(results)  # First 5 subsets
23# The generator stops after 5 yields — remaining branches are never explored

Generators naturally support lazy evaluation. Combined with itertools.islice, the recursion only runs as deep as needed to produce the requested results.

Solution 3: Exception-Based Early Termination

python
1class LimitReached(Exception):
2    pass
3
4def find_paths(graph, start, end, limit, path=None, results=None):
5    if path is None:
6        path = [start]
7    if results is None:
8        results = []
9
10    if len(results) >= limit:
11        raise LimitReached()
12
13    if start == end:
14        results.append(path[:])
15        return
16
17    for neighbor in graph.get(start, []):
18        if neighbor not in path:  # Avoid cycles
19            path.append(neighbor)
20            try:
21                find_paths(graph, neighbor, end, limit, path, results)
22            except LimitReached:
23                path.pop()
24                raise  # Propagate up the stack
25            path.pop()
26
27# Usage
28graph = {'A': ['B', 'C'], 'B': ['C', 'D'], 'C': ['D'], 'D': []}
29results = []
30try:
31    find_paths(graph, 'A', 'D', limit=2, results=results)
32except LimitReached:
33    pass
34
35print(results)  # First 2 paths from A to D

Solution 4: Return Boolean for Stop Signal

python
1def find_combinations(nums, target, limit, index=0, current=None, results=None):
2    if current is None:
3        current = []
4    if results is None:
5        results = []
6
7    if len(results) >= limit:
8        return True  # Signal: stop searching
9
10    current_sum = sum(current)
11    if current_sum == target:
12        results.append(current[:])
13        return len(results) >= limit
14
15    if current_sum > target or index >= len(nums):
16        return False
17
18    for i in range(index, len(nums)):
19        current.append(nums[i])
20        if find_combinations(nums, target, limit, i + 1, current, results):
21            current.pop()
22            return True  # Propagate stop signal
23        current.pop()
24
25    return False
26
27results = []
28find_combinations([1, 2, 3, 4, 5, 6], target=7, limit=3, results=results)
29print(results)  # First 3 combinations that sum to 7

Solution 5: Backtracking with Pruning (Java/C++)

java
1// Java example with early termination
2public class LimitedPermutations {
3    private List<List<Integer>> results = new ArrayList<>();
4    private int limit;
5
6    public List<List<Integer>> permute(int[] nums, int maxResults) {
7        this.limit = maxResults;
8        backtrack(nums, new ArrayList<>(), new boolean[nums.length]);
9        return results;
10    }
11
12    private boolean backtrack(int[] nums, List<Integer> current, boolean[] used) {
13        if (results.size() >= limit) return true;  // Stop signal
14
15        if (current.size() == nums.length) {
16            results.add(new ArrayList<>(current));
17            return results.size() >= limit;
18        }
19
20        for (int i = 0; i < nums.length; i++) {
21            if (used[i]) continue;
22            used[i] = true;
23            current.add(nums[i]);
24            if (backtrack(nums, current, used)) return true;
25            current.remove(current.size() - 1);
26            used[i] = false;
27        }
28        return false;
29    }
30}

Common Pitfalls

  • Not checking the limit before recursive calls: Checking the limit only at the base case allows unnecessary recursive calls to continue even after enough results are collected. Check the limit before each recursive branch to prune early.
  • Mutating shared state without copying: results.append(current) stores a reference to the same list that gets modified during backtracking. Always append a copy: results.append(current[:]) in Python or new ArrayList<>(current) in Java.
  • Generator not stopping early: Using yield from without islice or a similar limiter means the generator is ready to produce all results. The caller must stop iteration. If the caller collects into a list with list(generator), all results are generated.
  • Returning the wrong signal value: When using a boolean return value to signal "stop", returning True from all branches (not just the limit-reached branch) causes the recursion to stop prematurely. Only return True when len(results) >= limit.
  • Stack overflow on deep recursion: Limiting results does not limit recursion depth. A search space with millions of nodes may still recurse deeply before finding N results. Set sys.setrecursionlimit() in Python or convert to an iterative approach with an explicit stack for very deep search spaces.

Summary

  • Pass a shared results list and check len(results) >= limit before each recursive branch
  • Use Python generators with itertools.islice for lazy, memory-efficient result limiting
  • Return a boolean from recursive calls to propagate a "stop" signal up the call stack
  • Always copy mutable state (current[:]) before adding to results during backtracking
  • For very large search spaces, use iterative approaches with explicit stacks to avoid stack overflow

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.