原理:
选取数组中某一元素作为数组元素的基准记作v,通过方法使得在数组基准左边的元素小于v,右边的元素大于v。
解决方法:递归
为什么?
因为我们可以看到,通过我们不断寻找基准后,数组将不断地进行划分成两个数组,一个大于v的数组和一个小于v的数组,递归最后数组的长度变成了1。此时我们结束了递归深入然后进行返回。数组元素就排好了序,并通过函数返回当前基准所在位置的下标。
通过分析,我们就可以实现快速排序法的伪码:
public void quickSort(E arr[],int l,int r){
if (l>=r)
return;
int p = partition(arr,l,r);
quickSort(arr,l,p-1);
quickSort(arr,p+1,r);
}
对于递归的解决,首先要先找到最小问题的解决方法,对于快速排序法递归调用的最后,数组元素不会超过1个,所以它的边界条件就是l>=r;其中l,r分别是数组的首元素和末尾元素的下标。此时我们返回为空。
然后就是递归过程的实现:
根据递归的宏观语义,我们知道先要找到基准对应元素的下标,也就是知道partiton()方法的返回值是多少,然后才能继续根据基准下标进行划分。所以我们知道,partition()方法的实现是实现快速排序算法的关键。
以下是partiton代码的实现:
private static <E extends Comparable<E>> int partition(E arr[],int l,int r){
int i=l;
int j= l+1;
//循环不变量arr[l+1,i]<v,arr[i+1,j-1]>=v
while(j<=r){
if (arr[j].compareTo(arr[l])<0) {
i++;
swap(arr,i,j);
j++;
continue;
}
else{
j++;
}
}
swap(arr,l,i);
//当前基准的下标
return i;
}
它是一个原地排序
它的实现过程是这样的:
我们选择数组的首元素作为当前数组的基准,然后我们将数组划分为了三个区域,一个是小于基准v,一个是大于基准v,一个是将要判断是大于还是小于基准v的区域。
为此,我们设立两个变量i和j来分别指向小于v区域的最末一个元素和将要判断区域的首元素。
然后,整个过程将是这样的。首先因为刚刚设立基准还未进行判断,所以我们的i就等于l,我们的i就等于l+1。(l是数组的首元素下标)然后我们就进行判断arr[j]>=arr[l]?,如果大于,只需要进行j++操作。
但是我们发现,最重要的还是如果小于v的话,我们到底该如何进行操作?
以下就是我们的方法:
首先我们先要进行i++操作来扩充我们小于v的区域,然后此时i对应的元素就是当前数组大于v区域的第一个元素,j对应的元素就是小于v的元素。此时我们只需要通过swap方法,使得i对应元素和j对应元素进行交换,就完成了我们的小于?的条件。
最后我们将v元素和当前小于v区域最末一个元素也就是当前i所指定的元素来进行swap交换,此时基准就到了它应该在数组中的位置。
然后我们返回i,也就是返回基准对应元素的下标。
这就是partition方法的最终实现了。实现后,我们也就完成了快速排序的实现。
以下是实现代码:
public class QuickSort {
private QuickSort(){}
//第一版快速排序
private static<E extends Comparable<E>> void quickSort(E arr[]){
quickSort(arr,0, arr.length-1);
}
private static<E extends Comparable<E>> void quickSort(E arr[],int l,int r){
if (l>=r)
return;
int p = partition(arr,l,r);
quickSort(arr,l,p-1);
quickSort(arr,p+1,r);
}
private static <E extends Comparable<E>> int partition(E arr[],int l,int r){
int i=l;
int j= l+1;
//循环不变量arr[l+1,i]<v,arr[i+1,j-1]>=v
while(j<=r){
if (arr[j].compareTo(arr[l])<0) {
i++;
swap(arr,i,j);
j++;
continue;
}
else{
j++;
}
}
swap(arr,l,i);
//当前基准的下标
return i;
}
private static<E extends Comparable<E>> void swap(E arr[],int i,int j){
E temp = arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
当然,对于我们现在缩写的快速排序算法还是有小缺陷的。这个缺陷是当我们对一个已经排好序的元素量比较多数组进行排序的话,它的所用时间可能会非常多,而且有可能导致出现一个系统栈溢出折磨一个情况。
为什么呢?
假设我们整个数组的长度是n。
回看我们这个图我们发现我们永远是选用数组的第一个元素作为当前数组的基准
但是这个数组是排好序的,也就是说第一个元素永远是当前元素最小的元素,那么他的
小于v区域就是空,而大于v的元素个数就是n-1个。然后我们返回基准,然后跟据基准划分成两个数组。一个为空,一个为n-1的长度。
然后划分的n-1长度的数组继续以第一个元素作为基准,然后产生相同的情况,然后就划分成了两个数组一个为空,一个为n-2的长度。以此类推我们要进行O(n)的递归深度。这个算法的复杂度就变成了(1+2+3+4+...+n-2+n-1)=O(n^2)级别的时间复杂度。
而我们知道,归并排序法的时间复杂度是O(nlogn)级别,递归深度是O(logn)级别的,对于同样的n,归并排序可能只需要几次递归深度调用就能完成。
所以我们怎莫解决这个小问题呢?
问题解决:
我们发现,这个问题在于我们只是在使用固定的第一个元素作为我们基准。如果我们使用一个随机的,在数组范围内的一个元素作为我们数组的基准的话,那么就可以很好的避免这个问题了。
具体的实现就是导入java的util包下的一个Random类,然后具体实现一个Random类对象使得它生成一个随机数,这个随机数就是数组的某一元素的下标。
具体代码的实现:
private static <E extends Comparable<E>> int partition12(E arr[],int l,int r){
//生成[l,r]之间的随机数以此来解决顺序数组带来的系统栈溢出问题——>找出随机的一个下标
//虽然没有专门的生成指定区间里的随机数的方法,只能生成从0开始的,但是可以进行转化
int red = (new Random()).nextInt(r-l+1);
swap(arr,l,red+l);
int i = l;
//依然是同一种循环不变量
for (int j = i+1;j<=r;j++){
if (arr[j].compareTo(arr[l])<0){
i++;
swap(arr,i,j);
j++;
}
}
//交换元素使得基准在中间位置
swap(arr,i,l);
return i;
}
但是生成的随机数只能是从0开始,而不能从我们指定的一个l开始,所以有一个差值。我们可以先指定和数组相同长度的区间,然后最后让随机数加上l就可以了。
同时(new Random()).nextInt(r-l+1),它是左闭右开的【0,bound)区间,而我们是【l,r】的闭区间,所以填入的是r-l+1。
这就是我们的第一版partition的快速排序法了。