一、什么是递归
递归算法,就是直接或间接调用自身的函数,也就是把一个大的复杂的问题层层转换为一个小的和原问题相似的问题来求解的这样一种策略。
上面解释可能有点太官方了,来看看知乎上大神的通俗易懂的解释:
解释一:
“古之欲明明德于天下者,先治其国;欲治其国者,先齐其家;欲齐其家者,先修其身;欲修其身者,先正其心;欲正其心者,先诚其意;欲诚其意者,先致其知,致知在格物。物格而后知至,知至而后意诚,意诚而后心正,心正而后身修,身修而后家齐,家齐而后国治,国治而后天下平。”
这是一个调用自身的过程,我们把”明德于天下“当作函数本身来理解,每一层调用的参数依次是治国、齐家、修身、正心、诚意、致知、格物。最后在”格物“触发返回条件。
解释二:
天下有奇族人姓计,长生不老。一日其孙问其父:吾之18代祖名何?
其父不明,父问其父
其父不明,父问其父
其父不明,父问其父
其父不明,父问其父
…
晌后,其18代祖回其子:你猜
然其回其子:你猜
然其回其子:你猜
然其回其子:你猜
然其回其子:你猜
……
终,计姓末代孙知其18代祖名“你猜”
解释三:
假设你在一个电影院,你想知道自己坐在哪一排,但是前面人很多,你懒得去数了,于是你问前一排的人「你坐在哪一排?」,这样前面的人 (代号 A) 回答你以后,你就知道自己在哪一排了——只要把 A 的答案加一,就是自己所在的排了。不料 A 比你还懒,他也不想数,于是他也问他前面的人 B「你坐在哪一排?」,这样 A 可以用和你一模一样的步骤知道自己所在的排。然后 B 也如法炮制。直到他们这一串人问到了最前面的一排,第一排的人告诉问问题的人「我在第一排」。最后大家就都知道自己在哪一排了。
二、递归特点
递归算法解决问题的特点:
- 递归就是方法里调用自身。
- 在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
- 递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
- 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。
递归一般过程:
循环、迭代、遍历和递归的区别:
- 循环(loop),指的是在满足条件的情况下,重复执行同一段代码。比如,while语句。
循环则技能对应集合,列表,数组等,也能对执行代码进行操作。 - 迭代(iterate),指的是按照某种顺序逐个访问列表中的每一项。比如,for语句。
迭代只能对应集合,列表,数组等。不能对执行代码进行迭代。 - 遍历(traversal),指的是按照一定的规则访问树形结构中的每个节点,而且每个节点都只访问一次。
遍历同迭代一样,也不能对执行代码进行遍历。 - 递归(recursion),指的是一个函数不断调用自身的行为。比如,以编程方式输出著名的斐波纳契数列
三、递归例子
1、N的阶乘
递归版:
int factorial(int index)
{
return index==1?1:index*factorial(index - 1);
}
非递归版:
int factorial_norecur(int index)
{
int result=1;
while(index>0)
{
result*=index;
index-=1;
}
return result;
}
递归过程(比如5的阶乘):
其实也就是如下过程:
factorial(5)
5*factorial(4)
5*(4*factorial(3))
5*(4*(3*factorial(2)))
5*(4*(3*(2*factorial(1)))))
5*(4*(3*(2*1)))
5*(4*(3*2))
5*(4*6)
5*24
120
2、1到N的和
int nsum(int n)
{
return n==0?0:n+factorial(n- 1);
}
3、斐波拉契
1、递归实现
使用公式f[n]=f[n-1]+f[n-2],依次递归计算,递归结束条件是f[1]=1,f[2]=1。
int fib1(int index) //递归实现
{
if(index<1)
{
return -1;
}
if(index==1 || index==2)
return 1;
return fib1(index-1)+fib1(index-2);
}
2、数组实现
空间复杂度和时间复杂度都是0(n),效率一般,比递归来得快。
int fib2(int index) //数组实现
{
if(index<1)
{
return -1;
}
if(index<3)
{
return 1;
}
int *a=new int[index];
a[0]=a[1]=1;
for(int i=2;i<index;i++)
a[i]=a[i-1]+a[i-2];
int m=a[index-1];
delete a; //释放内存空间
return m;
}
3、vector实现
时间复杂度是0(n),时间复杂度是0(1),就是不知道vector的效率高不高,当然vector有自己的属性会占用资源。
int fib3(int index) //借用vector<int>实现
{
if(index<1)
{
return -1;
}
vector<int> a(2,1); //创建一个含有2个元素都为1的向量
a.reserve(3);
for(int i=2;i<index;i++)
{
a.insert(a.begin(),a.at(0)+a.at(1));
a.pop_back();
}
return a.at(0);
}
4、queue实现
当然队列比数组更适合实现斐波那契数列,时间复杂度和空间复杂度和vector一样,但队列太适合这里了,
f(n)=f(n-1)+f(n-2),f(n)只和f(n-1)和f(n-2)有关,f(n)入队列后,f(n-2)就可以出队列了。
int fib4(int index) //队列实现
{
if(index<1)
{
return -1;
}
queue<int>q;
q.push(1);
q.push(1);
for(int i=2;i<index;i++)
{
q.push(q.front()+q.back());
q.pop();
}
return q.back();
}
5、迭代实现
迭代实现是最高效的,时间复杂度是0(n),空间复杂度是0(1)。
int fib5(int n) //迭代实现
{
int i,a=1,b=1,c=0;
if(n<1) return -1;
else if(n<2) return 1;
for(i=2;i<n;i++)
{
c=a+b; //辗转相加法(类似于求最大公约数的辗转相除法)
a=b;
b=c;
}
return c;
}
6、公式实现
斐波那契数列有公式的,所以可以使用公式来计算的。
int fib6(int n)
{
double gh5=sqrt((double)5);
return (pow((1+gh5),n)-pow((1-gh5),n))/(pow((double)2,n)*gh5);
}
对于斐波拉契有很多变种,比如跳台阶,共n个台阶,每次最多两步,有多少种跳法。我们知道最后那个台阶有两种选择可到达,要不从n-1台阶跳一步,要不从n-2台阶跳两步,那么可得到f[n]=f[n-1]+f[n-2]。同理,如果每次最多跳三步,那么就是f[n]=f[n-1]+f[n-2]+f[n-3]。
4、求两个数的最大公约数
long gcd(int a,int b) //递归版
{
if(a%b==0)
return b;
return gcd(b,a%b);
}
long gcd_norecur(int a,int b) //非递归版
{
int temp;
while(b!=0)
{
temp=a%b;
a=b;
b=temp;
}
return a;
}
四、递归与非递归的转化
递归是指某个函数或过程直接或间接的调用自身。一般地一个递归包括递归出口和递归体两部分,递归出口确定递归到何时结束,而递归体确定递归求解时的递推关系。
递归算法有两个基本特征:一是递归算法是一种分而治之的、把复杂问题分解为简单问题的求解问题方法,对于求解某些复杂问题,递归算法分析问题的方法是有效地;而递归算法的时间、控件效率通常比较差。因此对解决某些问题时,我们希望用递归算法分析问题,用非递归算法解决问题,这就需要把递归算法转换为非递归算法。
把递归算法转化为非递归算法有如下三种基本方法:
- 通过分析,跳过分解过程,直接用循环结构的算法实现求解过程。比如N的阶乘,斐波拉契,最大公约数
- 自己用栈模拟系统的运行时栈,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。比如树的遍历。