排序算法之快速排序
这里是传送门⇒总结:关于排序算法
平均时间复杂度 | 最优时间复杂度 | 最差时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(logn) | 不稳定 |
快速排序是对冒泡排序的一种改进,是一种划分交换排序,采用了分治的思想
- 算法描述
- 从待排序列中选定一个基准数(比如序列的第一个数),把比基准数小的数放在左边,比基准数大的放在右边,一样大的放哪边都行(以下是让其在左边)。这个过程称为分区
- 然后再按照这种方式把左右两个区间再分别进行分区
- 以此类推...
- 整个排序过程可以递归进行(好像说递归有可能会爆栈)
- 分区的具体实现
- 总结起来就是“挖坑填数”
- 选定待排序列第一个数array[left]为基准数,把array[left]的值保存在变量baseVal中
- 由于array[left]的值已经被保存起来了,可以理解为array[left]的值被拿走了,现在array[left]上有一个坑,现在来找一个数填进来
- 这个坑在左边,所以从后向前扫描待排序列array,用变量j记录扫描位置
- 当发现此时的array[j]的值比基准值小或者等于基准数,把此时的array[j]的值填到坑里,也就是让a[left] = a[j]
- 现在的a[left]填完数了,可是a[j]的数被拿走填到a[left]了,现在变成a[j]有坑了,需要找另一个数来填
- 这个时候坑在右边了,从头向后扫描待排序列array,用变量i记录扫描位置
- 当发现此时的array[i]的值比基准值大,把此时的array[i]的值填到坑里,也就是让a[j] = a[i]
- 现在的a[j]填完数了,可是a[i]的数被拿走填到a[j]了,现在变成a[i]有坑了,需要找另一个数来填
- 以此类推...
- 直到i和j相遇了,即左右两边的坑都填完了,只剩下中间的坑
- 最后把一开始保存在baseVal的值填进最后一个坑
- 也就是在左边挖坑,去右边找一个应该在左边的数,填进坑里,这个行为导致右边有坑,所以得去左边找一个应该在右边的数来填,然后又导致左边有坑...最后剩一个坑,就把一开始保存的基准数填进去
- JS实现
// 此处传入的array会被直接改变
function QuickSort(array, left, right) {
if (left < right) {
var baseVal = array[left];
var i = left,
j = right;
while (i < j) {
while (i < j && array[j] > baseVal) {
j--;
}
if (i < j) {
array[i++] = array[j];
}
while (i < j && array[i] <= baseVal) {
i++;
}
if (i < j) {
array[j--] = array[i];
}
}
array[i] = baseVal;
QuickSort(array, left, i - 1);
QuickSort(array, i + 1, right);
}
}
- 分析
- 快速排序的一次分区是从两头交替寻找合适的数,直到i,j相遇,所以每一次分区的时间复杂度都是O(n),那么整个快速排序的时间复杂度与分区次数有关
- 最差情况是每次分区选择的基准数都是当前序列的最小值(即待排序列一开始就升序)的时候,这会导致每次分区后左区间的长度为0,(其实就相当于冒泡了,每次只排好一个数)这种情况下,长度为n的待排序列将进行n-1次分区,那么最差时间复杂度为O(n2)
- 最优情况是每次分区选择的基准数恰好将当前序列几乎等分,这种情况下,长度为n的待排序列将进行大概log2n次分区,那么最优时间复杂度为O(nlogn)
- 尽管快速排序只需要O(1)大小的辅助空间,但考虑递归深度,快速排序需要一个栈空间来实现递归。最好的情况下,所需栈的最大深度为log2(n+1),最优空间复杂度为O(logn);但最坏的情况下,所需栈的最大深度为n,最差空间复杂度为O(n)。快速排序的平均空间复杂度为O(logn)
- 想知道怎么由递归算法时间复杂度公式计算时间复杂度的,看这个别人家的博客
- 或者是这种利用递归树来描述递归算法执行情况的分析方式,看另外一个别人家的博客
- 由于键值的比较和交换是跳跃进行的,因此快速排序是一种不稳定的排序。假如觉得把键值比较条件
array[i] <= baseVal
改成array[i] < baseVal
可以变成稳定排序的话,请看这个例子[3,4,4,2,6]
- 优化
- 最理想的情况是,选择的基准数恰好能把待排序列分成两个几乎等长的区间
- 而上面的方法是以待排序列的第一个数为基准数
- 我们可以改变基准数的选择方案来优化这个算法
- 随机选取基准:取待排序列中任意一个元素作为基准
- 三数取中:使用左端、右端和中心位置上的三个元素的中值作为枢纽元
- 实现方法:按照上面的算法,只要让选择的基准和待排序列的第一个数字交换即可