码迷,mamicode.com
首页 > 编程语言 > 详细

排序算法之快速排序

时间:2018-05-22 22:27:37      阅读:248      评论:0      收藏:0      [点我收藏+]

标签:tps   oid   style   quick   递归调用   元素   最坏情况   bre   spl   

        基本概念

        快速排序是非常流行、应用非常广泛的排序算法,而且实现简单,适用于各种不同的输入数据,在一般应用中比其他排序算法都要快很多。快速排序是基于分治思想原地排序的排序算法,将长度为N的数组排序所需时间和NlgN成正比,而且内循环比大多数排序算法都要短小和简单,因此一般情况比其他排序算法效率高。它的缺点是非常脆弱,某些情况可能导致它的性能达到平方级别,因此实现时必须非常小心才能避免低劣的性能。
        快速排序的整体思路是根据某个元素将一个数组分成左右两个子数组,以该元素为界限,左侧子数组的值都小于或等于该元素,右侧子数组的值都大于或等于该元素(这一过程称为切分,下面会介绍)。然后递归地对数组的左右两个子数组继续进行上述操作,直到所有元素排列完成。上述每一趟切分都能排定一个元素(切分元素),如果左子数组和右子数组都是有序的,那么由左子数组、切分元素、右子数组组成的数组一定是有序的。

        切分的详细过程
        一般选取数组第一个元素为切分元素,也是那个会被排定的元素,然后定义两个指针,一个指针从数组左侧第二个元素开始向右侧遍历数组,直到找到一个大于或等于切分元素的元素时停止;另一个指针从数组右侧最后一个元素开始向左侧遍历数组,直到找到一个小于或等于切分元素的元素时停止,然后交换两个元素的位置。如此继续,直到两个指针相遇,然后将切分元素和左子数组最右侧元素交换位置,并返回交换后的切分元素位置即可。这样每一趟切分都能排定切分元素,并保证切分元素左侧的元素都小于或等于它,右侧的元素都大于或等于它。

        切分过程示意图如下:

