递归

递归是一种针对使用简单的循环难以编程实现的问題,提供优雅解决方案的技术。

阶乘

许多数学函数都是使用递归来定义的,比如整数的阶乘可以如下定义

\[n! = 1 \quad \text{if n is 0 else}\quad n * (n-1)! \]

假定计算n!的方法是factorial(n)。如果用n=0调用这个方法,立即就能返回它的结 果。这个方法知道如何处理最简单的情况,这种最简单的情况称为基础情况(basecase)或 终止条件(stoppingcondition)。

如果用n>0调用这个方法,就应该把这个问题简化为计算 n-1的阶乘的子问题。子问題在实质上和原始问题是一样的,但是它比原始问题更简单也更小。因为子问题和原始问题具有相同的性质,所以可以用不同的参数调用这个方法 ,这称作递归调用(recursivecall)。

如果递归不能使问題简化并最终收敛到基础情况,就有可能出现无限递归,并且会导致一个 StackOverflowError。

所有递归方法的基本特点可总结如下:

  • 方法使用if-else或switch语句来引导不同的情况。
  • 一个或多个基础情况(最简单的情况)用来停止递归。
  • 每次递归调用都会简化原始问题,让它不断地接近基础情况,直到它变成这种基础情况为止。

许多平凡的场景,也有递归的身影。使用递归进行思考非常有趣,比如喝咖啡,可以用如下的递归方式描述:

public static void drinkCoffee(Cup cup) { 
  if (!cup.isEmptyO) {
    cup.takeOneSip(); // Take one sip
		drinkCoffee(cup); }
}

假设 cup 是描述一杯咖啡的实例对象,具有 isEmpty 和 takeOneSip方法。可以将 问题转换为两个子问题:一个是喝一小口咖啡 ,另外一个是喝杯中剩下的咖啡。第二个问题和原问题是一样的,只是规模上更小。而问題的基础情形是杯子空了。

考虑打印一条消息 n 次的简单问题。可以将这个问题分解为两个子问题:一个是打印消 息一次,另一个是打印消息 n-1 次。第二个问题与原始问题是一样的,只是规模小一些。这个问题的基础情况是 n==0。

public static void nPrintln(String message, int times) { 
  if (times >= 1) {
		System.out.println(message);
		nPrintln(message, times -1); }
}

如果以递归的思路进行思考(think recursively), 那么许多编程问题都可以转化为递归来解决。

回文字符串问题

检査一个字符串是否是回文串的问题可以分解为两个子问题:

  • 检査字符串中的第一个字符和最后一个字符是否相等。
  • 忽略两端的字符之后检査子串的其余部分是否是回文串。

第二个子问题与原始问题是一样的,但是规模小一些。基本状态有两个:1)两端的字 符不同;2 ) 字符串大小是 0 或 1。在第一种情况下,字符串不是回文串;而在第二种情况下, 字符串是回文串。

public static boolean isPalindrome(String s) {
  if (s.length() <= 1) // base case
    return true
  else if (s.charAt(0) != s.charAt(s.length()-1)) // base case
    return false
  else
    return isPalindrome(s.substring(1, s.length()-1))
}

isPalindrome 方法要为每次递归调用创建一个新字符串,因此它不够高效。为避免创建新字符串,可以使用 low 和 high 下标来表明子串的范围。这两个 下标必须传递给递归方法。

public class RecursivePalindrome {
  public static boolean isPalindrome(String s) {
    return isPalindrome(s, 0, s.length()-1)
  }
  
  public static boolean isPalindrome(String s, int beg, int end) {
  if (end <= beg) // base case
    return true
  else if (s.charAt(beg) != s.charAt(end) // base case
    return false
  else
    return isPalindrome(s, beg+1, end-1)
}
}

递归程序设计中定义第二个方法来接收附加的参数是一个常用的设计技巧,这样的方法称为递归辅助方法(recursive helper method)。

辅助方法在设计关于字符串和数组问题的递归方案上是非常有用的。比如下面两个例子。

递归选择排序

选择排序法是先找到列表的最小数,并和 第一个元素交换。然后,在剩余的数中找到最小数,再将它和剩余列表中的第一个元素交换, 这样的过程一直进行下去,直到列表中仅剩一个数为止。这个问题可以分解为两个子问题:

