分治法中的合并排序和快速排序

分治法

分治法中的合并排序和快速排序_第1张图片

分治法的步骤:

  • 分:将问题分解为同一类型规模更小且最好相同的子问题;
  • 治:对这些子问题求解(一般是递归方法);
  • 合:将已解决的子问题合并,最终得出“母”问题的解。如果不合并,可以考虑用贪心法或动态规划法。

在划分阶段治:快速排序
在合并阶段治:合并排序

时间复杂度

算法运行时间的递推公式:T(n)=aT(n/b)+f(n)

一个规模为n的实例可以划分为b个规模为n/b的实例,其中a个实例是需要求解的。f(n)是一个函数,表示将问题分解成小问题和将结果合并起来所消耗的时间。(比如:a=b=2,f(n)=1)

得到以下通用的时间复杂度公式:
分治法中的合并排序和快速排序_第2张图片

具体解析参考:

  • 《算法设计与分析基础》
  • 分治法(一)
    链接:http://www.cnblogs.com/kkgreen/archive/2011/06/10/2077923.html

合并排序

伪代码:

mergesort(A[0,n-1],first,last)
//递归调用mergesort对数组A[0,n-1]进行排序
//输入:无序数组A[0,n-1],first数组起点,last数组终点
//输出:非降序数组A[0,n-1]
int mid = (first + last)/2 //初始时mid = (0 + n-1)/2
mergesort(A[],0,mid) //①左子序
mergesort(A[],mid+1,n-1) //①右子序
merge(A[],first,last) //②合并
  • ①: 分
  • ② :治、合(即算法的主要工作在合并子问题的解

时间复杂度

递推公式:n>1时,T(n)=2T(n/2)+f(n);T(1)=0
其中最坏情况下f(n)=n-1,得到:
n>1时,T w w (n)=2T(n/2)+n-1;T w w (1)=0

  • 精确解:T w w (n)=nlog 2 2 n-n+1
  • 主定理:Θ(nlogn)
  • 稳定性:稳定

JAVA代码:

注意这里的空间复杂度是:n
有的书上是在merge()合并有序数列时分配临时数组,但是过多的new操作会非常费时。因此作了下小小的变化。只在test()中new一个临时数组,后面的操作都共用这一个临时数组。

public class Main {

    public static void main(String[] args) {
        test();
    }

    /**
     * 递归合并排序
     * @param array
     * @param first
     * @param last
     * @param temp 辅助数组
     */
    public static void mergesort(int array[], int first, int last, int[] temp) {
        if(first < last){
            int mid = (first + last)/2;
            mergesort(array, first, mid, temp);//左子列排序
            mergesort(array, mid +1, last, temp);//右子列排序
            merge(array, first, last, mid, temp);//二路合并
        }
    }

    /**
     * 将两个有序数组合并为一个有序数组
     * @param array
     * @param first
     * @param last
     * @param mid
     * @param temp 辅助数组
     */
    public static void merge(int[] array, int first, int last, int mid, int[]temp) {
        int p = first;
        int q = mid + 1;
        int n = mid;
        int m = last;
        int k = 0;

        while(p<=n && q<=m){
            if(array[p] <= array[q]){
                temp[k++] = array[p++];
            }else {
                temp[k++] = array[q++];
            }
        }
        while(p <= n){
            temp[k++] = array[p++];
        }
        while(q <= m){
            temp[k++] = array[q++];
        }
        //temp赋值到array中,此时k代表temp有多少个有效元素
        for(int i=0; i/**
     * 测试用例
     */
    public static void test() {
        //int[] array = {1,2,3,4,5};
        //int[] array = {5,4,3,2,1};
        int[] array = {8,5,3,1,1,7,2,5,9,4};
        int n = array.length;
        //注:如果辅助数组在merge中,则每次合并都要new出一个
        int[] temp = new int[n];
        mergesort(array, 0, n-1, temp);
        for(int i=0; i" ");
        }
    }
}

快速排序

整体伪代码:

quicksort(A[l,r])
//递归调用quicksort对数组A[l,r]进行快速排序
//输入:数组A[0,n-1]的子数组A[l,r],由左右下标l和r定义
//输出:非降序排列的子数组A[l,r]
if(r{
    s<——Partion(A[l,r],p)//①s是分裂的位置
    quicksort(A[l,s-1])//②
    quicksort(A[s+1,r])//②
}
  • ,同时在找可以分的点。如何找到分裂的位置是关键,并且一边在找分裂位置,一边在排序(算法的主要工作在与划分阶段,而不需要再去合并子问题的解了)
  • 隐含在数组中,不需要合
  • 我们这里只选择子数组的第一个元素作为中轴点p

——>Partion()中是如何排序的呢?

不同的Partion()有不同的方法,这里讨论两种快速排序的方法。

一、霍尔(Hoare)快速排序法

根据《算法设计与分析基础》中的伪代码。

伪代码:

HoarePartion(A[l,r])
//以第一个元素作为中轴
//输入:数组A[0,n-1]的子数组A[l,r],由左右下标l和r定义
//输出:A[l,r]的一个划分,分裂点的位置作为返回值
p<——A[l]
i<——l+1
j<——r
repeat
    repeat i<——i+1 until A[i]>=p
    repeat j<——j-1 until A[j]<=p
    swap(A[i],A[j])
until i>=j //分裂点条件
swap(A[i],A[j])//撤销最后一次交换
swap(A[l],A[j])//分裂点元素交换,完成一次划分
return j

——>思想:

分别从子数组的两边进行扫描(除中轴点p,从第二个元素开始左到右扫描用指针i表示;从右到左扫面用指针j表示),当遇到A[i]大于等于中轴的元素,且遇到A[j]小于等于中轴的元素,则暂停扫描。

——>为什么需要等于呢?

当遇到相同的元素时,可以是数组分的更平均,便于减小分治问题规模。

——>i的扫描会可能会越过子数组的边界?

需要对i检查下标越界的可能性。而j不会越界,因为有中轴界限。

——>所有?

扫描暂停条件(注意与递归停止条件:l>r区别)是:A[i]大于等于中轴元素,A[j]小于等于中轴元素。接下来分为三种情况处理:

  1. 当i < j时,即i和j还未相遇,交换A[i]和A[j],然后i+1、j-1后,继续开始扫描。
  2. 当i > j时,即i和j已经相遇且交叉,一轮扫描结束,得到分裂点s=j(为什么不选i?此时A[i]比中轴元素大,A[j]比中轴元素小)。将A[j]与中轴交换以后,得到该数组的一个划分。
  3. 当i = j时,即i和j已经相遇,两边都遇到相同的轴点p才停止的,p=A[i]=A[j],即分裂点s=i=j,也得到该数组的一个划分。这里就是扫描暂停条件等于的作用了。

这里的2和3可以结合起来,只有i≥j,就交换中轴和A[j]的位置。——>递归停止条件,在代码中表示跳出循环。

JAVA代码:


public class Main {

    public static void main(String[] args) {
        test();
    }
    /**
     * 霍尔快速排序
     * @param A
     * @param l
     * @param r
     */
    public static void quicksort(int[] A,int l,int r) {
        if(l < r){
            int p = A[l];//轴点元素
            int i = l+1;
            int j = r;
            //不能写成while(i<=j)。注意i=j的情况,1、1时失效。
            //因为,i和j都指向第二个1,造成死循环。
            while(true){
                //i作为指针从左到右扫描,且不能超过j
                while(A[i] < p){
                    i++;
                    if(i >= r){
                        break;
                    }
                }
                //j作为指针从右到左扫描
                while(A[j] > p){
                    j--;
                }
                if(i < j){
                    swap(A, i, j);
                    i++;
                    j--;
                }else {
                    break;
                }
            }
            //分裂点条件
            if(i >= j){
                //j作为分裂点,A[j]与轴点元素交换
                swap(A, l, j);
                quicksort(A, l, j-1);
                quicksort(A, j+1, r);
            }
        }

    }
    /**
     * 交换数组中的元素
     */
    public static void swap(int A[], int i, int j) {
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }

    /**
     * 测试用例
     */
    public static void test() {
        //int[] array = {1,2,3,4,5};
        //int[] array = {5,4,3,2,1};
        //int[] array = {1,1};
        int[] array = {8,5,3,1,1,7,2,5,9,4};
        int n = array.length;
        quicksort(array, 0, n-1);
        for(int i=0; i" ");
        }
    }
}

时间复杂度

  • i和j扫描交叉时,键值的比较次数:n-1+2=n+1
  • i=j时,键值的比较次数:n-1+1=n

递推公式:n>1时,T(n)=2T(n/2)+f(n);T(1)=0

最好情况:f(n)=n,n>1时,T b b (n)=2T(n/2)+n-1;T b b (1)=0

  • 精确解:n=2 k k ,T b b (n)=2 k k T(1)+k2 k k =k2 k k =nlog 2 2 n
  • 主定理:Θ(nlog 2 2 n)
  • 稳定性:不稳定

最坏情况:就是已排好的升序。
T w w (n)=(n+1)+n+…+3=(n+1)(n+2)/2-3=Θ(n 2 2 )

平均情况:
T a a (n)=1.39nlog 2 2 n

二、通常的快速排序

伪代码:

 1 quicksort(A, lo, hi)
 2   if lo < hi
 3     p = partition(A, lo, hi)
 4     quicksort(A, lo, p - 1)
 5     quicksort(A, p + 1, hi)
 6 
 7 partition(A, lo, hi)
 8     pivot = A[hi]
 9     i = lo //place for swapping
10     for j = lo to hi - 1
11         if A[j] <= pivot
12             swap A[i] with A[j]
13             i = i + 1
14     swap A[i] with A[hi]
15     return i

——>思想:

首先选择表头作为中间元素temp。然后,从j开始扫描,遇到小于temp的停止扫描,将A[i](此时的i在中间元素位置,并保存在temp中)与A[j]交换,然后i++。接着,从i开始扫描,遇到大于temp的停止扫描,将A[j]与A[i]交换,然后j- -。以此类推,直到i与j交叉或相遇,将temp赋值到A[i]中。

JAVA代码:

public static void quicksort_general(int[] A,int l,int r){
         if (l < r)  
            {   
                int i = l;
                int j = r;
                int p = A[l];  
                while (i < j)  
                {  
                    //i
                    while(i < j && A[j] >= p) // 从右向左找第一个小于x的数  
                        j--;    
                    if(i < j)   
                        A[i++] = A[j];  

                    while(i < j && A[i] < p) // 从左向右找第一个大于等于x的数  
                        i++;    
                    if(i < j)   
                        A[j--] = A[i];  
                }  
                A[i] = p;  
                quicksort_general(A, l, i-1);
                quicksort_general(A, i+1, r);
            }
    }

参考资料:

  • 《算法设计与分析基础》
  • 分治法(一)
  • 白话经典算法系列之五 归并排序的实现

你可能感兴趣的:(算法)