以前咱们介绍了交换类排序中的冒泡排序,此次咱们介绍另外一种交换类排序叫作快速排序。快速排序的优势是原地排序,不占用额外空间,时间复杂度是O(nlogn)
。java
固然,对于快速排序来讲,它也是有缺点的,它对于含有大量重复元素的数组排序效率是很是低的,时间复杂度会降为O(n^2)
。此时须要使用改进的快速排序—双路快速排序,在双路快速排序的基础上,咱们又进一步优化获得了三路快速排序。数组
快速排序的基本思想是:经过一趟排序将要排序的数据分割成独立的两部分,其中一部分的全部数据都比另一部分的全部数据都要小,而后再按此方法对这两部分数据分别进行快速排序,整个排序过程能够递归进行,以此达到整个数据变成有序序列。
快速排序的步骤以下:dom
l
指向它。v
,一部分大于v
,用j
指向小于v
和大于v
的分界点,用i
指向当前访问的元素e
,此时,数组arr[l+1...j]<v
,arr[j+1...i-1]>v
。e>v
,那么直接将e
合并在大于v
那么部分的后面,而后i++
继续比较后面的元素。e<v
,那么将e
移动到j
所指向元素的后一个元素,接着j++
,而后i++
继续比较后面的元素。v
,中间部分是>v
,右边部分是<v
。l
指向的元素和j
指向的元素交换,这样就v
这个元素进行了快速排序,v
左边元素都小于v
,右边元素都大于v
。如今咱们使用上述方法对数组[2, 1, 4, 3, 7, 8, 5, 6]
进行快速排序,下图展现了整个快速排序的过程:性能
快速排序代码:优化
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } // 递归使用快速排序,对arr[l...r]的范围进行排序 private static void sort(Comparable[] arr, int l, int r) { if (l >= r) { return; } // 对arr[l...r]部分进行partition操做, 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] int p = partition(arr, l, r); sort(arr, l, p - 1); sort(arr, p + 1, r); } private static int partition(Comparable[] arr, int l, int r) { // 最左元素做为标定点 Comparable v = arr[l]; int j = l; for (int i = l + 1; i <= r; i++) { if (arr[i].compareTo(v) < 0) { swap(arr, j + 1, i); j++; } } swap(arr, l, j); return j; }
通过上述介绍,咱们能够发现快速排序不能保证每次切分的子数组大小相等,所以就可能一边很小,一边很大。对于一个有序数组,快速排序的时间复杂度就变成了O(n^2)
,至关于树退化成了链表,下图展现了这种变化:spa
上述咱们是固定使用左边的第一个元素做为标定元素,如今咱们随机挑选一个元素做为标定元素。此时咱们第一次选中第一个元素的几率为 1/n,第二次又选中第二个元素 1/n-1,以此类推,发生以前退化成链表的几率为1/n(n-1)(n-2)....,当 n 很大时,这种几率几乎为 0。3d
另外一个优化就是对小规模数组使用插入排序,由于递归会使得小规模问题中方法的调用过于频繁,而插入排序对小规模数组排序是很是快的。code
优化的快速排序代码:blog
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } // 递归使用快速排序,对arr[l...r]的范围进行排序 private static void sort(Comparable[] arr, int l, int r) { // 对于小规模数组, 使用插入排序 if (r - l <= 15) { InsertionSort.sort(arr, l, r); return; } // 对arr[l...r]部分进行partition操做, 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] int p = partition(arr, l, r); sort(arr, l, p - 1); sort(arr, p + 1, r); } private static int partition(Comparable[] arr, int l, int r) { // 随机在arr[l...r]的范围中, 选择一个数值做为标定点pivot swap(arr, l, (int) (Math.random() * (r - l + 1)) + l); Comparable v = arr[l]; int j = l; for (int i = l + 1; i <= r; i++) { if (arr[i].compareTo(v) < 0) { swap(arr, j + 1, i); j++; } } swap(arr, l, j); return j; }
对于含有大量重复元素的数组,使用上述的快速排序效率是很是低的,由于在咱们上面的判断中,若是元素小于v
,则将元素放在<v
部分,若是元素大于等于v
,则放在>v
部分。此时,若是数组中有大量重复元素,>v
部分会变得很长,致使左右两边不均衡,性能下降。排序
双路快速排序的步骤以下:
<v
和>v
两部分放在数组的两端,用i
指向<v
部分的下一个元素,用j
指向>v
部分的前一个元素。i
开始向后遍历,若是遍历的元素e<v
,则继续向后遍历,直到遍历的元素e>=v
,则中止遍历。一样从j
开始向前遍历,若是遍历的元素e>v
,则继续向前遍历,直到遍历的元素e<=v
,则中止遍历。i
指向的元素和j
指向的元素。而后i++
,j--
继续比较下一个。双路快速排序的代码:
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } private static void sort(Comparable[] arr, int l, int r) { // 对于小规模数组, 使用插入排序 if (r - l <= 15) { InsertionSort.sort(arr, l, r); return; } int p = partition(arr, l, r); sort(arr, l, p - 1); sort(arr, p + 1, r); } private static int partition(Comparable[] arr, int l, int r) { // 随机在arr[l...r]的范围中, 选择一个数值做为标定点pivot swap(arr, l, (int) (Math.random() * (r - l + 1)) + l); Comparable v = arr[l]; int i = l + 1, j = r; while (true) { // 注意这里的边界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0 // 不加等号若是遇到相等的状况,这时候while循环就会退出,即交换i和j的值,使得对于包含大量相同元素的数组, 双方相等的数据就会交换,这样就能够必定程度保证两路的数据量平衡 // 从i开始向后遍历,若是遍历的元素e<v,则继续向后遍历,直到遍历的元素e>=v,则中止遍历 while (i <= r && arr[i].compareTo(v) < 0) { i++; } // 从j开始向前遍历,若是遍历的元素e>v,则继续向前遍历,直到遍历的元素e<=v,则中止遍历 while (j >= l + 1 && arr[j].compareTo(v) > 0) { j--; } if (i >= j) { break; } swap(arr, i, j); i++; j--; } // 此时j指向的元素是数组中最后一个小于v的元素, i指向的元素是数组中第一个大于v的元素 swap(arr, l, j); return j; }
三路快速排序的步骤以下:
v
的元素单独做为一个部分。lt
指向小于v
部分的最后一个元素,gt
指向大于v
部分的第一个元素。i
开始向后遍历,若是遍历的元素e=v
,则e
直接合并到=v
部分,而后i++
继续遍历。若是遍历的元素e<v
,则将e
和=v
部分的第一个元素(lt+1
指向的元素)交换,而后lt++
,i++
继续遍历。若是遍历的元素e>v
,则将e
和>v
部分前一个元素(gt-1
指向的元素)交换,而后gt--
,不过此时i
不须要改变,由于i
位置的元素是和gt
位置前面的空白元素交换过来的。i=gt
,而后将l
指向元素和lt
指向元素交换。<v
部分和>v
部分进行以上操做。三路快速排序相比双路快速排序的优点在于:减小了对重复元素的比较操做,由于重复元素在一次排序中就已经做为单独一部分排好了,以后只须要对不等于该重复元素的其余元素进行排序。
三路快速排序代码:
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } private static void sort(Comparable[] arr, int l, int r) { // 对于小规模数组, 使用插入排序 if (r - l <= 15) { InsertionSort.sort(arr, l, r); return; } // 随机在arr[l...r]的范围中, 选择一个数值做为标定点pivot swap(arr, l, (int) (Math.random() * (r - l + 1)) + l); Comparable v = arr[l]; int lt = l; // arr[l+1...lt] < v int gt = r + 1; // arr[gt...r] > v int i = l + 1; // arr[lt+1...i) == v while (i < gt) { if (arr[i].compareTo(v) < 0) { swap(arr, i, lt + 1); i++; lt++; } else if (arr[i].compareTo(v) > 0) { swap(arr, i, gt - 1); gt--; } else { // arr[i] == v i++; } } swap(arr, l, lt); sort(arr, l, lt - 1); sort(arr, gt, r); }
本文介绍了快速排序、快速排序的优化、双路快速排序和三路快速排序。
对于快速排序,咱们须要选择合适的标定点,使得标定点的两边平衡;在快速排序中递归到小数组时,咱们可使用插入排序替换递归,减小没必要要的开销。
对于双路快速排序和三路快速排序,咱们使用的场合是数组中存在大量重复元素。
最后,提示一下 JDK 底层的排序使用的就是插入排序 + 双路快速排序 + 归并排序的组合。