How to eliminate this type of recursion?

Go To StackoverFlow.com

0

This is a bit more intricate than a simple left-recursion or tail-call recursion. So I'm wondering how I can eliminate this kind of recursion. I'm already keeping my own stack as you can see below, so the function needs to no params or return values. However, it's still calling itself up (or down) to a certain level and I want to turn this into a loop, but been scratching my head over this for some time now.

Here's the simplified test case, replacing all "real logic" with printf("dostuff at level #n") messages. This is in Go but the problem is applicable to most languages. Use of loops and goto's would be perfectly acceptable (but I played with this and it gets convoluted, out-of-hand and seemingly unworkable to begin with); however, additional helper functions should be avoided. I guess I should to turn this into some kind of simple state machine, but... which? ;)

As for the practicality, this is to run at about 20 million times per second (stack depth can range from 1 through 25 max later on). This is a case where maintaining my own stack is bound to be more stable / faster than the function call stack. (There are no other function calls in this function, only calculations.) Also, no garbage generated = no garbage collected.

So here goes:

func testRecursion () {
    var root *TMyTreeNode = makeSomeDeepTreeStructure()
    // rl: current recursion level
    // ml: max recursion level
    var rl, ml = 0, root.MaxDepth
    // node: "the stack"
    var node = make([]*TMyTreeNode, ml + 1)

    // the recursive and the non-recursive / iterative test functions:
    var walkNodeRec, walkNodeIt func ();

    walkNodeIt = func () {
        log.Panicf("YOUR ITERATIVE / NON-RECURSIVE IDEAS HERE")
    }

    walkNodeRec = func () {
        log.Printf("ENTER LEVEL %v", rl)
        if (node[rl].Level == ml) || (node[rl].ChildNodes == nil) {
            log.Printf("EXIT LEVEL %v", rl)
            return
        }
        log.Printf("PRE-STUFF LEVEL %v", rl)
        for i := 0; i < 3; i++ {
            switch i {
            case 0:
                log.Printf("PRECASE %v.%v", rl, i)
                node[rl + 1] = node[rl].ChildNodes[rl + i]; rl++; walkNodeRec(); rl--
                log.Printf("POSTCASE %v.%v", rl,  i)
            case 1:
                log.Printf("PRECASE %v.%v", rl, i)
                node[rl + 1] = node[rl].ChildNodes[rl + i]; rl++; walkNodeRec(); rl--
                log.Printf("POSTCASE %v.%v", rl,  i)
            case 2:
                log.Printf("PRECASE %v.%v", rl, i)
                node[rl + 1] = node[rl].ChildNodes[rl + i]; rl++; walkNodeRec(); rl--
                log.Printf("POSTCASE %v.%v", rl,  i)
            }
        }
    }

    // test recursion for reference:
    if true {
        rl, node[0] = 0, root
        log.Printf("\n\n=========>RECURSIVE ML=%v:", ml)
        walkNodeRec()
    }

    // test non-recursion, output should be identical
    if true {
        rl, node[0] = 0, root
        log.Printf("\n\n=========>ITERATIVE ML=%v:", ml)
        walkNodeIt()
    }

}

UPDATE -- after some discussion here, and further thinking:

I just made up the following pseudo-code which in theory should do what I need:

curLevel = 0
for {
    cn = nextsibling(curLevel, coords)
    lastnode[curlevel] = cn
    if cn < 8 {
        if isleaf {
            process()
        } else {
            curLevel++
        }
    } else if curLevel == 0 {
        break
    } else {
        curLevel--
    }
}

Of course the tricky part will be filling out nextsibling() for my custom use-case. But just as a general solution to eliminating inner recursion while maintaining the depth-first traversal order I need, this rough outline should do so in some form or another.

2012-04-05 03:00
by metaleap
This is hard to answer without more details. Why the for loop only 3 times? are there only ever 3 child nodes? Usually I would just use a queue of root,child1,...,childN pushing and popping off the stack until it was empty. But I feel like I'm missing something in what you want to accomplish - Jeremy Wall 2012-04-05 03:21
Hi Jeremy, I simplified and generalized my use-case here a bit, but any solution found to the above would help in the real case for sure. This is just a 3x example loop, what matters is the fact that we have zero or one recursive calls per for-loop iteration. Later on the number of for-loop iterations is max 8 but may terminate earlier. In fact it goes like this: for (flag < 8) { switch flag { case foo: flag = calcnewflagbetween0and8 } - metaleap 2012-04-05 03:25
Correction: per for-loop iteration, there is always exactly (at least and at most) one recursive call - metaleap 2012-04-05 03:34


1

I'm not really sure I understand what it is you want to do since your recursion code looks a little strange. However if I understand the structure of your TMyTreeNode then this is what I would do for a non recursive version.

// root is our root node
q := []*TMyTreeNode{root}
processed := make(map[*TMyTreeNode]bool
for {
  l := len(q)
  if l < 1 {
    break // our queue is empty
  }
  curr := q[l - 1]
  if !processed[curr] && len(curr.childNodes) > 0 {
    // do something with curr
    processed[curr] = true
    q = append(q, curr.childNodes...)
    continue // continue on down the tree.
  } else {
    // do something with curr
    processed[curr] = true
    q := q[:l-2] // pop current off the queue
  }
}

NOTE: This will go arbitrarily deep into the structure. If that's not what you want it will need some modifications.

2012-04-05 03:43
by Jeremy Wall
Thanks for the idea! I'm coding this experimentally right now. At first glance, not sure yet how to integrate my "code that really has to run in the for-loop-iteration's switch-case right-after the recursive call" but I should be able to figure that one out, too. Will post the full conversion here once it's working - metaleap 2012-04-05 03:59
Unfortunately, this doesn't work out for this special case -- you're simultaneously building a list of nodes to be processed and processing the list at the same time, which is a good idea but does not address the need for a strict travel-down-then-bubble-back-up ordering of processing the nodes. Example: at the root level, I may find 3 out of 8 sub-nodes worth processing, but it is crucial that I walk all the way down the 1st of those 3 before processing the other 2, and in fact I may have an early exit. So at 3 levels, the ordering would be 0.0.0, 0.0.1, 0.0.2, 0.1.0, 0.1.1, 0.1.2, 0.2.0, .. - metaleap 2012-04-05 04:23
Also we may be talking a potential total of a million nodes or so so I cannot pre-build an ordered traversal list in order to only process the max. 75 (for a depth of 25) nodes out of millions really required for this specific traversal. This needs ordered traversal (the for-switch construct obtaining the correct order btw), first all the way down, then all the way back up before proceeding to the next sibling - metaleap 2012-04-05 04:25
Ads