在前一篇文章中已经介绍了如何应用穷举法来解决具体的编程问题。穷举法最为常用,可以解决大部分常见的问题。今天再来介绍一种新的解题思路: 递归法。 它可以看成是对穷举法的一种补充。它的思路是在不方便穷举所有的可能时,通过设置特定的函数,在该函数内部反复地调用自身并输入不同的参数,以达到遍历所有可能性的目的。使用递归法代码量小,编码简单,逻辑清晰,不易出错。下面通过一个经典的例子来让大家感受下递归法的应用方法。
例1: 汉诺塔问题
汉诺塔问题是最经典的只能够使用递归的方法解决的问题,题目描述如下:
据传说,在古代世界中心的贝拿勒斯(印度北部)的圣庙里,一块在黄铜板上插着3根宝石针。印度教的主神梵天在创造世界时,在其中的一根针上自下而上地穿好了由大至小的64层金片,即为汉诺塔。无论白天黑夜,总有一个僧侣按如下的法则移动这些金片,一次只能够移动一层,不管在哪根针上,小片必须在大片的上面。 要求借助于第二根针将整个汉诺塔移至第三根针上。 图示如下:
假设汉诺塔只有4层,其具体的移动过程如下动画所示:
本题具有如下的特点,使之不能使用穷举法:
1. 不便于穷举: 如果用数学的方法估算,假设一秒钟移动一片,将64层的汉诺塔移至另一根针上所需的步骤是个天文数字:共需接近5845亿年。 由于取值范围过大,且无法循环,不可能将所有的过程都穷举实现出来。
2. 可以经由一系列有限的步骤实现,其中的每个步骤都很相似,这道题为例,初始时有n层,第一大步就是将上面的n-1层移动至第三根针上,再将最下面的第n层移动至第二根针上,再将第三根针上的n-1层借助第一根针移动至第二根针上。在移动n-1层时,所使用的过程与移动n层是相同的,无非是初始针,目标针和辅助针和移动的层数不同而已。
只要满足这样的条件,都可以试着使用递归的方法来设计,过程如下:
定义函数 moveHanNoi(层数,初始针,辅助针,目标针)
{
if(层数==1)
{直接从初始针移动至目标针}
else
{
moveHanNoi(层数-1,初始针,目标针,辅助针) //将(层数-1)层汉诺塔由初始针,利用目标针移至辅助针
直接从初始针移动第n层至目标针
moveHanNoi(层数-1,辅助针,初始针,目标针) //将(层数-1)层汉诺塔由辅助针,利用初始针移至目标针
}
}
这样一来,只需要第一次输入初始人层数,初始针,辅助针,目标针的参数,即可得到结果。省却了复杂的循环遍历的麻烦。
像这种在一个函数内部又调用自身,只是每次调用时传递参数不同的现象,称为递归。
下面再举两个例子来强化大家对于递归算法的认识:
例2: 给定一个正整数,输出它的阶乘。
数学中阶乘的定义就是从1开始逐步累乘自然数到当前的数,使用!来表示阶乘,如
1! = 1
2! = 1*2
3!=1*2*3
4! = 1*2*3*4
……
很快大家就会想到使用穷举法,确实,用穷举法能够非常直接地解决此问题,核心代码如下(C#)
……
int sum =1;
for(int i=1; i<=n; i++)
sum *= i;
输出sum的结果
再来仔细地分析阶乘的分解过程,很容易发现了如下的规律:
1! =1
2! = 2*1! = 2
3! = 3*2!=3*2*1! = 6
4! = 4*3!=4*3*2!=4*3*2*1! = 24
总结如下: 1的阶乘为1,其它任何数的阶乘等于n乘以(n-1)的阶乘。 这里为了求得n的阶乘,必须用同样的方法求得(n-1)的阶乘,再与当前数相乘得到结果。这与汉诺塔中的递归过程何其相似。设计递归的算法如下:
定义函数 jiecheng(int 当前数) 返回当前数的阶乘
{
if(当前数 == 1) 返回1;
else 返回当前数与(当前数-1)阶乘的乘积
}
用C#语言实现如下:
int jiecheng(int curNum){return curNum==1?1:curNum*jiecheng(curNum-1);}
例3: 在8*8的国际象棋棋盘上,放置了8个皇后,使之不能相互攻击,找出所有满足条件的布局
分析:
(1) 由于国际象棋中皇后的威力最大,横、竖、斜三个方向均可攻击。若想8个皇后和平共处,必须保证棋盘上横、竖、斜三个方向上最多只有一个皇后。
(2) 8*8的棋盘可以使用8*8的二维数组来表示,0代表空,1代表皇后,将此二维数组输出即可表示棋盘的状态。
(3) 以列为单位,自左至右依次地在每一列中由上到下放置皇后,每放置一个之后,看它是否与现有的皇后相冲突。若没有冲突,继续试探下一列。若有冲突,尝试摆放到下一行。最终当试探完最后一列且无冲突之后,输出结果。
设行号为0-7, 列号为0-7 其执行过程如下:
定义函数 place(当前列号,棋盘) //表示试探棋盘上的某一列
{
if(列号==8)
{输出当前棋盘的状态,返回;}
从第0行遍历至第7行,每次遍历时
{
1. 放置皇后,当前位置为1
2.观察是否与已有的皇后位置冲突 //可设置一个函数来进行判断
无冲突时: 试探下一列,调用自身place(当前列号+1,棋盘)
3. 撤消皇后所在的位置
}
}
通过上面的例子,可以得出递归法的编程模式如下:
1. 尝试着做某种最基本的操作,该操作将会改变现有的状态
汉诺塔: 移动一层塔,导致初始针,辅助针,目标针的状态改变。
阶乘: 当前数乘以比它小一的数的阶乘,改变了累乘的结果。
八皇后: 放置一个皇后在棋盘格式里,当前棋盘的状态改变了。
2. 根据当前的状态,观察是否到了 不需要递归的时刻,如:
汉诺塔: 前n-1层已经移动至辅助针,直接将当前层移动至目标针,不需要递归。
阶乘: 当前数为1时,不需要递归,直接返回1
八皇后:0-7列全部试探完毕,不再递归
3.如果未到不必递归时,递归地调用自身,注意每次调用时的参数不同:
汉诺塔: 若当前n-1层未完全移至辅助针,需要递归调用自身,使用的参数“层数”,“初始针”,“目标针”,“辅助针”都会发生变化
阶乘: 当前数不为1时,递归调用阶乘本身,参数要变成“当前数-1”,返回值为当前数*下一阶乘数
4. 撤消当前的操作,状态还原(可选)
八皇后问题中,在某一列上放置了皇后,无论是否合适,都要在试探完毕之后撤走,再放置到下一位置。这个撤消的操作在迷宫类的题目中非常重要,否则无法得到所有的可能状态。但汉诺塔与阶乘问题中不需要状态还原。
作业:
1. 根据上述的分析,使用C#语言实现6层汉诺塔问题的解法。假设3根针分别为A,B,C,初始时A针上自小而大叠放着6层塔片,要求输出移动到C针上时的全部具体的路径。
2. 求解8皇后问题。要求使用二维整型的数组,其中0值表示空,1值表示皇后,使用0与1的矩阵来表示满足条件的棋盘状态,要求找出所有可能的结果。
下图即为一种输出状态
00000100
01000000
00000010
10000000
00010000
00000001
00001000
00100000