排序5:快速排序

来源:互联网 发布:美国学费 知乎 编辑:程序博客网 时间:2024/05/18 02:03

题目:对于一个int数组,请编写一个快速排序算法,对数组元素排序。给定一个int数组A及数组的大小n,请返回排序后的数组。测试样例:[1,2,3,5,2,3],6 [1,2,2,3,3,5]

思路:


快速排序是使用二分思想,通过递归来实现排序的。对于一个数组,它先随机选择一个元素A将数组分成两个部分,将小于元素A的元素放到数组的A的左边,将大于A的元素放到A的右边,然后再对左右两侧的子数组分别进行递归的分割排序,递归的边界条件是当最终分割得到的数组只有一个元素,即被元素A分割得到的某个数组大小是1,那么这个大小为1的数组就直接就是结果,不用再进行递归了,对于元素数目多于1的数组继续进行分割递归排序。快速排序的关键①随机元素的选取,直接决定了排序的复杂度,但是通常选取数组的中间元素作为分界元素;②已知数组array和其中的分界元素array[k],如何将数组中小于等于array[k]的元素放在其左边而将大于array[k]的元素放在其右边?这里采取的策略是,创建一个抽象的小于等于区间{}。先将array[k]与数组的最后一个元素array[length-1]进行交换,使得分界元素位于最后面,然后假设初始的小于等于array[k]的数组smallArray[]array[0]位置的左侧,初始为空,即为{};然后使用两个指针p1,p2对数组array[]smallArray[]进行遍历,p1初始值是0p2初始值是-1,将array[p1]array[k]=A进行比较,如果array[p1]>A,那么p2保持不变,p1++,表示这个元素大于A,不能放入到左侧的{}中;如果array[p1]<A,那么说明A元素小于分界值A,可以放入到左侧的{}中,此时将p2++,表示{}要开始容纳一个元素,然后将array[p2]array[p1]进行交换,此时得到{0}5641723;然后再p1++即开始遍历下一个元素,即当遇到一个元素array[p1]<A时总是先对p2进行p2++,再将其array[p2]array[p1]进行交换,再对p1进行p1++。即快速排序也是基于交换的,需要进行nlogn次交换。

快速排序的时间复杂度O(nlogn),空间复杂度O(1),不稳定。

采用该思路失败的代码:

import java.util.*;//快速排序:借助二分法的思想,使用递归,选定中间元素,将小于该值的元素放到左边,大于等于的放到右边public class QuickSort {    public int[] quickSort(int[] A, int n) {        //特殊输入        if(A==null||A.length<=0) return A;        //调用递归方法进行二分和元素的移动,完成排序        this.adjust(A,0,A.length-1);        return A;}//写一个递归的分割方法,将数组二分,然后以此元素为界将数组进行移动,小于等于的位于左边,大于等于的位于右边    /*public void divide(int[] array,int start,int end){        //递归一定要写终止的边界条件        if(start==end) return;        //找任意一个元素作为分界点,但是通常选择中间元素作为分界点        int middle=(start+end)/2;        //对当前的数组根据中间点进行分界,使得小于等于的位于左边,大于的位于右边        this.adjust(array,start,end);        //继续对2个子数组进行二分        this.divide(array,start,middle);        this.divide(array,middle+1,end);        //调用adjust()方法将[start,end]范围内的数组以middle为界进行移动使之按照大小位于2边        this.adjust(array,start,end);    }   */     //写一个方法adjust(),将[start,end]范围内的元素按照中间值分成2个部分并调整大小,小于等于的在左,大的在右    public void adjust(int[] array,int start,int end){         //递归一定要写终止的边界条件,这里的边界条件是需要分界的数组只有一个元素        if(start==end) return;        //显然分界元素是middle        int middle=(start+end)/2;        //核心逻辑,将小于middle的元素放到左边,大于middle的元素放到右边        //①先将分界元素与最后的元素交换使得分界元素位于最后面        this.swap(array,middle,end);        //②设置指针p指向抽象的小于等于middle的数组smallArray{}456123;指针i用来遍历原始数组array,每次调整都是从0开始进行遍历调整        int p=start-1;        for(int i=start;i<=end;i++){            if(array[i]<=array[end]){                //如果元素i小于分界元素,那么就与小于等于数组区间{}的下一个元素进行交换                swap(array,++p,i);            }        }        /*        ③本次调整结束,小于等于array[middle]值的元素全部位于数组的前列,大于的位于后面,注意,middle只是位置上面的中间,并不是大小的中间值(中位数),于是调整后的数组原来的分界值并不在中间位置,只是确保该值前面的都是小于该值的,该值后面的都是大于该值的而已。于是在此之后要继续对分界后的2个子数组进行分界排序,即对2个子数组调用递归过程。注意此时的2个子数组的分界位置是调整后的分界值所在的新位置,即上面循环过后的p的位置。于是对[start,p]和[p+1,end]进行调整 *///假设执行adjust()方法后就完成了调整功能,即将[start,end]范围的元素进行了分界,不要考虑递归细节        this.adjust(array,start,p);        this.adjust(array,p+1,end);    }    //专门写一个辅助函数用来交换数组中的2个元素    public void swap(int[] array,int p1,int p2){        int temp=array[p1];        array[p1]=array[p2];        array[p2]=temp;    }}

