栈和递归

栈的另外一个重要应用就是在程序设计语言中实现递归。


1 递归 Recursion

简单来说,递归就是直接调用自身通过一系列调用语句间接调用自身的一种编程技巧或方法,使用递归来实现的函数被称为递归函数。

递归通常将一个大型的复杂问题层层转化为一个与原问题类似的小规模问题来求解,有助于解决复杂的重复度高的问题,并使代码变得简洁。

1.1 递归的基本结构

先通过一个简单的例子来了解递归,以斐波那契数列的树形递归函数为例:
Fib(n)={Fib(n−1)+Fib(n−2),n>11,n=10,n=0\mathrm{Fib}(n) = \left\{\begin{matrix} \mathrm{Fib}(n-1) + \mathrm{Fib}(n-2),\ &n>1\\ 1,\ &n=1\\ 0,\ &n=0 \end{matrix}\right. Fib(n)=⎩⎨⎧​Fib(n−1)+Fib(n−2), 1, 0, ​n>1n=1n=0​

当 n>1n>1n>1 时,斐波那契定义式在不断调用它自身,用程序语言写出来就是:

int fib(int n){                              if (n==0 || n==1) {                 // 判断语句,判断何时递归结束return n;} else {                             return fib(n-1) + fib(n-2);       // 递归式}
}

尽管上述函数并不是该问题的最优解法(详情可见斐波那契数列递归解法),但它依然可以为我们提供一个递归函数的基本框架:

  1. 一般递归都需要先定义返回条件,判断递归何时结束,开始返回,否则会一直递归下去导致溢出问题。
  2. 递归需要通过重复调用自己,来进入下一层递归。
  3. 基本操作可以在递归式前出现,也可在递归式后出现,目的是进行完成一些重复的步骤(具体情况具体要求)

1.2 递归的调用规则

通常,在一个函数运行期间调用另一个函数时,系统会有以下操作:

  1. 将所有的实参、返回地址等信息传递给被调用函数保存;
  2. 开辟一个独立的空间,保存被调用函数的局部变量;
  3. 将控制转移到被调用函数的入口。

而在被调用函数返回之前,系统也会进行一些操作:

  1. 保存被调用函数的计算结果;
  2. 释放被调用函数的数据区
  3. 依照之前保存的返回地址找到调用函数并移交控制。

当有多个函数形成嵌套调用时,系统总会先处理最后被调用的函数,因此,上述函数的信息传递和控制转移需要由栈来实现。

1.3 递归的优点和缺点

递归的优点十分明显:

  • 代码简洁,很多问题的递归解法代码量远小于非递归解法。
  • 逻辑清晰容易理解

但同时它的缺点也很明显:

  • 效率低下。代码简洁并不意味着运行效率高,相反,递归解法的运行效率一般都是不高的。这是因为递归并没有减少解决一个问题需要的步骤,只是用一种很简洁的方式来描述解决这个问题的步骤。递归过程中需要调用很多次函数,每一个函数都需要开辟新空间来存放变量,并且需要时间来保存调用函数传来的各种信息,这就让递归的空间和时间开销都比较大。
  • 可能有大量无意义的重复计算。例如斐波那契数列的树形递归函数,因此要尽可能的避免产生这种重复计算。
  • 可能造成栈溢出

1.4 递归、循环、迭代和遍历

递归、循环、迭代和遍历几个概念看上去很类似,都是表达重复做某件事的意思,但还是有些细微的差别:

  • 递归 Recursion:函数或方法自己调用自己。
  • 循环 Loop:在满足循环条件的情况下不断重复,例如 while 循环。
  • 迭代 Iteration:按顺序逐个访问迭代器中的每一项,例如 for。
  • 遍历 Traversal:按顺序访问数据结构中全体元素。

一般来说递归是最不容易和后面三个概念弄混的,它描述的是函数或方法。循环和迭代是最相似的,我认为循环是个很宽泛的概念,只要有一个判断条件并且在满足条件时不断重复某些操作的都可以被称为循环,因此在口头上也经常说 for 循环或循环遍历。迭代的定义就稍微严格一些,只有通过迭代器依次访问元素的操作才能称之为迭代。最后,一般在依次访问数据结构中的元素时才使用遍历这个词,例如遍历数组和遍历二叉树等。


2 递归和迭代的相互转换

递归和迭代是可以互相转换的,只不过迭代解法可能会比较复杂。递归和迭代的最大区别就是:递归是有去有回的过程,例如计算斐波那契数列时,我们从 fib(n) 开始一直计算到 fib(1) 为止,然后将函数计算得到的结果再一层一层返回到 fib(n);而迭代是有去无回的过程,例如 for (int i=0;i<10;i++){...},我们从 i=1i=1i=1一直迭代到 i=10i=10i=10就结束了。但是通过两次迭代,也可以实现有去有回的过程。

前面分析提到过递归的优缺点,其中效率低下是递归最大的缺点,而迭代解法通常效率会更高一点。因此,当一个问题可以用递归实现时,最好也想一想它的迭代解法。

2.1 尾递归和迭代的转换

2.1.1 尾递归的定义

总的来说,递归在结构上可分为两种:尾递归和非尾递归。尾递归是递归的一种特例,指函数的最后一个动作是函数调用,例如递归计算累加值:

int accumulate1(int n, int sum){if (n<=1){return sum+n;}return accumulate1(n-1, sum+n);
}

还有一类递归函数,虽然递归调用在函数尾部,但并不是尾递归,例如下列写法的累加函数:

int accumulate2(int n){if (n<=1){return n;}return n+accumulate2(n-1);
}

这两个函数虽然在最后一行都有递归的调用,但是第一个累加函数的 return 关键字后只有一个函数调用操作。换句话说,在累加函数返回前,它执行的最后一个操作就是调用下一层函数。再来看第二个累加函数,它的 return 后面跟的其实是一个表达式,是一种缩写形式,将其展开就变成了:

int accumulate2(int n){if (n<=1){return n;}int res = accumulate2(n-1);return n+res;
}

这种写法就可以看出在其返回前的最后一个操作其实是 add 操作而不是函数调用。

2.1.2 尾递归的特点

尾递归的一个特点是其每一层函数不依赖上一层的环境。
待补充。

2.1.3 尾递归函数和迭代的转换

尾递归在函数结束前的最后一个操作就是调用下一层函数,实际上就是一层一层向下递进的作用。这一点和迭代十分相似,例如:for (int i=0;i<10;i++){},迭代变量 iii 不断地更新自己的值,进入下一层。所以可以把尾递归的返回语句看做是迭代语句,那么将尾递归函数在返回前的其他代码直接移动到迭代语句内就可以完成他们之间的转换。

尾递归累加函数:

int accumulate1(int n, int sum){if (n<=1){return sum+n;}return accumulate1(n-1, sum+n);
}

迭代累加:

int sum;
for (int i=n;i>=1;i--){sum += i;
}

2.2 非尾递归函和迭代的转换

非尾递归函数和迭代的转换就需要借助栈来实现。来看下面一个例子,假设有一个数学公式为:
Pn(x)={1,n=02x,n=12xPn−1(x)−2(n−1)Pn−2(x),n>1\mathrm{P_n}(x) = \left\{\begin{matrix} 1,\ &n=0 \\ 2x,\ &n=1 \\ 2x\mathrm{P_{n-1}}(x) - 2(n-1)\mathrm{P_{n-2}}(x),\ &n>1 \end{matrix}\right. Pn​(x)=⎩⎨⎧​1, 2x, 2xPn−1​(x)−2(n−1)Pn−2​(x), ​n=0n=1n>1​
其递归解法为:

int p(int n, int x){if (n==0){ return 1;}if (n==1){ return 2*x;}return 2*x*p(n-1, x) - 2*(n-1)*p(n-2, x);
}

非尾递归的函数需要先正向递进到最后一层(即 n=0n=0n=0),然后计算并将结果返回倒数第二层,倒数第二层计算完毕后再将结果返回倒数第三层,以此类推。因此可以将整个过程分为正向和逆向两个过程,正向过程通过迭代将每一层要计算的值存入栈的结点中模拟;逆向过程则通过出栈来模拟,这样最后出栈的元素就是最终的结果。

int p(int n, int x){struct Stack{int no;double val;} St[20];int top=-1;for (int i=n;i>1;i--){St[++top].no = i;}double fv1=1, fv2=2*x;while (top>=0){St[top].val = 2*x*fv2-2*(n-1)*fv1;fv1 = fv2;fv2 = St[top--].val;}if (n==0){return fv1;}return fv2;
}

2 递归的应用

待补充。

九、【栈和队列】栈和递归相关推荐

  1. 栈与队列3——用递归和栈操作逆序一个栈

    题目 一个栈依次压入1,2,3:此时栈顶到栈底元素分别为:3,2,1:将栈反转,使得栈顶到栈底元素为:1,2,3,仅限递归函数,并且不能使用其他数据结构 思路 使用两个函数reverse和getAnd ...

  2. 数据结构与算法(C语言) | 栈和队列——栈(自己做过测试)

    栈是一种重要的线性结构,通常称,栈和队列是限定插入和删除只能在表的"端点"进行的线性表.(后进先出) –栈的元素必须"后进先出". –栈的操作只能在这个线性表的 ...

  3. 实现if_数组实现固定栈和队列+栈与队列相互实现

    文章目录 一.数组实现固定栈和队列 1.数组实现固定栈 2.数组实现固定队列 二.栈与队列相互实现 1.两个队列实现栈 2.两个栈实现队列 一.数组实现固定栈和队列 1.数组实现固定栈 代码如下: c ...

  4. 从无到有算法养成篇-栈和队列·栈

    一.栈结构示意图 二.栈的常规操作 1.定义一个栈结构 /* 顺序栈结构 */ typedef struct {SElemType data[MAXSIZE];int top; /* 用于栈顶指针 * ...

  5. 第三章:3.栈和队列 -- 栈与递归的实现

    前言: 栈还有一个总要应用是在程序设计语言中实现递归.一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数. 目录: 1.栈 2.栈的应用举例 3.栈与递归的实现 4.队列 5.离 ...

  6. 左神算法课笔记(二):链表、栈和队列、递归Master公式、哈希表、有序表

    单向链表 双向链表 单链表.双链表最简单的面试题 1.单链表和双链表如何反转 package class02;import java.util.ArrayList;public class Code0 ...

  7. 算法题复习(栈与队列、二叉树)

    目录 栈与队列 栈用于匹配的问题 队列用于堆 二叉树系列 深度遍历,递归与迭代 层序遍历 二叉树属性 二叉树修改与构造 二叉搜索树 公共祖先 二叉搜索树的修改与构造 栈与队列 栈用于匹配的问题 20. ...

  8. b+树时间复杂度_数据结构:线性表,栈,队列,数组,字符串,树和二叉树,哈希表...

    作者:张人大 代码效率优化 复杂度 -- 一个关于输入数据量n的函数 时间复杂度 -- 昂贵 与代码的结构设计有着紧密关系 一个顺序结构的代码,时间复杂度是O(1), 即任务与算例个数 n 无关 空间 ...

  9. Java数据结构(1.1):数据结构入门+线性表、算法时间复杂度与空间复杂度、线性表、顺序表、单双链表实现、Java线性表、栈、队列、Java栈与队列。

    数据结构与算法入门 问题1:为什么要学习数据结构          如果说学习语文的最终目的是写小说的话,那么能不能在识字.组词.造句后就直接写小说了,肯定是不行的, 中间还有一个必经的阶段:就是写作 ...

  10. 数据结构学习笔记——栈和队列

    4 栈与队列   栈是限定仅在表尾进行插入和删除操作的线性表.队列是只允许在一端进行插入操作.而在另一端进行删除操作的线性表. 4.1 栈的定义 栈(stack)是限定仅在表尾进行插入和删除操作的线性 ...

最新文章

  1. 获取本机IP地址[JavaScript / Node.js]
  2. 23个机器学习项目,助你成为人工智能大咖
  3. 如何分割合并ISO文件
  4. 的注册表怎么才能删干净_油烟净化器怎么清洗才能清理干净呢?
  5. Java JDBC PreparedStatement类
  6. Atlas系列一:Atlas功能特点FAQ
  7. C#之根据域名获取IP地址
  8. ELK filebeat或logstash修改规则之后重写记录到ElasticSearch
  9. Android MediaCodec学习笔记
  10. CRNN——文本识别算法
  11. 链接装载与库:第十一章——运行库
  12. 2018年第九届蓝桥杯决赛JAVA B 题解(全)
  13. 计算机管理丢失computer文件,Win7弹框提示找不到Computer Management.lnk文件怎么办?...
  14. 解决jenkins发版报错:JAR will be empty - no content was marked for inclusion
  15. python新浪股票接口_python 爬虫sina股票数据
  16. flex布局右列固定左列自适应,遇到white-space nowrap 影响布局超长的问题
  17. C#垃圾回收机制GC
  18. 实现登录和用户信息组件的按需展示
  19. 打飞机python(完整版)
  20. 机器学习之K近邻(KNN)模型

热门文章

  1. Java:Java和c的区别
  2. 计算机视觉编程——增强现实基础
  3. c语言字符串去重简单,C语言实现简单飞机大战
  4. ssh linux mysql 乱码_JAVA ,SSH中文及其乱码问题的解决 6大配置点 使用UTF-8编码
  5. opencv下载安装及介绍【初学,后续继续更新】
  6. 源码安装python
  7. vscode和anaconda结合的环境配置
  8. 文巾解题 190. 颠倒二进制位
  9. 如何使用 python 减少 kaggle Mushroom Classification 数据集中的特性数量?
  10. 推荐算法矩阵分解实战——keras算法练习