【算法】递归和分治策略

分治策略在我们的排序算法中的快速排序、归并以及二分查找中经常用到,本文就来探讨一下分治策略。当然,与分治策略密不可分的还有我们经常用到的递归。

目录

一、递归基础

二、分治策略基础

三、来看几个递归分治的例子

         1、快速排序 

2、求第K大(小)/最小(大)的K个数

3、最接近点对问题


一、递归基础

一个直接或间接的调用自身的算法称为递归算法。递归的使用会让我们对于问题的理解更加的清楚。但是并非所有的程序都适合用递归去进行解决。

二、分治策略基础

分治策略的设计思想:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。递归的解决这些子问题,然后将这些子问题的解合并得到原问题的解。

它缩小的是问题的规模,反复使用分治策略,可以使子问题与原问题类型一致而规模却不断缩小。在分治策略中它的主要思想就是划分,尽可能将问题的规模划分最小。

三、来看几个递归分治的例子

         1、快速排序 

         快速排序是我们八大排序中的一种,它的主要思想就是基于划分的,通过划分,将问题的规模缩小,对子问题进行解决最后合并得到原问题的解。快排的最主要的一点就是做好划分。因此在实现快排的时候,划分部分我们有三种方法。

(1)划分部分

         第一是按照通常的做法从第一个开始选取记录,进行划分;

         第二是三位取中法,即就是找到原序列中的左中右三个位置的值,找到第二大的数值作为标记;

         第三是随机化法,随机选取一个位置的数值,把最左边的值与其进行交换。

(2)排序部分

         第一种是按照递归的做法实现排序;

         第二种是非递归实现排序,在非递归实现排序的时候可以利用栈、队列、pair等容器作为支持;

具体实现代码:

