传统快排
void QSort(int arr[],int low,int high)
{
if(low < high)
{
int pivotPos = Partition(arr,low,high);
QSort(arr,low,pivotPos-1);
QSort(arr,pivotPos+1,high);
}
}
尾递归优化
尾递归详细介绍
尾递归概念:
如果一个函数中所有递归形式的调用都出现在函数的末尾,当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
尾递归原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
快排的递归函数和尾递归有些类似,但因为调用了两次自身,所以并不属于尾递归,也不会被编译器优化。
但是,我们可以手动将第二个递归改为迭代,对其进行优化,节省栈空间。
【TIP】两个调用函数位置可以颠倒,可以对任意一个进行优化,将其改为迭代。为了尽可能地节省递归栈,我们对比较两个区间的长度,对长度更长的进行优化,将其改为迭代。
伪代码:
TAIL-RECURSIVE-QUICKSORT'(A, p, r)
while p < r
q = PARTITION(A, p, r)
if (q - p < r - q)
TAIL-RECURSIVE-QUICKSORT'(A, p, q)
p = q + 1
else
TAIL-RECURSIVE-QUICKSORT'(A, q + 1, r)
r = q - 1
C++:
void QSort(int arr[],int low,int high)
{
while(low < high)
{
int mid = Partition(arr,low,high);
if(mid-low<high-mid){
QSort(arr,low,mid-1);
low=mid+1;
}
else{
QSort(arr,mid+1,high);
high=mid-1;
}
}
}
优点
1.减少栈深度
对于划分不平衡的情况,传统版本栈深度最多可以达到O(n),尾递归优化后,栈深度最多为O(lgn)。
例如:[1 2 3 4 5 6]最坏情况下 partion()函数 每次划分都选取末尾元素作为基准:6、5、4、3、2、1、0
未优化时:
QSort(arr,0,6)——>QSort(arr,0,5)——>QSort(arr,0,4)——>QSort(arr,0,3)——>QSort(arr,0,2)——>QSort(arr,0,1)—>QSort(arr,0,0)
箭头表示新开辟的栈,栈深度达到了O(n)。
优化后该情况下只需要O(1)栈空间,因为左区间的递归被循环取代了。
【TIP】网上的很多尾递归优化,固定优化右区间,对于上面的例子,仍然需要O(n)的栈深度。
2.栈深度减少了,因此大大增加了可排序的数据量,大大降低了爆栈的可能。