分治法-合并排序和快速排序

房东的猫 提交于 2020-03-19 12:41:37

分治法是按照以下方案工作的:

  • 将问题的实例划分为同一个问题的几个较小的实例,最好拥有同样的规模
  • 对这些较小的实例求解(一般使用递归方法,但在问题规模足够小的时候,有时会利用另一种算法以提高效率)
  • 如果必要的话,合并较小问题的解,以得到原始问题的解

分治法的流程:

clip_image001

4.1 合并排序

合并排序是成功应用分治技术的一个完美例子(书上说的)。

对于一个需要排序的数组,合并排序把它一分为二,并对每个子数组递归排序,然后把这两个排好序的子数组合并为一个有序数组。

代码实现:

/**
     * 合并排序
     * @author xiaofeig
     * @since 2015.9.16
     * @param array 要排序的数组
     * @return 返回排好序的数组
     * */
    public static int[] mergeSort(int[] array){
        if(array.length>1){
            int[] subArray1=subArray(array,0,array.length/2);
            int[] subArray2=subArray(array,array.length/2,array.length);
            subArray1=mergeSort(subArray1);
            subArray2=mergeSort(subArray2);
            return merge(subArray1,subArray2);
        }
        return array;
    }
    /**
     * 返回指定数组的子数组
     * @author xiaofeig
     * @since 2015.9.16
     * @param array 指定的数组
     * @param beginIndex 子数组的开始下标
     * @param endIndex 子数组的结束位置(不包括该元素)
     * @return 返回子数组
     * */
    public static int[] subArray(int[] array,int beginIndex,int endIndex){
        int[] result=new int[endIndex-beginIndex];
        for(int i=beginIndex;i<endIndex;i++){
            result[i-beginIndex]=array[i];
        }
        return result;
    }
    /**
     * 根据数值大小合并两个数组
     * @author xiaofeig
     * @since 2015.9.16
     * @param subArray1 待合并的数组
     * @param subArray2 待合并的数组
     * @return 返回合并好的数组
     * */
    public static int[] merge(int[] subArray1,int[] subArray2){
        int[] result=new int[subArray1.length+subArray2.length];
        int i=0,j=0;
        while(i<subArray1.length&&j<subArray2.length){
            if(subArray1[i]>subArray2[j]){
                result[i+j]=subArray2[j];
                j++;
            }else{
                result[i+j]=subArray1[i];
                i++;
            }
        }
        if(i==subArray1.length){
            while(j<subArray2.length){
                result[i+j]=subArray2[j];
                j++;
            }
        }else{
            while(i<subArray1.length){
                result[i+j]=subArray1[i];
                i++;
            }
        }
        return result;
    }

算法分析:

当n>1时,C(n)=2C(n-2)+Cmerge(n),C(1)=0

Cmerge(n)表示合并阶段进行键值比较的次数。最坏的情况下(比如,最小的元素轮流来自不同的数组),Cmerge(n)=n-1。所以:Cworst(n)=2Cworst(n-1)+(n-1),Cworst(1)=0。

因此,Cworst(n)属于θ(n*log(n))。可以求得最差效率递推式的精确解:Cworst(n)=nlog2n-n+1。

合并排序在最坏情况下的键值比较次数十分接近于任何基于比较的排序算法在理论上能够达到的最少次数。合并排序的主要缺点就是该算法需要线性的额外空间。

4.2 快速排序

快速排序是另一种基于分治技术的重要排序算法。不像合并排序是按照元素在数组中的位置对它们进行划分,快速排序按照元素的值对它们进行划分。它对于给定数组中的元素进行重新排列,以得到一个快速排序的分区。在一个分区中,所有在s下标之前的元素都小于等于A[s],所有在s下标之后的元素都大于等于A[s]。

具体实现方式:在数组中选择一个元素(中轴),选择的策略有很多种,这里可选择数组的第一个元素。分别从左向右和从右向左的扫描数组:从左向右的扫描(i表示下标)从第二个元素开始,自动忽略小于中轴的元素,直到遇到大于等于中轴的元素才会停止;从右向左的扫描(j表示下标)从最后一个元素开始,扫描自动忽略大于中轴的元素,直到遇到小于等于中轴的元素才停止。当两端的扫描都停止后,会出现两种情况(i<j或i>=j),当i<j时,交换A[i]和A[j],并继续扫描;当i>=j时,返回j作为s的值,并继续递归位于s左右两边的子数组。

代码实现:

/**
     * 合并排序
     * @author xiaofeig
     * @since 2015.9.17
     * @param array 要排序的数组
     * */
    public static void quickSort(int[] array){
        if(array.length>1){
            int s=partition(array);
            int[] leftPart=Arrays.copyOfRange(array, 0, s);
            int[] rightPart=Arrays.copyOfRange(array, s+1, array.length);
            quickSort(leftPart);
            quickSort(rightPart);
            //合并中轴元素的左右两部分,包括中轴元素
            for(int i=0;i<leftPart.length;i++){
                array[i]=leftPart[i];
            }
            for(int i=0;i<rightPart.length;i++){
                array[i+leftPart.length+1]=rightPart[i];
            }
        }
    }
    /**
     * 合并排序划分区
     * @author xiaofeig
     * @since 2015.9.17
     * @param array 要分区的数组
     * @return 返回中轴元素的最终下标
     * */
    public static int partition(int[] array){
        int i=0,j=array.length;
        do{
            do{
                i++;
            }while(i<array.length-1&&array[i]<array[0]);
            do{
                j--;
            }while(j>1&&array[j]>array[0]);
            int temp=array[i];
            array[i]=array[j];
            array[j]=temp;
        }while(i<j);
        int temp=array[i];
        array[i]=array[j];
        array[j]=temp;
        if(array[j]<=array[0]){//中轴右侧元素均大于中轴元素时无需交换
            temp=array[0];
            array[0]=array[j];
            array[j]=temp;
        }else{
            j--;//中轴右侧元素均大于中轴元素时须将j值降至0
        }
        return j;
    }

算法分析:

当n>1时,Cbest(n)=2Cbest(n/2)+n,Cbest(1)=0

所以,Cbest(n)属于θ(nlog2n);对于n=2k的情况求得Cbest(n)=nlog2n

最差的情况是输入的数组已经排过序了,如果A[0..n-1]是严格递增的数组,并且我们将A[0]作为中轴,从左到右的扫描会停在A[1]上,而从右往左的扫描会一直处理到A[0]为止(我上面的代码是扫描到A[1]),导致分列点出现在0位置上。这种情况下,键值比较的总次数应该等于:

Cworst(n)=(n+1)+n+…+3=(n+1)(n+2)/2-3,属于θ(n2)

对于大小为n的随机排列的数组,快速排序的平均键值比较次数记为Cavg(n)。假设分区的分裂点s(0<=s<=n-1)位于每个位置的概率都是1/n,得到下面的递推关系式:

当n>1时,clip_image001[1]

 

 

Cavg(0)=0,Cavg(1)=1,计算结果:Cavg(n)≈2n*ln(n)≈1.38nlog2n。

因此,快速排序在平均情况下,仅比最优情况多执行38%的比较操作。此外,它的最内层循环效率非常高,使得在处理随机排列的数组时,速度要比合并排序快

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!