//划分部分:第一个数值作为记录进行交换
template
int Partition(Type *arr,int left,int right)
{
	int i = left;
	int j = right;
	Type temp = arr[i];
	while(i < j)
	{
		while(i temp) --j;
		if(i < j) arr[i] = arr[j];
		while(i
int MidPartition(Type *arr,int left,int right)
{
	int mid = (right - left + 1)/2+left;
	//arr[left],arr[mid],arr[right]
	if(arr[mid] > arr[left] && arr[mid] < arr[right])
	{
		swap(arr[left],arr[mid]);
	}else if(arr[right] > arr[m] && arr[right] < arr[left])
	{
		swap(arr[left],arr[right]);
	}
	return Partition(arr,left,right);
}
//划分部分:随机化法
template
int RandPartition(Type *arr,int left,int right)
{
	srand(time());
	int rpos = rand() % (right - left + 1)+left;
	swap(arr[left],arr[rpos]);
	return Partition(arr,left,right);
}
//递归实现
template
void PassQuick(Type *arr,int left ,int right)
{
	if(left < right)
	{
		int mid = Partition(arr,left,right);
		PassQuick(arr,left,mid-1);
		PassQuick(arr,mid+1,right);
	}
}
//非递归实现,利用栈
template
void SNicePassQuick(Type *arr,int left ,int right)
{
	stack st;
	if(left >= right) return;
	st.push(left);
	st.push(right);
	while(!st.empty())
	{
		right = st.top();st.pop();
		left = st.top();st.pop();
		int mid = Partition(arr,left,right);
		if(left < mid -1)
		{
			st.push(left);
			st.push(mid-1);
		}
		if(mid+1 < right)
		{
			st.push(mid+1);
			st.push(left);
		}
	}
}
//非递归实现,利用队列
template
void QNicePassQuick(Type *arr,int left ,int right)
{
	queue qu;
	if(left >= right) return;
	qu.push(left);
	qu.push(right);
	while(!qu.empty())
	{
		left = qu.front();qu.pop();
		right = qu.front();qu.pop();
		
		int mid = Partition(arr,left,right);
		if(left < mid -1)
		{
			qu.push(left);
			qu.push(mid-1);
		}
		if(mid+1 < right)
		{
			qu.push(mid+1);
			qu.push(left);
		}
	}
}
//非递归实现,利用队列中的pair
template
void PQNicePassQuick(Type *arr,int left ,int right)
{
	queue> qu;
	if(left >= right) return;
	pairLR(left,right);
	qu.push(LR);
	while(!qu.empty())
	{
		LR = qu.front();qu.pop();
		int mid = Partition(arr,LR.first,LR.second);
		if(LR.first < mid -1)
		{
			pairS1(left,mid-1);
			qu.push(S1);
		}
		if(mid+1 < LR.second)
		{
			pairS2(mid+1,right);
			qu.push(S2);
		}
	}
}
template
void QuickSort(Type *arr,int n)
{
	//PassQuick(arr,0,n-1);
	//SNicePassQuick(arr,0,n-1);
	//QNicePassQuick(arr,0,n-1);
	PQNicePassQuick(arr,0,n-1);
}

2、求第K大(小)/最小(大)的K个数

这类题目的思想,当然也是我们的分治+递归。

例如,求第K大,对原序列进行划分,第一划分后得到以下下标index,然后利用K和index进行比较,如果K

找最小的K个数,同样也是进行划分,直到在第K个位置前的数都小于第k个位置的数,这样我们就找到了最小的K个数。

具体的实现如下:

/*查找第k小的数:将区域划分到最小,也就是当区域中只有一个数时,返回的就是该数
主要思想是划分,分治策略
*/
template
const Type &Select_K(Type *arr,int left, int right,int k)//k指的时第k小
{
	if(left == right && k ==1) return arr[left];
	int pos = Partition(arr,left,right);
	int j = pos - left + 1;//划分后得到当前区域的第j小
	if(k <= j)//当要找的第k小的k值小于等于一次划分后得到的第j小的j,那么就在j位置之前的范围寻找
	{
		return Select_K(arr,left,pos,k);
	}
	else //当要找的第k小的k值大于划分后得到的第j小的j,那么就在j位置之后的范围寻找,
		//下次寻找时,总体的第k小就是后部分的第k-j小的值
		return Select_K(arr,pos+1,right,k-j);
}
template
const Type &Select_K_Min(Type *arr,int n,int k)
{
	assert(arr!= NULL && n > 0 && k >= 1 && k <= n);
	return Select_K(arr,0,n-1,k);
}

3、最接近点对问题

问题的提法:给定水平面上的n个点,找其中一对点,使得在n个点所组成的所有点对中,该点对间的距离最小。

这里我们可以将它映射为一维数组上相差最小的两个值。这看起来很简单将其排序然后一次线性扫描就能找到。

二维的情况下,假设用x轴上某个点m将S划分为两个集合S1和S2,最接近点对有可能在s1中,也有可能在s2中,还有可能在s1和s2之间,对于第三种情况找到s1中最小的,找到s2中最大的,三种情况进行比较,找到最接近的。

【算法】递归和分治策略_第1张图片

具体的实现如下:

template
const Type &Select_K(Type *arr,int left, int right,int k)//k指的时第k小
{
	if(left == right && k ==1) return arr[left];
	int pos = Partition(arr,left,right);
	int j = pos - left + 1;//划分后得到当前区域的第j小
	if(k <= j)//当要找的第k小的k值小于等于一次划分后得到的第j小的j,那么就在j位置之前的范围寻找
	{
		return Select_K(arr,left,pos,k);
	}
	else //当要找的第k小的k值大于划分后得到的第j小的j,那么就在j位置之后的范围寻找,
		//下次寻找时,总体的第k小就是后部分的第k-j小的值
		return Select_K(arr,pos+1,right,k-j);
}
template
const Type &Select_K_Min(Type *arr,int n,int k)
{
	assert(arr!= NULL && n > 0 && k >= 1 && k <= n);
	return Select_K(arr,0,n-1,k);
}
/*
最接近点对问题:
(    S1(在这里有一个最短的距离d1))中间线( S2  (在这里有一个最短的距离d2) )
									s1的最大值和s2的最小值之间的距离大小
*/
int MaxS1(int *arr,int left,int right)
{
	return arr[right];
}
int MinS2(int*arr,int left,int right)
{
	int tmp = arr[left];
	for(int i = left + 1;i <= right;++i)
	{
		if(tmp > arr[i])
		{
			tmp = arr[i];
		}
	}
	return tmp;
}
/*
最接近点对问题
*/
int CPair(int *arr,int left,int right)
{
	if(right-left <= 1) return INT_MAX;
	int k = (right - left + 1)/2;
	Select_K(arr,left,right,k);
	int pos = left + k -1;
	int d1 = CPair(arr,left,pos);
	int d2 = CPair(arr,pos+1,right);
	int maxs1 = MaxS1(arr,left,pos);
	int mins2 = MinS2(arr,pos+1,right);
	return min(min(d1,d2),mins2-maxs1);
}
int Cpair(int *arr,int n)
{
	return CPair(arr,0,n-1);
}

当然除了这些还有像求阶乘、斐波那契数列、全排列、归并排序以及二分查找等,这些都可以利用这样的思想进行实现。

你可能感兴趣的:(高级数据结构和算法)