前段时间太忙,来不及整理自己的思路,最近陆续将自己前段时间遗留的问题都整理一下,首先来说下尾递归,这是上次跟某个面试官讨论如何优化堆排序,使得如果数组很大并且大致有序的情况下,如何避免栈溢出的问题。
首先说明问题吧,我们都知道,堆排序在最坏的情况下,算法复杂度即为O(n^2),如果我们用递归的话,则会导致栈的空间也为O(n^2),我们知道操作系统给我们程序的栈空间是有限的,比如x86计算机,在vs中堆栈大小默认为 1 MB(来自msdn) ,这样的话如果数组很大的话,则很容易造成栈溢出,这样程序就崩溃了,那么我们如何解决这个问题呢,有一个方法就是使用尾递归。
下面我们先说尾递归的概念(引自维基百科):
“在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这种情形下称该调用位置为尾位置。若这个函数在尾位置调用本身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归,是递归的一种特殊情形。”
根据这个定义,我们很容易写出来尾递归的程序,比如求最简单的n的阶乘,我们有如下代码,左边为普通递归方式,右边为尾递归方式:
int factorial(int n){ if ( n <= 1) return 1; else return n*factorial(n-1); } | int tailFactorial(int n, int result){ if (n <= 1) return result; else return tailFactorial(n-1,n*result); } |
我们从上面的程序可以看出,我们以计算5的阶乘为例进行说明:
factorial(5) {5 * factorial(4)} {5 * {4 *factorial(3)}} {5 * {4 * {3 *factorial(2}}} {5 * {4 * {3 * {2 *factorial()}}}} {5 * {4 * {3 * {2 * 1}}}} {5 * {4 * {3 * 2}}} {5 * {4 * 6}} {5 * 24} 120 | tailFactorial(5, 1) tailFactorial(4, 5) tailFactorial(3, 20) tailFactorial(2, 60) tailFactorial(1, 120) 120 |
我们可以看到普通的递归需要将程序运行的状态保存然后一步步返回,但是使用尾递归则不需要,每次函数调用并不需要增加栈的深度,因为在调用这个函数的时候,函数内没有需要保存的变量与状态,所有的状态我们都保存在了函数的参数里,这样如果编译器支持这种调用的时候,就会不改变栈的空间,只是将参数信息改变,这样就保证了栈空间为O(1)。
如果想要了解更多关于尾递归的内容,可以参照老赵的博客
那么下面,对于快排我们如何使用尾递归进行优化从而使得栈空间缩小呢,首先我们来看算法导论上快排程序:
int swap(int & a, int &b){
int temp = a;
a = b;
b = temp;
}
int partion(int *array, int a, int b){
int i = a-1;
int key = array[b];
for ( j = a; j < b; j++){
if(array[j] < key){
i++;
swap(array[i],array[j]);
}
}
i++;
swap(array[i],array[b]);
return i;
}
int qsort(int *array ,int low, int high){
if(low < high){
int mid = partion(array, low, high);
qsort(array,low,mid-1);
qsort(array,mid+1,high);
}
}
我们可以明显看到,虽然函数最后一次是调用了自己,但是在之前却还调用了一次递归,这样就有问题,栈的深度还是会存在的那么我们如何改呢,我们可以通过优先调用较短侧的递归使得我们的函数编程尾递归,如下:
int tailQsort(int * array, int low, int high){
int mid;
while (low < high){
mid = Partition(list,low,high);
if (mid - low < high - mid)
{
tailQsort(list,low,mid - 1);
low = mid + 1;
}
else
{
tailQsort(list,mid + 1,high);
high = mid - 1;
}
}
}
当我们将if改成while后,因为第一次递归以后,变量low就没有用处了,所以可以将mid+1赋值给low,再循环后,来一次partition(array,low,high),其效果等同于“qsort(array,mid+1,high);”结果相同。但因采用迭代而不是递归的方法可以缩减堆栈深度,从而提高了整体性能。