技术分享图片

        切分过程详细代码:

    public static int partition(int[] arr,int p,int q){
        int i=p,j=q+1;  //i指针从数组左边向右遍历,j指针从数组右边向左遍历
        int val = arr[p];  //将数组第一个元素设置为切分元素
        while(true){
            while(arr[++i] < val){  //找到大于或等于切分元素的元素时停止
                if(i == q) break;
            }
            while(arr[--j] > val){  //找到小于或等于切分元素的元素时停止
                if(j == p) break;
            }
            if(i >= j) break;  //两指针相遇,终止整个循环
            int num = arr[i];  //交换i、j指针处元素,使小于切分元素的元素在左边,大于切分元素的元素在右边
            arr[i] = arr[j];
            arr[j] = num;
        }
        arr[p] = arr[j];  //将切分元素和左子数组最右侧元素交换位置
        arr[j] = val;
        
        return j;  //返回切分元素位置
    }

        有了上面每一趟切分的详细过程之后,就可以对数组递归地进行切分。
        递归过程代码如下:

    //快速排序(递归)
    public static void quickSort(int[] arr){
        if(arr == null) return;
        quickSort(arr,0,arr.length-1);
    }
    public static void quickSort(int[] arr,int p,int q){
        if(p >= q) return;
        int i = partition(arr,p,q);  //切分的详细过程,返回切分元素位置
        quickSort(arr,p,i-1);  //递归切分切分元素左侧元素
        quickSort(arr,i+1,q);  //递归切分切分元素右侧元素
    }

        快速排序完整代码:

    //快速排序(递归)
    public static void quickSort(int[] arr){
        if(arr == null) return;
        quickSort(arr,0,arr.length-1);
    }
    public static void quickSort(int[] arr,int p,int q){
        if(p >= q) return;
        int i = partition(arr,p,q);  //切分的详细过程,返回切分元素位置
        quickSort(arr,p,i-1);  //递归切分切分元素左侧元素
        quickSort(arr,i+1,q);  //递归切分切分元素右侧元素
    }
    public static int partition(int[] arr,int p,int q){
        int i=p,j=q+1;  //i指针从数组左边向右遍历,j指针从数组右边向左遍历
        int val = arr[p];  //将数组第一个元素设置为切分元素
        while(true){
            while(arr[++i] < val){  //找到大于或等于切分元素的元素时停止
                if(i == q) break;
            }
            while(arr[--j] > val){  //找到小于或等于切分元素的元素时停止
                if(j == p) break;
            }
            if(i >= j) break;  //两指针相遇,终止整个循环
            int num = arr[i];  //交换i、j指针处元素,使小于切分元素的元素在左边,大于切分元素的元素在右边
            arr[i] = arr[j];
            arr[j] = num;
        }
        arr[p] = arr[j];  //将切分元素和左子数组最右侧元素交换位置
        arr[j] = val;
        
        return j;  //返回切分元素位置
    }

        性能分析与改进
        从代码可以看出,快速排序的内循环非常简洁,元素的比较和移动次数很少,因此一般比归并排序、希尔排序等速度要快。当然,快速排序的效率取决于切分的数组是否平衡,平衡与否取决于切分元素的值。如果切分的数组是平衡的,那么快速排序算法性能与归并排序一样;如果切分的数组是不平衡的,那么快速排序的性能接近于插入排序。理想的切分元素是能将数组正好切分成两个相等的子数组,而实际中往往不是这样,两个子数组的长度往往不相等,左子数组可能比右子数组大或右子数组比左子数组大。最极端的情况是左子数组为空或右子数组为空,这种情况往往发生在数组已经有序或切分元素是最大或最小元素,这时快速排序的效率会非常低,应尽量避免这种情况发生,可以通过随机打乱数组避免这种情况发生。将长度为N的无重复数组排序,快速排序平均需要约2NlnN次比较,最极端情况最多需要约N2/2次比较。因此,快速排序在平均情况下的时间复杂度为O(NlgN),最坏情况下时间复杂度为O(N2)
        虽然快速排序已经比其他排序算法快很多了,但它依然有改进的余地。对于大多数基于递归的排序算法而言(比如快速排序或归并排序),递归产生的小数组也会调用排序算法自身,对它们使用插入排序比快速排序或归并排序要快,因此对于长度为5-15之间的小数组可以使用插入排序。对于含有大量重复元素的数组,快速排序的递归会使所有元素都相等的子数组经常出现,对这些子数组有很大的改进潜力,能使算法性能由线性对数级别提高到线性级别。改进方法通过“三向切分的快速排序”,根据切分元素将数组分为三个子数组,第一个子数组的元素都小于切分元素,第二个子数组的元素都等于切分元素,第三个子数组的元素都大于切分元素。

        与归并排序的区别
        快速排序和归并排序都是分治思想的排序算法,都可以通过递归方式进行排序。两者之间是互补的,归并排序将数组分成两个子数组分别进行排序,然后将两个有序的子数组归并,来将整个数组排序;而快速排序则是当两个子数组都有序时,整个数组自然就有序了。归并排序的递归调用发生在处理整个数组之前,将数组等分为两半;快速排序的递归调用发生在处理整个数组之后,切分的位置取决于数组的内容。两者都能使算法的时间复杂度达到线性对数级别,但归并排序中有一个辅助数组,需要额外的空间,而快速排序是原地排序,因此总体上来看,快速排序优于归并排序,一般情况下优先使用快速排序。归并排序的详细介绍见 排序算法之归并排序

        下面代码生成5000000个随机数,并复制到两个数组中(两个数组元素相同),然后分别用快速排序和归并排序算法进行排序,比较排序时间。代码如下:

package Test;

import java.util.Random;

public class Compare {
    
