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 + "\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.

问题五:打印杨辉三角。

Java算法

 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 }

 

你可能感兴趣的:(java算法)