递归算法工作栈的变化详解

合集下载

栈与递归

栈与递归

栈与递归的关系一、引言栈是一种重要的线性结构。

从数据结构角度看,栈也是线性表,其特殊性在于栈的基本操作是线性表操作的子集,它们是操作受限的线性表。

栈是限定仅在表尾进行插入或删除操作的线性表。

栈还有一个重要应用是在程序设计语言中实现递归。

一个直接调用自己或通过一系列的调用语句间接地调用直接的函数,称做递归函数。

二、栈与递归的关系:递归在实现过程中是借助于栈来实现的。

高级语言的函数调用,每次调用,系统都要自动为该次调用分配一系列的栈空间用于存放此次调用的相关信息:返回地址,局部变量等。

这些信息被称为工作记录(或活动记录)。

而当函数调用完成时,就从栈空间内释放这些单元,但是,在该函数没有完成前,分配的这些单元将一直保存着不被释放。

递归函数的实现,也是通过栈来完成的。

在递归函数没有到达递归出口前,都要不停地执行递归体,每执行一次,就要在工作栈中分配一个工作记录的空间给该“层”调用存放相关数据,只有当到达递归出口时,即不再执行函数调用时,才从当前层返回,并释放栈中所占用的该“层”工作记录空间。

请大家注意,递归调用时,每次保存在栈中的是局部数据,即只在当前层有效的数据,到达下一层时上一层的数据对本层数据没有任何影响,一切从当前调用时传过来的实在参数重新开始。

由此可见,从严老师P版教材中,利用栈将递归向非递归转化时所采用的方法,实质是用人工写的语句完成了本该系统程序完成的功能,即:栈空间中工作记录的保存和释放。

可以参照以上的分析来理解递归函数的运行过程。

三、对栈与递归的分析1、栈的定义栈(Stack)是限制仅在表的一端进行插入和删除运算的线性表。

(1)通常称插入、删除的这一端为栈顶(Top),另一端称为栈底(Bottom)。

(2)当表中没有元素时称为空栈。

(3)栈为后进先出(Last In First Out)的线性表,简称为LIFO表。

栈的修改是按后进先出的原则进行。

每次删除(退栈)的总是当前栈中"最新"的元素,即最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除。

数据结构(C语言)第3章 栈和队列

数据结构(C语言)第3章 栈和队列

Data Structure
2013-8-6
Page 13
栈的顺序存储(顺序栈)
利用一组地址连续的存储单元依次存放自栈底到栈顶的数 据元素。 结构定义: #define STACK_INIT_SIZE 100; // 存储空间初始分配量 #define STACKINCREMENT 10; // 存储空间分配增量 typedef struct { SElemType *base; // 存储空间基址 SElemType *top; // 栈顶指针 int stacksize; // 当前已分配的存储空间,以元素位单位 } SqStack;
解决方案2:
顺序栈单向延伸——使用一个数组来存储两个栈
Data Structure 2013-8-6 Page 21
两栈共享空间 两栈共享空间:使用一个数组来存储两个栈,让一个 栈的栈底为该数组的始端,另一个栈的栈底为该数组 的末端,两个栈从各自的端点向中间延伸。
Data Structure
2013-8-6
链栈需要加头结点吗? 链栈不需要附设头结点。
Data Structure
2013-8-6
Page 27
栈的链接存储结构及实现
Data Structure
2013-8-6
Page 11
GetTop(S, &e) 初始条件:栈 S 已存在且非空。 操作结果:用 e 返回S的栈顶元素。 Push(&S, e) 初始条件:栈 S 已存在。 操作结果:插入元素 e 为新的栈顶元素。 Pop(&S, &e) 初始条件:栈 S 已存在且非空。 操作结果:删除 S 的栈顶元素,并用 e 返回其值。
Data Structure

程序设计员实操考核:深入理解递归算法

程序设计员实操考核:深入理解递归算法

程序设计员实操考核:深入理解递归算法一、引言递归算法是计算机科学中的重要概念,也是程序设计员实际工作中经常使用的算法之一。

通过递归算法,我们可以解决一系列与问题的分解和子问题求解有关的计算任务。

在程序设计员的实操考核中,深入理解递归算法是一项重要的能力要求。

本文将从概念、原理、应用和实操等多个方面对递归算法进行介绍和解析,以帮助程序设计员更好地掌握和运用递归算法。

二、概念与原理2.1 递归算法的定义递归算法是一种通过函数调用自身的方式来解决问题的方法。

在递归算法中,将一个大问题分解为一个或多个同类的子问题,逐步解决子问题,最终得到原问题的解。

2.2 递归算法的基本原理递归算法的基本原理包括以下几点:•基准情况:递归算法必须设定一个或多个基准情况,当满足基准情况时,递归停止,并返回结果。

•递归调用:递归算法通过调用自身来解决子问题。

在每次递归调用中,问题的规模都会减小,直到满足基准情况。

•合并子问题:递归算法在解决子问题之后,需要将子问题的解合并起来,得到原问题的解。

2.3 递归算法的特点递归算法有以下几个特点:•问题分解:递归算法将一个大问题分解为多个同类的子问题,将问题简化为更小的规模。

•自相似性:在递归算法中,子问题与原问题具有相同的性质,只是规模不同。

•代码简洁:递归算法通常代码较为简洁,能够更清晰地表达问题的结构。

•空间复杂度高:递归算法通常会占用较多的栈空间,可能会导致栈溢出。

三、递归算法的应用递归算法在实际工作中有广泛的应用,以下是几个常见的应用场景:3.1 数学运算递归算法可以用于解决数学中的一些复杂运算问题,比如计算阶乘、斐波那契数列等。

通过递归调用自身,可以简化问题的解决过程。

3.2 数据结构操作递归算法在处理树、图等数据结构时往往更加方便。

通过递归算法,可以对树进行遍历、搜索、插入、删除等操作。

递归算法还可以用于实现图的深度优先搜索等算法。

3.3 文件系统操作递归算法在处理文件系统中的文件和目录时也很常见。

递归算法详解完整版

递归算法详解完整版

递归算法详解完整版递归算法是一种重要的算法思想,在问题解决中起到了很大的作用。

它通过将一个大问题划分为相同或类似的小问题,并将小问题的解合并起来从而得到大问题的解。

下面我们将详细介绍递归算法的定义、基本原理以及其应用。

首先,我们来定义递归算法。

递归算法是一种通过调用自身解决问题的算法。

它通常包括两个部分:基础案例和递归步骤。

基础案例是指问题可以被直接解决的边界情况,而递归步骤是指将大问题划分为较小问题并通过递归调用自身解决。

递归算法的基本原理是"自顶向下"的思维方式。

即从大问题出发,不断将问题划分为较小的子问题,并解决子问题,直到达到基础案例。

然后将子问题的解合并起来,得到原始问题的解。

递归算法的最大特点是简洁而优雅。

通过将复杂问题分解为简单问题的解决方式,可以大大减少代码的复杂程度,提高程序的效率和可读性。

但是递归算法也有一些缺点,包括递归深度的限制和复杂度的不确定性。

过深的递归调用可能导致栈溢出,而不合理的递归步骤可能导致复杂度过高。

递归算法有许多应用场景,我们来介绍其中一些典型的应用。

1.阶乘问题:计算一个数的阶乘。

递归算法可以通过将问题划分为更小的子问题来解决。

例如,n的阶乘可以定义为n乘以(n-1)的阶乘。

当n 等于1时,我们可以直接返回1作为基础案例。

代码如下:```int factorial(int n)if (n == 1)return 1;}return n * factorial(n - 1);```2.斐波那契数列问题:求斐波那契数列中第n个数的值。

斐波那契数列的定义是前两个数为1,然后从第三个数开始,每个数都是前两个数的和。

递归算法可以通过将问题划分为两个子问题来解决。

当n等于1或2时,直接返回1作为基础案例。

代码如下:```int fibonacci(int n)if (n == 1 , n == 2)return 1;}return fibonacci(n - 1) + fibonacci(n - 2);```3.二叉树问题:对于给定的二叉树,递归算法可以通过递归调用左子树和右子树的解来解决。

