用类比方式学习编程中函数递归(个人理解仅供参考)(内含汉诺塔问题的求解)

Posted 摆烂小青菜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用类比方式学习编程中函数递归(个人理解仅供参考)(内含汉诺塔问题的求解)相关的知识,希望对你有一定的参考价值。

目录

1.前言

2.递归的数学模型

3.相关的c语法

4.将递归的数学模型写成编程语言

5.利用类比方法将实际问题的代码写成函数递归的形式

例1:

 例2:

6.汉诺塔问题的求解


1.前言

本人在学习函数递归编程方法的过程中,发现用类比的方式学习递归法可帮助我们在各种编程问题中将函数递归法运用于代码设计中。

在我看来,函数递归最底层的思维模型是:数学中的数列的递推公式(数列的递推公式是研究数列性质和求通项公式的核心,将递推迭代思维模型应用于代码设计中,就产生了函数递归).

2.递归的数学模型

现在我们任意给出的一个数列 An 的首项和它的递推公式:

A1 == 1, An ==  2*A(n-1) +  1 (n>1)

在数学中求解该数列的通项公式的方法有很多种,其中有一种叫做迭代法:(迭代法可以用于求解各种递推公式为一阶递推式的数列的通项) 

所谓迭代法就是将递推公式中的项按照递推公式本身逐项展开(即从An开始一直逐项递值展开到A1,然后通过计算返回出结果

接下来我们用迭代法来试求An的通项公式: (n>1)

An=2*A(n-1) + 1 = 2*(2*A(n-2) +1) +1 = 2*(2*(2*A(n-3)+1)+1) +1=................................

以这种方式一直展开到A1再通过归纳整理便可以得出An的通项公式.

3.相关的c语法

我们知道,计算机最擅长做的事情就是重复大量地进行同一形式的计算。然而在迭代法中,我们从An开始按照 同一个递推公式 将其逐项展开直到A1这种算法显然十分符合计算机的思维模式.在C语言中我们允许函数嵌套自身即:       

int  func(int n)    

    func(n); 
    return 0;

     

调用函数的时候 函数本身在返回输出之前会先调用自身并且这样的过程会不断重复进行

为了能够最终让函数返回输出,我们要对函数向内递值进行条件约束 并且每次向内传递的值都要逼近约束条件比如:

   


int func(int n) 

     if(n>=1)
      
        func(n-1);
     
     else
     
        return0;
     

 这样的话函数就会逐层向内递值n次后再逐层返回输出. 

4.将递归的数学模型写成编程语言

现在我们想让计算机帮我们求数列An的任意一项的值.要求设计一个函数int func(int)输入正整数n作为形参,函数便返回An的值.

思路引导:An的递推公式 An= 2*A(n-1)  + 1    

如果递推公式中An相当于代码中的 func(n),那么A(n-1)则相当于代码中的func(n-1)

结合迭代法求通项的思路和相关C语法便可写出如下代码

int func(n)

   if(n>1)                  //递推公式的成立条件
   
      return 2*func(n-1)+1; //数列的递推公式
    
   else
   
      return 1 ;            // 数列首项为以1
   

计算机在执行这段代码时就会自动帮助我们完成迭代的过程.(该过程中函数减值向内递值直到最后一次向内递入整数1【“递”过程】,再从最内层函数开始逐层向外返回数值【“归”过程】并最终得到结果)(递推公式决定函数递值的方式和函数结构,首项决定归值节点

进一步思考可以得知,任意给定一个数列,只要我们知道其首项和递推公式,我们就可以写出与上面代码形式相同的代码用于求任意项的数值.

5.利用类比方法将实际问题的代码写成函数递归的形式

例1:

问题:设计一个函数代替库函数strlen()  ,用于求字符串长度,形参是char*类型 返回值是int类型.

我们自定义函数取名叫做restrlen() 

假设我们要求字符串“bite”的长度. 创建字符串数组char arr[]="bite".

先抽象出一个数列的某一项 restrlen("bite")

我们可以得到递推关系 restrlen("bite") = restrlen("ite") + 1

鉴于题目要求,求“bite”长度时我们只能将arr即字母b的地址传入函数中

那么restrlen("bite")就相当于restrlen(arr),类似地,restrlen("ite")就相当于restrlen(arr+1)

于是递推关系就变成了 restrlen(arr)=restrlen(arr+1) +1  

该问题中首项是restrlen(arr+x) = 0 , x为某一整数 并且满足*(arr+x)="\\0"  于是我们便有了如下代码:

int restrlen(char* x)

    if(*(x+1)!=0)                  //类似于递推公式成立条件
    
         return restrlen(x+1) +1;  //类似于数列递推公式
    
    else
    
         return 0;                 //类似于首项
    

 例2:

问题:设计一个函数当输入一个整数时,函数会依次输出打印出该整数每一个十进制位上的数字如:输入123 打印1 2 3

我们给函数取名叫做 prt 以输入123为例子

要先打印出1 那么1肯定在最内层函数完成输出(归值从内而外)

先抽象出一个数列的项prt(123) 其前一项可以抽象为prt(12)

那么我们便可以抽象出其递推公式  prt(123)= prt(12) + "printf("%d",123 %10)

如果数列的中n相当于“123”,那么n-1则相当于“123/10”

当输入值整除10等于0时就得到“首项” (类比意义上) “首项”的值为0

由于函数输出方式为打印输出所以无需返回值(注意要先向内层递值再打印输出)

void Print(int n)

   if(n/10!=0)
   
       Print(n/10);
       printf("%d",n%10);
   

6.汉诺塔问题的求解

先从数学角度对汉诺塔问题进行分析:现在设 将A柱上n个圆盘借助B柱移动到C柱 需要移动圆盘的总次数为Xn.
显然我们可以得到数列Xn ,其中n>=1     接下来我们来研究该数列的递推公式 即尝试研究Xn与Xn-1的关系
从特殊到一般的方法对问题进行分析 我们可以分析出如下规律;

  1. 为了将A柱上n个圆盘借助B柱移动到C柱,我们必须经历这样一个步骤:将A柱上最大的圆盘移动要C柱上.
  2. 在完成1.分析中所述的步骤之前,A柱上必须只剩一个最大的圆盘,C柱必须的空的,此时B柱上则有n-1个圆盘
  3. 因此我们必须在游戏开始后先完成另外一个步骤:将A柱上n-1个圆盘借助C柱移动到B柱上.
  4. 我们称分析3.中所述步骤为:第一步骤(我们无需理会该步骤具体是如何进行的)称分析1.中所述步骤为:第二步骤
  5. 很显然若Xn代表n个圆盘的汉诺塔问题中需要移动圆盘的总次数,那么第一步骤需要移动圆盘的总次数为Xn-1次
  6. 同时,第二步骤需要移动圆盘的总次数为1次
  7. 在完成了第一步骤和第二步骤后我们便可以无视C柱上那个最大的圆盘接着分析下一步
  8. 接下来,我们只需 将B上的n-1个圆盘借助A柱移动到C柱 我们称这一步为 第三步骤(我们同样无需理会该步骤具体是如何进行的)
  9. 显然完成第三步骤所需移动圆盘的总次数同样为Xn-1 次

通过上述分析我们可以得到结论:为了解决n(n>1)个圆盘的汉诺塔问题我们需要经历如下三个步骤(设总移动圆盘的次数为Xn) (这里A为起始柱,B为过渡柱,C为目标柱)
第一步骤:将A柱上n - 1个圆盘借助C柱移动到B柱上(移动Xn-1次)(这里A为起始柱,C为过渡柱,B为目标柱)
第二步骤:将A柱上剩下的那个最大的圆盘移动要C柱上(移动1次)   
第三步骤:将B上的n - 1个圆盘借助A柱移动到C柱(移动Xn-1次)(这里B为起始柱,A为过渡柱,C为目标柱)
即得到数列的递推公式(n>1):      

          Xn       =     ( Xn-1 )   +   (1)   +   (Xn-1)
  /*移动圆盘的总次数*/    /*第一步骤*/ /*第二步骤*/   /*第三步骤*/

当n==1时显然我们只需执行一次第二步骤 即X1==1

利用上面的数学结论我们来尝试解决汉诺塔的c语言编程问题
编程的要求是:设计一个函数 输入形参n作为初始圆盘总数 函数(比如输入n==3时)以如下的形式打印输出(以这种形式告诉用户解决n个圆盘的汉诺塔问题的每个具体的圆盘移动步骤)

               A-->C        
               A-->B
               C-->B
               A-->C
               B-->A
               B-->C
               A-->B

根据数学递推公式的分析我们可以初步得出:汉诺塔函数中我们有两个减值向内层函数递值的步骤(“递”步骤) 中间还有一个移动单个圆盘的步骤
很显然中间那个移动单个圆盘的步骤可以作为我们函数递归的返回输出节点(即从最内层函数开始向外层函数返回输出的“归”步骤)
于是我们可以得到如下函数递归框架:

    void Hanoi(int n)     //总过程:将A柱上n个圆盘借助B柱移动到C柱
    
        Hanoi(n - 1);     /*第一步骤(函数向内递值 即“递”的步骤)*/
        printf() ;        /*第二步骤(该步骤具体实行打印输出 即"归"的步骤)*/
        Hanoi(n - 1);     /*第三步骤(函数再一次向内递值)*/
    

以上便是根据汉诺塔问题数学分析得出的基本递归框架
然而为了将 《最外层函数的“将A柱上n个圆盘借助B柱移动到C柱”过程》以及《第一个内层函数的“将A柱上n - 1个圆盘借助C柱移动到B柱上”过程》以及《第二个内层函数的“将B上的n - 1个圆盘借助A柱移动到C柱”过程》  三者区分开来
我们必须引入三个字符形参代表A B C三个柱子 并且通过这三个字符形参在函数小括号()中的排列顺序的区别来区分前句分析的三个过程
于是我们可以进一步填充函数递归框架 并给出函数向内递值的限制条件完成代码设计

int count = 0;
void Hanoii(char a, char b, char c, int n)          

    if (n >= 1)
      
        Hanoii(a, c, b, n - 1);                     
        count++;
        printf("%d.  %c-->%c\\n", count, a, c);      
        Hanoii(b, a, c, n - 1);                      
                                                    
    

int main()

    char x = 'A';
    char y = 'B';
    char z = 'C';
    int k = 0;
    scanf("%d", &k);
    Hanoii(x, y, z, k);
    return 0;


以上是关于用类比方式学习编程中函数递归(个人理解仅供参考)(内含汉诺塔问题的求解)的主要内容,如果未能解决你的问题,请参考以下文章

费曼学习法的浅显理解

从示例逐渐理解Scala尾递归

用递归函数计算从n个人中选择k个人组成一个委员会的不同组合数

通过动画轻松理解递归与动态规划

Linux C编程一站式学习笔记5

关于计算机编译原理