php 尾递归 快速排序,关于递归:快速排序和尾递归优化

在算法简介p169中,它讨论了对Quicksort使用尾递归。

本章前面的原始Quicksort算法是(用伪代码)

Quicksort(A, p, r)

{

if (p < r)

{

q:

Quicksort(A, p, q)

Quicksort(A, q+1, r)

}

}

使用尾部递归的优化版本如下

Quicksort(A, p, r)

{

while (p < r)

{

q:

Quicksort(A, p, q)

p:

}

}

其中Partition根据枢轴对数组进行排序。

不同之处在于第二种算法仅调用Quicksort一次即可对LHS进行排序。

有人可以向我解释为什么第一种算法会导致堆栈溢出,而第二种却不会吗? 还是我误会了这本书。

复杂度显然不同。这本书有什么要说的吗?

这是否意味着仅第二种算法更有效,但不一定会阻止溢出

请在下面检查我的答案。

我很难看到第二个版本是尾递归的。如果最后执行的计算是递归调用,则递归函数为尾递归(或更普遍的是,调用后不需要保留任何本地数据)。这就是将新堆栈框架覆盖在现有堆栈框架之上的原因。但是,在上面的第二个版本中,在递归调用之后进行了计算,特别是q仍然是必需的。这意味着必须保留至少一部分堆叠框架。如果q传递给递归调用并从递归调用返回,则可能有效

此优化的版本没有更好的内存最坏时间。您需要在最长的一端进行尾部优化,例如:stackoverflow.com/a/12455631/895245

答案仅说明了尾递归,但几乎没有提及问题中的代码。

首先,让我们从一个简短的,可能不准确但仍然有效的堆栈溢出定义开始。

您可能现在知道,有两种不同的内存以太不同的数据结构实现:堆和堆栈。

就大小而言,堆大于堆栈,为简单起见,我们假设每次进行函数调用时都会在堆栈上创建一个新的环境(局部变量,参数等)。因此,鉴于这一事实以及堆栈大小有限的事实,如果您进行过多的函数调用,则会用完空间,从而导致堆栈溢出。

递归的问题在于,由于您每次迭代都在堆栈上创建至少一个环境,因此您将很快在有限的堆栈中占用大量空间,因此堆栈溢出通常与递归调用相关联。

因此,有一种称为" Tail递归调用优化"的东西,它将在每次进行递归调用时重用相同的环境,因此堆栈中占用的空间是恒定的,从而防止了堆栈溢出问题。

现在,有一些规则可以执行尾部调用优化。首先,每个调用最完整,也就是说,如果您中断执行,该函数应该能够在任何时候给出结果,在SICP中

即使函数是递归的,这也称为迭代过程。

如果分析您的第一个示例,您将看到每个迭代都是由两个递归调用定义的,这意味着,如果您随时停止执行,则将无法给出部分结果,因为结果取决于这些调用最后,在这种情况下,您将无法重用堆栈环境,因为总的信息将在所有这些递归调用之间分配。

但是,第二个示例没有这个问题,A是常数,并且可以局部确定p和r的状态,因此,由于所有要保持的信息都存在,因此可以应用TCO。

+1实用的在线书

我仍然有些困惑,因为p: 行取决于进一步递归的结果。从我对TRO的了解来看,它可以在将来递归使用且不需要时使用,这意味着该解决方案是迭代的,可以在不存储返回调用的情况下随时退出。

@happygilmore不是,该行取决于Partition的结果。使用while的实现并不明显,但是堆栈在每次迭代中都被重用。我现在正在工作,但是稍后我将尝试在此处发布仅使用if语句的版本,这样优化将更容易理解。

如果可以的话,那太好了。您是在说这将永远不会导致堆栈溢出或不太可能吗?

参见上面的MK答案,他似乎在说,堆栈深度为log n时可能发生堆栈溢出。似乎与您所说的相矛盾。

