怎样写出一个递归程序

    做为小白,我看到递归程序只是能看懂,可是本身写不出来,我知道要有一个临界条件(这个并不难找),但我不知道怎么演进,这让我十分头疼,所以找到了一篇我的认为写的不错的文章以下,根据我对递归的理解和疑问对原文作了一些标注,欢迎各位大佬,写下本身对递归的理解,本小白感激涕零。算法

如何写一个递归程序编程

  老是听到大大们说递归递归的,本身写程序的时候却用不到递归。其中的缘由,一个是惧怕写递归,另外一个就是不知道何时用递归。这篇文章就浅析一下,但愿看完以后再也不惧怕递归,这就是本文最大的目的。
  递归到底有什么意义?
  在说怎么写递归以前必需要说一下它的意义,其实这就是为何大多数人在看了许多递归的例子后仍是不明因此的缘由。能够确定的是,递归是个十分强大的工具,有许多算法若是不用递归可能很是难写。不少地方介绍递归会用阶乘或者斐波那契数列做例子,这彻底是在误导初学者。尽管用递归实现阶乘或者斐波那契数列是能够的,可是这是没有意义的。
数组

  先掉一下书袋,递归的定义是这样的:程序调用自身的编程技巧称为递归( recursion)。在函数调用的过程当中是有一个叫函数调用栈的东西存在的。调用一个函数,首先要把原函数的局部变量等压入栈中,这是为了保护现场,保证调用函数完成后可以顺利返回继续运行下去。当调用函数返回时,又要将这些局部变量等从栈中弹出。在普通的函数调用中,通常调用深度最多不过十几层,可是来到了递归的世界状况就不同了。先看一段随便从网上就能找到的阶乘程序:函数

double fab(int n) 
{ 
   if(n == 0 || n == 1){ 
     return 1; 
   }else{ 
     return n*fab(n-1); 
   } 
} 
工具

       若是n = 100,很显然这段程序须要递归地调用自身100次。这样调用深度至少就到了100。栈的大小是有限的,当n变的更大时,有朝一日总会使得栈溢出,从而程序崩溃。除此以外,每次函数调用的开销会致使程序变慢。因此说这段程序十分很差。那什么是好的递归,先给出一个结论,接着看下去天然会明白。结论是若是递归可以将问题的规模缩小,那就是好的递归
  怎样才算是规模缩小了呢。举个例子,好比要在一个有序数组中查找一个数,最简单直观的算法就是从头至尾遍历一遍数组,这样必定能够找到那个数。若是数组的大小是N,那么咱们最坏状况下须要比较N次,因此这个算法的复杂度记为O(N)。有一个大名鼎鼎的算法叫二分法,它的表达也很简单,因为数组是有序的,那么找的时候就从数组的中间开始找,若是要找的数比中间的数大,那么接着查找数组的后半部分(若是是升序的话),以此类推,知道最后找到咱们要找的数。稍微思考一下能够发现,若是数组的大小是N,那么最坏状况下咱们须要比较logN次(计算机世界中log的底几乎老是2),因此这个算法的复杂度为O(logN)。当N变大后,logN $amp;
  简单的分析一下二分法为何会快。能够发现二分法在每次比较以后都帮咱们排除了一半的错误答案,接下去的一次只须要搜索剩下的一半,这就是说问题的规模缩小了一半。而在直观的算法中,每次比较后最多排除了一个错误的答案,问题的规模几乎没有缩小(仅仅减小了1)。这样的递归就稍微像样点了。
  从新看阶乘的递归,每次递归后问题并无本质上的减少(仅仅减少1),这和简单的循环没有区别,但循环没有函数调用的开销,也不会致使栈溢出。因此结论是若是仅仅用递归来达到循环的效果,那仍是改用循环吧。
  总结一下,递归的意义就在于将问题的规模缩小,而且缩小后问题并无发生变化(二分法中,缩小后依然是从数组中寻找某一个数),这样就能够继续调用自身来完成接下来的任务。咱们不用写很长的程序,就能获得一个十分优雅快速的实现。
google


  怎么写递归程序?
  终于进入正题了。不少初学者都对递归心存畏惧,其实递归是符合人思考方式的。写递归程序是有套路的,总的来讲递归程序有几条法则的。
  用二分查找做为例子,先给出函数原型:
