递归和分治思想 (上)

http://blog.fishc.com/2194.html

让编程改变世界

Change the world by program


 

递归

 下集为  http://blog.csdn.net/miao6664659/article/details/8284075

妹子,甲鱼哥今天给你讲一个故事吧,从前我有个小弟,酷爱探险,有一次他进了一个山洞,然后又出来,然后又进去,然后又出来,然后又进去,然后又出来。。。。。。后来他很开心~

艹,你说什么呢?

妹子悟性真高^_^

 

事实上递归就跟鸡生蛋蛋又生鸡的道理一样,只有等哪一天鸡不想生蛋了,做了绝孕手术或者用上了杜蕾斯,这个递归就算结束了。

 

斐波那契(Fibonacci)数列的递归实现

 

插句话:Sierpinski三角形源代码放在论坛,有需要的朋友可以去下载。

斐老跟小甲鱼有个共同爱好,就是老爱拿交配说事儿,不同的是小甲鱼注重过程和细节,斐老更关心结果,下边就有他讲的一个故事:

 

如果说兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。假设所有兔子都不会死去,能够一直干下去,那么一年以后可以繁殖多少对兔子呢?

 

斐波那契数列

 

斐波那契数列的迭代实现

 

我们都知道兔子繁殖能力是惊人的,如下图:

斐波那契数列

 

我们可以用数学函数来定义:

斐波那契数列

 

课间练习:假设我们需要打印出前40位斐波那契数列数,我们不妨一起考虑下用迭代如何实现?

 

斐波那契数列的递归实现

 

递归事实上就是函数自己调用自己,我们先一起看下代码的实现,然后再来分析:

//递归是自顶向下的算法,会有很多重复,效率十分低下
int test1(int n)
{
	if(n==0||n==1)
	{
		return n==0?0:1;
	}
	else
		return test1(n-1)+test1(n-2);
}

斐波那契数列


递推的实现:

//递推和递归
void test(int n)
{
	int temp1=0;int temp2=1;
	for(int i=2;i<=n;i++)
	{
		int temp=temp2;
		temp2=temp1+temp2;
		temp1=temp;
		printf("%d ",temp2);
	}
}

递归定义

 

在高级语言中,函数自己调用和调用其他函数并没有本质的不同。

我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。

 

不过,写递归程序最怕的就是陷入永不结束的无穷递归中。切记,每个递归定义必须至少有一个条件,当满足这个条件时递归不再进行,即函数不再调用自身而是返回值。

比如之前我们的Fbi函数结束条件是:i < 2。

 

对比了两种实现斐波那契的代码,迭代和递归的区别是:迭代使用的是循环结构,递归使用的是选择结构。

使用递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。

 

但大量的递归调用会建立函数的副本,会消耗大量的时间和内存,而迭代则不需要此种付出。

递归函数分为调用和回退阶段,递归的回退顺序是它调用顺序的逆序。

 

举个例子,计算n的阶乘n!

n的阶乘

这样我们就不难设计出递归算法:

int calN(int n)
{
	if(n==1||n==0)
	{
		return 1;
	}
	else
	{
		return n*calN(n-1);
	}
}

假设我们n的值传入是5,那么:

 

实例分析

 

题目要求:编写一个递归函数,实现将输入的任意长度的字符串反向输出的功能。

例如输入字符串FishC,则输出字符串ChsiF。

 

应用递归的思想有时可以很轻松地实现一些看似不太容易实现的功能,例如这道题。

要将一个字符串反向地输出,童鞋们一般采用的方法是将该字符串存放到一个数组中,然后将数组元素反向的输出即可。

 

这道题要求输入是任意长度,所以不用递归的话,实现起来会比较麻烦(当然你可以用之前我们讲过的动态申请内存那招)。

我们说过,递归它需要有一个结束的条件,那么我们可以将“#”作为一个输入结束的条件。

void print()
{
	char a;
	scanf("%c",&a);
	if(a!='#')
		print();
	if(a!='#')
		printf("%c",a);
}

以下是另外一种情况:

void swap(char& s,char & t)
{
	char temp;
	temp=s;
	s=t;
	t=temp;
}
//反向一个字符串
void reverse(char* str,int begin,int len)
{
	if(len==1||len==0)
		return;
	else
	{
		swap(str[begin],str[begin+len-1]);
		reverse(str,begin+1,len-2);
	}
}

假设我们从屏幕上输入字符串:ABC#

 

分治思想

 

分而治之的思想古已有之,秦灭六国,统一天下正是采取各个击破、分而治之的原则。

而分治思想在算法设计中也是非常常见的,当一个问题规模较大且不易求解的时候,就可以考虑将问题分成几个小的模块,逐一解决。

 

分治思想和递归算是有亲兄弟的关系了,因为采用分治思想处理问题,其各个小模块通常具有与大问题相同的结构,这种特性也使递归技术有了用武之地。