python中的递归算法详解

python中的递归算法详解

python中的递归算法详解递归算法是一种在函数内部调用自身的方法。

在Python中,递归可以实现复杂的问题解决,它的实现遵循以下几个关键步骤。

第一,定义基本情况:递归算法在每一次调用中都需要判断是否达到了基本情况,即算法可以直接返回结果而不继续调用自身。

这个基本情况确保算法不会无限递归下去,而是能够找到问题的解。

第二,拆分问题:递归算法需要将大问题拆分成子问题,在每一次递归调用中解决子问题。

这样,较大的问题就会逐渐转化为较小的问题,直到最终达到基本情况。

第三,调用自身:递归算法通过调用自身来解决子问题。

在每次递归调用中,算法会传递不同的参数,以便在下一次调用中解决不同的子问题。

递归算法的关键在于它能够将复杂的问题转化为简单的子问题,并通过不断调用自身来解决这些子问题。

举个例子,考虑一个计算阶乘的递归算法:```pythondef factorial(n):# 基本情况if n == 0:return 1# 拆分问题subproblem = factorial(n - 1)# 调用自身result = n * subproblemreturn result```在这个例子中,递归算法使用了基本情况 `n == 0` 来停止递归。

当输入参数为0时,算法直接返回1,否则继续调用自身,解决 `n - 1` 的阶乘问题。

最终,算法将计算 `n` 的阶乘并返回结果。

需要注意的是,递归算法在处理大规模问题时可能会遇到效率问题。

递归算法会调用自身多次,每次调用都需要保存状态并分配栈空间,所以在某些情况下,循环迭代可能会更加高效。

因此,在使用递归算法时,需要谨慎考虑问题的规模和算法实现的复杂度,以充分利用递归的优势。

离散数学中递归算法的工作原理解析

离散数学中递归算法的工作原理解析

离散数学中递归算法的工作原理解析离散数学是一门研究离散对象和离散结构的数学学科,其在计算机科学中有着广泛的应用。

递归算法是离散数学中的一个重要概念,本文将对递归算法的工作原理进行解析。

1. 递归算法的定义递归算法是一种通过反复调用自身来解决问题的算法。

它通常包含了一个递归出口(基本情况)和一个递归体(递归情况)。

当问题达到递归出口时,算法停止递归并返回结果。

否则,算法继续递归调用自身,将问题分解为规模更小的子问题,并在子问题上进行递归求解。

2. 递归算法的优点与注意事项递归算法具有以下优点:1) 逻辑清晰简洁:递归算法能够使用简洁的方式描述问题的解决过程。

2) 结构灵活:递归算法能够解决各种类型的问题,适用范围广泛。

然而,递归算法也需要注意以下事项:1) 递归深度:递归算法的性能与问题的规模成反比。

递归深度过大可能导致栈溢出或性能下降。

2) 重复计算:递归算法中可能存在重复计算,增加了计算量。

可以使用记忆化技术(如动态规划)来优化递归算法。

3. 递归算法的应用场景递归算法在计算机科学中有广泛的应用,包括但不限于以下领域:1) 数据结构:递归算法常用于处理树、图、链表等数据结构,如树的遍历、图的深度优先搜索等。

2) 排列组合:递归算法可以用于生成排列组合,如全排列、组合数等。

3) 分治算法:分治算法通常使用递归来将问题分解为更小的子问题,并分别求解。

4. 递归算法的实现步骤实现一个递归算法通常包括以下步骤:1) 定义递归出口:确定递归算法何时停止递归,返回结果。

2) 确定递归体:根据问题的特点,将问题分解为规模更小的子问题,并调用自身来解决子问题。

3) 设计递归调用:根据子问题的规模和性质,设计递归调用的方式。

4) 处理子问题的结果:将子问题的结果合并得到原问题的结果。

5. 递归算法的示例:阶乘计算下面通过计算阶乘的例子来具体说明递归算法的工作原理:```python# 递归算法计算阶乘def factorial(n):if n == 0:return 1else:return n * factorial(n-1)```上述代码中,factorial函数通过递归来计算阶乘。

栈与递归的关系

栈与递归的关系

栈与递归的关系姓名:郭小兵学号:1007010210专业:信息与计算科学院系:理学院指导老师:彭长根2012年10月17日栈与递归的关系郭小兵摘要递归是计算机科学中一个极为重要的概念,许多计算机高级语言都具有递归的功能,对于初学计算机者来讲,递归是一个简单易懂的概念,但真正深刻理解递归,正确自如的运用递归编写程序却非易事,本文通过一些实例来阐述递归在计算机内的实现及递归到非递归的转换,也许使读者能加深对递归的理解。

关键词栈递归非递归引言递归是一种程序设计的方式和思想。

计算机在执行递归程序时,是通过栈的调用来实现的。

栈,从抽象层面上看,是一种线性的数据结构,这中结构的特点是“先进后出”,即假设有a,b,c三个元素,依次放某个栈式存储空间中,要从该空间中拿到这些元素,那么只能以c、b、a的顺序得到。

递归程序是将复杂问题分解为一系列简单的问题,从要解的问题起,逐步分解,并将每步分解得到的问题放入“栈”中,这样栈顶是最后分解得到的最简单的问题,解决了这个问题后,次简单的问题可以得到答案,以此类推。

分解问题是进栈(或者说压栈)的过程,解决问题是一个出栈的过程。

科学家对栈与递归都做了很多深入的研究,研究表明“递归算法和栈都有后进先出这个性质,基本上能用递归完成的算法都可以用栈完成,都是运用后进先出这个性质的”这个性质可用于进制的转换。

与汇编程序设计中主程序和子程序之间的链接及信息交换相类似,在高级语言编制的程序中,调用函数和被调用函数之间的链接及信息交换需过栈来进行。

递归是计算科学中一个极为重要的概念。

许多计算机高级语言都具有递归的功能,本文将通过一些是例来阐述递归在计算机内的实现及递归到非递归的转换,也许能加深对递归的理解。

递归是某一事物直接或间接地由自己完成。

一个函数直接或间接地调用本身,便构成了函数的递归调用,前者称之为直接递归调用,后者为间接递归调用。

递归会使某些看起来不容易解决的问题变得容易解决。

递归与栈的转化(迷宫+n皇后+汉诺塔)

递归与栈的转化(迷宫+n皇后+汉诺塔)

递归与栈的转化(迷宫+n皇后+汉诺塔)参考:《数据结构教程》第五版李春葆⼀,递归到⾮递归的转换1,递归的分类 递归可分为尾递归和⾮尾递归2,尾递归 ① 如果⼀个递归过程会递归函数中的递归调⽤语句是最后⼀条语句,则称这种递归调⽤为尾递归 ② ⼀般情况下,尾递归可以通过循环或者迭代⽅式转化为等价的⾮递归算法。

3,⾮尾递归 ① 对于⾮尾递归算法,在理解递归调⽤实现过程的基础上可以⽤栈来模拟递归执⾏过程。

4,总结 尾递归需要只⽤到递归函数的搜索功能,所以可以⽤循环替换; ⽽⾮尾递归除了循环部分,还⽤到了递归函数的回溯功能,所以只能⽤栈模拟。

