一个函数调用了它自己,称为递归。递归和循环可以互相替代。一种程序设计语言,支持 递归就可以不需要支持循环,支持循环就可以不需要支持递归。例如,早期的Lisp语言,就不 支持循环,只支持递归。当然,为了方便使用,程序设计语言一般都既支持递归,也支持循环。 从替代循环的角度看,递归和循环一样是一种手段,可以用来解决任何问题。但是递归更 多的是一种解决问题的思想———从这个角度看,也可以称递归是一种算法。 本章主要通过具体例题,分为以下三种情况讲述递归的用途。 (1)替代多重循环来进行枚举。 (2)解决用递归形式定义的问题。 (3)将问题分解为规模更小的子问题进行求解。 上述三个方面其实没有也不需要有严格的区分界限。有的问题,归类为上述不止一种情 况都说得过去。例如,5.2节的例题“绘制雪花曲线”,归类为第(2)或第(3)种情况都可以。 第(3)种情况,如果分解出来的子问题不止一个且相互无重叠,一般来说就可以算是“分治”。 还有一种递归可以称为“间接递归”,例如,函数A 调用了函数B,函数B 调用了函数C, 函数C 又调用了函数A ,就可以说函数A 、B、C 都间接递归调用了自身。本章不涉及这类 递归。 5.1 用递归进行枚举 5.1.1 案例: N 皇后问题(P0230) 将N 个皇后摆放在一个N 行N 列的国际象棋棋盘上,要求任何两个皇后不能互相攻击 (两个皇后在同一行,或同一列,或某个正方形的对角线上,就会互相攻击,称为冲突)。输入皇 后数N (1≤N ≤9),输出所有的摆法。无解输出“NOANSWER”。行列号都从0开始算。 输入:一个整数N ,表示要把N 个皇后摆放在一个N 行N 列的国际象棋棋盘上。 输出:所有的摆放放案。每个方案一行,依次是第0行皇后位置、第1行皇后位置、……、 第N -1行皇后位置。多种方案输出顺序如下:优先输出第0行皇后列号小的方案。如果两 个方案第0行皇后列号一致,那么优先输出第1行皇后列号小的方案,以此类推。 60 样例输入 4 样例输出 1 3 0 2 2 0 3 1 解题思路:在皇后数目不确定的情况下,用循环解决比较麻烦,因此可以采用递归的方式 进行枚举,程序如下。 //prg0350.java 1. import java.util.*; 2. class NQueensProblem { 3. int N; 4. int [] result; //result[i]是第i 行皇后摆放位置 5. NQueensProblem(int n_) { 6. N = n_; result = new int[N]; 7. } 8. boolean isOk(int n,int pos) { //判断第n 行的皇后放在第pos 列是否可行 9. //此时第0 行到第n-1 行的皇后的摆放位置已经存放在result[0]至result[n-1]中 10. for (int i = 0;i < n; ++i) 11. //检查位置pos 是否会和前0~n-1 行已经摆好的皇后冲突 12. if (result[i] == pos || 13. Math.abs(i-n) == Math.abs(result[i] - pos)) 14. return false; 15. return true; 16. } 17. boolean queen(int i) { 18. //解决N 皇后问题,现在第0 行到第i-1 行的i 个皇后已经摆放好了 19. //要摆放第i 行的皇后。返回值表示这种情况下最终能否成功 20. if (i == N) { //已经摆好了N 个皇后,说明问题已经解决,输出结果即可 21. for (int k=0;k 1 && result.charAt(0) == '0') 16. return -1; 17. return Integer.parseInt(result); 18. } 19. private boolean done(int i) { //被调用时,a[0,i-1]已存放了i 个字母代表的数 20. //返回值表示在此情况下最终能否找到解 21. if (i == 5) { 22. int n1 = toInt(s1),n2 = toInt(s2), 23. n3 = toInt(s3); 24. if (n1 >= 0 && n2 >= 0 && n3 >= 0 25. && n1 + n2 == n3) { 26. System.out.printf("%d+%d=%d\n",n1,n2,n3); 27. return true; 28. } 29. } 30. else { //i!= 5 31. for(int k=0;k<10;++k) { 32. int j = 0; 33. for(;j1时,n 的阶乘等于n 乘以(n-1)的阶乘。 第(2)句话,定义“阶乘”这个概念的时候,用到了“阶乘”这个词,看上去像循环定义,让人 无法理解。但是,由于有语句(1)的存在,上面这两句“自然数n 的阶乘”的定义,就是严密且 可以理解的。例如,若问“3的阶乘是什么”,要先回答“2的阶乘是什么”;要回答“2的阶乘是 65 什么”,就要回答“1的阶乘是什么”。按照语句(1),1的阶乘是1,因此往回倒推就可以知道3 的阶乘是什么了。 可以说,在上面的两句话的阶乘定义中,语句(2)的形式是递归的,而语句(1)就是递归的 终止条件。 按照这种定义方式,求自然数n 的阶乘的函数可以写成: int factorial(int n) { if (n == 1) return 1; return n * factorial(n-1); } 这是最简单的用递归函数解决递归形式的问题的例子。 在上面的程序中,判断“n==1”是否为真是基本操作,即进行次数最多的操作。设求n 阶乘的基本操作次数为T(n),则有: T(n)=1+T(n-1)=1+1+T(n-2)=1+1+1+T(n-3)=… =1+1+…+T(1)=1+1+…+1(n 个1) 所以函数的复杂度是O(n)。 5.2.1 案例: 波兰表达式(P0250) 波兰表达式是一种把运算符前置的算术表达式。例如,一般形式的表达式2+3的波兰 表示法为+23。波兰表达式的优点是计算时不需要考虑运算符的优先级,因此不必用括号 改变运算次序,例如,(2+3)*4的波兰表示式为* +234。本题求解波兰表达式的值,其 中运算符包括+、-、*、/4个。 输入:输入为一行,其中运算符和运算数之间都用空格分隔,运算数是浮点数。 输出:输出为一行,表达式的值。 样例输入 * + 11.0 12.0 + 24.0 35.0 样例输出 1357.000000 虽然什么是“波兰表达式”不难理解,但是题目其实并没有给出“波兰表达式”的准确定义。 波兰表达式可以用递归的形式准确定义如下。 (1)一个数是一个波兰表达式,其值就是该数本身。 (2)若一个波兰表达式不是一个数,则其形式为:“运算符波兰表达式1波兰表达式2”, 其值是以“波兰表达式1”的值作为第一操作数,以“波兰表达式2”的值作为第二操作数,进行 “运算符”所代表的运算后的值。“运算符”有加减乘除4种,分别表示为“+”“-”“*”“/”。 根据上述定义,可以写出解题程序如下。 //prg0370.java 1. import java.util.*; 2. class PolishExpression { 3. int N; 4. String [] exp; //exp[i]要么是一个数的字符串形式如"11.0",要么是一个运算符 5. double polish() { //从exp[N]处开始取出若干元素构成一个波兰表达式并返回其值 66 6. int M = N; 7. ++ N; 8. if (exp[M].equals("+")) 9. return polish() + polish(); 10. else if (exp[M].equals("-")) 11. return polish() - polish(); 12. else if (exp[M].equals( "*")) 13. return polish() * polish(); 14. else if (exp[M].equals( "/")) 15. return polish() / polish(); 16. else 17. return Double.parseDouble(exp[M]); 18. } 19. void solve() { 20. Scanner reader = new Scanner(System.in); 21. exp = reader.nextLine().split(" "); 22. N = 0; 23. System.out.printf("%.6f\n",polish()); 24. } 25. } 26. public class prg0370 { //请注意:在OpenJudge 提交时类名要改成Main 27. public static void main(String [] args) { 28. new PolishExpression().solve(); 29. } 30. } 第3行:N 相当于一个指针,表示polish函数被调用时,应该从字符串数组exp中下标为 N 的元素开始,取出若干个元素(假设x 个)构成一个波兰表达式,计算出其值并返回。而且 polish函数还必须让N 变为N +x,以便下一次调用polish函数时可以从正确的位置继续取 元素。例如,N ==0时调用polish(),一定是将exp中的所有元素都取出作为一个波兰表 达式。第 8、9行:按照波兰表达式定义,如果exp[M ]是“+”,则其后面一定跟着两个波兰表达 式。“+”取走后,N 的值被加1。然后调用一次polish()取出第一个波兰表达式并算出值,且 让N 推进到合适位置,再调用一次polish()取出第二个波兰表达式,算出值,和第一个波兰表 达式的值相加后返回。当然,第二次调用polish()的时候也会推进N 到合适位置。 第17行:按照波兰表达式定义,如果exp[M ]不是运算符,则其一定是一个数。那么取出 的波兰表达式就是exp[M ],其值为exp[M ]代表的那个数。 由于只需要从头到尾扫描整个表达式一遍,所以复杂度为O(n)。 ★★5.2.2 案例: 绘制雪花曲线 绘制雪花曲线,更是典型的以递归形式定义的问题。 要进行绘图,需要导入javax.swing包和java.awt包。前者中的JFrame类用于生成一个 窗口,后者中的Graphics类用于画图。Graphics对象可以看作窗口上的画板,调用Graphics 对象的drawLine()方法,可以绘制线段。 绘图是在一个窗口中进行的,创建了一个JFrame对象,就创建了一个窗口。JFrame对 象的setSize()方法可以设置窗口大小。窗口是一个平面直角坐标系,窗口的左上角是坐标系 原点,即其坐标是(0,0)。规定正东方向是0°,正南方向是90°,正西方向是180°,正北方向是 67 270°。当然也可以说正北方向是-90°,正西方向是-180°。 绘制雪花曲线须在窗口上进行。雪花曲线也称为科赫曲线,其递归定义如下。 (1)长为size,方向为x(单位:弧度)的0阶雪花曲线,是沿方向x 绘制的一根长为size 的线段。 (2)长为size,方向为x 的n 阶雪花曲线,由以下4部分依次拼接组成。 ① 长为size/3,方向为x 的n-1阶雪花曲线。 ② 长为size/3,方向为x-π/3的n-1阶雪花曲线。 ③ 长为size/3,方向为x+π/3的n-1阶雪花曲线。 ④ 长为size/3,方向为x 的n-1阶雪花曲线。 图5.1~图5.3是几个雪花曲线的示意图。 图5.1 0阶和1阶雪花曲线 图5.2 2阶雪花曲线 图5.3 3阶雪花曲线 绘制长度为600像素,方向为0的3阶雪花曲线的程序如下。 //prg0380.java 1. import javax.swing.*; //图形界面需要 2. import java.awt.*; //画图需要 3. class SnowCurveWindow extends JFrame { 4. public void drawSnowCurve(Graphics g, int n,int x,int y, 5. double size,double dir) { 6. //在g 上绘制一个n 阶长度为size,方向为dir(单位:弧度)的雪花曲线,起点坐标为(x,y) 7. if(n == 0) { //下面画一条0 阶雪花曲线,即一个线段 8. int x2 = (int)Math.round(x + Math.cos(dir)*size); 9. int y2 = (int)Math.round(y + Math.sin(dir)*size); 10. g.drawLine(x,y,x2,y2); //从起点(x,y)到终点(x2,y2)画一条线段 11. } 12. else { 13. double delta[] = {0,-Math.PI/3,Math.PI/3,0}; //PI 是π 14. size /= 3; 15. for(int i=0;i<4;++i) { 16. drawSnowCurve(g,n-1,x,y,size,dir+delta[i]); 68 17. x = (int) Math.round((x + Math.cos(dir+delta[i])*size)); 18. y = (int) Math.round((y + Math.sin(dir+delta[i])*size)); 19. } 20. } 21. } 22. public SnowCurveWindow(int n,int x,int y, 23. int size,double dir){ 24. setSize(800,600); //JFrame 的方法,设置窗体大小为800px*600px 25. setLocationRelativeTo(null); //让窗口在屏幕上居中 26. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 27. setContentPane(new JPanel() { 28. public void paint(Graphics g){ 29. drawSnowCurve(g,n,x,y,size,dir); 30. } 31. }); 32. setVisible(true); 33. } 34. } 35. public class prg0380 { 36. public static void main(String[] args) { 37. new SnowCurveWindow (3,100,400,600,0); 38. //绘制3 阶长度为600,方向为0 的雪花曲线,起点坐标为(100,400) 39. } 40. } 程序运行结果如图5.4所示。 图5.4 3阶0°雪花曲线 类SnowCurveWindow从JFrame派生而来,第37行创建一个SnowCurveWindow对象, 即创建了一个窗口。SnowCurveWindow 构造方法中调用的各种以“set”开头的方法,都是 JFrame类的方法。 第27行:本行new出来的对象,是一个匿名类的对象,该匿名类从JPanel类派生而来并 重写了paint()方法。JPanel对象可以看作一个面板,可以在上面绘图。setContentPane在窗 口上放置该匿名类对象,窗口显示和刷新时,会调用该匿名类对象的paint()方法并传入画布 对象Graphicsg。因此要将绘制雪花曲线的代码,即对drawSnowCurve函数的调用,写在 paint()方法中。 第13~19行:按照雪花曲线的递归定义,一条dir方向上的长为size的n 阶雪花曲线,应