算法基础3 —— 递归(下) 杨辉三角 + 欧几里得算法 + 汉诺塔 + 递归与数组

例1 递归求解杨辉三角

在这里插入图片描述

(如果想打印出杨辉三角,可以开一个二维数组,将每行每列的数值存入数组之中。本题使用递归求杨辉三角中的数值,可以不开数组,但是同样需要利用二位数组的思想来解题。)

求解第一步:先将上图的杨辉三角向左旋转,并建立坐标,如下图所示:
在这里插入图片描述

设delta(x,y)表示二维数组中第x行第y列所对应的值。由上图不难发现

  • 当x == y的时候,delta(x,y) = 1; 即delta(0,0) = delta(1,1) = delta(2,2) = delta(3,3) = delta(4,4) = delta(5,5) = 1
  • 当y == 0的时候,即上图中的第一列全为1
  • 其余的数字等于该数字左上角和其正上方的数字之和

由此推出杨辉三角的递归式:

if (x == y || y == 0) delta(x,y) = 1;
else delta (x,y) = delta(x - 1,y - 1) + delta(x - 1,y);

代码如下:

#include <iostream>

using namespace std;

int delta(int x,int y)
{
    if (x == y || y == 0) return 1;
    else return delta(x - 1,y - 1) + delta(x - 1,y);
}

int main()
{
    cout << delta(4,0) << endl;//1
    cout << delta(4,1) << endl;//4
    cout << delta(4,2) << endl;//6
    cout << delta(4,3) << endl;//4
    return 0;
}

思考题1:下列代码的执行结果是?
【答案】01234

#include <iostream>

using namespace std;

void f(int  x)
{
    if(x > 0) f(x - 1);
    cout << x;    
}

int main()
{
    f(4);
    return 0;
}

思考题2:下列代码的执行结果是?
【答案】43210

#include <iostream>

using namespace std;

void f(int  x)
{
    cout << x;
    if(x > 0) f(x - 1);
}

int main()
{
    f(4);
    return 0;
}

思考题3:下列代码的执行结果是?
【答案】4321001234

#include <iostream>

using namespace std;

void f(int  x)
{
    cout << x;
    if(x > 0) f(x - 1);
    cout << x;
}

int main()
{
    f(4);
    return 0;
}
递归求两个数的最大公约数

最大公约数的定义:两个数中大家都能相约且最大的数

辗转相除法
  • 辗转相除法又名欧几里得算法(Euclidean algorithm)
  • 作用:求出两个正整数的最大公约数
  • 历史意义:是最古老的算法,可追溯至公元前300年前
  • 辗转相除法基于一个定理:两个正整数 a 和 b(a 大于 b),它们的最大公约数等于 a 除以 b 的余数 c 和 较小数 b 之间的最大公约数。即gcd(a,b) = gcd(a % b,b)
  • 例如:求48和36的最大公约数,需要先计算出48和36的余数(48 % 36 = 12),根据定理可以得到,48和36的最大公约数等于12和36的最大公约数。也就是gcd(48,36) = gcd(36,12)。同理,接着求36和12的最大公约数,需要先计算出36和12的余数(36 % 12 = 0),当余数为0的时候,此时的b = 12就是48和36的最大公约数。
  • 再例如,按照下图所示的过程,可以求出72与56的最大公约数

在这里插入图片描述
不难看出:
① 当r = 0时,与a相比的较小数b = 8即为72和56的最大公约数,gcd(72,56) = 8
② 由上表可以大致归纳出辗转相除法的过程:将b赋值给a,再将余数r的值赋给b,以此辗转赋值,然后相除,直到余数为0为止,因此得名辗转相除法

下面给出辗转相除法的非递归代码

int gcd(int a,int b)
{
    int r = a % b;
    while (r != 0)
    {
        a = b;
        b = r;
        r = a % b;//重新计算余数
    }
    return b;
}

注意:循环结构都可以转化为递归结构,但是递归未必可以转化为循环结构,且使用辗转相除法传参数时不需要保证gcd(int a,int b)满足关系:a > b,即gcd(72,56) = gcd(56,72)

根据以上分析以及定理,得到辗转相除法递归写法

int gcd(int a,int b)
{
    int r = a % b;
    if (r == 0) return b;
    else return gcd(b,a % b);
}
汉诺塔

问题描述:

汉诺塔(又称河内塔)问题是源于印度一个古老传说:大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘且每次只能移动一个圆盘。当所有的圆盘都移动到另一根柱子,世界就会毁灭。

(默认将所有的圆盘从柱子A移动到柱子C且每次只能移动柱子最上面的原盘)在这里插入图片描述
如上图所示,当柱子A上只有一个圆盘的时候,执行步骤A ——> C即可移动成功。