⼆,⼀个栈帧只有⼀个递归函数的递归转化1,迷宫寻路递归:#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<stdlib.h>#define N 1111int n, m, cnt;int map[N][N];int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };struct Node{int x, y;}way[N];void show(){printf("(0,0)");for (int i = 0; i < cnt; i++)printf("->(%d,%d)", way[i].x, way[i].y);puts("");}void DFS(int a, int b){if (a == n - 1 && b == m - 1){show();return;}for (int i = 0; i < 4; i++){if (a + dx[i] < 0 || b + dy[i] < 0 || a + dx[i] >= n || b + dy[i] >= m)continue;if (map[a + dx[i]][b + dy[i]] == 0){map[a][b] = 1;way[cnt].x = a + dx[i], way[cnt].y = b + dy[i];cnt++;DFS(a + dx[i], b + dy[i]);cnt--;map[a][b] = 0;}}}int main(void){// ⼊⼝是 map[0][0], 出⼝是 map[m-1][n-1]while (scanf("%d%d", &n, &m) != EOF){for (int i = 0; i < n; i++)for (int j = 0; j < m; j++)scanf("%d", &map[i][j]);cnt = 0;DFS(0, 0);}system("pause");return0;}/*测试数据第⼀组6 50 0 1 1 10 0 0 0 11 0 1 0 11 1 1 0 11 0 1 0 0结果(0,0)->(0,1)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4) (0,0)->(1,0)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4)第⼆组6 50 0 1 1 10 0 0 0 11 0 1 0 11 0 1 0 11 0 1 0 11 0 0 0 0结果(0,0)->(0,1)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4) (0,0)->(0,1)->(1,1)->(2,1)->(3,1)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4) (0,0)->(1,0)->(1,1)->(1,2)->(1,3)->(2,3)->(3,3)->(4,3)->(5,3)->(5,4) (0,0)->(1,0)->(1,1)->(2,1)->(3,1)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4) */View Code栈:#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<stdlib.h>#define N 101#define MaxSize 10086int dx[] = { -1, 0, 1, 0 }, dy[] = { 0, 1, 0, -1 };int map[N][N];typedef struct Box{int x, y;int di; // 点的编号}bx;typedef Box any; // 可修改数据类型typedef struct SqStack // 顺序栈{#define MaxSize 666any a[MaxSize];int pt; // 栈顶指针SqStack() {pt = -1;}void push(any e) { // ⼊栈a[++pt] = e;}void pop() { // 出栈pt--;}any top() { // 取栈顶元素return a[pt];}bool empty() { // 判断栈是否为空return pt == -1;}int size() { // 返回栈的⼤⼩return pt + 1;}}st;void show(st s){st t;while (!s.empty()){bx v = s.top();s.pop();t.push(v);}int cnt = 0;while (!t.empty()){bx v = t.top();t.pop();if (cnt++ == 0)printf("(%d,%d)", v.x, v.y);elseprintf("->(%d,%d)", v.x, v.y);}puts("");}void dfs(int xi, int yi){st s; // 定义栈bx start = { xi,yi,0 };map[xi][yi] = -1;s.push(start); // 起点进栈int cnt = 1; // 记录路径数while (!s.empty()){// 1,取栈顶元素,相当于函数形参bx vertex = s.top();// 2,找到终点,回溯(关键点:消除搜索的痕迹)if (vertex.x == n - 1 && vertex.y == m - 1){show(s);s.pop();map[vertex.x][vertex.y] = 0;continue;}// 3,如果有路可⾛,就继续搜索int find = 0; // find = 1 表⽰下⼀步可⾛,0 表⽰下⼀步不可⾛for (int i = vertex.di; i < 4; i++){bx next{ vertex.x + dx[i], next.y = vertex.y + dy[i] ,0 };if (next.x < 0 || next.y < 0 || next.x >= n || next.y >= m)continue;if (map[next.x][next.y] == 0){find = 1;vertex.di = i + 1; // 标记这个⽅块已经⾛过的⽅向,⼿动确定回溯时不会重复回溯。

C语言的递归算法解析

C语言的递归算法解析

C语言的递归算法解析递归算法是一种经常在编程中使用的重要技术。

在C语言中,递归算法可以通过函数的自我调用来实现。

本文将对C语言中的递归算法进行详细解析,并介绍递归算法在实际应用中的一些常见场景。

一、什么是递归算法递归算法是一种通过函数的自我调用来解决问题的方法。

在递归算法中,一个函数可以直接或间接地调用自身。

递归算法通常分为两个部分:基本情况和递归情况。

基本情况是指能够直接解决的问题,而递归情况是指将问题划分为子问题并通过递归调用解决。

递归算法的核心思想是将原问题转化为规模更小的子问题,并通过递归调用解决子问题。

递归算法必须要有一个终止条件,否则会进入无限循环,导致程序崩溃或者运行时间过长。

二、递归算法的实现在C语言中,递归算法可以通过函数的自我调用来实现。

下面是一个求解斐波那契数列的递归算法示例:```c#include <stdio.h>int fibonacci(int n) {if (n == 0 || n == 1) {return n;} else {return fibonacci(n-1) + fibonacci(n-2);}}int main() {int n = 10;int result = fibonacci(n);printf("The %dth Fibonacci number is: %d\n", n, result);return 0;}```在上述代码中,`fibonacci`函数通过递归调用自身来求解斐波那契数列的第n个数。

如果n为0或者1,那么直接返回n,否则将问题划分为求解第n-1个数和第n-2个数的和,并通过递归调用来解决子问题。

三、递归算法的优缺点递归算法具有以下优点:1. 递归算法可以简化问题的解决过程,将大问题划分为小问题,降低了问题解决的复杂度。

2. 递归算法的逻辑清晰,代码简洁,易于理解和维护。

然而,递归算法也存在一些缺点:1. 递归算法需要占用大量的系统栈空间,函数的调用层级过深可能导致栈溢出的问题。

用栈实现递归

用栈实现递归

用栈实现递归递归是一种常见的编程技巧,它可以让程序更加简洁、易读。

但是递归也有一些缺点,比如递归深度过大会导致栈溢出等问题。

为了解决这些问题,我们可以使用栈来实现递归。

栈是一种后进先出(Last In First Out,LIFO)的数据结构,它可以用来保存函数调用的上下文信息。

当一个函数被调用时,它的参数、局部变量和返回地址等信息都会被压入栈中。

当函数执行完毕后,这些信息会被弹出栈,控制流程回到调用该函数的地方。

使用栈实现递归的基本思路是将递归函数转化为一个循环,每次循环都将函数调用的参数和局部变量保存在栈中。

当递归函数返回时,从栈中弹出保存的信息,继续执行下一次循环。

下面是一个使用栈实现阶乘函数的例子:```pythondef factorial(n):stack = []result = 1while True:if n == 0:if stack:n, result = stack.pop()result *= nn -= 1else:breakelse:stack.append((n, result))n -= 1return result```在这个例子中,我们使用一个栈来保存函数调用的上下文信息。

当 n 等于 0 时,我们从栈中弹出保存的信息,继续执行下一次循环。

当栈为空时,函数返回结果。

使用栈实现递归可以避免递归深度过大导致栈溢出的问题,同时也可以提高程序的效率。

但是需要注意的是,使用栈实现递归可能会增加代码的复杂度,需要仔细考虑每个函数调用的上下文信息,确保程序的正确性。

总之,使用栈实现递归是一种有用的编程技巧,可以帮助我们更好地处理递归函数。

在实际编程中,我们可以根据具体情况选择使用递归或栈来实现函数调用。

详解递归(基础篇)———函数栈、阶乘、Fibonacci数列

详解递归(基础篇)———函数栈、阶乘、Fibonacci数列

详解递归(基础篇)———函数栈、阶乘、Fibonacci数列⼀、递归的基本概念递归函数:在定义的时候,⾃⼰调⽤了⾃⼰的函数。

注意:递归函数定义的时候⼀定要明确结束这个函数的条件!⼆、函数栈栈:⼀种数据结构,它仅允许栈顶进,栈顶出,先进后出,后进先出。

我们可以简单的理解为栈就是⼀个杯⼦,这个杯⼦⾥⾯有很多隔层,每⼀层都可以放东西,第⼀个放⼊的东西就在杯⼦最后⼀层,第⼆个放⼊的东西就在倒数第⼆层,现在我们要取出最后⼀层的东西,就必须先把第⼆层的东西给出来。

函数栈:栈⾥⾯每⼀层都是装的都是函数的栈就是函数栈,调⽤⼀个函数的时候,这个函数就⼊栈,这个函数调⽤完成了(执⾏到了函数的最后⼀个语句或者说return了),就出栈。

