快排
快排思想:
- 从序列中挑选出一个元素(一般是第一个或者是最后一个)作为"基准"元素
- 把序列分成2个部分,其数值大于"基准"元素的元素放在"基准"元素的右边,否在放在"基准"元
素的左边,此时"基准"元素所在的位置就是正确的排序位置,这个过程被称为 partition(分区) - 递归将"基准"元素左边的序列和"基准"元素右边的序列进行partition操作
其中partiton即划分,方法有交换法和挖坑填数法
法一:分治+挖坑填数
代码: 包括合并形式和分开形式
public int[] sort(int[] arr){
quickSort(arr,0,arr.length-1);
return arr;
}
public void quickSort(int[] arr,int start,int end){
if (start<end){
int i = start;
int j = end;
int temp = arr[start];
//partition划分-挖坑填数
while (i<j){
while (i<j && arr[j]>=temp) j--;
if (i<j) arr[i] = arr[j];
while (i<j && arr[i]<temp) i++;
if (i<j) arr[j] = arr[i];
}
arr[i] = temp;
//分治
quickSort(arr,start,i-1);
quickSort(arr,i+1,end);
}
}
//分开形式
public void quick_sort(int[] arr,int start,int end){
if (start<end){
//划分
int mid = partition(arr,start,end);
//分治
quick_sort(arr,start,mid-1);
quick_sort(arr,mid+1,end);
}
}
public int partition(int[] arr,int start,int end){
int temp = arr[start];
int i = start;
int j = end;
while(i<j){
//找到右侧小于基准的值
while (i<j && arr[j]>=temp){
j--;
}
if (i<j){
//小的放到左边(填坑-产生新坑)
arr[i] = arr[j];
i++;
}
//找到左侧侧大于基准的值
while (i<j && arr[i]<temp){
i++;
}
if (i<j){
//大的放到右边(填坑-产生新坑)
arr[j] = arr[i];
j--;
}
}
//最终i、j重合留下最后一个坑,将temp填入
arr[j] = temp;
//将基准值索引返回
return j;
}
注意到划分代码,两种形式的if (i<j)里有的进行了i++、j–,另一个没有,其实是一样的,不加自增自减,下一次会多一次判断而已,不影响结果
法二: 分治+交换
思路:
代码:
public int[] sort(int[] arr){
quickSort(arr,0,arr.length-1);
return arr;
}
public void quickSort_swap(int[] arr,int start,int end){
if (start<end){
int i = start;
int j = end;
int temp = arr[start];
while (i<j){
while (i<j && arr[j]>=temp) j--;
while (i<j && arr[i]<=temp) i++;
swap(arr,i,j);
}
//先从右边往左寻找,可以保证最后i、j相遇的值一定是小于等于基准值,
// 这样最后交换才不会出现错误,如果从左边先开始,最后会把大的换到左侧
//所以要从基准值对面先开始
swap(arr,start,i);
//分治
quickSort_swap(arr,start,i-1);
quickSort_swap(arr,i+1,end);
}
}
public void swap(int[] arr ,int i ,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
参考:面试必备:八种排序算法原理及Java实现
参考:排序可视化
随机数快排
思路:
随机从start-end间选择一个与start交换,避免出现已排序数组利用快排时间复杂度成最坏情况O(n^2)
public int[] sort(int[] arr){
quickSort(arr,0,arr.length-1);
return arr;
}
public void quickSort(int[] arr,int start,int end){
if (start<end){
int i = start;
int j = end;
//随机数
int random_index = start + (int)(Math.random()*(end-start+1));
//random_index与start交换
swap(arr,random_index,start);
int temp = arr[start];
//partition划分-挖坑填数
while (i<j){
while (i<j && arr[j]>=temp) j--;
if (i<j) arr[i] = arr[j];
while (i<j && arr[i]<temp) i++;
if (i<j) arr[j] = arr[i];
}
arr[i] = temp;
//分治
quickSort(arr,start,i-1);
quickSort(arr,i+1,end);
}
}
public void swap(int[] arr ,int i ,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
三分区快排-解决包含大量重复值情况
前面的路子都是以基准值为准分为小于子序列和大于子序列,考虑一种特殊的数据集,数据集中有大量重复元素,这种情况下使用两分区递归会对大量重复元素进行处理。
一个优化的方向就是使用三分区模式:小于区间、等于区间、大于区间,这样在后续的处理中则只需要处理小于区和大于区,降低了等于基准值区间元素的重复处理,加速排序过程。
三分区原理
如图为三分区模式中某个时刻的快照,其中展示了几个关键点和区间,包括基准值、小于区、等于区、处理值、待处理区、大于区。
在实际过程中根据处理值与基准值的大小关系,进行相应分区合并和交换,再进行下标移动就可以了,实际中分三种情况,这也是写代码的依据:
- 处理值e==p,将e合并到等于区,i++;
- 处理值e<p,将e与(lt+1)位置的数据交换,扩展小于区lt++,等于区长度不变,相当于整体平移;
- 处理值e>p,将e与(gt-1)位置的数据交换,扩展大于区gt–,此时i不变,交换后的值是之前待处理区的尾部数据;
- 分区最终调整
处理完待处理区的全部数据之后的调整也非常重要,主要是将基准值P与lt位置的数据交换,从而实现最终的三分区,如图所示:
从最终的分区可以看到,我们下一次的循环可以不处理等于区的数据而只处理两端分区数据,这样在大量重复场景下优化效果会非常明显。
代码:
public int[] sort(int[] arr){
quick_sort(arr,0,arr.length-1);
return arr;
}
public void quick_sort(int[] arr,int start,int end){
if (start<end){
//划分
//随机基准值
int randomIndex = new Random().nextInt(end-start+1)+start;
swap(arr,start,randomIndex);
int pivot = arr[start];
int lt = start;
int gt = end + 1;
int i = start+1;
while(i<gt){
if (arr[i]<pivot){
lt++;
//交换的是处理过的等于pivot值或i自身,i++
swap(arr,i,lt);
i++;
}else if (arr[i]>pivot){
gt--;
//交换的是未处理区的数据,不用i++
swap(arr,i,gt);
}else {
i++;
}
}
swap(arr,start,lt);
//分治
quick_sort(arr,start,lt-1);
quick_sort(arr,gt,end);
}
}
public void swap(int[] arr ,int i ,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
复杂度分析
时间复杂度:基于随机选取主元的快速排序时间复杂度为期望 O(nlogn),其中 n 为数组的长度。详细证明过程可以见《算法导论》第七章,这里不再大篇幅赘述。
空间复杂度:O(h),其中 h 为快速排序递归调用的层数。我们需要额外的 O(h)的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为 logn,空间复杂度为 O(logn)。
快排稳定化、快排非递归实现待续。。。
参考:
邵顺增. 稳定快速排序算法研究[J]. 计算机应用与软件, 2014, 31(7):263-266.