在这里插入图片描述
当柱子A上有两个圆盘的时候,需要执行步骤:
①A ——> B
②A ——> C
③B ——> C

在这里插入图片描述
当柱子A上有三个圆盘的时候,需要执行步骤:
①A ——> C
②A ——> B
③C ——> B
④A ——> C
⑤B ——> A
⑥B ——> C
⑦A ——> C

按照以上步骤,大家可以在此链接中尝试通关~

将以上7个步骤分为三个大步骤:

  • 借助C将A上的前两个圆盘移动到B上(①②③)
  • 将A上的最后一个圆盘移动到C上(④)
  • 借助A将B上的两个圆盘移动到C上(⑤⑥⑦)

同理,如果是n层的汉诺塔,我们需要将n个圆盘从A柱子移动到C柱子,同样可以分成三大步骤:

  • 借助C将A上的前n - 1个圆盘移动到B上
  • 将A上的最后一个圆盘移动到C上
  • 借助A将B上的n - 1个圆盘移动到C上

如果将这三大步骤接着分为三个步骤,如图所示:
在这里插入图片描述

不难发现,如果设f(n)表示将n个圆盘从一个柱子移动到另一个柱子所需要的移动次数,那么

  • 当n = 1时,f(1) = 1 = 2 ^ 1 - 1
  • 当n = 2时,f(2) = 3 = 2 ^ 2 - 1
  • 当n = 3时,f(3) = 7 = 2 ^ 3 - 1
  • … …
  • 当n = 64时,f(64) = 2 ^ 64 - 1 = 1.8 * (10 ^ 19)
    注:如果移动一次圆盘需要1秒,那么移动1.8 * (10 ^ 19)次大约需要5800亿年
  • 当n = n时,f(n) = 2 ^ n - 1
  • 时间复杂度为O(2 ^ n) —— 指数级

汉诺塔代码如下:
在这里插入图片描述

例4 利用递归求数组某区间元素的和

对于数组a[ ] = {1,2,3,4,5,6,7,8}
写一个递归函数求这个数组中所有元素的和。
(注:本题也是中国矿业大学2021年的研究生初试试题,如果使用for循环实现对数组求区间和,写法很简单,遍历求和即可,但使用递归的思想解题往往时人们容易忽略的)

递归出口:

  • 当L = R时,求和结果即a[L]或者a[R]
    递归关系:
  • 当L与R不等时,求和结果为a[L] + 区间(L + 1 ~ R) 之内的所有元素和

模板如下

#include <iostream>

using namespace std;

int a[8] = {1,2,3,4,5,6,7,8};

int sum(int a[],int L,int R)//L、R分别表示区间的左边界和右边界
{
    if (L == R) return a[L];//递归出口:左右边界相等时表示求和元素只有一个数
    else return a[L] + sum(a,L + 1,R); //递归关系
}

int main()
{
    int res = sum(a,0,7);
    cout << res << endl;//36
    return 0;
}
例5 :求数组的前n项的和

设sum(a,n)表示求数组a的前n项和,经分析得

if (n == 1)sum(a,n) = a[1];
else sum(a,n) = sum(a,n - 1) + a[n];
例6 :求数组的最大值

非递归写法:
思路:假设数组中第一个数为最大值,并且将他赋值给maxNum,然后从第二个数开始遍历数组,如果存在某个数大于maxNum,那么就可以更新maxNum的数值,最后得到答案。
代码如下:

#include <iostream>

using namespace std;

int a[8] = {1,2,3,4,5,6,7,8};

int findMax(int a[],int n)
{
    int maxNum = a[0];
    for (int i = 1;i < n;i++)
        if (a[i] > maxNum)
            maxNum = a[i];
    return maxNum;
    
}

int main()
{
    int res = findMax(a,8);
    cout << res << endl;//8
    return 0;
}

递归写法:
思路分治思想 —— 求数组中的最大值,可以将数组原本的区间(L,R)分成两个部分

  • 第一个部份为a[L]
  • 第二个部分为a[L + 1,R]

然后比较这两个区间中的最大值,较大的那个就是整个数组的最大值
代码如下:

#include <iostream>

using namespace std;

int arr[8] = {1,2,3,4,5,6,7,8};

int findMax(int arr[],int L,int R)
{
    if (L == R) return arr[L];//当数组中只有一个元素时
    int a = arr[L];
    int b = findMax(arr,L + 1,R);
    if (a > b) return a;
    else return b;
}

int main()
{
    int res = findMax(arr,0,7);
    cout << res << endl;//8
    return 0;
}

总结:递归与数组相结合的时候一般采用区间的写法

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值