分治策略在我们的排序算法中的快速排序、归并以及二分查找中经常用到,本文就来探讨一下分治策略。当然,与分治策略密不可分的还有我们经常用到的递归。
目录
一、递归基础
一个直接或间接的调用自身的算法称为递归算法。递归的使用会让我们对于问题的理解更加的清楚。但是并非所有的程序都适合用递归去进行解决。
二、分治策略基础
分治策略的设计思想:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。递归的解决这些子问题,然后将这些子问题的解合并得到原问题的解。
它缩小的是问题的规模,反复使用分治策略,可以使子问题与原问题类型一致而规模却不断缩小。在分治策略中它的主要思想就是划分,尽可能将问题的规模划分最小。
三、来看几个递归分治的例子
1、快速排序
快速排序是我们八大排序中的一种,它的主要思想就是基于划分的,通过划分,将问题的规模缩小,对子问题进行解决最后合并得到原问题的解。快排的最主要的一点就是做好划分。因此在实现快排的时候,划分部分我们有三种方法。
(1)划分部分
第一是按照通常的做法从第一个开始选取记录,进行划分;
第二是三位取中法,即就是找到原序列中的左中右三个位置的值,找到第二大的数值作为标记;
第三是随机化法,随机选取一个位置的数值,把最左边的值与其进行交换。
(2)排序部分
第一种是按照递归的做法实现排序;
第二种是非递归实现排序,在非递归实现排序的时候可以利用栈、队列、pair等容器作为支持;
具体实现代码:
//划分部分:第一个数值作为记录进行交换
template<class Type>
int Partition(Type *arr,int left,int right)
{
int i = left;
int j = right;
Type temp = arr[i];
while(i < j)
{
while(i<j && arr[j] > temp) --j;
if(i < j) arr[i] = arr[j];
while(i<j && arr[i] <= temp) ++i;
if(i < j) arr[j] = arr[i];
}
arr[i] = temp;
return i;
}
//划分部分:三位取中法
template<class Type>
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<class Type>
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<class Type>
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<class Type>
void SNicePassQuick(Type *arr,int left ,int right)
{
stack<int> 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<class Type>
void QNicePassQuick(Type *arr,int left ,int right)
{
queue<int> 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<class Type>
void PQNicePassQuick(Type *arr,int left ,int right)
{
queue<pair<int ,int>> qu;
if(left >= right) return;
pair<int,int>LR(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)
{
pair<int,int>S1(left,mid-1);
qu.push(S1);
}
if(mid+1 < LR.second)
{
pair<int,int>S2(mid+1,right);
qu.push(S2);
}
}
}
template<class Type>
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<index,在划分后的序列的左边进行继续划分;反之,在划分后的序列的右边进行继续划分。直到找到第K大。
找最小的K个数,同样也是进行划分,直到在第K个位置前的数都小于第k个位置的数,这样我们就找到了最小的K个数。
具体的实现如下:
/*查找第k小的数:将区域划分到最小,也就是当区域中只有一个数时,返回的就是该数
主要思想是划分,分治策略
*/
template<class Type>
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<class Type>
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中最大的,三种情况进行比较,找到最接近的。

具体的实现如下:
template<class Type>
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<class Type>
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);
}
当然除了这些还有像求阶乘、斐波那契数列、全排列、归并排序以及二分查找等,这些都可以利用这样的思想进行实现。
本文深入探讨了递归和分治策略,讲解了它们的基础概念和在快速排序、求K大(小)数、最接近点对问题中的应用。递归是直接或间接调用自身解决问题的方法,而分治策略则是将大问题分解为小问题解决,两者常结合使用,如在快速排序中的划分思想。此外,文章还介绍了不同类型的划分方法和递归实现方式。

2027

被折叠的 条评论
为什么被折叠?



