一个函数调用了它自己,称为递归。递归和循环可以互相替代。一种程序设计语言,支持
递归就可以不需要支持循环,支持循环就可以不需要支持递归。例如,早期的Lisp语言,就不
支持循环,只支持递归。当然,为了方便使用,程序设计语言一般都既支持递归,也支持循环。
下面这个循环调用了n次doSomething函数: 
for(int i=0;i<n;++i) 
doSomething(); 
可以用对递归函数的调用来替代: 
void loop(int n) 
{ 
if (n ==0) 
return; 
doSomething(); 
loop(n-1); 
}l
oop(8); //调用8 次doSomething()函数
从替代循环的角度来看,递归和循环一样是一种手段,可以用来解决任何问题。但是递归
更多的是一种解决问题的思想———从这个角度看,也可以称递归是一种算法。
求整型数组a的最大值,可以按如下方法来实现: 
int maxValue(int [] a) 
{ 
int ans =a[0]; 
for (int i=1;i<a.length;++i) 
if( ans <a[i]) 
ans =a[i]; 
return ans; 
}
也可以用递归的思想来实现。思路是:如果a的长度是1,则最大值就是a[0];否则,最
大值就是a[1]到a[a.length-1]中的最大值以及a[0]这两者中较大的那个。递归程序如下: 
int maxValue (int [] a,int start) 
{ //求从a[start]开始往后的所有元素中的最大值

50 
if( start ==a.length-1) 
return a[start]; 
return Math.max(a[start], maxValue(a,start+1)); 
}
调用该函数的代码如下: 
int a[] ={1111,2,43,23,332,11222}; 
int m =maxValue(a,0); 
该递归函数与循环的写法相比没有什么优势,但其体现的问题分解的思路是特别有用的, 
即把规模大的问题不断分解为规模小的问题,最终加以解决。要求a的最大值,可以先解决一
个形式相同但规模更小的子问题,即a[1]到a[a.length-1]中的最大值(因a[1]到a[a.length-1] 
可以看作一个比a更短的数组)。这个子问题解决了,将其答案和a[0]做个比较,即可得到原
问题的答案。
本章主要通过具体例题,分以下3种情况讲述递归的用途: 
(1)替代多重循环来进行枚举。
(2)解决用递归形式定义的问题。
(3)将问题分解为规模更小的子问题进行求解。
上述3种情况其实没有、也不需要有严格的区分界限。有的问题,归类为上述不止一种情
况都说得过去。例如5.2节的例题“绘制雪花曲线”,归类为第(2)、第(3)种情况都可以。
第(3)种情况,如果分解出来的子问题不止一个且相互无重叠,一般来说就可以算是“分
治”。
还有一种递归可以称之为“间接递归”,例如函数A 调用了函数B,函数B调用了函数C, 
函数C又调用了函数A,就可以说函数A、B、C 都间接递归调用了自身。本章不涉及这类
递归。
5.1 用递归进行枚举 
有一类问题,本质上可以归结为:有n 个变量,每个变量可以有m 种取值。这n 个变量
取值的某些组合,能够满足某个条件,求出所有满足条件的组合。本章习题中的“奥数问题” 
“棋盘问题”,接下来要讲的案例“N 皇后问题”和“全排列”,本质都是如此。
这类问题最简单的解法就是暴力枚举所有组合,对每个组合检验其是否满足条件。这样
一共要枚举mn 种组合,可以用n 重循环来解决。当n 大的时候,多重循环不好写,因此可以
用递归的写法进行枚举。
5.1.1 案例:N 皇后问题(P0230) 
将N 个皇后摆放在一个N 行N 列的国际象棋棋盘上,要求任何两个皇后不能互相攻击
(两个皇后在同一行,或同一列,或某个正方形的对角线上,就会互相攻击,称为冲突)。输入皇
后数N(1≤N≤9),输出所有的摆法。无解输出“NO ANSWER”。行列号都从0开始算。
输入:一个整数N,表示要把N 个皇后摆放在一个N 行N 列的国际象棋棋盘上。
输出:所有的摆放方案。每个方案一行,依次是第0行皇后位置、第1行皇后位置、……、
第N-1行皇后位置。多种方案输出顺序如下:优先输出第0行皇后列号小的方案。如果两

