快速排序知道人很多,懂写懂用的人也很多,但是里面每个步骤都知其所以然了吗?
为什么快排的算法能够保证得出有序数组?
感觉快排的思想和网上实现的代码不太一样啊?
为什么算法要先后面开始遍历?
为什么要一下从前遍历一下从后遍历?
为什么基准都默认定在第一位啊?
不要急,看完这篇文章,你就懂了:)~
快排的根本思想
我们先复习一下快排的基本思想:
- 选数组中一个数组为基准 key;
- 所有小于Key的数放左边,大于Key的数放右边,Key放中间。
- 对分出的左边小于Key的数以及右边大于Key的数,再看作两个排序数组进行以上步骤,两个数组不包含key(分治与递归)
1、为什么根据上面的思路能得出有序数组?
每次左右分组后,至少能保证基准key是排序好的,因为左边都是小于它的右边都是大于它的所以位置肯定是没错了,之后的继续分组每次都会有新的基准key被选出来并排序好,所以一直分下去肯定能保证得出排序好的数组。
2、时间复杂度是怎样的,怎么得出来的?
快排的时间复杂度是不确定的,从O(nlog₂ n)到O(n^2),我们分开来分析。
每次的基准能够正好将数组平分,即基准为该数组的中位数时,时间复杂度最低。
为什么?
可以这么理解,基准能够将数组平分,保证了下一层,会有两个数组,也就意味着会选出两个基准,也就是一层能够保证排序好两个数,一次能排序好两个数,总共n个数,需要多少次? log₂ n次。若基准每次都选在边边,也就是最大值或最小值,就会导致每次只分出了一个数组,也就是一次分组只能排序好一个数字。
其实这里的n
与log₂ n
对于的就是O(nlog₂ n
)到O(n*n
)后面那块。
现在还差前面的n不知道是怎么来的了。
n是怎么来的,我们先看第一次排序,是不是所有数要和基准数对比后才能确定位置?这个比较次数就是n.对于分组后的情况来说,虽然分成了很多个组,但是其实每个数都要和自己分到的那个小组里的基准数进行比较,从整体来看,还是比较了n次,所以最终,分组的次数*分组后总的比较次数=时间复杂度
3、空间复杂度为什么是O(log₂ n),不应该是O(1)吗?
因为每次排序需要o(1)的空间,而之前分析过需要排序因为递归会执行log₂n次,相乘即可得空间复杂度为log₂ n.
代码实现
最简单易理解的实现
思想
1、多拿出一个空数组空间,搞前后两个指针i j ,一个int保存基准数。
2、然后遍历一遍数组,小于基准数的跟着前面的指针i++放,大于的跟着后面的指针j–放,最后把基准数放在两指针重合的位置。
3、这就完成了一轮排序。 4、下一轮排序则赋值到另一个数组上做就好,如此递归就ok了。
上面那种方式并不是广为流传的版本,而且需要长度为n的额外空间,所以也没人传。
手动笑哭
广为流传的版本
有两种,有思想上细微的的差别:
交换法
算法步骤
1.i =L; j = R; temp 交换需要的临时变量。
2.j–由后向前找比它小的数。
3.i++由前向后找比它大的数,找到后将i,j位置的数交换。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
代码实现
public void sort3(int arr[],int start,int end){
if(arr.length<=0||start<0||end>arr.length-1||start>end){
return;
}
int keyIndex = (start+end)/2;
int key = arr[keyIndex];
int i = start,j = end;
int temp;
while(i<j){
while(arr[j]>=key&&i<j){
j--;
}
while(arr[i]<=key&&i<j){
i++;
}
//已经遍历完
if(i==j){
temp = arr[i];
arr[i] = arr[keyIndex];
arr[keyIndex] = temp;
keyIndex = i;
}else if(i<j){//找到了可以互换的数,不是ij遍历完毕
//交换i,j位置的数
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
sort3(arr,start,keyIndex-1);
sort3(arr,keyIndex+1,end);
}
挖坑法填数法
算法步骤
1.i =L; j = R;key = 基准数。 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比基准数小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比基准数大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
具体代码
public void sort(int[]arr,int start,int end){
if(start>=end){
return;
}
int i = start;
int j = end;
int key = arr[start];
while(i<j){
//寻找小于基准数的数
while(arr[j]>=key&&i<j){
j--;
}
arr[i] = arr[j];
//寻找大于基准数的数
while(arr[i]<=key&&i<j){
i++;
}
arr[j] = arr[i];
}
arr[i] = key;
sort(arr,start,i-1);
sort(arr,i+1,end);
}
两种实现方式的异同
两种方法其实本质上是一样的,挖坑法本质上也是将一个小区间的大数与一个大区间里的小数进行了交换,只不过一开始其中一个数先和基准数进行了交换。基准数需要存在额外空间,原来的位置空出来作为第一个temp交换空间。
这些版本的算法思想就可能有点不好理解了,比如有的童鞋会有疑问:
4、在快排根本思想中没提到遍历什么的,怎么实现就这么麻烦理解不了为什么要这样呀?
其实是因为思想那部分省略了,如何实现将小于基准的数放左边,大于它的放右边这个过程。
而这个分组的实现我们可以简化描述为:
只使用O(1)的空间复杂度,在数组中完成,将数组根据基准数左右分开的任务。
5、为什么遍历要从后面用j
指针开始
对于交换法,区别在于,最后i,j重合的位置。
若是从前开始,最终位置会在大区间的左边界上重合。 如例子的4 处
若是从后开始,最终会在小区间的右边界上重合。 如例子的 1 处
例子:
3是基准
3 2 1|4 6 5
我们分析以后可以发现,若是要把3放到4的位置,并保证左右分组以3为界限并不容易实现。而3要移1的位置并保证分组正常,只需要交换位置即可。其中的原因就在于,左右两边在ij重合的时候已经分好组了,如果跨组交换必定会导致之前的划分失效。同样的道理,若我们的基准数取最后一位(即5),则我们先从前遍历较好,因为可以方便的和4交换完成最后基准数的位置校正。
6、为什么从前遍历完要换到从后遍历,而不是一直从前面遍历
在交换法中,算法思想是从前遍历获取一个大于基准的数,从后遍历一个小于基准的数,然后将他们交换,如此反复来达到分组的目的。所以才会在获取一个需要交换的数后反向遍历。
而挖坑法本质上也是大小数的交换,所以他们的思想是类似的。
7、基准的位置能不能换,不放在首位?
挖坑法,只能放在首尾,但是放在中间不行,因为中间的坑我们无法确定需要填大于还是小于基准的数进去。
交换法,可以放在任何位置,但是在两头会方便很多。参考疑问4的例子,当左右划分成功后,因为基准位置不再两头,所以我们需要先弄清楚基准数是在哪个分组里,我们才能确定怎样将他放到中间位置,而这个又需要多一些判断,所以一般都是放在头尾,处理方便。