我们可以通过递归来遍历二叉搜索树,如:
void traverse1(node t) {
if( t != NULL) {
visit(t);
traverse1(t->left);
traverse1(t->right);
}
}
以及通过循环(使用堆栈),如:
void traverse2(node root) {
stack.push(root);
while (stack.notEmpty()) {
node next = stack.pop();
visit(next);
if (next->right != NULL)
stack.push(next->right);
if (next->left != NUll)
stack.push(next->left)
}
}
问题
哪一个效率更高?为什么呢?
我认为这两种方法的时间复杂度都是O(n)。所有的差异都在空间复杂性或......?
答案 0 :(得分:2)
这取决于您如何定义效率?它是在运行时,代码量,可执行文件的大小,使用了多少内存/堆栈空间,或者理解代码有多容易?
递归非常容易编码,希望很容易理解,而且代码更少。循环可能会更复杂(取决于您如何查看复杂性)和代码。递归可能更容易理解,代码量和可执行文件大小也会更少。假设您有一些横向项目,递归将使用更多的堆栈空间。
循环将有更多的代码(如上面的示例所示),并且可能被认为有点复杂。但是,横向只是一个调用,而不是几个。因此,如果您有很多项目要横向,那么循环会更快,因为您没有时间在堆栈上推送项目并将其弹出,这是使用递归时会发生的情况。
答案 1 :(得分:0)
两个版本都具有相同的空间和时间复杂性。
递归隐式使用堆栈(内存位置)来存储调用上下文,第二次使用堆栈抽象数据类型有效地模拟第一个版本,显式地使用堆栈。
不同之处在于,对于第一个版本,您可能会遇到深度不平衡树的堆栈溢出风险,但它在概念上更简单(错误的机会更少)。第二种方法使用动态分配来存储指向父节点的指针。
答案 2 :(得分:0)
除了效率之外,如果您的树太深或者您的堆栈空间有限,您可能会遇到溢出 - 堆栈溢出!!
使用迭代方法,您可以使用更大的堆空间来放置已分配的堆栈。通过递归,您无法选择,因为会为您推送和弹出堆栈帧。
我知道这种受限制的堆栈环境可能有点罕见;然而,人们需要意识到这一点。
答案 3 :(得分:0)
您必须确定差异才能确定。我个人有一种感觉,在这个特定的例子中,递归公式将击败具有显式堆栈的公式。
非递归版本的用途是消除调用。另一方面 - 根据确切的库实现 - 推送和弹出也可能解析为函数调用。
任何体面的编译器实际上都会以类似于以下伪代码的方式编码递归函数:
void traverse1(node t) {
1:
if( t != NULL) {
visit(t);
traverse1(t->left);
t = t->right;
goto 1;
}
}
因此消除了一个递归调用。这被称为尾部呼叫消除。
答案 4 :(得分:-1)
时间和空间的复杂性是相同的。唯一的区别是traverse2
不会递归调用自身。这个应该使它稍微快一点,因为从堆栈推送/弹出比调用函数更便宜。
那就是说,我认为递归版本是“更干净”,所以我个人会使用它,除非事实证明它太慢了。