你所能用到的数据结构(四)
五、如何递,怎样归?
很多人看完递归的原理之后会有这种感觉,喔,这个原理我懂了,然后再找一道其余的题目看一看能不能写的出来,突然发现,我勒个去,还是不会。其实这种现象很普遍,所以如果你是这种的,也没有什么好沮丧的,我敢保证你能看的懂递归的执行过程,基本上已经比30%的人要强了。所以我觉得,我写一写我对递归思维的理解好了。递归这个词我的理解应该是传递和回归,如何把自身的状态传递下去和如何回归到一个结果上是递归问题的基本思维方式。
所谓如何传递,我觉得思维的难点是如何抽象出数学模型,如果是斐波那契数列那种有明确公式的话,很简单,直接按照公式该怎么操作怎么操作,难得是只有叙述性语言的,比如这种题目:有一段楼梯n个阶梯,你可以选择一次上一个阶梯,也可以选择一次上两个阶梯,请问走到顶部一共有多少种走法?看似很高深吧?其实这就是斐波那契数列的一个变体而已。这种描述性的题目如果要抽象出数学模型,我觉得最好的办法就先列举几个试试,先看看有什么规律没有,然后再猜想,再证明。你先看看你上2层楼梯有几种方法,2层楼梯要么是1次性上去,要么分成两步,一次性上一步,于是就是F(2)=2,如果只有一层和没有呢,那明显只有一种走法(一次上一层和不走),也就是F(0)=1,F(1)=1,下面,你要上第三层,你的办法要么是从第二层上一层到第三层,要么是在第一层上两层到第三层,要么一层一层的走上去,这样F(3)=3,看起来还是没有什么规律,接着往下来,现在要上第四层了,那么让我们换一种思维方式,怎样到第四层呢?要么你在第三层到第四层,要么从第二层到第四层,为什么不说从第一层到第四层呢?因为如果你把这个当作一种情况的话,你会发现在第一层的时候,无论下一步你怎样做都会回到上面两种情况之中。所以到第四层的作法就是F(3)+F(2),因为你到了第三层或者到了第二层(如果你在第二层选择上一层那么就会和在第三层的走法重合),后面的走法就确定了,不同的是前面的走法,也就是F(4)=5,现在让我们增加点难度,如果你要到第n层,那么应该说最后一步你有可能是从第n-1层走两层上来的,也有可能是从第n层走两层上来的,也就是说到第n层的走法决定于你怎么走到第n-1层和第n层的,所以这个走法应该是F(n)=F(n-1)+F(n-2)。
还有一种不知道如何传递是不知道怎样将递归算法转换成程序,你知道怎样用语言描述出递归,但是就是不知道怎样用程序描写出来,所以最好的方式是找一段递归的程序,然后看他每一次递归的输出。
关于如何归,就是要找到递归中止的条件,比如斐波那契数列那个,n=0就是数列的中止条件,别小看这个中止条件,如果不能找出这个中止条件或者定义错误的话,后果就是无限的递归,导致程序堆栈的崩溃,最终整个程序就很快的崩溃掉了。
我们从一个简单的开始,使用递归算法求最大公约数,利用辗转相除法,简单的说就是对于两个数m和n,利用公式gcd(m,n)=gcd(n,m%n)=
gcd(m%n,m%n%n),直到后面的余数为0为止,这是个有数学公式的比较明显的递归模式,所以按照这个数学公式的逻辑,这个递归算法的回归的话n==0的时候,所以这个算法很容易写出来。
代码相当的简单,思路要很清晰。那么,再来看一个二分搜索的好了,二分搜索是在已经排序好的数列里面寻找目标数,比如{1,2,...,10},这种,如果是寻找2,那么先求出这一组数的中值5,2比5小,从而转到0-5这个部分,其中值是2,然后就找到了。这种搜索的过程也是一种不断传递的过程,将某个数列的中值和要查找的目标值比较,如果比它小,就在数列的后半部分做同样的操作,如果比它大,就在前半部分做相同的操作。那么这个回归的条件是什么呢?应该说有两个,一个是找到了,也就是某一个中值等于目标值,一个是没有找到,可以定义为找到了第一个元素和最后一个元素还是没有找到,那么也结束递归,其代码如下:
1 int BinarySearch(int a[],int n,int low,int high) 2 { 3 int mid=(low+high)/2; 4 if(n==a[mid]) 5 return mid; 6 if((mid==high||mid==low)&&n!=a[mid]) 7 return 0; 8 9 if(n>a[mid]) 10 BinarySearch(a,n,mid+1,high); 11 else 12 BinarySearch(a,n,low,mid); 13 14 }
通过代码可以看到思路和我上面语言描述的基本是一致的,这就是递归的好处,可以使得代码更加清晰。
六、“高帅富”的装备
如果你看过一些时间复杂度可以到O(NLOGN)的排序算法,可以看到它们不仅效率高,代码也很简洁,因为使用递归使得很多复杂的过程变得简单,使得某个算法可以更容易的实现出来,我先要说的是归并排序。
归并排序简单的将就是将一个数列不断的平均分为两个小数列,然后每个小数列独立排序之后再合并在一起排序,也就是用若干有序的子序列得到一个有序的数列,为了说明,还是用一个例子好了,就用百度的这个例子好了:
如 设有数列{6,202,100,301,38,8,1}
初始状态: [6] [202] [100] [301] [38] [8] [1]
i=1 [6 202 ] [ 100 301] [ 8 38] [ 1 ]
i=2 [ 6 100 202 301 ] [ 1 8 38 ]
i=3 [ 1 6 8 38 100 202 301 ]
整个过程就是不断的划分为子序列,不断的用子序列排序,这明显是一个递归的过程,传递的过程是不断传递子序列,那么回归条件是什么呢?貌似这里不太能够看出来,从上面的过程可以大概看出来如果当数列的个数只有1的话,那么就要开始回归了,所以我们采用了一个方法,既然需要找中间的那个值,那么就要保存左边的索引和右边的索引,利用这两个索引,可以确定出数组中有多少个值,那么先看一下代码吧。
1 void MergeSort(int numbers[],int array_size) 2 { 3 int* tmpArray =new int[array_size-1]; 4 MergeSort(numbers,tmpArray,0,array_size-1); 5 6 } 7 8 void MergeSort(int numbers[],int tmpArray[],int left,int right) 9 { 10 if(left<right) 11 { 12 int center=(left+right)/2; 13 cout<<"0"<<endl; 14 MergeSort(numbers,tmpArray,left,center); 15 cout<<"1"<<endl; 16 MergeSort(numbers,tmpArray,center+1,right); 17 cout<<"2"<<endl; 18 Merge(numbers,tmpArray,left,center+1,right); 19 cout<<"3"<<endl; 20 } 21 } 22 23 void Merge(int numbers[],int tmpArray[],int leftPos,int rightPos,int rightEnd) 24 { 25 int leftEnd=rightPos-1; 26 int tmpPos=leftPos; 27 int numElements=rightEnd-leftPos+1; 28 29 while(leftPos<=leftEnd&&rightPos<=rightEnd) 30 { 31 if(numbers[leftPos]<=numbers[rightPos]) 32 tmpArray[tmpPos++]=numbers[leftPos++]; 33 else 34 tmpArray[tmpPos++]=numbers[rightPos++]; 35 } 36 while(leftPos<=leftEnd) 37 tmpArray[tmpPos++]=numbers[leftPos++]; 38 while(rightPos<=rightEnd) 39 tmpArray[tmpPos++]=numbers[rightPos++]; 40 41 for(int i=0;i<numElements;i++,rightEnd--) 42 numbers[rightEnd]=tmpArray[rightEnd]; 43 44 for (int i = 0; i < numElements; i++){ 45 cout<<numbers[i]<<" "; 46 } 47 cout<<endl; 48 }
代码开始有些复杂了,真正有点算法的感觉了,先看看三个函数,第一个函数没有什么特别的含义,只是屏蔽掉一些细节而已,从第二个MergeSort开始,可以看到就像我们描述的思路那样,第一个是比较是否数组里只有一个值,需要回归啦,然后求出中值,左边的排序成有序的子序列,然后排序右边的,最后将两个子序列合并起来,是不是思路特别的清晰?那么接下来看看Merge函数,如果有两个有序的子序列如何将他们合并成一个?因为这两个子序列都是有序的,记为子序列A和子序列B,A[0]到A[size-1]是有序的,B[0]到B[size-1]也是有序的,那么对比的过程就简单了,不断的对比不断的合并就可以了。
对于函数执行的递归过程,肯定很多人还是一头雾水,这很正常,毕竟没有一个清晰的直观的认识,那么让我们看看递归的每一步都是怎么走的吧。
对于百度给的那个例子,从头看起,我们调用的代码是 MergeSort(a,7); 所以,
1. left最开始等于0,right等于6,进入MergeSort以后left<right,进入if,输出0,center=3,调用 MergeSort(numbers,tmpArray,left,center);
1.1 left等于0,right等于center等于3,依然满足left<right,进入if,输出0,center=1,继续调用 MergeSort(numbers,tmpArray,left,center);
1.2 left等于0,right等于center等于1,依然满足left<right,进入if,输出0,center=0,继续调用 MergeSort(numbers,tmpArray,left,center);
1.3 left等于0,right等于center等于0,不满足left<right,掉回1.2往下执行,到此步,输出了三个0
1.2.1 left等于0,right等于center等于1,执行MergeSort(numbers,tmpArray,left,center);下一行语句,输出1,执行 MergeSort(numbers,tmpArray,center+1,right);
1.2.2 left等于center+1等于1,right=1,不满足left<right,回到1.2.1,执行MergeSort(numbers,tmpArray,center+1,right);下面的语句,输出2,然后进入
Merge(numbers,tmpArray,left,center+1,right);排序完成之后输出3。
以上是一次完整的递归过程,对着输出可以看到这个过程的执行,作为理解递归的练习,完全可以对照着后面的输出熟悉递归的过程,对于递归的执行,我觉得可以理解为执行到调用自己的函数的时候就不断的困在自己的这个函数中,直到到达某一个条件时,被自己释放,回到上一个过程才能进行,这个过程就像有的人失恋了,每天都和自己纠结,每天都在痛苦和不安中度过,这个过程中他是不能往下走的,一旦到某一个条件,比如时间慢慢的冲淡了感觉,他又可以继续进行了。