下⾯是⼀个演⽰函数栈运⾏机制的C语⾔程序,并⽆实际意义,仅⽤于理解函数栈:#include<stdio.h>void function1(){printf("function1 done!")return; //第三步,函数function1调⽤完毕,出栈}void function2(){printf("function2 done!")return; //第五步,函数function2调⽤完毕,出栈}void function3(){printf("function3 done!")return; //第⼋步,函数function3调⽤完毕,出栈}void function4(){printf("function4 done!")function3(); //第七步,调⽤函数function3,⼊栈return; //第九步,函数function4调⽤完成,出栈}int main()//第⼀步,调⽤主函数,主函数⼊栈(这个C语⾔程序的⼊⼝){function1(); //第⼆步,调⽤函数function1,⼊栈function2(); //第四步,调⽤函数function2,⼊栈function4(); //第六步,调⽤函数function4,⼊栈return 0; //第⼗步,主函数调⽤完成,出栈(整个程序执⾏完成)}三、实例1、阶乘题⽬:⽤递归⽅法实现计算整数n的阶乘n!解析:⾸先,我们易知0!=1; 1!=1; 2!=2*1!; 3!=3*2!; 4!=4*3!;……那么我们可以得到递推公式n!=1[n=0,1]n!=n*(n-1)![n>=2]从⽽我们就可以写出计算阶乘的递归程序的C语⾔代码,如下:#include<stdio.h>long Fact(int n);int main(){int n;long result;printf("Input n:");scanf("%d",&n);result = Fact(n);if(result == -1)printf("n<0,data error!\n");elseprintf("%d! = %ld\n", n, result);return 0;}long Fact(int n){//对传⼊函数的值判断其合法性if(n < 0){return -1;}//计算n的阶乘的表达式改写的代码else if(n == 0 || n == 1){return 1;}else{return (n*Fact(n-1));}}当然,写出来了并不代表我们理解,下图讲解了在这个程序中是如何计算4的阶乘的。

二叉树的先序,中序,后序遍历的递归工作栈的关系

二叉树的先序,中序,后序遍历的递归工作栈的关系

二叉树的先序,中序,后序遍历的递归工作栈的关系在计算机科学中,二叉树是一种非常重要的数据结构,它在很多算法和数据处理中都有着广泛的应用。

而二叉树的先序、中序、后序遍历以及它们与递归和工作栈的关系更是程序员面试中常见的问题。

本文将从深度和广度两个方面,按照先序、中序、后序的顺序逐步展开对这个主题的探讨。

一、先序遍历先序遍历是指先访问根节点,然后递归地先序遍历左子树,最后递归地先序遍历右子树。

在实际的计算机算法中,我们可以使用递归或者栈来实现先序遍历。

1.1 递归实现当我们使用递归来实现先序遍历时,可以很容易地写出下面这段代码:```pythondef preorderTraversal(root):if not root:return []return [root.val] + preorderTraversal(root.left) + preorderTraversal(root.right)```这段代码非常简洁明了,但是在实际执行时,会使用工作栈来保存递归中间结果。

因为递归本质上就是一个栈结构,在调用递归函数时,会将当前函数的局部变量和参数压入栈中,直到递归结束,栈中的内容才会依次出栈执行。

1.2 栈实现除了递归之外,我们也可以使用显式栈来实现先序遍历。

这种方法通常会更加高效一些,因为递归会有一定的性能损耗。

栈的实现思路是,我们首先将根节点压入栈中,然后弹出栈顶节点并访问它,接着先将右子节点压入栈中,再将左子节点压入栈中。

重复上述操作直到栈为空。

这样就可以保证先访问根节点,再访问左子树,最后访问右子树,符合先序遍历的要求。

二、中序遍历中序遍历是指先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。

中序遍历同样可以用递归或者显式栈来实现。

2.1 递归实现递归实现中序遍历同样非常简单:```pythondef inorderTraversal(root):if not root:return []return inorderTraversal(root.left) + [root.val] + inorderTraversal(root.right)```在这个递归函数中,同样使用了递归的工作栈来保存中间结果。

递归调用详解,分析递归调用的详细过程

递归调用详解,分析递归调用的详细过程

递归调⽤详解,分析递归调⽤的详细过程⼀、栈在说函数递归的时候,顺便说⼀下栈的概念。

栈是⼀个后进先出的压⼊(push)和弹出(pop)式。

在程序运⾏时,系统每次向栈中压⼊⼀个对象,然后栈指针向下移动⼀个位置。

当系统从栈中弹出⼀个对象时,最近进栈的对象将被弹出。

然后栈指针向上移动⼀个位置。

程序员经常利⽤栈这种数据结构来处理那些最适合⽤后进先出逻辑来描述的编程问题。

这⾥讨论的程序中的栈在每个程序中都是存在的,它不需要程序员编写代码去维护,⽽是由运⾏是系统⾃动处理。

所谓的系统⾃动维护,实际上就是编译器所产⽣的程序代码。

尽管在源代码中看不到它们,但程序员应该对此有所了解。

再来看看程序中的栈是如何⼯作的。

当⼀个函数(调⽤者)调⽤另⼀个函数(被调⽤者)时,运⾏时系统将把调⽤者的所有实参和返回地址压⼊到栈中,栈指针将移到合适的位置来容纳这些数据。

最后进栈的是调⽤者的返回地址。

当被调⽤者开始执⾏时,系统把被调⽤者的⾃变量压⼊到栈中,并把栈指针再向下移,以保证有⾜够的空间存储被调⽤者声明的所有⾃变量。

当调⽤者把实参压⼊栈后,被调⽤者就在栈中以⾃变量的形式建⽴了形参。

被调⽤者内部的其他⾃变量也是存放在栈中的。

由于这些进栈操作,栈指针已经移动所有这些局部变量之下。

但是被调⽤者记录了它刚开始执⾏时的初始栈指针,以他为参考,⽤正或负的偏移值来访问栈中的变量。

当被调⽤者准备返回时,系统弹出栈中所有的⾃变量,这时栈指针移动了被调⽤者刚开始执⾏时的位置。

接着被调⽤者返回,系统从栈中弹出返回地址,调⽤者就可以继续执⾏了。

当调⽤者继续执⾏时,系统还将从栈中弹出调⽤者的实参,于是栈指针回到了调⽤发⽣前的位置。

可能刚开始学的⼈看不太懂上⾯的讲解,栈涉及到指针问题,具体可以看看⼀些数据结构的书。

要想学好编程语⾔,数据结构是⼀定要学的。

⼆、递归递归,是函数实现的⼀个很重要的环节,很多程序中都或多或少的使⽤了递归函数。

递归的意思就是函数⾃⼰调⽤⾃⼰本⾝,或者在⾃⼰函数调⽤的下级函数中调⽤⾃⼰。

第6章 递归

第6章  递归

第6章 递归
用递归方法分析考虑如下。设盘子的总数为n,我 们给A柱上的盘子从上至下编号为1到n。当n=1时, 问题可直接求解,即我们可直接把A柱上的盘子移到 C柱上;当n>1时,移动由以下三步组成: (1)用C柱做过渡把A柱上的n-1个盘子移到B 柱上; (2)把A柱上的最后一个盘子移到C柱上; (3)用A柱做过渡把B柱上的n-1个盘子移 到C柱上。
第6章 递归
*6.5 转化递归算法为非递归算法
总结我们本章至此讨论过的递归算法,可得出递归 算法的两个特性: (1)递归算法是一种分而治之的、把复杂问题分解 为简单问题的求解问题方法,对求解某些复杂问题, 递归算法分析问题的方法是十分有效的。 (2)递归算法的时间效率通常非常差,其时间效率 经常是实际应用中不可忍受的。
第6章 递归
另一个典型的例子是汉诺塔问题的求解。汉诺塔 问题是:设有3根标号为A,B,C的柱子,在A柱上放 着n个盘子,每一个都比下面的略小一点,要求把A柱 上的盘子全部移到C柱上。移动的规则是:(1)一次 只能移动一个盘子;(2)移动过程中大盘子不能放在 小盘子上面;(3)在移动过程中盘子可以放在A,B, C的任意一个柱子上。
第6章 递归
6.5.1 尾递归和单向递归的消除 尾递归是递归调用语句只有一个,而且是处于算法 的最后。以阶乘问题的递归算法Fact(n)为例讨论尾递归 算法的运行过程。为讨论方便,我们再次列出阶乘问题 的递归算法Fact(n),并简化掉参数n的出错检查语句,改 写递归调用语句的位置在最后,算法如下: long Fact(int n) { if(n==0)return1; return n*Fact(n-1); }
第6章 递归
分析上述算法可以发现,当递归调用返回时,返 回到上一层递归调用的下一语句,而这个返回位置正 好是算法的末尾。也就是说,以前每次递使用。因此,对于尾递归形式的递归 算法,不必利用系统的运行时栈保存各种信息。尾递 归形式的算法实际上可变成循环结构的算法。循环结 构的阶乘问题算法Fact2(n)如下:

