快速排序的实现和优化

快速排序的三种实现 

快速排序是对冒泡排序的一种改进,使用了分治的思想。

基本思想:

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归 进行,以此达到整个数据变成有序序列。

算法描述:

  1. 从数列中挑出一个元素,称为"基准"(pivot)。
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

选择基准的方式: 

  1. 固定位置(一般取待排序列第一个元素或者最后一个元素)
  2. 随机选取基准(随机抽取待排序列中任意一个元素为基准)
  3. 三数取中(取待排序列首、中、尾三个元素的中位数为基准)

选取基准有以上三种方式,每种方式代表一种快速排序的实现 

第一种实现:选取待排序列首元素为基准

                      

代码实现:

 //找基准,返回第一趟排序后的基准(low)的位置
    public static int partion(int[] array,int low,int high){
         int tmp=array[low];
      //hign向前走找比tmp小的值
     while(low<high){
        while(low<high&&array[high]>=tmp){
             high--;
         }
         if(low>=high){  
             array[low] = tmp;
             break;
         }else{
             array[low] = array[high];
         }
         //low向后走找比tmp大的值
         while(low<high&&array[low]<=tmp){
             low++;
         }
         if(low>=high){   //low与high相遇,即没有比tmp大的值了,此时需把基准放在相遇的位置
            array[low] = tmp;
             break;
         }else{
             array[high] = array[low];
         }
     }
     return low;//此时low和high相遇,返回第一趟排序后的基准(low)的位置
    }

    public static void quick(int[] array,int start,int end) {
        int par = partion(array,start,end); //找基准
        //递归左边
        if(par>start+1){  //至少保证有两个数据
            quick(array,start,par-1);
        }
        //递归右边
        if(par<end-1){
            quick(array,par+1,end);
        }
    }

    public static void quickSort1( int[] array){
        quick(array,0, array.length-1);
    }

 第二种实现:随机选取待排序列中任意元素为基准 

 首先,分析选取固定位置为基准的方式,当待排序列为有序时,使用递归实现,数据量特别大时,会出现栈溢出的情况

因为当待排序列有序时(假设待排序列为1,2,3,4,5,6,7),每次的基准就是1,2,3…,当基准为1时,high从后向前找比基准小的元素,需要遍历完整个序列,没有找到(如果找到了,把找到的数放在low的位置),然后low从前向后找比基准大的元素,找到2,把2放在high的位置,以此类推,此时快速排序就相当于是冒泡排序。


再来分析一下随机选取的基准的方式,此种方式在待排序列元素全部相等的情况下,算法时间复杂度仍为O(N^2),因此,这种选取基准的方式也不是最好的。

  第三种实现:取待排序列首、中、尾三个元素的中位数为基准

 采用三数取中选取基准实现快速排序时,只需要把在每次递归调用基准之前,调用三数取中方法(medianOfThree),交换三个数的位置,把中位数放在待排序列第一个位置,即low得位置即可。

   public static void swap(int[] array,int low,int high){
        int tmp = array[low];
        array[low] = array[high];
        array[high] = tmp;
    }
   
 public static void medianOfThree(int[] array,int low,int high){
        int mid=(low+high)/2;
        //low  mid  high  3个数取中位数,
        //交换三数位置,确保low位置存放的数是中位数
        if(array[mid]>array[low]){
            swap(array,mid,low);
        }  //确定array[mid]<array[low]
        if(array[low]>array[high]){
            swap(array,low,high);
        }  //确定array[low]<array[high]
    }

public static void quick(int[] array,int start,int end) {
        //找基准之前三数取中,把中位数放在low的位置
        medianOfThree(array,start,end);
        int par = partion(array,start,end); //找基准
        //递归左边
        if(par>start+1){  //至少保证有两个数据
            quick(array,start,par-1);
        }
        //递归右边
        if(par<end-1){
            quick(array,par+1,end);
        }
    }

    public static void quickSort1( int[] array){
        quick(array,0, array.length-1);
    }

快速排序的非递归实现  

一般情况下,将递归实现转化为非递归实现,需要借助栈 

这里使用的思想就是:每趟快速排序之后,如果子序列包含两个元素以上,就把low和par-1以及par+1和high压入栈,一趟快速排序之后,就取出子序列区间,继续进行下一趟快速排序,直到栈为空,说明快速排序完成,待排序列有序。

public static void quickSort(int[] array) {
    Stack<Integer> stack = new Stack<>();
    int low = 0;
    int high = array.length-1;
    int par = partion(array,low,high);//一趟快速排序
    if(par > low+1){ //左边有两个以上的数据
        stack.push(low);
        stack.push(par-1);
    }
    if(par < high-1) {//右边有两个以上的数据
        stack.push(par+1);
        stack.push(high);
    }
    while(!stack.empty()){
        high = stack.pop();
        low = stack.pop();
        //取出子序列区间,继续进行快速排序
        par = partion(array,low,high);
        if(par > low+1){
            stack.push(low);
            stack.push(par-1);
        }
        if(par < high-1) {
            stack.push(par+1);
            stack.push(high);
        }
    }
}

 快速排序的两种优化

 第一种优化方式当待排序列分割到一定长度后,使用直接插入排序

当子序列长度小到一定范围时,假设end和start之间有16个元素(待排序列总长度为1000000),此时的子序列已经基本有序,可以使用直接插入排序,该排序算法的特点就是越有序越快

那么,如何修改代码呢?

方法很简单,只需要在每次递归找基准之前,判断子序列长度是小于16即可,如果小于,就使用直接插入排序算法

 public static void quick(int[] array,int start,int end) {
        //找基准之前三数取中,把中位数放在low的位置
        if(end-start+1<=16){
            insertSort2(array,start,end);
        }
        medianOfThree(array,start,end);
        int par = partion(array,start,end); //找基准
        //递归左边
        if(par>start+1){  //至少保证有两个数据
            quick(array,start,par-1);
        }
        //递归右边
        if(par<end-1){
            quick(array,par+1,end);
        }
    }

第二种优化方式在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割

举例:

待排序序列 1 4 6 7 6 6 7 6 8 6

三数取中选取枢轴:下标为4的数6


转换后,待分割序列:6 4 6 7 1 6 7 6 8 6         枢轴key:6

本次划分后,未对与key元素相等处理的结果:1 4 6 6 7 6 7 6 8 6

下次的两个子序列为:1 4 6 和 7 6 7 6 8 6


本次划分后,对与key元素相等处理的结果:1 4 6 6 6 6 6 7 8 7

下次的两个子序列为:1 4 和 7 8 7

经过对比,我们可以看出,在一次划分后,把与key相等的元素聚在一起,能减少迭代次数,效率会提高不少

算法效率

平均时间复杂度 最好时间复杂度 最坏时间复杂度 空间复杂度 稳定性
O(nlogn) O(nlogn)  O(N^2) O(logn)~ O(n)  不稳定

最好时间复杂度:当每次划分时,算法若都能分成两个等长的子序列时,分治算法效率达到最大

最坏时间复杂度:待排序列有序时,相当于冒泡排序,递归实现会出现栈溢出的现象,时间复杂度为O(N^2)

最好空间复杂度:每次都把待排序列分为相等的两部分,2^x=n (分割x次,保存x个par) ,x = logn

最坏空间复杂度:1 2 3 4 5 6 7   N个数据就保存N个par

发布了50 篇原创文章 · 获赞 39 · 访问量 8268

猜你喜欢

转载自blog.csdn.net/Racheil/article/details/95789531
今日推荐