14.排序优化:如何实现一个通用的、高性能的排序函数?
markdown文件已上传至github
1.如何选择合适的排序算法
线性排序算法的时间复杂度比较低,使用场景比较特殊,所以实现通用的排序算法不能选择线性排序算法。
对小规模数据可以选择时间复杂度为 O ( n 2 ) O(n^2) O(n2)的算法,对大规模数据进行排序选择 O ( n l o g n ) O(nlogn) O(nlogn)的排序算法。所以为了兼顾任意规模数据的排序一般都会首选时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)的算法来实现排序函数。
时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn):归并排序、快速排序、堆排序。后面两种用的多一点。比如 Java 语言采用堆排序实现排序函数,C 语言使用快速排序实现排序函数。归并排序空间复杂度为 O ( n ) O(n) O(n),所以用的比较少。
快速排序在最坏的情况下时间复杂度是 O ( n 2 ) O(n^2) O(n2),如何解决这个“复杂度恶化”的问题?
2.如何优化快速排序?
为什么最坏情况下快速排序的时间复杂度是 $O(n^2) 呢 ? 我 们 前 面 讲 过 , 如 果 数 据 原 来 就 是 有 序 的 或 者 接 近 有 序 的 , 每 次 分 区 点 都 选 择 最 后 一 个 数 据 , 那 快 速 排 序 算 法 就 会 变 得 非 常 糟 糕 , 时 间 复 杂 度 就 会 退 化 为 呢?我们前面讲过,如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那快速排序算法就会变得非常糟糕,时间复杂度就会退化为 呢?我们前面讲过,如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那快速排序算法就会变得非常糟糕,时间复杂度就会退化为O(n^2)$ 。实际上,这种 O ( n 2 ) O(n^2) O(n2) 时间复杂度出现的主要原因还是因为我们分区点选得不够合理。
理想的区分点:被区分的两个分区中,数据的数量差不多。
两个常用的分区方法:
1.三数取中法
我们从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。
2.随机法
随机法就是每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选得很差的情况,所以平均情况下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的 O(n2) 的情况,出现的可能性不大。
快速排序是用递归来实现的。我们在递归那一节讲过,递归要警惕堆栈溢出。为了避免快速排序里,递归过深而堆栈过小,导致堆栈溢出,我们有两种解决办法:第一种是限制递归深度。一旦递归过深,超过了我们事先设定的阈值,就停止递归。第二种是通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈的过程,这样就没有了系统栈大小的限制。
时间复杂度代表的是一个增长趋势,如果画成增长曲线图,你会发现$O(n^2) $ 比 O(nlogn) 要陡峭,也就是说增长趋势要更猛一些。但是,我们前面讲过,在大 O 复杂度表示法中,我们会省略低阶、系数和常数,也就是说,O(nlogn) 在没有省略低阶、系数、常数之前可能是 O(knlogn + c),而且 k 和 c 有可能还是一个比较大的数。
假设 k=1000,c=200,当我们对小规模数据(比如 n=100)排序时, n 2 n^2 n2的值实际上比 knlogn+c 还要小。
3.参考
这个是我学习王争老师的《数据结构与算法之美》所做的笔记,王争老师是前谷歌工程师,该课程截止到目前已有87244人付费学习,质量不用多说。
截取了课程部分目录,课程结合实际应用场景,从概念开始层层剖析,由浅入深进行讲解。本人之前也学过许多数据结构与算法的课程,唯独王争老师的课给我一种茅塞顿开的感觉,强烈推荐大家购买学习。课程二维码我已放置在下方,大家想买的话可以扫码购买。
本人做的笔记并不全面,推荐大家扫码购买课程进行学习,而且课程非常便宜,学完后必有很大提高。