快速排序
- 1.基本思想
- 2.图示详解——以升序排列为例
- 3.对基本思想和图示的补充说明
- 4.代码实现
- 5.空间、时间复杂度
- 5.1最好情况
- 5.2最坏情况
- 6.区间按照基准值划分的方法
- 6.1 Hoare法
- 6.2 挖坑法
- 7.优化措施
- 7.1三数取中法
- 7.1.1三数取中法——核心代码
- 7.1.2优化效果
- 7.1.3补充说明
- 7.2 递归到小的子区间时,使用插入排序
- 8.排序算法的非递归实现
- 8.1核心思路
- 8.2详细图解
- 8.3代码实现
- 8.3.1伪码
- 8.3.2源码
- 8.3.3测试结果
1.基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.图示详解——以升序排列为例
3.对基本思想和图示的补充说明
①1.基本思想里的任取待排序元素序列中的某元素作为基准值——这个基准值的选取是任意的,可以是给定数据中的任何一个数据。只不过为了演示方便,我们一般选举数据的第一个或最后一个值作为基准值。
②图示详解里的排序过程,我们会发现与二叉树前序遍历规则非常像。这也就是为什么说快速排序二叉树结构的交换排序方法。
4.代码实现
/*** 快速排序* @param array*/public static void quickSort(int[] array){quick(array,0,array.length -1); //quickSort()调用quick()方法是为了使参数统一为数组array[]}private static void quick(int[] array,int start,int end){if(start >= end){return; //递归结束的条件}int pivot = partitionHoare(array,start,end);//划分原数组并找出中心点pivotquick(array,start,pivot - 1);quick(array,pivot+1,end);}/*** Hoare法* @param array* @param left* @param right* @return*/private static int partitionHoare(int[] array,int left,int right){int tmp = array[left];int i = left;//i记录待排序数据的最左侧的下标值while(left < right){while(left < right && array[right] >= tmp){//right往前走直到遇到小于tmp的值right--;}while(left < right && array[left] <= tmp){//left往后走直到遇到大于tmp的值left++;}swap(array,left,right);//left、right停下时交换其处的数据值}//此时,left和right相遇,left的值就是我们要找的中心点的下标swap(array,i,left);//交换基准值和pivot下标处的值return left;}
运行结果:
注意事项:
上述代码,外层循环的控制条件为:left < right,两个内层循环也需要加上left < right。举个例子:对于一组数据1,2,3,4,5。
如果内层循环不加上left < right,则right可能会走到负数坐标位置,导致数组越界。
5.空间、时间复杂度
5.1最好情况
参考图示详解和二叉树的相关知识,如果给定一组容量为n的数据,如果每次寻找中心点 pivot,pivot的位置都在待排序的那组数据的正中间,则可以创建一棵完全二叉树,此时树的高度是log2(n+1)(向上取整),因此空间复杂度为O(log2(n))。对每一层,left和right总的遍历次数是n,因此时间总的复杂度为O(n*log2(n))。
5.2最坏情况
如果给定的一组容量为n的数据,本身已经升序排列了。则最后形成的二叉树是一个只有右子树没有左子树的二叉树,共有n层,因此空间复杂度为O(n)。第1层,left和right共遍历n次,第2层eft和right共遍历n-1次,第3层eft和right共遍历n-2次…最后left和right总共的遍历次数为n + (n-1) + (n-2) + … + 1 = n (n+1) / 2。因此总的时间复杂度为O(n^2)。
6.区间按照基准值划分的方法
将区间按照基准值划分为左右两半部分的常见方式有多种。常见的方式有:
6.1 Hoare法
Hoare法即前文所演示的方法,下面介绍挖法。
6.2 挖坑法
挖坑法图示:
核心代码:
/*** 挖坑法* @param array* @param left* @param right* @return*/private static int partitionDigPit(int[] array,int left,int right){int tmp = array[left];while(left < right){while(left < right && array[right] >= tmp){ //left往后走直到遇到大于tmp的值right--;}array[left] = array[right];while(left < right && array[left] <= tmp){ //right往前走直到遇到小于tmp的值left++;}array[right] = array[left];}//此时,left和right相遇,将tmp的值填回left处,left的值就是我们要找的中心点的下标array[left] = tmp;return left;}
7.优化措施
7.1三数取中法
由前面的演示我们知道,理想情况下,如果每次划分数据时,都能均匀地分成两组,这样最终递归而成地就是一个完全二叉树,快速排序地用时将会减少。为此,我们采用三数取中法,以寻找每次排序时的基准值。
举例如下:给定一组数据3,4,9,12,14,18,27 。如果采用快速排序进行升序排列,由前文知道,此时递归过程相当于创建了一个层高为7的单分支(只有右子树)二叉树。这是由于每次排序时,都以当前待排序数据的最左侧元素作为基准值。现采用三数取中法,调整每次排序时选取的基准值。图示如下:
7.1.1三数取中法——核心代码
private static void quick(int[] array,int start,int end){if(start >= end){return; //递归结束的条件}//1.三数取中int index = middleNumberIndex(array,start,end);//找到中间值的下标swap(array,start,index);//交换中间值和第一个元素的值int pivot = partitionHoare(array,start,end);//划分原数组并找出中心点pivot//int pivot = partitionDigPit(array,start,end);//划分原数组并找出中心点pivotquick(array,start,pivot - 1);quick(array,pivot+1,end);}
/*** 寻找给定数组的第一个元素值、最后一个元素值、中间位置的元素中的中间值,并返回其坐标* @param array* @param left* @param right* @return*/private static int middleNumberIndex(int[] array,int left,int right){int mid = left + (right - left) / 2;if(array[left] < array[right]){if(array[mid] < array[left]){return left;}else if(array[mid] > array[right]){return right;}else{return mid;}}else{if(array[mid] < array[right]){return right;}else if(array[mid] > array[left]){return left;}else{return mid;}}}
7.1.2优化效果
对一组容量为200000的数据分别初始化为升序、降序、乱序,采用三数取中法优化前后的效果图如下,测试结果的单位是毫秒(每一组测试笔者都测试了多次,这里取的是一个大概的均值):
可以看到,在本案例中,三数取中法对升序数组的排列优化效果最明显,这与我们之前的分析吻合。如果快速排序每次选取的基准值不是待排序数据的第1个,而是最后一个呢?感兴趣的读者可以自己下去研究以下。
7.1.3补充说明
三数取中法可以使我们每次对数据分割时,分割后的两组数据趋于均衡,而不是绝对均衡,绝对均衡的情况只在理想情况下才出现。但是这不妨碍三数取中法成为一个不错的优化策略,毕竟相对于不优化,三数取中法在某些情况下(对于升序、乱序数据的处理)还是具有明显的降时间效果的。
7.2 递归到小的子区间时,使用插入排序
分析:当我们递归到很小的子区间时,如果再进行递归排序的话,这个时间开销会很大。因为递归的过程包括递归进去和回退两方面,效率是比较低的,想象一下,一棵完全二叉树的最后一层节点个数占了整个二叉树节点个数的将近一般。所以此时采用的策略是:在每个小子区间上进行插入排序,这样比一直递归下去要节省时间。
8.排序算法的非递归实现
8.1核心思路
利用栈模拟递归过程,从而实现排序算法。
8.2详细图解
通过图示我们发现,整个过程是将整个数组从后往前逐渐变得有序的。
8.3代码实现
8.3.1伪码
8.3.2源码
public static void quickSortNotRecursion(int[] array){Stack<Integer> stack = new Stack<>();int left = 0;int right = array.length - 1;int pivot = partitionHoare(array,left,right);if(pivot - 1 > left){//中心点两侧有>=2个数据stack.push(left);stack.push(pivot - 1);}if(pivot + 1 < right){//中心点两侧有>=2个数据stack.push(pivot + 1);stack.push(right);}while(stack.isEmpty() == false){right = stack.pop();left = stack.pop();pivot = partitionHoare(array,left,right);if(pivot - 1 > left){//中心点两侧有>=2个数据stack.push(left);stack.push(pivot - 1);}if(pivot + 1 < right){//中心点两侧有>=2个数据stack.push(pivot + 1);stack.push(right);}}}