算法模型从入门到起飞系列——递归(探索自我重复的奇妙之旅)

文章目录

  • 前言
  • 一、递归本质
    • 1.1 递归的要素
    • 1.2 递归特点
  • 二、递归&迭代
    • 2.1 递归&迭代比较
    • 2.2 递归&迭代如何实现相同功能
      • 2.2.1 递归实现
      • 2.2.2 迭代实现
      • 2.2.3 性能对比
  • 三、优雅的递归理解
    • 3.1 阶乘计算分解
    • 3.2 [DFS](https://blog.csdn.net/qq_38315952/article/details/146374720?spm=1001.2014.3001.5501)分解
  • 四、尾递归优化(Tail Call Optimization, TCO)
    • 4.1 尾递归优化概念
    • 4.2 尾递归优化演示
  • 五、哪些问题更适合递归解决
    • 1. 数学问题
    • 2. 分治算法
    • 3. 树形结构遍历
    • 4. 回溯算法
    • 5. 动态规划问题
    • 6. 解析嵌套结构
    • 7. 递归关系明确的问题
  • 献给读者


在这里插入图片描述
在这里插入图片描述


前言

在编程的世界里,递归是一种既神秘又强大的工具。它宛如一个魔法盒子,能够将复杂的问题简化为更小规模的相同问题,直至找到最基础、可以直接解决的情况。无论是优雅地遍历复杂的树形结构,还是巧妙地解开经典的汉诺塔谜题,递归都能以其独特的方式展现其非凡的魅力。

递归的核心在于函数直接或间接地调用自身,这一过程看似简单,实则蕴含着深邃的逻辑和无限的可能性。通过递归,我们可以构建出简洁而富有表现力的代码,使得解决方案看起来自然而直观。然而,正如任何强大的工具一样,递归也有其双刃剑的一面:如果使用不当,可能会导致程序陷入无限循环,或是消耗过多的系统资源,甚至引发栈溢出错误。

本书旨在深入探讨递归的概念、应用及其背后的设计哲学。我们将从最基本的原则出发,逐步揭开递归的神秘面纱,揭示其在算法设计、数据结构处理以及实际软件开发中的广泛应用。无论你是编程新手,渴望理解递归的基础知识;还是经验丰富的开发者,希望进一步掌握递归优化技巧,本书都将为你提供宝贵的见解和实用的指导。

让我们一同踏上这段探索递归奥秘的旅程,在不断的实践中体会其带来的乐趣与挑战,共同解锁编程艺术中这扇独特的门扉。准备好迎接这场自我重复的奇妙之旅了吗?前方等待着你的,是更加深刻的理解和无尽的创造可能。

一、递归本质

递归是一种在编程和数学中使用的方法,它指的是一个函数或过程直接或间接地调用自身。递归通常用于解决可以被分解为更小的相同问题的问题。这种方法非常强大,但需要小心使用,因为它可能导致无限循环或其他错误,如果设计不当的话。

💡贴士:递归核心就是调用自己,自我分解,化繁为简。

1.1 递归的要素

  1. 基准情况(Base Case):这是递归过程中的终止条件,用来停止递归调用。没有有效的基准情况会导致无限递归,最终可能耗尽系统资源或导致栈溢出错误。
  2. 递归步骤(Recursive Step):这是将问题规模减小,并朝向基准情况前进的步骤。在这个过程中,函数会调用自己并传入较小规模的问题参数。

1.2 递归特点

  1. 自我调用
    递归的核心特点是函数直接或间接地调用自身。通过这种方式,复杂的问题可以被分解为更小、更易管理的子问题,直到达到一个可以直接解决的基础情况(基准情况)。

  2. 基准情况(Base Case)
    每个递归函数都必须定义至少一个基准情况,作为递归终止的条件。基准情况是递归过程中最简单的情况,不需要进一步递归来解决。缺乏有效的基准情况会导致无限递归,最终可能引发栈溢出错误。

  3. 递归步骤(Recursive Step)
    在递归步骤中,问题被分解为一个或多个较小规模的相同问题,并通过再次调用递归函数来求解这些子问题。递归步骤的设计至关重要,它决定了递归过程能否逐步接近基准情况,从而正确终止。

  4. 空间复杂度高
    每次递归调用都会在调用栈上创建一个新的帧,用于保存局部变量和返回地址等信息。因此,递归可能会消耗大量的内存,特别是在深度递归的情况下,这可能导致栈溢出错误。相比之下,迭代通常需要较少的额外空间。

  5. 可能存在性能开销
    由于函数调用本身有一定的开销,包括参数传递、局部变量初始化以及控制权转移等,递归可能会比等效的迭代实现慢。此外,重复计算相同子问题的情况(如未优化的斐波那契数列计算)也可能导致效率低下。

  6. 提升代码可读性
    对于某些类型的问题,如树遍历、图搜索算法和分治算法,使用递归可以使代码更加简洁明了,易于理解和维护。递归结构能够直观地反映问题的本质,使得解决方案看起来自然而优雅。

  7. 尾递归优化
    一些现代编程语言支持尾递归优化(Tail Call Optimization, TCO),在这种情况下,如果递归调用是函数执行的最后一步且不需保留当前调用的状态,则编译器或解释器可以优化该递归调用以减少栈空间的使用,甚至将其转换为迭代形式,从而提高效率并避免栈溢出风险。

了解这些特点有助于合理选择何时使用递归解决问题,并采取适当措施优化递归函数的表现。无论是为了提升代码的清晰度还是性能,掌握递归的特点都是编程技能中的重要一环。

二、递归&迭代

2.1 递归&迭代比较

特性递归迭代
定义函数直接或间接调用自身来解决问题使用循环结构重复执行一段代码,直到满足特定条件
基准情况必须明确至少一个终止条件(基准情况),以避免无限递归通过循环条件控制终止,无需显式定义基准情况
代码风格对于某些问题(如树遍历、分治算法)更加直观和简洁通常需要手动管理循环变量和状态,代码可能较冗长但清晰
空间复杂度每次递归调用都会在栈上创建一个新的帧,可能导致较高的内存消耗空间效率高,因为只需要固定的额外空间来存储循环变量
性能可能存在函数调用开销,特别是深度递归时可能导致栈溢出错误通常比递归更高效,因为它避免了函数调用的额外开销
适用场景天然适合处理具有自我相似性质的问题(如树形结构、图搜索等)更适合解决可以通过简单的循环来解决的问题
优化可能性支持尾递归优化(部分语言),可以减少栈空间使用不需要特殊的优化,但在某些情况下可能需要考虑循环不变量的优化
示例应用阶乘计算、斐波那契数列、汉诺塔、快速排序、归并排序阶乘计算、斐波那契数列、线性搜索、二分查找

2.2 递归&迭代如何实现相同功能

为了更清楚地理解递归和迭代如何实现相同的功能,我们可以以计算阶乘为例。阶乘n!定义为从1到n的所有正整数的乘积(0! = 1)。下面是使用递归和迭代两种方法来实现这一功能的例子。

2.2.1 递归实现

public class Factorial {// 递归方法计算阶乘public static int factorialRecursive(int n) {if (n == 0 || n == 1) {return 1;} else {return n * factorialRecursive(n - 1);}}public static void main(String[] args) {// 测试阶乘函数int number = 5;  // 你可以更改这个值来测试不同的输入System.out.println(number + "! = " + factorialRecursive(number));}
}

在这里插入图片描述

2.2.2 迭代实现

public class Factorial {// 迭代方法计算阶乘public static int factorialIterative(int n) {if (n < 0) {throw new IllegalArgumentException("Number must be non-negative.");}int result = 1;for (int i = 2; i <= n; i++) {result *= i;}return result;}public static void main(String[] args) {// 测试阶乘函数int number = 5;  // 你可以更改这个值来测试不同的输入try {System.out.println(number + "! = " + factorialIterative(number));} catch (IllegalArgumentException e) {System.out.println(e.getMessage());}}
}

在这里插入图片描述

2.2.3 性能对比

在这里插入图片描述
在这里插入图片描述

💡贴士:可以看出递归和迭代实现的效果一样,区别在递归会使用多次重复计算,比如计算5的阶乘会计算4的阶乘一直到1的阶乘,然后又从1的阶乘一个一个叠加上去,相当于走了一个来回,增加了很多开销。

三、优雅的递归理解

如果看到这里还没有理解递归的思想或者无法使用递归处理问题,那么接下来不要眨眼睛,见证奇迹的时刻就要到了。

👉👉👉 所有的递归思想都可以转化成nn-1的状态关系。

三步走思想:

  1. 参数(和n有关)。
  2. 临界条件(数学归纳法中临界条件,这个是最远的状态,一般是当n = 1时的状态值)。
  3. 计算fn的公式。

3.1 阶乘计算分解

递归思想:
阶乘问题是最好理解的,可以直接分解成f(n) = f(n-1) * n 。那么给定方法
在这里插入图片描述

3.2 DFS分解

在这里插入图片描述

💡贴士:所有的递归问题都可以换成fnfn-1…之间的关系,只不过像DFS这种不是计算的具体的值,而是用的处理操作。需要仔细体会一下fnfn-1之间的关系。

四、尾递归优化(Tail Call Optimization, TCO)

4.1 尾递归优化概念

尾递归优化(Tail Call Optimization, TCO)是一种编译器或解释器优化技术,旨在优化尾递归调用,使其不会增加额外的栈帧。这意味着在某些情况下,尾递归可以被转换为等效的迭代代码,从而避免了栈溢出的风险并提高了性能。

尾递归是指一个函数在其执行的最后一步调用自身,并且这个递归调用是函数返回值的一部分。换句话说,如果递归调用是函数的最后一个操作,那么它就是一个尾递归调用。

在Java中,标准的JVM并不直接支持尾递归优化(Tail Call Optimization, TCO)。这意味着即使你的函数是尾递归的,JVM也不会自动将其优化为迭代形式。然而,你可以通过手动将尾递归转换为迭代来避免栈溢出问题。

4.2 尾递归优化演示

尽管如此,理解如何编写尾递归函数以及如何手动进行优化是非常有用的。下面我们将展示一个尾递归的阶乘函数示例,并讨论如何手动将其转换为迭代形式。

public class Factorial {// 尾递归方法计算阶乘public static long factorialTailRecursive(long n, long accumulator) {if (n == 0) {return accumulator;} else {return factorialTailRecursive(n - 1, n * accumulator);}}public static void main(String[] args) {// 测试阶乘函数long number = 5;  // 你可以更改这个值来测试不同的输入System.out.println(number + "! = " + factorialTailRecursive(number, 1));}
}

在这个例子中,factorialTailRecursive 函数使用了一个累积器 accumulator 来存储中间结果。每次递归调用时,它都会更新 n 和 accumulator 的值,直到达到基准情况(n == 0)。
由于Java不支持尾递归优化,我们可以手动将上述尾递归函数转换为等效的迭代形式:

public class Factorial {// 迭代方法计算阶乘public static long factorialIterative(long n) {long result = 1;for (long i = 2; i <= n; i++) {result *= i;}return result;}public static void main(String[] args) {// 测试阶乘函数long number = 5;  // 你可以更改这个值来测试不同的输入System.out.println(number + "! = " + factorialIterative(number));}
}

虽然Java本身不支持尾递归优化,但可以通过使用辅助类或数据结构来模拟这种行为。以下是一个示例,展示了如何使用一个简单的栈来模拟尾递归优化:
在这里插入图片描述

import java.util.ArrayDeque;
import java.util.Deque;public class Factorial {// 辅助类用于模拟尾递归private static class FactorialTask {long n;long accumulator;FactorialTask(long n, long accumulator) {this.n = n;this.accumulator = accumulator;}}// 模拟尾递归优化的方法public static long factorialSimulatedTCO(long n) {Deque<FactorialTask> stack = new ArrayDeque<>();stack.push(new FactorialTask(n, 1));while (!stack.isEmpty()) {FactorialTask task = stack.pop();if (task.n == 0) {continue;} else if (task.n == 1) {return task.accumulator;} else {stack.push(new FactorialTask(task.n - 1, task.n * task.accumulator));}}return 1; // 基准情况}public static void main(String[] args) {// 测试阶乘函数long number = 5;  // 你可以更改这个值来测试不同的输入System.out.println(number + "! = " + factorialSimulatedTCO(number));}
}

💡贴士:
尽管Java不支持尾递归优化,但我们可以通过以下几种方式来处理这个问题:
👉手动转换为迭代:将尾递归函数转换为等效的迭代形式。
👉使用辅助类模拟:利用栈或其他数据结构来模拟尾递归的过程,从而避免栈溢出。

五、哪些问题更适合递归解决

1. 数学问题

阶乘计算:计算一个数的阶乘是一个经典的递归问题。

斐波那契数列:斐波那契数列中的每一项是前两项之和,非常适合用递归来实现。

最大公约数(GCD):欧几里得算法通过递归方式求解两个数的最大公约数。

2. 分治算法

归并排序:将数组分成两半,分别对每一半进行排序,然后合并结果。

快速排序:选择一个基准元素,将数组分为比基准大和比基准小的两部分,分别对这两部分进行排序。

二分查找:在一个有序数组中查找某个值,每次将搜索范围缩小一半。

3. 树形结构遍历

二叉树遍历:包括前序遍历、中序遍历和后序遍历等。

多叉树遍历:对于任意深度的多叉树,递归遍历是一种直观且高效的解决方案。

图的深度优先搜索(DFS):递归地访问每个相邻节点,直到所有节点都被访问过。

4. 回溯算法

八皇后问题:找到所有可以在棋盘上放置八个皇后而不互相攻击的方案。

迷宫问题:从起点开始,尝试每一条可能的路径,直到找到出口或所有路径都已尝试完毕。

组合和排列问题:生成一组元素的所有可能组合或排列。

5. 动态规划问题

虽然动态规划问题通常可以通过迭代来解决,但在某些情况下,递归结合记忆化(Memoization)可以简化问题的解决过程:

背包问题:给定一组物品,每个物品有一个重量和一个价值,在限定总重量的前提下,如何选择物品使得总价值最大。
最长公共子序列(LCS):找到两个序列的最长公共子序列。

6. 解析嵌套结构

解析表达式树:例如,解析算术表达式时,可以将其表示为一棵树,并通过递归方式对其进行求值。

解析JSON/XML数据:这些数据格式通常包含嵌套的对象和数组,递归方法可以方便地处理这种嵌套结构。

7. 递归关系明确的问题

汉诺塔问题:经典的递归问题,涉及将一系列盘子从一个柱子移动到另一个柱子。

字符串操作:例如,反转字符串、检查回文等,这些问题往往可以通过递归方式进行简单而直观的实现。

献给读者


💯 计算机技术的世界浩瀚无垠,充满了无限的可能性和挑战,它不仅是代码与算法的交织,更是梦想与现实的桥梁。无论前方的道路多么崎岖不平,希望你始终能保持那份初心,专注于技术的探索与创新,用每一次的努力和进步书写属于自己的辉煌篇章。

🏰在这个快速发展的数字时代,愿我们都能成为推动科技前行的中坚力量,不忘为何出发,牢记心中那份对技术执着追求的热情。继续前行吧,未来属于那些为之努力奋斗的人们。


亲,码字不易,动动小手,欢迎 点赞 ➕ 收藏,如 🈶 问题请留言(评论),博主看见后一定及时给您答复,💌💌💌


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

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

相关文章

Android 系统进程启动Activity方法说明

前面文章Android Activity的启动器ActivityStarter入口说到Activity的恢复执行是由 mRootWindowContainer.resumeFocusedTasksTopActivities(mTargetRootTask, mStartActivity, mOptions, mTransientLaunch)来实现的&#xff0c;下面就看下它的实现。 RootWindowContainer类的…

PostgreSQL_安装

目录 前置&#xff1a; 安装过程&#xff1a; 1 下载软件 2 创建安装文件夹和放置数据的文件夹 3 双击安装 4 连接服务 前置&#xff1a; PostgreSQL 15 windows 10 专业版 安装过程&#xff1a; 1 下载软件 PostgreSQL: Downloads 大小326MB 2 创建安装文件夹和放…

docker desktop 集成WSL Ubuntu22.04

Windows docker desktop 设置WSL ubuntu 22.04启用与其他发行版的集成 Windows docker desktop 安装参考 wsl ubuntu 22.04 查看我宿主机的docker desktop 容器全部的信息 wsl -d Ubuntu-22.04 -u root

从国家能源到浙江交通投资,全息技术在能源交通领域的创新应用

一、3D全息技术行业应用参数及设计制作要求 全息投影 全息投影技术通过激光器、全息片等设备&#xff0c;将物体的三维信息记录下来&#xff0c;并在特定条件下再现。应用参数包括投影距离、投影面积、投影亮度等。设计制作要求&#xff1a;高清晰度、高亮度、低噪音、稳定性好…

新能源汽车充换站如何实现光储充一体化管理?

长三角某换电站光伏板晒到发烫&#xff0c;却因电网限电被迫切机&#xff1b;北京五环充电站每月多缴6万超容费&#xff1b;深圳物流车充电高峰排队3小时...当95%的充换站深陷“用不起绿电、扛不住扩容、算不清碳账”困局&#xff0c;安科瑞用一组真实数据撕开行业潜规则&#…

二手Mac验机过程

1.1 外观检查 螺丝是否拧过螺丝 1.2 关于本机中 序列号&#xff0c;盒子序列号&#xff0c;机器背部 核对参数 https://checkcoverage.apple.com/coverage 1.3 检查apple ID与查找 1 登出 iCloud、iTunes、FaceTime、iMessage 在 Mac 上打開「訊息」應用程式&#xff0c;從上方…

C语言-状态模式详解与实践 - OTA升级状态机

文章目录 C语言状态模式详解与实践 - OTA升级状态机1. 什么是状态模式&#xff1f;2. 为什么需要状态模式&#xff1f;3. 实际应用场景4. 代码实现4.1 UML 关系图4.2 头文件 (ota_state.h)4.3 实现文件 (ota_state.c)4.4 使用示例 (main.c) 5. 代码分析5.1 关键设计点5.2 实现特…

数据结构5(初):续写排序

目录 1、外排序 2、计数排序 1、外排序 上一节中提到的排序都可以用来进行内排序&#xff0c;但是只有归并排序的思想可以用来进行外部排序&#xff0c;因为文件数据是没办法像数组那样进行访问的。 例如&#xff1a; #include <stdio.h> #include <assert.h> …

《当人工智能遇上广域网:跨越地理距离的通信变革》

在数字化时代&#xff0c;广域网作为连接全球信息的纽带&#xff0c;让数据能够在不同地区的网络之间流动。然而&#xff0c;地理距离给广域网数据传输带来诸多挑战&#xff0c;如高延迟、低带宽、信号衰减和不稳定等问题。幸运的是&#xff0c;飞速发展的人工智能技术为解决这…

Linux冯诺依曼体系与计算机系统架构认知(8)

文章目录 前言一、冯诺依曼体系冯•诺依曼体系结构推导内存提高冯•诺依曼体系结构效率的方法你用QQ和朋友聊天时数据的流动过程与冯•诺依曼体系结构相关的一些知识 二、计算机层次结构分析操作系统(Operator System)驱动层的作用与意义系统调用接口(system call)用户操作接口…

OpenCV的基本用法全解析

《小白入门&#xff1a;OpenCV的基本用法全解析》 嗨&#xff0c;朋友们&#xff01;之前咱们知道了OpenCV在机器视觉里就像个超级厉害的瑞士军刀&#xff0c;那今天咱们就来好好唠唠&#xff0c;**OpenCV到底该怎么用呢&#xff1f;**这就像是拿到了一把好剑&#xff0c;咱们…

汇川EASY系列之以太网通讯(MODBUS_TCP做从站)

汇川easy系列PLC做MODBUS_TCP从站,不需要任何操作,但是有一些需要知道的东西。具体如下: 1、汇川easy系列PLC做MODBUS_TCP从站,,ModbusTCP服务器默认开启,无需设置通信协议(即不需要配置),端口号为“502”。ModbusTCP从站最多支持31个ModbusTCP客户端(ModbusTCP主站…

在 Offset Explorer 中配置多节点 Kafka 集群的详细指南

一、是否需要配置 Zookeeper&#xff1f; Kafka 集群的 Zookeeper 依赖性与版本及运行模式相关&#xff1a; Kafka 版本是否需要 Zookeeper说明0.11.x 及更早版本✅ 必须配置Kafka 完全依赖 Zookeeper 管理元数据2.8 及以下版本✅ 必须配置Kafka 依赖外置或内置的 Zookeeper …

前端-选中pdf中的文字并使用,显示一个悬浮的翻译按钮(本地pdfjs+iframe)不适用textlayer

使用pdfjs移步– vue2使用pdfjs-dist实现pdf预览&#xff08;iframe形式&#xff0c;不修改pdfjs原来的ui和控件&#xff0c;dom层可以用display去掉一部分组件&#xff09; 方案1&#xff1a;获取选择文本内容的最前面的字符坐标的位置&#xff08;这种写法会导致如果选择超出…

生活电子常识-deepseek-r1本地化部署+ui界面搭建

前言 deepseek-r1 14b模型&#xff0c;32b模型部署在本地电脑上也能实现非常好的性能。 因此有兴趣研究了下如何在本地部署。 同时最新流行mauns工作流&#xff0c;他们提供一句话实现网页端任意应用的能力。实际上&#xff0c;你也可以用本地的模型来实现离线的ai工作流功能。…

mac丝滑安装Windows操作系统【丝滑简单免费】

mac丝滑安装Windows操作系统【丝滑&简单&免费】 记录mac丝滑安装windows系统1、安装免费版 VMware fusion 132、安装Windows镜像文件3、跳过联网安装&#xff08;完成1后将2拖入1 点点点 即可来到3的环节&#xff09;4、 安装vmware 工具【非常重要&#xff0c;涉及联网…

基于Spring Boot的企业内管信息化系统的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

Pytorch实现之对称卷积神经网络结构实现超分辨率

简介 简介:针对传统的超分辨率重建技术所重建的图像过于光滑且缺乏细节的问题,作者提出了一种改进的生成对抗图像超分辨率网络。 该改进方法基于深度神经网络,其生成模型包含多层卷积模块和多层反卷积模块,其中在感知损失基础上增加了跳层连接和损失函数。 该判别模型由多…

Scikit-learn模型构建全流程解析:从数据预处理到超参数调优

模型选择与训练步骤及示例 1. 数据准备与探索 步骤说明&#xff1a;加载数据并初步探索其分布、缺失值、异常值等。 注意事项&#xff1a; 检查数据类型&#xff08;数值/类别&#xff09;、缺失值和异常值。对类别型特征进行编码&#xff08;如独热编码&#xff09;。 实例&…

001-JMeter的安装与配置

1.前期准备 下载好JMeter : https://jmeter.apache.org/download_jmeter.cgi 下载好JDK : :Java Downloads | Oracle 中国 下载图中圈蓝的JMeter和JDK就行&#xff0c;让它边下载&#xff0c;我们边往下看 2.为什么要下载并安装JDK ? JMeter 是基于 Java 开发的工具&#…