快速排序
快速排序是改进的冒泡排序
1、快速排序最多需要约 N2 / 2 次比较,但随机打乱数组能够预防这种情况
(1)潜在缺点:在切分不平衡时可能会极为低效
(2)例如,如果第一次从最小的元素切分,第二次从第二小的元素切分,如此这般,每次调用只会移除一个元素,这会导致一个大子数组需要切分很多次
(3)要在快速排序前将数组随机排序的主要原因就是要避免这种情况
2、快速排序是最快的通用排序算法
(1)快速排序之所以最快是因为它的内循环中的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据,所以它的运行时间的增长数量级为 c * N * lgN,而这里的 c 比其他线性对数级别的排序算法的相应常数都要小
(2)在使用三向切分之后,快速排序对于实际应用中可能出现的某些分布的输入变成线性级别的了,而其他的排序算法则仍然需要线性对数时间
3、将长度为 N 的无重复数组排序,快速排序平均需要 1.39 * N * lnN 次比较(以及 1 / 6 的交换)
基本思想(从小到大排序)
1、从数组中找基准,可用数组第一个元素作固定基准,也可三数取中,获取中间索引下的值作基准
2、将数据分割为两部分:一部分比基准小,一部分比基准大
3、两组各自选定新基准,按上述方法分割,两组分为四组,以此类推,直到不能再分割
4、以递归进行,直到整体有序
代码实现
public class QuickSort {//从小到大排序
public static void quickSort(int[] array, int start, int end) {//start传入0,end传入array.length-1
if (array == null || array.length <= 1) {
return;
}
if (start < end) {
int standard = array[start];//固定位置作基准
int left = start;//左指针初始为分组的开头
int right = end;//右指针初始为分组的结尾
while (left < right) {
while (left < right && standard <= array[right]) {
right--;//右指针向左移
}//左右指针碰撞或找到小于基准值的值就退出循环
array[left] = array[right];//小于基准值覆盖左指针下的值
while (left < right && standard >= array[left]) {
left++;//左指针向右移
}//左右指针碰撞或找到大于基准值的值就退出循环
array[right] = array[left];//大于基准值覆盖右指针下的值
}//退出循环时,left == right 必定成立
array[left] = standard;//重新加入基准值(覆盖),等价于array[right] = standard;
left -= 1;//左指针向左移一位
right += 1;//右指针向右移一位
//用基准值作中轴,中轴分隔左右指针,基准值不参与递归
quickSort(array, start, left);//左指针作为该组的结尾
quickSort(array, right, end);//右指针作为该组的开头
}
}
}
基准的选择
1、固定位置基准
(1)取序列的第一个或最后一个元素作为基准
(2)如果输入序列是随机的,处理时间可以接受的
(3)如果数组已经有序时,每次划分只能使待排序序列减 1 ,此时为最坏情况,快速排序变为冒泡排序,时间复杂度为 O(n2),而且,输入的数据是有序或部分有序的情况是相当常见的
2、随机选取基准
(1)引入原因:待排序列是(部分)有序时,固定选取枢轴使快排效率底下
(2)取待排序列中任意一个元素作为基准
3、三数取中(median-of-three)(优化有序的数据)
(1)引入原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是最坏情况下还是 O(n2),由于随机基准选取的随机性,使得它并不能很好的适用于所有情况,即使是同一个数组,多次运行的时间也大有不同
(2)选取数组开头、中间、结尾的元素,通过比较,选择中间的值作为基准,其实可以将这个数字扩展到更大(5 数取中、7 数取中等),将取样大小设为 3 并用首、尾、居中的元素切分的效果最好
(3)最佳的划分是将待排序的序列分成等长的子序列,最佳的状态是使用序列的中间值,但很难算出来,并且会明显减慢快速排序的速度。这样的中间值的估计可以通过随机选取三个元素,并用它们的中间值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端、中心位置上的三个元素的中间值作为枢纽元。显然三数取中消除输入序列基本有序的情形,而且选取的基准没有随机性
4、以上三种基准都无法优化重复数组
优化
1、序列长度达到一定大小时,使用插入排序
(1)当快速排序达到一定深度后,划分的区间很小时,再使用快速排序的效率不高
(2)当待排序列长度为 5 ~ 20 之间,此时使用插入排序能避免一些有害的退化情形
(3)三数取中 + 插入排序:可以提高随机数组效率,但是无法优化基本有序的序列、重复序列
2、尾递归优化
(1)快排算法和大多数分治排序算法一样,都有两次递归调用,但是快速与归并排序不同,归并排序的递归则在函数一开始,快速排序的递归在函数尾部,这就使得快速代码可以实施尾递归优化
(2)使用尾递归优化后,可以缩减堆栈的深度,由原来的 O(n) 缩减为 O(log2n)
(3)尾递归概念:如果一个函数中所有递归形式的调用都出现在函数的末尾,当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码
(4)尾递归原理:当编译器检测到一个函数调用是尾递归时,它就覆盖当前的活动记录而不是在栈中去创建一个新的栈。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高
3、聚集元素
(1)在一次分割结束后,将与本次基准相等的元素聚集在一起,再分割时,不再对聚集过的元素进行分割
(2)具体过程有两步,在划分过程中,将与基准值相等的元素放入数组两端;划分结束后,再将两端的元素移到基准值周围
(3)在数组中,如果有相等的元素,那么就可以减少冗余的划分,可以优化重复序列
4、使用并行或多线程处理子序列
5、熵最优的排序
(1)实际应用中经常会出现含有大量重复元素的数组
(2)在有大量重复元素的情况下,快速排序的递归性会使元素全部重复的子数组经常出现,这就有很大的改进潜力,将当前实现的线性对数级的性能提高到线性级别
(3)一个简单的想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素,这种切分实现起来比目前使用的二分法更复杂
三向切分
1、不存在任何基于比较的排序算法能够保证在 N * H - N 次比较之内将 N 个元素排序,其中 H 为由主键值出现频率定义的香农信息量
2、当所有的主键值均不重复时有 H = lgN(所有主键的概率均为 1 / N)
(1)三向切分的最坏情况正是所有主键均不相同
(2)当存在重复主键时,它的性能就会比归并排序好得多
(3)1、2 这两个性质一起说明了三向切分是信息量最优的
(4)即对于任意分布的输入,最优的基于比较的算法平均所需的比较次数,和三向切分的快速排序平均所需的比较次数相互处于常数因子范围之内
3、对于标准的快速排序,随着数组规模的增大其运行时间会趋于平均运行时间,大幅偏离的情况非常罕见,因此可以肯定三向切分的快速排序的运行时间和输入的信息量的 N 倍是成正比的
(1)在实际应用中这个性质很重要,因为对于包含大量重复元素的数组,它将排序时间从线性对数级降低到了线性级别
(2)这和元素的排列顺序没有关系,因为算法会在排序之前将其打乱以避免最坏情况
(3)元素的概率分布决定了信息量的大小,没有基于比较的排序算法能够用少于信息量决定的比较次数完成排序
(4)这种对重复元素的适应性使得三向切分的快速排序成为排序库函数的最佳算法选择,需要将包含大量重复元素的数组排序的用例很常见
(5)经过精心调优的快速排序在绝大多数计算机上的绝大多数应用中都会比其他基于比较的排序算法更快
(6)快速排序在今天的计算机业界中的广泛应用,正是因为数学模型说明了它在实际应用中比其他方法的性能更好,而近几十年的大量实验和经验也证明了这个结论
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战