对于快速排序,换成这种思路:

快速排序和归并排序一样,都采用分治的思想,先分再合,写一个divide(array,start,end)方法来对数组array中从startend范围的数组进行分界;显然需要递归的划分数组,要想递归的划分数组需要求得分界元素,于是在递归调用divide()之前,写一个分界函数partition()来确定[start,end]范围的分界值将数组元素进行分界返回分界后分界元素位于的新的位置p1。即在divide()方法中通过先调用partitiom()方法返回分界后的新的分界元素下标mid然后递归的调用divide(startmid)divide(mid+1,end)来进行新的分界。

即核心的逻辑是写一个partition(Astartend)方法,选择以中间位置的元素作为分界点,middle=(start+end)/2,先将这个元素与最后的元素array[end]交换位置,使得该分界元素位于数组的末尾,然后将小于等于分界值的元素移动到数组的前面,将大于等于分界值的元素移动到数组的后面部分,使用的交换策略是设置2个指针p1,p2分别从数组的startend-1开始进行遍历,p1逐步向后面移动,将元素逐个与分界值进行比较,如果小于分界值就不交换,直接p1++,直到遇到array[p1]>=分界值为止;p2从数组的end-1开始向前遍历,如果array[p2]>分界值则不交换,p2--,直到array[p2]<=分界值为止,此时array[p1]<=分界值;array[p2]>=分界值;此时将array[p1]array[p2]进行交换,依次进行,直到p1>=p2,即p1p2交错时停止,此时p1所在位置是第一个大于等于分界值的元素,将array[p1]与分界值array[end]进行交换,此时完成一轮分割,于是小于等于分界值的元素都在前面部分,大于等于分界值的元素都在后面部分,分界值的新的下标是p1,即此时[start,end]数组的分界值在p1位置(注意,这里采取的交换策略中,对于左边的指针p1,认为大于等于分界值的元素都应该移到右边;对于右边的指针p2,认为小于等于分界值的元素都应该移动到左边,即都包含等于的情况,这样可以使得结果均衡,避免出现最坏情况。)在完成了这一轮的分界之后,应该对分界后的2个子数组进行递归的分界,即已经得到了分界值p1,于是对于[start,p1][p1+1,end]要分别调用partition()方法进行分界。

与归并排序不同的是,归并排序是先递归调用divide(),再调用非递归方法merge()方法进行合并;

this.divide(array,tempArray,start,middle);

this.divide(array,tempArray,middle+1,end);

this.merge(array,tempArray,start,end);

快速排序是先调用非递归方法partition()确定分界值,再递归调用divide()进行进一步分界。

int mid = partition(A, start, end);

quick(A, start , mid);

quick(A, mid+1, end); 

注意对于递归方法,一定要有递归结束的边界条件。

注意:快速排序非常容易出错,不仅要理解,对于易出错的点还要记住解决方案,直接按照规范的操作来,不要随便写,直接避免出错就行了。

①例如如果对于区间[6,7]进行快排,那么(6+7)/2=6p1=6,p2=6,4444进行交换,之后p1=7,p2=5,结束循环,将array[p1]array[end]进行交换,即array[end]array[end]自身进行交换。相当于没有交换,于是程序陷入死循环,死递归最终出现栈溢出的错误。

