递归
递归是一种争对使用简单的循环难以编程实现的问题,提供优雅解决方案的技术。
使用递归就是使用递归方法(recursive method)编程,递归方法就是直接或者间接调用自身的方法。递归是一个很有用的程序设计技术。在某些情况下,对于用其它方法很难解决的问题,使用递归就可以给出一个直观、直接的简单解法。例如需要遍历某个路径下的所有文件,但这个路径下文件夹的深度是未知的,那么就可以使用递归来实现这个需求。再如编写程序显示H树。
示例:计算阶乘
数字n的阶乘可以定义如下:
0! = 1;
n! = n * (n - 1)!;
对给定的n如何求n!呢?由于已经知道0!=1,而1!=1 * 0!,因此很容易求得1!。假设已经知道(n-1)!,使用n! = n * (n - 1)!就可以得到n!。这样,计算n!的问题就简化为计算(n - 1)!.当计算(n -1)!时,可以递归地应用这个思路直到n递减为0.
假定计算n!的方法是factorial(n)。如果用n=0调用这个方法,立即就能返回它的结果。这个方法知道如何处理最简单的情况,这种最简单的情况称为基础情况(base case)或终止条件(stopping condition)。如果用n>0调用这个方法,就把这个问题简化为n-1的阶乘的子问题。子问题在实质上和原始问题是一样的,但它比原始问题更简单也更小。因为子问题和原始问题具有相同的性质,所以可以用不同的参数调用这个方法,这称着递归调用(recursive call)。
计算factorial(n)的递归算法可以简单地描述如下:
if(n == 0) return 1; else return n * factorial(n - 1);
一个递归调用可以导致更多的递归调用,因为这个方法继续把每个子问题分解成新的子问题。要终止一个递归方法,问题最后必须达到一个终止条件。当问题达到这个终止条件时,就将结果返回给调用者。然后调用者进行计算并将结果返回给它的自己的调用者。这个过程持续进行,直到结果传给原始的调用者为止。现在,原始问题就可以将factorial(n - 1)的结果乘以n得到。
public class ComputeFactorial{ public static long factorial(int num) {//num! = num * (num - 1)! 0! = 1 if(num == 0)return 1; else return num * factorial(num - 1); } }
现在,你看到了如何用java编写一个递归方法,那么递归是如何工作的呢?下图展示一个递归调用的执行过程,从n=4开始。
注意:使用循环来实现factorial方法是比较简单且更加高效的。这里只是使用递归factorial方法演示递归的概念。比如我们前面所说的罗列出一个文件夹下的所有文件如果不使用递归很难解决。
如果递归不能使问题简化并最终收敛到基础情况,就有可能出现无限递归。例如假设将factorial方法错误的写成 public static long factorial(int i){return n * factorial(n - 1);}
那么这个方法会无限执行下去,最终导致一个StackOverflowError。针对递归调用的栈空间使用下画说明。
示例:计算斐波拉契数(前文练习的小兔子繁殖数量的增长模型)
数列:0 1 1 2 3 5 8 13 21 34 55 89 ...
下标:0 1 2 3 4 5 6 7 8 9 10 11
斐波拉契数列从0和1开始,之后的每个数都是序列的前两个数的和。数列可以递归定义为:
fib(0) = 0;
fib(1) = 1;
fib(index) = fib(index - 2) + fib(index - 1); index >= 2;
对给定的index,怎样求fib(index)呢?因为已知fib(0)和fib(1),所以很容易求得fib(2),这样计算fib(index)的问题简化为计算fib(index - 2) 和fib(index - 1)的问题。递归的应用这个思路直到index递减为0或1.基础情况是index = 0或index = 1.若用index = 0或index = 1调用这个方法,它会立即返回结果。若用index >=2调用这个方法,则通过递归调用把问题分解成计算fib(index-2)和fib(index-1)两个子问题。
public static int fib(int index) {
if(index == 0) return 0;
else if(index == 1) return 1;
else return fib(index - 1) + fib(index - 2);
}
使用递归解决问题
前面两个经典的递归例子。所有的递归方法都具有以下特点:
- 这些方法使用if-else或switch语句来引导不同的情况。
- 一个或多个基础情况(最简单的情况)用来终止递归。
- 每次递归调用都会简化原始问题,让它不断地接近基础情况,直到它会变成这种基础情况为止。
通常,要使用递归解决问题,就要将这个问题分解为子问题。每个子问题几乎与原始问题是一样的,只是规模小一些。可以应用相同的方法来递归解决子问题。
1.用递归的思想考虑打印一条消息n次的简单问题。可以将这个问题分解成两个子问题:一个打印消息一次,另一个打印消息n-1次。第二个问题与原始问题是一样的,只是规模小一些。这个问题的基础情况是n == 0.可以使用递归来解决这个问题:
public static void nPrintln(String message,int times) { if(times >= 1) { System.out.println(message); nPrintln(message, times - 1); } }
2.用递归检查一个字符串是否是回文串的问题可以分解为两个子问题:检查字符串第一个字符和最后一个字符是否相等;忽略两端的字符之后检查子串的其余部分是否是回文串:
public class RecursivePalindrome { /** * 判断一个字符串是否为回文串 * @param s 字符串 * @return true:回文串;false:不是回文串 */ public static boolean isPalindrome(String s) { return isPalindrome(s,0,s.length() - 1); } /** * 递归辅助方法 * @param s 要判断的字符串 * @param low 起始字符索引 * @param high 结尾字符索引 * @return 首字符和尾字符是否相等 */ private static boolean isPalindrome(String s, int low, int high) { if(high <= low) return true; else if(s.charAt(low) != s.charAt(high)) return false; else return isPalindrome(s,low + 1,high - 1); } public static void main(String[] args) { System.out.println("moom :" + isPalindrome("moom")); System.out.println("did :" + isPalindrome("did")); System.out.println("hello :" + isPalindrome("hello")); } }
3.递归选择排序
package edu.uestc.avatar; import java.util.Arrays; /** * 递归选择排序 * 1、找出列表中的最小元素,然后将它和第一个元素进行交换 * 2、忽略第一个元素,对余下的较小的一些列表进行递归排序 * 基础条件:列表只包含一个元素 * */ public class RecursiveSelectionSort { public static void sort(int[] list) { sort(list,0,list.length - 1); } public static void sort(int[] list,int low,int high) { if(low < high) { int minIndex = low; int min = list[low]; for(int i = low + 1; i <= high; i++) { if(list[i] < min) { minIndex = i; min = list[i]; } } list[minIndex] = list[low]; list[low] = min; sort(list,low + 1,high); } } public static void main(String[] args) { int[] list = new int[10]; for(int i = 0; i < 10; i++) list[i] = (int)(Math.random() * 100); System.out.println("排序前:" + Arrays.toString(list)); sort(list); System.out.println("排序后:" + Arrays.toString(list)); } }
4.递归二分查找
package edu.uestc.avatar; /** * 二分查找 * 前提:list是一个有序数组 * 二分查找首先将关键字key和数组的中间元素进行比较: * 1、如果关键字比中间元素小,那么只需要在前一半数组中进行递归查找 * 2、如果关键字比中间元素大,那么只需要在后一半数组中进行递归查找 * 3、如果关键字比中间元素相等,匹配成功,查找结束 * 情况3为一个基础条件,另一个基础条件为查找完毕没有一个成功的匹配 * */ public class RecursiveBinarySearch { public static int recusiveBinarySearch(int[] list, int key) { int low = 0; int high = list.length - 1; return recusiveBinarySearch(list,key,low,high); } /** * 递归辅助方法 * @param list 有序列表 * @param key 关键字 * @param low 低位 * @param high 高位 * @return 关键字在数组中的索引,如果没有找到,返回-low - 1:表示改关键字应该在什么位置 */ private static int recusiveBinarySearch(int[] list, int key, int low, int high) { if(low > high) //基础条件(查找完毕没有一个成功的匹配) return -low - 1; int mid = (low + high) / 2; if(key < list[mid]) return recusiveBinarySearch(list,key,low,high - 1); else if(key > list[mid]) return recusiveBinarySearch(list,key,mid + 1,high); else return mid; //基础条件(关键字和中间元素相等,匹配成功,查找结束) } }
示例:打印目录下所有的文件名(对于具有递归结构的问题,采用递归方法求解更高效)
import java.io.File; /** * 罗列出指定文件夹下所有的文件名称 * */ public class DirectoryName { public static void printFileName(File file) { //递归终止条件 if(file.isFile()) System.out.println(file.getName()); else { File[] files = file.listFiles();//得到改目录下的所有文件 for(int i = 0; files != null && i < files.length; i++) { printFileName(files[i]); } } } public static void main(String[] args) { printFileName(new File("D:/")); } }
示例:汉诺塔
汉诺塔是一个经典的递归例子。用递归可以很容易地解决这个问题,但是,不使用递归则非常难解决:
- n个盘子标记为1,2,3,……,n,而三个塔标记为A、B和C。
- 任何时候盘子都不能放在比它小的盘子的上方。
- 初始状态时,所有的盘子都放在塔A上。
- 每次只能移动一个盘子,并且这个盘子必须在塔顶的位置。
这个问题的目标是借助塔C把所有的盘子从塔A移到塔B。例如,如果有三个盘子,将所有的盘子从A移到B的步骤如下图所示:
该问题的基础情况是n=1。若n=1,就可以简单的把盘子从A移到B。当n>1时,可以将原始问题拆分成下面三个子问题,然后依次解决:
1.借助塔B将前n-1个盘子从A移到C(下图步骤1)
2.将盘子n从A移到B(下图步骤2)
3.借助塔A将n-1个盘子从C移到B(下图步骤3)
public class Hanoi { public static void moveDisk(int n,char from,char to,char aux) { if(n == 1) { System.out.println("移动盘子 " + n + " 从 " + from + " 到 " + to); }else { moveDisk(n - 1, from, aux, to); System.out.println("移动盘子 " + n + " 从 " + from + " 到 " + to); moveDisk(n - 1,aux, to, from); } } public static void main(String[] args) { moveDisk(4, 'A', 'B', 'C'); } }
递归与迭代
递归会产生相当大的系统开销。程序每调用一个方法,系统就要给方法中所有的局部变量和参数分配空间。这就要占用大量的内存,还需要额外的时间来管理内存。
任何用递归解决的问题都可以用非递归的迭代解决。递归有很多副作用:耗费太多时间并占用了太多内存。那么为什么还要用它呢?因为在某些情况下,本质上有递归特性的问题很难用其它方法解决,而递归可以给出一个清晰、简单的解决方案。如目录大小,汉诺塔等问题都是不使用递归就很难解决的问题。
练习:1.字符串反转
2.打印xml节点下所有子节点名称
3.计算指定目录下所有文件大小总和