递归与分治策略

一、递归的概念

递归函数:用函数自身给出定义的函数称为递归函数。

递归算法:直接或者间接地调用自身的算法称为递归算法。

二、分治法的基本思想

分治法:分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同;递归地求解这些子问题,然后将各个子问题的解合并得到原问题的解。

分治法的一般算法模式:

   divide-and-conquer(P)

    {

        if(|P|<=n0) adhoc(P)

divide P into smaller subinstances P1,P2,P3,P4,P5......Pk;

        for(i=1;i<=k;i++)

             yi = divide-and-conquer(Pi);

       return merge(y1,y2,y3,....yk);

     }

三、算法实例

1.二分搜索算法

 问题:给定已排好序的n个元素a[0:n-1],查找在这n个元素中是否存在特定元素x;

 解决方法:二分搜索方法采用分支策略,将n个元素分为个数大致相同的两半,取a[n/2]与x做比较,如果x=a[n/2],则找到x,返回;如果x<a[n/2],则只在数组a的左半段继续搜索x;如果x>a[n/2],则只在数组a的右半段继续搜索x。

算法时间复杂性:可在最坏情况下用O(logn)时间完成搜索任务。

算法如下:

<pre name="code" class="java">package com.caitang.algorithm.recursivepartition;
/**
 * 二分查找算法
 * @author Administrator
 *
 */
public class BinarySearch 
{
	public static int search(int N,int [] array)
	{
		//查找起始位置 0  查找结束位置 N-1
		int left = 0;
		int right = array.length-1;
		while(left <= right)
		{
			//查找起始位置和结束位置的中间位置
			int middle = left + (right - left)/2;
			//如果中间位置的值等于查找值,直接返回
			if(N == array[middle]) return N;
			//如果查找值大于中间位置的值,则查找开始位置右移一位  为什么不是直接从中间位置开始? left = middle;
			if(N>array[middle]) left = middle+1;
			//如果查找值小于中间值,则查找结束位置左移一位   为什么不是直接将中间位置作为结束位置?  right = middle;
			else right = middle-1;
		}
		return -1;
	}
	public static int search_one(int N,int [] array)
	{
		//查找起始位置 0  查找结束位置 N-1
				int left = 0;
				int right = array.length-1;
				while(left <= right)
				{
					//查找起始位置和结束位置的中间位置
					int middle = left + (right - left)/2;
					//如果中间位置的值等于查找值,直接返回
					if(N == array[middle]) return N;
					//如果查找值大于中间位置的值,则查找开始位置右移一位  为什么不是直接从中间位置开始? left = middle;
					if(N>array[middle])  left = middle;
					//如果查找值小于中间值,则查找结束位置左移一位   为什么不是直接将中间位置作为结束位置?  right = middle;
					else right = middle;
				}
				return -1;
	}
 public static void main(String [] args)
 {
	 int [] array = {1,23,45,67,88,98,120,150};
	// System.out.println(search(150,array));
	 System.out.println(search_one(150,array));
 }
}


 

2.棋盘覆盖问题

问题:在一个2^k*2^k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘;要用如下图二所示的4种不容形态的L型骨牌覆盖一个给定的特殊棋盘上的除特殊方格外的所有方格,且任何2个L型骨牌不得重叠覆盖。(关于可完全覆盖这个问题,我想过,最后证明一定是可以完全覆盖的。)

                                                                                      

                       图一   棋盘                                                                                                                                                                         图二   L型骨牌

解决思路:采用分治策略;按照如图三的方式分割2^k*2^k的棋盘(如图三),分割后,特殊方格必位于4个较小的子棋盘之一中,其余3个棋盘中无特殊方格;为了将这三个五个数方格的子棋盘转换为有特殊方格的子棋盘,用编号为t的L型骨牌覆盖这三个无特殊方格的子棋盘会合处的三个方格(如图四);然后对各个子棋盘重复上述步骤,直到棋盘简化为1*1时,问题解决。

2^k-1*2^k-12^k-1*2^k-1
2^k-1*2^k-12^k-1*2^k-1
         

                                    图三    棋盘分割

      

                     

                                                      图 四    棋盘分割覆盖

算法实现如下:

package com.caitang.algorithm.recursivepartition;
/**
 * 棋盘覆盖问题解决算法
 * @author Administrator
 *
 */