尾部递归优化的实质是在实际执行程序时不进行递归。当编译器或解释器能够执行TRO时,这意味着它实际上将弄清楚如何使用不用于存储嵌套函数调用的堆栈将递归定义的算法重写为一个简单的迭代过程。

无法对第一个代码段进行TR优化,因为其中有2个递归调用。

只是要澄清一下,除了显而易见的事实,即第二算法的复杂度较低,而且不太可能发生溢出,您是说第二算法永远不会溢出吗?谢谢

阅读此en.wikipedia.org/wiki/Quicksort,它解释说QuickSort的尾部递归版本限制了登录的堆栈深度。因此您仍然可以溢出,可能性较小。

很有道理,谢谢

@MK。"第一个代码段不能进行TR优化,因为其中有2个递归调用。"

@ManuelJacob我的意思是编译器/解释器无法优化。人类是不同的。

@MK。那取决于编译器。在快速测试中,clang和gcc都消除了代码直接C语言转换中的第二个递归调用。

但是仍然有第一个递归调用,对吗?

仅靠尾递归是不够的。使用while循环的算法仍可以使用O(N)堆栈空间,将其减少为O(log(N))留在CLRS的该部分中作为练习。

假设我们正在使用一种具有数组切片和尾调用优化的语言。考虑以下两种算法之间的区别:

坏:

Quicksort(arraySlice) {

if (arraySlice.length > 1) {

slices = Partition(arraySlice)

(smallerSlice, largerSlice) = sortBySize(slices)

Quicksort(largerSlice) // Not a tail call, requires a stack frame until it returns.

Quicksort(smallerSlice) // Tail call, can replace the old stack frame.

}

}

好:

Quicksort(arraySlice) {

if (arraySlice.length > 1){

slices = Partition(arraySlice)

(smallerSlice, largerSlice) = sortBySize(slices)

Quicksort(smallerSlice) // Not a tail call, requires a stack frame until it returns.

Quicksort(largerSlice) // Tail call, can replace the old stack frame.

}

}

确保第二个永远不需要超过log2(length)个堆栈帧,因为smallSlice的长度小于arraySlice的一半。但是对于第一个,不等式是相反的,它将始终需要大于或等于log2(length)个堆栈帧,并且在最坏的情况下(较小的切片始终具有长度1)可能需要O(N)个堆栈帧。

如果您不跟踪哪个切片更大或更小,即使平均需要O(log(n))堆栈帧,也会遇到与第一个溢出情况相似的最坏情况。如果始终始终对较小的切片进行排序,则将不需要超过log_2(length)个堆栈帧。

如果您使用的语言没有尾部调用优化功能,则可以将第二个版本(而不是堆栈溢出)编写为:

Quicksort(arraySlice) {

while (arraySlice.length > 1) {

slices = Partition(arraySlice)

(smallerSlice, arraySlice) = sortBySize(slices)

Quicksort(smallerSlice) // Still not a tail call, requires a stack frame until it returns.

}

}

另一点值得注意的是,如果您实现的是Introsort之类的东西,如果递归深度超过与log(N)成正比的某个数字,则将其更改为Heapsort,您将永远不会遇到quicksort的O(N)最坏情况的堆栈内存使用情况,因此您从技术上讲,不需要这样做。进行此优化(首先弹出较小的切片)仍然可以提高O(log(N))的常数因子,因此强烈建议使用。

好吧,最明显的观察结果是:

最常见的堆栈溢出问题-定义

The most common cause of stack overflow is excessively deep or infinite recursion.

第二个使用的递归深度小于第一个(每个调用使用n分支而不是n^2),因此它不太可能导致堆栈溢出。

(因此较低的复杂度意味着较少的导致堆栈溢出的机会)

但是有人必须补充为什么第二个永远不会导致堆栈溢出而第一个却可以。

资源

好吧,如果考虑这两种方法的复杂性,第一种方法显然比第二种方法复杂,因为它在LHS和RHS上都调用Recursion,因此有更多的机会出现堆栈溢出

注意:这并不意味着在第二种方法中绝对没有机会获得SO

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值