排序算法的工业理解 | 青训营

70 阅读5分钟

课程内容主要是体验在经典的数据结构与算法领域工业界的一些最新进展,以及了解一下工业界数据结构算法和我们学校学习的有什么共同点和差异。

为什么学习数据结构与算法

例子:抖音直播排行榜功能

  • 需求:某个时间段内,直播间礼物数 TOP10房间获得奖励,需要在每个房间展示排行榜
  • 解决方案:
    • 礼物数量存储在 Redis-zset 中,使用 skiplist 使得元素整体有序
    • 使用 Redis 集群,避免单机压力过大,使用主从算法、分片算法
    • 保证集群原信息的稳定,使用一致性算法
    • 后端使用缓存算法 (LRU)降低 Redis 压力,展示房间排行榜

数据结构和算法几乎存在程序开发的所有地方,(所以面试基本必考)

Q:那问题来了,什么是最快的排序算法?Go 的排序算法有无提升空间(1.18->1.19排序算法提升)?
A:Python-timesort,C++-introsort,Go-pdqsort

经典排序算法

BestAvgWorst
InsertionSortO (n)O (n^2)O (n^2)
QuickSortO (n*logn)O (n*logn)O (n*logn)
HeapSortO (n*logn)O (n*logn)O (n*logn)

经典算法理论印象

  • 插入排序平均和最坏情况时间复杂度都是 O (n^2), 性能不好
  • 快速排序整体性能处于中间层次
  • 堆排序性能稳定,“众生平等”

实际场景 benchmark 下经典算法的表现

  • 完全随机的情况 (random)
    • 插入排序在短序列中速度最快
    • 快速排序在其他情况中速度最快
    • 堆排序速度于最快算法差距不大
  • 有序/逆序的情况 (sorted/reverse)
    • 插入排序在序列已经有序的情况下最快
  • 元素重复度较高的情况 (mod8)
  • 在此基础上,还需要根据序列长度的划分 (16/128/1024)

整体结论:

  • 所有短序列和元素有序情况下,插入排序性能最好
  • 在大部分的情况下,快速排序有较好的综合性能
  • 几乎在任何情况下,堆排序的表现都比较稳定

以交通工具做类比,插入排序->单车,快速排序->汽车,堆排序->地铁,不同场景下不同的算法表现更好;就像我们到达目的地,可能会选择多种交通工具,排序算法也是如此。

从零打造 pdqsort

pdqsort(pattern-defeating-quicksort)一种不稳定的混合排序算法。

version1

结合三种排序方法的优点

  • 对于短序列(小于一定长度)我们使用插入排序(具体长度在不同语言和场景下略有不同,12~32,泛型版本中选定为24)
  • 其他情况,使用快速排序来保证整体性能
  • 当快速排序表现不佳时(具体来说,pivot 位置离两端很近时,如小于 length/8,判定表现不佳,累计达到 limit=bits. Len (length)时,切换排序),使用堆排序来保证最坏情况下时间复杂度仍然为 O (n*logn)

优化思路

  • QuickSort 的 pivot 尽量是中位数->改进 choose pivot
  • Partition 的速度->改进 Partition,此优化在 Go 表现不好,略?
version2

pivot 寻找近似中位数:寻找开销与带来的性能优化之间的 trade-off。

  • 首个元素,简单,但是效果不一定好,如 sorted 情况下
  • 遍历寻找中位数,代价太高
  • 根据序列长度不同,决定选择策略
    • 短序列(<=8):选择固定元素
    • 中序列 (<=50):采样3个元素
    • 长序列 (>50):采样9个元素

另外 pivot 的采样还可以让我们得知序列的状态

  • 采样的元素都是逆序->序列可能是逆序的->翻转整个序列
  • 采样的元素都是顺序->序列可能是有序的->使用插入排序(实际上使用 partialInsertSort,限制次数的插入排序)

Version1升级到 version2优化总结

  • 升级 pivot 选择策略(近似中位数)
  • 发现序列可能逆序,则翻转序列->应对 reverse 场景
  • 发现序列可能有序,使用有限插入排序->应对 sorted 场景

思考还有什么场景没有优化?
短序列√ 极端情况√ 完全随机√ 有序或者逆序√ 元素重复度较高?

version3

针对重复元素很多的情况优化

怎么感知重复元素多

  • 通过采样 pivot,样本有限,无法有效检测重复度
  • 如果两次 partition 生成的 pivot 相同,即 partition 进行了无效分割,此时认为 pivot 的值为重复元素

优化-重复元素较多的情况(partitionEqual)

  • 当检测到此时的 pivot 和上次相同时(发生在 leftSubArray)使用 partitionEqual 将重复元素排列在一起,减少重复元素对于 pivot 选择的干扰

优化-当pivot选择策略表现不佳时,随机交换元素

  • 避免一些极端情况使得 QuickSort 总是表现不佳,以及一些黑客攻击情况

总结

Q:高性能的排序算法是如何设计的?
A:根据不同情况选择不同策略,取长补短

Q:生产环境中使用的的排序算法和课本上的排序算法有什么区别?
A:理论算法注重理论性能,例如时间、空间复杂度等。生产环境中的算法需要面对不同的实践场景,更加注重实践性能

Q:Go 语言(<=1.18)的排序算法是快速排序么? A:实际一直是混合排序算法,主体是快速排序。Go <= 1.18时的算法也是基于快速排序,和pdqsort的区别在于fallback时机、pivot 选择策略、是否有针对不同pattern优化等