排序算法(三):快速排序和三向快速排序
快速排序
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立的排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在归并排序中,递归调用发生在处理整个数组之前,而快速排序中,递归调用发生在处理整个数组之后。
快速排序算法是最快的通用排序算法,大部分情况下,可以直接选择快速排序
如上图所示,将第一个元素K作为切分元素,使得左边的元素均不大于K,右边的元素均不小于K,这样如果左右两边都有序了,那么整个数组也就有序了。
这就是快速排序的基本原理,如果看过我前一篇关于归并排序的博客,那么很容易理解,这里需要用到递归。
先上代码:
}
public void quickSort(Integer[] a,Integer low,Integer high) { if(low >= high) return; Integer j = partion(a,low,high); quickSort(a,low,j); quickSort(a,j+1,high); }
这是递归调用的代码,我们先不管partion函数,现在只需要知道这个函数返回切分元素的下表,如上面图示的那样,返回K元素所在的下标,那么就将数组分成两个数组:
- 所有小于K的元素的数组A
- 所有大于K的元素的数组B
- 同样在A中找到一个切分元素将A分成两个子数组A'
- 同样在B中找到一个切分元素将A分成两个子数组B'
- 一直递归下去,直到不能再切分
- 当最小切分的数组有序时,再一次往回递归排序,这样最后整个数组就有序了。
说的有些乱,但是其实还是很好理解的。下面给出partion函数的代码:
public Integer partion(Integer[] a,Integer low,Integer high) { Integer i = low; Integer j = high + 1; while(true) { //在循环中最好使用++i,如果使用i++,则会导致很多逻辑错误,i++是在循环结束时(即运行到大括号时)才会++,这也是为什么前面需要使用high+1 while(a[++i]<a[low]) if(i == high) break; while(a[--j]>a[low]) if(j == low) break; if(i >= j) break; change(a,i,j); } change(a,low,j); System.out.println("low: " + low); return j; }
这个函数的作用是可以用下图说明:
1、以第一个元素为切分元素
2、从low往右找到第一个大于a[low]的元素
4、交换a[low]和a[high]
5、重复2-4,
6、最后就得到如上图第三幅图所示的结果。
使得第一个切分元素v左边的元素全部小于v,右边的元素全部大于v,并返回v的下标j。
完整代码如下:
public class QuickSort extends SortBase { /* (non-Javadoc) * @see Sort.SortBase#sort(java.lang.Integer[]) */ @Override public Integer[] sort(Integer[] a) { print("init",a); quickSort(a,0,a.length-1); print("result",a); return a; } public void quickSort(Integer[] a,Integer low,Integer high) { if(low >= high) return; Integer j = partion(a,low,high); quickSort(a,low,j); quickSort(a,j+1,high); } public Integer partion(Integer[] a,Integer low,Integer high) { Integer i = low; Integer j = high + 1; while(true) { //在循环中最好使用++i,如果使用i++,则会导致很多逻辑错误,i++是在循环结束时(即运行到大括号时)才会++,这也是为什么前面需要使用high+1 while(a[++i]<a[low]) if(i == high) break; while(a[--j]>a[low]) if(j == low) break; if(i >= j) break; change(a,i,j); } change(a,low,j); System.out.println("low: " + low); return j; } public static void main(String[] args) { Integer[] a = {2,1,5,9,0,6,8,7,3}; (new QuickSort()).sort(a); } }
平均时间复杂度NlogN
三向快速排序
实际情况中经常会出现含有大量重复元素的数组,例如我们可能需要将大量人员资料按照生日排序,或是按照性别区分。在这些情况下,快速排序算法性能尚可,但还有巨大的改进空间,这就是三向快速排序。
简单的说,三向快速排序的原理为:将数组切分成三部分们分别对应于小于、等于、
大于切分元素的数组元素。与快速排序相比,增加了一个等于切分元素的区域。
流程如下:
从做到右遍历数组一次,维护一个指针lt使得a[low…lt-1]中的元素都小于v,一个指针gt使得a[gt+1…high]中的元素都大于 v,一个指针i使得a[lt…i-1]中的元素都等于v,a[i…gt]中的元素都还为确定,一开始i和lo相等。
使得i递增,对于a[i]:
a[i]小于v,将a[lt]和a[i]交换,将lt和i加一
a[i]大于v,将a[gt]和a[i]交换,将gt减一
a[i]等于v,将i加一
最后使数组呈现下图中的情况:
实现代码:
public void quickSort3Way(Integer[] a,Integer low,Integer high) { if(low >= high) return; Integer lt = low; Integer i = low + 1; Integer gt = high; while(i<=gt) { if(a[i] < a[lt]) { change(a,i,lt); i++; lt++; } else if(a[i] > a[lt]) { change(a,i,gt); //这里不能使用i--,因为交换a[gt]和a[i]后,现在的a[i]并没有确定位置,如果使用i++,就将跳过交换后该元素的排序 gt--; } else { i++; } print("a",a); } quickSort3Way(a,low,lt-1); quickSort3Way(a,gt+1,high); }
运行结果:
init: [2 ,1 ,5 ,9 ,0 ,6 ,8 ,7 ,3] a: [1 ,2 ,5 ,9 ,0 ,6 ,8 ,7 ,3] a: [1 ,2 ,3 ,9 ,0 ,6 ,8 ,7 ,5] a: [1 ,2 ,7 ,9 ,0 ,6 ,8 ,3 ,5] a: [1 ,2 ,8 ,9 ,0 ,6 ,7 ,3 ,5] a: [1 ,2 ,6 ,9 ,0 ,8 ,7 ,3 ,5] a: [1 ,2 ,0 ,9 ,6 ,8 ,7 ,3 ,5] a: [1 ,0 ,2 ,9 ,6 ,8 ,7 ,3 ,5] a: [1 ,0 ,2 ,9 ,6 ,8 ,7 ,3 ,5] a: [0 ,1 ,2 ,9 ,6 ,8 ,7 ,3 ,5] a: [0 ,1 ,2 ,6 ,9 ,8 ,7 ,3 ,5] a: [0 ,1 ,2 ,6 ,8 ,9 ,7 ,3 ,5] a: [0 ,1 ,2 ,6 ,8 ,7 ,9 ,3 ,5] a: [0 ,1 ,2 ,6 ,8 ,7 ,3 ,9 ,5] a: [0 ,1 ,2 ,6 ,8 ,7 ,3 ,5 ,9] a: [0 ,1 ,2 ,6 ,5 ,7 ,3 ,8 ,9] a: [0 ,1 ,2 ,5 ,6 ,7 ,3 ,8 ,9] a: [0 ,1 ,2 ,5 ,6 ,3 ,7 ,8 ,9] a: [0 ,1 ,2 ,5 ,3 ,6 ,7 ,8 ,9] a: [0 ,1 ,2 ,3 ,5 ,6 ,7 ,8 ,9] a: [0 ,1 ,2 ,3 ,5 ,6 ,7 ,8 ,9] result: [0 ,1 ,2 ,3 ,5 ,6 ,7 ,8 ,9]
平均时间复杂度:介于N和NlogN之间
作者