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

news2025/3/20 14:24:47

文章目录

  • 前言
  • 一、递归本质
    • 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.coloradmin.cn/o/2318416.html

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

相关文章

YOLO+OpenCV强强联手:高精度跌倒检测技术实战解析

目录 关于摔倒检测 摔倒检测核心逻辑 摔倒检测:联合多种逻辑判断 原理详细解释 1. 导入必要的库 2. 定义函数和关键点连接关系 3. 筛选有效关键点并计算边界框 4. 计算人体上下半身中心点和角度 5. 绘制关键点和连接线 6. 绘制角度标注和检测跌倒 7. 返回处理后的图…

麒麟银河V10服务器RabbitMQ安装

安装步骤 rabbitMQ依赖于erlang的环境&#xff0c;所以需要先安装erlang&#xff0c;erlang跟rabbitMQ是有版本之间的关联关系的&#xff0c;根据对应的版本去安装下载&#xff0c;保证少出问题。 可以通过官网来查看RabbitMQ和erlang之间的版本对应关系 rabbitMQ和erlang之间…

extern和static的作用(有例子)

一、extern extern的作用 声明而非定义 extern告诉编译器某个变量或函数存在于其他地方&#xff08;通常是另一个源文件&#xff09;&#xff0c;当前只是声明它&#xff0c;而不是定义它&#xff08;分配内存&#xff09;。定义只能在一个地方出现&#xff0c;而声明可以多次…

【8】分块学习笔记

前言 分块是一种重要的高级数据结构思想&#xff0c;核心为大段维护&#xff0c;局部朴素。 顺带一提&#xff0c;由于个人技术水平,本篇博客的难度并没有标题所述的 8 8 8 级。分块还是很难的。 分块 分块&#xff0c;是“优雅的暴力”。 分块的基本思想是把数据分为若干…

【蓝桥杯】省赛:分糖果(思维/模拟)

思路 数据很小&#xff0c;直接暴力模拟。 有意思的是一个列表如何当成循环队列写&#xff1f;可以arr[(i1)%n]让他右边超出时自动回到开头。 code import os import sysn int(input()) arr list(map(int,input().split()))ans 0 while 1:arr1 arr.copy()for i in range…

进程间通信(1)——管道

1. 进程间通信简介 进程间通信&#xff08;Inter-Process Communication&#xff0c;IPC&#xff09;是指不同进程之间交换数据的机制。由于进程具有独立的地址空间&#xff0c;它们无法直接访问彼此的数据&#xff0c;因此需要IPC机制来实现信息共享、数据传递或同步操作。 …

【正点原子K210连载】第七十六章 音频FFT实验 摘自【正点原子】DNK210使用指南-CanMV版指南

第七十六章 音频FFT实验 本章将介绍CanMV下FFT的应用&#xff0c;通过将时域采集到的音频数据通过FFT为频域。通过本章的学习&#xff0c;读者将学习到CanMV下控制FFT加速器进行FFT的使用。 本章分为如下几个小节&#xff1a; 32.1 maix.FFT模块介绍 32.2 硬件设计 32.3 程序设…

【杂记二】git, github, vscode等

一、前言 暂时空着... 二、git 2.1 可能的疑问 1. VSCode 项目名和 GitHub 仓库名是否需要一致&#xff1f; 不需要一致。 VSCode 项目名&#xff08;也就是你本地的文件夹名字&#xff09;和 GitHub 仓库名可以不一样。 Git 是一个分布式版本控制系统&#xff0c;它主要关…

《基于Spring Boot+Vue的智慧养老系统的设计与实现》开题报告

个人主页:@大数据蟒行探索者 一、研究背景及国内外研究现状 1.研究背景 根据1982年老龄问题世界大会联合国制定的标准,如果一个国家中超过65岁的老人占全国总人口的7%以上,或者超过60岁的老人占全国总人口的10%以上,那么这个国家将被定义为“老龄化社会”[1]。 随着国…

ModBus TCP/RTU互转(主)(从)|| Modbus主动轮询下发的工业应用 || 基于智能网关的串口服务器进行Modbus数据收发的工业应用

目录 前言 一、ModBus TCP/RTU互转&#xff08;从&#xff09;及应用|| 1.1 举栗子 二、ModBus TCP/RTU互转&#xff08;主&#xff09; 2.1 举栗子 三、ModBus 主动轮询 3.1 Modbus主动轮询原理 3.2 Modbus格式上传与下发 3.2.1.设置Modbus主动轮询指令 3.2.2 设…

