以下是一个简单循环改造成递归的例子:
1 /** 2 * 采用循环的方式打印0~n 3 */ 4 private static void print1(int n) { 5 6 for (int i = 0; i <= n; i++) 7 System.out.print(i + "\t"); 8 } 9 10 /** 11 * 打印从0到n【第一种递归方式】 12 */ 13 private static void print2(int n) { 14 15 if (n > 0){ 16 print2(n - 1); // 打印0~n-1 17 } 18 System.out.print(n + "\t"); // 打印n 19 } 20 21 /** 22 * 从begin打印到end【第二种递归方式】 23 */ 24 private static void print3(int start, int end) { 25 26 if (start > end) 27 return; 28 System.out.print(start + "\t"); // 打印start 29 print3(start + 1, end); // 打印start+1~end 30 }
在第一种递归方式中,我们实际上应用到了“分治算法”,将问题分解为两部分:①打印0~n-1(递归处理);②打印n(可以直接处理),先是处理的n;思考:是否可以先处理前面的内容?如果递归方法的参数仍然只有一个参数:我们首先应该处理打印0,由于没有一个相似的结构,我们无法进行下一次的递归操作——我们可以给打印方法增加一个参数(让它从start打印到end),那么该问题就可以分解为:①打印start(直接处理);②打印start+1~end(递归处理)。
我们可以在满足条件的时候递归(以上代码的第一种递归方式),或者在不满足条件的时候使用return语句(以上代码的第二种递归方式)。
如果没有相似性,需要主动构造;不能相似的原因很可能是缺少参数;递归和数学上的递推公式很相似。
以下是一个新的循环改造成递归的例子(实现对数组元素的求和)。
1 /** 2 * 利用循环取得数组元素的累加和 3 */ 4 public static int addAll1(int[] arr){ 5 6 int sum = 0; 7 for (int i = 0; i < arr.length; i++) 8 sum += arr[i]; 9 return sum; 10 } 11 12 /** 13 *求arr数组中从第start项到数组最后的元素和 14 */ 15 public static int addAll2(int[] arr,int start){ 16 17 if (start==arr.length) // 递归出口,刚刚越界的时候返回0 18 return 0; 19 return arr[start] + addAll2(arr, start+1); 20 } 21 22 public static int addAll3(int[] arr,int end){ 23 24 if (end==-1) 25 return 0; 26 return addAll3(arr, end-1) + arr[end]; 27 } 28 29 /** 30 * 将问题划分两个子问题,整个数组求和等于前面的一半元素额和加上后面一半元素的和 31 */ 32 public static int addAll4(int[] arr,int start,int end){ 33 34 int mid = (start + end) / 2; 35 if (start == end) { 36 return arr[start]; // 递归出口,此时的start=end 37 } 38 return addAll4(arr, start, mid) + addAll4(arr, mid + 1, end); 39 }
实例三:不调用javaAPI的equals()方法比较两个字符串是否相等。
1 public static boolean isSameString(String str1,String str2){ 2 3 if (str1.length()!=str2.length()) 4 return false; 5 if (str1.length()==0) // 这里是需要判断str1,因为如果str1和str2的长度不相等在前面就已经返回了 6 return true; // 这个是递归出口,空字符串 7 return str1.charAt(0)==str2.charAt(0) && isSameString(str1.substring(1), str2.substring(1)); 8 }
1.先比较两个字符串的长度,长度不相等直接返回false,否则跳到步骤二;
2.比较两个字符串的首字符是否相等,如果不相等返回false,否则执行步骤三;
3.递归比较比较两个新的字符串(重复步骤1~3)是否相等(新字符串是原来的字符串去掉首字符)。
递归出口:两个字符串为空串时返回true。
问题一:在n个球中,任意取出m个(不放回),有多少种不同的取法?
算法思想:分治。假设在n个球中有一个幸运球,我们要取出m个球有2种取法:①取出此幸运球,再从n-1个球中取出m-1个球;②不取出此幸运球(此幸运球被排除在外),我们需要从n-1个球中取出m个球。
1 /** 2 * 从n个球中取出m个球 3 * 算法思想:分治 4 * @param n 5 * @param m 6 * @return 取出方法的个数 7 */ 8 private static int getBall(int n,int m){ 9 10 /* 注意以下3句话的顺序 */ 11 if(n < m) return 0; // 从4个球中取出5个球(无解) 12 if(n == m) return 1; // 从3个球中取出3个球(1种) 13 if(m == 0) return 1; // 从一定个数的球中取出0个球只有1种方式(不取) 14 15 return getBall(n - 1, m - 1) + getBall(n - 1, m); 16 }
以上的这种算法我们叫做“平地起风雷”。
问题二:生成n个元素的全排列(例如abc的全排列有6种:abc、acb、bac、bca、cab、cba)。
利用减治的思想,我们可以看到每个元素都可以放到第一个位置,然后将剩下的2个元素进行全排列——原来规模为n的问题划分成了一个退化的子问题和一个规模为n-1的子问题!
1 /** 2 * 打印数组中元素的全排列 3 * @param data 4 * @param current 当前的交换位置(关注点),与其后的元素进行交换 5 */ 6 private static void printAllAssignment(char[] data, int current) { 7 8 if(current == data.length){ 9 for (int i = 0; i < data.length; i++) { 10 System.out.print(data[i] + " "); 11 } 12 System.out.println(); 13 } 14 15 for (int i = current; i < data.length; i++) { 16 // 试探(交换) 17 swap(data, current, i); 18 // 递归 19 printAllAssignment(data, current + 1); 20 // 回溯(换回来) 21 swap(data, current, i); 22 } 23 } 24 25 /** 26 * 交换数组中指定索引的元素 27 * @param data 28 * @param pos1 29 * @param pos2 30 */ 31 public static void swap(char[] data,int pos1,int pos2){ 32 33 char temp = data[pos1]; 34 data[pos1] = data[pos2]; 35 data[pos2] = temp; 36 }
以上算法中的回溯就是回复到初始状态。
问题三:求2个串的最长公共子序列(CLS)的长度。串“abcdef”的子序列定义为从串的左边向右扫描,任意取出的字符构成的串,例如abc、acd、c等,注意:子串的定义比子序列严格(子串必须要连着)。abc和xbacd的最长公共子序列就是bc。
上面问题的基本思路就是利用减治的思想,先取得2个字符串的头元素,如果头元素相同,则最长公共子序列的长度就是1+将两个字符串的截短1之后的最长公共子序列的长度。如果两个元素的头元素不相同,则最长公共子序列的解就是两个最长公共子序列解的最大值。
/** * 求2个字符串的最长公共子序列的长度 * @param str1 * @param str2 * @return */ public static int cls(String str1,String str2){ // 递归基 if (str1.length() == 0 || str2.length() == 0) { return 0; } // 头元素是否相同 if (str1.charAt(0) == str2.charAt(0)) { return cls(str1.substring(1), str2.substring(1)) + 1; } else { return Math.max(cls(str1, str2.substring(1)),cls(str1.substring(1), str2)); } }
问题四:求一个字符串的反转字符串。
/** * 求一个字符串的反转字符串 * @param str * @return */ public static String getReverseString(String str) { return str.length() < 1 ? str : getReverseString(str.substring(1)) + str.charAt(0); }
同样利用减治的思想我们可以将一反转之后的字符串想象成长度为n-1的反转字符串和字符串的第一个字符进行反转操作,递归的出口就是子串的长度小于1.
问题五:打印杨辉三角。
1 public static void main(String[] args) { 2 3 for(int i = 0;i <= 5;i++){ 4 5 printPascalTriangle(i); 6 System.out.println(); 7 } 8 } 9 10 /** 11 * 打印杨辉三角第level层的元素 12 * @param level 三角形层数 13 */ 14 public static void printPascalTriangle(int level){ 15 16 for(int i = 0;i <= level;i++){ 17 System.out.print(getElement(level,i) + " "); 18 } 19 20 } 21 22 /** 23 * 取得杨辉三角的第level层的第i个元素 24 * @param level 25 * @param i 26 * @return 27 */ 28 private static int getElement(int level, int i) { 29 30 if(i == 0 || i == level) return 1; 31 32 return getElement(level - 1, i) + getElement(level - 1, i - 1); 33 }
上面的if判断分别是该层的第一个元素和最后一个元素。
问题五:整数划分问题。例如:6 = 6 = 5+1 = 4+2 = 4+1+1 = 3+3 = 3 + 2 + 1 = 3+1+1+1 = …… = 1+1+1+1+1+1.
1 /** 2 * 打印整数的加法划分 3 * @param number 整数 4 * @param arr 缓存,临时存储 5 * @param pos 当前的位置(调用的时候传入0) 6 */ 7 public static void printIntegerDivide(int number,int[] arr,int pos){ 8 9 // 递归出口 10 if(number == 0){ 11 for (int i = 0; i < pos; i++) { 12 System.out.print(arr[i] + " "); 13 } 14 System.out.println(); 15 return; 16 } 17 18 for(int i = number ; i > 0;i--){ 19 if(pos > 0 && i > arr[pos -1]) continue; // 不允许出现后一项比前一项大的情况 20 arr[pos] = i; 21 printIntegerDivide(number-i, arr, pos + 1); 22 } 23 }