递归问题——以全排列、青蛙过河问题为例

一、递归的概念

      递归函数是一种自身调用自身的函数。递归的基本思想是将规模大的问题转化为规模小的相似的子问题来解决。例如,我们可以将一个大洋葱看成一个带着一层洋葱皮的小洋葱,要剥开这个洋葱,就是递归求解的过程。递归包括两种:直接递归和间接递归。直接递归是指一个函数的代码中直接包括了调用自身的代码。间接递归是指类似这样的函数调用:函数A调用函数B,函数B又调用函数A。当然每层递归所给的参数是不同的。

      递归问题的求解过程和数学中的归纳证明是相通的。归纳证明一个与自然数n有关的问题的一般过程是这样的:

  1. 对于n的一个或多个基本的值(如n=0)该问题是成立的;
  2. 假设当n=k时问题是成立的,其中k是一个任意整数;
  3. 利用假设条件,证明对于n的下一个值k+1问题也是成立的。

      递归问题和归纳问题的相似之处就在于它们首先都有一个问题的基本部分,其次再有一个递归部分或者是验证部分。递归问题中每次应用递归部分的结果是更趋近于基本部分,归纳问题中利用比n值小时结论的正确性来证明取值为n时结论的正确性,归纳证明过程的重复应用可以减少基本值验证的应用。

      下面再解释一下递归的两个组成部分:(1)递归出口,即递归的基本部分,也是最小的子问题,递归到这里结束;(2)递归部分,这部分要解决的问题是如何将大问题转换为小问题。

二、递归问题举例

       1.求n的阶乘

#include<iostream>
using namespace std;

int factorial(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * factorial(n-1);
} 

int main()
{
	cout << factorial(5) << endl;
	return 0;
}

     程序输出结果为120。factorial函数的基本部分就是n<=1的情况,结果为1;递归部分中求n的阶乘转化为求n乘以n-1的阶乘,将大问题转化为小问题。

     整个求解过程可以用下图来表示

       2.求n个不同元素的全排列

     首先我们知道n的不同元素的全排列总共有n!种。比如a,b,c的排列方式有abc,acb,bac,bca,cab,cba。当元素较少的时候我们可以列举出来,但是当元素个数较多时就很难列举。这个时候我们可以考虑递归。从三个元素的排列我们可以发现以下求排列的方式:求n个元素的排列,将其划分为n个子问题,每个子问题中首元素分别为n个不同的元素,然后对这n个子问题中的任意一个进一步划分。将除去首元素的n-1个元素再分别放在首元素之后,划分为n-1个子问题,然后再求剩余n-2个元素的全排列,以此类推。直到问题划分为求一个元素的排列方式,那么很显然就是该元素自身了,然后再层层回溯,得到2个元素的排列,3个元素的排列,最后求解出结果。从每层划分的子问题数也就可以得出排列的总数为n * (n-1) * (n-2) * ... * 2 * 1种。

     下面看具体的算法

#include<iostream>
#include<algorithm>

using namespace std;
template<class T>
void permutation(T list[], int k, int m)
{// 生成list[k : m]的所有排列方式 
	int i;
	if (k == m)
	{// 输出排列方式 
		for (i = 0; i <= m; i++)
		cout << list[i] << " ";
		cout << endl;
	}
	else if (k < m)
	{// 递归地产生list[k : m]的排列方式 
		for (i = k; i <= m; i++)
		{
			swap(list[k], list[i]);
			permutation(list, k+1, m);
			swap(list[k], list[i]);
		}
	}
}

