对于强大的递归。要想做到灵活运用,是需要花时间进行练习并总结。往往递归学习的入门也是难度也比较大,常常会处于看得明,却写不出的"尴尬"情况。
递归的定义
将一个大的问题分解成比较小的、有着相同形式的问题。
递归是一种强有力的思想。在计算机科学的学习中,一个重要的必须学习的概念是递归。递归是一种编程策略,它把一个大的问题分解成具有相同形式的简单问题。
使用递归的必需条件
- 可以通过递归调用来缩小问题规模,且新问题与原问题有着相同的形式
- 存在一种简单情境,可以使递归在简单情境下退出
一般对递归思想的介绍,都是说将大问题分解为一个个小问题。本人觉得,带着 “如何将问题规模缩少”的思想 比 “将大问题分解为一个个小问题” 的思想要更好地编写递归程序。
递归跳跃的信任
当尝试理解递归程序时,必须能够抛开底层的细节,将注意力集中在单个计算层次上。在这个层次上,只要一个递归调用的参数在某些方面能比前一个参数更简单,那么就可以认为任何递归调用都能够自动地得到正确的答案。这种心理策略——假设任何更简单的递归都能正确地实现——叫做对递归跳跃的信任。在实际应用中,学习应用这个策略是使用递归的基础。
以递归的方式思考
保持整体观:递归思维要求整体考虑。在递归领域中,只考虑局部是理解的敌人,将会妨碍对递归的理解。为了保持这种整体观,必须习惯于采用对递归跳跃的信任。无论是在写递归程序或是理解递归程序,都必须达到忽视单个递归调用细节的地步。只要选择了正确的分解,确认了相应的简单情景,并且正确地实现了策略,那么这些递归调用能够自己运行,不必过多考虑。
避免常见的错误
- 检验递归实现是不是以检查简单情景开始。在几乎所有情况中,递归函数都开始于关键字 if 。如果你的函数不是这样,那么应该仔细检查程序并确信知道自己在做什么
- 正确地解决了简单情景了吗?在递归程序中很多bug都是起源于简单情景的不正确解决。如果简单情景是错误的,那么对更复杂问题的递归解决将会继承相同的错误
- 递归分解使问题更加简单了吗?递归的作用在于,随着问题解决的进行,它将变得越来越就简单。即每一次递归调用中,参数值将会越来越小
- 简化的过程是不是逐渐地达到了简单情景,或者是不是遗漏了一些可能性?常见的一个错误就是没有将全部情况的简单情景测试都包括其中,这些情况可能作为递归分解的结果而产生出来。简单说:你需要正确地将所有的简单情景分析出来
- 函数中的递归调用是不是表示了在形式上和初始问题真的完全相同的子问题?当使用递归分解问题时,关键在于子问题形式的同一性。如果递归调用改变了问题的本性,或者违反了一个初始化的假设,那么全部过程就被破环
- 当使用对递归跳跃的信任时,递归子问题的解决方案是否对初始问题提供了一个完整的解决方案?将一个问题分解为递归子问题仅仅是递归过程的一部分,一旦子问题得到了解决,那么也必须能够将他们重新组装从而得到全部的答案
在示例练习中会尽量以上述几点为基础去思考问题的解法
递归示例
阶乘函数
//对许多人而言,理解递归的最好方法是从简单的数学函数开始。
//因为数学函数中递归结构直接能从问题的陈述中得到,并且可以很容易地看到。
//在这些数学函数中,最常见的就是阶乘函数——在数学中的传统表示为 n!
//——它被定义为 1 到 n 之间的所有整数的连乘积。
//(当然,此题的解法用迭代也能轻松解决)
public class Factorial {
public static int factorial(int n) {
if (n < 0) {
return 0;
}
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(factorial(-1));
System.out.println(factorial(0));
System.out.println(factorial(5));
}
}
斐波那契数列
斐波那契数列指的是这样一个数列:0、1、1、2、3、5、8、13、21、……
在数学上,斐波纳契数列以如下被以递归的方法定义:
F(0)=0,
F0=1,
Fn=F(n-1)+F(n-2)(n>=2,n∈N*)
问题:输入 n,求斐波那契数列第n个数
解法:递归
Fn=F(n-1)+F(n-2) 像这种类型的表达式,序列中的每一个元素都由先前的元素来确定,这种序列被称为递归关系
有了递归分解式,还需简单情景进行结束递归。
简单情景:
观察可知,当n > 3 时,每项的值为前两项之和。即当n = 1 和 n = 2 时分别取值为0、1
public class Fibonacci {
public static int fibonacci(int n) {
if (n <= 0) {
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(fibonacci(-1));
System.out.println(fibonacci(0));
System.out.println(fibonacci(1));
System.out.println(fibonacci(2));
System.out.println(fibonacci(5));
System.out.println(fibonacci(10));
}
}
回文字符串
所谓回文字符串,就是一个字符串,从左到右读和从右到左读是完全一样的。比如"level" 、 “aaabbaaa”
题目:判断一个字符串是否为回文
解法:递归
递归的作用在于把问题的规模不断缩少,直到问题缩少到能简单地解决
问:如何缩少问题规模?
答:通过观察可以知道,一个回文字符串其中内部也是回文。所以,我们只需要以去掉两端的字符的形式一层层检查,每一次的检查都去掉了两个字符,这样就达到了缩少问题规模的目的。
新问题与原问题有着相同的形式
当去掉两端字符后的字符串,其产生的新问题同样是检查这个字符串是否回文。
递归的结束需要简单情景
1. 字符串长度可能会奇数或偶数:
- 如果字符串长度是奇数,字符串会剩下最中间那位字符,但其不影响回文。当检查到长度为1的时候即代表此字符串是回文
- 如果字符串长度是偶数,当两端的字符串两两比较检查后不会剩下字符。即检查到长度为0的时候即代表此字符串是回文
2. 如果检查到两端两个字符不相同。则说明此字符串不是回文,直接返回0,不需要继续检查
递归跳跃的信任
此题的递归分解比较简单,所以对在递归过程中细节的实现,我们可以直接看出。但是一些较复杂的题目上,我们就没那么容易看出过程中细节的实现,这时候就需要我们递归跳跃的信任!
public class PalindromeString {
public static boolean isPalindrome(String str) {
if (str == null || str.length() == 0) {
return false;
}
int begin = 0;
int end = str.length() - 1;
int length = str.length();
return is_Plindrome(str, begin, end, length);
}
private static boolean is_Plindrome(String str, int begin, int end, int length) {
if (length == 0 || length == 1) {
return true;
}
if (str.charAt(begin) == str.charAt(end)) {
return is_Plindrome(str, begin + 1, end - 1, length - 2);
} else {
return false;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(isPalindrome("a"));
System.out.println(isPalindrome("aa"));
System.out.println(isPalindrome("ab"));
System.out.println(isPalindrome("ab***************ba"));
}
}
字符串翻转
字符串翻转:将字符串 test 翻转,变为 tset
解法:递归
此题的递归跟判断回文字符串的解法原理一样。只是不是比较两端字符,而是直接交换。
public class ReverseString {
public static String reverseString(String str) {
String reString = new String();
if (str == null || str.length() == 0) {
return reString;
}
if (str.length() == 1) {
return str;
}
int length = str.length();
int begin = 0;
int end = length - 1;
char[] chararray = str.toCharArray();
reverse_string(chararray, begin, end, length);
return String.copyValueOf(chararray);
}
private static void reverse_string(char[] chararray, int begin, int end, int length) {
if (length == 0 || length == 1) {
return;
}
char temp = chararray[begin];
chararray[begin] = chararray[end];
chararray[end] = temp;
reverse_string(chararray, begin + 1, end - 1, length - 2);
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(reverseString("b"));
System.out.println(reverseString("ab"));
System.out.println(reverseString("abca"));
System.out.println(reverseString("987654321"));
}
}
折半(二分)查找
假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
折半(二分)查找也是一个分而治之策略的完美示例。所以,其可以用递归实现也不足为奇。
解法:递归
递归的作用在于把问题的规模不断缩少,直到问题缩少到能简单地解决
由于我们查找的须是一个有序表,所以在每一次的比较中,缩少的规模将是上一次查找规模的一半,可见这效率是相当大
新问题与原问题有着相同的形式
新问题与原问题具有相同的形式,就是查找某个字符
递归的结束需要简单情景
- 当 low >high 时,说明查找失败
- 当 p[mid] == key 时,说明查找成功
递归跳跃的信任
由于实现细节较易就能看出,未能突出体现出递归跳跃的信任的重要性
public class BinarySearch {
public static int binarysearch(int[] array, int key) {
// 查找失败返回-1
if (array == null || array.length == 0) {
return -1;
}
int begin = 0;
int end = array.length - 1;
int mid = (begin + end) >> 1;
return binary_search(array, key, begin, end, mid);
}
private static int binary_search(int[] array, int key, int begin, int end, int mid) {
// 查找失败返回-1
if (begin > end) {
return -1;
}
if (array[mid] == key) {
return mid;
} else if (array[mid] > key) {
end = mid - 1;
mid = (begin + end) >> 1;
return binary_search(array, key, begin, end, mid);
} else if (array[mid] < key) {
begin = mid + 1;
mid = (begin + end) >> 1;
return binary_search(array, key, begin, end, mid);
}
// 查找失败返回-1
return -1;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] testarray = { 1, 2, 3, 4, 5, 8000 };
System.out.println(binarysearch(testarray, 5));
System.out.println(binarysearch(testarray, 8100));
System.out.println(binarysearch(testarray, 8000));
System.out.println(binarysearch(testarray, -1));
}
}
判断一个数是偶数还是奇数
交互递归
到目前为止,看到的递归函数都是直接调用自己。虽然大多数的递归函数都符合这一形式,但其实递归的定义更为广泛,如果某个函数被细分成了几个子函数,那么可以在更深的嵌套层次上应用递归调用。例如:如果函数 f 调用函数 g ,而函数 g 反过来又调用函数 f ,这些函数的调用仍然被看作是递归。这种类型的递归被成为交互递归
下面通过判断一个数是偶数还是奇数来展示交互递归的应用,并且此题突出了递归跳跃的信任的重要性
首先,先看奇数和偶数的描述:
- 如果一个数的前一个数是奇数,那么该数是偶数
- 一个树不是偶数就是奇数
- 定义0是偶数
递归跳跃的信任
从代码可以看出,代码的实现是完全基于上面奇数和偶数的描述的三点。初看,这是多么的不可思议。如果想要探索其底层是如何实现的,也只需用一个较少的数字代入,跟踪调用验证就OK
如单纯地从表面看,单凭 “定义0是偶数” 这个简单情景真的没法看出这递归竟然能正确工作。所以,对于没法一下子就能看出的这种情况,我们需要的就是递归跳跃的信任,只要我们递归分解正确和简单情景分析正确,实现细节就不必去担心,交给计算机。也因此,只要掌握了递归的思维,解决一个问题是多么简单和快捷。
public class Odd_OR_Even {
public static boolean isodd(int n) {
return !(iseven(n));
}
public static boolean iseven(int n) {
if (n == 0) {
return true;
} else {
return isodd(n - 1);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(isodd(5));
System.out.println(iseven(5));
}
}
POJ1664 放苹果
题目:
放苹果
Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 16055 Accepted: 10133
Description
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
Input
第一行是测试数据的数目t(0 <= t <= 20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。
Output
对输入的每组数据M和N,用一行输出相应的K。
Sample Input
1
7 3
Sample Output
8
Source
lwx@POJ
- n = 1,盘子剩下一个,即只有一种放法;
- m < 0,即存在空盘子。所以这种情况包含了在 n 不断减少的情况中;
- m = 0,即苹果已经全放完,没有多余。所以这属于一种放法;
public class PutApple {
public static int Apple_Put(int m, int n) {
// 当苹果已全部放满时
if (m == 0)
return 1;
// 当盘子剩下一个时
if (n == 1)
return 1;
// 当m<0时候的放法包含在fun(m,n-1)中
if (m < 0)
return 0;
return Apple_Put(m - n, n) + Apple_Put(m, n - 1);
}
/**
* @param args
*/
public static void main(String[] args) {
));
System.out.println(Apple_Put(2, 2));
System.out.println(Apple_Put(7, 3));
}
}
解题思路:这题的思想是找递归关系,我们不妨令f(m,n)表示m个苹果放到n个盘子里有多少种放法,下面对不同的情况给予讨论:
(1):当盘子数为1的时候,只有一种放法就是把所有苹果放到一个盘子里。
(2):当苹果数为1的时候,也只有一种放法,注意题目中说明,盘子之间并无顺序,所以不管这个苹果放在哪个盘子里,结果都算一个。
(3):当m<n时,因为此时最多只能放到m个盘子中去(一个里放一个),实际上就相当于把m个苹果放到m个盘子里一样,也就是f(m,m);
(4):当m==n时,此时分两种情况讨论,一种是一个盘子里放一个,只是一种,第二种是,至少有一个盘子里不放苹果这就相当于是f(m,m-1);
(5):当m>n时,也分两种情况讨论,一种是至少有一个盘子里不放苹果,这样子就相当于f(m,n-1),第二种是,先取出n个苹果一个盘子里放一个,再将剩下的m-n个苹果放到n个盘子里去,即f(m-n,n);
综上所述:
得到递归表达式:
f(m,n)=1 当 m=1或n=1;
f(m,n)=f(m,m) 当m<n;
f(m,n)=1+f(m,m-1) 当m=n;
f(m,n)=f(m-n,n)+f(m,n-1);
在递归的过程中采用记忆化搜索可以减少不必要的时间,算过的东西就不要再算了。
public class PutApple {
private static int f[][] = new int[11][11];
public static int ApplePut(int m, int n) {
if (f[m][n] != 0) {
return f[m][n];
} else if (m == 1 || n == 1) {
f[m][n] = 1;
return f[m][n];
} else if (m < n) {
f[m][n] = ApplePut(m, m);
return f[m][n];
} else if (m == n) {
f[m][n] = 1 + ApplePut(m, m - 1);
return f[m][n];
} else {
f[m][n] = ApplePut(m - n, n) + ApplePut(m, n - 1);
return f[m][n];
}
}
public static void main(String[] args) {
System.out.println(ApplePut(1, 5));
System.out.println(ApplePut(5, 1));
System.out.println(ApplePut(3, 3));
System.out.println(ApplePut(7, 3));
}
}