  • 找出列表中的最小数,然后将它与第一个数进行交换。
  • 忽略第一个数,对余下的较小一些的列表进行递归排序。
public class RecursiveSelectionSort {
  public static void sort(double[] list) {
    sourt(list, 0, list.length() - 1);
  }
  
  private static void sort(double[] list, int low, int high) {
    if (low >= high) // base case
      return;
    // find the smallest number and its index
    int idxMin = low;
    double min = list[low];
    for (int i = low + 1; i <= high; i++) {
      if (list[i] < min) {
        min = list[i];
        idxMin = i;
      }
    }
    // swap element
    list[idxMin] = list[low];
    list[low] = min;
    
    // sort the remaining list
    sort(list, low+1, high);
  }
}

递归二分査找

二分査找的前提条件是数组元素必须已经排好序。 二分査找法首先将关键字与数组的中间元素进行比较,考虑下面三种情况。

  • 情况 1: 如果关键字比中间元素小,那么只需在前一半数组元素中进行递归査找。
  • 情况 2: 如果关键字和中间元素相等,则匹配成功,査找结束。
  • 情况 3: 如果关键字比中间元素大,那么只需在后一半数组元素中进行递归査找。

情况 1 和情况 3 都将査找范围降为一个更小的数列。而当匹配成功时,情况 2 就是一个基础情况。另一个基础情况是査找完毕而没有一个成功的匹配。

public class RecursiveBinarySearch {
  public static int recursiveBinarySearch(int[] list, int key) 
    return recursiveBinarySearch(list, key, 0, list.length()-1);
  private static int recursiveBinarySearch(int[] list, int key, int low, int high) {
    if (low > high)// basic case:list exhausted without a match
      return -low -1
    
    int mid = (low + high) / 2;
    if (key == list[mid])
      return mid;
    else if (key < list[mid])
      return recursiveBinarySearch(list, key, low, mid-1);
    else
      return recursiveBinarySearch(list, key, mid+1, high);
  }
}

得到目录的大小

一个目录的大小是指该目录下所有文件大小之和。目录可能会包含子目

录。假设一个目录包含文件f1, f2, ...,fm以及子目录d1, d2, ...,dn。

File 类可以用来表示一个文件或一个目录,并且获取文件和目录的属性。File 类中的两个方法对这个问题很有用:length 方法返回一个文件的大小;listFiles 方法返回一个目录下的 File 对象构成的数组。

import java.io.File;
import java.util.Scanner;

public class DirectorSize {
  public static void main(String[] args) {
    System.out.print("Enter a directory or a file: ");
    Scanner input = new Scanner(System.in);
    String dir = input.nextLine();
    
    // Display the size
    System.out.println(getSize(new File(dir)) + "bytes.");
  }
  public static long getSize(File file) {
    long size = 0;  // store the total size of all files.
    if (file.isDirectory()) {
      File[] files = files.listFiles();  // all files and sub-directories
      for (int i = 0; files != null && i < files.length(); i++)
        size += getSize(files[i]);	// recursive call
    }
    else{
      size += file.length();	// base case
    }
    return size;
  }
}

汉诺塔问题

汉诺塔问題是一个经典的递归例子。用递归可以很容易地解决这个问題,但是,不使用递归則非常难解决。

这个问题本身就具有递归性质,可以得到直观的递归解法。

问题的基础情况是 n=1。若 n=1,就可以简单地把盘子从 A 移到 B。当 n>1 时,可以将 原始问题拆成下面三个子问题,然后依次解决。

1 ) 借助塔 B 将前 n-1 个盘子从 A 移到 C;

2 ) 将盘子 n 从 A 移到 B;

3 ) 借助塔 A 将 n-1 个盘子从 C 移到 B。

public static void moveDisks(int n, char fromTower, char toTower, char auxTower) {
  if (n==1)  // base case; stopping condition
    System.out.println("move disk "+ n +" from " +
fromTower + " to " + toTower);
  else {
    moveDisks(n-1, fromTower, auxTower, toTower);
    System.out.println("move disk "+ n +" from " +
fromTower + " to " + toTower);
    moveDisks(n-1, auxTower, toTower, fromTower);
  }
}

