目录

  • 迭代
  • 递归
    • 基本概念
    • 应用场景
    • 尾递归
  • 递归与迭代区别
  • 递归与迭代的转换
  • 参考

文章放置于:https://github.com/zgkaii/CS-Notes-Kz,欢迎批评指正!

迭代

迭代(iteration)是重复反馈过程的活动,其目的通常是为了接近并到达所需的目标或结果。 每一次对过程的重复被称为一次“迭代”,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。

利用迭代算法解决问题,需要做好以下三个方面的工作:

  • 确定迭代变量
  • 建立迭代关系式
  • 对迭代过程进行控制

以计算n的阶乘n!为例,先计算1乘2,然后得到结果再乘以3,在用得到结果乘以4…一直乘到n。用Java代码表示:

    public static int factorial(int n) { int res = 1;                       // 1.迭代变量for (int i = 2; i < n; i++) {        // 3.迭代过程进行控制res *= i;                     // 2.建立迭代关系式}return res; }

递归

基本概念

程序调用自身的编程技巧称为递归( recursion)。递归算法是一种直接或者间接调用自身函数或者方法的算法,它实质上是把一个大型复杂的原问题拆分成具有相同性质的子问题来求解。

递归至少满足以下两个条件:

  • 在过程或函数内调用自身;
  • 必须有一个明确的递归终止条件。

还是以阶乘为例,n的阶乘n! = n*(n-1)*... *1 (n>0),用Java代码表示:

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

那么计算5!时程序的执行过程如下:

factorial(5) factorial(4) factorial(3) factorial(2) factorial(1) return 1 return 2*1 = 2 return 3*2 = 6 return 4*6 = 24 return 5*24 = 120

又如裴波拉契(Fibonacci)数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, ...,其递推公式为:F(n) = F(n−1) + F(n−2) 其中(n > 2), F(1) = 1, F(0) = 0,用Java代码表示:

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

以计算fibonacci(5) 为例,一层一层的分解过程画成图,递归树如下:

应用场景

递归应用广泛,除了运用在裴波拉契数列,阶乘,汉诺塔等问题上(可查看Recursion),还用于处理具有天然的递归性的数据结构的问题,例如链表反转、求树的深度等。

写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。这里有一个思维误区,有人认为要写出递归代码一定要画出完整的递归树,明白每一步递归过程才行。既然递归是一个反复调用自身的过程,这就说明它每一级的功能都是一样的,实际上我们只需要关注一级递归的解决过程即可

也就是说,在用递归解决问题时,只需关注递归终止条件 与 (当前层)逻辑处理 和 (传递到的下一层)递归调用即可,大致模板如下:

public void recursion(参数0) {if (终止条件) {    // 必须return;}逻辑处理           // 非必须recursion(参数1) // 递归调用(必须)...逻辑处理           // 非必须
}

以树的前序遍历(PreOrder Traversal in a binary tree)为例:

前序遍历,按照访问根节点—>左子树—>右子树的方式遍历这棵树,而在访问左子树或者右子树的时候,按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。

    public List<Integer> preorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();preorder(root, list);return list;}private void preorder(TreeNode root, List<Integer> list) {// 终止条件if (root == null) {return;}// 递归调用list.add(root.val);preorder(root.left, list);preorder(root.right, list);}

尾递归

实际开发中,递归代码很容易造成堆栈溢出。这是因为函数调用会使用栈来保存临时变量,每调用一个函数,都会把临时变量封装为栈帧压入内存栈,等函数执行完成时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,或者没有设置好出口,或者调用层级太多,都有可能导致栈空间不够,而出现栈溢出(stack overflow)的问题。

实际上,有很多递归问题都可以优化成尾递归的形式。很多编译器都能够将尾递归的形式优化成循环的形式。那什么是尾递归呢?

我们先讨论一个概念:尾调用。尾调用 (tail call) 指的是一个函数的最后一条语句也是一个返回调用函数的语句。在函数体末尾被返回的可以是对另一个函数的调用,也可以是对自身调用(即自身递归调用)(尾调用优化)。

例如:

    function foo(data) {d(data);         if ( a(data) ) {return b(data); // 尾调用}return c(data);     // 尾调用   }

若函数在尾位置调用自身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。对尾递归的优化也是关注尾调用的主要原因。尾调用不一定是递归调用,但是尾递归特别有用,也比较容易实现。

举个例子:

def recsum(x):if x == 1:return xelse:return x + recsum(x - 1)

调用recsum(5)为例,相应的栈空间变化:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15

可观察,堆栈从左到右,增加到一个峰值后再计算从右到左缩小,这往往是我们不希望的,使用迭代、尾递归,对普通递归进行优化,减少可能对内存的极端消耗。修改以上代码,可以成为尾递归:

def tailrecsum(x, running_total=0):if x == 0:return running_totalelse:return tailrecsum(x - 1, running_total + x)

对比后者尾递归对内存的消耗:

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

则是线性的。

我们再来看裴波拉契(Fibonacci)数列的例子,将其改为尾递归:

  • 思路1:统计步数从简单情境递增到n
    public static int fibonacci(int n, int i, int pre, int cur) { if (n <= 2)return 1;else if (i == n)return cur;else return fibonacci(n, i + 1, cur, pre + cur);}// 以计算fib(5)为例fibonacci(5, 2, 1, 1)fibonacci(5, 3, 1, 2)fibonacci(5, 4, 2 ,3)fibonacci(5, 5, 3, 5)
  • 思路2:统计步数从n递减到简单情境
    public static int fibonacci(int i, int pre, int cur) {if (i <= 2)return cur;else return fibonacci(i - 1, cur, pre + cur);}// 以计算fib(5)为例fibonacci(5, 1, 1)fibonacci(4, 1, 2)fibonacci(3, 2, 3)fibonacci(2, 3, 5)

递归与迭代区别

递归是一个树结构,可以将其理解为重复“递推”和“回归”的过程,当“递推”到达底部时就会开始“回归”;而迭代是一个环结构,从初始迭代变量开始,每次迭代都更新迭代变量,多次迭代直到到达结束状态。

理论上递归和迭代时间复杂度方面是一样的,但实际应用中(函数调用和函数调用堆栈的开销)递归比迭代效率要低。

递归与迭代的转换

理论上递归和迭代可以相互转换,从上面的n的阶乘的问题也可以看出。但实际从算法结构来说,递归声明的结构并不总能转换为迭代结构(原因有待研究)。迭代可以转换为递归,但递归不一定能转换为迭代。

将递归算法转换为非递归算法有两种方法,一种是直接求值(迭代),不需要回溯;另一种是不能直接求值,需要回溯。前者使用一些变量保存中间结果,称为直接转换法,后者使用栈保存中间结果,称为间接转换法。

直接转换法

直接转换法通常用来消除尾递归(tail recursion)和单向递归,不借助堆栈结构将递归转化为迭代结构来替代。(单向递归 → 尾递归 → 迭代)

尾递归函数递归调用返回时正好是函数的结尾,因此递归调用时就不需要保留当前栈帧,可以直接将当前栈帧覆盖掉。

这里以LeetCode 爬楼梯为例,其本质上就是一个斐波拉契数列。

按照递归求解(超时):

    public int climbStairs(int n) {if (n <= 2) return n;return climbStairs(n - 1) + climbStairs(n - 2);}

尾递归(通过并超过100%)

    public int climbStairs(int n) {if (n <= 2) return n;return helper(n, 1, 2);}private int helper(int i, int pre, int cur) {if (i <= 2)return cur;else return helper(i - 1, cur, pre + cur);}

迭代(通过并超过100%)(这里也体现了动态规划的思想)

    public int climbStairs(int n) {if (n <= 1) {return n;}int n1 = 0, n2 = 1, res = 1;for (int i = 2; i <= n; ++i) {n1 = n2; n2 = res; res = n1 + n2;}return res;}

间接转换法

递归实际上利用了系统堆栈实现自身调用,那么我们也可以通过借助堆栈模拟递归的执行过程或者使用堆栈的循环结构算法。因为递归本身就是通过堆栈实现的,我们只要把递归函数调用的局部变量和相应的状态放入到一个栈结构中,在函数调用和返回时做好push和pop操作。保存中间结果模拟递归过程,将其转为非递归形式。

还是以上面爬楼梯为例,采用栈(通过并超过100%):

    public int climbStairs(int n) {if(n < 3) return n;Stack<Integer> st = new Stack<Integer>();st.push(new Integer(1));st.push(new Integer(2));int i = 3;while(i <= n){Integer a = st.peek();st.pop();Integer b = st.peek();st.push(a);st.push(new Integer(a.intValue() + b.intValue()));i++;}return st.peek().intValue();}

参考

  • https://www.cnblogs.com/bakari/p/5349383.html
  • https://leetcode-cn.com/circle/article/yXFal5/

算法 - 递归与迭代 区别与联系相关推荐

  1. 递归、迭代、分治、回溯、动态规划、贪心算法

    今天就简单来谈谈这几者之间的关联和区别 递归 一句话,我认为递归的本质就是将原问题拆分成具有相同性质的子问题. 递归的特点: 1.子问题拆分方程式,比如:f(n) = f(n-1) * n 2.终止条 ...

  2. 递归与迭代 | 求斐波那契数列第n项值的四种算法

    前言: 昨儿晚上三点多睡不着,不知道胡思乱想了些啥,好不容易睡着了又做了些稀奇古怪的梦.考研还是继续,真难. 这一篇博客记录一下求斐波那契数列第n项值得几种方法,用到了递归和迭代的方法,所以首先我们来 ...

  3. 递归和迭代的区别——以DNS为例

    递归和迭代的区别--以DNS为例 2021.7.22 可能或多或少都听说过:函数的递归调用,牛顿迭代法,在DNS(Domain Name System域名系统)中有递归查询和迭代查询两种······那 ...

  4. 迭代和递归的应用例子c语言,递归和迭代的应用以及区别

    斐波那契数列: 1 1 2 3 5 8 13 21 34 55 - fb(n) : 1 n <= 2 fb(n-1) + fb(n-2) n > 2 int fb(n) { if(n &l ...

  5. 研究青蛙跳台阶问题区别函数递归与迭代

    文章目录 前言 一.什么是青蛙跳台阶问题 二.使用递归和迭代(非递归)的区别 1.递归实现 2.迭代(非递归) 前言 青蛙跳台阶问题是函数递归的经典问题,也是求斐波那契数的变式,通过研究非递归(迭代) ...

  6. 计算机科学中的递归算法是把问题,从计算思维的视角辨析算法中的递归与迭代...

    周世杰 算法思维是计算思维的一个方面,而在计算机科学中,基于递归和迭代的思维方式在算法和程序设计中广泛应用,是算法思维的重要构成部分.因此,信息技术学科教师在基础课教学中辨析递归与迭代算法,将其作为发 ...

  7. 基础算法(二):迭代、递归与分治

    前言 在这篇文章中,荔枝会梳理一些迭代.递归和分治的基本概念.同时也会有样题示例辅助理解这三种算法的应用. 文章目录 前言 一.迭代 1.1 概念 1.2 样题示例 二.递归 2.1 概念 2.2 样 ...

  8. 编程中,循环、迭代、遍历和递归之间的区别

    表示"重复"这个含义的词有很多, 比如循环(loop), 递归(recursion), 遍历(traversal), 迭代(iterate). 循环算是最基础的概念, 凡是重复执行 ...

  9. 详解二叉树的三种遍历方式(递归、迭代、Morris算法)

    详解二叉树的三种遍历方式(递归.迭代.Morris算法) 最重要的事情写在前面:遍历顺序不一定就是操作顺序!!! 递归解法 首先,一颗二叉树它的递归序列是一定的,导致其前中后序不同的原因只不过是访问节 ...

最新文章

  1. 基于深度学习Superpoint 的Python图像全景拼接
  2. Deep Learning in a Nutshell: Core Concepts
  3. 巅峰极客2021 what_pickle——一道综合性的python web
  4. C++实现connected component连通分量(附完整源码)
  5. Eclipse中javascript文件 clg 变为console.log();
  6. 利用反射获得类的public static/const成员的值
  7. ubuntu 系统U盘中 文件出现小锁子
  8. F2812 DSP程序运行在片内RAM和FLASH的区别
  9. idea查看ruby代码_Java代码审计入门篇:WebGoat 8(初见)
  10. C语言学习 数独游戏
  11. 【愣锤笔记】能解决80%场景的Git必会知识点
  12. 利用Github探测发现特斯拉API请求漏洞
  13. python爬虫实例100例-10个python爬虫入门实例
  14. 计算机应用情话,2018最新版情话大全浪漫情话 好听感人的情话
  15. 为什么不建议执行超过3表以上的多表关联查询?
  16. 这些 Linux 技巧你应该知道
  17. Python基础入门:条件语句--阿里云天池
  18. jQuery背景墙聚光灯效果
  19. Revit建模软件:如何在Revit中准确放置族组件?
  20. 数据通信基础 - 调制技术

热门文章

  1. python,pandas计算布林带(Bollinger Band)
  2. html直角梯形div,css如何让div变成直角梯形
  3. AI的星辰与大海,百度的理性和感性
  4. 怎样减少企业中那些惊人的“无效工作”?
  5. c语言课程设计人事部门,C语言课程设计人事管理系统
  6. SuRF: 一个优化的 Fast Succinct Tries
  7. EasyRecovery数据恢复软件 恢复了我两年前的照片视频数据
  8. Get两大主流简历模版
  9. 华为手机中的计算机怎么用高级,华为手机电脑模式怎么用鼠标
  10. PLC实训4:简单红绿灯设计