暑期算法集训之递归篇:探索数据结构和算法中的递归思想

大家好!很高兴与大家分享我的第一篇博客。从现在开始我将会持续更新我的暑期算法集训系列,今天,我们将深入研究递归,一种强大而常用的编程思想。

通过理解和掌握递归,我们能够解决各种复杂的问题,包括数据结构和算法中的许多经典案例。 本篇博客将重点介绍递归的基本原理和应用。


目录

1、什么是递归? 

有递有归

以数的阶乘为例

树形结构

2、递归的基本原理

基本概念

形象描述

3、递归的应用场景

汉诺塔问题


1、什么是递归? 

递归是一种常见的编程思想,它通过将一个问题分解为更小的、与原问题类似的子问题来解决

有递有归

在递归过程中,函数会调用自身来处理这些子问题,直到达到某个基本情况,然后逐层返回结果,最终得到原问题的解。

需要注意的是,递归递归,那就必定要先递再归,不能只有递没有归,也不能只有归没有递。

在一个具体的问题中,我们先要进行数据的传递,最后在满足某种条件时再返回每一次传递的数据,这在我们后面的用二叉树展示递归思想可以得到理解,同时我们可以从下面的例子中理解。

以数的阶乘为例

题目:计算n!(n的阶乘)

答案很简单,自然为:n*(n-1)*(n-2)*(n-3)*.........*3*2*1  

代码:(我们假设n=99)

int f(n){                                
    if(n==0){                         //基本情况:0的阶乘为1
        return 1;
    }else{                            //递归调用:计算 n 的阶乘
        return n*f(n-1);
    }
}

递:

从代码中可以看到,只要n不等于0(n≠0),判断语句将会执行n*f(n-1),而从第一次n=99来到n*f(n-1)时,将n-1(也就是98)传入自己的函数,在内存中,99的这个数据依然会保留,转而先执行函数 f ,直到n==0时,if判断结束,然后怎么办呢?看下面的“归”就知道啦

归: 

由于在每一次的n*f(n-1)中的n都是保存在内存当中的,因此本函数直到调用到最后,是99*98*97*........3*2*1的状态,由于最后一次调用是1*1(n=0时返回1),然后返回到之前的状态,也就是2*1,再返回之前的状态,也就是3*2,再下一次就是4*6(3*2)依次类推直到99。

树形结构

递归的思想可以通过树形结构来直观地解释。考虑一个树的节点,每个节点都可以看作是根节点,并具有相同的结构。通过递归,我们可以在每个节点上应用相同的操作,从而解决整个树的问题。

 

在上述示例中,我们可以将根节点 A 视为递归的起点。从 A 节点开始,我们可以看到 A 节点调用了两个子节点 B 和 C。进一步观察,节点 B 又调用了两个子节点 D 和 E,而节点 C 调用了一个子节点 F。这个过程可以一直持续下去,形成一个递归的树形结构。

2、递归的基本原理

基本概念

递归的基本原理是将一个问题分解为更小的、与原问题类似的子问题,并通过递归调用来解决这些子问题。递归的过程中,函数会反复调用自身来处理这些子问题,直到达到某个基本情况,然后逐层返回结果,最终得到原问题的解。

递归的基本原理可以通过以下步骤来理解:

  1. 定义基本情况:递归函数必须定义一个或多个基本情况,即递归的停止条件。基本情况是在问题已经足够简单,不需要继续递归下去时,直接返回结果或执行特定操作。

  2. 将问题分解为子问题:在递归函数中,将原问题分解为一个或多个与原问题相似但规模更小的子问题。这些子问题通常是原问题的简化版本或原问题的部分。

  3. 使用递归调用解决子问题:在递归函数中,通过调用自身来解决子问题。递归调用会重复执行相同的操作,直到达到基本情况,逐层返回结果。

  4. 组合子问题的解:当子问题的结果返回时,递归函数将使用这些结果来组合出原问题的解。这可能涉及合并、比较、累加等操作,具体取决于问题的性质。

递归的关键在于将原问题分解为子问题,并通过递归调用来解决子问题,从而逐步推进问题的求解。在实现递归函数时,需要确保基本情况的正确性,以避免无限递归,并且每次递归调用都能将问题规模减小,最终达到基本情况。

总结来说,递归的基本原理是将一个问题转化为更小的子问题,并通过递归调用来解决这些子问题,直到达到基本情况并逐层返回结果。理解和应用递归的基本原理是学习和使用递归思想的关键。

形象描述

想象你面前有一座大山,你的任务是从山脚走到山顶。但是这座山太陡峭了,你无法一步到位地到达山顶。

递归就像是在攀登这座大山的过程。你可以将整个攀登过程分解为一系列子任务,每个子任务就是攀登山的一部分。

首先,你发现前方有一块小山丘,你决定先攀登这个小山丘。但是这个小山丘同样太陡峭,你无法直接到达山顶。于是你再次将这个小山丘分解为更小的子任务,比如攀登山丘的左侧和右侧

你重复这个过程,继续将每个子任务分解为更小的子任务,直到你遇到的山丘足够小,你可以直接迈过它,到达山顶。

最终,你成功攀登每个子任务,逐步征服了整个山脉,最终达到了山顶。大家有没有发现,下面的图示其实就是一颗数,而山顶就是你要解决的最大的问题。(忽略滑稽的左右两个箭头,它可以类比为最后一次调用函数的值)

3、递归的应用场景

