分治法典型算法

分治法

二分搜索算法

基本思想

二分搜索算法是分治法的一个典型策略。

给定已经排好序的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],按以下步骤进行排序:

  1. 分解:以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]。通俗的来说就是,小于基准的放左边,大于基准的放右边。
  2. 递归求解:通过递归的调用快排算法,分别对a[p : q-1],a[q+1 : r]两段递归调用算法。
  3. 合并:对于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]的最大子段和分为三种情况:

  1. a[1 : n]的最大子段和同a[1 : n/2]的最大子段和相同。
  2. a[1 : n]的最大子段和同a[n/2 : n]的最大子段和相同。
  3. 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)

posted @ 2021-11-08 09:34  ins1mnia  阅读(679)  评论(0编辑  收藏  举报