最全面的递归算法详解,一篇足矣(高手必备)

        在编程中,递归和循环是两种常用的控制结构,各有其独特的优缺点。理解这两者的特点和应用场景,对于编写高效、可读的代码至关重要。

什么是递归?

递归是一种强大的编程技术,允许函数在其定义中调用自身。递归通常涉及两个主要部分:

  1. 基本情况:这是递归的终止条件,当满足此条件时,函数将不再调用自身。
  2. 递归情况:这是函数调用自身的部分,通常会将问题规模缩小。

 

如何写出递归?

只需要做到下面两点

1.终止条件;

2.一般规律,也就是需要重复执行循环的部分。

递归的优点

  • 简洁性:递归可以使代码更加简洁,尤其是在处理复杂数据结构(如树和图)时。
  • 可读性:递归代码通常比迭代代码更易于理解,因为它直接反映了问题的定义。
  • 解决复杂问题:许多算法(如排序和搜索)可以通过递归轻松实现。

递归的缺点

  • 性能问题:递归可能导致大量的函数调用,消耗更多的内存和时间,特别是在没有优化的情况下。
  • 栈溢出:深度递归可能导致栈溢出错误,因为每次函数调用都会占用栈空间。

Java中的递归实现 

以下是一些简单的递归示例,展示了如何在Java中实现递归算法。

1. 计算阶乘

public static int factorial(int n) {if (n == 1) {return 1;} else {return n * factorial(n - 1);}
}

2. 斐波那契数列

public static int fibonacci(int n) {if (n == 1 || n == 2) {return 1;} else {return fibonacci(n - 1) + fibonacci(n - 2);}
}

3. 反向打印字符串

public static void reversePrint(String str) {if (str.length() == 0) {return;}reversePrint(str.substring(1));System.out.print(str.charAt(0));
}

4. 1递归二分查找

//递归二分查找public static int binarySearch(int[] arr,int i, int j,int target) {int m = (i+j) >> 1;if (i > j)return -1;if (arr[m] < target)return binarySearch(arr, m+1, j, target);else if (arr[m] > target)return binarySearch(arr, i, m-1, target);elsereturn m;}

4.2普通二分查找

如果想了解二分查找的可以去我的这一篇,传送门