public class ChessBoardCover 
{
	//L型骨牌编号,依次为1,2,3,4,5,6,....N
	private static int title =1;
	//棋盘边长
	private static final int SIZE = 16;
	//二维数组作为棋盘
	private static int [][] chessBoard = new int [SIZE][SIZE];
	/**
	 * 分治法解决棋盘覆盖问题
	 * @param tr 当前棋盘左上角的方格的横坐标
	 * @param tc  当前棋盘左上角的方格的纵坐标
	 * @param dr   特殊方格的横坐标
	 * @param dc   特殊方格的纵坐标
	 * @param size  当前棋盘的边长
	 */
	public static void cover(int tr,int tc,int dr,int dc,int size)
	{
		//如果棋盘的边长为1,则表示棋盘覆盖完毕(上次覆盖中已经完成了这些方格的覆盖,所以不需要进行覆盖),直接返回。
		if(size == 1)
		{
			return;
		}
		//骨牌编号自增   下面使用编号为t的L型骨牌对方格进行覆盖
		int t = title ++;
		//对当前子棋盘进行分割,分割为4个边长为原来边长一半的子棋盘
		int s = size/2;
		//覆盖左上角棋盘
		//如果特殊方格在分割后的左上角的子棋盘上
		if(dr<tr+s&&dc<tc+s)
		{
			//继续递归对该子棋盘进行覆盖    棋盘边长变为原来一半
			cover(tr,tc,dr,dc,s);
		}
		else
		{
			//如果特殊方格不在左上角子棋盘中,则需要将该子棋盘右下角的方格进行覆盖以将子棋盘转换为特殊棋盘,然后对转换后的子棋盘的剩余方格进行覆盖
			chessBoard[tr+s-1][tc+s-1] = t;
			cover(tr,tc,tr+s-1,tc+s-1,s);
		}
		//覆盖棋盘右上角
		//如果特殊方格在分割后的右上角的子棋盘中
		if(dr>=tr+s&&dc<tc+s)
		{
			//继续递归对该子棋盘进行覆盖    棋盘边长变为原来一半
			cover(tr+s,tc,dr,dc,s);
		}
		else
		{
			//如果特殊方格不在左上角子棋盘中,则需要将该子棋盘右下角的方格进行覆盖以将子棋盘转换为特殊棋盘,然后对转换后的子棋盘的剩余方格进行覆盖
			chessBoard[tr+s][tc+s-1] = t;
			cover(tr+s,tc,tr+s,tc+s-1,s);
		}
		
		//覆盖棋盘左下角
		//如果特殊方格在分割后的左下角的子棋盘中
		if(dr<tr+s&&dc>=tc+s)
		{
			//继续递归对该子棋盘进行覆盖    棋盘边长变为原来一半
			cover(tr,tc+s,dr,dc,s);
		}
		else
		{
			//如果特殊方格不在左上角子棋盘中,则需要将该子棋盘右下角的方格进行覆盖以将子棋盘转换为特殊棋盘,然后对转换后的子棋盘的剩余方格进行覆盖
			chessBoard[tr+s-1][tc+s] = t;
			cover(tr,tc+s,tr+s-1,tc+s,s);
		}
		
		//覆盖棋盘右下角
		//如果特殊方格在分割后的右下角的子棋盘中
		if(dr>=tr+s&&dc>=tc+s)
		{
			//继续递归对子棋盘进行覆盖 
			cover(tr+s,tc+s,dr,dc,s);
		}
		else
		{
			//如果特殊方格不在右下角子棋盘中,则需要将右下角子棋盘的左上角的方格进行覆盖
			chessBoard[tr+s][tc+s] = t;
			cover(tr+s,tc+s,tr+s,tc+s,s);
		}
	}
	/**
	 * 打印数组元素
	 * @param array
	 */
	public static void printChessBoard(int [][] array)
	{
		for(int i = 0;i<array.length;i++)
		{
			for(int j = 0;j<array[i].length;j++)
			{
				int item = array[i][j];
				if(j==array[i].length-1)
				{
					System.out.println(" " + item);
				}
				else System.out.print(" " + item);
			}
		}
	}
 public static void main(String [] args)
 {
	 System.out.println("------------------覆盖前棋盘-------------------------------");
	 printChessBoard(chessBoard);
	 //棋盘左上角方格坐标(0,0),特殊方格位置(16,16),棋盘边长为16;
	 cover(0,0,16,16,SIZE);
	 System.out.println("---------------------覆盖后棋盘----------------------------");
	 printChessBoard(chessBoard);
 }
}

运行结果:



结果分析:对比运行前后棋盘,可以发现所使用的L型骨牌的数目  (16^2-1)/3 = 85 ,坐标为(16,16)的方格为特殊方格。

3.归并排序

对给定序列进行排序,是最常见的算法实现问题;归并排序基于分治法,将需要排序的序列进行持续分割,直到序列的长度为1,则认为该序列是有序的;然后对这些有序的子序列进行合并;合并完成以后,排序完成。

算法实现:

package com.caitang.algorithm.recursivepartition;
/**
 * 归并排序算法
 * @author Administrator
 *
 */