java递归方法原理栈

java递归方法原理栈

java递归方法原理栈Java递归方法原理栈是什么?在计算机科学中,递归是一种通过调用自身来解决问题的方法。

递归方法原理栈指的是在递归过程中,每次调用方法时所使用的内存空间的一种数据结构。

栈是一种后进先出(LIFO)的数据结构,类似于我们平时使用的堆栈。

当递归方法被调用时,就会在内存中创建一个栈帧来存储该方法的局部变量和参数。

在递归调用结束后,栈帧会被销毁,内存空间被释放。

接下来我们将逐步解答有关Java递归方法原理栈的相关问题。

1. 什么是递归方法?递归方法是一种基于自身调用的算法,通常用于解决可以被分解成相同问题的子问题的问题。

递归方法通过调用自身来解决问题,直到达到基本情况并返回结果。

递归方法有助于简化问题的解决过程。

2. 递归方法的实现原理是什么?在递归方法的实现中,每一次递归调用都会创建一个新的栈帧,并将其推入栈中。

栈帧包含了方法的局部变量和参数。

当递归方法的基本条件满足时,也就是递归到达终止条件时,递归方法开始从栈中弹出栈帧,返回结果并销毁栈帧。

3. 如何理解递归方法原理栈中的栈帧?栈帧是指在递归方法调用期间,存储方法的局部变量和参数的内存区域。

在每次递归调用时,都会创建一个新的栈帧,并将其推入栈中。

当递归方法返回时,栈帧会被弹出栈并销毁,释放内存空间。

4. 为什么需要使用栈帧来实现递归方法?栈帧是实现递归方法所必需的,因为它们提供了一种可以安全地存储方法的局部变量和参数的方式。

每次递归调用都会创建一个新的栈帧,这样就可以在递归调用之间独立地维护方法的局部状态。

5. 栈的深度对递归方法有什么影响?栈的深度是指递归方法调用的嵌套层数。

每次递归调用都会创建一个新的栈帧,并将其推入栈中。

如果递归嵌套层数过深,栈的深度可能会超过系统的内存限制,导致栈溢出错误。

6. 如何避免栈溢出错误?为了避免栈溢出错误,可以采取以下措施:- 优化递归算法,减少递归的深度。

- 将递归方法转化为迭代方法。

- 增加系统的栈空间大小。

递归算法工作栈的变化详解

递归算法工作栈的变化详解