递归在计算机科学中有广泛的应用场景,特别是在算法和数据结构中。以下是一些常见的递归应用场景:

  1. 树和图的遍历:递归可以用于遍历树和图结构,并对每个节点执行相同的操作。例如,深度优先搜索(DFS)和广度优先搜索(BFS)算法都可以使用递归来实现。

  2. 分治算法:分治算法是一种将问题分解为更小的子问题并分别解决的算法策略。递归在分治算法中发挥着重要的作用,它可以将原问题递归地分解为多个规模较小的子问题,然后将子问题的结果合并得到原问题的解。例如,归并排序和快速排序都是使用递归实现的分治算法。

  3. 动态规划:动态规划是一种通过将问题分解为重叠子问题并存储子问题的解来优化计算的方法。递归在动态规划中常用于解决具有重叠子问题性质的问题。通过递归调用,可以逐步解决子问题并将结果存储在缓存中,以避免重复计算。例如,斐波那契数列和背包问题都可以使用递归实现的动态规划算法来解决。

  4. 回溯算法:回溯算法是一种通过穷举所有可能的解空间来解决问题的方法。递归在回溯算法中非常常见,它可以用于在解空间中搜索并尝试所有可能的选择。每次递归调用都代表了一个选择,当达到某个条件时,可以回溯并尝试其他选择。八皇后问题和组合问题是回溯算法的经典示例。

  5. 数据结构的定义和操作:递归可以用于定义和操作许多数据结构,包括链表、树、图等。通过递归调用,可以逐层构建和操作数据结构。例如,在二叉树中插入节点、查找节点或遍历树的操作都可以使用递归来实现。

 例如最为经典的汉诺塔问题:

汉诺塔问题

问题描述: 有三根柱子,标记为A、B和C。开始时,A柱子上有一堆从小到大依次叠放的圆盘。目标是将所有圆盘从A柱子移动到C柱子,同时遵守以下规则:

  1. 每次只能移动一个圆盘。
  2. 每次移动时,只能将较小的圆盘放在较大的圆盘上面。

递归解法:

  1. 如果只有一个圆盘,直接将它从起始柱子移动到目标柱子上。
  2. 如果有多个圆盘,按照以下步骤进行递归操作:
    1. 将除最底下一个圆盘以外的上方所有圆盘从起始柱子移动到辅助柱子上(借助目标柱子)。
    2. 将最底下的圆盘从起始柱子移动到目标柱子上。
    3. 将辅助柱子上的所有圆盘移动到目标柱子上(借助起始柱子)。

 如果只有两个圆盘,那很简单,只需要把上面的那个圆盘放到中间,再把另一个放到最右边,再把中间那个放到最右边就完成了,但是要是有很多圆盘呢?

我们前面说到,要把大问题化解为同样解法或者相似解法的小问题,那我们就可以把多个圆盘看为两个圆盘的小问题,如图:

可是根据规则,一次性移动那么多,这样肯定是不可以的,那我们根据递归的方法,可以把这个小问题化为更加小的小小问题,即把他看成三个圆盘的状况,也就是最下面是一个,最下面的倒数第二个算一个,除此之外算一个,可是这样明显也是不行的,那我们就继续分解问题....

注意!此时我们需要注意,我觉得也是所有人刚开始难以理解的问题,那就是到底该怎么移动,一下子A->B,一下子A->C,一下子B->C,C->B......(ABC分别代表三根柱子)非常的晕,包括我自己也是刚开始无法理解,后来才知道应该怎么样理解。那就是我们需要清楚的明白:什么时候的哪根柱子是目标位置或是辅助柱子。

在平常,我们可以经常看到这样的代码来解决汉诺塔问题,我认为是不好的:将递归中的变量名称也用ABC来代替柱子移动时的ABC(注意这是两个不同的ABC),这样一来,对于初学者来说,我们就很难分辨到底从哪移到哪了,A怎么变成C了,又或者B怎么变成C了。

#include<iostream>
using namespace std;
void Hanio(char A,char B,char C,int n){    //这里的参数名称为ABC
    if(n==1){
        cout<<A<<"->"<<C<<endl;
        return;
    }
    Hanio(A,C,B,n-1);
    cout<<A<<"->"<<C<<endl;
    Hanio(B,A,C,n-1);
    return;
}

int main(){
    int n;
    cin>>n;
    Hanio('A','B','C',n);                  //这里传入的值也是ABC
    return 0;
}

因此,我认为应该将Hanio函数中变量名ABC的名称改为:

A:source(起始柱子)

B:auxiliary(辅助柱子)

C:target(目标柱子)

这样就可以深刻理解汉诺塔问题中,盘子移动时的具体原理,下面我们看一下代码:

#include <iostream>
using namespace std;
void hanoi(int n, char source, char auxiliary, char target) {
    if (n == 1) {
        cout <<source << "->" << target << endl;
    } else {
        hanoi(n - 1, source, target, auxiliary);
        cout <<source << "->" << target << endl;
        hanoi(n - 1, auxiliary, source, target);
    }
}

int main() {
    int n;
    cout << "请输入圆盘的数量:";
    cin >> n;
    hanoi(n, 'A', 'B', 'C');
    return 0;
}

但是切忌把递归问题太深层的思考了,不然你脑子会爆炸的(别问为什么,都是过来人),你只需要知道,我们是把大问题变成小问题,然后我们只要知道怎么解决这个小问题,然后往前推就可以了,不要一直想小问题又是怎么解决大问题的,这个你不用知道,而且我已经说过了,递归适用于小问题对于大问题而言有相似或者相同的解法,我们才可以用递归!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LifeGPT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值