付笑 发表于 2013-8-28 12:00:30

递归思想 (转载)

递归思想n和n-1的行为相同,且每个步骤都是可行的,就可以使用递归。
例如汉诺塔问题中的,移n个盘和移n-1所经过的步骤是一样的,并且移n个盘是以n-1个盘为条件的,由于移一个盘时是可行的,因此后面从一个盘开始叠加的步骤都是可行的。所以移n-1个盘也是可行的,所以移n个盘就是可行的。
递归关键是处理n和n-1的关系,尤其是共用参数的时候。
yidong($n, $one, $two, $three)这个函数表示n 从one 移到three的功能
相当于先n-1 个从one移到two,然后one移到three,最后是n-1 从two移到three
yidong($n-1, $one, $three, $two) one-three yidong($n-1, $two, $one, $three)
这是程序的表现形式
这是函数的表达式
这和数学表达式的关系有什么不同。
递归关键处理两个相邻的关系。
老实说,楼上的回答只能是误人子弟,令人越听越模糊,似乎递归算法是高深莫测的一样。其实递归很简单的,请看我的分析。
递归算法适用的两个条件:1.每个步骤的完成形式是一样的2.后一个步骤的完成情况依赖于前一个步骤的完成情况。
前提是:每个步骤都可执行的。
比如:
汉诺塔的递归算法:
移n个盘从a到c,相当于把n-1个先从a移到b,然后把1一个从a移到c,最后再把n-1个从b移到c.
那么我首先就要问,把n-1个先从a移到b是不是可行的?
先看前提,由于移动1个盘是可行的,移2个盘是依赖于移1个盘的,所以移2个盘也是可行的,移3个盘是依赖移2个盘,所以移3个盘也是可行的,移n个盘时,依赖于n-1个盘,如果层层分解那么最终将会转化为依赖1一个盘。所以把n-1个先从a移到b是可行的。
既然我确定把n-1个先从a移到b和n-1个从b移到c是可行的,那么我下面要做的就是找出两个相邻步骤的关系,就是说,找出一个能够表达二者关系的表达式。
假如 这个函数hanoi(int n,char one,char two,char three)表达把n个盘从a移到c的过程。
那么把n-1个先从a移到b就可以表示成hanoin_1(n-1,one,three,two ),a移到c printf("a--->c") ,把n-1个从b移到c可以表示成hanoi(n-1,two,one,three)
由于从1到n每个两个相邻的步骤都是这种关系,两种的依赖形式是:n个步骤
如果n为偶数的话:

hanoi(int n,char one,char two,char three){
hanoin_1(n-1,one,three,two );
move(one,three);
hanoin_1(n-1, $two, $one, $three );


}

而hanoin_1(n-1,one,three,two );里面又调用了hanoin_2(n-1,one,two,three);move(one,two);hanoin_2(n-1,three,one,two);
...hanoi1(1,one,three,two);move(one,two);hanoi1(n-1,three,one,two);
至于hanoi1函数的原型可以写成同样的形式hanoi1(int n,char one,char two,char three){
if(n == 1){
move(one,three);

}else{
hanoin_1(n-1,one,three,two );
move(one,three);
hanoin_1(n-1, $two, $one, $three );
}


}
既然n个函数的参数和结构的是一致的,那么可不可以用一个函数来代替上述的函数呢?
答案是可以的。为什么?因为所有编程语言都支持对这身的调用,如果不支持,那么它肯定是一个失败的语言。
递归的调用和普通函数的调用没什么区别,同样是为每个函数开辟一个新栈。
语言的设计者肯定是看到递归可以节省大量代码的这个优点而设计的。
所以说递归一点也不深奥,和普通函数的调用没有什么区别。如果没有那个终极条件,那么函数的嵌套将是无数层的,所以必须要有终极条件。
下面从应用的角度看递归函数。

牡丹花下死做鬼 发表于 2013-8-28 12:10:37

O(∩_∩)O~ 不错 O(∩_∩)O~

付笑 发表于 2013-8-28 12:14:55

牡丹花下死做鬼 发表于 2013-8-28 12:10 static/image/common/back.gif
O(∩_∩)O~ 不错 O(∩_∩)O~