public static int binarySearchIterative(int[] arr, int target) {int left = 0;int right = arr.length - 1;while (left <= right) { // 终止条件int mid = left + (right - left) / 2; // 一般规律if (arr[mid] == target) {return mid; // 找到目标值} else if (arr[mid] < target) {left = mid + 1; // 在右半部分查找} else {right = mid - 1; // 在左半部分查找}}return -1; // 目标值未找到
}

5.1递归冒泡排序

//递归冒泡排序public static void bubbleSort(int[] arr,int j) {if (j == 0) //j代表未排序区域的右边界return;int x = 0; //x代表最后一次交换的位置,可以认为是已排序区域和未排序区域的分界线for (int i = 0; i < j; i++) {if (arr[i] > arr[i+1]) {int temp = arr[i];arr[i] = arr[i+1];arr[i+1] = temp;x = i;}}bubbleSort(arr, x);}

递归冒泡排序引入变量 x 的好处主要体现在以下几个方面:

  1. 减少不必要的遍历

    • 在传统的冒泡排序中,每一轮遍历都会将最大的元素“冒泡”到未排序区域的末尾。然而,在某些情况下,未排序区域的前面部分可能已经是有序的。引入 x 变量后,x 记录了最后一次交换的位置,这意味着从 x 之后的元素已经是有序的。因此,下一轮递归只需要处理到 x 位置,而不需要再遍历整个未排序区域,从而减少了不必要的比较和交换操作。
  2. 提高效率

    • 通过减少每一轮的遍历范围,递归冒泡排序的效率得到了提升。特别是在数组接近有序的情况下,这种优化尤为明显。
  3. 简化代码逻辑

    • 引入 x 变量后,代码逻辑更加清晰。x 作为已排序区域和未排序区域的分界线,使得递归调用的参数更加明确,便于理解和维护。

总结来说,引入 x 变量使得递归冒泡排序在处理接近有序的数组时更加高效,减少了不必要的操作,提高了算法的性能。

 

5.2普通循环冒泡排序

public static void bubbleSortIterative(int[] arr) {int n = arr.length;for (int i = 0; i < n - 1; i++) {for (int j = 0; j < n - 1 - i; j++) {if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}

 递归与循环的比较

特性递归实现循环实现
可读性通常更简洁,易于理解问题的结构可能较冗长,但在简单情况下更直观
性能可能导致栈溢出,尤其在深度递归时通常更高效,避免了函数调用的开销
终止条件明确的基本情况,通常是一个简单的条件循环条件,通常是一个范围或计数器
一般规律通过递归关系定义问题,通常简洁明了通过迭代逻辑处理问题,可能需要更多的代码行

多路递归

以斐波那契数列为例,让我们来了解一下什么是多路递归。

斐波那契数列是一个经典的递归问题,定义如下:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) 对于 n >= 2

单路递归

        单路递归是指函数在其定义中仅调用自身一次。换句话说,函数的递归调用只有一个分支。例如,计算阶乘的递归实现就是单路递归:

public static int factorial(int n) {if (n == 0 || n == 1) return 1; // 终止条件return n * factorial(n - 1); // 单路递归调用
}

多路递归

        多路递归是指一个函数在递归调用时,同时调用多个子问题。在斐波那契数列的例子中,fibonacci(n) 同时调用了 fibonacci(n - 1) 和 fibonacci(n - 2),这两个调用是并行的,即它们是同时进行的。

public static int fibonacci(int n) {if (n == 0) return 0;if (n == 1) return 1;return fibonacci(n - 1) + fibonacci(n - 2);
}

多路递归的特点

  1. 并行调用

    • 在多路递归中,函数会同时调用多个子问题,这些子问题是并行进行的。
  2. 重复计算

    • 由于多个子问题可能会重复计算相同的子问题,多路递归可能会导致大量的重复计算,效率较低。例如,在计算 fibonacci(5) 时,fibonacci(3) 会被计算多次。
  3. 空间复杂度

    • 多路递归的空间复杂度较高,因为每次递归调用都会在调用栈中占用一定的空间。

 

        可以看到,我们计算了f(5)时已经把f(4),f(3),f(2),f(1)计算过一遍了,而我们计算f(4)时又要重新计算,这就多了许多重复的执行步骤,我们可以考虑优化一下 。

优化多路递归

        为了优化多路递归,可以使用记忆化技术(Memoization)或动态规划(Dynamic Programming)来避免重复计算。例如:

public static int fibonacci(int n, int[] memo) {if (n == 0) return 0;if (n == 1) return 1;if (memo[n] != 0) return memo[n];memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);return memo[n];
}

         在这个优化版本中,我们使用了一个数组 memo 来存储已经计算过的斐波那契数,从而避免了重复计算,提高了效率。 

         

        所谓记忆化就是,我们可以用一个数组来储存已经计算过的值,再次用到的时候直接拿出用即可,与没有优化前的进行对比,可以看得出分支明显减少了许多,大大提高了效率。

总结

        多路递归是指一个函数在递归调用时,同时调用多个子问题。在斐波那契数列的例子中,fibonacci(n) 同时调用了 fibonacci(n - 1) 和 fibonacci(n - 2),这种并行调用的方式就是多路递归。多路递归可能会导致重复计算和较高的空间复杂度,但可以通过记忆化或动态规划来优化。

递归-爆栈问题 

        递归中的爆栈问题(Stack Overflow)是指在递归调用过程中,由于递归深度过大,导致调用栈(Call Stack)空间耗尽,从而引发程序崩溃或异常。

调用栈的作用

        调用栈是计算机程序在执行过程中用于管理函数调用的一种数据结构。每当一个函数被调用时,系统会在调用栈中为该函数分配一块内存空间,用于存储函数的局部变量、返回地址等信息。当函数执行完毕后,这块内存空间会被释放。

递归调用的特点

        在递归调用中,函数会不断地调用自身,每次调用都会在调用栈中分配一块新的内存空间。如果递归深度过大,调用栈中的内存空间会不断累积,最终可能导致调用栈空间耗尽,引发爆栈问题。

爆栈问题的示例

以计算阶乘为例:

public static int factorial(int n) {if (n == 0) return 1;return n * factorial(n - 1);
}

        在这个递归函数中,factorial(n) 会调用 factorial(n - 1)factorial(n - 1) 会调用 factorial(n - 2),依此类推,直到 n 减到 0。如果 n 非常大,递归深度会非常深,调用栈的空间会迅速累积,最终可能导致爆栈问题。

        可以看到数字过大时,递归调用过深,内存空间不够,会爆出异常,这就是爆栈。

爆栈问题优化-尾递归

        尾递归优化(Tail Recursion Optimization)是一种针对递归函数的优化技术,它通过将递归调用转换为迭代形式,从而避免在调用栈中累积大量的内存空间,避免爆栈问题。

尾递归的特点

        尾递归是指递归调用是函数的最后一个操作。换句话说,递归调用返回的结果直接作为函数的返回值,不再进行任何其他操作。

尾递归优化的原理

        尾递归优化的原理是通过编译器或解释器的优化,将尾递归调用转换为迭代形式。具体来说,编译器或解释器会在编译或解释阶段,将尾递归函数转换为一个循环结构,从而避免在调用栈中累积大量的内存空间。

尾递归优化的示例

以计算阶乘为例,传统的递归实现如下:

public static int factorial(int n, int result) {if (n == 0) return result;return factorial(n - 1, n * result);
}

        在这个实现中,factorial(n, result) 调用 factorial(n - 1, n * result) 后,直接返回递归调用的结果,不再进行其他操作,因此这是尾递归。

尾递归优化的效果

尾递归优化可以显著减少调用栈的使用,避免爆栈问题。具体效果如下:

  1. 减少调用栈空间

    • 由于尾递归调用是函数的最后一个操作,编译器或解释器可以将递归调用转换为迭代形式,从而避免在调用栈中累积大量的内存空间。
  2. 提高性能

    • 尾递归优化可以减少函数调用的开销,提高程序的执行效率。
  3. 避免爆栈问题

    • 尾递归优化可以有效避免递归深度过大导致的爆栈问题,提高程序的稳定性和可靠性。

支持尾递归优化的语言

        并非所有编程语言都支持尾递归优化,很遗憾的是Java并不支持尾递归优化哈,以下是一些支持尾递归优化的编程语言:

  • Scheme:Scheme 语言规范要求实现必须支持尾递归优化。
  • Haskell:Haskell 通过惰性求值和尾递归优化来避免爆栈问题。
  • Scala:Scala 编译器支持尾递归优化。
  • Erlang:Erlang 虚拟机(BEAM)支持尾递归优化。
  • C++:C++ 编译器通常支持尾递归优化,但这取决于具体的编译器和编译选项。尾递归优化通常是通过编译器的优化选项来实现的。

总结

        爆栈问题就是由于递归调用深度过高,导致调用栈的内存空间不够用,从而引发的程序崩溃或异常。解决爆栈问题的方法包括尾递归优化、迭代替代递归和限制递归深度等。通过这些方法,可以有效避免递归中的爆栈问题,提高程序的稳定性和性能。

 

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

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

相关文章

nvm ls-remote: N/A

背景&#xff1a; 项目因为node版本问题运行失败&#xff0c;在彻底删除node后再重新安装 问题描述&#xff1a; 原因分析&#xff1a; 可能是因为终端不能获取镜像包 解决办法&#xff1a; 【方法一】 输入&#xff1a; step1. export NVM_NODEJS_ORG_MIRRORIndex of …

常用电路及分析

前言 最近在研究一些简单的硬件知识&#xff0c;把在网上看到的一些常见电路分析总结了一下。 有纰漏请指出&#xff0c;转载请说明。 学习交流请发邮件 1280253714qq.com 串联稳压电路 三极管串联线性稳压电路原理详解及Multisim仿真_三极管稳压电路-CSDN博客 线性稳压电…

LeetCode 206. 反转链表

题目描述 分析 迭代代码与之前的K个一组翻转链表相同。 递归代码的一个首要任务是找到整个链表的尾结点&#xff08;反转后的头结点&#xff09;。 之后一步一步地将tail结点向前返回&#xff0c;但在返回的过程中不利用&#xff0c;只是传递最终答案。绿线的操作就是当head…

空间数据库概述

空间数据库简介 空间数据库是 地理信息系统 在计算机物理存储介质中存储的&#xff0c;与GIS应用相关的地理空间数据的总和。一般以一系列特定结构的文件形式组织后存储在介质上。 空间数据库的特点 可以存储、处理空间数据相比普通数据库提供更多、更复杂的数据类型以及更多…

即插即用篇 | YOLOv8 引入维度互补注意力混合Transformer模块 | 轻量级互补注意力网络:RAMiT引领图像修复新突破

本改进已同步到YOLO-Magic框架! 摘要:虽然许多近期的研究在图像修复(IR)领域取得了进展,但它们通常存在参数数量过多的问题。另一个问题是,大多数基于Transformer的图像修复方法只关注局部或全局特征,导致感受野有限或参数不足的问题。为了解决这些问题,我们提出了一种…

Linux_kernel移植rootfs10

一、动态更改内核 1、low level&#xff08;静态修改&#xff09; 【1】将led_drv.c拷贝到kernel/drivers/char/目录中 【2】修改当前目录下的Makefile文件 obj-y led_drv.o #将新添加的驱动文件加入到Makefile文件中 【3】退回kernel目录&#xff0c;执行make uImage …

2024.9.11(k8s环境搭建)

一、k8s环境搭建 编号主机名称ip配置1k8s-master192.168.8.1772k8s-node1192.168.8.1783k8s-node2192.168.8.168 1、免密登录 [rootk8s-master ~]# ssh-keygen [rootk8s-master ~]# ssh-copy-id root192.168.8.178 [rootk8s-master ~]# ssh-copy-id root192.168.8.168 2、3台…

西安近期学术会议,诚邀学者参会投稿!

第十二届信息系统与计算技术国际会议&#xff08;ISCTech 2024&#xff09;由长沙理工大学主办&#xff0c;联合同济大学、西北工业大学、江西农业大学协办&#xff0c;并由IEEE西安分会提供技术支持&#xff0c;会议将于11月8日至11日在中国西安隆重举行。ISCTech系列会议自创…

Golang | Leetcode Golang题解之第392题判断子序列

题目&#xff1a; 题解&#xff1a; func isSubsequence(s string, t string) bool {n, m : len(s), len(t)f : make([][26]int, m 1)for i : 0; i < 26; i {f[m][i] m}for i : m - 1; i > 0; i-- {for j : 0; j < 26; j {if t[i] byte(j a) {f[i][j] i} else {…

java设计模式(行为型模式:状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式、解释器模式)

6&#xff0c;行为型模式 6.5 状态模式 6.5.1 概述 【例】通过按钮来控制一个电梯的状态&#xff0c;一个电梯有开门状态&#xff0c;关门状态&#xff0c;停止状态&#xff0c;运行状态。每一种状态改变&#xff0c;都有可能要根据其他状态来更新处理。例如&#xff0c;如果…

突破性进展!只需单张参考图,完美仿写各种手写内容!华南理工等开源One-DM

文章链接&#xff1a;https://arxiv.org/pdf/2409.04004 git链接&#xff1a;https://github.com/dailenson/One-DM 亮点直击 提出一种创新的扩散模型&#xff0c;用于生成风格化的手写文本。这一模型的显著特点是只需一个参考样本作为风格输入&#xff0c;便能模仿该样本的书写…

索引:数据库查询性能提升的利器

在数据库的世界里&#xff0c;索引就像是一把神奇的钥匙&#xff0c;能够极大地提高查询性能。那么&#xff0c;什么是索引呢&#xff1f;它又是如何发挥作用的呢&#xff1f;让我们一起来揭开索引的神秘面纱。 一、什么是索引&#xff1f; 索引&#xff0c;简单来说&#xf…

【机器学习-监督学习】集成学习与梯度提升决策树

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈Python机器学习 ⌋ ⌋ ⌋ 机器学习是一门人工智能的分支学科&#xff0c;通过算法和模型让计算机从数据中学习&#xff0c;进行模型训练和优化&#xff0c;做出预测、分类和决策支持。Python成为机器学习的首选语言&#xff0c;…

chapter14-集合——(List-HashSet)——day18

目录 519-HashSet全面说明 520-数组链表模拟 521-HashSet扩容机制 重要 522-HashSet源码解读1 526-HashSet最佳实践 527-hashSet思考题 519-HashSet全面说明 题一、 两个tom都可以添加成功是因为这是两个对象 看源码做分析&#xff1a;不是直接指向常量池的吗&#xff1f;…

2024/9/9 408“回头看”:b树

B树是什么&#xff1f;有什么作用&#xff1f;B树的插入和删除具体细节是什么&#xff1f;除了B树还有一个是B&#xff0b;树、还是B-树&#xff0c;他们有什么区别&#xff0c;又有什么相同点&#xff1f; b树在王道考研查找这一章&#xff0c;所以他的主要作用就是查找。 在…

【python】OpenCV—Age and Gender Classification

文章目录 1、任务描述2、网络结构2.1 人脸检测2.2 性别分类2.3 年龄分类 3、代码实现4、结果展示5、参考 1、任务描述 性别分类和年龄分类预测 2、网络结构 2.1 人脸检测 输出最高的 200 个 RoI&#xff0c;每个 RoI 7 个值&#xff0c;&#xff08;xx&#xff0c;xx&#x…

基于SpringBoot+Vue+MySQL的校园生活服务平台

系统展示 用户前台界面 管理员后台界面 系统背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上不可跨域的鸿沟&#xff0c;信息的…

C++当中的多态(一)

&#xff08;一&#xff09;什么是多态 1.现实中的多态&#xff1a; 所谓的多态在我们的生活当中其实很常见。举一个简单的例子&#xff1a;当我们需要买票的时候有很多种不同的票可以供我们购买&#xff0c;如果你是学生就可以享受半价票的优惠&#xff0c;如果你是VIP用户就可…

Leetcode 只出现一次的元素

题目要求我们找到数组中只出现了一次的元素&#xff0c;而其他元素都出现了两次。 解题思路&#xff1a; 我们可以使用位运算中的异或操作&#xff08;XOR&#xff09;。异或操作有以下两个特性&#xff1a; 相同的两个数字异或结果为0&#xff0c;例如&#xff1a;a ^ a 0…

Android12——Launcher3文件夹布局修改调整

文章声明&#xff1a;本文是笔者参考良心大佬作品后结合实际需求进行相应的定制&#xff0c;本篇主要是笔者记录一次解析bug笔记&#xff0c;文中可能会引用大佬文章中的部分图片在此声明&#xff0c;并非盈利目的&#xff0c;如涉嫌侵权请私信&#xff0c;谢谢&#xff01; 大…