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

问题描述 投票:0回答:1

这个问题在这里已有答案:

直觉上我明白我会像(node, iterator)一样保持堆栈对,但我仍然无法找到有效的解决方案。

algorithm tree tree-traversal
1个回答
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;
}

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

现在,如果你愿意,我们做一些代数来消除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;
}

注意++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;
      }
    }
  }
}

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.