前言:快速排序是一种分治的排序算法,他将一个数组分成两个数组,将两部分独立的排序。快速排序和归并排序是互补的:归并排序是将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序,而快速排序将数组排序的方式是当两个子数组都有序时整个数组也就有序了。
大致过程如下:
其中左边的不大于K,右边的部分不小于K。
代码实现
1 public static void sort(Comparable[] a,int start,int end) 2 { 3 if(end<=start) return; 4 int j=partition(a,start,end); 5 sort(a,start,j-1); 6 sort(a,j+1,end); 7 }
快速排序递归的将子数组a[start……end]排序,然后调用partition()方法将a[j]放到一个合适的位置,然后再递归调用将其他位置的元素排序。该方法的关键在于切分,
(1)对于某个J,a[j]已经确定,
(2)a[start……j-1]中的元素都不大于a[j];
(3) a[j+1……end]中的元素都不小于a[j];
切分的过程总能正确排序一个元素,用归纳法可以证明通过递归调用能将数组排序。
要实现这个方法,一般策略是先随意取一个a[start]作为切分元素,这个元素就是那个将要被排序的元素,然后从数组的左端向右扫描直到找到一个大于等于它的元素,从数组右端向左扫描直到找到一个小于等于它的元素这两个元素我们交换位置,依此类推
我们就可以保证指针i左边的元素不大于a[start](图上为a[lo]),指针j右边的元素不小于a[start].当两指针相遇时,我们只需要将a[start]和左子数组最右边的元素a[j]交换位置即可
切分代码实现:
1 private static int partition(Comparable[] a,int start,int end) 2 { 3 Comparable v=a[start]; 4 int i=start;; 5 int j=end+1; 6 while(true) 7 { 8 while(less(a[++i],v)) 9 if(i==end) break; 10 11 12 while(less(v,a[--j])) 13 if(j==start) break; 14 if(i>=j) break; 15 exch(a,i,j); 16 } 17 exch(a,start,j); 18 return j; 19 }
算法轨迹:
分析这段轨迹:
首先执行while(less(a[++i],v)),扫描到R,发现R大于K,此时这个循环跳出,执行while(less(v,a[--j])),扫描到S,满足,继续扫描,直到发现C小于K。这样就找到了两个元素,此时两个扫描指针没有相遇,交换两个元素位置。继续扫描,交换T和I两个元素,继续扫描,交换L和E两个元素,此时左扫描指针i=5,右扫描指针j=6,继续扫描,++i为6,--j为5,此时执行if(i>=j) break;跳出循环。接着执行 exch(a,start,j);交换切分元素的位置。
算法分析:
事实上,再算法中,j==start的条件是可以去掉的,因为切分元素就是a[start],他不可能比自己小,当j=start+1时,在执行到while(less(v,a[--j])),此时判断条件不成立,直接跳出循环。
左侧扫描我们最好要选择遇到大于等于切分元素的时候停下,右侧扫描最好是遇到小于等于切分元素的时候停下,尽管可能会交换一些不必要的值,但是再某些应用下他能避免算法的运行时间变为平方级别。例如如果不停下来,当我们用这种算法处理只有若干元素值的数组的运行时间为平方级别。
快速排序在切分方法的内循环中用一个递增或递减的索引将数组元素和一个定值比较,而归并排序比它慢(通常情况下)的一个原因就是在内循环中要移动数据。
但是这种算法也有一个缺点,如果我们的切分不平衡这个程序可能极为低效,例如第一次从最小的元素切分,第二次从第二小的元素切分。每次调用只会移除一个元素。解决的一个方法是在排序前将数组随机打乱。
算法改进:
(一)切换到插入排序。
原因:
1、对于小数组,插入排序更快。
2、因为递归,sort()方法就算在小数组中也会调用自己。
if(end<=start) return;改为if(end<=start+M){ Innertion.sort(a,start,end);} ,M一般取5~15,
(二)三取样切分
使用子数组中的一小部分元素的中位数来切分数组,这样做效果更好,但是代价是需要计算中位数。人们发现将取样大小设为3并取大小居中的元素效果最好
(三)熵最优的排序。
实际应用中在应用程序中经常出现具有大量重复元素的数组。我们的算法会使元素全部重复的子数组经常出现,在这些数组中,有可能将从线性对数级别的性能提高到到线性级别
一个想法是将数组切分为三部分,分别对应于小于,等于和大于切分元素的数组元素(有兴趣的可以百度Dijkstra提出的荷兰国旗问题),如图:
它从左到右遍历数组一次,维护一个指针lt使得a[lo……lt-1]中的元素都小于V,一个指针gt使得a[gt+1……hi]中的元素都大于V,一个指针i使得a[lt……i-1]中的元素都等于V,而a[i……gt]中的元素未确定。一开始i和lo相等,我们使用Comparable接口(非less())对a[i]进行三向比较来处理一下情况。
(1)、a[i]小于V,将a[i]与a[lt]交换,i++,lt++;
(2)、 a[i]大于V,将a[i]与a[gt]交换,gt--;i不变
(3)、a[i]等于V,i++;
代码实现
1 private static void sort0(Comparable[] a,int lo,int hi) 2 { 3 if(hi>=lo) return; 4 int lt=lo,i=lo+1,gt=hi; 5 Comparable v=a[lo]; 6 while(i<=gt) 7 { 8 int cmp=a[i].compareTo(v); 9 if(cmp<0) exch(a,lt++,i++); 10 else if(cmp>0) exch(a,i,gt--); 11 else i++; 12 13 } 14 sort0(a,lo,lt-1); 15 sort0(a,gt+1,hi); 16 }
算法轨迹
完整代码:
1 public class Quick { 2 3 private static boolean less(Comparable<Object> a,Comparable<Object> b) 4 { 5 return a.compareTo(b)<=0; 6 } 7 8 9 private static void exch(Comparable<Object> [] ary,int i,int j) 10 { 11 Comparable<Object> temp=ary[i]; 12 ary[i]=ary[j]; 13 ary[j]=temp; 14 15 } 16 17 18 19 private static int partition(Comparable[] a,int start,int end) 20 { 21 Comparable v=a[start]; 22 int i=start;; 23 int j=end+1; 24 while(true) 25 { 26 while(less(a[++i],v)) 27 if(i==end) break; 28 29 30 while(less(v,a[--j])) 31 if(j==start) break; 32 if(i>=j) break; 33 exch(a,i,j); 34 } 35 exch(a,start,j); 36 return j; 37 } 38 39 public static void sort(Comparable[] a,int start,int end) 40 { 41 if(end<=start) return; 42 int j=partition(a,start,end); 43 sort(a,start,j-1); 44 sort(a,j+1,end); 45 } 46 47 private static void sort0(Comparable[] a,int lo,int hi) 48 { 49 if(hi>=lo) return; 50 int lt=lo,i=lo+1,gt=hi; 51 Comparable v=a[lo]; 52 while(i<=gt) 53 { 54 int cmp=a[i].compareTo(v); 55 if(cmp<0) exch(a,lt++,i++); 56 else if(cmp>0) exch(a,i,gt--); 57 else i++; 58 59 } 60 sort0(a,lo,lt-1); 61 sort0(a,gt+1,hi); 62 } 63 }