我们接下来通过实例来讲解。

折半查找算法的递归(和非递归)实现和快速排序递归的实现 极为相似

 

折半查找法是一种常用的查找方法,该方法通过不断缩小一半查找的范围,直到达到目的,所以效率比较高。

因为这个在《零基础入门学习C语言》等基础教程中已经详细讲解过,小甲鱼这里就通过文字教程简单给大家回顾下算法的主要思路。

 

从算法的折半查找的过程我们不难看出,这实际上也是一个递归的过程:因为每次都将问题的规模减小至原来的一半,而缩小后的子问题和原问题类型保持一致。

作为课后题大家实现并体验下分治思想的妙处~

//折半查找  分治法 递归实现
int Find(int *a,int begin,int end,int p)
{
	if(begin<=end)
	{
		int mid=(begin+end)>>1;
		if(a[mid]==p)
			return mid;
		else if(a[mid]<p)
		{
			return Find(a,mid+1,end,p);
		}
		else
			return Find(a,begin,mid-1,p);
	}
	return -1;
}
#define N 10
//迭代
int Find(int *a,int p,int len)
{
	int begin=0;
	int end=len-1;
	while(begin<=end)
	{
		int mid=(begin+end)>>1;
		if(a[mid]==p)
			return mid;
		else if(a[mid]<p)
		{
			begin=mid+1;
		}
		else
			end=mid-1;
	}
	if(begin>end)
		return -1;

}
int a[N]={0,1,2,3,5,6,7,8,9};
//快速排序 之分治法 递归 注意边界
void QuickSort(int* a,int l,int r)
{
	if(l>=r)
	{
		return;
	}
	int left=l;
	int right=r;
	int key=a[left];
	while(l<r)
	{		
		while(a[r]>key&&l<r)
		{
			r--;
		}
		if(l>=r)
			break;
		a[l++]=a[r];
		while(a[l]<key&&l<r)
		{
			l++;
		}
		if(l>=r)
			break;
		a[r--]=a[l];
	}
	a[l]=key;
	QuickSort(a,left,l-1);
	QuickSort(a,l+1,right);
	return;
}
递归之经典--汉诺塔

让编程改变世界

Change the world by program


 

汉诺塔

 

一位法国数学家曾编写过一个印度的古老传说:在世界中心贝拿勒斯的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。

不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。

僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。

 

汉诺塔

 

这其实也是一个经典的递归问题。

 

我们可以做这样的考虑:

先将前63个盘子移动到Y上,确保大盘在小盘下。

再将最底下的第64个盘子移动到Z上。

最后将Y上的63个盘子移动到Z上。

 

这样子看上去问题就简单一点了,但是关键在于第1步和第3步应该如何执行呢?

我们先一起来体验一下这个游戏:汉诺塔游戏.swf

 

在游戏中,我们发现由于每次只能移动一个圆盘,所以在移动的过程中显然要借助另外一根针才行。

也就是说第1步将1~63个盘子借助Z移到Y上,第3步将Y针上的63个盘子借助X移到Z针上。那么我们把所有新的思路聚集为以下两个问题:

问题一:将X上的63个盘子借助Z移到Y上;

问题二:将Y上的63个盘子借助X移到Z上。

 

解决上述两个问题依然用相同的方法:

 

问题一的圆盘移动步骤为:

先将前62个盘子移动到Z上,确保大盘在小盘下。

再将最底下的第63个盘子移动到Y上。

最后将Z上的62个盘子移动到Y上。

 

问题二的圆盘移动步骤为:

先将前62个盘子移动到X上,确保大盘在小盘下。

再将最底下的第63个盘子移动到Z上。

最后将X上的62个盘子移动到Y上。

 

那我们是不是发现了什么?

接下来的实验和解说请观看视频……

详细代码如下:
void mov(int n,int src,int temp,int des)
{
	if(n>=2)
	{
		mov(n-1,src,des,temp);
		printf("mov %d from %d to %d\n",n,src,des);
		mov(n-1,temp,src,des);
	}
	else
		printf("mov %d from %d to %d\n",n,src,des);
}

int _tmain(int argc, _TCHAR* argv[])
{
	mov(3,0,1,2);
	return 0;
}

递归和分治思想4:八皇后问题

 

让编程改变世界

Change the world by program


 

八皇后问题(递归解法)

 

八皇后问题

 

八皇后问题,是一个古老而著名的问题,是回溯算法的典型例题(这节课小甲鱼先用递归算法来解)。

 

该问题是十九世纪著名的数学家高斯1850年提出:

 

在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

 

高斯先生当年由于没有学好计算机编程,没日没夜地计算呀,得出结论是76种,硬生生把自己给“搞死”了!对了,当年还没有计算机……

