Java 快速排序
算法介绍
-
一种很快的排序和其名字一样
-
和希尔排序的思想类似,每次排序都是对整体进行相对有序化,着手处理整体的顺序.
-
将序列分为无数个小序列,最终每个小序列都相对有序
实现步骤
- 寻找一个基准值,然后比基准值小的放到基准值的左边,比基准值大的放到基准值的右边
- 执行完上述操作之后,我们以基准值进行分割,将数组分为两部分。
- 每一个部分都当成一个独立的数组,分别找出基准值,都执行上述操作操作
- 重复流程直至序列有序
看上去不难不难理解,我们给出一个图例
1.整理序列
如图所示的序列,我们找一个基准值,比如array.length/2也就是图中的0的位置。
接下来我们维护两个指针,分别指向数组的一位和最后一位。
如果left大于0,说明我们准备要把left所指向的值放到基准值的右边,这时我们在基准值右边的一个数字,来与之交换,我们判断right指向的是不是小于0,只要不小于0,我们让指针左移。
我们之前的流程是先找left找到了再去找right,所以这里我们继续寻找比0大的,移动left指针
这时我们找到了7大于0,我们移动right,我们发现right的位置以及到达了base的位置,而且我们right是为了表示base右边的值,如果我们的right可以跑到base左边显然不合理。
这里我们有个操作,交换一下left和base,因为right既然能到达base的地方,说明base的右边已经排好了,但是left的位置还没有排好,但是left的左边也是排好的,所以我们这个操作的目的是让漏掉的元素都能参与排序
第一步结束了,我们执行第二步
2.分割序列
按照算法的要求我们按照基准值分割序列,我们按照基准值0进行分割序列
每一个子序列都进行快速排序
左边的基准值为-2,右边的为1,最终排序结果
再分割,由于-2是基准,-2的右边为空,左边有一个,则右边没法分,左边分来
排序
分割,当所有的子序列长度都是1,则停止排序
通过上述流程我们得到几个规律
- 当子序列长度为2时,我们拆分之后的部分无需再排序了
- 如果基准值在头部或者尾部,比如在头部,那么左边没有序列不需要分割,右边进行分割
- 基准不需要再进行排序,因为基准的左边小于基准了,右边大于基准,只要左右都排好了,基准的顺序自然正确
1.整理(代码实现)
public class QuickSort implements SortAlgorithm {
@Override
public void sort(int[] array) {
//第一次排序
int left=0;
int right=array.length-1;
doQuickSort(array,left,right);
}
public void doQuickSort(int[] array,int start,int end){
int left=start;
int right=end;
//计算基准值
int base=(right+left)/2;
while (left<right){
//直到left=base或者找到一个比基准值大的
while (left<base && array[left]<=array[base]){
left++;
}
//直到base=right或者找到一个比基准值小的
while (right>base && array[right]>=array[base]){
right--;
}
//判断是否达到了基准值,如果达到,下次交换操作的时候基准值也应该相应的跟着变
if (base==left){
base=right;
}
else if (base==right){
base=left;
}
else{
base=base;
}
//交换操作
//这个方法是SortAlgorithm接口的default方法,就是简单的交换操作
reverse(array,left,right);
}
}
}
测试代码
//测试数组
int[] ints = {12,76,23,11,47,98,23,75,23};
new QuickSort().sort(ints);
System.out.println(Arrays.toString(ints));
运行结果
我们看到比47小的在47左边,比47大的在47右边
[12, 23, 23, 11, 23, 47, 98, 75, 76]
2.拆分(代码实现)
我们知道有几种特殊情况
假设我们的序列为array,基准值为base,序列的的最左边的元素的下标为left,最右边为left,那么我们的子序列array1为{left,base-1},array
2为{base+1,right};
如果base+1=right,或者base+1>right这种子序列直接卡掉
更新代码
public void doQuickSort(int[] array,int start,int end){
int left=start;
int right=end;
//程序出口
if (left>=right){
return;
}
int base=(right+left)/2;
while (left<right){
while (left<base && array[left]<=array[base]){
left++;
}
while (right>base && array[right]>=array[base]){
right--;
}
if (base==left){
base=right;
}
else if (base==right){
base=left;
}
else{
base=base;
}
reverse(array,left,right);
}
//计算子序列
doQuickSort(array,start,base-1);
doQuickSort(array,base+1,end);
}
}
运行结果
[11, 12, 23, 23, 23, 47, 75, 76, 98]
我们的排序算法写好了,但是我们折中的方式,有一定的不好理解,这里我们再给出一种基准值的思路
方式2
我们把第一位作为基准值,然后定义一个变量保存这个值,依旧维护两个指标left和right。
我们的基准值为第一个元素,我们从right开始,寻找比1小的数字,我们找到了-2
接下来的操作很厉害,就是-2去覆盖1,也就是right覆盖left,然后right指针不动,改为移动left,寻找比1要小的值,刚好-5就是了!
-5覆盖-2,也就是left覆盖right,right开始移动,按照如上流程我们快速的运行一遍,当我们left>=right作为循环的出口,最后我们还要把基准值加入到left=right的位置
代码实现
- 如何在覆盖之后改为让另一个指针运行起来?
直接上代码
public class QuickSortPro implements SortAlgorithm {
@Override
public void sort(int[] array) {
recursive(array,0,array.length-1);
}
public void recursive(int[] array,int left,int right){
//程序出口
if(left>=right){
return;
}
int start=left;
int end=right;
int tmp=array[start];
//通过维护一个flag标记来实现交替操作
boolean flag=true;
while(start<end){
if(flag){
if(array[end]>tmp){
end--;
}
else {
array[start]=array[end];
start++;
flag= false;
}
}
else {
if (array[start]<tmp){
start++;
}
else {
array[end]=array[start];
end--;
flag=true;
}
}
}
//循环结束,此时start=end,我们用之前保存的基准值覆盖array[start]
array[start]=tmp;
recursive(array,left,start-1);
recursive(array,start+1,right);
}
}
运行结果
[11, 12, 23, 23, 23, 47, 75, 76, 98]
总结
- 学会选择基准值,让在基准值左右满足交换条件的元素不同的情况下,能对基准值的位置继续调整
- 判断哪些子序列是没有意义的
- 学会用boolean指针来实现一个交替操作的案例
彩蛋
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
//方式1的时间测试
SortUtils.timeComplexityTest(QuickSort.class);
//方式2时间测试
SortUtils.timeComplexityTest(QuickSortPro.class);
}
运行结果
开始计算....
QuickSort:26ms
开始计算....
QuickSortPro:14ms
第二种覆盖的方式比折中的方式快了一倍。