    //快速排序(递归)
    public static void quickSort(int[] arr){
        if(arr == null) return;
        quickSort(arr,0,arr.length-1);
    }
    public static void quickSort(int[] arr,int p,int q){
        if(p >= q) return;
        int i = partition(arr,p,q);  //切分的详细过程,返回切分元素位置
        quickSort(arr,p,i-1);  //递归切分切分元素左侧元素
        quickSort(arr,i+1,q);  //递归切分切分元素右侧元素
    }
    public static int partition(int[] arr,int p,int q){
        int i=p,j=q+1;  //i指针从数组左边向右遍历,j指针从数组右边向左遍历
        int val = arr[p];  //将数组第一个元素设置为切分元素
        while(true){
            while(arr[++i] < val){  //找到大于或等于切分元素的元素时停止
                if(i == q) break;
            }
            while(arr[--j] > val){  //找到小于或等于切分元素的元素时停止
                if(j == p) break;
            }
            if(i >= j) break;  //两指针相遇,终止整个循环
            int num = arr[i];  //交换i、j指针处元素,使小于切分元素的元素在左边,大于切分元素的元素在右边
            arr[i] = arr[j];
            arr[j] = num;
        }
        arr[p] = arr[j];  //将切分元素和左子数组最右侧元素交换位置
        arr[j] = val;
        
        return j;  //返回切分元素位置
    }
    
    //归并排序(递归,自顶向下)
    public static void mergeSort(int[] arr){  //本方法只会执行一次,下面两个方法执行多次
        if(arr == null) return;
        int[] aux = new int[arr.length];  //辅助数组
        mergeSort(arr,aux,0,arr.length-1);
    }
    public static void mergeSort(int[] arr,int[] aux,int p,int q){
        if(p>=q) return;
        int mid = (p+q)>>1;
        mergeSort(arr,aux,p,mid);  //左半块归并
        mergeSort(arr,aux,mid+1,q);  //右半块归并
        merge(arr,aux,p,mid,q);  //归并详细过程
    }
    public static void merge(int[] arr,int[] aux,int p,int mid,int q){
        for(int k=p;k<=q;k++){  //先复制到辅助数组中
            aux[k] = arr[k];
        }
        int i=p,j=mid+1;  //i、j指向辅助数组左右半块指针,从起始位置开始
        for(int k=p;k<=q;k++){  //k指向原数组arr[],根据i、j指针位置判断左右半块是否遍历完
            if(i > mid)  arr[k] = aux[j++];  //左半块遍历完
            else if(j>q) arr[k] = aux[i++];  //右半块遍历完
            else if(aux[j]>aux[i]) arr[k] = aux[i++];
            else arr[k] = aux[j++];
        }
    }
    public static void main(String[] args) {
        Random random = new Random();
        int[] arg = new int[5000000];
        for(int n=0;n<5000000;n++){  //从[0-5000000]中生成5000000个随机数
            arg[n] = random.nextInt(5000000);
        }
        int[] arg1 = new int[arg.length];
        int[] arg2 = new int[arg.length];
        for(int n=0;n<arg.length;n++){  //将随机生成的数组复制到2个数组中
            arg1[n] = arg[n];
            arg2[n] = arg[n];
        }
        System.out.println("复制完成");
        long startTime1 = System.currentTimeMillis();  //获取开始时间
        quickSort(arg1);
        long endTime1 = System.currentTimeMillis();  //获取结束时间
        long startTime2 = System.currentTimeMillis();  //获取开始时间
        mergeSort(arg2);
        long endTime2 = System.currentTimeMillis();  //获取结束时间
        
        System.out.println("快速排序执行时间:"+(endTime1-startTime1)+"ms");  //567ms 567ms 573ms 568ms 563ms
        System.out.println("归并排序执行时间:"+(endTime2-startTime2)+"ms");  //907ms 886ms 880ms 906ms 886ms
    }
}

        快速排序5次时间分别为:567ms 567ms 573ms 568ms 563ms,平均时间为567.6ms。

        归并排序5次时间分别为:907ms 886ms 880ms 906ms 886ms,平均时间为893.0ms。

        通过排序的平均时间来看,对于一般情况下的随机数组,快速排序比归并排序速度快,大约快30%-40%,因此一般情况下优先选择快速排序算法。

        转载请注明出处 https://www.cnblogs.com/Y-oung/p/9074044.html

        工作、学习、交流或有任何疑问,请联系邮箱:yy1340128046@163.com

排序算法之快速排序

标签:tps   oid   style   quick   递归调用   元素   最坏情况   bre   spl   

原文地址:https://www.cnblogs.com/Y-oung/p/9074044.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!