正确的结果应该是92种,小甲鱼这节课和大家一起边敲代码边总结思路!

以下是自己的代码:
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <windows.h>
#include<vector>
#include <iostream>
using namespace std;

void print(int (*chess)[8])
{
	for(int i=0;i<8;i++)
	{
		for(int j=0;j<8;j++)
		{
			printf("%d ",chess[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}


bool check(int row,int col,int (*chess)[8])
{
	for(int i=0;i<8;i++)
	{
		if(chess[i][col]!=0)
		{
			return false;
		}
	}
	//左上角
	for(int i=row,j=col;i>=0&&j>=0;j--,i--)
	{
		if(chess[i][j]==1)
			return false;
	}
	//右下角
	for(int i=row,j=col;i<8&&j<8;i++,j++)
	{
		if(chess[i][j]==1)
			return false;
	}
	//you上角
	for(int i=row,j=col;i>=0&&j<8;i--,j++)
	{
		if(chess[i][j]==1)
			return false;
	}
	//左下角
	for(int i=row,j=col;i<8&&j>=0;i++,j--)
	{
		if(chess[i][j]==1)
			return false;
	}
	return true;

}

int count;
void EightQueue(int row,int col,int (*chess)[8])
{
	if(row==8)
	{
		count++;
		print(chess);
		return; 
	}
	else
	{
		for(int i=0;i<col;i++)
		{
			if(check(row,i,chess))
			{
				chess[row][i]=1;
				EightQueue(row+1,col,chess);
				chess[row][i]=0;
			}
		}
	}
}

int chess[8][8];
int _tmain(int argc, _TCHAR* argv[])
{
	EightQueue(0,8,chess);
	printf("count==%d",count);
	return 0;
}


擦,check中的
for(int i=row,j=col;i>=0&&j<8;i--,j++
写成了 for(int i=row,j=col;i>=0,j<8;i--,j++) 搞了一下午,马虎要不得啊。

小甲鱼提供的代码

#include <stdio.h>

int count = 0;

int notDanger( int row, int j, int (*chess)[8] )
{
	int i, k, flag1=0, flag2=0, flag3=0, flag4=0, flag5=0;

	// 判断列方向
	for( i=0; i < 8; i++ )
	{
		if( *(*(chess+i)+j) != 0 )
		{
			flag1 = 1;
			break;
		}
	}

	// 判断左上方
	for( i=row, k=j; i>=0 && k>=0; i--, k-- )
	{
		if( *(*(chess+i)+k) != 0 )
		{
			flag2 = 1;
			break;
		}
	}

	// 判断右下方
	for( i=row, k=j; i<8 && k<8; i++, k++ )
	{
		if( *(*(chess+i)+k) != 0 )
		{
			flag3 = 1;
			break;
		}
	}

	// 判断右上方
	for( i=row, k=j; i>=0 && k<8; i--, k++ )
	{
		if( *(*(chess+i)+k) != 0 )
		{
			flag4 = 1;
			break;
		}
	}

	// 判断左下方
	for( i=row, k=j; i<8 && k>=0; i++, k-- )
	{
		if( *(*(chess+i)+k) != 0 )
		{
			flag5 = 1;
			break;
		}
	}

	if( flag1 || flag2 || flag3 || flag4 || flag5 )
	{
		return 0;
	}
	else
	{
		return 1;
	}
}

// 参数row: 表示起始行
// 参数n: 表示列数
// 参数(*chess)[8]: 表示指向棋盘每一行的指针
void EightQueen( int row, int n, int (*chess)[8] )
{
	int chess2[8][8], i, j;

	for( i=0; i < 8; i++ )
	{
		for( j=0; j < 8; j++ )
		{
			chess2[i][j] = chess[i][j];
		}
	}

	if( 8 == row )
	{
		printf("第 %d 种\n", count+1);
		for( i=0; i < 8; i++ )
		{
			for( j=0; j < 8; j++ )
			{
				printf("%d ", *(*(chess2+i)+j));
			}
			printf("\n");
		}
		printf("\n");
		count++;
	}
	else
	{
		for( j=0; j < n; j++ )
		{
			if( notDanger( row, j, chess ) ) // 判断是否危险
			{
				for( i=0; i < 8; i++ )
				{
					*(*(chess2+row)+i) = 0;
				}
				
				*(*(chess2+row)+j) = 1;

				EightQueen( row+1, n, chess2 );
			}
		}
	}
}

int main()
{
	int chess[8][8], i, j;

	for( i=0; i < 8; i++ )
	{
		for( j=0; j < 8; j++ )
		{
			chess[i][j] = 0;
		}
	}

	EightQueen( 0, 8, chess );

	printf("总共有 %d 种解决方法!\n\n", count);

	return 0;
}

结果一样



友荐云推荐
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值