51 
个方案第0行皇后列号一致,那么优先输出第1行皇后列号小的方案,以此类推。
样例输入 
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<N;++k) 
22. System.out.print(result[k]+" "); 
23. System.out.println(); 
24. return true; 
25. } 
26. boolean succeed = false; 
27. for (int k=0;k<N;++k) //枚举所有位置
28. if (isOk(i,k)) { //看可否将第i 行皇后摆在第k 列
29. result[i] = k; //可以摆在第k 列,就摆上
30. succeed = queen(i+1) || succeed; //接着去摆放第i+1 行的皇后
31. } 
32. return succeed; 
33. } 
34. void solve() { 
35. if (!queen(0)) 
36. System.out.println("NO ANSWER"); 
37. } 
38. } 
39. public class prg0350 { //请注意:在OpenJudge 提交时类名要改成Main 
40. public static void main(String[] args){ 
41. Scanner reader = new Scanner(System.in); 
42. int n = reader.nextInt();

52 
43. new NQueensProblem(n).solve(); 
44. } 
45. } 
queen函数返回值为true或者false,queen(i)的返回值表示:当前,前i 个皇后已经摆好
且不冲突,它们的摆法放在result[0,i-1]中(本书中用a[x,y]表示数组或字符串a 中从
a[x]到a[y]这样连续的一段),在不改变这前i 个皇后的摆法的前提下,继续往下摆放,最终
能否找到至少一种成功的N 皇后摆法。在第27行,函数试图为第i 行的皇后找到所有和前i 
个皇后不冲突的位置,所谓“枚举”,就体现在本行。每找到一个合适的位置,就摆放之,然后递
归调用一次queen(i+1)继续后面皇后的摆放;如果一个合适位置都找不到,则返回false,表
示在当前情况下,最终无法摆放成功。
第24行:由于函数是递归调用的,所以有几个解,本行就会被执行几次。例如,在第0行
皇后摆在第0列的情况下,若执行queen(1)最终会成功,本行会得到执行;在第0行皇后摆放
在第1列的情况下,若执行queen(1)最终也会成功,本行又会得到执行。实际上,如果有k 个解
的第0行皇后都是摆放在第0列的,则会有k 次执行到本行时,第0行皇后是摆放在第0列的。
第26行:succeed表示在当前情况下,再往下摆能否至少找到一个解,先假定为false。
第29行:假设找到的第一个合法摆放位置是k1,第i 行皇后摆放在第k1 列的情况下,会
继续执行queen(i+1),从此处一直递归下去,有可能会找到多种N 皇后的最终合法摆放方
案,即多次走到第21行,输出多个解。这些解的第0行到第i 行的摆放方案都是相同的,例
如,第i 行都是摆放在k1 位置。queen(i+1)执行过程中经历层层多分支递归,终究会返回, 
返回后回到第27行的for循环,继续寻找下一个可行的第i 行摆放位置k2,然后再继续递归
下去。因
此上面的程序会输出所有的解。
第30行:在第27 行开始的循环中,只要有一次执行本行时queen(i+1)返回true, 
succeed的值就会是true,意味着在当前情况下最终能够摆放成功。
本程序中,result数组中的一个元素,本质上就相当于多重循环解法里的一个循环控制变
量,它们都是表示一行皇后的位置的。
请注意:如果要将程序改写成找到一个解就结束,则只需要将第30行替换为下面两行。 
//prg0351.java 
if(queen(i+1)) //接着去摆放第i+1 行的皇后 
return true; 
这样的话,摆放第i 行皇后的时候,一旦发现一个能导致最终摆放成功的摆法,就不会去
尝试第i 行的下一个摆法,因此对每行的皇后,都只找到一个能导致最终成功的摆法,于是程
序的第24行只会执行1次,程序只输出一个解。
N 皇后问题的本质,是有N 个变量,这N 个变量取值的某些组合,能够满足某个条件,要
求出这些满足条件的组合。下面的奥数问题、接下来的全排列问题、习题中的棋盘问题本质都
是如此,都可以用类似N 皇后问题的办法解决。
5.1.2 案例:全排列(P0240) 
给定一个由不同的小写字母组成的字符串,输出这个字符串的所有排列。假设对于小写
字母有a' '<b' '<…<y' '<z' ',而且给定的字符串中的字母已经按照从小到大的顺序排列。