呵呵,好文章转过来方便自己看

付笑 发表于 2013-8-28 14:18:37

付笑 发表于 2013-8-28 12:14 static/image/common/back.gif
呵呵,好文章转过来方便自己看

C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数。
   许多教科书都把计算机阶乘和菲波那契数列用来说明递归,非常不幸我们可爱的著名的老潭老师的《C语言程序设计》一书中就是从阶乘的计算开始的函数递归。导致读过这本经书的同学们,看到阶乘计算第一个想法就是递归。但是在阶乘的计算里,递归并没有提供任何优越之处。在菲波那契数列中,它的效率更是低的非常恐怖。

   这里有一个简单的程序,可用于说明递归。程序的目的是把一个整数从二进制形式转换为可打印的字符形式。例如:给出一个值4267,我们需要依次产生字符‘4’,‘2’,‘6’,和‘7’。就如在printf函数中使用了%d格式码,它就会执行类似处理。

   我们采用的策略是把这个值反复除以10,并打印各个余数。例如,4267除10的余数是7,但是我们不能直接打印这个余数。我们需要打印的是机器字符集中表示数字‘7’的值。在ASCII码中,字符‘7’的值是55,所以我们需要在余数上加上48来获得正确的字符,但是,使用字符常量而不是整型常量可以提高程序的可移植性。‘0’的ASCII码是48,所以我们用余数加上‘0’,所以有下面的关系:

          ‘0’+ 0 =‘0’
          ‘0’+ 1 =‘1’
          ‘0’+ 2 =‘2’
             ...

  从这些关系中,我们很容易看出在余数上加上‘0’就可以产生对应字符的代码。接着就打印出余数。下一步再取商的值,4267/10等于426。然后用这个值重复上述步骤。

  这种处理方法存在的唯一问题是它产生的数字次序正好相反,它们是逆向打印的。所以在我们的程序中使用递归来修正这个问题。

  我们这个程序中的函数是递归性质的,因为它包含了一个对自身的调用。乍一看,函数似乎永远不会终止。当函数调用时,它将调用自身,第2次调用还将调用自身,以此类推,似乎永远调用下去。这也是我们在刚接触递归时最想不明白的事情。但是,事实上并不会出现这种情况。

  这个程序的递归实现了某种类型的螺旋状while循环。while循环在循环体每次执行时必须取得某种进展,逐步迫近循环终止条件。递归函数也是如此,它在每次递归调用后必须越来越接近某种限制条件。当递归函数符合这个限制条件时,它便不在调用自身。

在程序中,递归函数的限制条件就是变量quotient为零。在每次递归调用之前,我们都把quotient除以10,所以每递归调用一次,它的值就越来越接近零。当它最终变成零时,递归便告终止。

/*接受一个整型值(无符号0,把它转换为字符并打印它,前导零被删除*/

#include <stdio.h>

int binary_to_ascii( unsigned int value)
{
          unsigned int quotient;
   
     quotient = value / 10;
     if( quotient != 0)
           binary_to_ascii( quotient);
     putchar ( value % 10 + '0' );
}

递归是如何帮助我们以正确的顺序打印这些字符呢?下面是这个函数的工作流程。
       1. 将参数值除以10
       2. 如果quotient的值为非零,调用binary-to-ascii打印quotient当前值的各位数字

  3. 接着,打印步骤1中除法运算的余数

  注意在第2个步骤中,我们需要打印的是quotient当前值的各位数字。我们所面临的问题和最初的问题完全相同,只是变量quotient的值变小了。我们用刚刚编写的函数(把整数转换为各个数字字符并打印出来)来解决这个问题。由于quotient的值越来越小,所以递归最终会终止。

  一旦你理解了递归,阅读递归函数最容易的方法不是纠缠于它的执行过程,而是相信递归函数会顺利完成它的任务。如果你的每个步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总是能正确的完成任务。

  但是,为了理解递归的工作原理,你需要追踪递归调用的执行过程,所以让我们来进行这项工作。追踪一个递归函数的执行过程的关键是理解函数中所声明的变量是如何存储的。当函数被调用时,它的变量的空间是创建于运行时堆栈上的。以前调用的函数的变量扔保留在堆栈上,但他们被新函数的变量所掩盖,因此是不能被访问的。

  当递归函数调用自身时,情况于是如此。每进行一次新的调用,都将创建一批变量,他们将掩盖递归函数前一次调用所创建的变量。当我追踪一个递归函数的执行过程时,必须把分数不同次调用的变量区分开来,以避免混淆。

  程序中的函数有两个变量:参数value和局部变量quotient。下面的一些图显示了堆栈的状态,当前可以访问的变量位于栈顶。所有其他调用的变量饰以灰色的阴影,表示他们不能被当前正在执行的函数访问。

