递归:计算思维的核心

news2024/9/24 13:24:24

引言

人类对这个世界的认识是从特例到普遍,从具体到抽象,从简单到复杂的,是一个递推Iterative)的过程。这种人类固有的认知与思维方式令我们可以轻易的理解具体的事物,但同时却限制了我们的抽象能力大局观

而计算机执行任务的方式(可以称之为“计算思维”)与人类正好相反,是自顶向下,由全局到局部的,往往是一个递归Recursive)的过程,是人为的进行了抽象设计的。递归是计算思维中的核心概念,通过递归,将复杂的计算问题分解为更小的子问题,然后计算机从全局进入到局部,一步步迭代执行简化的子问题,最终求得整个问题的解。

可见,若将人类普遍的思维方式当做正向的思维,那么计算机的“处事方式”则往往采取了“逆向思维”!

递归思想

递归是一种强大且优雅的解决问题的方法。它以简洁的方式处理复杂问题,尤其适用于具有重复性质的问题。

试想一下求解 5 ! 5! 5! 的方式,如果按照正向思维,我们可以通过 5 × 4 × 3 × 2 × 1 5 \times 4 \times 3 \times 2 \times 1 5×4×3×2×1 来求解。其 Python 代码如下:

def factorial(n):
    result = 1
    for i in range(1, n+1):
        result *= i
    return result

而如果使用递归思想,我们可以将 5 ! 5! 5! 分解为 5 × 4 ! 5 \times 4! 5×4!,然后再将 4 ! 4! 4! 分解为 4 × 3 ! 4 \times 3! 4×3!,以此类推,直到 1 ! 1! 1! 为止。Python 代码如下:

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

通过上述递归求解 5 ! 5! 5! 的方式,我们可以了解递归求解问题的两个关键要素:

  • 基本情况(Base Case),不再递归的条件,即最初始的、简单的、可以直接得出结果的情况。比如阶乘里的 1 ! = 1 1! = 1 1!=1
  • 递归情况(Recursive Case),即当问题每一步变更符合固定规律时,可以根据前后两步的固定的关系基于前一步的解求得当前迭代轮次的解,递归就是基于这个关系从后往前计算的过程。比如阶乘里的 n ! n! n! = n × ( n − 1 ) ! n \times (n-1)! n×(n1)!

递归的本质就是调用自身。在阶乘的例子中,它在逻辑上更接近自然的数学定义,更符合问题的自然分治结构。递归调用将问题拆解成更小的子问题,并在达到递归基条件时终止递归。

递归历险

通过求解 5 ! 5! 5! 这样简单的问题我们可能还无法感受到递归的妙处,下面我们以复杂一些的示例来展示递归思想的优势。

抢 20 游戏

两个人做游戏,轮流从 1 和 2 中挑选一个数字,并计算所有被选到数字的总和,当数字的总和正好为 20 时,当前出手的人获胜。问题是如何设定策略可以保证一定能赢?

这个问题正向思维的解法是通过穷举所有可能的情况,然后找到必胜策略,但要穷举所有情况是相当困难的。但是如果采用递归思维,将这个问题分解一下,问题可能会变得相当简单。

我们试想一下,如果想要保证最后一轮轮到自己时,一定能使数字的总和为 20,那么在上一轮轮到自己时数字的总和一定要是 17,因为只有在 17 的基础上,我们可以保障无论对手选择 1 还是 2,我们都可以使数字的总和为 20。

同理,要保障自己可以得到总和为 17 的结果,那么在上上轮轮到自己时数字的总和一定要是 14;以此类推,我们可以得到一个规律,即在轮到自己时,数字的总和一定要是 20 的倍数减去 3 的倍数。最终我们可以得出,只要我们能先抢到 2 + 3 × n 2 + 3 \times n 2+3×n 的总和数,我们就一定能赢,比如我们首轮首次出手就抢到 2。

