【问题标题】:What is the algorithm to traverse a non-binary tree without recursion (using stack) [duplicate]什么是在没有递归的情况下遍历非二叉树的算法(使用堆栈)[重复]
【发布时间】:2017-12-31 03:06:26
【问题描述】:

凭直觉,我知道我一直使用像 (node, iterator) 这样的堆栈对,但我仍然无法找到有效的解决方案。

【问题讨论】:

  • 您使用哪种语言,您可以发布您使用的代码吗?
  • 每次出栈时,增加子迭代器。如果有下一个孩子,将其压入堆栈并重复;如果没有,则弹出当前节点。这直接来自二叉树的特殊情况。

标签: algorithm tree tree-traversal


【解决方案1】:

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

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;
}

你有它:一个显式的堆栈实现,它必须与递归版本一样工作。都是一样的。

现在,如果您愿意,我们可以做一些代数来消除gotos。首先我们可以将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;
}

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

现在请注意,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;
      }
    }
  }
}

可以让代码更漂亮一些,但我会把它留给你。需要注意的是,没有直觉或试图想象指针在做什么。我们只是对代码进行了代数,并产生了一个相当不错的实现。我没有运行它,但除非我犯了代数错误(这是可能的),否则它应该“正常工作”。

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

相反,我们在这里推送父级和一个整数,表示接下来应该搜索哪个子级。这是您提到的节点+迭代器。它有点复杂,但在堆栈大小方面也更有效。我们堆栈的最大大小是 O(D),其中 D 是树的最大深度。教科书算法中的堆栈大小为 O(KD),其中 K 是节点可以拥有的最大子节点数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    • 2011-09-08
    • 1970-01-01
    • 1970-01-01
    • 2012-09-23
    相关资源
    最近更新 更多