【11】 排序优化:如何实现一个通用的、高性能的排序函数?

1. 如何选择合适的排序算法?

  1. 排序算法一览表
排序算法 时间复杂度 是否稳定排序 是否原地排序
冒泡排序 O(n^2)
插入排序 O(n^2)
选择排序 O(n^2)
快速排序 O(nlogn)
归并排序 O(nlogn)
桶排序 O(n)
计数排序 O(n+k),k是数据范围
基数排序 O(dn),d是纬度
  1. 为什选择快速排序?
    1)线性排序时间复杂度很低但使用场景特殊,如果要写一个通用排序函数,不能选择线性排序。
    2)为了兼顾任意规模数据的排序,一般会首选时间复杂度为O(nlogn)的排序算法来实现排序函数。
    3)同为O(nlogn)的快排和归并排序相比,归并排序不是原地排序算法,所以最优的选择是快排。

2. 如何优化快速排序?

导致快排时间复杂度降为O(n^2)的原因是分区点选择不合理,最理想的分区点是:被分区点分开的两个分区中,数据的数量差不多。如何优化分区点的选择?有2种常用方法,如下:

  1. 三数取中法
    1)从区间的首、中、尾分别取一个数,然后比较大小,取中间值作为分区点。
    2)如果要排序的数组比较大,那“三数取中”可能就不够用了,可能要“5数取中”或者“10数取中”。
  2. 随机法:每次从要排序的区间中,随机选择一个元素作为分区点。
  3. 警惕快排的递归发生堆栈溢出,有2种解决方法,如下:
    1)限制递归深度,一旦递归超过了设置的阈值就停止递归。
    2)在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈过程,这样就没有系统栈大小的限制。

3. 通用排序函数实现技巧

  1. 数据量不大时,可以采取用时间换空间的思路
  2. 数据量大时,优化快排分区点的选择
  3. 防止堆栈溢出,可以选择在堆上手动模拟调用栈解决
  4. 在排序区间中,当元素个数小于某个常数是,可以考虑使用O(n^2)级别的插入排序
  5. 用哨兵简化代码,每次排序都减少一次判断,尽可能把性能优化到极致

4. 思考

  1. 数据库里面的Order BY,用的是什么排序呢?
    如果有limit限制,则是堆排序。如果数据无法全部读入内存,则是归并排序,否则是快排。有索引的话是b+树的排序。
  2. 熟悉的语言中的排序函数都是用什么排序算法实现的呢?都有哪些优化技巧?
    C qsort():归并+快排(三数取中法+手动模拟栈)+插入(哨兵)
    Java Arrays.sort:主要采用TimSort算法, 大致思路是这样的:
    1)元素个数 < 32, 采用二分查找插入排序(Binary Sort)
    2)元素个数 >= 32, 采用归并排序,归并的核心是分区(Run)
    3)找连续升或降的序列作为分区,分区最终被调整为升序后压入栈
    4)如果分区长度太小,通过二分插入排序扩充分区长度到分区最小阙值
    5)每次压入栈,都要检查栈内已存在的分区是否满足合并条件,满足则进行合并
    6)最终栈内的分区被全部合并,得到一个排序好的数组
    Timsort的合并算法非常巧妙:
    1)找出左分区最后一个元素(最大)及在右分区的位置
    2)找出右分区第一个元素(最小)及在左分区的位置
    3 )仅对这两个位置之间的元素进行合并,之外的元素本身就是有序的
    Cpython list.sort:主要用的Timsort排序算法,还有待看源代码
    列表的实现源代码
    列表的排序说明官方文档
    心得:优秀的通用排序算法都是多个经典的基本排序算法组合得到,从而实现效率的提升。

5. 参考资料

  1. 王争老师在极客时间的专栏《数据结构与算法之美》
  2. 专栏下的所有评论

6. 声明

本文章是学习王争老师在极客时间专栏——《数据结构与算法之美》的学习总结,文章很多内容直接引用了专栏下的回复,推荐大家购买王争老师的专栏进行更加详细的学习。本文仅供学习使用,勿作他用,如侵犯权益,请联系我,立即删除。

发布了128 篇原创文章 · 获赞 157 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_27283619/article/details/102261354