分治法典型算法
分治法
二分搜索算法
基本思想
二分搜索算法是分治法的一个典型策略。
给定已经排好序的n个元素a[0 : n-1],现在要从中找出一个特定的元素x。
首先容易想到的是用顺序搜索方法,逐个比较a[0 : n-1]中元素,直到找到x或者搜索遍整个数组后确定x不在其中。这个方法没有很好地利用n个元素已经排好序这个条件,因此在最坏的情况下,顺序搜索方法需要O(n)次比较。
二分搜索方法充分利用了元素间的次序关系,采用分治策略,可以在最坏情况下用O(log n)时间找到元素x。
它的基本思想是:将这n个元素分成个数大致相同的两半,取a[n/2]与x进行比较,如果a[n/2]==x,则程序结束;如果x<a[n/2],只需要在a[0 : n/2]中再利用这种方法进行搜索;如果x>a[n/2],只需要在a[n/2 : n]中再利用这种方法进行搜索;
伪代码
public static int binarySearch(int []a,int x,int n)
{
int left=0;
int right=n-1;
while(left<=right)
{
int middle=(left+right)/2;
if(x==a[middle]) return middle;
if(x<a[middle]) right=middle-1;
else left=middle+1;
}
return -1;
}
容易看出,每执行一次算法的while循环,待搜索的数组就将减少一半。因此,在最坏的情况下,while循环被执行O(log n)次。循环体内运算需要O(1)时间,因此整个算法在最坏的情况下时间复杂性为O(log n)。
合并排序
基本思想
合并排序算法是用分治策略实现对n个元素进行排序的算法。
其基本思想是:将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并为所要求排好序的集合。下图可以很好的理解合并算法的分治思想。
注:图源网络(dreamcatcher-cx)
伪代码
public static void mergeSort(int []a,int left,int right)
{
if(left<right)
{
int mid=(left+right)/2;
mergeSort(a,left,mid-1);
mergeSort(a,mid+1,right);
merge(a,b,left,mid,right);//合并到数组b
copy(a,b,left,right);//复制回数组a
}
}
private static void merge(int[] a,int[] b,int left,int mid,int right){
int i = left;
int j = mid+1;
int t = 0;
while (i<=mid && j<=right){
if(a[i]<=a[j]){
b[t++] = a[i++];
}else {
b[t++] = a[j++];
}
}
while(i<=mid){//左边剩余元素
b[t++] = a[i++];
}
while(j<=right){//右边剩余元素
b[t++] = a[j++];
}
}
其中的merge方法,就是将一组一组子集合递归的合并到数组b。合并排序算法对n个元素进行排序,在最坏情况下所需的计算时间为T(n)满足:T(n)=2T(n/2)+O(n) n>1
求解递归方程可知T(n)=O(n log n)。但是排序算法的时间下界为Ω(n log n),故合并排序算法是渐进最优算法。
快速排序
基本思想
快速排序算法是基于分治算法的另一个排序算法。
对于输入的子数组a[p : r],按以下步骤进行排序:
- 分解:以a[p]为基准元素将a[p : r]划分为3段a[p : q-1],a[q]和a[q+1 : r],并且让a[p : q-1]中的任何元素都小于等于a[q],a[q+1 : r]中的任何元素都大于等于a[q]。通俗的来说就是,小于基准的放左边,大于基准的放右边。
- 递归求解:通过递归的调用快排算法,分别对a[p : q-1],a[q+1 : r]两段递归调用算法。
- 合并:对于a[p : q-1],a[q+1 : r]的排序是就地进行的,所以在a[p : q-1],a[q+1 : r]排好序后不需要执行任何计算,原数组就排好序。
伪代码
public static void qSort(int p,int r)
{
if(p<r)
{
int q=partition(p,r);
qSort(p,q-1);
qSort(q+1,r);
}
}
public static int partition(int p,int r)
{
int left=p;
int right=r;
int x=a[left];
while(left<right)
{
while(left<right&&a[right]>=x)
right--;
if(left<right)
a[left]=a[right];
while(left<right&&a[left]<=x)
left--;
if(left<right)
a[right]=a[left];
}
a[left]=x;
return left;
}
算法的关键在于partition中以确定的基准元素a[p]进行划分。partition方法每次都以x=a[p]为基准,然后左边left和右边right移位,右边开始,如果a[right]比基准元素x大或者等于,那么right继续移位;如果a[right]<x,那么则将当前的a[right]赋值到a[left],因为left此时并未开始移位,未动,因此是就地排序。赋值完成后left就开始移动,同理。
快速排序最差情况划分过程产生的两个区域分别包含n-1个元素和1个元素,并且每一次都出现这种不对称的划分,则有T(n)=T(n-1)+O(n);解得最坏情况下T(n)=O(n^2);
快排最好和平均情况下的时间复杂度都是O(n log n);
快速排序算法是不稳定的算法。
线性时间选择
基本思想
元素选择的问题一般是:给定线性序列有n个元素,一个整数k,找到这n个元素中第k小的元素。
线性时间选择算法实际上是模仿快速排序算法设计出来的。其基本思想也是对输入数组进行递归划分。随机选择一个下标 i 作为基准 a[i],将小于 a[i] 的放左边,大于 a[i] 的放在右边。j 为划分后左边有的元素个数,这样只需要比较k和j的大小,如果k <= j,那么说明第k小的元素一定在基准左边,接下来就只需要对左半部分递归找第 k 小的元素即可;同理,如果k>j,说明,第k小的元素在基准右边,那么就对右半部分递归找第k-j小的元素即可。
伪代码
public static int randomizedSelect(int p,int r,int k)
{
int i=randomizedPartition(p,r);
int j=i-p+1;
if(k<=j)
return randomizedSelect(p,i,k);
else return randomizedSelect(i+1,r,k-j)
}
public static int randomizedPartition(int p,int r)
{
int i=random(p,r);
MyMath.swap(a,i,p);
return partiton(p,r);
}
可以看出,最坏情况下randomizedSelect需要Ω(n^2)计算时间,这里的partition函数同快速排序算法的partition函数是一致的。由于随机划分算法randomizedPartition使用了随机数产生器random,它能随机的产生p和r之间的一个随机整数,因此,randomizedPartition产生的划分基准是随机的,在这个条件下,可以证明,算法randomizedSelect可以在O(n)平均时间内找出n个输入元素中第k小的数。
最大子段和
问题描述:
给定由n个整数(可负)组成的序列a1 ,a2 ,…,an,求该序列形如∑ak的子段和的最大值。当所有整数均为负整数时定义其最大子段和为0。 依此定义,所求的最优值为
例如,当(a1 ,a2 ,…,a6 )= (-2, 11, -4, 13, -5, -2)时,最大子段和为 20 ( 11, -4, 13)。
基本思想
如果将所给的序列a[1 : n]分为长度相同的两段a[1 : n/2]、a[n/2+1 : n],分别求出这两段的最大子段和,则a[1 : n]的最大子段和分为三种情况:
- a[1 : n]的最大子段和同a[1 : n/2]的最大子段和相同。
- a[1 : n]的最大子段和同a[n/2 : n]的最大子段和相同。
- a[1 : n]的最大子段和等于位于a[1 : n/2]的子段和和位于a[n/2+1 : n]的子段和的和
1和2的情况可以直接递归求得,第3种情况,a[n/2]和a[n/2+1]在最优子序列之中,我们只需要计算出左边a[1 : n/2]从n/2开始的最大子段和s1,右边a[ n/2+1 : n]从n/2+1开始的最大子段和,然后将两者相加s=s1+s2;s即为第3种情况的最优值。
伪代码
public int MaxSubSum(int []a,int left,int right)
{
if(left==right)
return a[left]>0?a[left]:sum;
else
{
int mid=(left+right)/2;
int leftSum=MaxSubSum(a,left,mid);
int rightSum=MaxSubSum(a,mid+1,right);
int s1=0;
int tempS=0;
for(int i=mid;i>=left;i--)
{
tempS+=a[i];
if(tempS>s1)
s1=tempS;
}
int s2=0;
tempS=0;
for(int i=mid+1;i<=right;i++)
{
tempS+=a[i];
if(tempS>s2)
s2=tempS;
}
//返回三者中最大的
return max(leftSum,rightSum,s1+s2);
}
}
T(n)=2T(n/2)+O(n),因此时间复杂度为O(n log n)