假定我们以4267这个值调用递归函数。当函数刚开始执行时,堆栈的内容如下图所示:



执行除法之后,堆栈的内容如下:




接着,if语句判断出quotient的值非零,所以对该函数执行递归调用。当这个函数第二次被调用之初,堆栈的内容如下:



堆栈上创建了一批新的变量,隐藏了前面的那批变量,除非当前这次递归调用返回,否则他们是不能被访问的。再次执行除法运算之后,堆栈的内容如下:



quotient的值现在为42,仍然非零,所以需要继续执行递归调用,并再创建一批变量。在执行完这次调用的出发运算之后,堆栈的内容如下:



此时,quotient的值还是非零,仍然需要执行递归调用。在执行除法运算之后,堆栈的内容如下:




  不算递归调用语句本身,到目前为止所执行的语句只是除法运算以及对quotient的值进行测试。由于递归调用这些语句重复执行,所以它的效果类似循环:当quotient的值非零时,把它的值作为初始值重新开始循环。但是,递归调用将会保存一些信息(这点与循环不同),也就好是保存在堆栈中的变量值。这些信息很快就会变得非常重要。

  现在quotient的值变成了零,递归函数便不再调用自身,而是开始打印输出。然后函数返回,并开始销毁堆栈上的变量值。

每次调用putchar得到变量value的最后一个数字,方法是对value进行模10取余运算,其结果是一个0到9之间的整数。把它与字符常量‘0’相加,其结果便是对应于这个数字的ASCII字符,然后把这个字符打印出来。

   输出4:




接着函数返回,它的变量从堆栈中销毁。接着,递归函数的前一次调用重新继续执行,她所使用的是自己的变量,他们现在位于堆栈的顶部。因为它的value值是42,所以调用putchar后打印出来的数字是2。

输出42:




接着递归函数的这次调用也返回,它的变量也被销毁,此时位于堆栈顶部的是递归函数再前一次调用的变量。递归调用从这个位置继续执行,这次打印的数字是6。在这次调用返回之前,堆栈的内容如下:

输出426:




现在我们已经展开了整个递归过程,并回到该函数最初的调用。这次调用打印出数字7,也就是它的value参数除10的余数。

输出4267:




然后,这个递归函数就彻底返回到其他函数调用它的地点。
如果你把打印出来的字符一个接一个排在一起,出现在打印机或屏幕上,你将看到正确的值:4267

汉诺塔问题递归算法分析:


  一个庙里有三个柱子,第一个有64个盘子,从上往下盘子越来越大。要求庙里的老和尚把这64个盘子全部移动到第三个柱子上。移动的时候始终只能小盘子压着大盘子。而且每次只能移动一个。

  1、此时老和尚(后面我们叫他第一个和尚)觉得很难,所以他想:要是有一个人能把前63个盘子先移动到第二个柱子上,我再把最后一个盘子直接移动到第三个柱子,再让那个人把刚才的前63个盘子从第二个柱子上移动到第三个柱子上,我的任务就完成了,简单。所以他找了比他年轻的和尚(后面我们叫他第二个和尚),命令:

          ① 你丫把前63个盘子移动到第二柱子上

          ② 然后我自己把第64个盘子移动到第三个柱子上后


          ③ 你把前63个盘子移动到第三柱子上

      2、第二个和尚接了任务,也觉得很难,所以他也和第一个和尚一样想:要是有一个人能把前62个盘子先移动到第三个柱子上,我再把最后一个盘子直接移动到第二个柱子,再让那个人把刚才的前62个盘子从第三个柱子上移动到第三个柱子上,我的任务就完成了,简单。所以他也找了比他年轻的和尚(后面我们叫他第三和尚),命令:


          ① 你把前62个盘子移动到第三柱子上


          ② 然后我自己把第63个盘子移动到第二个柱子上后


          ③ 你把前62个盘子移动到第二柱子上


  3、第三个和尚接了任务,又把移动前61个盘子的任务依葫芦话瓢的交给了第四个和尚,等等递推下去,直到把任务交给了第64个和尚为止(估计第64个和尚很郁闷,没机会也命令下别人,因为到他这里盘子已经只有一个了)。

  4、到此任务下交完成,到各司其职完成的时候了。完成回推了:

第64个和尚移动第1个盘子,把它移开,然后第63个和尚移动他给自己分配的第2个盘子。
第64个和尚再把第1个盘子移动到第2个盘子上。到这里第64个和尚的任务完成,第63个和尚完成了第62个和尚交给他的任务的第一步。


  从上面可以看出,只有第64个和尚的任务完成了,第63个和尚的任务才能完成,只有第2个和尚----第64个和尚的任务完成后,第1个和尚的任务才能完成。这是一个典型的递归问题。 现在我们以有3个盘子来分析:

第1个和尚命令:

          ① 第2个和尚你先把第一柱子前2个盘子移动到第二柱子。(借助第三个柱子)

          ② 第1个和尚我自己把第一柱子最后的盘子移动到第三柱子。

          ③ 第2个和尚你把前2个盘子从第二柱子移动到第三柱子。


   很显然,第二步很容易实现(哎,人总是自私地,把简单留给自己,困难的给别人)。

其中第一步,第2个和尚他有2个盘子,他就命令:

          ① 第3个和尚你把第一柱子第1个盘子移动到第三柱子。(借助第二柱子)


          ② 第2个和尚我自己把第一柱子第2个盘子移动到第二柱子上。


          ③ 第3个和尚你把第1个盘子从第三柱子移动到第二柱子。

   同样,第二步很容易实现,但第3个和尚他只需要移动1个盘子,所以他也不用在下派任务了。(注意:这就是停止递归的条件,也叫边界值)

第三步可以分解为,第2个和尚还是有2个盘子,命令:


          ① 第3个和尚你把第二柱子上的第1个盘子移动到第一柱子。

          ② 第2个和尚我把第2个盘子从第二柱子移动到第三柱子。

          ③ 第3个和尚你把第一柱子上的盘子移动到第三柱子。
                  
分析组合起来就是:1→3 1→2 3→2 借助第三个柱子移动到第二个柱子 |1→3 自私人留给自己的活| 2→1 2→3 1→3借助第一个柱子移动到第三个柱子|共需要七步。

如果是4个盘子,则第一个和尚的命令中第1步和第3步各有3个盘子,所以各需要7步,共14步,再加上第1个和尚的1步,所以4个盘子总共需要移动7+1+7=15步,同样,5个盘子需要15+1+15=31步,6个盘子需要31+1+31=64步……由此可以知道,移动n个盘子需要(2的n次方)-1步。

   从上面整体综合分析可知把n个盘子从1座(相当第一柱子)移到3座(相当第三柱子):

(1)把1座上(n-1)个盘子借助3座移到2座。
   (2)把1座上第n个盘子移动3座。
(3)把2座上(n-1)个盘子借助1座移动3座。

下面用hanoi(n,a,b,c)表示把1座n个盘子借助2座移动到3座。

很明显:    (1)步上是 hanoi(n-1,1,3,2)
               (3)步上是 hanoi(n-1,2,1,3)
