前言:遍历二叉树,是学习树这种数据结构首先要理解的一种基本操作,比较简单地方式就是用递归去遍历。鉴于递归这种调用方法有一定的特殊性,本篇博客就来介绍一下递归的定义以及几个递归的经典算法题。
一说起递归,我想每个人都不陌生。举个从小就听过的例子:从前有座山,山里有座庙,庙里有个和尚,和尚在讲故事,从前有座山,山里有座庙,庙里有个和尚,和尚在讲故事,从前有座山...
这其实是抽象出来的递归现象,但是严格来说并不是递归,因为会一直重复下去,没有终止条件,那就称为死循环了。
递归:程序调用自身的编程技巧称为递归( recursion)。
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
1、递归调用的特殊性在于自己调用自己,一层一层调用,直到递归结束条件成立,再一层一层返回。递归是需要终止条件的,否则就变成了死循环。递归算法它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
故构成递归需具备的条件:
a. 子问题须与原始问题为同样的事,且更为简单;
b. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。
2、一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
举例:假设你在一个电影院,你想知道自己坐在哪一排,但是前面人很多,你懒得去数了,于是你问前一排的人「你坐在哪一排?」,这样前面的人 (代号 A) 回答你以后,你就知道自己在哪一排了——只要把 A 的答案加一,就是自己所在的排了。不料 A 比你还懒,他也不想数,于是他也问他前面的人 B「你坐在哪一排?」,这样 A 可以用和你一模一样的步骤知道自己所在的排。然后 B 也如法炮制。直到他们这一串人问到了最前面的一排,第一排的人告诉问问题的人「我在第一排」。最后大家就都知道自己在哪一排了。
总结:正确的递归函数必须包含基础部分和递归部分。每一次递归调用,其参数值都比上一次的参数值要小,从而重复调用递归函数使参数值达到基础部分的值,结束递归。基础部分就是中止/边界条件。
1、递归算法一般用于解决三类问题:
(1)数据的定义是按递归定义的。(Fibonacci函数)
(2)问题解法按递归算法实现。这类问题虽则本身没有明显的递归结构,但用递归求解比迭代求解更简单,如Hanoi问题。
(3)数据的结构形式是按递归定义的。如二叉树、广义表等,由于结构本身固有的递归特性,则它们的操作可递归地描述。
2、递归的优点
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
3、递归的缺点:递归算法解题相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,用来保存函数断点的状态比如函数返回地址和局部变量等等;每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
1、阶乘(factorial)
问题描述:一个正整数n的阶乘是所有小于及等于该数的正整数的积,公式为:n! = n * (n-1)!,其中0! = 1
递归思想:
基础部分为n<=1,所以边界条件为n<=1时,return 1;
递归部分为n *n-1。
public int factorial(int n)
{
if(n<=1)
return 1;
else
return n*factorial(n-1);
}
2、输出n个不同元素的全排列
a、b和c的排列有abc、acb、bac、bca、cab、cba。n个元素的排列个数为n!
问题描述:设计一个递归算法生成n个元素{r1,r2,…,rn}的全排列。
算法思想:
(1)n个元素的全排列=(n-1个元素的全排列)+(另一个元素作为前缀);
(2)出口:如果只有一个元素的全排列,则说明已经排完,则输出数组;
(3)不断将每个元素放作第一个元素,然后将这个元素作为前缀,并将其余元素继续全排列,等到出口,出口出去后还需要还原数组;
【算法讲解】
设R={r1,r2,…,rn}是要进行排列的n个元素,Ri=R-{ri}。
集合X中元素的全排列记为perm(X)。
(ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀得到的排列。
R的全排列可归纳定义如下:
当n=1时,perm(R)=(r),其中r是集合R中唯一的元素;
当n>1时,perm(R)由(r1)perm(R1),(r2)perm(R2),…,(rn)perm(Rn)构成。
算法实现思想:将整组数中的所有的数分别与第一个数交换,这样就总是在处理后n-1个数的全排列。
/*
* 全排列问题,设计一个递归算法生成n个元素{r1,r2,…,rn}的全排列。
*/
public class AllSequence
{
//产生list[k:m]的所有排列,数组元素list[k]至list[m]简记为list[k:m],其中m>=k
public void Perm(int list[], int k, int m)
{
if (k == m)
{
//list[k:m],仅有一个排列,输出它(只有一个元素)
for (int i = 0; i <= m; i++)
System.out.print(list[i]);
System.out.println();
}
else
{
//还有多个元素待排列,递归产生排列
for (int i = k; i <= m; i++)
{
//从固定的数后第一个依次交换
swap(list, k, i);
Perm(list, k + 1, m);
// 这组递归完成之后需要交换回来
swap(list, k, i);
}
}
}
//交换数组元素
public void swap(int[] list, int a, int b) {
int temp = list[a];
list[a] = list[b];
list[b] = temp;
}
public static void main(String[] args) {
AllSequence allSequence = new AllSequence();
int[] arr = {1,2,3,4};
allSequence.Perm(arr, 0, 3);
}
}
输出结果:
3、斐波那契数列(Fibonacci sequence)
问题描述:斐波那契数列,又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。指的是这样一个数列:1,1,2,3,5,8,13,21,34,55,89,144……依次类推下去。你会发现,这个数列从第3项开始,每一项都等于前两项之和。在这个数列中的数字,就被称为斐波那契数。
递归思想:一个数等于前两个数的和。(这并不是废话,这是执行思路)
import java.util.Scanner;
/*
* 斐波那契数列递归和普通循环实现
*/
public class Demo {
//递归实现
public static int f(int n)
{
if(n <= 1){
return n;
}
return f(n-1)+f(n-2);
}
public static void main(String[] args)
{
System.out.println("请输入要计算第多少位数字:");
Scanner scanner = new Scanner(System.in);
int next = scanner.nextInt();
for (int i = 0; i < next; i++)
System.out.println(f(i));
}
// 普通循环写法,输出结果为:1 1 2 3 5 8 13 21 34 55 89 144
public void ordinaryF()
{
int num1 = 1;
int num2 = 1;
int num3 = 0;
System.out.print(num1);
System.out.print(num2);
//此处n为10,共打印了12个斐波那契数
for (int i =0; i < 10; i++) {
num3 = num1 + num2;
num1 = num2;
num2 = num3;
System.out.print(num3);
}
}
}
兔子数列对应的兔子繁殖问题,可以变形为上台阶问题,这篇文章介绍得很详细:什么叫斐波那契数列?
最后,感谢前辈博客:递归整理及几个经典题目