通常,一个函数在调用另一个函数之前,要作如下的事情:a)将实在参数,返回地址等信息传递给被调用函数保存; b)为被调用函数的局部变量分配存储区;c)将控制转移到被调函数的入口.从被调用函数返回调用函数之前,也要做三件事情:a)保存被调函数的计算结果;b)释放被调函数的数据区;c)依照被调函数保存的返回地址将控制转移到调用函数.所有的这些,不论是变量还是地址,本质上来说都是"数据",都是保存在系统所分配的栈中的.ok,到这里已经解决了第一个问题:递归调用时数据都是保存在栈中的,有多少个数据需要保存就要设置多少个栈,而且最重要的一点是:控制所有这些栈的栈顶指针都是相同的,否则无法实现同步.下面来解决第二个问题:在非递归中,程序如何知道到底要转移到哪个部分继续执行?回到上面说的树的三种遍历方式,抽象出来只有三种操作:访问当前结点,访问左子树,访问右子树.这三种操作的顺序不同,遍历方式也不同.如果我们再抽象一点,对这三种操作再进行一个概括,可以得到:a)访问当前结点:对目前的数据进行一些处理;b)访问左子树:变换当前的数据以进行下一次处理;c)访问右子树:再次变换当前的数据以进行下一次处理(与访问左子树所不同的方式).下面以先序遍历来说明:void preorder_recursive(Bitree T) /* 先序遍历二叉树的递归算法*/{if (T) {visit(T); /* 访问当前结点*/preorder_recursive(T->lchild); /* 访问左子树*/preorder_recursive(T->rchild); /* 访问右子树*/}}visit(T)这个操作就是对当前数据进行的处理, preorder_recursive(T->lchild)就是把当前数据变换为它的左子树,访问右子树的操作可以同样理解了.现在回到我们提出的第二个问题:如何确定转移到哪里继续执行?关键在于一下三个地方:a)确定对当前数据的访问顺序,简单一点说就是确定这个递归程序可以转换为哪种方式遍历的树结构;b)确定这个递归函数转换为递归调用树时的分支是如何划分的,即确定什么是这个递归调用树的"左子树"和"右子树"c)确定这个递归调用树何时返回,即确定什么结点是这个递归调用树的"叶子结点".二).三个例子好了上面的理论知识已经足够了,下面让我们看看几个例子,结合例子加深我们对问题的认识.即使上面的理论你没有完全明白,不要气馁,对事物的认识总是曲折的,多看多想你一定可以明白(事实上我也是花了两个星期的时间才弄得比较明白得).1)例子一: f(n) = n + 1; (n <2)= f[n/2] + f[n/4](n >= 2);这个例子相对简单一些,递归程序如下:int f_recursive(int n){ int u1, u2, f;if (n < 2)f = n + 1;else {u1 = f_recursive((int)(n/2)); u2 = f_recursive((int)(n/4));f = u1 * u2;}return f; }下面按照我们上面说的,确定好递归调用树的结构,这一步是最重要的.首先,什么是叶子结点,我们看到当n < 2时f = n + 1,这就是返回的语句,有人问为什么不是f = u1 * u2,这也是一个返回的语句呀?答案是:这条语句是在u1 = exmp1((int)(n/2))和u2 = exmp1((int)(n/4))之后执行的,是这两条语句的父结点. 其次,什么是当前结点,由上面的分析,f = u1 * u2即是父结点.然后,顺理成章的u1 = exmp1((int)(n/2))和u2 = exmp1((int)(n/4))就分别是左子树和右子树了.最后,我们可以看到,这个递归函数可以表示成后序遍历的二叉调用树.好了,树的情况分析到这里,下面来分析一下栈的情况,看看我们要把什么数据保存在栈中:非递归程序中我们已经看到了要加入一个标志域,因此在栈中要保存这个标志域;另外,u1,u2和每次调用递归函数时的n/2和n/4参数都要保存,这样就要分别有三个栈分别保存:标志域,返回量和参数,不过我们可以做一个优化,因为在向上一层返回的时候,参数已经没有用了,而返回量也只有在向上返回时才用到,因此可以把这两个栈合为一个栈.如果对于上面的分析你没有明白,建议你根据这个递归函数写出它的递归栈的变化情况以加深理解,再次重申一点:前期对树结构和栈的分析是最重要的,如果你的程序出错,那么请返回到这一步来再次分析,最好把递归调用树和栈的变化情况都画出来,并且结合一些简单的参数来人工分析你的算法到底出错在哪里.ok,下面给出我花了两天功夫想出来的非递归程序(再次提醒你不要气馁,大家都是这么过来的).代码:int f_nonrecursive(int n){int stack[20], flag[20], cp;/* 初始化栈和栈顶指针*/cp = 0;stack[0] = n;flag[0] = 0;while (cp >= 0) {switch(flag[cp]) {case 0: /* 访问的是根结点*/if (stack[cp] >= 2) { /* 左子树入栈*/flag[cp] = 1; /* 修改标志域*/cp++;stack[cp] = (int)(stack[cp - 1] / 2);flag[cp] = 0;} else { /* 否则为叶子结点*/stack[cp] += 1;flag[cp] = 2;}break;case 1: /* 访问的是左子树*/if (stack[cp] >= 2) { /* 右子树入栈*/flag[cp] = 2; /* 修改标志域*/cp += 2;stack[cp] = (int)(stack[cp - 2] / 4);flag[cp] = 1;} else { /* 否则为叶子结点*/stack[cp] += 1;flag[cp] = 2;}break;case 2: /* */if (flag[cp - 1] == 2) { /* 当前是右子树吗? *//** 如果是右子树, 那么对某一棵子树的后序遍历已经* 结束,接下来就是对这棵子树的根结点的访问*/stack[cp - 2] = stack[cp] * stack[cp - 1];flag[cp - 2] = 2;cp = cp - 2;} else/* 否则退回到后序遍历的上一个结点*/cp--;break; } }return stack[0]; }算法分析:a)flag只有三个可能值:0表示第一次访问该结点,1表示访问的是左子树,2表示已经结束了对某一棵子树的访问,可能当前结点是这棵子树的右子树,也可能是叶子结点.b)每遍历到某个结点的时候,如果这个结点满足叶子结点的条件,那么把它的flag域设为2;否则根据访问的是根结点,左子树或是右子树来设置flag域,以便决定下一次访问该节点时的程序转向.2)例子二快速排序算法递归算法如下:代码:void swap(int array[], int low, int high){ int temp;temp = array[low];array[low] = array[high];array[high] = temp; }int partition(int array[], int low, int high){int p;p = array[low];while (low < high) {while (low < high && array[high] >= p)high--;swap(array,low,high);while (low < high && array[low] <= p)low++;swap(array,low,high);}return low; }void qsort_recursive(int array[], int low, int high){ int p;if(low < high) {p = partition(array, low, high);qsort_recursive(array, low, p - 1);qsort_recursive(array, p + 1, high);} }需要说明一下快速排序的算法: partition函数根据数组中的某一个数把数组划分为两个部分,左边的部分均不大于这个数,右边的数均不小于这个数,然后再对左右两边的数组再进行划分.这里我们专注于递归与非递归的转换,partition函数在非递归函数中同样的可以调用(其实partition函数就是对当前结点的访问).再次进行递归调用树和栈的分析: 递归调用树:a)对当前结点的访问是调用partition函数;b)左子树:q sort_recursive(array, low, p - 1);c)右子树:qsort_recursive(array, p +1, high); d)叶子结点:当low < high时;e)可以看出这是一个先序调用的二叉树.栈:要保存的数据是两个表示范围的坐标.代码: void qsort_nonrecursive(int array[], int low, int high){ int m[50], n[50], cp, p;/* 初始化栈和栈顶指针*/cp = 0;m[0] = low;n[0] = high;while (m[cp] < n[cp]) {while (m[cp] < n[cp]) { /* 向左走到尽头*/p = partition(array, m[cp], n[cp]); /* 对当前结点的访问*/cp++; m[cp] = m[cp - 1];n[cp] = p - 1; }/* 向右走一步*/m[cp + 1] = n[cp] + 2;n[cp + 1] = n[cp - 1];cp++;} }3)例子三阿克曼函数: 代码:akm(m, n) = n + 1; (m = 0时)akm(m - 1, 1); (n = 0时)akm(m - 1, akm(m, n - 1)); (m != 0且n != 0时)递归算法如下: 代码:int akm_recursive(int m, int n){ int temp;if (m == 0)return (n + 1);else if (n == 0)return akm_recursive(m - 1, 1);else {temp = akm_recursive(m, n - 1);return akm_recursive(m - 1, temp);} }这道题的难点就是确定递归调用树的情况,因为从akm函数的公式可以看到,有三个递归调用,一般而言,有几个递归调用就会有几棵递归调用的子树,不过这只是一般的情况,不一定准确,也不一定非要机械化的这么作,因为通常情况下我们可以做一些优化,省去其中的一些部分,这道题就是一个例子.递归调用树的分析:a)是当m=0时是叶子结点;b)左子树是akm(m - 1, akm(m, n - 1))调用中的akm(m, n - 1)调用,当这个调用结束得出一个值temp时,再调用akm(m - 1, temp),这个调用是右子树.c)从上面的分析可以看出,这个递归调用树是后序遍历的树.栈的分析:要保存的数据是m, n,当n = 0 或m = 0时开始退栈,当n = 0时把上一层栈的m值变为m - 1,n变为1,当m = 0时把上一层栈的m值变为0,n变为n + 1.从这个分析过程可以看出,我们省略了当n = 0时的akm(m - 1, 1)调用,原来在系统机械化的实现递归调用的过程中,这个调用也是一棵子树,不过经过分析,我们用修改栈中数据的方式进行了改进.代码int akm_nonrecursive(int m, int n){ int m1[50], n1[50], cp;cp = 0; m1[0] = m; n1[0] = n;do {while (m1[cp] > 0) { /* 压栈, 直到m1[cp] = 0 */while (n1[cp] > 0) { /* 压栈, 直到n1[cp] = 0 */cp++;m1[cp] = m1[cp - 1];n1[cp] = n1[cp - 1] - 1; }/* 计算akm(m - 1, 1),当n = 0时*/m1[cp] = m1[cp] - 1;n1[cp] = 1;}/* 改栈顶为akm(m - 1, n + 1),当m = 0时*/cp--;m1[cp] = m1[cp] - 1; n1[cp] = n1[cp + 1] + 1;} while (cp > 0 || m1[cp] > 0);return n1[0] + 1;}递归算法详解C通过运行时堆栈支持递归函数的实现。

递归时内存中的调用栈的变化

递归时内存中的调用栈的变化

递归时内存中的调⽤栈的变化
先看⼀个最简单的累加的递归函数:
1function sum(n){
2if(n <= 1) return 1
3return n + sum(n - 1)
4 }
5 console.log(sum(100)); // 5050
当传⼊的参数过⼤时会导致栈溢出:
即:超过了最⼤调⽤堆栈⼤⼩
那这是如何导致栈溢出的呢?这⾥需要知道正常我们写的JS代码是如何来运⾏的,可以参考另⼀篇博客:
⼤致就是执⾏到哪个⽅法,那个⽅法就压⼊栈中,函数执⾏结束就从栈中移除。

那么,递归执⾏时调⽤栈是如何变化的呢?
就以上⾯哪个累加的递归函数为例,如传⼊的参数是5,就以⼀个图表展⽰:
说明:
这个图表是本⼈的⼤致理解,如果有错误之处请⼤神指出
执⾏sum(5),调⽤栈中从下往上压⼊了9个事件,全部压⼊了以后,从上往下执⾏
可以看出如果传⼊的参数越⼤,调⽤栈中的待执⾏事件将越多,达到⼀定程度就会超出栈能保存的最⼤容量解决递归的调⽤栈溢出可以使⽤。

递归过程与递归工作栈

递归过程与递归工作栈
}
广义表 (General Lists )
广义表的概念 n ( 0 )个表元素组成的有
限序列,记作
LS = (a0, a1, a2, …, an-1) LS是表名,ai是表元素,它可以是表 (称 为子表),可以是数据元素(称为原子)。 n为表的长度。n = 0 的广义表为空表。 n > 0时,表的第一个表元素称为广义表
n=1
sum=0+1 n=2-2
3 1 3 2
4 1 4 1 n=0
sum=1+0 n=3-2
4 1 4 2 n=1
sum=1+1 n=4-2
Fib(4)
Fib(3)
Fib(2) Fib(1)
Fib(2)
Fib(1)
Fib(0)
Fib(1)
Fib(0)
2 1 2 2
4 2 4 2 4 2
递归过程与递归工作栈
递归的概念
递归的定义 若一个对象部分地包含它 自己, 或用它自己给自己定义, 则称这 个对象是递归的;若一个过程直接地或 间接地调用自己, 则称这个过程是递归 的过程。
以下三种情况常常用到递归方法。
定义是递归的 数据结构是递归的 问题的解法是递归的
定义是递归的
例如,阶乘函数
在第 j 列安放一个皇后:
如果在列、主对角线、次对角线方向有其
它皇后,则出现攻击,撤消在第 j 列安放 的皇后。 如果没有出现攻击,在第 j 列安放的皇 后不动,递归安放第 i+1行皇后。
设置 4 个数组
col [n] :col[i] 标识第 i 列是否安放 了皇后
md[2n-1] : md[k] 标识第 k 条主对角 线是否安放了皇后
个单链表;

