Recursive function calls are a simple and understandable way to code. For certain problems, recursion can provide more concise code and more intuitive readability, such as the familiar traversal of tree structures.
However, when the number of function calls becomes too many, it can lead to a common issue: stack overflow. This is because the stack space generally has strict size limits. While we can adjust the stack space size through configuration parameters, this is merely a stopgap solution and does not address the root of the problem.
From another perspective, when the operating system calls a function, it saves stack information (such as the position of the next instruction after the function returns, local variables, etc.). This implicitly contains the state of each function call. The function checks this state to decide whether the stopping condition is met. Naturally, we think of using a stack data structure to simulate the process of function recursion.
Combining the knowledge of state machines, it’s not difficult to analyze the idea of converting a recursive function to an iterative one. Suppose we have the following recursive function:
void func(...args){
// List of local variables (function parameters are also part of local variables)
();
doSomething
// Recursive call appears somewhere
(...args1);
func}
Then we can set up a state structure to save the function information when the recursive version is called, with one part saving the current function variable information of the call stack and the other part saving the position during execution. If the function has a return value, a variable can be used to store it:
enum Loc{
;
START;
FIRST;
SECOND;
THIRD// ...other position information
};
struct State{
// List of local parameters
struct Info context;
// Position during execution
enum Loc location;
};
void func(...args){
<State> s;
stack= {}; // Set the initial state
State init_state .push(std::move(init_state));
swhile(!s.empty()){
&cnt_state = s.top();
State switch(cnt_state.location){
case START:
();
doSomethingcase FIRST:
.location = SECOND;
cnt_state.context = ...;
cnt_state= {...};
State new_state .push(new_state);
scontinue;
//....
}
}
}
Taking the in-order traversal of LeetCode as an example, it’s not hard to write the following recursive version:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
<int> inorderTraversal(TreeNode* root) {
vector<int> res;
vectorif(root == nullptr){
return res;
}
<int> && left = inorderTraversal(root -> left);
vector.insert(res.end(),left.begin(),left.end());
res.push_back(root -> val);
res
<int> && right = inorderTraversal(root -> right);
vector.insert(res.end(),right.begin(),right.end());
resreturn res;
}
};
In-order traversal has two recursive call points: one when visiting the left subtree and another after reading the middle node to visit the right subtree. Since the important local variable in the whole function is the parameter root
, the structure only needs to save one parameter. Finally, the iterative version of in-order traversal is as follows:
enum Loc{
, // Initial state, indicating the node has just been visited
START, // Left child node has been visited
VISITED_LEFT, // Middle node has been visited
VISITED_MIDDLE};
struct State {
*node; // Current node
TreeNode ; // Visit state of the current node
Loc location};
class Solution {
public:
<int> inorderTraversal(TreeNode* root) {
vector<int> result;
vector<State> s;
stack
// Helper function to push the node and its state into the stack
auto pushState = [&](TreeNode *node) {
.push({node, START});
s};
// Initial condition: push the root node if it's not null
if (root != nullptr) {
(root);
pushState}
// Loop to simulate recursive calls
while (!s.empty()) {
&cnt_state = s.top(); // Get the top element of the stack, simulating the parameters and local variables of the current function call
State
switch (cnt_state.location) {
case START:
// First time visiting the node, visit the left subtree first
if (cnt_state.node->left != nullptr) {
.location = VISITED_LEFT; // Change state to left child visited
cnt_state(cnt_state.node->left); // Push the left child node into the stack
pushStatecontinue; // End the current loop early
}
case VISITED_LEFT:
// Left subtree visited, visit the current node
.push_back(cnt_state.node->val); // Add the node value to the result
result.location = VISITED_MIDDLE; // Change state to middle node visited
cnt_state// Visit the right subtree
if (cnt_state.node->right != nullptr) {
(cnt_state.node->right); // Push the right child node into the stack
pushStatecontinue;
}
case VISITED_MIDDLE:
// Current node and its left and right subtrees have been visited, return and pop from the stack
.pop();
scontinue;
}
}
return result;
}
};