在算法设计中,递归是一个非常重要的工具。许多看似复杂的问题,都能用递归轻易地解决。之所以如此,是因为可以把那些看似复杂的问题分解成一些相对简单些的问题,这些简单的问题得到解决后,复杂的问题也就得到了解决,也就是常说的“化繁为简、分而治之”。
由于计算机的速度很快,在算法设计时,有一类问题可以通过递归来遍历所有可能的情况以找到解。比如八皇后问题、走迷宫问题,都可以简单的通过递归来找到解。这一类算法被称为Brute Force算法。
递归的理论基础是数学归纳法。
数学归纳法我们都很熟悉。在数学中,它是一种非常重要的证明工具。
用数学归纳法证明函数f (n)(n≥1)时分为两个步骤。
-
第一步:归纳基础。证明n=1时f(n)成立。
-
第二步:递推。假设f (n)成立,证明f (n+1)也成立。
例:请用数学归纳法证明
n
∑(2i-1)= n2
i=1
证明:
1)当i=1时, 左式=(2*1-1)=1。
右式=12=1。
所以,左式=右式。
2)假设当i=k(k>1)时成立, 我们来证明当i=k+1时也成立。
当i=k+1时,左式=[2(k+1)-1] + k2=k2+2k+1=(k+1)2=右式。
综合1)、2)可知,上式成立。
1) 递归的概念
所谓递归,就是一个函数自己调用自己。比如,
voidFoo(int n)
{
……
Foo(n-1); // 函数Foo的实现调用了它自己
……
}
如果一个函数A自己直接调用自己,则称为直接递归。如果一个函数A调用函数B,而函数B又调用了A,则称为间接递归。
间接递归可以中间经过多个函数,比如函数 A调用B,B调用C,C调用D, D调用 A。
2) 递归的执行
例:斐波那契数列定义如下:
F(0)=0,
F(1)=1,
F(n)=F(n-1)+F(n-2),n≥2。
请编写一函数计算F(i)(i ≥0)。
解:计算斐波那契数列的C语言函数如下:
intFibonacci(int n)
{
if( n== 0)
{
return 0;
}
else if( n == 1)
{
return 1;
}
else
{
return Fibonacci(n-1) + Fibonacci(n-2);
}
}
比较上述计算斐波那契数列的算法与斐波那契数列的定义,可以看出:它们是多么的相似啊!
许多初学递归的人往往想不明白:在程序运行时,上述算法是到底是如何工作的。
为了描述上述算法的工作过程,读者自己可以手工执行一下上面的算法,并跟踪一下调用栈的变化。不过,这对于理解递归的帮助并不大。因为,它把计算机的具体执行过程给引入了进来,容易让人考虑过多具体执行的细节。
其实,理解上述算法最简单的办法就是数学归纳法。
我们可以这样来理解上述的算法:
归纳基础:
if( n == 0)
{
//根据Fibonacci数列的定义, Fibonacci(0)=0
return 0;
}
else if( n == 1)
{
//根据Fibonacci数列的定义, Fibonacci(1)=1
return 1;
}
递推:
else
{
//假如Fibonacci (n-1)和Fibonacci (n-2)是正确的,
//那么,根据Fibonacci数列的定义,
// Fibonacci(n)= Fibonacci(n-1) +Fibonacci(n-2)。
returnFibonacci(n-1) + Fibonacci(n-2);
}
1) 集合的子集问题
问题描述:给定一个含单个字符的集合A,请打印出其全部子集。
分析:根据集合论知识,我们知道,对于有n个(n≥1)元素的集合A,假设a为A中的某一元素,那么A的全部子集为:
集合A的全部子集= 所有包含a的子集+ 所有不包含a的子集
另外,还有一个集合的表示问题。由于是单个字符的集合,我们可以用字符数组(char[])来表示集合。
由此,我们可以写出打印一个集合全部子集的算法如下:
//打印长度为len的含单个字符的集合a的全部子集
voidPrintAllSubsets(char a[], int len)
{
PrintAllSubsets(a, 0, len -1, len);
}
//打印由a[from]到a[to]中的字符组成的集合的全部子集
voidPrintAllSubsets(char a[], int from, int to, int len)
{
if( from > to ) {
PrintSubset(a, len);
return;
}
// 打印所有不包含 a[from]的子集
char temp = a[from];
a[from] = ' ';
PrintAllSubsets(a, from +1, to, len);
a[from] = temp;
// 打印所有包含 a[from]的子集
PrintAllSubsets(a, from +1, to, len);
}
//打印长度为len的含单个字符的集合中的各个元素
voidPrintSubset(char a[], int len)
{
for (int i = 0; i < len; i++) {
if(a[i] != ' ' ) {
printf("%c", a[i]);
}
}
printf("\n");
}
下面的调用即可打印集合{'a','b', 'c' }的全部子集:
char a[] ={'a', 'b', 'c' };
PrintAllSubsets(a, sizeof(a)/sizeof(a[0]));
输出:
下面的调用即可打印集合{'a', 'b', 'c' }的全部子集:
c
b
b c
a
a c
a b
a b c
2) 0-1背包问题
问题描述:给定N(N≥1)个物品,和一个最多能容纳重量为C(C>0)的物品的背包。这些物品的重量和价值分别为(Wi,Vi)(N≥i≥1)。求一个解,使得在不超过背包容量的情况下,所装物品的价值最大。所谓0-1背包,是指对于任一物品,要么取,要么不取,不能只取该物品的一部分。
分析:背包问题其实和集合的子集问题很类似,只不过,它只找一个物品的子集,使得该子集的价值尽可能的大。所以,很自然的想法就是利用上述的求集合子集的算法,将所有物品的子集枚举出来,然后比较其价值的大小即可。
typedefstruct
{
int weight;
int value;
}ITEM;
ITEMitems[] = { {7, 42}, {3, 12}, {4, 40}, {5, 25} };
intkanpsackCapacity = 10;
#defineITEM_COUNT ( sizeof(items)/sizeof(items[0]) )
intmostValuableSolution[ITEM_COUNT];
inthightestValue =0;
intCalculateSolutionValue(int solution[])
{
int totalValue =0;
for(int i=0;i<ITEM_COUNT; i++)
{
if( solution[i] ) // 第i个item是否在该solution中?
{
totalValue += items[i].value;
}
}
return totalValue;
}
intCalculateSolutionWeight(int solution[])
{
int totalWeight =0;
for(int i=0;i<ITEM_COUNT; i++)
{
if( solution[i] ) // 第i个item是否在该solution中?
{
totalWeight += items[i].weight;
}
}
return totalWeight;
}
voidFillKanpsack(int currentItem, int solution[])
{
if( currentItem == ITEM_COUNT )
{
int solutionValue =CalculateSolutionValue(solution);
if( solutionValue >hightestValue )
{
// 找到了一个更好的解
hightestValue =solutionValue;
memcpy(mostValuableSolution,solution, sizeof(mostValuableSolution));
}
return;
}
// 不包含items[currentItem]
solution[currentItem] = 0;
FillKanpsack( currentItem + 1, solution);
// 包含items[currentItem]
solution[currentItem] = 1;
int solutionWeight =CalculateSolutionWeight(solution);
if( solutionWeight > kanpsackCapacity )
{
// 超重了
return;
}
FillKanpsack( currentItem + 1, solution);
}
int_tmain(int argc, _TCHAR* argv[])
{
int solution[ITEM_COUNT];
FillKanpsack(0, solution);
printf("hightestValue = %d\n",hightestValue);
for(int i=0;i<ITEM_COUNT; i++)
{
if( mostValuableSolution[i] )// 第i个item是否在该solution中?
{
printf("Item %d is in the mostvaluable solution.\n", i);
}
}
return 0;
}
上述程序的输出为:
hightestValue= 65
Item 3 is inthe most valuable solution.
Item 4 is inthe most valuable solution.
3) 八皇后问题
问题描述:在一个8×8的国际象棋棋盘上,有8个皇后。按照国际象棋的规则,若两个皇后处在同一行上、同一列上或同一平行与对角线的斜线上,则二者可以相互攻击。请找出所有的解,使得8个皇后不能相互攻击。
下面就是一个符合条件的解:
分析:因为两个皇后不能处在同一行上,所以这8个皇后必须各占一行。那么,问题就简化为给这8个皇后各找一个合适的列位置。
我们可以将第1个皇后放在(1,1)位置。
然后,给第二个皇后找合适的列位置。先从(2,1)开始,显然不行,因为这样就与第一个皇后处在了同一列上。那么,就试试下一列,即(2,2)位置,也不行,因为这样就与第一个皇后处在了同一斜线上。接着试下一列,即(2,3)位置,这回可以了。
依次为第三个及后面的皇后找位置。
在找的过程中,如果试遍了所有8个列位置,都不能成功地找到一个位置。那就只能调整一下前面皇后的位置。然后再接着试。这就是所谓的“回溯”。
下面的算法能打印出8皇后问题的全部解。
#defineQUEEN_NUMBER ( 8 )
intg_totalSolutionCount = 0;
intCanAttack(int row1, int col1, int row2, int col2)
{
int columnDifference = col1 - col2;
if (row1 == row2 ||
col1 == col2 ||
row2 + columnDifference == row1||
row2 - columnDifference == row1)
{
return 1;
}
return 0;
}
//第0个到第queenRow个皇后已经放好,若将第nextQueentRow个皇后
//放在第nextQueenColumn列,是否合适?
intCanAttack(int queenPositions[QUEEN_NUMBER], int queenRow, int nextQueentRow,int nextQueenColumn)
{
for(int i = 0; i <= queenRow; i ++ )
{
if( CanAttack( i, queenPositions[i],nextQueentRow, nextQueenColumn))
{
return 1;
} // if
}
return 0;
}
voidPrintResult(int queenPositions[QUEEN_NUMBER])
{
g_totalSolutionCount ++;
printf("\n");
for(int i = 0; i < QUEEN_NUMBER; i ++ )
{
printf("Queen[%d]=%d\n",i, queenPositions[i]);
}
}
//放第row(0<= row <= QUEEN_NUMBER - 1)个皇后
voidPutNextQueen(int queenPositions[QUEEN_NUMBER], int row)
{
if( row == QUEEN_NUMBER )
{
// 已经全部摆放完毕,找到了一个解
PrintResult(queenPositions);
}
for(int col = 0; col < QUEEN_NUMBER; col++)
{
if( ! CanAttack(queenPositions,row-1, row, col))
{
// 将第row个皇后放在第col列
queenPositions[row]=col;
PutNextQueen(queenPositions,row +1);
}
}
}
int_tmain(int argc, _TCHAR* argv[])
{
int queenPositions[QUEEN_NUMBER];
PutNextQueen(queenPositions,0);
printf("Total SolutionCount=%d\n", g_totalSolutionCount);
return 0;
}
上述程序所得的解的总数为92个。
有些数据结构的题目经常要求写出某个问题的非递归算法。比如,写出二叉树前序、中序或后序遍历的非递归算法。
对于这类题目,大家首先要明白:本质上,递归算法与非递归算法是一样的,区别仅仅在于形式上。递归算法将状态信息保存在编译器生成的调用栈中,而非递归算法则是将状态信息保存在自己维护的栈中,仅此而已。
1) 八皇后问题的非递归算法
下面给出一个八皇后问题的非递归算法,大家可以与上面的递归算法做一比较,体会一下二者的不同。
usingnamespace std;
#defineQUEEN_NUMBER ( 8 )
intg_totalSolutionCount = 0;
stack<int>g_currentColumn;
voidFindSolutions()
{
int queenPositions[QUEEN_NUMBER];
int row = 0;
int col = 0;
do
{
if( row == QUEEN_NUMBER )
{
// 已经全部摆放完毕,找到了一个解
PrintResult(queenPositions);
// 回溯到上个皇后,继续寻找下一个解
row --;
col =g_currentColumn.top();
g_currentColumn.pop();
}
// 放第row个皇后
for(; col < QUEEN_NUMBER;col ++)
{
if( !CanAttack(queenPositions, row-1, row, col))
{
// 将第row个皇后放在第col列
queenPositions[row]=col;
break;
}
}
if( col < QUEEN_NUMBER )
{
// 为第row个皇后成功地找到了一个位置
g_currentColumn.push(col+ 1);//记下下次开始尝试的位置
row ++;
col = 0;
}
else
{
// 未能为第row个皇后找到一个位置
// 回溯到上个皇后重试
row--;
col =g_currentColumn.top();
g_currentColumn.pop();
}
} while( ! (row==0 && col ==QUEEN_NUMBER));
}
int_tmain(int argc, _TCHAR* argv[])
{
FindSolutions();
printf("Total SolutionCount=%d\n", g_totalSolutionCount);
return 0;
}
这个非递归算法用到了一个栈(g_currentColumn),为了实现的方便,我们用了C++标准模板库中的栈。这个栈中存放的是已经放好的那些行最近一次尝试的列位置。
通过与前面的递归算法比较,我们可以看出:递归算法保存将最近一次尝试的列位置信息保存在一个局部变量(col)中,而这个局部变量col则被保存在编译器生成的运行栈中。而非递归算法则是将最近一次尝试的列位置信息保存在自己维护的一个栈(g_currentColumn)中。
这就是递归算法与非递归算法的不同。
明白了这一点,大家就很容易将递归算法改写成非递归算法了。