迭代与递归
算法中会经常遇见重复执行某个任务,那么如何实现呢,本文将详细介绍两种实现方式,迭代与递归。
本文基于 Java 语言。
一、迭代
迭代(iteration),就是说程序会在一定条件下重复执行某段代码,直到条件不再满足。
在 Java 语言中,可以理解为就是循环遍历,Java 中有多种遍历方式,如 for 循环、while 循环。接下来讲解如何进行代码实现。
(一) for 循环
这个是最常见的迭代形式之一,适合在预先知道迭代次数(遍历次数)时使用。
比如,我们通过 for 循环计算1 + 2 + ... + n
的结果。
public int forLoop(int n){int result = 0;for (int i = 0; i <= n; i++) {result += i;}return result;
}
流程图如下:
(二) while 循环
与 for 循环类似,while 循环也是一种实现迭代的方法。在 while 循环中,程序每轮都会先检查条件,如果条件为真,则继续执行,否则就结束循环。
比如,我们通过 while 循环计算1 + 2 + ... + n
的结果。
public int whileLoop(int n) {int result = 0;int i = 1; // 初始化条件变量while (i <= n) {result += i;i++; // 更新条件变量}return result;
}
(三) 嵌套循环
我们可以在一个循环结构内嵌套另一个循环结构,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”“四次方关系”,以此类推。
下面以冒泡排序为例,原理是从后两两对比,更小的往前放。数组内位置交换,不懂得话看一下我总结的另一篇文字--《位运算详解》。
public class FuXing {public static void main (String[] args) {int[] arr = {9,6,1,5,2,3,4,7,8};System.out.println("排序后:" + Arrays.toString( bubbleSort(arr)));}/*** 冒泡排序*/public static void bubbleSort (int[] arr){// 过滤无序排序的数组if(arr == null || arr.length < 2){return;}// 倒序遍历所有的数for (int i = arr.length - 1; i > 0; i--) {//两两比较,更小的往前放for (int j = 0; j < i; j++) {if(arr[j] > arr[j+1]){swap(arr,j,j+1);}}}}/*** 数组内位置交换*/public static void swap(int[] arr,int i ,int j){arr[i] ^= arr[j];arr[j] ^= arr[i];arr[i] ^= arr[j];}
}
二、递归
(一) 简介
递归(recursion)是一种算法策略,通过直接或者间接地调用自身来解决问题,将大问题分解成更多的子问题,主要解决同一大类问题。
从数据结构角度看,递归天然适合处理链表、树和图的相关问题,因为它们非常适合用分治思想进行分析。
主要包含两个阶段:
- 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
- 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
递归的三个重要要素(须记住):
- 终止条件:用于决定什么时候由“递”转“归”。
- 递归调用:对应“递”,函数调用自身。
- 返回结果:对应“归”,将当前递归层级的结果返回至上一层
(二) 如何设计递归
在写一些递归算法时,应该如何去操作呢?这里分享一点个人经验,当我们需要写一个递归程序时:
- 找重复:找到的相同的子问题;
- 找变化:聚焦于某一个子问题,查看变化的量,通常会作为参数,这时可定义函数体;
- 找出口:也就是找终止条件,这里注意关注返回值。
我们需要明确递归函数本身是为了做什么,返回值是什么,从最终想要的答案出发,逐步寻找上一层的答案,并且用它们构造当前层的答案。
比如,我们通过递归计算1 + 2 + ... + n
结果。
- 找重复:n 的累加 = n + (n - 1)的累加,n - 1 就是原问题的重复,即子问题;
- 找变化:聚焦于某一个子问题,这里的变化就是 n 的自减,递归参数就是 n - 1;
- 找出口:当返回值为 1 时,终止条件就是
n = 1
,n 为 1 时无法计算阶乘。
代码如下:
public int recursion (int n) {//终止条件if (n == 1) return 1;//递:递归调用int result = recursion(n - 1);//归:返回结果return result + n;}
流程图如下:
(三) 调用栈
在 Java 中,递归函数每次调用自身时,JVM 都会为新开启的函数分配内存,以存储局部变量、调用地址和其他信息等。也就是在 Java 虚拟机栈中新生成一个栈帧。
因为栈空间是随着函数结果返回才会释放,所以递归通常比迭代更加消耗内存空间。且调用递归函数会产生额外的开销,因此时间效率上也会受影响。过深的递归操作,可能导致栈内存溢出。
(四) 尾部递归
如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为尾递归(tail recursion)。
递归方式 | 说明 |
普通递归 | 当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。 |
尾递归 | 递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无须继续执行其他操作,因此系统无须保存上一层函数的上下文。 |
比如,还是通过递归计算1 + 2 + ... + n
结果。尾部递归的求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。而普通递归每层返回后都要再执行一次求和操作。
代码如下:
public int tailRecursion (int n, int result) {// 终止条件if (n == 0) return res;// 尾递归调用return tailRecursion(n - 1, res + n);}
流程图如下:
(五) 递归树
当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。以“斐波那契数列”为例。
斐波那契数列是指这样一个数列:0,1,1,2,3,5,8,13,21,34… 这个数列从第3项开始 ,每一项都等于前两项之和,求该数列的第 n 个数字。
回顾一下上面说的方法,进行设计递归函数:
1. 找重复:原问题的重复,即子问题。我们设斐波那契数列的第 n 个数字为 f(n),可得:
f(3) = f(2) + f(1) = 0
;f(4) = f(3) + f(2) = 2
;f(n) = f(n - 1) + f(n - 2)
;
这里每一项都等于前两项之和
2. 找变化:聚焦于某一个子问题,查看变化的量,会作为参数,这时可定义函数体。
如,f(n) = f(n - 1) + f(n - 2)
,这里的变化量有两个,n -1 和 n - 2,即分别做为入参。
函数体如下:
int fib(int n) {// 递归调用 f(n) = f(n-1) + f(n-2)int res = fib(n - 1) + fib(n - 2);// 返回结果 f(n)return res;
}
3. 找出口:当返回值为 1 或 2 时,则会终止条件,n 为 1 或 2 时无法拆分为子问题,则完整函数如下。
int fib(int n) {// 终止条件 f(1) = 0, f(2) = 1if (n == 1 || n == 2) return n - 1;// 递归调用 f(n) = f(n-1) + f(n-2)int res = fib(n - 1) + fib(n - 2);// 返回结果 f(n)return res;
}
观察以上代码,我们在函数内递归调用了两个函数,这意味着从一个调用产生了两个调用分支。如下图所示,这样不断递归调用下去,最终将产生一棵层数为 n 的递归树(recursion tree)。
(六) 如何避免陷入递归
不知道你有没有被递归逻辑搞晕的经历,递归调用步骤中,经常会有很多深层操作,所以我们可能会想看看每一层的返回值,我们就会一层层深入的去思考下一步的逻辑,随着思考层数加深,就会觉得递归越来越困难,心态就崩了。
递归不像迭代是正向思维,是一个逆向思维的过程,很多情况其实并不好理解,也不清楚什么时候该用,很容易被层层嵌套搞晕。
那么如何解决这种问题呢?我们可以这么理解,迭代是正向思维,从已知去推未知,也就是从最开始的已知的基础步骤,不断的重复去累计,直到任务完成。
而递归是未知推已知,是将原问题分解成多个子问题,我们并不知道子问题的解,所以需要不断地通过递归分解,直到分解成基本的已知的解,最后在统一起来。
我们被搞晕的主要因素还是忍不住的跟随者递归函数去不断的深入思考,要想避免这种情况。只要我们不再探究它内部的深层操作,将所有的递归调用的操作当成一个整体,相信它自己就能完成使命。
比如,我们需要把大象装进冰箱,一共需要几步?是不是只要打开冰箱门,把大象放进去,然后关掉冰箱门。我们把大象放进冰箱当作终止条件,而递归调用过程当作把大象,我们并不需要知道大象怎么放进冰箱的,即我们不需要知道递归是怎么执行的。
这样我们在看一些递归算法时,可以避免陷入循环的递归逻辑中。
三、两者对比
对比维度 | 迭代 | 递归 |
实现方式 | 循环结构 | 函数调用自身 |
时间效率 | 效率通常较高,无函数调用开销 | 每次函数调用都会产生开销 |
内存使用 | 通常使用固定大小的内存空间 | 累积函数调用可能使用大量的栈空间 |
适用问题 | 适用于简单循环任务,代码直观、可读性好 | 适用于子问题分解,如树、图、分治、回溯等,代码结构简洁、清晰 |
尽管迭代和递归在很多情况下可以互相转化,但不一定值得这样做,有以下两点原因:
- 转化后的代码可能更加难以理解,可读性更差。
- 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。
总之,选择迭代还是递归取决于特定问题的性质。在编程实践中,权衡两者的优劣并根据情境选择合适的方法至关重要。
参考:
[1] 靳宇栋. Hello 算法.
相关文章:

迭代与递归
算法中会经常遇见重复执行某个任务,那么如何实现呢,本文将详细介绍两种实现方式,迭代与递归。 本文基于 Java 语言。 一、迭代 迭代(iteration),就是说程序会在一定条件下重复执行某段代码,直…...

wo是如何克服编程学习中的挫折感的?
你是如何克服编程学习中的挫折感的? 编程学习之路上,挫折感就像一道道难以逾越的高墙,让许多人望而却步。然而,真正的编程高手都曾在这条路上跌倒过、迷茫过,却最终找到了突破的方法。你是如何在Bug的迷宫中找到出口的…...

vue3基础ref,reactive,toRef ,toRefs 使用和理解
文章目录 一. ref基本用法在模板中使用ref 与 reactive 的区别使用场景 二. reactive基本用法在模板中使用reactive 与 ref 的区别使用场景性能优化 三. toRef基本用法示例在组件中的应用主要用途对比 ref 和 toRef 四. toRefs基本用法示例在组件中的应用主要用途对比 ref 和 t…...
【Python机器学习】NLP的部分实际应用
自然语言处理在现实中非常多的应用,下表是其中的一些例子: 应用示例1示例2示例3搜索web文档自动补全编辑拼写语法风格对话聊天机器人助手行程安排写作索引用语索引目录电子邮件垃圾邮件过滤分类优先级排序文本挖掘摘要知识提取医学诊断法律法律断案先例…...

LLM 压缩之二: ShortGPT
0. 资源链接 论文: https://arxiv.org/pdf/2403.03853 项目代码: 待开源 1. 背景动机 现有的大语言模型 LLM 推理存在以下问题: LLM 模型因为 scale law 极大的提高模型的预测能力,但是同样带来较大的推理延时;对于 LLM 应用部署带来较大…...

EmguCV学习笔记 VB.Net 5.2 仿射变换
版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…...
Fink初识
文章目录 1. Flink核心组件2. Flink核心概念3. 执行应用程序的三种模式3.1 session mode3.2 per-job mode3.3 application mode 4. Job Manager4.1 Resource Manager4.2 Dispatcher4.3 Job Master 5. Watermark6. State7.时间属性7.1 处理时间 processing time7.2 事件时间 Eve…...

PyTorch的torchvision内置数据集使用,transform+pytorch联合使用
一、PyTorch的torchvision内置数据集介绍 我们前面的文章里谈到的数据集是我们自己找的一些自定义数据集。那么在Pytorch中存在2种数据集(Dataset),即内置数据集(Built-in dataset)和自定义数据集(Custom d…...

MT1619 (A/B/C对应18W/22W/25W)如何避免温度高、电磁干扰
MT1619系列是一款开关电源芯片,其内部集成了一颗高集成度、高性能的电流模式 PWM 控制器和一颗功率 MOSFET。MT1619 具有恒功率功能,特别适用于 PD 充电器、电源适配器等中小功率的开关电源设备。极低的启动电流与工作电流、以及轻载或者无负载情况下的 …...
Hadoop 的基本 shell 命令
Hadoop 的基本 shell 命令主要用于与 Hadoop 分布式文件系统(HDFS)和 MapReduce 进行交互。以下是一些常用的 Hadoop shell 命令: 一、 HDFS 命令 1. 查看 HDFS 状态 hdfs dfsadmin -report: 显示 HDFS 的健康状态和容量信息。 2. 文件系统操…...

HCIP-交换实验
根据实验要求,完成实验内容: 实验拓扑图如下所示 : 搭建拓补图: LSW1,LSW2: [LS1]interface Eth-Trunk 0 [LS1-Eth-Trunk0]q [LS1]interface g0/0/3 [LS1-GigabitEthernet0/0/3]eth-trunk 0 [LS1]interf…...

Windows下线程的创建与使用(win32-API)
一、前言 线程是比进程更轻量级的执行单元,允许在一个进程中并发执行多个控制流。每一个线程都有自己的程序计数器、寄存器集和栈空间,但它们共享所属进程的全局数据和资源。这种共享内存模型使线程间的通信比进程间通信更为高效,同时也带来…...
华为OD机试(C卷,100分)- 游戏分组
题目描述 部门准备举办一场王者荣耀表演赛,有 10 名游戏爱好者参与,分为两队,每队 5 人。 每位参与者都有一个评分,代表着他的游戏水平。为了表演赛尽可能精彩,我们需要把 10 名参赛者分为示例尽量相近的两队。 一队的实力可以表示为这一队 5 名队员的评分总和。 现在给你…...

centos7.9系统按cloudpods
1. 简介: Cloudpods 是一款简单、可靠的企业IaaS资源管理软件。帮助未云化企业全面云化IDC物理资源,提升企业IT管理效率。 Cloudpods 帮助客户在一个地方管理所有云计算资源。统一管理异构IT基础设施资源,极大简化多云架构复杂度和难度&…...

android apk 加固后的地图加载异常及重新签名
1.首先根据需求将打包生成后的APK进行加固,可以使用360、阿里、腾讯加固等。 2.加固后的APK无法直接安装,需要重新进行签名。 3.首先找到sdk的位置,进入build-tools目录。 4.根据gradle文件选择版本目录。 5.将加固后的APK放至该目录下。在…...

手把手搭建私人在线备份系统
对于打工人来说,什么文件最重要? 那就是——打不开的文件最重要! 那么,如何才能避免这样的事情发生呢?这时候就需要使出我们的大杀器——文件备份! 文件备份怎么搞才最合适呢? 是使用移动硬盘&a…...

数据分析实操案例分享:如何对人事数据进行BI分析?
在数据驱动时代,数据分析已经成为企业和个人获取竞争优势的关键技能。特别是在人力资源管理领域,数据分析的应用正变得越来越重要。通过对在职和离职数据的深入分析,企业不仅能够洞察员工的动态,揭示员工流动的模式、预测人才需求…...
谷粒商城实战笔记-228-商城业务-认证服务-自定义SpringSession完成子域session共享
文章目录 一,228-商城业务-认证服务-自定义SpringSession完成子域session共享1. cookieSerializer()2. springSessionDefaultRedisSerializer() 一,228-商城业务-认证服务-自定义SpringSession完成子域session共享 前面弄清楚了分布式服务中的两个问题&…...

Elasticsearch核心
一、几个核心概念 1、节点:一个节点(Node)就是一个es进程,一个服务器可以部署多个节点 查询节点以及节点信息: http://127.0.0.1:9200/_cat/nodes?v 2、角色,是指节点在集群中担任什么角色:…...

Python.NET:打开Python与.NET世界互通的大门
Python.NET 是一个强大的工具,它为 Python 程序员提供了一种与 .NET 公共语言运行时 (CLR) 无缝集成的途径。它就像一座桥梁,将 Python 的灵活性与 .NET 的强大功能连接起来,为开发者提供了前所未有的自由和可能性。 1. Python.NET 的核心价值…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...