在没有递归的情况下遍历非二叉树的算法是什么(使用堆栈)

时间:2017-12-31 03:06:26

标签: algorithm tree tree-traversal

直观地说,我理解我继续使用像(node, iterator)这样的堆栈对,但我仍然无法找到可行的解决方案。

1 个答案:

答案 0 :(得分:2)

您始终可以将递归算法转换为具有显式堆栈的算法。从递归代码开始:

void traverse(NODE *p) {
  if (p) {
    for (int i = 0; i < p->n_children; ++i) {
      traverse(p->child[i]);
    }
  }
}

替换递归调用:

struct {
  NODE *p;
  int i;
} stk[MAX_RECURSION_DEPTH];
int sp = 0;

void traverse(NODE *p) {
 start:
  if (p) {
    for (int i = 0; i < p->n_children; ++i) {
      // Save local values on stack.
      stk[sp].p = p;
      stk[sp++].i = i;
      // Simulate recursive call.  
      p = p->child[i];        
      goto start;
      // Goto this label for return.
     rtn:
    }
  }
  // Simulate recursive return, restoring from stack if not empty.
  if (sp == 0) return;
  p = stk[--sp].p;
  i = stk[sp].i;
  goto rtn;
}

你有它:一个显式的堆栈实现,只要递归版本就可以工作。它是一回事。

现在如果你愿意,我们做一些代数来消除goto。首先,我们可以将for重写为while并重构rtn标签

void traverse(NODE *p) {
  int i;
 start:
  if (p) {
    i = 0;
   rtn_2:
    while (i < p->n_children) {
      stk[sp].p = p;
      stk[sp++].i = i;
      p = p->child[i];        
      goto start;
    }
  }
  if (sp == 0) return;
  p = stk[--sp].p;
  i = stk[sp].i;
  ++i;
  goto rtn_2;
}

请注意++i中的while是死代码,因此可以安全删除。

现在注意while的主体永远不会执行多次。它可以替换为if。我们还可以将goto rtn_2替换为它导致执行的代码。

void traverse(NODE *p) {
  int i;
 start:
  if (p) {
    i = 0;
    if (i < p->n_children) {
      stk[sp].p = p;
      stk[sp++].i = i;
      p = p->child[i];        
      goto start;
    }
  }
  for (;;) {
    if (sp == 0) return;
    p = stk[--sp].p;
    i = stk[sp].i;
    ++i;
    if (i < p->n_children) {
      stk[sp].p = p;
      stk[sp++].i = i;
      p = p->child[i];        
      goto start;
    }
  }
}

最后,我们可以通过使用循环取消start标签:

void traverse(NODE *p) {
  int i;
  for (;;) {
    if (p) {
      i = 0;
      if (i < p->n_children) {
        stk[sp].p = p;
        stk[sp++].i = i;
        p = p->child[i];        
        continue;
      }
    }
    for (;;) {
      if (sp == 0) return;
      p = stk[--sp].p;
      i = stk[sp].i;
      ++i;
      if (i < p->n_children) {
        stk[sp].p = p;
        stk[sp++].i = i;
        p = p->child[i];        
        break;
      }
    }
  }
}

另一个清理是要注意i中的if始终为0,continue实际上是实现嵌套循环,我们可以将其显式化。还有一个可以删除的冗余stk[sp].p = p;。它只是将一个值复制到已经存在的堆栈中:

void traverse(NODE *p) {
  for (;;) {
    while (p && p->n_children > 0) {
      stk[sp].p = p;
      stk[sp++].i = 0;
      p = p->child[0];        
    }
    for (;;) {
      if (sp == 0) return;
      p = stk[--sp].p;
      int i = stk[sp].i + 1;
      if (i < p->n_children) {
        stk[sp++].i = i; // stk[sp].p = p; was redundant, so deleted
        p = p->child[i];        
        break;
      }
    }
  }
}

可以让代码更漂亮,但我会把它留给你。需要注意的是,没有直觉或试图想象指针在做什么。我们只对代码进行了代数处理,得到了相当不错的实现。我没有经营它,但除非我犯了代数错误(这是可能的),否则这应该只是工作。&#34;

请注意,这与您在教科书中看到的典型基于堆栈的DFS略有不同。那些推送堆栈中新发现的节点的所有子节点,必须首先完成最右边的子节点以获得正常的DFS顺序。

相反,我们正在推动父母一起使用一个整数来说明接下来应该搜索哪个孩子。这是您提到的节点+迭代器。它有点复杂但堆栈大小也更高效。我们堆栈的最大大小是O(D),其中D是树的最大深度。教科书算法中堆栈的大小为O(KD),其中K是节点可以拥有的最大子节点数。