数据结构与算法-递归

递 归(Recursion)

递归的定义

若一个对象部分地包含它自己, 或用它自己给自己定义, 则称这个对象是递归的;
若一个过程直接地或间接地调用自己, 则称这个过程是递归的过程。
在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。

因为每次调用函数时,系统都会为参数和局部变量分配新的存储区空间,因此函数调用它自身是可能的。这种类型的函数称为递归函数。
当函数调用它自身时,这个过程称为直接递归。
如果过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。
如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。

int fac(int n)
{    
        if (n==0||n==1) 			
             return  1;		
        else 			
             return  n * fac(n-1);	
}

这是求n!(n为正整数)的递归函数。在该函数fac(n)求解过程中,直接调用fac(n-1)自身,所以它是一个直接递归函数。又由于递归调用是最后一条语句,所以它又属于尾递归。

何时使用递归

在以下三种情况下,常常用到递归方法:
1. 定义是递归的
许多数学公式、数列等的定义是递归的。这些问题的求解过程可以将其递归定义直接转化为对应的递归算法。
【例】阶乘公式 阶乘的递归算法

在这里插入图片描述
【例】斐波那契数列的定义
在这里插入图片描述
斐波那契数列的递归算法
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……

long Fib ( long n ) 
{    if ( n <= 1 ) 
	return n;		    else 
	return Fib (n-1) + Fib (n-2);
}	

2. 数据结构是递归的
【例】单链表结构
在这里插入图片描述

next的数据类型是自身(Node)类型

在这里插入图片描述
对于递归数据结构,采用递归的方法编写算法既方便又有效。
【例】查找链表最后一个结点
在这里插入图片描述
【例】在链表中查找等于给定值的结点
在这里插入图片描述
3. 问题的解法是递归的
【例】汉诺塔(Tower of Hanoi)问题
在这里插入图片描述
任务:将塔座A上的圆盘移动到塔座C上。
规则:1. 每次移动一个圆盘;
2. 圆盘只能在塔座A,B,C之间移动;
3. 大圆盘不能放在小圆盘上。
在这里插入图片描述

递归模型

递归模型是递归算法的抽象,它反映一个递归问题的递归结构。

例如,前面阶乘递归算法对应的递归模型如下:
fun(1)=1 (1)
fun(n)=n*fun(n-1) n>1 (2)
其中,第一个式子给出了递归的终止条件,第二个式子给出了fun(n)的值与fun(n-1)的值之间的关系,我们把第一个式子称为递归出口,把第二个式子称为递归体。

一般地,一个递归模型是由递归出口和递归体两部分组成,前者确定递归到何时结束,后者确定递归求解时的递推关系。递归出口的一般格式如下:
f(s1)=m1
这里的s1与m1均为常量,有些递归问题可能有几个递归出口。递归体的一般格式如下:
f(sn)=g( f(sn-1), c )
这里的sn是一个递归“大问题”,sn-1为递归“小问题”,c是可以直接(用非递归方法)解决的问题,g是一个非递归函数,可以直接求值。

递归的执行过程

递归思路是把一个不能或不好直接求解的“大问题”转化成一个或几个“小问题”来解决,再把这些“小问题”进一步分解成更小的“小问题”来解决,如此分解,直至每个“小问题”都可以直接解决(分解到递归出口)。

一旦遇到递归出口,分解过程结束,开始求值过程,所以分解过程是“量变”过程,即原来的“大问题”在慢慢变小,但尚未解决,遇到递归出口后,便发生了“质变”,即原递归问题便转化成直接问题。

递归分解要保证“大问题”与“小问题”相似,即求解过程与环境都相似。
递归的执行过程由分解和求值两部分构成

递归工作栈

递归过程在实现时,需要自己调用自己。
主程序第一次调用递归过程为外部调用;
递归过程每次递归调用自己为内部调用。
层层向下递归,退出时的次序正好相反:

一个递归函数的运行过程类似于多个函数的嵌套调用,差别仅在于“调用函数和被调用函数是同一个函数”。

为了保证“每一层的递归调用”都是对“本层”的数据进行操作,在执行递归函数的过程中需要一个“递归工作栈”。它的作用是:

  • 将递归调用时的实参和函数返回地址传递给下一层执行的递归函数;
  • 保存本层的参数和局部变量,以便从下一层返回时重新使用它们。

递归的设计方法

递归的求解的过程均有这样的特征:先将整个问题划分为若干个子问题,通过分别求解子问题,最后获得整个问题的解。而这些子问题具有与原问题相同的求解方法,于是可以再将它们划分成若干个子问题,分别求解,如此反复进行,直到不能再划分成子问题,或已经可以求解为止。

这种自上而下将问题分解、求解,再自下而上引用、合并,求出最后解答的过程称为递归求解过程。这是一种分而治之的算法设计方法。

递归函数的一般设计格式

Recursion(参数表n)
{
	if(递归结束条件) //递归出口
		return(一般为常量); //可直接求解的步骤
	else
		return Recursion(较小的n); //递归项
}

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

递归算法有两个基本特性:

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

  • 二是递归算法的时间效率通常比较差。因此,对求解某些问题时,我们希望用递归算法分析问题,用非递归算法具体求解问题。这就需要把递归算法转换为非递归算法。

转换方法
对于尾递归和单向递归的算法,可用循环结构的算法替代。(直接转化)
自己用栈模拟系统的运行时栈,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。(间接转化,需要使用栈)
利用系统栈保存参数,由于栈的后进先出特性符合递归算法的执行过程,因而可以用非递归算法替代递归算法。(需要使用栈)

递归:
优点:易于理解、结构清晰;
缺点:需要进行大量栈操作,压入函数参数和返回地址,递归的层次与系统的栈大小受限有关;
循环:
优点:效率比较高;
缺点:有的时候非常难以理解,在从递归向循环转换的时候需要大量的模仿栈操作;
【建议】简单的问题尽量使用循环解决,而对于复杂的或者明显的递归思想则使用递归,这样易于理解和维护。

发布了141 篇原创文章 · 获赞 28 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42554191/article/details/103978183