一、实验目的和要求:(本次实验所涉及并要求掌握的知识点)
1.体会递归问题的两个基本条件。
2.比较递归与显式用栈实现的方法。
3.思考递归编程存在的局限性及解决方法。
4.完成实验报告。
二、实验环境:(本次实验所需要的平台和相关软件)
Windows 11
Visual Studio 2022
Dev-C++5.10
三、实验内容及步骤:(本次实验计划安排的实验内容和具体实现步骤)
1.编写把十进制正整数转换为S进制(S=2,8,16)数输出的递归算法。
2.编写出计算Fib(n)的递归算法。并分析递归算法和非递归算法的时间复杂度和空间复杂度。
3.杨辉三角形的递归实现。
4.一个射击运动员打靶,靶一共有10环,连开10枪打中90环的可能性有多少种?试用递归算法实现。 程序结果一共有92 378种可能。
5.编写一个递归算法,输出自然数1~n这n个数的全排列
6.编写GRAY码的递归算法。
四、实验过程和结果:(记录实验过程和结果、以及所出现的问题和解决方法)
1.进制转换:
(1)进制转换递归实现:
每一次取余的操作都是一致的。且终止条件存在,为n==0。
正常输入输出: 健壮性:
(2)进制转换链栈实现:
将余数转换成字符压入栈内,随后让所有元素依序出栈,改变余数字符的存储顺序,存入字符数组内并输出。
正常输入输出: 健壮性:
2.斐波那契数列:
(1)递归实现:第一和第二个元素为1,后续元素等于前两个元素之和。
<1>计算时间复杂度:
调用总次数为2^(N-1)-1,则时间复杂度为O(2^n)
<2>计算空间复杂度:
递归的斐波那契数列的空间复杂度是O(n)。Fib(N-1)一路沿Fib(2)返回后消毁栈帧,调用Fib(N-2)并建立栈帧后,实际上 Fib(N-2)与Fib(N-1)用的是同一块栈帧空间。这是由于时间是累积的,而空间是可以重复利用的。
(2)非递归实现:
法一:迭代实现
<1>时间复杂度:O(n)
<2>空间复杂度:O(1)
法二:用三个变量来回计算
<1>时间复杂度:O(n)
<2>空间复杂度:O(1)
3.杨辉三角递归:第一列和正对角线上的数字全为1,其他数字为其上方和左上方的数字之和。
4.射击运动员打靶递归:
当前为第u个靶子,共有n个靶子,cnt为当前环数总和,sum是环数总和。
第u个靶子可以取的环数范围为0~10,若到达最后一个靶子后,当前环数总和与最后环数总和相等,则输出存储各靶子环数的数组path,否则退回最后一个靶子,重新从0~10取环数。以此类推。
5.全排列递归:总共有n个数字的位置,第u个位置, 用1~n的数字填充且不能重复,用w数组记录数字是否使用过,用path数组记录当前数字,由于填充的位置下标为0~n-1,所以当到达第n个位置时,输出path数组中存储的数字。以此类推,返回并采用当前位置未使用过的数字。
6.GRAY码递归:求num个n位Gray码,分治到只有1位Gray码的情况;将求解n位Gray码的问题划分成求解n-1位Gray码的问题,n位Gray码的前半部分最高位置0,其余位不变,n位Gray码的后半部分最高位置1,其余位由n-1位Gray码翻转而来。
五、实验中的问题及解决:(本次实验中在编程中碰到的错误等问题及解决方法)
1.递归与尾递归的区别:
(1)实现斐波那契数列的递归函数:
int fib (int n)
{
if (n <= 1)
return n;
else
return fib (n - 1) + fib (n - 2);
}
这个递归函数会不断地调用自己来计算斐波那契数列。这个实现在小规模的输入上是有效的,但当输入值较大时,递归的层数会变得很深,导致性能下降,并可能导致栈溢出的问题。
(2)实现斐波那契数列的尾递归函数:
int fib (int n, int a = 0, int b = 1)
{
if (n == 0)
return a;
else
return fib(n - 1, b, a + b);
}
在这个尾递归函数中,递归调用位于函数的最后一步,并且使用新的参数直接取代了之前的参数。这样就避免了在递归调用之后还需要进行额外的计算或操作。这个方式下,递归调用并不会增加栈的深度,因此不会有栈溢出的风险。尾递归函数可以通过对参数的更新来实现迭代的效果,从而提高性能和减少内存的使用。
2.斐波那契数列三种方法的时空复杂度计算:
(1)递归:
<1>时间复杂度:
由于每次递归调用都会生成两个新的递归调用,且递归树是指数级的,因此时间复杂度为 O(2^n)。
<2>空间复杂度:
每个递归调用会在栈中创建一个新的函数帧,而递归栈的深度取决于输入值 n,因此空间复杂度为 O(n)。
(2)迭代:
<1>时间复杂度:
迭代方式的斐波那契数列计算仅需要一次循环遍历,每次迭代都在常数时间内完成。因此,时间复杂度为 O(n)。
<2>空间复杂度:
尽管数组大小为 100,但实际上只使用了数组中的前 n 个元素,其中 n 是输入的斐波那契数列位置。因此,在实际计算过程中,空间复杂度是与 n 相关的。不过根据该代码实现中 n 的输入上限为 100,因此空间复杂度可以视为常数级别。
(3)三变量:
<1>时间复杂度:
迭代方式的斐波那契数列计算使用了一个循环来计算第 n 位的数值。循环的次数与输入值 n 相关,因此时间复杂度为 O(n)。
<2>空间复杂度:
在迭代方式中,只使用了几个整数变量来保存中间结果和计算,没有使用数组或其他额外的数据结构。因此,空间复杂度为 O(1),即常数级别的空间复杂度。
六、实验总结和思考:(填写收获和体会,分析成功或失败的原因):
1.什么是递归:
递归函数直接调用自己或通过一系列调用语句间接调用自己。
有些问题使用传统的迭代算法是很难求解甚至无解的,而使用递归却可以很容易的解决。比如汉诺塔问题。但递归的使用也是有它的劣势的,因为它要进行多层函数调用,所以会消耗很多堆栈空间和函数调用时间。
2.递归问题的两个基本条件:
(1)大问题可以分解为具有相同形式、相同解法的小问题。
(2)递归存在明确的终止条件。
3.递归与显式用栈实现的方法比较:
递归的优点是代码简洁易懂,可以自然地表达问题的逻辑。然而,递归也有一些缺点。由于每次函数调用都需要保存现场,递归在大规模问题上耗费的空间很大。此外,递归的执行过程中会有多次函数调用和返回,这在一些情况下可能导致性能问题。
显式用栈,指的是显式地使用栈数据结构来解决问题。通过手动操作栈的入栈和出栈操作,可以模拟递归的行为。相比于递归,使用显式栈的方式可以对栈中的数据和状态进行更加灵活的控制。同时,显式用栈不会出现递归过程中的函数调用和返回操作,因此性能会相对较好。然而,显式使用栈需要手动维护栈的状态和数据,这可能会增加编码的复杂度。
在空间要求较高、问题规模较大、性能要求较高的情况下,显式用栈会更适合。而在问题逻辑较为简单、代码易读易写的情况下,递归会更方便使用。
4.什么是栈溢出:
当一个函数被调用时,程序会在栈上为其分配一块内存区域,用于存储函数的参数、局部变量和返回地址等信息。每个函数调用都会在栈上创建一个新的栈帧,以保存这些信息。当函数执行完毕后,对应的栈帧会被销毁,释放对应的内存。
当递归函数无限递归调用,或者函数内部使用大量的局部变量导致栈空间无法容纳时,就会发生栈溢出。此时,栈空间被耗尽,无法继续为新的函数调用创建栈帧,导致程序异常终止。
5.使用栈缓解栈溢出的问题:
(1)优化递归算法:递归算法是常见的导致栈溢出的原因之一。可以考虑优化递归算法,将其转换为迭代算法或者使用尾递归优化,以减少函数调用的层次。
(2)减少局部变量的使用:局部变量会占用栈空间。如果函数中使用的局部变量较多,可以尝试减少或者合并局部变量的使用,以降低栈的负担。
(3)动态内存分配:将一部分数据从栈空间转移到堆空间,通过动态内存分配(如使用new或malloc函数)来存储大量数据或者较大的数据结构,以减轻对栈空间的占用。
(4)使用循环代替递归:如果递归不是必要的,可以考虑使用循环来替代递归调用,这样可以避免递归过深而导致的栈溢出问题。