DEV Community

Cover image for How to Know When a for Loop Belongs Inside Recursion
Mohd Abdul Naveed
Mohd Abdul Naveed

Posted on

How to Know When a for Loop Belongs Inside Recursion

Many developers struggle with knowing when a for loop belongs inside a recursive function. The confusion isn’t about syntax, it’s about understanding what recursion is responsible for, and what it isn’t.

Why this matters in real code

I ran into this issue while solving backtracking problems and building traversal logic.

My recursive functions:

  • compiled ✔️
  • ran ✔️
  • looked correct ✔️

…but the results were incomplete or duplicated.
The bug wasn’t the recursion itself.

It was a misunderstanding when multiple choices must be explored at the same level.

Diagram showing recursive calls stacking downward like floors in a building, illustrating how recursion moves deeper with each function call

A recursive function has two essential parts:

  1. Base case — when to stop
  2. Recursive call — how to move closer to that stop

Each recursive call:

  • creates a new stack frame
  • represents one level deeper in the problem
Call Stack (top)
┌─────────────┐
│ f(1)        │ ← last in
│ f(2)        │
│ f(3)        │
└─────────────┘
Call Stack (bottom)
Enter fullscreen mode Exit fullscreen mode

Stacks are LIFO — last in, first out.

Problems with no branching (no loops)

Story: going down the stairs

Illustration of a person going down floors using a single door at each level, representing recursion with only one possible path and no branching

Imagine a building with many floors.
Each recursive call moves you down one floor.
You stop at the ground floor — the base case.

int go_down(int floor) {
    if (floor == 0) return;
    go_down(floor - 1);
}
Enter fullscreen mode Exit fullscreen mode
Floor 3
  ↓
Floor 2
  ↓
Floor 1
  ↓
Floor 0  (base case)
Enter fullscreen mode Exit fullscreen mode

There is:

  • only one path
  • no choices

👉 No for loop belongs here

When loops become necessary

Now imagine each floor has multiple doors 🚪🚪🚪
Each door leads to the next floor.
That means:

  • Floors → recursion
  • Doors → loop

Visualizing the idea

Floor 3
 ├─ Door A → Floor 2
 ├─ Door B → Floor 2
 └─ Door C → Floor 2
Enter fullscreen mode Exit fullscreen mode

Process:

  1. Choose a door (loop)
  2. Go down one floor (recursion)
  3. Repeat

Code example: visiting rooms

void visit_rooms(int floor) {
    if (floor == 0) {
        cout << "Ground floor reached" << endl;
        return;
    }

    char doors[] = {'A', 'B', 'C'};
    for (char door : doors) {
        cout << "Using door " << door << " on floor " << floor << endl;
        visit_rooms(floor - 1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why the loop exists

At the same level, you must try:

  • Door A
  • Door B
  • Door C Same level → loop Next level → recursion

When a loop does NOT belong

If there is only one action, looping is incorrect.

Countdown example

void countdown(int n) {
    if (n == 0) return;
    cout << n << endl;
    countdown(n - 1);
}
Enter fullscreen mode Exit fullscreen mode

OutPut:

3 → 2 → 1 → 0
Enter fullscreen mode Exit fullscreen mode

There is:

  • No choice
  • No doors
  • No flavors

Golden rule (remember this forever)

Recursion goes down
Loops go across

If these roles blur, your mental model breaks down.

A quick self-check

Ask:

“At this step, do I need to try multiple options?”

  • ❌ No → no loop
  • ✅ Yes → loop required

If you can’t name the options, the loop doesn’t belong.

A very common mistake

You:

  • have a base case
  • call the function recursively
  • get output

…but it’s missing results.

This happens when recursion is expected to handle branching.

It won’t.

Example: binary strings (classic pitfall)

Goal: generate all binary strings of length n

Expected output for n = 2:

00
01
10
11
Enter fullscreen mode Exit fullscreen mode

❌ Incorrect approach

void generate(int n, string s) {
    if (n == 0) {
        cout << s << endl;
        return;
    }

    generate(n - 1, s + "0");
}
Enter fullscreen mode Exit fullscreen mode

What actually happens

generate(2, "")
  → generate(1, "0")
    → generate(0, "00")
Enter fullscreen mode Exit fullscreen mode

Only one path is explored.

What was missing?

At each level, there are two choices:

"" 
 ├─ "0"
 └─ "1"
Enter fullscreen mode Exit fullscreen mode

👉 Recursion went down, but never sideways.

Think in terms of STATE (this is the mental unlock)

A recursive function always represents a state.
A state answers:

  • “Where am I right now?”
  • “What has already been decided?”

Examples of state:

  • current index
  • current depth
  • remaining steps
  • current path
  • current floor

Example

generate(2, "")
Enter fullscreen mode Exit fullscreen mode

State:

  • depth = 2
  • string = ""

Next level (same depth, different choices):

generate(1, "0");
generate(1, "1");
Enter fullscreen mode Exit fullscreen mode

Diagram:

Level 0: ""
 ├─ Level 1: "0"
 │   ├─ "00"
 │   └─ "01"
 └─ Level 1: "1"
     ├─ "10"
     └─ "11"
Enter fullscreen mode Exit fullscreen mode

The rule (clearly stated)

Recursion moves between states
Loops explore multiple actions from the same state

Skip the loop → lose possibilities.

Classic example: permutations

Input

[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Output

[1,2,3]
[1,3,2]
[2,1,3]
[2,3,1]
[3,1,2]
[3,2,1]
Enter fullscreen mode Exit fullscreen mode

Step-by-step thinking

You build the permutation one position at a time.
At each step:

  • choose one unused number
  • try every possible choice

State definition

  • path → current permutation
  • used[] → which numbers are taken
void permute(vector<int>& nums, vector<int>& path, vector<bool>& used) {
    if (path.size() == nums.size()) {
        print(path);
        return;
    }

    for (int i = 0; i < nums.size(); i++) {
        if (used[i]) continue;

        used[i] = true;
        path.push_back(nums[i]);

        permute(nums, path, used);

        path.pop_back();
        used[i] = false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Diagram

[]
 ├─ [1]
 │   ├─ [1,2]
 │   │   └─ [1,2,3]
 │   └─ [1,3]
 │       └─ [1,3,2]
 ├─ [2]
 │   ├─ [2,1]
 │   └─ [2,3]
 └─ [3]
     ├─ [3,1]
     └─ [3,2]
Enter fullscreen mode Exit fullscreen mode

Why recursion alone isn’t enough
This line:

permute(nums, path, used)
Enter fullscreen mode Exit fullscreen mode

does not automatically try other numbers.
Recursion:

  • moves deeper Loops:
  • create branches They solve different problems.

The real golden rule

Recursion answers: “What is the next state?”
Loops answer: “What are my options right now?”

If recursion is expected to replace branching, the algorithm will silently miss valid results.

Where you’ll see this pattern again

  • DFS / BFS
  • Backtracking problems
  • Tree traversals
  • Graph search
  • Combinatorics (subsets, permutations, combinations)

Once you separate state transitions from choice exploration, recursion problems become easier to reason about and debug.

Top comments (0)