用C语言表示出来,就是:
#include <stdio.h>
int method(int n,char a, char b)
{
   printf("number..%d..form..%c..to..%c.."n",n,a,b);
   return 0;
}
int hanoi(int n,char a,char b,char c)
{
   if( n==1 ) move (1,a,c);
   else
          {
               hanoi(n-1,a,c,b);
               move(n,a,c);
               hanoi(n-1,b,a,c);
          };
   return 0;
}
int main()
{
   int num;
   scanf("%d",&num);
   hanoi(num,'A','B','C');
   return 0;
}

shi_1236 发表于 2013-9-2 21:41:26

{:1_1:}厉害{:1_1:}厉害{:1_1:}厉害

icecool 发表于 2013-9-24 09:09:17

{:7_148:} 顶你楼主

ylfeiu 发表于 2013-10-3 11:56:31

{:5_103:}好长啊   mark

回忆あ殇痛 发表于 2013-10-3 12:03:04

支持楼主。。

magicyuc 发表于 2013-10-8 10:41:11

学习一下哈

magicyuc 发表于 2013-10-8 17:05:25

学习一下啦

devotedtoc 发表于 2013-10-8 18:18:51

这期我们就在学这个,顺便研究一下吧

peng3726 发表于 2013-10-10 23:30:38


不错不错,学习了哈

风吹绿野 发表于 2013-11-16 23:48:16

好文章,楼主好人,谢谢分享

猪猪BBUn咕咕 发表于 2013-11-17 08:41:24

好文章转过来方便自己看

付笑 发表于 2014-2-11 23:57:27

本帖最后由 付笑 于 2014-2-12 17:26 编辑

图解是什么意思呀。
这个算法 那么简单没必要搞得那么复杂吧。
an = an-1 + 1;
你明白这个等式的意义吗?这个等式已经包含了递归算法的全部含义。
an 表示 n个数的和,an-1 表示n-1个数的和 ,an = an-1 + 1;
表示n个数的和可以通过n-1个数的和来求的。
上述说明哪些情况可以使用递归呢?
那就是:已知前一个步骤可以求得后一个步骤的结果的情况,并且前一个步骤和后一个步骤是有规律过度的。
比如汉诺塔问题:移n个盘是已移n-1个盘为条件的,两者的共同点是移盘。
所以可以用f(n)表示移n个盘,f(n-1)表示移n-1个盘,那么移n个盘和移n-1个盘有什么关系呢?

这就需要预先分析问题才能得出具体的关系在这个问题中,把n个盘从a移到c需要三个步骤来完成。
1.n-1个盘从a移到b2 1个盘从a移到c3 n-1个盘从b移到c已知n-1个盘从a移到b是可行的,为什么?

因为移1个盘是可行,那么移2个盘也是可行,移 3个盘是已移2个盘为条件的,所以移3个盘也是可行的,所以移n个 盘是可行的。

所以根据已知条件可以解得:设f(n, a, b,c) 表示 把n个盘从a移到c 借助b --------------------------这里很关键,这是搞懂递归的关键关键。
那么把n-1个盘从a移到b 借助c 怎样表示呢?很明显是:f(n-1, a, c,b)那么把1个盘从a移到c怎样表示呢?很明显是:f(1, a, b,c)

那么把n-1个盘从b移到c 借助a 怎样表示呢?很明显是:f(n-1, b, a,c)所以f(n, a, b,c) = ( f(n-1, a,c,b) , f(1, a, b,c), f(n-1, b,a,c))这和等差等比数列一个原理。
没有什么 特别的。记住是问题有这样递推关系才可以使用这种方法。

如果要你计算1+2+8+22 的结果 你就不能使用递归。

因为该问题的后一步骤与前一步骤不具有规律性,所以已知前一个步骤并不能求的后一个步骤的值1+2+3+4 ...+ 111111111111111111111111111111这个问题就可以使用递归原因你懂了吧。

至于爬楼梯问题,无限级分类 问题等一些递归问题,那不过时小菜一碟。
一句话:后一步骤依赖前一步骤并且二者联系具有规律性,运用递归必然成功。

7sDream 发表于 2014-2-12 00:02:22

想要理解递归,你首先要理解递归

yuzhouliu2000 发表于 2014-2-28 11:56:29

谢谢楼主分享……

枫界易城 发表于 2014-2-28 12:14:24

感谢楼主分享!

驻留的习惯 发表于 2014-3-15 19:39:50

{:2_25:}!!!!

Stduy_Student 发表于 2014-5-7 11:57:48

感谢楼主分享了
页: [1] 2
查看完整版本: 递归思想 (转载)