3

I've heard that any recursive algorithm can always be expressed by using a stack. Recently, I've been working on programs in an environment with a prohibitively small available call stack size.

I need to do some deep recursion, so I was wondering how you could rework any recursive algorithm to use an explicit stack.

For example, let's suppose I have a recursive function like this

function f(n, i) {
  if n <= i return n
  if n % i = 0 return f(n / i, i)
  return f(n, i + 1)
}

how could I write it with a stack instead? Is there a simple process I can follow to convert any recursive function into a stack-based one?

Peter Olson
  • 139,199
  • 49
  • 202
  • 242
  • possible duplicate of [Way to go from recursion to iteration](http://stackoverflow.com/questions/159590/way-to-go-from-recursion-to-iteration) – Marcin Mar 22 '12 at 23:26

4 Answers4

10

If you understand how a function call affects the process stack, you can understand how to do it yourself.

When you call a function, some data are written on the stack including the arguments. The function reads these arguments, does whatever with them and places the result on the stack. You can do the exact same thing. Your example in particular doesn't need a stack so if I convert that to one that uses stack it may look a bit silly, so I'm going to give you the fibonacci example:

fib(n)
    if n < 2 return n
    return fib(n-1) + fib(n-2)

function fib(n, i)
    stack.empty()
    stack.push(<is_arg, n>)
    while (!stack.size() > 2 || stack.top().is_arg)
        <isarg, argn> = stack.pop()
        if (isarg)
            if (argn < 2)
                stack.push(<is_result, argn>)
            else
                stack.push(<is_arg, argn-1>)
                stack.push(<is_arg, argn-2>)
        else
            <isarg_prev, argn_prev> = stack.pop()
            if (isarg_prev)
                stack.push(<is_result, argn>)
                stack.push(<is_arg, argn_prev>)
            else
                stack.push(<is_result, argn+argn_prev>)
     return stack.top().argn

Explanation: every time you take an item from the stack, you need to check whether it needs to be expanded or not. If so, push appropriate arguments on the stack, if not, let it merge with previous results. In the case of fibonacci, once fib(n-2) is computed (and is available at top of stack), n-1 is retrieved (one after top of stack), result of fib(n-2) is pushed under it, and then fib(n-1) is expanded and computed. If the top two elements of the stack were both results, of course, you just add them and push to stack.

If you'd like to see how your own function would look like, here it is:

function f(n, i)
    stack.empty()
    stack.push(n)
    stack.push(i)
    while (!stack.is_empty())
        argi = stack.pop()
        argn = stack.pop()
        if argn <= argi
            result = argn
        else if n % i = 0
            stack.push(n / i)
            stack.push(i)
        else
            stack.push(n)
            stack.push(i + 1)
    return result
Shahbaz
  • 46,337
  • 19
  • 116
  • 182
4

Your particular example is tail-recursive, so with a properly optimising compiler, it should not consume any stack depth at all, as it is equivalent to a simple loop. To be clear: this example does not require a stack at all.

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • This doesn't really help me. It does point out that I picked a poor example, but it doesn't really demonstrate how recursion is done with a stack. Anyway, most interpreters for the language I am working with, Javascript, do not even have tail call optimization. – Peter Olson Mar 22 '12 at 22:56
  • Also: That example is in fact *not* tail-recursive, as it doesn't literally return the result of a call to itself. It could be transformed to a tail-recursive implementation by adding an extra argument, but it currently isn't. –  Mar 22 '12 at 22:58
  • @duskwuff Yes, it does literally return the result of a call to itself. What are you talking about? – Marcin Mar 22 '12 at 23:22
  • The example was edited. The original returned either `f(n * 2) + 1` or `f(n + 1) - 3`. –  Mar 22 '12 at 23:23
  • @PeterOlson I'm pretty sure it helps you to recognise that you sometimes do not require a stack. – Marcin Mar 22 '12 at 23:23
  • @duskwuff No, not in the original version: http://stackoverflow.com/revisions/9831666/1 – Marcin Mar 22 '12 at 23:24
4

You can convert your code to use a stack like follows:

stack.push(n)
stack.push(i)
while(stack.notEmpty)
    i = stack.pop()
    n = stack.pop()
    if (n <= i) {
        return n
    } else if (n % i = 0) {
        stack.push(n / i) 
        stack.push(i)
    } else {
        stack.push(n) 
        stack.push(i+1)
    }
}

Note: I didn't test this, so it may contain errors, but it gives you the idea.

sch
  • 27,436
  • 3
  • 68
  • 83
  • That's more or less what I was going to post (along with the observation that the question is misphrased; recursion always explicitly uses a stack in the call stack) but I think it's worth pointing out that in this case the stack never grows beyond two items so you get the tail recursion observation for free. – Tommy Mar 22 '12 at 23:05
  • Yes, a stack is not necessary here and a simple while loop is sufficient, but I still used a stack because OP asked specifically for that. – sch Mar 22 '12 at 23:07
1

Both your example and the fibonacci function can be rewritten iteratively without using stack.

Here's an example where the stack is required, Ackermann function:

def ack(m, n):
    assert m >= 0 and n >= 0
    if m == 0: return n + 1
    if n == 0: return ack(m - 1, 1)
    return ack(m - 1, ack(m, n - 1))

Eliminating recursion:

def ack_iter(m, n):
    stack = []
    push = stack.append
    pop = stack.pop
    RETURN_VALUE, CALL_FUNCTION, NESTED = -1, -2, -3

    push(m) # push function arguments
    push(n)
    push(CALL_FUNCTION) # push address
    while stack: # not empty
        address = pop()
        if address is CALL_FUNCTION:
            n = pop()  # pop function arguments
            m = pop()
            if m == 0: # return n + 1
                push(n+1) # push returned value
                push(RETURN_VALUE)
            elif n == 0: # return ack(m - 1, 1)
                push(m-1)
                push(1)
                push(CALL_FUNCTION)
            else: # begin: return ack(m - 1, ack(m, n - 1))
                push(m-1) # save local value
                push(NESTED) # save address to return
                push(m)
                push(n-1)
                push(CALL_FUNCTION)
        elif address is NESTED: # end: return ack(m - 1, ack(m, n - 1))
            # old (m - 1) is already on the stack
            push(value) # use returned value from the most recent call
            push(CALL_FUNCTION)
        elif address is RETURN_VALUE:
            value = pop() # pop returned value
        else:
            assert 0, (address, stack)
    return value

Note it is not necessary here to put CALL_FUNCTION, RETURN_VALUE labels and value on the stack.

Example

print(ack(2, 4)) # -> 11
print(ack_iter(2, 4))
assert all(ack(m, n) == ack_iter(m, n) for m in range(4) for n in range(6))
print(ack_iter(3, 4)) # -> 125
jfs
  • 399,953
  • 195
  • 994
  • 1,670