Recursion with parameters and return values

Two ways to carry an answer through recursion: pass it down, or build it on the way back up.

Pattern: Recursion25 min read
Quick recall3 questions
  1. 01What are the two parts of every recursive function?

    A base case that answers the smallest input at once, and a recursive case that calls the function on a smaller input.

  2. 02When does a line placed after the recursive call run?

    On the way back up, as the stack unwinds. The deepest call runs it first.

  3. 03How do you take the last digit off a number n?

    n % 10 gives the last digit. Integer division by 10 removes it: n / 10 in Java, n // 10 in Python, Math.floor(n / 10) in TypeScript.

The functions in the last lesson only printed. Most problems want an answer back: a sum, a count, true or false.

The question that stops learners: while ten calls wait on the stack, where does the answer live? There are two places to keep it, and once you see both, most early recursion problems look the same.

Two ways to carry an answer

Take the smallest example: the sum of the first n numbers. For n = 3, the answer is 1 + 2 + 3 = 6.

Pass it down: parameterised recursion

Give the function an extra parameter, total, that holds the sum so far. Each call adds its number and passes the new total to the next call.

int sumDown(int i, int total) {
    if (i == 0) return total; // nothing left to add
    return sumDown(i - 1, total + i);
}

// sumDown(3, 0) returns 6
  • total is the sum of the numbers already added.
  • The base case returns total, because by then it holds the whole answer.

Return it up: functional recursion

Here no call carries a total. Each call asks the smaller call for its sum, then adds its own number to the reply.

int sumUp(int n) {
    if (n == 0) return 0; // the sum of no numbers
    return n + sumUp(n - 1);
}

// sumUp(3) returns 6
  • sumUp(n - 1) is the sum of 1 to n − 1. Trust it.
  • n + is this call's own work, done after the smaller call returns.

Pass it down

The total grows on the way down. The base case already holds 6, and every call passes it back up unchanged.

Return it up

Nothing is added on the way down. Each call adds its own n to the reply on the way back.

Read each card top to bottom for the calls (each line is called by the line above it), then bottom to top for the returns. Same answer, same number of calls. The difference is when the adding happens:

Pass it downReturn it up
The answer lives ina parameterthe return value
Work happenson the way downon the way back up
The base case returnsthe finished answerthe answer for the smallest input
Good forprinting, collecting as you gobuilding an answer from smaller answers

Learn both, but practise the second one most. Trees, divide and conquer and dynamic programming all return answers up.

The pass-it-down version still uses one frame per call. Some languages reuse the frame for a , but Java, Python and Node do not. In our runs, sumDown(100000, 0) overflowed the stack in all three, the same as sumUp.

One shape, many problems

Most early recursion problems answer the same three questions from the last lesson. Only the answers change:

ProblemBase caseSmaller callCombine
Sum of first Nn = 0: return 0sum(n − 1)n + rest
Factorialn ≤ 1: return 1fact(n − 1)n × rest
Sum of arrayi = length: return 0sum(arr, i + 1)arr[i] + rest
Sum of digitsn < 10: return nsum(n without its last digit)n % 10 + rest

Factorial, for example, is the sum with two changes: the base case returns 1, and it multiplies.

long factorial(int n) {
    if (n <= 1) return 1; // 0! and 1! are both 1
    return n * factorial(n - 1);
}

// factorial(5) returns 120

Factorials grow fast, so watch the number type:

  • In Java, an int holds factorials only up to 12!. The code returns long, which holds up to 20!.
  • In Python, integers never overflow, so 30! comes out exact.
  • In TypeScript, a number stays exact up to 22!. Past that, use BigInt.

Reverse an array: two pointers, one call per swap

Some problems change the input instead of building a value. To reverse an array in place, swap the two ends, then reverse the part between them.

  • left marks the first item not yet swapped.
  • right marks the last item not yet swapped.
  • When left >= right, zero or one item is left in the middle, so there is nothing to swap.
void reverseArray(int[] arr, int left, int right) {
    if (left >= right) return; // the pointers met: done
    int temp = arr[left];
    arr[left] = arr[right];
    arr[right] = temp;
    reverseArray(arr, left + 1, right - 1);
}

// with nums = {1, 2, 3, 4, 5}:
// reverseArray(nums, 0, nums.length - 1) makes it {5, 4, 3, 2, 1}

The swap is one line in Python and TypeScript. Java has no such swap, so it uses a temp variable.

reverseArray, called with 0 and 4swaps 1
5
0
2
1
3
2
4
3
1
4
call stack{(0, 4)}

1/5Call (0, 4). left < right, so swap 1 and 5.

Each call swaps one pair and hands the middle to the next call.

The same two-pointer recursion answers this lesson's main problem. There it returns a value.

Read the problem

Problem: Check if String is Palindrome or Notn ≤ 1,000
it asks
Return true if the string reads the same from left to right and from right to left. Otherwise return false.
example 1
in s = "racecar" out true

Read backwards, it is still racecar.

example 2
in s = "abca" out false

The ends match, but b and c in the middle do not.

limits
  • 1 ≤ n ≤ 1,000
  • lowercase English letters
budget
About 10⁸ steps run in one second. With n up to 1,000, aim for O(n²) or faster.
Which pattern? Decide first, then open.

“reads the same from both ends”compare the first and last letters, then move inward

“the middle must also read the same”the same question on a smaller string: recursion

pattern: Recursion

