快速排序是一个知名度极高的排序算法,其对于大数据的优秀排序性能和相同复杂度算法中相对简单的实现使它注定得到比其他算法更多的宠爱。
算法概述/思路
快速排序一般基于递归实现。其思路是这样的:
1.选定一个合适的值(理想情况中值最好,但实现中一般使用数组第一个值),称为“枢轴”(pivot)。
2.基于这个值,将数组分为两部分,较小的分在左边,较大的分在右边。
3.可以肯定,如此一轮下来,这个枢轴的位置一定在最终位置上。
4.对两个子数组分别重复上述过程,直到每个数组只有一个元素。
5.排序完成。
public static void main(String[] args) {
int arr[] = {88,8,2,1,4,3,9,7,6,5,89,73,62,5};
quickSort(arr,0,arr.length-1);
printArr(arr);
}
public static void quickSort(int[] arr,int low,int high){
if(low<high){
int pivot=partitaion(arr, low, high);
quickSort(arr,low,pivot-1);
quickSort(arr, pivot+1, high);
}
}
public static int partitaion(int[] arr,int low, int high){
int pivot=arr[low];
while(low<high){
while(low<high && arr[high]>=pivot) high--;
arr[low]=arr[high];
while(low<high && arr[low]<=pivot) low++;
arr[high]=arr[low];
}
arr[low]=pivot;
return low;
}
public static void printArr(int[] arr){
for(int i=0;i<arr.length;i++){
System.out.print(arr[i] + " ");
}
}
算法性能/复杂度
可以看出,每一次调用partition()方法都需要扫描一遍数组长度(注意,在递归的时候这个长度并不是原数组的长度n,而是被分隔出来的小数组,即n*(2^(-i))),其中i为调用深度。而在这一层同样长度的数组有2^i个。那么,每层排序大约需要O(n)复杂度。而一个长度为n的数组,调用深度最多为log(n)层。二者相乘,得到快速排序的平均复杂度为O(n ㏒n)。
通常,快速排序被认为是在所有同数量级的排序方法中,平均性能最好。
从代码中可以很容易地看出,快速排序单个栈的空间复杂度不高,每次调用partition方法时,其额外开销只有O(1)。所以,最好情形下快速排序空间复杂度大约为O(㏒n)。
算法优化
上面这个快速排序算法可以说是最基本的快速排序,因为它并没有考虑任何输入数据。但是,我们很容易发现这个算法的缺陷:这就是在我们输入数据基本有序甚至完全有序的时候,这算法退化为冒泡排序,不再是O(n㏒n),而是O(n^2)了。
究其根源,在于我们的代码实现中,每次只从数组第一个开始取。如果我们采用“三者取中”,即arr[low],arr[high],arr[(low+high)/2]三者的中值作为枢轴记录,则可以大大提高快速排序在最坏情况下的性能。但是,我们仍然无法将它在数组有序情形下的性能提高到O(n)。还有一些方法可以不同程度地提高快速排序在最坏情况下的时间性能。
此外,快速排序需要一个递归栈,通常情况下这个栈不会很深,为log(n)级别。但是,如果每次划分的两个数组长度严重失衡,则为最坏情况,栈的深度将增加到O(n)。此时,由栈空间带来的空间复杂度不可忽略。如果加上额外变量的开销,这里甚至可能达到恐怖的O(n^2)空间复杂度。所以,快速排序的最差空间复杂度不是一个定值,甚至可能不在一个级别。
为了解决这个问题,我们可以在每次划分后比较两端的长度,并先对短的序列进行排序(目的是先结束这些栈以释放空间),可以将最大深度降回到O(㏒n)级别。
算法稳定性
快速排序并不是稳定的。这是因为我们无法保证相等的数据按顺序被扫描到和按顺序存放。
算法适用场景
快速排序在大多数情况下都是适用的,尤其在数据量大的时候性能优越性更加明显。但是在必要的时候,需要考虑下优化以提高其在最坏情况下的性能。