递归与循环

递归会产生相当大的系统开销。程序每调用一个方法,系统就要给方法中所有的局部变量和参数分配空间。这就要占用大量的内存,还需要额外的时间来管理内存。

任何用递归解决的问题都可以用非递归的迭代解决。但是在某些情况下,本质上有递归特性的问题很难用其他方法解决,而递归可以给出一个清晰, 简单的解决方案。像目录大小问题 、汉诺塔问题和分形问题的例子都是不使用递归就很难解决的问题。

应该根据要解决的问题的本质和我们对这个问题的理解来决定是用递归还是用迭代。根据经验,选择使用递归还是迭代的原则,就是看它能否给出一个反映问题本质的直观解法。如果迭代的解决方案是显而易见的,那就使用迭代。迭代通常比递归效率更高。

尾递归

如果在从递归调用返回时没有继续的操作要完成,那么这个递归方法就称为尾递归(tail recursive)。

尾递归更可取: 因为当最后一个递归调用结束时,方法也结束,因此,无须将中间的调用存储在栈中。编译器可以优化尾递归以减小栈空间。

通常,可以使用辅助参数将非尾递归方法转换为尾递归方法。这些参数被用于保存结果。思路是将后续的操作以一种方式结合到辅助参数中,这样递归调用中将不再有后续操作。可以定义一个带辅助参数的新的辅助递归方法,这个方法可以重载原始方法,具有相同的名字而签名不同。

public class ComputeFactorialTailRecursion {
  public static long factorial(int n) {
    return factorial(n, 1);		// call auxiliary method.
  }
  private static long factorial(int n, int res) {
    return (n==0)? res: factorial(n-1, n*result);
  }
}
public class ComputeFibTailRecursion {
    /** Return the Fibonacci number for the specified index */
  public static long fib(long index) {
    return fib(index, 1, 0);
  }

  /** Auxiliary tail-recursive method for fib */
  private static int fib(long index, int next, int result) {
    if (index == 0) 
      return result;
    else
      return fib(index - 1, next + result, next);
  }
}

其他适合用递归解决的问题:

字符串全排列

public class Permutation {
  public static void displayPermutation(String s) {
    displayPermutation("", s);
  }
  
  public static void displayPermutation(String s1, String s2) {
    if (s2.length() > 0) {
      for (int i = 0; i < s2.length(); i++) {
        display(s1 + s2.charAt(i), s2.substring(0, i) + s2.substring(i+1));
      }
    }
    else
      System.out.println(s1);
  }
}

国际象棋骑士周游

import java.util.Scanner;

public class KnightTravel {
    private static int X = 8;
    private static int Y = 8;
    private static int[] chess = new int[X*Y];
    private static int[][] dirs = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-2, -1}, {-1, -2}, {-2, 1}, {-1, 2}};

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter initial location of x: ");
        int x = input.nextInt();
        System.out.print("Enter initial location of y: ");
        int y = input.nextInt();
        chess[x*X +y] = 1;
        boolean found = traverse(x, y, 2);
        if (found) {
            displayChess();
        } else {
            System.out.println("solve faild.");
        }
    }

    private static boolean passable(int x, int y) {
        if (x >= 0 && x < X && y >= 0 && y < Y && chess[x*X + y]==0) {
            return true;
        }
        return false;
    }

    public static boolean traverse(int x, int y, int step) {
        if (step>X*Y)
            return true;
        boolean result;
        for (int i = 0; i < dirs.length; i++) {
            int nextx = x + dirs[i][0];
            int nexty = y + dirs[i][1];
            if (passable(nextx, nexty)) {
                chess[nextx*X + nexty] = step;
                result = traverse(nextx, nexty, step+1);
                if (result){
                    return true;
                }
                else{
                    chess[nextx*X + nexty] = 0;
                }
            }
        }
        return false;
    }
    private static void displayChess() {
        for (int i=0; i < X; i++)
        {
            for (int j=0; j < Y; j++) {
                System.out.printf("%d\t", chess[i*X + j]);
            }
            System.out.println();
        }
    }
}
posted @ 2021-05-30 11:56  geeks_reign  阅读(120)  评论(0编辑  收藏  举报