Interview Kickstart has enabled over 21000 engineers to uplevel.
For software engineers, recursive thinking is a key factor in solving complex programming problems. It helps in breaking down larger problems into smaller subproblems, making them easier to visualize. Most FAANG and tier-1 tech companies ask candidates to come up with a recursive approach during technical interviews; the aim is to test their ability to solve problems. In this article, we focus on tail recursion:
Recursion is a process in which a function calls itself, either directly or indirectly. The function involved is called a recursive function. The condition that terminates the further call of the function by defining the termination state is called the base condition.
Recursion is a very broad field and has many branches like:
This article, as you know, covers tail recursion. So let’s get right into it.
In tail recursion, there is no other operation to perform after executing the recursive function itself; the function can directly return the result of the recursive call.
In simple words, in tail recursion, the recursive function is called last. So it is more efficient than non-tail recursion. Also, the compiler can easily optimize the tail-recursive function, as there isn’t any instruction left to be executed because the recursive call is the last statement. Hence, there is no need to save the stack frame of the function.
Here’s an example of tail recursion:
#include <bits/stdc++.h>
using namespace std;
// Find gcd of the number
int gcd(int a, int b)
{
if(b == 0) {
return a;
}
return gcd(b, a % b); // Tail recursion
}
int main()
{
int a = 10, b = 4;
cout << gcd(a, b);
return 0;
}
Tail recursion functions are considered better than linear-, binary-, or mutual-recursive functions because a modern compiler can optimize them. Modern compilers implement a method called tail call elimination to optimize the tail recursion code.
Let’s look at our gcd function again:
int gcd(int a, int b)
{
if(b == 0) {
return a;
}
return gcd(b, a % b); // Tail recursion
}
We can easily convert this function with the help of the goto statement:
int gcd(int a, int b)
{
start:
if(b == 0) {
return a;
}
int c = a % b;
a = b;
b = c;
goto start;
}
Not only does tail recursion optimize time complexity by removing the overhead of the function call, but it can also optimize space complexity. Let’s see how.
Each function call requires its own memory space to get executed. For each function call, the activation record of the function or function’s stack frame gets stored on the top of the memory stack, which stores all the information related to the function, such as:
Let’s assume each function call requires constant or O(1) space, so for n function calls, it will require O(n) space. Tail recursion reduces the space complexity of the function from O(n) to O(1) with the help of the tail-call-elimination method. As no new function call occurs, no new stack frames are created, and the function is created in constant memory space.
The function’s stack frame need not be preserved because:
Hence, the function executes in constant memory space. This makes tail recursion faster and memory efficient.
In tail recursion, there is no other operation to perform after executing the recursive function itself; the function can directly return the result of the recursive call.
In non-tail recursion, some operations need to be performed using the returned value of the recursive call.
Tail-recursive functions are considered better than non-tail-recursive functions — the compiler can easily optimize the tail-recursive function as there is nothing left to do in the current function after the recursive call. Hence, the function's stack frame need not be saved.
Example: Tail recursion
#include <bits/stdc++.h>
using namespace std;
// Find gcd of the number
int gcd(int a, int b)
{
if(b == 0) {
return a;
}
return gcd(b, a % b); // Tail recursion
}
int main()
{
int a = 10, b = 4;
cout << gcd(a, b);
return 0;
}
Example: Non-tail recursion
#include <bits/stdc++.h>
using namespace std;
// Find factorial of the number
int factorial(int n)
{
if(n == 0) {
return 1;
}
return n * factorial(n - 1); // Non tail recursion
}
int main()
{
int a = 10;
cout << factorial(a);
return 0;
}
Any recursive algorithm can be rewritten as an iterative algorithm, and every iterative algorithm can be written as a tail-recursive algorithm. Therefore, any recursive algorithm can be converted or rewritten as a tail-recursive algorithm by adding additional parameters to maintain the state of the recursive call.
Another way of converting is writing the iterative code for non-tail-recursive function and then deriving the tail-recursive algorithm from the iterative algorithm. However, for some recursive algorithms, this may compromise the algorithm’s time complexity and result in a more complex code.
But there are some exceptions; sometimes, converting a non-tail-recursive algorithm to a tail-recursive algorithm can get tricky because of the complexity of the recursion state. (The Tak function is a good example.)
Problem: Write a recursive function to find the sum of n natural numbers where n >= 1
Let’s start with non-tail recursion:
#include <bits/stdc++.h>
using namespace std;
// Find sum of n natural number
int sum(int n)
{
if(n == 1) {
return n;
}
return n + sum(n - 1);
}
int main()
{
int n = 4;
cout << sum(n);
return 0;
}
If we call the function for n = 4, then, since its value is greater than 1, it will go on a recursive call for n = 3. Similarly, for n = 3, it will go on a recursive call for n = 2. This continues until n = 1, as this satisfies the base condition. Here, when the recursive function returns the value, its value gets added with the value of n.
Since the return value is needed later in the program, the function's stack frame will remain in the memory to retrieve its return value when needed.
Here’s what it looks like:
sum(4) = 4 + sum(3)
= 4 + (3 + sum(2))
= 4 + (3 + (2 + sum(1)))
= 4 + (3 + (2 + (1)) = 10
You can see that every function call needs to be completed before calculating the sum.
In this way, the space complexity of the function will be the number of frames remaining in the memory stack (equal to the total number of function calls). So the space complexity will be O(n).
Now, let’s solve this with tail recursion.
#include <bits/stdc++.h>
using namespace std;
// Find sum of n natural number
int sum(int n, int running_sum)
{
if(n == 1) {
return running_sum + 1;
}
return sum(n - 1, running_sum + n);
}
int main()
{
int n = 4;
cout << sum(n, 0);
return 0;
}
Here, the recursive call is the last instruction to be executed. Not even a single operation depends on the return value of the function call.
Let’s call now call the sum function by passing n = 4 and running_sum = 0. So the sequence of function call will be:
sum(4, 0)
sum(3, 4)
sum(2, 7)
sum(1, 9)
Here, running_sum stores the total of each function call without waiting for the whole function call to get completed. Also, each function call is independent because we are not using the return value of any function call to find the result of the current function. Therefore, the total space complexity of the solution will be constant.
Problem: Find nth fibonacci number using tail recursion
Fibonacci numbers are: 0, 1, 1, 2, 3, 5, 7, ...
#include <bits/stdc++.h>
using namespace std;
// Find nth fibonacci number
int fibonacci(int n, int first, int second)
{
/*
For ith recursive call
param first holds ith value in the fibonacci series
Param second holds (i + 1)th value in the fibonacci series
*/
if(n == 0) {
return first;
}
if(n == 1) {
return second;
}
return fibonacci(n - 1, second, second + first);
}
int main()
{
int n = 10;
cout << fibonacci(n, 0, 1);
return 0;
}
In this program, instead of calling fibonacci number in a non-tail-recursive manner, we have used two variables (first and second) to maintain the function’s state. The first variable holds the ith Fibonacci number, and the second value holds the (i+1)th Fibonacci number for the ith recursive call. Here too, the function’s stack frame need not be preserved as its return value is used only in the current function call.
Advantages:
Disadvantages:
Following are a few examples of technical interview questions related to tail recursion that software engineers or developers can expect at FAANG and other tier-1 tech companies:
Question1: When should tail recursion be used?
Answer: Tail recursion is helpful in resolving the stack overflow error. Also, compiler optimization is possible in tail recursion, which makes the code more efficient. However, the downside is that it compromises the readability of code, and sometimes you need extra variables to maintain the state of the recursive function. So, you can use tail recursion when you think code efficiency is much more important than readability. Also, tail recursion code can easily be converted to iterative code, which is more efficient than tail recursion as there is no overhead involved in the function call and return.
Question 2: What are the problems that can easily be solved using tail recursion?
Answer: Many functions that are not obviously tail-recursive can be converted to tail-recursive form by adding some extra parameters. For example, the standard implementation of the recursive Fibonacci function is not tail-recursive. But, if we add two more parameters, it can easily be converted into a tail-recursive function.
Question 3: Is iterative implementation possible for every recursive algorithm?
Answer: Yes, every recursive algorithm can be rewritten as an iterative algorithm. To learn more about the differences between recursion and iteration and to understand when to use which, check out the article “Difference Between Recursion and Iteration.”
Nailing tech interviews at FAANG and Tier-1 tech companies can be challenging even for seasoned software engineers. It requires a deep understanding of data structure and algorithms as well as systems design.
If you’re looking for guidance and help to nail these questions and more, sign up for our free webinar. As pioneers in the field of technical interview prep, we have trained thousands of software engineers to crack the toughest coding interviews and land jobs at their dream companies, such as Google, Facebook, Apple, Netflix, Amazon, and more!
----------
Article contributed by Problem Setters Official
Attend our webinar on
"How to nail your next tech interview" and learn