这里快速排序采用的分组方式其实很简单,不需要找到之间元素后与最后的元素进行交换,在对ij进行遍历交换最后再将最后的元素更换回来并记录分界点新的位置。采用的分组策略是这样的:partition(array,start,end)方法用于对数组array[start,end]区间内的元素进行分界,注意partition()方法不是递归方法,它先找到[start,end]数组中间位置的元素值,注意时值middleValue,不需要将其与最后的元素交换,然后使用2个指针从头和尾开始向后和向前进行遍历,这里指针可以直接使用startend,比较的逻辑还是一样的,如果array[start]<middleValuestart继续向后移动,如果array[end]>middleValueend继续向前移动,当遇到array[end]<=middleValuearray[start]>=middleValue时将array[start]array[end]交换然后start++end--;直到start>=end即交错时结束交换并返回此时的start值到quick()方法中进行继续的分界,此时这个返回的start作为待分界数组的分界点,之后分别对2个子数组进行分界即可,但是这里千万千万注意,有一个麻烦的细节,在得到int middle=this.partition(array,start,end)即数组的分界点后,通过递归调用quick()方法对两个子数组进行分界,此时采取的分界方式是[start,middle-1][middle,end即为:

this.quick(array, start, middle-1);

this.quick(array, middle , end);

为什么不是用:

this.quick(array, start, middle);

this.quick(array, middle+1 , end);

进行分界:如果使用[start,middle][middle+1,end]进行分界,那么存在一种情况:对于quick(0,2)


对于[0][1][2]3个元素,是顺序排列的,交换时在start=end=1之后start=2end=0;此时返回的middle=2,即以[2]作为数组[0,2]的分界,相当于没有进行分界,于是递归调用quick(array,0,2)一直陷入死循环,死递归,最终导致栈溢出。而采用[start,middle-1][middle,end]可以避免这个问题。记住这个问题直接避免即可。

quick()是一个递归方法,它的结束的边界条件还是if(start>=end) return;

总结:快速排序方法逻辑还是很清楚直接的,和归并排序一样,需要写2个方法,quickSort(array,n)是调用者方法;写一个quick(array,start,end)方法,这是一个递归方法,用来计算int middle=partition(array,start,end);即数组[start,end]的分界点;然后递归调用quick(start,middle-1)方法和quick(array,middle,end)方法来对子数组进行分界;关键是写一个partition(array,start,end)方法,用来先找位置中间值middleVlaue,然后将数组元素分界到middleValue2边,然后返回新的分界点位置,即start的位置,将其返回到quick()方法中作为int middle即数组分界的分界点。

import java.util.*;//快速排序,使用分治思想,通过递归来分割地解决问题,关键是返回分界之后分界点的位置,以便进行下一次的递归分界public class QuickSort { public int[] quickSort(int[] A, int n) {      //特殊输入      if(A==null||A.length<=0) return A;      //调用一个递归的quick()方法来实现快速排序      this.quick(A,0,A.length-1);      return A;  }  //写一个递归方法quick()通过递归调用自己来不断分割给定的区间,假设执行quick(array,start,end)方法后数组就完成排序  public void quick(int[] array,int start,int end){      //递归方法一定要有递归结束的边界条件,本题结束的边界条件是要分割的区域只有一个元素      if(start>=end) return;      //调用partition()方法来对[start,end]范围的数组进行分界,并返回分界元素的位置下标      int middle=this.partition(array,start,end);      //递归调用divide()方法对已经得到的2个子数组进行分界,假设调用divide()方法后数组就可以对该范围完成分界//if (middle > start + 1) {不需要写,递归终止条件已经可以终止递归this.quick(array, start, middle-1);//}//if (middle<end) {不需要写,递归终止条件已经可以终止递归this.quick(array, middle, end);//}  }  //核心方法partition(),用来对[start,end]范围的数组进行分界并且返回分界值的新下标  public int partition(int[] array,int start,int end){  //先找出分界值  int middleValue=array[(start+end)/2];  //以start,end作为2个指针,分别从数组的开头和结尾向后和向前遍历数组,符合交换条件时就进行交换,不符合就继续移动,直到2个指针交错或者重合(重合时交换与不交换等价,于是是否包含=号不影响结果)  while(start<=end){  //当数组有序排列时是start和end移动可能导致越界,但可以在后面交换条件时再进行判断  while(array[start]<middleValue){  start++;  }  while(array[end]>middleValue){  end--;  }  //可以防止越界的情况  if(start<=end){  //交换2个元素的位置  this.swap(array,start,end);  start++;  end--;  }  }  //start是大于等于分界值的第一个元素,下一次就在以此分界点形成的2个子数组中进行递归分界  return start;  }  //辅助函数,专门用来交换数组中的2个元素  public void swap(int[] array,int p1,int p2){      int temp=array[p1];      array[p1]=array[p2];      array[p2]=temp;  }}

0 0