53 
输入:输入只有一行,是一个由不同的小写字母组成的字符串,已知字符串的长度为
1~6。输
出:输出这个字符串的所有排列方式,每行一个排列。要求字母序比较小的排列在前
面。字母序如下定义。
已知S=s1s2…sk ,T =t1t2…tk ,则S<T 等价于,存在p(1≤p≤k),使得s1=t1,s2= 
t2,…,sp-1=tp-1,sp <tp 成立。
样例输入 
abc 
样例输出 
abc 
acb 
bac 
bca 
cab 
cba 
本题的本质就是在n 个位置(编号0~n-1)摆n 个不同字母,要求给出符合“每个字母只
出现一次”这个要求的所有摆法。解题程序如下。 
//prg0360.java 
1. import java.util.*; 
2. class AllPermutations { 
3. String lst; //存放输入的字符串
4. char [] result; //result[i]表示第i 个位置摆放的字母
5. boolean [] used; //used[i]表示lst 中的第i 个字母是否已经用过
6. int n; //排列的长度是n 个字符
7. void permutation(int i) { //从第i 个位置起摆放字母
8. if (i == n) { //条件满足则说明n 个位置都摆上字母了,即发现了一个排列
9. for (char x:result) 
10. System.out.print(x); 
11. System.out.println(""); 
12. return; 
13. } 
14. for (int k=0;k<n;++k) 
15. if (!used[k]) { //第k 个字母还没用过
16. result[i] = lst.charAt(k);//在第i 个位置摆上第k 个字母
17. used[k] = true; 
18. permutation(i+1); //从第i+1 个位置起继续往下摆
19. used[k] = false; 
20. } 
21. } 
22. void solve() { 
23. Scanner reader = new Scanner(System.in); 
24. lst = reader.next(); 
25. n = lst.length(); 
26. result = new char [n]; 
27. used = new boolean[n]; 
28. for (int i=0;i<n;++i) 
29. used[i] = false; 
30. permutation(0); //从第0 个位置开始摆放字母
31. }

54 
32. } 
33. public class prg0360 { //请注意:在OpenJudge 提交时类名要改成Main 
34. public static void main(String[] args){ 
35. new AllPermutations().solve(); 
36. } 
37. } 
函数permutation(i)表示,在0~i-1这i 个位置已经摆好字母的情况下,从第i 个位置
开始继续摆放字母。位置k 摆放的字母,记录在result[k]中(k=0,1,…,n-1)。
第14行:枚举所有在第i 个位置可能摆放的字母,k 是字母在lst中的下标。由于在每个
位置枚举字母的时候都是按照字母从小到大的顺序进行,所以最终会按从小到大的顺序得到
一个个排列,类似于N 皇后问题得到解的顺序。
第15行:一个排列中,每个字母只能用一次,因此用数组元素used[k]记录字母lst[k]是
否已经被用过。lst[k]还没用过,就可以摆在位置i。摆上后就要将used[k]设置为true,表示
lst[k]已经被使用,如第17行所示。
第19行:下次循环就要在位置i 尝试摆放别的字母。要在位置i 摆别的字母,就应该将
刚才在第16行摆放在位置i 的字母lst[k]拿走,那么字母lst[k]就应该恢复成没用过,这样
在后续位置还可以摆放它。因此要让used[k]=false。
5.2 解决用递归形式定义的问题 
有一些问题或者概念,本身的定义就是递归形式的。例如“自然数n 的阶乘”这个概念, 
可以定义成1×2×3×…×(n-1)×n,也可以用以下两句话来定义。
(1)1的阶乘是1。
(2)n>1时,n 的阶乘等于n 乘以(n-1)的阶乘。
第(2)句话,定义“阶乘”这个概念的时候,用到了“阶乘”这个词,看上去像循环定义,让人
无法理解。但是,由于有语句(1)的存在,上面这两句“自然数n 的阶乘”的定义,就是严密且
可以理解的。例如,若问“3的阶乘是什么”,要先回答“2的阶乘是什么”;要回答“2的阶乘是
什么”,就要回答“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)

55 
所以函数的复杂度是O(n)。
5.2.1 案例:波兰表达式(P0250) 
波兰表达式是一种把运算符前置的算术表达式。例如,一般形式的表达式2+3的波兰
表示法为+23。波兰表达式的优点是计算时不需要考虑运算符的优先级,因此不必用括号
改变运算次序,例如,(2+3)*4的波兰表示式为* +234。本题求解波兰表达式的值,其
中运算符包括+、-、*、/四个。
输入:输入为一行,其中运算符和运算数之间都用空格分隔,运算数是浮点数。
输出:输出为一行,为表达式的值。
样例输入 
* +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]处开始取出若干元素构成一个波兰表达式并返回其值
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. }

56 
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°,正北方
向是270°。当然也可以说正北方向是-90°,正西方向是-180°。
绘制雪花曲线须在窗口上进行。雪花曲线也称为科赫曲线,其递归定义如下。
(1)长为size,方向为x(单位:弧度)的0阶雪花曲线,是沿方向x 绘制的一根长为size 
的线段。
(2)长为size,方向为x 的n 阶雪花曲线,由以下4部分依次拼接组成。
1.长为size/3,方向为x 的n-1阶雪花曲线。
2.长为size/3,方向为x-π/3的n-1阶雪花曲线。
3.长为size/3,方向为x+π/3的n-1阶雪花曲线。
4.长为size/3,方向为x 的n-1阶雪花曲线。
图5.1~图5.3是几个雪花曲线的示意图。
绘制长度为600px,方向为0的3阶雪花曲线的程序如下。 
//prg0380.java 
1. import javax.swing.*; //图形界面需要
2. import java.awt.*; //画图需要
3. class SnowCurveWindow extends JFrame {

57 
图5.1 0阶和1阶雪花曲线
图5.2 2阶雪花曲线
图5.3 3阶雪花曲线 
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]); 
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 的方法,设置窗体大小为800*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 {

58 
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 阶雪花曲线,应
该由4段长为size/3的n-1阶雪花曲线连接而成。这个循环就依次画出这四段。若n 阶雪
花曲线的方向是dir(单位:弧度),则这4段的方向依次是x,x-π/3,x+π/3,x。每一段的
终点就是下一段的起点。
5.3 用递归进行问题分解 
递归解决问题的一种特别常用的基本思路是:要解决某一问题,可以先做一步,做完一步
以后,剩下的问题也许就会变成和原问题形式相同,但规模更小的子问题,那就可以递归求解
了。第一步如果有多种选择,则要枚举第一步的所有选择。当然,还需要归纳出不需要递归就
能直接解决的边界条件。
5.3.1 案例:上台阶(P0260) 
有n 级台阶(0<n<20),从最下面开始走,最终要走到所有台阶上面,每步可以走一级或
两级,问有多少种不同的走法?