编码

                 int binary_search(int* array, int start, int end, int num_wanted)
spa

返回值是元素在数组中的位置,若是查找失败返回-1。.net


  1. 基准状况
  基准状况其实就是递归的终止条件。其实在实际中,这是十分容易肯定的。例如在二分查找中,终止条件就是找到了咱们想要的数或者搜索完了整个数组(查找失败)。
if(end < start){ 
   return -1; 
}else if(num_wanted == array[middle]){ 
   return middle; 
} 
设计

2. 不断演进
   演进的过程就是咱们思考的过程,二分查找中,就是继续查找剩下的一半数组。
if(num_wanted > array[middle]){ 
   index = binary_search(array, middle+1, end, num_wanted); 
}else{ 
   index = binary_search(array, start, middle-1, num_wanted); 
} 

固然这是比较简单的演进方式,其余的好比快速排序、树、堆的相关算法中会出现更复杂一点的演进过程(其实也复杂不到哪里去)。


  3. 用人的思考方式设计
  这条法则我认为是很是重要的,它不会出如今编码中,但倒是理解递归的一条捷径。它的意思是说,在通常的编程实践中,咱们一般须要用大脑模拟电脑执行每一条语句,从而肯定编码的正确性,然而在递归编码中这是不须要的。递归编码的过程当中,只须要知道前两条法则就够了。以后咱们就会看到这条法则的如何工做的了。


  4. 不要作重复的事情
  在任何编码中,这都是不该该出现的事情,可是在递归中这更加可怕,可能因为一次多余的递归使得算法增长数级复杂度。以后也会看到相关的例子。
  如今咱们能够写出咱们完整的二分法的程序了

int binary_search(int* array, int start, int end, int num_wanted) 
{ 
  int middle = (end - start)/2 + start; // 1
  if(end < start){ 
    return -1; 
  }else if(num_wanted == array[middle]){ 
    return middle; 
  } 
  int index; 
  if(num_wanted > array[middle]){ 
    index = binary_search(array, middle+1, end, num_wanted); // 2
  }else{ 
    index = binary_search(array, start, middle-1, num_wanted); // 3
  } 
  return index; // 4
} 

程序中除了1和4都已经在前两条法则的实现中了。1没必要多说,4是一个比较关键的步骤,常常容易被忘记。这里就用到第3条法则,

编写的时候只要认为2或者3必定会正确运行,而且马上返回,不要考虑2和3内部是如何运行的,由于这就是你如今在编写的。

这样4该如何处理就是显而易见的了,在这里只须要将找到的index返回就能够了。
  第4条法则在这个例子里并无出现,咱们能够看一下斐波那契数列的递归实现

long int fib(int n) 
{ 
   if(n <= 1){ 
     return 1; 
   }else{ 
     return fib(n-1) + fib(n-2); // 1
   } 
} 

乍看之下,这段程序很精练,它也是一段正确的递归程序,有基准条件、不断推动。可是若是仔细分析一下它的复杂度能够发现,

若是咱们取n=N,那么每次fib调用会增长额外的2次fib调用(在1处),即fib的运行时间T(N) = T(N-1) + T(N-2),能够获得其复杂度是

O(2^N),几乎是可见的复杂度最大的程序了(其中详细的计算各位有兴趣能够google一下,这里就不展开了^_^)。因此若是在一个

递归程序中重复屡次地调用自身,又不缩小问题的规模,一般不是个好主意。
  PS. 你们能够比较一下二分法与斐波那契数列的递归实现的区别,尽管二分法也出现了2次调用自身,可是每次运行只有其中一个会被真正执行。


  到此其实你已经能够写出任何一个完整的递归程序了,虽然上面的例子比较简单,可是方法老是这样的。不过咱们能够对递归程序再进一步分析。二分查找的递归算法中咱们注意到在递归调用以后仅仅是返回了其返回值,这样的递归称做尾递归。尽管在编写的时候没必要考虑递归的调用顺序,但真正运行的时候,递归的函数调用过程能够分为递和归两部分。在递归调用以前的部分称做递,调用以后的部分称做归。而尾递归在归的过程当中实际上不作任何事情,对于这种状况能够很方便的将这个递归程序转化为非递归程序(好处就是不会致使栈的溢出)。

转自:http://blog.csdn.net/u010697982/article/details/45875913

相关文章
相关标签/搜索