10. 递归与分治思想

1. 递归

1.1 什么是递归

  所谓的递归就是在求解的过程中,函数在不停地自己调用自己解决问题。但是在实际的变成过程中能用迭代操作的就不要用递归进行操作(比如说 for 循环就是一种常见的递归方式),以为循环相当于知根知底,我们知道程序会在什么样的情况下停止,但是递归不能。所以有的时候我们也可以将本应该应用递归求解的过程转化为用迭代进行求解。

  在高级语言中,函数自己调用和调用其他函数并没有本质的不同。我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。不过,写递归程序最怕的就是陷入永不结束的无穷递归中。切记,每个递归定义必须至少有一个条件,当满足这个条件时递归不再进行,即函数不再调用自身而是返回值。

  使用递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但大量的递归调用会建立函数的副本,会消耗大量的时间和内存,而迭代则不需要此种付出。递归函数分为调用和回退阶段,递归的回退顺序是它调用顺序的逆序。

1.2 斐波那契数列

  兔子数列是递归思想的一种典型的应用,如下图所示

《10. 递归与分治思想》

1.2.1 使用迭代方法实现

  使用迭代的方法的源代码如下,代码的过程就是按照上面的说明顺序执行的,浅显易懂

#include <stdio.h>

int main()
{
    int i;
    int a[40];

    a[0] = 0;
    a[1] = 1;
    printf("%d %d ", a[0], a[1]);

    for( i=2; i < 40; i++ )
    {
        a[i] = a[i-1] + a[i-2];
        printf("%d ", a[i]);
    }

    return 0;
}

1.2.2 使用递归方法实现

  使用递归实现的代码如下

#include <stdio.h>

int Fib(int i)
{
    if( i < 2 )
    {
        return i == 0 ? 0 : 1;     //判断输入的数字是否为零,为零的话返回0,不为零的话返回1
    }

    return Fib(i-1) + Fib(i-2);    //递归的过程,一直在自己调用自己
}

int main()
{
    int i;
    for( i=0; i < 40; i++ )
    {
        printf("%d ", Fib(i));
    }

    return 0;
}

在上述的过程中,mian 函数的功能十分简单就是在不停地打印输出,直至输出结果的数量满足题目的要求。具体过程如下图所示

《10. 递归与分治思想》

所以递归的过程就是将求解 FIib(5) 的过程转变为求解下面这一堆 FIib(0) 和 FIib(1) 的和的问题。

1.3 阶乘的递归实现

  阶乘的数学表达形式如下

《10. 递归与分治思想》

采用递归实现的代码如下所示

int  factorial( n )
{
    if( 0 == n )    return 1;
    else   return  n * factorial( n - 1 );
}

实际上可以看到,这里不停地调用自己的过程与数学表达式中的表达过程如出一辙,这也就是所说的“使用递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间”。

《10. 递归与分治思想》

注意在这里调用的顺序与回退的顺序是相反的。

1.4 字符串反向输出

  编写一个递归函数,实现将输入的任意长度的字符串反向输出的功能。例如输入字符串FishC,则输出字符串ChsiF。递归它需要有一个结束的条件,那么我们可以将“#”作为一个输入结束的条件。

void print()
{
    char a;
    scanf(“%c”, &a);
    if( a !=‘#’)
        print();
    if( a !=‘#’)
        printf(“%c”, a);
}

调用与退回的顺序如下所示

《10. 递归与分治思想》

  当输入不是“#”的时候,它会一直调用自身的 print () 函数;当输入是“#”的时候,不执行print();,也不执行printf(“%c”, a);,但是这个时候就开始了回退了,它会首先打印输出 C ,接着打印输出 B A。

2. 分治思想

  分而治之的思想古已有之,秦灭六国,统一天下正是采取各个击破、分而治之的原则。而分治思想在算法设计中也是非常常见的,当一个问题规模较大且不易求解的时候,就可以考虑将问题分成几个小的模块,逐一解决。分治思想和递归算是有亲兄弟的关系了,因为采用分治思想处理问题,其各个小模块通常具有与大问题相同的结构,这种特性也使递归技术有了用武之地。

  折半查找法是一种常用的查找方法,该方法通过不断缩小一半查找的范围,直到达到目的,所以效率比较高。从算法的折半查找的过程我们不难看出,这实际上也是一个递归的过程:因为每次都将问题的规模减小至原来的一半,而缩小后的子问题和原问题类型保持一致。

2.1 汉诺塔问题

  一位法国数学家曾编写过一个印度的古老传说:在世界中心贝拿勒斯的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。

  这其实也是一个经典的递归问题。我们可以做这样的考虑:先将前63个盘子移动到Y上,确保大盘在小盘下;再将最底下的第64个盘子移动到Z上;最后将Y上的63个盘子移动到Z上。这样子看上去问题就简单一点了,但是关键在于第1步和第3步应该如何执行呢?

  在游戏中,我们发现由于每次只能移动一个圆盘,所以在移动的过程中显然要借助另外一根针才行。也就是说第1步将1~63个盘子借助Z移到Y上,第3步将Y针上的63个盘子借助X移到Z针上。那么我们把所有新的路聚集为以下两个问题:问题一:将X上的63个盘子借助Z移到Y上;问题二:将Y上的63个盘子借助X移到Z上。解决上述两个问题依然用相同的方法:

  问题一的圆盘移动步骤为:
  先将前62个盘子移动到Z上,确保大盘在小盘下;再将最底下的第63个盘子移动到Y上;最后将Z上的62个盘子移动到Y上。
问题二的圆盘移动步骤为:
  先将前62个盘子移动到X上,确保大盘在小盘下;再将最底下的第63个盘子移动到Z上;最后将X上的62个盘子移动到Y上。

  所以上面的整体的过程相当于一个递归过程。

#include <stdio.h>

// 将 n 个盘子从 x 借助 y 移动到 z
void move(int n, char x, char y, char z)
{
    if( 1 == n )
    {
        printf("%c-->%c\n", x, z);
    }
    else
    {
        move(n-1, x, z, y);             // 将 n-1 个盘子从 x 借助 z 移到 y 上
        printf("%c-->%c\n", x, z);      // 将 第 n 个盘子从 x 移到 z 上
        move(n-1, y, x, z);             // 将 n-1 个盘子从 y 借助 x 移到 z 上
    }
}

int main()
{
    int n;

    printf("请输入汉诺塔的层数: ");
    scanf("%d", &n);
    printf("移动的步骤如下: \n");
    move(n, 'X', 'Y', 'Z');

    return 0;
}
    原文作者:递归与分治算法
    原文地址: https://blog.csdn.net/dugudaibo/article/details/79211901
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