不用递归遍历非二叉树的算法是什么(使用栈)
What is the algorithm to traverse a non-binary tree without recursion (using stack)
直觉上我知道我一直在像 (node, iterator)
这样的堆栈对,但我仍然无法找到有效的解决方案。
您始终可以将递归算法转换为具有显式堆栈的算法。从递归代码开始:
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;
}
请注意 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;
}
}
}
}
可以让代码更漂亮一些,但我会把它留给你。需要注意的是,没有直觉或试图想象指针在做什么。我们只是对代码进行了代数计算,结果是一个相当不错的实现。我没有 运行,但除非我犯了代数错误(这是可能的),否则应该 "just work."
请注意,这与您将在教科书中看到的典型的基于堆栈的 DFS 有点不同。那些将新找到的节点的所有子节点推送到堆栈上,必须首先完成最右边的子节点才能获得正常的 DFS 顺序。
相反,在这里我们将父项与一个整数一起推送,说明接下来应该搜索哪个子项。这就是你说的节点+迭代器。它有点复杂,但在堆栈大小方面也更有效。我们堆栈的最大大小是 O(D),其中 D 是树的最大深度。教科书算法中堆栈的大小是 O(KD),其中 K 是一个节点可以拥有的最大子节点数。
直觉上我知道我一直在像 (node, iterator)
这样的堆栈对,但我仍然无法找到有效的解决方案。
您始终可以将递归算法转换为具有显式堆栈的算法。从递归代码开始:
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;
}
请注意 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;
}
}
}
}
可以让代码更漂亮一些,但我会把它留给你。需要注意的是,没有直觉或试图想象指针在做什么。我们只是对代码进行了代数计算,结果是一个相当不错的实现。我没有 运行,但除非我犯了代数错误(这是可能的),否则应该 "just work."
请注意,这与您将在教科书中看到的典型的基于堆栈的 DFS 有点不同。那些将新找到的节点的所有子节点推送到堆栈上,必须首先完成最右边的子节点才能获得正常的 DFS 顺序。
相反,在这里我们将父项与一个整数一起推送,说明接下来应该搜索哪个子项。这就是你说的节点+迭代器。它有点复杂,但在堆栈大小方面也更有效。我们堆栈的最大大小是 O(D),其中 D 是树的最大深度。教科书算法中堆栈的大小是 O(KD),其中 K 是一个节点可以拥有的最大子节点数。