函数的递归调用

递归是计算机科学的一个重要概念,是程序设计中的一种有效的方法,在程序设计语言中被广泛应用。它是指函数、过程、子程序在运行过程中直接或间接调用自身而产生的重入现象。采用递归编写程序能使程序变得简洁清晰,使人容易理解。

递归算法描述简洁,结构清晰,算法的正确性比较容易证明。但是,递归算法的执行效率低,空间消耗多,有时还会受到一些软、硬件环境条件的限制,不能使用递归技术,因此,在某些时候,还需要将递归算法转换为非递归算法。

1. 递归的概念

在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。

C语言的特点之一就在于允许函数的递归调用。

写一个最简单的C语言递归代码:

c 复制代码
#include<stdio.h>

// function recursion

int main() {
	printf("hello world\n");
	main();
	return 0;
}

上述就是一个简单的的递归程序,只不过上面的递归是为了演示递归的基本形式,不是为了解决问题,代码最终会陷入死递归,导致栈溢出。

2. 具有递归特性的问题

递归算法就是在算法中直接或间接调用算法本身的算法。使用递归算法有以下两个两个前提:

① 原问题可以层层分解为类似的子问题,且子问题比原问题的规模更小。

② 规模最小的子问题具有直接解。

设计递归算法的原则是用自身的简单情况来定义自身。设计递归算法的方法如下。

① 寻找分解方法:将原问题转化为子问题求解。

② 设计递归出口:根据规模最小的子问题确定递归终止条件。

3. 递归的限制条件

① 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。

② 每次递归调用之后越来越接近这个限制条件。

4. 递归过程的实现

递归进层时系统需要做三件事:

① 保留本层参数与返回地址。

② 为被调用函数的局部变量分配存储区,给下层参数赋值。

③ 将程序转移到被调用函数的入口。

而从被调用函数返回调用函数之前,递归退层时系统也相应完成三件事:

① 保存被调用函数的计算结果。

② 释放被调用函数的数据区,恢复上层参数。

③ 依照被调用函数保存的返回地址,将控制转移回调用函数。

当递归函数调用时,应按照"后调用先返回"的原则处理调用过程。系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,而每当从一个函数推出时,就释放它的存储区。

一个递归函数的运行中,调用函数和被调用函数是同一个函数,因此,与每次调用相关的一个重要概念就是递归函数运行的"层次"。

假设调用该递归函数的主函数为第 0 层,则从主函数调用递归函数为进入第 1 层...从第 i 层递归调用该函数为进入其"下一层",即第 i+1 层;反之,退出第 i 层递归应返回至其"上一层",即第 i-1 层。为了保证递归函数正确执行,系统需要设立一个递归工作栈 ,作为整个递归函数运行期间使用的数据存储区。每层递归所需信息构成一个工作记录 ,其中包含所有实在参数、所有局部变量以及上一层返回地址。每进入一层递归,就产生一个新的工作记录压入栈顶;每退出一层递归,就从栈顶弹出一个工作记录。因此当前执行层的工作记录必为递归工作栈栈顶的工作记录;该记录称为活动记录 ,指示活动记录的栈顶指针成为当前环境指针。由于递归工作栈是由系统来管理的,无须用户操心,所以用递归法编制程序非常方便。

5. 递归算法到非递归算法的转换

递归算法具有以下两个特性:

① 递归算法是一种分而治之、把复杂问题分解为简单问题的问题求解方法,对求解某些复杂问题,递归分析方法是有效的。

② 递归算法的效率较低。

为此,在求解某些问题时,希望用递归算法分析问题,用非递归算法求解具体问题。

消除递归的原因:

① 有利于提高算法的时空性能,因为递归执行时需要系统提供隐式栈来实现递归,所以效率较低。

② 无应用递归语句的语言设施环境条件下,有些计算机语言不支持递归功能,如 FORTRAN。

③ 递归算法是一次执行完成的,中间过程对用户不可见,这在处理有些问题时不适用。

基于以上原因,需要把一个递归算法转换为非递归算法。

理解递归机制是掌握递归程序设计的必要前提。消除递归应基于对问题的分析,常用的有以下两类消除递归的方法:

① 简单递归问题的转换,对于尾递归和单向递归的算法,可用循环结构的算法代替。

② 基于栈的方式,即递归中隐含的栈机制转换为由用户直接控制的明显的栈,利用栈来保存参数;由于栈的后进先出特性吻合算法的执行过程,因而可以用非递归算法代替递归算法。

简单递归的消除:

在简单情况下,可以将递归算法转换为线性操作序列,直接用循环实现。

① 单向递归

单向递归是指递归函数中虽然有多次递归调用语句,但各次递归调用语句的参数只和主调用函数有关,相互参数之间无关,并且这些递归调用语句处于算法的最后。

② 尾递归

尾递归是指递归调用语句只有一个,而且是处于算法的最后。尾递归是单向递归的特例。

6. 递归举例

6.1 求n的阶乘

6.2 顺序打印一个整数的每一位

c 复制代码
#include<stdio.h>

void Print(int x) {
	if (x > 9) {
		Print(x / 10);
	}
	printf("%d ", x%10);
}

int main() {
	int n = 0;
	scanf("%d", &n);
	Print(n);
	return 0;
}

6.3 求第n个斐波那契数

c 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<time.h>

int fib(int x) {
	if (x <= 2) {
		return 1;
	}
	else {
		return fib(x - 1) + fib(x - 2);
	}
}

int main() {
	clock_t start, end;
	start = clock();

	int n = 0;
	scanf("%d", &n);
	printf("%d\n", fib(n));

	end = clock();
	printf("%.6f\n", (double)((end - start) / CLOCKS_PER_SEC));
	return 0;
}
c 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<time.h>

int main() {
	clock_t start, end;
	start = clock();

	int a = 1;
	int b = 1;
	int c = 1;
	int n = 0;
	scanf("%d", &n);
	int i = 0;
	while (n > 2) {
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	end = clock();
	printf("%d\n", c);
	printf("%.6f\n", (double)((end - start) / CLOCKS_PER_SEC));
	return 0;
}
相关推荐
zhangfeng113310 小时前
ThinkPHP5 事件系统的标准最佳实践 事件系统的完整设计逻辑tags.php tags.php(事件地图)
android·开发语言·php
xyq202410 小时前
HTML 标签简写及全称
开发语言
tongluowan00710 小时前
数据结构 Bitmap(位图)示例 - 用户签到系统
开发语言·数据结构·bitmap·用户签到系统
就叫_这个吧10 小时前
Java线程池应用的四种方式+线程池底层实现原理
java·开发语言
Rust研习社10 小时前
Rust 官方拟定 LLM 政策,防止 LLM 污染开源社区?
开发语言·后端·ai·rust·开源
muqsen10 小时前
Java 分布式相关面试题总结
java·开发语言·分布式
fenglllle10 小时前
JDK8升级JDK17使用CompletableFuture在线程中classloader的变化
java·开发语言·jvm
流浪00110 小时前
告别静态打印:Linux C 实现实时刷新进度条
linux·运维·c语言
froginwe1110 小时前
Scala 正则表达式
开发语言