【HarmonyOS Next之旅】DevEco Studio使用指南(三)

目录 1 -> 一体化工程迁移 1.1 -> 自动迁移 1.2 -> 手动迁移 1.2.1 -> API 10及以上历史工程迁移 1.2.2 -> API 9历史工程迁移 1 -> 一体化工程迁移 DevEco Studio从 NEXT Developer Beta1版本开始&#xff0c;提供开箱即用的开发体验&#xff0c;将SD…

冯・诺依曼架构深度解析

一、历史溯源&#xff1a;计算机科学的革命性突破 1.1 前冯・诺依曼时代 在 1940 年代之前&#xff0c;计算机领域呈现 "百家争鸣" 的格局&#xff1a; 哈佛 Mark I&#xff08;1944&#xff09;&#xff1a;采用分离的指令存储与数据存储ENIAC&#xff08;1946&a…

C++ 语法之函数和函数指针

在上一章中 C 语法之 指针的一些应用说明-CSDN博客 我们了解了指针变量&#xff0c;int *p;取变量a的地址这些。 那么函数同样也有个地址&#xff0c;直接输出函数名就可以得到地址&#xff0c;如下&#xff1a; #include<iostream> using namespace std; void fun() …

网络协议抓取与分析(SSL Pinning突破)

1. 网络协议逆向基础 1.1 网络协议分析流程 graph TD A[抓包环境配置] --> B[流量捕获] B --> C{协议类型} C -->|HTTP| D[明文解析] C -->|HTTPS| E[SSL Pinning突破] D --> F[参数逆向] E --> F F --> G[协议重放与模拟] 1.1.1 关键分析目标…

蓝桥杯真题——洛谷Day13 找规律(修建灌木)、字符串(乘法表)、队列(球票)

目录 找规律 P8781 [蓝桥杯 2022 省 B] 修剪灌木 字符串 P8723 [蓝桥杯 2020 省 AB3] 乘法表 队列 P8641 [蓝桥杯 2016 国 C] 赢球票 找规律 P8781 [蓝桥杯 2022 省 B] 修剪灌木 思路&#xff1a;对某个特定的点来说有向前和向后的情况&#xff0c;即有向前再返回到该位置…

【2025】基于Springboot + vue实现的毕业设计选题系统

项目描述 本系统包含管理员、学生、教师三个角色。 管理员角色&#xff1a; 用户管理&#xff1a;管理系统中所有用户的信息&#xff0c;包括添加、删除和修改用户。 配置管理&#xff1a;管理系统配置参数&#xff0c;如上传图片的路径等。 权限管理&#xff1a;分配和管理…

JAVA并发编程 --- 补充内容

1 线程状态 1.1 状态介绍 当线程被创建并启动以后&#xff0c;它既不是一启动就进入了执行状态&#xff0c;也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢&#xff1f;Java中的线程 状态被定义在了java.lang.Thread.State枚…

【ArduPilot】Windows下使用Optitrack通过MAVProxy连接无人机实现定位与导航

Windows下使用Optitrack通过MAVProxy连接无人机实现定位与导航 配置动捕系统无人机贴动捕球配置无人机参数使用MAVProxy连接Optitrack1、连接无人机3、设置跟踪刚体ID4、校正坐标系5、配置IP地址&#xff08;非Loopback模式&#xff09;6、启动动捕数据推流 结语 在GPS信号弱或…

qt 图像后处理的软件一

这是一个图像后处理软件刚刚&#xff0c;目前功能比较单一&#xff0c;后续会丰富常用的功能。 目前实现的功能有 1.导入图像 2图像可中心缩放&#xff08;右上角放大缩小&#xff0c;按钮及滚轮双重可控&#xff09;。 3.图像重置功能 软件界面如下。 代码放在我的资源里…

Ardunio 连接OLED触摸屏(SSD1106驱动 4针 IIC通信)

一、准备工作 1、硬件 UNO R3 &#xff1a;1套 OLED触摸屏&#xff1a;1套 导线诺干 2、软件 arduino 二、接线 UNO R3OLED5VVCCGNDGNDA5SCLA4SDA 脚位如下图所示&#xff1a; Uno R3脚位图 触摸屏脚位图 查阅显示屏的驱动规格&#xff1a;通常显示屏驱动芯片有SSD1306,SH110…