快速排序及优化
快排的基本思想:
1.从待排序区间选择一个数,作为基准值(pivot);
2.切分(Partition): 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
3.采用分治思想,对基准值左右两个小区间按照同样的方式处理,直到小区间的长度为小于等于1,代表数组已经有序;
1.普通快排
基准值:最左侧或最右侧值
public class QuickSort1 {
public static void main(String[] args) {
//测试
Scanner in=new Scanner(System.in);
String a=in.nextLine();
String b=a.substring(1,a.length()-1);
String[] str=b.split(",");
int[] n=new int[str.length];
for(int i=0;i<n.length;i++) {
n[i] = Integer.parseInt(str[i]);
}
quickSort(n,0, n.length-1);
for(int j=0;j<n.length;j++) {
System.out.println(n[j]);
}
}
public static void quickSort(int[] s,int left,int right){
//3.快速排序
//每次选定一个阈值,小于阈值的放左边,大于阈值的放右边。
//阈值怎么选?怎么快速放?
if(left>=right) return ;//终止条件
int key=partition(s,left,right);//得到切分轴
quickSort(s,left,key-1);//左侧区间递归
quickSort(s,key+1,right);//右侧区间递归
}
public static int partition(int[] s,int left,int right){
int pivot = s[left];//基准值
if(left<right) {
while (left < right) {
while (s[right] >= pivot && left < right) {
right--;
}
if (left < right) {
s[left] = s[right];
left++;
}
while (s[left] < pivot && left < right) {
left++;
}
if (left < right) {
s[right] = s[left];
right--;
}
}
}
s[left] = pivot;//得到数组划分轴
return left;
}
}
对于普通快排来说:
在数组有序、逆序等基本有序情况下,时间复杂度是O(n^2):每次都选择了最左侧或者最右侧的值作为切分轴,相当于将除基准值之外的区间做n次递归(可以想象成只有右子树的树),递归树深度为n,每一层操作都需要遍历一次剩下的所有元素,这个操作时间复杂度为O(n),总时间复杂度为O(n^2)。
对于近乎有序数组,每次选择边上元素作为基准值,会造成切分严重不平衡:
例如举一个极端的例子:[1,2,3,4,5,6,7,9,8]数据量可以类比放大,此时如果我们选择左边第一个元素为基准值,每次处理只会移除一个元素,会导致整个排序过程会切分很多次,此时效率会非常差
在数组乱序,理想的情况下,我们选取的分界点刚好就是这个区间的中位数。也就是说,在操作之后,正好将区间分成了满足数字个数相等的左右两个子区间(快排是按照值的大小划分,个数可能相等,可能不等)。此时基准值左右两个区间都可以递归,递归树的深度应该是1+2+4+...+2^k=n-->k=logn层。对于每一个区间,处理的时候,都需要遍历一次区间中的每一个元素。这也就意味着,快速排序和归并排序一样,每一层的总时间复杂度都是O(n),因为需要对每一个元素遍历一次。所以快速排序最好的时间复杂度为O(nlogn)。
在处理有序数据之前,我们可以先将当前数组的顺序进行打乱,然后再排序,这样可以提升一定的效率,但是处理极端的数据时效果肯定不是最好的,且代码实现变得更加复杂。
四种优化方式:
在基本快速排序的基础上进行优化:三数取中确定key值、数组较小时选用插入排序、聚集相等元素、尾递归优化等。
快速排序的优化主要是在基准的选择上和尽量减少partition的运行:
基准值选择:
1.随机选择法:每次在待排序区间里随机选择一个索引作为基准值。
2.三分取中法:array[left], array[mid], array[right] 大小是中间的为基准值。
减少partition的方法:
1.聚合相同元素:聚合相等的元素,这样下次就不用再切分这部分区间。
2.数组较小时选择插入排序:直接利用插入排序对小区间处理,而不是将其切分到长度0或1,这样可以减少切分次数,提升效率。
2.随机快排(随机选择+插入)
基准值:随机选择
主要解决:基本有序的问题
import java.util.Scanner;
public class QuickSortRand {
public static void quickSortRand(int[] s, int left, int right){
//1.基准值选择:随机选择
//2.小区间用插入排序
if(left>=right){
return;
}
int key=partition(s,left,right);//得到划分轴
//小区间用插入排序
//大区间继续递归
if(key-left<=15){
insertSort(s,left,key-1);
}else{
quickSortRand(s,left,key-1);
}
if(right-key<=15){
insertSort(s,key+1,right);
}else{
quickSortRand(s,key+1,right);
}
}
public static int partition(int[] s,int left,int right){
//随机选择: 产生start - end 的随机数
int randNum=(int)(Math.random()*(right-left)+left);
//int pivot=s[randNum];//error
//这里需要把randNum和left的位置换一下,才能直接用以下代码
//因为pivot是空出来的一个坑,之前left空出坑,right才先走,现在randNum空出坑,最简单的方法就是randNum和left交换位置。
swap(s,left,randNum);
int pivot=s[left];
while(left<right){
while(s[right]>=pivot&&left<right){
right--;
}
if(s[right]<pivot){
s[left]=s[right];
left++;
}
while(s[left]<pivot&&left<right){
left++;
}
if(s[left]>pivot){
s[right]=s[left];
right--;
}
}
s[left]=pivot;
return left;
}
public static void swap(int[] s,int left,int num){
int tmp=s[left];
s[left]=s[num];
s[num]=tmp;
}
private static void insertSort(int[] s, int left, int right) {
if(left>=right){
return;
}
//直接插入排序原理:当插入第i个元素时,前面的i-1个元素已经时有序的,
//此时用第i个元素的值和前面i-1个元素进行比较,直到找到插入位置,插入即可,原来位置的元素顺序后移。
for (int i=left+1;i<=right;i++) {//s[left]有序
for (int j=i;j>left&&s[j]<s[j-1];j--) {//j>left:比到最左侧
swap(s,j,j-1);
}
}
}
public static void main(String[] args) {
//测试
Scanner in =new Scanner(System.in);
String a=in.next().toString();
String b=a.substring(1,a.length()-1);
String[] c=b.split(",");
int[] t=new int[c.length];
for(int i=0;i<c.length;i++){
t[i]=Integer.parseInt(c[i]);
}
quickSortRand(t,0,t.length-1);
for(int i=0;i<t.length;i++){
System.out.println(t[i]);
}
}
}
3.三路快排(三数取中+聚合相同元素+小区间插入排序) 目前终极版
基准值:三数取中
主要解决:大量重复数据问题
三路快排将数组分割为三个部分:小于基准值、等于基准值、等于基准值。
在下一次递归时,只需要处理小于基准值和大于基准值两个区间。
相当于聚合了等于基准值的相同元素,会减少partition
tips:
等差数列和公式
Sn=n(a1+an)/2=na1+n(n-1)/2 d
等比数列求和公式
q≠1时 Sn=a1(1-q^n)/(1-q)=(a1-anq)/(1-q)
q=1时Sn=na1
(a1为首项,an为第n项,d为公差,q 为等比)