int main()
{
	int list[4] = {1, 2, 3, 4};
	permutation(list, 0, 3);
	return 0;
}
我们规定list[0 : k-1]为list[0]到list[k-1]的所有元素。算法输出所有前缀为list[0 : k-1],后缀为list[k : m]的排列方式。当k=m时,仅有一个后缀list[m],因此list[0 : m]就是所要产生的输出;当k<m时,先用list[k]与list[k : m]中的每个元素进行交换,然后产生list[k+1 : m]的所有排列方式,并用它作为list[0 : k]的后缀。swap完成两个变量值的交换,需要包含头文件<algorithm>,当然这个自己写也可以。

       3.青蛙过河问题

     首先问题阐述如下:

     一条小溪尺寸不大,青蛙可以从左岸跳到右岸,在左岸有一石柱L,面积只容得下一只青蛙落脚,同样右岸也有一石柱R,面积也只容得下一只青蛙落脚。有一队青蛙从尺寸上一个比一个小。我们将青蛙从小到大,用1,2,…,n编号。规定初始时这队青蛙只能趴在左岸的石头L上,当然是一个落一个,小的落在大的上面。不允许大的在小的上面。在小溪中有S个石柱,有y片荷叶,规定溪中的柱子上允许一只青蛙落脚,如有多只同样要求一个落一个,大的在下,小的在上。对于荷叶只允许一只青蛙落脚,不允许多只在其上。对于右岸的石柱R,与左岸的石柱L一样允许多个青蛙落脚,但须一个落一个,小的在上,大的在下。当青蛙从左岸的L上跳走后就不允许再跳回来;同样,从左岸L上跳至右岸R,或从溪中荷叶或溪中石柱跳至右岸R 上的青蛙也不允许再离开。问在已知溪中有S根石柱和y片荷叶的情况下,最多能跳过多少只青蛙?

     这里我们先给出分析结果,再给出分析过程。经过分析我们可以得出的结论是:每增加一片荷叶,那么可以跳过的青蛙数加一;每增加一个石柱,可以跳过的青蛙数是原来的两倍。那么为什么是这个结果呢?下面我们一步步分析。

       1.首先考虑没有石柱的情况,即S=0。

          (1)当y=0时,只能跳过一只青蛙,由L直接跳到R。

          (2)当y=1时,可以跳过两只青蛙。过程为:青蛙1从L跳到荷叶上,青蛙2从L直接跳到R,最后青蛙1从荷叶跳到R。

          (3)当y=2时,可以跳过3只青蛙。过程为:青蛙1从L跳到荷叶1,青蛙2从L跳到荷叶2,青蛙3从L跳到R,青蛙2从荷叶2跳到R,青蛙1从荷叶1跳到R。

       由上面的例子我们可以看出,当只考虑荷叶时,每增加一片荷叶,跳过的青蛙数加一,即青蛙数为y+1。

       2.再考虑增加石柱的情况。

          (1)当S=1,y=0时,可以跳过两只青蛙。过程为:青蛙1从L跳到石柱上,青蛙2从L跳到R,青蛙1从石柱跳到R。

          (2)当S=1,y=1时,可以跳过4只青蛙。过称为:青蛙1从L跳到荷叶上,青蛙2从L跳到石柱上,青蛙1从荷叶上跳到石柱上,青蛙3从L跳到荷叶上,青蛙4从L跳到R,青蛙3从荷叶上跳到R,青蛙1从石柱上跳到荷叶上,青蛙2从石柱上跳到R,青蛙1从荷叶跳到R。

          上述过程可以总结为3步:步骤1:青蛙1和青蛙2借助荷叶跳到石柱上;步骤2:青蛙3和青蛙4借助荷叶跳到R;步骤3:青蛙1和青蛙2借助荷叶由石柱跳到R。

          (3)当S=1,y为任意值时,可以跳过2 * (y+1)只青蛙。过程可以理解为3步:步骤1:前y+1只青蛙借助荷叶跳到石柱上;步骤2:后y+1只青蛙借助荷叶跳到R;步骤3:前y+1只青蛙借助荷叶由石柱跳到R。

          (4)当S=2,y为任意值时,可以跳过4 * (y+1)只青蛙。显然当S=1时,y为相同值时可以跳过2 * (y+1)只青蛙。那么这个过程可以理解为:步骤1:前2 * (y+1)只青蛙利用荷叶和其中一个石柱(这里设为S1)从L跳到另外一根石柱(S2)上;步骤2:后2 * (y+1)只青蛙借助荷叶和S1从L跳到R;步骤3:前2 * (y+1)只青蛙从S2借助荷叶和S1跳到R。

     最后,将这个问题总结为3步。步骤1:前2 * (y+1)只青蛙利用y片荷叶和S-1根石柱从L跳到剩余的一根石柱上;步骤2:后2 * (y+1)只青蛙借助y片荷叶和S-1根石柱从L跳到R;步骤3:前2 * (y+1)只青蛙从剩余的那根石柱借助y片荷叶和S-1根石柱跳到R。

     该问题的基本部分为S=0的情况,青蛙数为y+1,递归部分为S不为0的情况。下面给出实现代码:

#include<iostream>

using namespace std;

int cross_river(int S, int y)
{
	if(0 == S)
		return y + 1;
	else
		return 2 * cross_river(S-1, y);
}
int main()
{
	int S, y;
	cout << "Please input the number of pillars and lotus leaves:" << endl;
	cin >> S >> y;
	cout << "Number of frogs: " << cross_river(S, y) << endl;
	return 0;
} 

三、递归转化为非递归

     将递归算法转换为非递归算法有两种方法,一种是直接求值,不需要回溯;另一种是不能直接求值,需要回溯。前者使用一些变量保存中间结果,称为直接转换法;后者使用栈保存中间结果,称为间接转换法,下面简单讨论这两种方法。

       1.直接转换法:

#include<iostream>

using namespace std;

int factorial(int n)
{
	int i, s=1;
	for(i = 1; i <= n; i++)
		s = s * i; // 用s保存中间结果
	return s; 
} 

int main()
{
	int n;
	cin >> n;
	cout << "factorial(" << n << "):" << factorial(n);
	return 0;
}
     上述算法用循环求解阶乘。一般的,直接转换法可以使用变量保存中间结果,将递归结构转换为循环结构。      

       2.间接转换法
     该方法使用栈保存中间结果,一般需根据递归函数在执行过程中栈的变化得到。其一般过程如下:

将初始状态s0进栈
while (栈不为空)
{
    退栈,将栈顶元素赋给s;
    if (s是要找的结果)
        返回;
    else
    {
      寻找到s的相关状态s1;
      将s1进栈
  }
}

      间接转换法在数据结构中有较多实例,如二叉树遍历算法的非递归实现、图的深度优先遍历算法的非递归实现等等。

     递归转化为非递归部分参考博文:http://blog.csdn.net/wangjinyu501/article/details/8248492

  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值