分治法
注:下文中的log n均表示以2为底的对数
一、概述
1、设计思想
对一个规模为n的问题,将其分解为k个规模较小的子问题,这些子问题相互独立且与原问题形式相同,递归解决这些子问题,再合并它们的解得到原问题。
2、求解过程
分解(找到基线条件)——求解子问题——合并
- 【算法实现】
divide-and-conquer(P)
{
if |P|≤n0 return adhoc(P);
将P分解为较小的子问题 P1,P2,…,Pk;
for(i=1;i<=k;i++) //循环处理k次
yi=divide-and-conquer(Pi); //递归解决Pi
return merge(y1,y2,…,yk); //合并子问题
}
//k=1,减治法
//k=2,二分法
二、排序问题
1、快速排序(quick sort)
- 【基本思想】
①取基准:任取待排序(n个元素)序列中的一个元素作为基准,一般取端点值
②分区:小于基准值的所有数字组成的子数组+基准值+大于基准值的所有数字组成的子数组
③递归(重复):对两个子数组重复以上过程直至每个子序列长度为1或0
- 【快速排序的分治策略】
①分解:将原序列a[s…t]分解成两个子序列a[s…i-1]和a[i+1…t],其中i为划分的基准位置
②求解子问题:若子序列的长度为0或为1,则它是有序的,直接返回;否则递归地求解各个子问题
③合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作
- 【算法实现】
int Partition(int a[],int s,int t) //划分算法
{
int i=s,j=t;
int tmp=a[s]; //用序列的第1个记录作为基准
while (i!=j) //从序列两端交替向中间扫描,直至i=j为止
{
while (j>i && a[j]>=tmp)
j--; //从右向左扫描,找第1个关键字小于tmp的a[j]
a[i]=a[j]; //将a[j]前移到a[i]的位置
while (i<j && a[i]<=tmp)
i++; //从左向右扫描,找第1个关键字大于tmp的a[i]
a[j]=a[i]; //将a[i]后移到a[j]的位置
}
a[i]=tmp;
return i;
}
void QuickSort(int a[],int s,int t) //对a[s..t]元素序列进行递增排序
{
if (s<t) //序列内至少存在2个元素的情况
{
int i=Partition(a,s,t);
QuickSort(a,s,i-1); //对左子序列递归排序
QuickSort(a,i+1,t); //对右子序列递归排序
}
}
- 【算法分析】
快速排序算法的平均时间复杂度(最佳情况)是O(n log2n) ,但最糟时其运行时间为O(n²)。
2、归并排序(Merge sort)
- 【基本思想】
①将a[0…n-1]看成是n个长度为1的有序表,将相邻的k(k≥2)个有序子表成对归并,得到n/k个长度为k的有序子表
②将得到的有序子表继续归并,得到 n/k² 个长度为 k² 的有序子表
③重复上述方法直至得到长度为n的有序表
- 【二路归并排序的分治策略】
循环log n(上取整,log为以2为底的对数,下同)次,length依次取1、2、…、log n。每次执行以下步骤:
① 分解:将原序列分解成length长度的若干子序列。
② 求解子问题:将相邻的两个子序列调用Merge算法合并成一个有序子序列。
③ 合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作。
【自底向上的二路归并排序算法】
自单个元素开始向上成对归并
void MergeSort(int a[],int n) //二路归并算法
{
int length;
for (length=1;length<n;length=2*length)
MergePass(a,length,n);
}
- 【算法分析】
对于上述二路归并排序算法,当有n个元素时,需要log n趟归并,每一趟归并,其元素比较次数不超过n-1,元素移动次数都是n,因此归并排序的时间复杂度为O(n log n)
【自顶向下的二路归并排序算法】
自整序列成对分解成单个序列再归并
- 【分治策略】
设归并排序的当前区间是a[low…high],则递归归并的两个步骤如下:
① 分解:将序列a[low…high]一分为二,即求mid=(low+high)/2;递归地对两个子序列a[low…mid]和a[mid+1…high]进行继续分解。其终结条件是子序列长度为1(因为一个元素的子表一定是有序表)
② 合并:与分解过程相反,将已排序的两个子序列a[low…mid]和a[mid+1…high]归并为一个有序序列a[low…high]
- 【算法实现】
void MergeSort(int a[],int low,int high) //二路归并算法
{ int mid;
if (low<high) //子序列有两个或以上元素
{
mid=(low+high)/2; //取中间位置
MergeSort(a,low,mid); //对a[low..mid]子序列排序
MergeSort(a,mid+1,high); //对a[mid+1..high]子序列排序
Merge(a,low,mid,high); //将两子序列合并,见前面的算法
}
} //递归出口为序列长度为1或0
- 【算法分析】
设MergeSort(a,0,n-1)算法的执行时间为T(n),显然Merge(a,0,n/2,n-1)的执行时间为O(n),所以得到以下递推式:
T(n)=1 当n=1
T(n)=2T(n/2)+O(n) 当n>1
容易推出,T(n)=O(n log n)
三、求解查找问题
1、查找最大和次大元素
- 【基本思路】
对无序序列a[low.high]中,采用D&C求最大元素M1和次大元素M2
①a[low.high]中只有一个元素:
M1=a[low]
M2=-INF(-∞)(要求它们是不同的元素)
②a[low.high]中只有两个元素:
M1=MAX{a[low],a[high]}
M2=MIN{a[low],a[high]}
③a[low.high]中有两个以上元素:
按中间位置mid=(low+high)/2划分为a[low…mid]和a[mid+1…high]左右两个区间
求出左区间最大元素LM1和次大元素LM2,求出右区间最大元素RM1和次大元素RM2。
合并:
若LM1>RM1,
则M1=LM1,M2=MAX{LM2,RM1};
否则M1=RM1,M2=MAX{LM1,RM2}
- 【算法实现】
void solve(int a[],int low,int high,int &M1,int &M2)
{ if (low==high) //区间只有一个元素
{
M1=a[low];
M2=-INF;
}
else if (low==high-1) //区间只有两个元素
{
M1=max(a[low],a[high]);
M2=min(a[low],a[high]);
}
else //区间有两个以上元素
{
int mid=(low+high)/2;
int LM1,LM2;
solve(a,low,mid,LM1,LM2); //左区间求LM1和LM2
int RM1,RM2;
solve(a,mid+1,high,RM1,RM2); //右区间求RM1和RM2
if (LM1>RM1)
{
M1=LM1;
M2=max(LM2,LM1); //LM2,RM1中求次大元素
}
else
{
M1=RM1;
M2=max(LM1,RM2); //LM1,RM2中求次大元素
}
}
}
- 【算法分析】
对于solve(a,0,n-1,M1,M2)调用,
其比较次数的递推式为:
T(1)=T(2)=1
T(n)=2T(n/2)+1 //合并的时间为O(1)
可以推导出T(n)=O(n)
2、折半查找
- 【基本思路】
设a[low…high]是当前的查找区间,首先确定该区
间的中点位置mid=(low+high)/2(下取整);然后将待查的k值与a[mid]比较:
① 若k==a[mid],则查找成功并返回该元素的物理下标;
② 若k<a[mid],则由表的有序性可知a[mid…high]均大于k,因此若表中存在关键字等于k的元素,则该元素必定位于左子表a[low…mid-1]中,故新的查找区间是左子表a[low…mid-1];
③ 若k>a[mid],则要查找的k必在位于右子表a[mid+1…high]中,即新的查找区间是右子表a[mid+1…high]。
下一次查找是针对新的查找区间进行的
- 【算法实现】
int BinSearch(int a[],int low,int high,int k) //拆半查找算法
{
int mid;
if (low<=high) //当前区间存在元素时
{
mid=(low+high)/2; //求查找区间的中间位置
if (a[mid]==k) //找到后返回其物理下标mid
return mid;
if (a[mid]>k) //当a[mid]>k时
return BinSearch(a,low,mid-1,k);
else //当a[mid]<k时
return BinSearch(a,mid+1,high,k);
}
else return -1; //若当前查找区间没有元素时返回-1
}
- 【算法分析】
折半查找算法的主要时间花费在元素比较上,对于含有n个元素的有序表,采用折半查找时最坏情况下的元素比较次数为C(n),则有:
C(n)=1 ——n=1
C(n)≤1+C(n/2) ——n≥2 (其中(n/2)下取整)
由此得到:C(n)≤log n+1 (log n是底为2下取整)
折半查找的主要时间花在元素比较上,所以算法的时间复杂度为O(log n)
- 【问题】
存在相同元素时怎么办?
3、查一个序列中的第k小元素
- 【基本思路】
给定的含有n元素的无序序列,求这个序列中
第k(1≤k≤n)小的元素:
设无序序列存放在a[0…n-1]中,若将a递增排序,则第k小的元素为a[k-1]。
- 【算法实现】
int QuickSelect(int a[],int s,int t,int k) //在a[s..t]序列中找第k小的元素
{
int i=s,j=t,tmp;
if (s<t)
{
tmp=a[s];
while (i!=j) //从区间两端交替向中间扫描,直至i=j为止
{
while (j>i && a[j]>=tmp) j--;
a[i]=a[j]; //将a[j]前移到a[i]的位置
while (i<j && a[i]<=tmp) i++;
a[j]=a[i]; //将a[i]后移到a[j]的位置
}
a[i]=tmp;
if (k-1==i)
return a[i];
else if (k-1<i)
return QuickSelect(a,s,i-1,k); //在左区间中递归查找
else return QuickSelect(a,i+1,t,k); //在右区间中递归查找
}
else if (s==t && s==k-1) //区间内只有一个元素且为a[k-1]
return a[k-1];
}
- 【算法分析】
对于QuickSelect(a,s,t,k)算法,设序列a中含
有n个元素,其比较次数的递推式为:
T(n)=T(n/2)+O(n)
可以推导出T(n)=O(n),这是最好的情况,即每次划分的基准恰好是中位数,
将一个序列划分为长度大致相等的两个子序列。
在最坏情况下,每次划分的基准恰好是序列中的最大值或最小值,则处理区间只比上一次减少1个元素,此时比较次数为O(n²)。
在平均情况下该算法的时间复杂度为O(n)
4、查两个等长有序序列的中位数
- 【基本思路】
两个等长有序序列的中位数是含它们所有元素的有序序列的中位数,为了方便,只讨论下取整。
用二分法求含n个有序元素的序列a、b的中位数:
分别求出a、b的中位数a[m1]和b[m2]:
① 若a[m1]=b[m2],则a[m1]或b[m2]即为所求中位数,算法结束。
② 若a[m1]<b[m2],则舍弃序列a中前半部分(较小的一半),同时舍弃序列b中后半部分(较大的一半)要求舍弃的长度相等。
③ 若a[m1]>b[m2],则舍弃序列a中后半部分(较大的一半),同时舍弃序列b中前半部分(较小的一半),要求舍弃的长度相等。
- 【算法实现】
int midnum(int a[],int s1,int t1,int b[],int s2,int t2)
{
//求两个有序序列a[s1..t1]和b[s2..t2]的中位数
int m1,m2;
if (s1==t1 && s2==t2) //两序列只有一个元素时返回较小者
return a[s1]<b[s2]?a[s1]:b[s2];
else
{
m1=(s1+t1)/2; //求a的中位数
m2=(s2+t2)/2; //求b的中位数
if (a[m1]==b[m2]) //两中位数相等时返回该中位数
return a[m1];
if (a[m1]<b[m2]) //当a[m1]<b[m2]时
{
postpart(s1,t1); //a取后半部分
prepart(s2,t2); //b取前半部分
return midnum(a,s1,t1,b,s2,t2);
}
else //当a[m1]>b[m2]时
{
prepart(s1,t1); //a取前半部分
postpart(s2,t2); //b取后半部分
return midnum(a,s1,t1,b,s2,t2);
}
}
}
- 【算法分析】
对于含有n个元素的有序序列a和b,设调用
midnum(a,0,n-1,b,0,n-1)求中位数的执行时间为T(n),显然有以下递归式:
T(n)=1 当n=1
T(n)=2T(n/2)+1 当n>1
容易推出,T(n)=O(log n)
四、求解组合问题
1、求解最大连续子序列和问题
- 【基本思路】
对于含有n个整数的序列a[0…n-1],若n=1,表示该序列仅含一个元素,如果该元素大于0,则返回该元素;否则返回0。
若n>1,采用分治法求解最大连续子序列时,取其中间位置mid=(n-1)/2(下取整),该子序列只可能出现3个地方。
①该子序列完全落在左半部即a[0…mid]中。采用递归求出其最大连续子序列和maxLeftSum。该子序列完全落在右半部即a[mid+1…n-1]中。采用递归求出其最大连续子序列和maxRightSum。
②该子序列完全落在右半部即a[mid+1…n-1]中。采用递归求出其最大连续子序列和maxRightSum。
③该子序列跨越序列a的中部而占据左右两部分。max3( maxLeftSum,
maxRightSum,
maxLeftBorderSum+maxRightBorderSum )
- 【算法实现】
long maxSubSum(int a[],int left,int right)
//求a[left..high]序列中最大连续子序列和
{
int i,j;
long maxLeftSum,maxRightSum;
long maxLeftBorderSum,leftBorderSum;
long maxRightBorderSum,rightBorderSum;
if (left==right) //子序列只有一个元素时
{
if (a[left]>0) //该元素大于0时返回它
return a[left];
else //该元素小于或等于0时返回0
return 0;
}
int mid=(left+right)/2; //求中间位置
maxLeftSum=maxSubSum(a,left,mid); //求左边
maxRightSum=maxSubSum(a,mid+1,right); //求右边
maxLeftBorderSum=0,leftBorderSum=0;
for (i=mid;i>=left;i--) //求出以左边加上a[mid]元素
{
leftBorderSum+=a[i]; //构成的序列的最大和
if (leftBorderSum>maxLeftBorderSum)
maxLeftBorderSum=leftBorderSum;
}
maxRightBorderSum=0,rightBorderSum=0;
for (j=mid+1;j<=right;j++) //求出a[mid]右边元素
{
rightBorderSum+=a[j]; //构成的序列的最大和
if (rightBorderSum>maxRightBorderSum)
maxRightBorderSum=rightBorderSum;
}
return max3(maxLeftSum,maxRightSum,
maxLeftBorderSum+maxRightBorderSum);
}
- 【算法分析】
设求解序列a[0…n-1]最大连续子序列和的执行时间
为T(n),第(1)、(2)两种情况的执行时间为T(n/2),第(3)种情
况的执行时间为O(n),所以得到以下递推式:
T(n)=1 当n=1
T(n)=2T(n/2)+n 当n>1
容易推出,T(n)=O(n log n)