深入理解递归

深入理解递归

深⼊理解递归递归的思想以此类推是递归的基本思想。

具体来讲就是把规模⼤的问题转化为规模⼩的相似的⼦问题来解决。

在函数实现时,因为解决⼤问题的⽅法和解决⼩问题的⽅法往往是同⼀个⽅法,所以就产⽣了函数调⽤它⾃⾝的情况。

另外这个解决问题的函数必须有明显的结束条件,这样就不会产⽣⽆限递归的情况了。

递归的两个条件可以通过递归调⽤来缩⼩问题规模,且新问题与原问题有着相同的形式。

(⾃⾝调⽤)存在⼀种简单情境,可以使递归在简单情境下退出。

(递归出⼝)递归算法的⼀般形式:func( mode){if(endCondition){ //递归出⼝end;}else{func(mode_small) //调⽤本⾝,递归}}求⼀个数的阶乘是练习简单⽽典型的例⼦,阶乘的递推公式为:factorial(n)=n*factorial(n-1),其中n为⾮负整数,且0!=1,1!=1我们根据递推公式可以轻松的写出其递归函数:public static long factorial(int n) throws Exception {if (n < 0)throw new Exception("参数不能为负!");else if (n == 1 || n == 0)return 1;elsereturn n * factorial(n - 1);}递归的过程在求解6的阶乘时,递归过程如下所⽰。

我们会惊奇的发现这个过程和栈的⼯作原理⼀致对,递归调⽤就是通过栈这种数据结构完成的。

整个过程实际上就是⼀个栈的⼊栈和出栈问题。

然⽽我们并不需要关⼼这个栈的实现,这个过程是由系统来完成的。

那么递归中的“递”就是⼊栈,递进;“归”就是出栈,回归。

我们可以通过⼀个更简单的程序来模拟递进和回归的过程:/*** 关于递归中递进和回归的理解* @param n*/public static void recursion_display(int n) {int temp=n;//保证前后打印的值⼀样System.out.println("递进:" + temp);if (n > 0) {recursion_display(--n);}System.out.println("回归:" + temp);}递归的例⼦斐波那契数列斐波那契数列的递推公式:Fib(n)=Fib(n-1)+Fib(n-2),指的是如下所⽰的数列:1、1、2、3、5、8、13、21.....按照其递推公式写出的递归函数如下:public static int fib(int n) throws Exception {if (n < 0)throw new Exception("参数不能为负!");else if (n == 0 || n == 1)return n;elsereturn fib(n - 1) + fib(n - 2);}递归调⽤的过程像树⼀样,通过观察会发现有很多重复的调⽤。

使用递归反转堆栈

使用递归反转堆栈

使用递归反转堆栈一、什么是堆栈(Stack)?堆栈是一种先进后出(LIFO)的数据结构,类似于现实生活中的一摞书。

在堆栈中,最后一个进入的元素首先被弹出。

我们可以从顶部插入(push)元素,也可以从顶部删除(pop)元素。

堆栈的一个重要特性是它只允许访问或操作最顶部的元素。

二、堆栈的应用场景堆栈在计算机科学中有着广泛的应用。

以下是一些常见的堆栈应用场景: 1. 函数调用:函数调用时,局部变量和参数会被存储在堆栈中,每个函数调用都会在堆栈中创建一个新的帧。

2. 表达式求值:在解析和计算数学表达式时,使用堆栈来跟踪和计算操作符和操作数的顺序。

3. 内存分配和回收:堆栈用于分配和回收内存资源,确保正确的内存管理。

三、递归的概念递归是一种解决问题的方法,它将问题分解为更小的子问题,然后通过解决子问题来解决原始问题。

在递归中,函数调用自己,直到达到基本情况,即不能再继续调用自身的情况。

四、使用递归反转堆栈的原理要使用递归反转堆栈,我们需要遵循以下步骤: 1. 如果堆栈为空或只有一个元素,那么堆栈已经反转。

返回堆栈。

2. 否则,从堆栈中弹出一个元素,然后递归地反转剩余的堆栈。

3. 将被弹出的元素插入到已反转的堆栈的底部。

五、递归反转堆栈的实现下面是使用递归反转堆栈的步骤的实现:def reverse_stack(stack):if len(stack) <= 1:return stackelse:element = stack.pop()reversed_stack = reverse_stack(stack)reversed_stack.append(element)return reversed_stack在上述实现中,我们首先检查堆栈的长度。

如果堆栈为空或只有一个元素,我们直接返回堆栈。

否则,我们从堆栈中弹出一个元素,然后递归地调用reverse_stack函数来反转剩余的堆栈。

最后,我们将被弹出的元素插入到已反转的堆栈的底部,并返回反转的堆栈。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

通常,一个函数在调用另一个函数之前,要作如下的事情:a)将实在参数,返回地址等信息传递给被调用函数保存; b)为被调用函数的局部变量分配存储区;c)将控制转移到被调函数的入口.从被调用函数返回调用函数之前,也要做三件事情:a)保存被调函数的计算结果;b)释放被调函数的数据区;c)依照被调函数保存的返回地址将控制转移到调用函数.所有的这些,不论是变量还是地址,本质上来说都是"数据",都是保存在系统所分配的栈中的.ok,到这里已经解决了第一个问题:递归调用时数据都是保存在栈中的,有多少个数据需要保存就要设置多少个栈,而且最重要的一点是:控制所有这些栈的栈顶指针都是相同的,否则无法实现同步.下面来解决第二个问题:在非递归中,程序如何知道到底要转移到哪个部分继续执行?回到上面说的树的三种遍历方式,抽象出来只有三种操作:访问当前结点,访问左子树,访问右子树.这三种操作的顺序不同,遍历方式也不同.如果我们再抽象一点,对这三种操作再进行一个概括,可以得到:a)访问当前结点:对目前的数据进行一些处理;b)访问左子树:变换当前的数据以进行下一次处理;c)访问右子树:再次变换当前的数据以进行下一次处理(与访问左子树所不同的方式).下面以先序遍历来说明:void preorder_recursive(Bitree T) /* 先序遍历二叉树的递归算法*/{if (T) {visit(T); /* 访问当前结点*/preorder_recursive(T->lchild); /* 访问左子树*/preorder_recursive(T->rchild); /* 访问右子树*/}}visit(T)这个操作就是对当前数据进行的处理, preorder_recursive(T->lchild)就是把当前数据变换为它的左子树,访问右子树的操作可以同样理解了.现在回到我们提出的第二个问题:如何确定转移到哪里继续执行?关键在于一下三个地方:a)确定对当前数据的访问顺序,简单一点说就是确定这个递归程序可以转换为哪种方式遍历的树结构;b)确定这个递归函数转换为递归调用树时的分支是如何划分的,即确定什么是这个递归调用树的"左子树"和"右子树"c)确定这个递归调用树何时返回,即确定什么结点是这个递归调用树的"叶子结点".二).三个例子好了上面的理论知识已经足够了,下面让我们看看几个例子,结合例子加深我们对问题的认识.即使上面的理论你没有完全明白,不要气馁,对事物的认识总是曲折的,多看多想你一定可以明白(事实上我也是花了两个星期的时间才弄得比较明白得).1)例子一: f(n) = n + 1; (n <2)= f[n/2] + f[n/4](n >= 2);这个例子相对简单一些,递归程序如下:int f_recursive(int n){ int u1, u2, f;if (n < 2)f = n + 1;else {u1 = f_recursive((int)(n/2)); u2 = f_recursive((int)(n/4));f = u1 * u2;}return f; }下面按照我们上面说的,确定好递归调用树的结构,这一步是最重要的.首先,什么是叶子结点,我们看到当n < 2时f = n + 1,这就是返回的语句,有人问为什么不是f = u1 * u2,这也是一个返回的语句呀?答案是:这条语句是在u1 = exmp1((int)(n/2))和u2 = exmp1((int)(n/4))之后执行的,是这两条语句的父结点. 其次,什么是当前结点,由上面的分析,f = u1 * u2即是父结点.然后,顺理成章的u1 = exmp1((int)(n/2))和u2 = exmp1((int)(n/4))就分别是左子树和右子树了.最后,我们可以看到,这个递归函数可以表示成后序遍历的二叉调用树.好了,树的情况分析到这里,下面来分析一下栈的情况,看看我们要把什么数据保存在栈中:非递归程序中我们已经看到了要加入一个标志域,因此在栈中要保存这个标志域;另外,u1,u2和每次调用递归函数时的n/2和n/4参数都要保存,这样就要分别有三个栈分别保存:标志域,返回量和参数,不过我们可以做一个优化,因为在向上一层返回的时候,参数已经没有用了,而返回量也只有在向上返回时才用到,因此可以把这两个栈合为一个栈.如果对于上面的分析你没有明白,建议你根据这个递归函数写出它的递归栈的变化情况以加深理解,再次重申一点:前期对树结构和栈的分析是最重要的,如果你的程序出错,那么请返回到这一步来再次分析,最好把递归调用树和栈的变化情况都画出来,并且结合一些简单的参数来人工分析你的算法到底出错在哪里.ok,下面给出我花了两天功夫想出来的非递归程序(再次提醒你不要气馁,大家都是这么过来的).代码:int f_nonrecursive(int n){int stack[20], flag[20], cp;/* 初始化栈和栈顶指针*/cp = 0;stack[0] = n;flag[0] = 0;while (cp >= 0) {switch(flag[cp]) {case 0: /* 访问的是根结点*/if (stack[cp] >= 2) { /* 左子树入栈*/flag[cp] = 1; /* 修改标志域*/cp++;stack[cp] = (int)(stack[cp - 1] / 2);flag[cp] = 0;} else { /* 否则为叶子结点*/stack[cp] += 1;flag[cp] = 2;}break;case 1: /* 访问的是左子树*/if (stack[cp] >= 2) { /* 右子树入栈*/flag[cp] = 2; /* 修改标志域*/cp += 2;stack[cp] = (int)(stack[cp - 2] / 4);flag[cp] = 1;} else { /* 否则为叶子结点*/stack[cp] += 1;flag[cp] = 2;}break;case 2: /* */if (flag[cp - 1] == 2) { /* 当前是右子树吗? *//** 如果是右子树, 那么对某一棵子树的后序遍历已经* 结束,接下来就是对这棵子树的根结点的访问*/stack[cp - 2] = stack[cp] * stack[cp - 1];flag[cp - 2] = 2;cp = cp - 2;} else/* 否则退回到后序遍历的上一个结点*/cp--;break; } }return stack[0]; }算法分析:a)flag只有三个可能值:0表示第一次访问该结点,1表示访问的是左子树,2表示已经结束了对某一棵子树的访问,可能当前结点是这棵子树的右子树,也可能是叶子结点.b)每遍历到某个结点的时候,如果这个结点满足叶子结点的条件,那么把它的flag域设为2;否则根据访问的是根结点,左子树或是右子树来设置flag域,以便决定下一次访问该节点时的程序转向.2)例子二快速排序算法递归算法如下:代码:void swap(int array[], int low, int high){ int temp;temp = array[low];array[low] = array[high];array[high] = temp; }int partition(int array[], int low, int high){int p;p = array[low];while (low < high) {while (low < high && array[high] >= p)high--;swap(array,low,high);while (low < high && array[low] <= p)low++;swap(array,low,high);}return low; }void qsort_recursive(int array[], int low, int high){ int p;if(low < high) {p = partition(array, low, high);qsort_recursive(array, low, p - 1);qsort_recursive(array, p + 1, high);} }需要说明一下快速排序的算法: partition函数根据数组中的某一个数把数组划分为两个部分,左边的部分均不大于这个数,右边的数均不小于这个数,然后再对左右两边的数组再进行划分.这里我们专注于递归与非递归的转换,partition函数在非递归函数中同样的可以调用(其实partition函数就是对当前结点的访问).再次进行递归调用树和栈的分析: 递归调用树:a)对当前结点的访问是调用partition函数;b)左子树:q sort_recursive(array, low, p - 1);c)右子树:qsort_recursive(array, p +1, high); d)叶子结点:当low < high时;e)可以看出这是一个先序调用的二叉树.栈:要保存的数据是两个表示范围的坐标.代码: void qsort_nonrecursive(int array[], int low, int high){ int m[50], n[50], cp, p;/* 初始化栈和栈顶指针*/cp = 0;m[0] = low;n[0] = high;while (m[cp] < n[cp]) {while (m[cp] < n[cp]) { /* 向左走到尽头*/p = partition(array, m[cp], n[cp]); /* 对当前结点的访问*/cp++; m[cp] = m[cp - 1];n[cp] = p - 1; }/* 向右走一步*/m[cp + 1] = n[cp] + 2;n[cp + 1] = n[cp - 1];cp++;} }3)例子三阿克曼函数: 代码:akm(m, n) = n + 1; (m = 0时)akm(m - 1, 1); (n = 0时)akm(m - 1, akm(m, n - 1)); (m != 0且n != 0时)递归算法如下: 代码:int akm_recursive(int m, int n){ int temp;if (m == 0)return (n + 1);else if (n == 0)return akm_recursive(m - 1, 1);else {temp = akm_recursive(m, n - 1);return akm_recursive(m - 1, temp);} }这道题的难点就是确定递归调用树的情况,因为从akm函数的公式可以看到,有三个递归调用,一般而言,有几个递归调用就会有几棵递归调用的子树,不过这只是一般的情况,不一定准确,也不一定非要机械化的这么作,因为通常情况下我们可以做一些优化,省去其中的一些部分,这道题就是一个例子.递归调用树的分析:a)是当m=0时是叶子结点;b)左子树是akm(m - 1, akm(m, n - 1))调用中的akm(m, n - 1)调用,当这个调用结束得出一个值temp时,再调用akm(m - 1, temp),这个调用是右子树.c)从上面的分析可以看出,这个递归调用树是后序遍历的树.栈的分析:要保存的数据是m, n,当n = 0 或m = 0时开始退栈,当n = 0时把上一层栈的m值变为m - 1,n变为1,当m = 0时把上一层栈的m值变为0,n变为n + 1.从这个分析过程可以看出,我们省略了当n = 0时的akm(m - 1, 1)调用,原来在系统机械化的实现递归调用的过程中,这个调用也是一棵子树,不过经过分析,我们用修改栈中数据的方式进行了改进.代码int akm_nonrecursive(int m, int n){ int m1[50], n1[50], cp;cp = 0; m1[0] = m; n1[0] = n;do {while (m1[cp] > 0) { /* 压栈, 直到m1[cp] = 0 */while (n1[cp] > 0) { /* 压栈, 直到n1[cp] = 0 */cp++;m1[cp] = m1[cp - 1];n1[cp] = n1[cp - 1] - 1; }/* 计算akm(m - 1, 1),当n = 0时*/m1[cp] = m1[cp] - 1;n1[cp] = 1;}/* 改栈顶为akm(m - 1, n + 1),当m = 0时*/cp--;m1[cp] = m1[cp] - 1; n1[cp] = n1[cp + 1] + 1;} while (cp > 0 || m1[cp] > 0);return n1[0] + 1;}递归算法详解C通过运行时堆栈支持递归函数的实现。

相关文档
最新文档