A recursive call corresponds to pushing a state on a stack. For the
mechanical translation of recursive calls to iterative code, as the
compiler does with C++ code, the state pushed includes a return address,
i.e. where to continue when that state is popped. For a simulation of
that in C++ I think it would be easiest to use case label values as
simulated return addresses, and to think of the rest of the state as the
current local variables' state, instead of as call arguments.
For more intelligent human level translation of recursive to iterative,
algorithm transformation, one may think instead of what sequence of
states the stack will hold at any time.
* * *
As a first example consider Euclid's greatest common divisor algorithm:
auto gcd1( int const a, int const b )
-> int
{ return (b == 0? a : gcd1( b, a%b )); }
The idea is simply that if a>b, then either b is the greatest common
divisor D, or else, since there's a whole number of D's in b, and then
further a whole number of D's in a-b, D must be the common divisor of
a-b and b. And to avoid the inefficiency of Euclid's repeated
subtraction, we just reduce that immediately to a%b and b.
The recursive call pushes a pair of numbers on a stack, so you can
translate it like this:
using Pair = pair<int, int>;
auto gcd2( int const a, int const b )
-> int
{
stack<Pair> s;
s.push( Pair{ a, b } );
for( ;; )
{
Pair const current = s.top();
s.pop();
if( current.second == 0 ) { return current.first; }
s.push( Pair{ current.second, current.first%current.second } );
}
}
In this code the stack will never have more than one item, so it can be
replaced with a single variable:
auto gcd3( int const a, int const b )
-> int
{
Pair current = {a, b};
for( ;; )
{
if( current.second == 0 ) { return current.first; }
current = Pair( current.second, current.first%current.second );
}
}
And then there are a number of further trivial simplifications, which I
leave to you.
* * *
With the basics covered, let's do an inorder traversal of a binary tree:
void display1( Node const* const root )
{
if( root == nullptr ) { return; }
display1( root->left );
cout << root->value << ' ';
display1( root->right );
}
Each recursive call pushes a pointer onto a stack. Expressing both
recursive calls as iteration is difficult to do immediately. But the
first recursive call is not so difficult:
void display2( Node const* const root )
{
stack<Node const*> s;
Node const* p = root;
while( p != nullptr )
{
s.push( p );
p = p->left;
}
while( not s.empty() )
{
Node const* const current = s.top();
s.pop();
cout << current->value << ' ';
display2( current->right );
}
}
The idea here is to display the left subtree plus the root node, with
the help of the single remaining recursive call, and then to let that
call also handle the right subtree.
Now for the remaining recursive call, it clearly pushes the right child
node pointer onto the stack, and then proceeds left again:
void display3( Node const* const root )
{
stack<Node const*> s;
Node const* p = root;
while( p != nullptr )
{
s.push( p );
p = p->left;
}
while( not s.empty() )
{
Node const* const current = s.top();
s.pop();
cout << current->value << ' ';
Node const* p = current->right;
while( p != nullptr )
{
s.push( p );
p = p->left;
}
}
}
But at least for me it's not entirely obvious at a glance that this
should work, so I tested it with a little 7-node tree. Disclaimer: there
may be bugs still. But it worked with that tree.
Now, this code has the form of an unrolled LOOP-AND-A-HALF, so, roll
that loop back up:
void display4( Node const* const root )
{
stack<Node const*> s;
Node const* p = root;
for( ;; )
{
while( p != nullptr )
{
s.push( p );
p = p->left;
}
if( s.empty() )
{
return;
}
Node const* const current = s.top();
s.pop();
cout << current->value << ' ';
p = current->right;
}
}
Some further simplifications may be possible but, again, I leave that to
you.
* * *
Typically a problem either fits a recursive or an iterative form. GCD is
an exception, it's just about equally well expressed as recursive or
iterative (I suspect this holds in general for any tail-recursive
function, that it's just about equally as clear when expressed
iteratively). But the in-order traversal above is much more clear to me
as recursive function than as iterative. And this should IMO be the main
consideration. Not whether one can possibly shave off a nanosecond or
two, but which approach yields the most clear and easiest to understand
code.
Cheers & hth.,
- Alf