尾递归本身是不够的。带有 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) 的堆栈帧,并且在 smallslice 总是长度为 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) 成比例的某个数字,你将永远不会达到快速排序的 O(N) 最坏情况堆栈内存使用量,所以你技术上不需要这样做。进行这种优化(首先弹出较小的切片)仍然可以提高 O(log(N)) 的常数因子,因此强烈推荐。