Java算法

一、递归与循环

  • 理论上任何的循环都可以改写为递归的形式。
    • 有时候因为栈的限制,需要“尾递归”(C可以用goto语句模拟尾递归);
    • java不支持尾递归。
  • 有些语言没有循环语句,只能使用递归(LISP)。

循环改递归的关键

  • 发现循环的逻辑相似性。
  • 不要忘记递归“出口”。

以下是一个简单循环改造成递归的例子:

 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 + "	");
 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 + "	"); // 打印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 + "	");    // 打印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 }
原文地址:https://www.cnblogs.com/happyfans/p/4441175.html