这就是递归思维的妙处,通过分析问题的结构,我们可以找到问题的关键规律,比如只要在上一轮我们抢到了比最终总和 20 差 3 的总和数,就能赢得游戏。在此基础上可以将整个问题拆解成简单的子问题,如保障每一轮都能得到比上一轮总和差 3 的总和数,就一定能赢得游戏。如此,我们只需要关注并依次解决当前简单的子问题,直到迭代达到递归基条件时终止,如只要保障最初可以抢到 2 就一定能赢。问题就这样被神奇的解决了。

递归思维的解法是通过分析问题的结构,找到问题每一步变动时的规律,如果这个变动是一个固定关系,那么可以假设我们已经拿到前一步的解,基于这个固定关系和前一步的解,我们就能够计算出下一步的解,如此从后往前递归迭代,直到遇到初始的状态。

上台阶问题

总共有 20 个台阶,每次可以上 1 或者 2 个台阶,那么总共有多少种上到 20 阶的方式?

与“抢 20 游戏”类似,我们求解这个问题的正向思路就是将所有的可能性统统列举出来,比如(1、4、7、10、12、15、18、20),(1、2、5、8、11、14、17、20)等等,然后试图从中找到一般性规律,从而找到第 n 阶的做法数与 n 的关系( F ( n ) F(n) F(n))。但这无疑是非常困难的,我们看看 F(n) 的公式就知道这有多难:

F ( n ) = 1 5 [ ( 1 + 5 2 ) n − ( 1 − 5 2 ) n ] F(n) = \frac{1}{\sqrt{5}}[(\frac{1+\sqrt{5}}{2})^n - (\frac{1-\sqrt{5}}{2})^n] F(n)=5 1[(21+5 )n(215 )n]

但是如果我们换个思路,采用逆向思维,问题就会变得很简单。我们只要知道,当前所处的台阶位置只能是从低它 1 阶或 2 阶这两种情况而来,比如第 20 阶只能是从第 19 阶或第 18 阶而来,那么自然而然的就可以得出 F(20) 其实就是 F(19) 与 F(18) 的和,更一般的:

F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n) = F(n-1) + F(n-2) F(n)=F(n1)+F(n2)

我们不难知道 F(1) = 1,F(2) = 2,因此采用递归方法很容易实现求解:

def climb_stairs(n):
    if n == 1:
        return 1
    if n == 2:
        return 2
    return climb_stairs(n-1) + climb_stairs(n-2)

汉诺塔问题

有三根杆子 A、B、C。A 杆上有 N 个 (N > 1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:

  1. 每次只能移动一个圆盘;
  2. 大盘不能叠在小盘上面;
  3. B 可以临时作为中转。

这个问题的解法是一个典型的递归问题,我们可以将其分解为三个子问题:

  1. 将 n-1 个盘子从 A 经过 C 移到 B;
  2. 将第 n 个盘子从 A 移到 C;
  3. 将 n-1 个盘子从 B 经过 A 移到 C。

如此我们不难实现圆盘的移动解法:

def hanoi(n, A, B, C):
    if n == 1:
        print(f"Move disk {n} from {A} to {C}") # 当只需要移动 1 个盘子时,直接将它从 A 移到 C;
    else:
        hanoi(n-1, A, C, B)                     # 将 n-1 个盘子从 A 经过 C 移到 B;
        print(f"Move disk {n} from {A} to {C}") # 将第 n 个盘子从 A 移到 C;
        hanoi(n-1, B, A, C)                     # 将 n-1 个盘子从 B 经过 A 移到 C;

若是依旧采用正向思维,我们可能会陷入无尽的穷举中,但是递归思维却能够很好的解决这个问题,通过分析问题的结构,我们可以找到问题的关键规律,比如只要将 n-1 个盘子从 A 经过 C 移到 B,然后将第 n 个盘子从 A 移到 C,最后将 n-1 个盘子从 B 经过 A 移到 C,就轻易地解决了整个问题。

八皇后问题

在 8×8 的国际象棋棋盘上摆放 8 个皇后,使得任意两个皇后都不能在同一行、同一列或同一斜线上。问有多少种摆法?

请添加图片描述

同样采用递归思路求解,我们采用逐行放置皇后的方式,并判断当前放置是否安全。如果安全,则递归地继续放置下一行的皇后;如果不安全,则回溯,移除当前已放置的皇后,尝试下一个位置。

求解思路如下:

  1. 使用一个一维数组 queens,其中 queens[i] 表示第 i 行放置皇后的列编号;
  2. 定义一个递归函数 place_queen(row),表示在第 row 行放置皇后;
  3. 在 place_queen(row) 中,尝试在第 row 行的每一列放置皇后,如果放置的位置安全(不与前面的皇后冲突),则递归调用 place_queen(row + 1);
  4. 基准条件是当 row 达到 8 时,说明所有皇后已成功放置,记录一种解法;
  5. 安全检查包括同列检查和对角线检查。

Python 代码如下:

def solve_n_queens(n):
    solutions = []
    queens = [-1] * n

    def is_safe(row, col):
        for i in range(row):
            if queens[i] == col or \
               queens[i] - i == col - row or \
               queens[i] + i == col + row:
                return False
        return True

    def place_queen(row):
        if row == n:
            solutions.append(queens[:])
            return
        for col in range(n):
            if is_safe(row, col):
                queens[row] = col
                place_queen(row + 1)
                queens[row] = -1  # 回溯

    place_queen(0)
    return len(solutions), solutions

# 示例
num_solutions, solutions = solve_n_queens(8)
print(f"共有 {num_solutions} 种摆法。")
for solution in solutions:
    print(solution)

其中数组 queens 记录了每一行皇后的位置,queens[i] 是第 i 行皇后的列。安全检查 is_safe 检查是否存在同列(列序相同)和对角线上(行列差与行列和相同)的皇后。尝试在当前行的每一列放置皇后,如果放置成功,则递归调用下一行的放置函数,如果下一行无法放置,则回溯,尝试下一列位置,如果所有行都成功放置,则找到一个合法解。

通过这种递归回溯的方法,可以有效地找到八皇后问题的所有解法,这个问题改用穷举的思路是非常困难的,实际上就连大数学家高斯穷其一生也只找到了 76 中解法,而实际包含 92 种解法。

二叉树遍历

给定一个二叉树,返回它的中序遍历。

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def inorder_traversal_recursive(root):
    if root is None:
        return []
    return inorder_traversal_recursive(root.left) + [root.val] + inorder_traversal_recursive(root.right)

# 示例
root = TreeNode(1)
root.right = TreeNode(2)
root.right.left = TreeNode(3)

print(inorder_traversal_recursive(root))
# 输出:[1, 3, 2]

递归实现简洁明了,递归正好匹配了树结构的自然递归的特性。

我们可以看一下采用递推思路的实现方式:

def inorder_traversal_iterative(root):
    result, stack = [], []
    while stack or root:
        while root:
            stack.append(root)
            root = root.left
        root = stack.pop()
        result.append(root.val)
        root = root.right
    return result

# 示例
print(inorder_traversal_iterative(root))
# 输出:[1, 3, 2]

这里使用了栈模拟递归,性能更高,但代码复杂度明显就增加了。

递归 vs. 递推

对比递归递推
性能递归消耗栈空间,深度过大可能导致栈溢出,需要谨慎处理。递推通常更节省内存,因为它只需少量额外的变量来保持状态。
复杂度递归可能引入重复计算的问题(如斐波那契数列的简单递归),需要优化(如记忆化)来提高效率。递推避免了这些重复计算,通过迭代一步步推进,一般情况下效率更高。
简洁性递归代码往往更简洁和优雅,对于自相似问题特别有用。递推代码在某些情况下可能显得冗长,但通常更直观。
自然性对于一些问题,递归是最自然的解决方案,比如树的遍历、汉诺塔问题等。

在一些情况下,递推可能是更高效、更实际的选择,但这并不是说递归就不好,在大多情况下,递归提供了更清晰和自然的解题思路,而一般递归的解法都可以被转换成循环求解的实现方式,这也是为什么我们至少应该掌握递归思想这种计算思维的原因。

结语

递归是一种强大且优雅的计算解题思路,它以简洁的方式处理复杂问题,尤其适用于具有一定规律的问题。人们习惯于以归纳总结的方式思考问题,而计算思维恰恰相反。计算机在执行复杂计算任务的过程,天生就是栈式的调用一个个简单计算的递归的过程,想要在计算的世界中达到随心所欲,驾轻就熟的地步,我们要让自己的头脑按照计算机的方式去思考问题。


  • 上一篇:排序算法优化思考
  • 专栏:「数智通识」 | 「算法通解」

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1989500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【电控笔记z6】无感文献综述

高频注入 afabeta注入 lq/ld越大统好 凸极性大反电动势ZVCD pwm电压向量为主 增加动态特性 设计隆博戈估测器 高频注入: lq/ld比较大 运用在低转速 到高速的时候 , 切换到model_base的方法进行反电动势侦测 smo :速度无法很低 有个极限 受杂讯影响大 高速时候用 总结 用spm …

ArrayList集合源码解读(一)

ArrayList集合源码解读(一) 前言 笔者在阅读网上众多的ArrayList源码解读时发现他们都是以1.8版本的来进行讲解,并且很多都是囫囵吞枣,看的人一脸懵逼。 其实现在的很多公司都换成了17版本的jdk。笔者决定自己写一个ArrayList集…

网工内推 | 国企运维工程师,华为认证优先,最高年薪20w

01 上海陆家嘴物业管理有限公司 🔷招聘岗位:IT运维工程师 🔷岗位职责: 1、负责对公司软、硬件系统、周边设备、桌面系统、服务器、网络基础环境运行维护、故障排除。 2、负责对各部门软件操作、网络安全进行检查、指导。 3、负责…

14.Lambda表达式、可变参数

一.Lambda表达式 1.1 函数式接口 1.什么是函数式接口 在Java中,函数式接口是指只包含单个抽象方法的接口,但它也可以有其他方法,例如默认方法和静态方法。函数式接口可以使用Lambda表达式或方法引用来创建该接口的实例。Java 8引入了函数式…

分享一个基于SpringBoot的大学生创新能力培养平台Java(源码、调试、LW、开题、PPT)

💕💕作者:计算机源码社 💕💕个人简介:本人 八年开发经验,擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等,大家有这一块的问题可以一起交流&…

七夕表白代码包

目录 1.像素爱心代码 2.心动爱心代码 3.问答样式代码 1.像素爱心代码 今年的最火的当然是像素风&#xff0c;一个一个小方块拼成的爱心超级可爱。 (1)桌面新建一个文本档.txt (2)输入以下代码,可以直接复制 <!DOCTYPE html><html><head><meta chars…

Vue项目通过宝塔部署之后,页面刷新后浏览器404页面

目录 报错 解决方法 报错 将vue项目在宝塔上部署&#xff0c; 当项目挂载到服务器上去&#xff0c;进行浏览器的访问&#xff0c;是能正常访问的&#xff0c;可是当我们在浏览器上进行刷新之后&#xff0c;浏览器会给我们返回一个404的页面。 解决方法 &#xff08;1&#…

如何利用 LNMP 搭建 WordPress 站点

作者 乐维社区&#xff08;forum.lwops.cn&#xff09; 许远 在这个信息爆炸的时代&#xff0c;拥有一个能够迅速传达信息、展示个性、并能够与世界互动的在线平台&#xff0c;已成为企业和个人的基本需求。WordPress&#xff0c;以其无与伦比的易用性和强大的扩展性&#xff0…

Redis5-缓存

目录 什么是缓存 添加Redis缓存 缓存更新策略 三种策略 数据库和缓存不一致的解决方案 缓存穿透 缓存雪崩 缓存击穿 缓存工具封装 什么是缓存 缓存是数据交换的缓冲区&#xff08;Cache&#xff09;&#xff0c;是存贮数据的临时地方&#xff0c;一般读写性能较高 多…

【机器学习】BP神经网络中的链式法则

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 BP神经网络中的链式法则1. 引言2. 链式法则基础2.1 什么是链式法则&#xff1f;…

springboot mybatis plus 固定查询条件及可选查询条件的组合查询,使用QueryWrapper.and()来解决。

1、我们在写查询SQL的时候&#xff0c;经常会碰到&#xff0c;比如&#xff0c;同一个类别下的某一个编号的物料信息&#xff0c;或者是同一批次的物料库存问题等等。 所属类别fid物料编号bm物料批次pc110.01.0220240807110.01.0320240807 210.02.0120240805 2、那么我…

定点数的运算

目录 1.定点数的移位运算 1.1算数移位 数学含义&#xff1a; 规律总结&#xff1a; 1.2逻辑移位 1.3循环移位 不带进位位 带进位位 2.定点数的加减运算 3.定点数的乘除运算 3.1原码 一位乘法 除法 3.2补码 一位乘法 除法 1.定点数的移位运算 1.1算数移位 数学…

org.gitlab4j使用报错问题

报错如上&#xff0c;刚开始报错Caused by: java.lang.NoClassDefFoundError: javax/ws/rs/core/StreamingOutput。 原因&#xff1a;项目是JDK17引起的版本不兼容 解决&#xff1a;升级高版本即可。

4.1 图标资源、光标资源

图标资源 添加资源 添加资源(可视化完成) 注意图标的大小&#xff0c;一个图标文件中&#xff0c;可以有多个不同大小的图标。加载 LoadIcon 是 Windows API 中用于加载图标资源的函数 HICON WINAPI LoadIcon(HINSTANCE hInstance,LPCTSTR lpIconName );参数说明 1.hInstanc…

牛客JS题(二十七)Getter

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; class基础用法getter的应用setter的应用 题干&#xff1a; 我的答案 <!DOCTYPE html> <html><head><meta charsetutf-8></head><body><script type"text/javascript&qu…

机器人帮助文档

文章目录 机器交流使用群使用图例1. 查看机器人使用文档2. 直接问问题&#xff08;系统默认AI&#xff09;3. 系统默认AI切换4. 直接问问题&#xff08;指定讯飞星火AI&#xff09;5. 直接问问题&#xff08;指定百度文心AI&#xff09;6. 直接问问题&#xff08;指定谷歌AI&am…

Python的全局变量

我来举个例子 像下面&#xff0c;我把全局变量写在函数外面&#xff0c;导致func函数里得不到变量 正确做法应该是在函数内引入全局变量&#xff0c;利用global关键字 请注意&#xff01;由于1的操作导致全局变量发生改变&#xff0c;可能会影响到其他引用全局变量的地方。这点…

基于统计检验与机器学习研究客户对保险兴趣的因素

1.项目背景 保险单是一种安排&#xff0c;公司承诺为特定的损失、损坏、疾病或死亡提供赔偿保证&#xff0c;以换取支付指定的保费。保费是客户需要定期向保险公司支付的一笔钱&#xff0c;以提供此保证&#xff0c;与医疗保险一样&#xff0c;也有车辆保险&#xff0c;客户每…

【Python】requests获取网络响应的时候,遇到url超过最大重试次数的解决方法

我们在使用requests连接网址后&#xff0c;获取网络响应的时候&#xff0c;有时候可能会遇到这样的问题&#xff1a; 问题&#xff1a; Maxretries exceeded with url: /tags-%E9%A1%B9%E7%9B%AE-5.html(Caused by SSLError(SSLEOFError(8,‘EOFoccurred in violation of prot…

大模型层数过多影响

当层数过多时候&#xff0c;梯度是累乘关系&#xff0c;如100 最后可能超过f16精度 梯度爆炸 后面梯度和权重值特别大 梯度消失 后台梯度和权重趋近于0 梯度合理范围e-6 到 e3 优化方法 1、优化点 乘法改为加法 resnet lstm 2、归一 梯度归一&#xff0c;大于小于阈值…