public class MergeSortAlgorithm 
{
	/**
	 * 进行有序序列的合并操作
	 * @param array 需要合并的序列
	 * @param start  第一段子序列开始位置
	 * @param mid     中间位置
	 * @param end    第二段子序列结束位置
	 */
	public static void mergerArray(int [] array ,int start,int mid ,int end)
	{
		//临时数组,存储合并结果
		int [] temp = new int [end-start+1];
		//第一段子序列的元素位置标志
		int i = start;
		//第二段子序列的元素位置标志
		int j = mid + 1;
		//临时数组的元素位置标志
		int k = 0;
		//从元素标志位开始对第一段序列和第二段子序列进行扫描,比较对应元素的大小,将较小的元素存入临时数组
		while(i<=mid && j<=end)
		{
			//此处非常精妙,只有较小的数所在的序列中的元素位置标志会自增;这就不会导致较大的数被遗弃的问题。
			if(array[i]>array[j]) temp[k++] = array[j++];
			else temp[k++] = array[i++];
		}
		//上面循环完毕以后,如果有某个子序列没有扫描完毕,直接将该子序列剩余的元素存入临时数组中。
		while(i<=mid)
		{
			temp[k++] = array[i++];
		}
		
		while(j<=end)
		{
			temp[k++] = array[j++];
		}
		//将合并完成的序列复制到原数组中,从start位置开始
		for(int l = 0;l<temp.length;l++)
		{
			array[l+start] = temp[l];
		}
	}
	/**
	 * 对给定序列进行排序
	 * @param array  需要排序的数组
	 * @param start  需要排序的序列的开始位置
	 * @param end    需要排序的序列的结束位置
	 */
	public static int [] mergeSort(int [] array,int start,int end)
	{
		int mid = start + (end-start)/2;
		//当只数组元素大于1时,进行排序;如果stat = end ,即数组只有一个元素,则认为该数组是有序的,直接返回;这也是递归调用能够停止的条件。
		if(start<end)
		{
			//对前一半子序列进行排序
			mergeSort(array,start,mid);
			//对后一半子序列进行排序
			mergeSort(array,mid+1,end);
			//将排序后的序列进行合并
			mergerArray(array, start, mid, end);
		}
		return array;
	}
	/**
	 * 打印数组元素
	 * @param array
	 * @return
	 */
	public static void printArray(int [] array)
	{
		for(int temp : array)
		{
			System.out.print(" "+temp);
		}
	}
	/**
	 * 测试
	 * @param args
	 */
	public static void main(String [] args)
	{
		int [] array = {11,2,1,5,67,32,3,54,6,123,4,23,45,130,16,18,50};
		System.out.println("排序前:");
		printArray(array);
		mergeSort(array,2,14);
		System.out.println("排序后:");
		printArray(array);
	}
}

运行结果:


4.Hanio塔问题

问题:设A,B,C是三个塔座,在塔座A上一共叠有N个圆盘,这些圆盘自上而下,有小到大叠放在一起,各圆盘从小到大编号为1,2,3,...N;现要求将塔座A上的这一叠圆盘移到塔座B上,并仍然按照同样的顺序重叠;在移动圆盘时应遵守以下移动规则:

     1)每次只能移动一个圆盘;

      2)任何时刻都不允许将较大的圆盘压在较小的圆盘之上;

     3)在满足条件1)和2)的前提下,可以将圆盘移动到A、B、C中任一塔座。

解决方法:上述问题中,当N=1时,直接将A塔座上的圆盘移动到B塔座上即可;

    当N>1时,设法将A塔座上的N-1个圆盘移动到C塔座上,然后将A塔座上剩下的最大的圆盘移动到B塔座上;最后再设法在A柱的辅助下将C柱上的N-1个圆盘移动到B塔座上即可。

算法如下:

package com.caitang.algorithm.recursivepartition;
/**
 * Hanio塔问题:A,B,C三立柱;A柱上从上到下由小到大依次叠放着N个圆盘;
 * 要求:每次移动一个圆盘且圆盘叠放必须是小盘在上,大盘在上的顺序;
 * 问题:在C柱的辅助下,将A柱上的圆盘转移到B盘上去。
 * @author Administrator
 *
 */
public class HanoiAlgorithm 
{
	/**
	 * 将A柱的盘子移动到B柱上去。
	 * @param A
	 * @param B
	 */
	public static void move(int N,char A,char B)
	{
		System.out.println("将"+A+"柱上的第"+N+"个圆盘移动到"+B+"柱上");
	}
	/**
	 * 将A柱上的盘子按照移动规则移动到B柱上去
	 * @param N A柱上的盘子数,从上至下依次编号为:1,2,3,4,5,.......N
	 * @param A A柱
	 * @param B B柱
	 * @param C C柱 辅助
	 */
	public static void recursionHanio(int N,char A,char B,char C)
	{
		//如果A柱上一共有一个盘子,则直接将盘子移动到B柱上即可。这也是递归调用停止的条件。
		if(N==1)
		{
			move(N,A,B);
		}
		else
		{
			//若N>1,则需要先将上面N-1个盘子移动到C柱
			recursionHanio(N-1, A, C, B);
			//然后将A柱上的最后一个盘子(第N个盘子)移动到B柱
			move(1, A, B);
			//最后再将C柱上的N-1个盘子移动到B柱,至此,N个盘子已经成功从A柱转移到B柱
			recursionHanio(N-1, C, B, A);
		}
	}
	public static void main(String [] args)
	{
		char A = 'A';
		char B = 'B';
		char C = 'C';
		int N = 4;
		recursionHanio(N, A, B, C);
	}
}




  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值