Ask these before you code:

  • One letter, or two equal letters? Both are palindromes. That shapes the base case.
  • Even and odd lengths? Check both: "abba" and "racecar".
  • Capital letters, spaces, punctuation? Not here: lowercase letters only. Some versions ask you to ignore them, so ask.
  • How deep can it go? The recursion makes about n / 2 calls. For n = 1,000, that is 500 frames. That is safe in all three languages, even under Python's default limit of 1,000.

n ≤ 1,000 allows even O(n²). Every answer below is O(n), so the real question is the extra space.

Brute force: reverse and compare

The first idea: build the reversed string and compare it with the original.

boolean isPalindrome(String s) {
    return s.equals(new StringBuilder(s).reverse().toString());
}

Each language builds the reversed copy its own way: StringBuilder.reverse() in Java, the slice s[::-1] in Python, and an array that is reversed and joined in TypeScript.

Building the copy reads n letters. The comparison reads up to n more. Time is O(n), and the copy costs O(n) extra space.

It fits the limits. Its weakness: it always reads the whole string, even when the first two letters already differ.

The loop you know: two pointers

The usual loop, from string basics, puts one pointer at each end:

boolean isPalindromeLoop(String s) {
    int left = 0;
    int right = s.length() - 1;
    while (left < right) {
        if (s.charAt(left) != s.charAt(right)) return false;
        left++;
        right--;
    }
    return true;
}

O(n) time, O(1) extra space, and it stops at the first mismatch. Now write the same idea as recursion.

The key insight: a palindrome has a palindrome inside

Peel off the two end letters. If they match, the string is a palindrome exactly when the middle is one.

Each row is one call. The ends must match, and the middle becomes the next call.

That gives the whole function:

  • Base case 1: left >= right. Zero or one letter is left, so return true.
  • Base case 2: the end letters differ. Return false at once.
  • Recursive case: the ends match, so the answer is the answer for the middle.

Recursive solution

boolean isPalindrome(String s, int left, int right) {
    if (left >= right) return true; // 0 or 1 letters left
    if (s.charAt(left) != s.charAt(right)) return false; // the ends differ
    return isPalindrome(s, left + 1, right - 1); // the middle decides
}

// isPalindrome("racecar", 0, 6) returns true

This is functional recursion: the answer comes back up as a return value. Each call passes the reply from its inner call up unchanged.

isPalindrome on racecarcomparisons 1
r
0
a
1
c
2
e
3
c
4
a
5
r
6
call stack{(0, 6)}

1/5Call (0, 6). r and r match. Ask the middle: call (1, 5).

Three comparisons, four calls. The answer comes back up through every waiting call.

Cost. About n / 2 calls, each doing O(1) work, so O(n) time. The stack holds up to n / 2 + 1 frames, so O(n) extra space.

Edge cases. The empty string starts with right = -1, so the first call returns true. "a" returns true. "ab" returns false on the first comparison.

Depth. With n ≤ 1,000 the stack stays at 500 frames. Past the limits, each language gives out at a different length. In our runs, the longest palindrome that passed was:

  • Python: about 2,000 letters, because the default limit is 1,000 frames.
  • JavaScript (Node): about 15,000 letters. 20,000 overflowed.
  • Java: about 30,000 letters. 40,000 overflowed.

For long strings, use the loop.

Side by side

ApproachIdeaTimeExtra spaceFits n ≤ 1,000?
Reverse and comparebuild a reversed copyO(n)O(n)yes
Two pointers, loopcompare ends, move inO(n)O(1)yes
Two pointers, recursionends match, middle decidesO(n)O(n) stackyes, 500 deep

In an interview, the loop is the best answer to this problem. Learn the recursive one for its shape: "handle the ends, then ask about the middle" returns in trees and in divide and conquer, where a loop is much harder to write.

How to spot this pattern

SignalWhy it points here
The same question on a smaller partrecursion with a smaller input
Work from both ends toward the middletwo pointers: left + 1, right − 1
The answer is built from a smaller answerreturn it up
You print or collect as you gopass it down

The template for "return it up", on Sum of Array Elements II:

int sumFrom(int[] arr, int i) {
    if (i == arr.length) return 0; // 1. base case: no items left
    int rest = sumFrom(arr, i + 1); // 2. trust the smaller call
    return arr[i] + rest; // 3. combine this item with the rest
}

The same shape fits other problems on this lesson's list:

  • Check if the Array is Sorted II: if arr[i] > arr[i + 1], return false. Otherwise ask about the array from i + 1.
  • Sum of Digits: combine n % 10 with the sum of digits of n without its last digit (n / 10 in Java, n // 10 in Python, Math.floor(n / 10) in TypeScript). Stop when n has one digit.
  • Reverse a String I: the same two-pointer swap as the array, on an array of characters.
  • Check if a Number is Prime: try a divisor d from 2. If d * d > n, return true. If n % d == 0, return false. Otherwise try d + 1.
take U forwardYouTube
Re 3. Parameterised and Functional Recursion: Striver's A2Z DSA Course
Remember4 points
  1. 01

    A recursive answer lives in one of two places: pass it down in a parameter, or return it up and combine on the way back.

  2. 02

    Most early problems share one shape: base case, trust the smaller call, combine. Only the three answers change.

  3. 03

    Two pointers by recursion: handle the ends, call on left + 1 and right - 1, and stop when left >= right.

  4. 04

    Recursion is not free: the palindrome check is O(n) time like the loop, but O(n) stack space instead of O(1).

Read more

Next: a function that calls itself twice, and why Fibonacci gets slow so fast.

Practice

Solve these on the sheet after the lesson. Try each one for 20 minutes before you open its solution.

Striver's A2Z sheet0/10 solved
Open the sheet