冒泡排序和快速排序都属于交换排序,其中快排更像是冒泡的进化版本,我们先看冒泡排序:
思想:每轮循环将本轮最大元素移动到数组末尾,以此类推,直到长度为一。假设数组长度为 n,第一轮循环将最大元素移动到下标为 n-1 的位置,第二轮需要判断前 n -1 个元素,并将最大元素移动到 n-2 的位置,以此类推。计算最大值通过比较交换完成。
快排在冒泡的基础上做了优化,它不在局限于每轮找最大的或最小的,而是找比它小的和比它大的。每次将比它小的元素放在自身左边,比它大的元素放在右边,从全局层面保证有序。
思想:以数组下标开始元素为基准,比较交换。交换后左边都是比自己小的元素,右边都是比自己大的元素。左集合和右集合再分别进行上述判断,直到集合中只有一个元素。
从代码可以看出:quickSort() 计算出的 index 越靠近中心,递归的次数越少,性能越好。如果每次计算出的中点恰好都在中间,那时间复杂度仅为 O(nlog2n)。如果每次计算出的中点恰好都在边沿,时间复杂度为 O(n^2),和冒泡排序相等。
通常情况下由于数组无序,冒泡排序的时间复杂度约处于 O(nlog2n)。但当数组本身有序时,由于快排总拿第一个元素作为基准数,这样计算出的 index 总在边沿,时间复杂度非常高。为了解决该问题,可以从基准数的选择优化快排,通常有两种方式优化:
- 三数取中间数
- 随机选基准数
优化后的代码如下:
new Random().nextInt(n):返回小于 n 大于等于 0 的所有整数
只要保证选择的基准数尽可能的可以将数组划分为平均的两部分,就可以提高快排的性能
quickSort() 在满意 left <= right 条件时总要进行递归计算,当数组长度很小时,快排的效率可能不如冒泡排序,因为此时时间复杂度相等但快排涉及到创建栈的消耗。此时就可以优化,当数组长度很小时,直接采用冒泡排序:
假设数组中存在多个和基准数相同的数,目前的逻辑相等不交换,从全局维度来说,划分的并不是很干净,举个例子:
从结果来说,虽然左边全部是小于等于它的,右边全部是大于等于它的,但两者都包含和相等值,后续递归过程中仍需判断。我们可以想办法让相等的值集中在中间,这样下轮循环就可以不判断这部分值了,拿上面的例子来说,理想状态为分割为 432 555 68,下次只需递归 432 和 68 集合即可,下面我给出聚合以及跳过的逻辑:
至此关于快排的三种优化介绍完毕:
- 基准数选择优化
- 小数组采用其它排序方式,如插入、冒泡
- 基准数相等聚合跳过
最后由于快排采用分治的思想,单次任务实际上可以分配到不同的线程执行:对于每次任务,创新新线程执行,线程体就是快排的实现逻辑,把递归改为创建新线程执行即可,充分发挥 CPU 效率: