这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记
1. 例子引入
抖音直播排行榜
某个时间段内,直播间礼物数Top10的房间获得奖励,每个房间展示排行榜
解决方案:
- 礼物数量存储在Redis-zset中,使用skiplist使得元素整体有序
- 使用Redis集群,避免单机压力过大,使用主从算法、分片算法
- 保证集群原信息的稳定,使用一致性算法
- 后端使用缓存算法(LRU)降低Redis压力,展示房间排行榜
2. 各语言最快的排序方法
- Python:timesort
- C++:introsort
- Rust:pdqsort
- GO:introsort,1.19默认排序算法
3.排序算法回顾
-
-
选择排序、希尔排序、快速排序和堆排序都是不稳定的
-
稳定排序:排序前后两个相等的数位置相对不变;常见的稳定排序算法:插入,冒泡,归并
-
排序方式
- in-place 占用常数内存,不占用额外内存
- out-place 占用额外内存(归并,计数,桶排序和基数排序)
-
快排优化:
-
3 种快排基准选择方法: 随机(rand函数)、固定(队首、队尾)、三数取中(队首、队中和队尾的中间数)
- hoare版本(左右指针法)
- 挖坑法
- 前后指针法
-
4种优化方式:
- 好优化1:当待排序序列的长度分割到一定大小后,使用插入排序
- 优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
- 优化3:优化递归操作
- 优化4:使用并行或多线程处理子序列
-
4. pdqsort
根据序列元素排列情况划分
4.1Version 1
-
结合三种排序方法的优点
- 对于短序列使用插入排序
- 其它情况使用快速排序来保证整体性能
- 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)
-
实现
- 短序列插入,长序列快速,快速表现不佳,使用堆排序
-
细节
- 短序列的长度——12-32,反省版本选定24
- 如何得知快速排序表现不佳,何时切换?——当最终的pivot的位置离序列两端很接近时(距离小于length/8)判定其表现不佳,当这种情况的次数达到limit(即bits.Len(length))时切换成堆排序了
-
优化
- 尽量使快速排序的pivot为序列的中位数——改进choose pivot
- Partition速度更快——改进Partition
4.2Version 2
-
pivot如何选择?
- 如果使用首个元素:实现简单,但是效果不好
- 遍历数组,寻找中位数:代价很高,性能不好
-
需要平衡寻找pivot的开销和pivot带来的性能优化,寻找近似中位数
-
优化Pivot的选择
- 短序列(<=8),选择固定元素
- 中序列(<=50),采样三个元素
- 长序列(>50),采样九个元素
- Pivot的采样方式使得我们有探知序列当前状态的能力,如果逆序可以翻转,如果顺序可以直接使用插入排序
-
优化点
- 升级pivot选择策略(近似中位数)
- 发现序列可能逆序,反转序列(reverse)
- 发现序列可能有序,使用有限插入排序(sorted)
4.3final version
-
元素重复度较高的情况
-
解决方法
-
采样pivot的时候检测重复度
-
采样数量有限,不一定能够采样到相同的元素
-
如果两次的partition生成的pivot相同,即partiotion进行了无效分割,此时认为pivot的值为重复元素
- 当pivot选择策略表现不佳时,随机交换元素
-
-
总结
优化点:
- 短序列:使用插入排序(v1)
- 极端情况,使用堆排序保证算法的可行性(v1)
- 完全随机的情况(random),更好的pivot选择策略(v2)
- 有序/逆序的情况(reverse and sorted):根据序列状态翻转或者插入排序(v2)
- 元素重复度较高的情况(mod8):(v3)
5.Q&A
高性能的排序算法是如何设计的?
-
根据不同的情况选择不同的策略,取长补短
-
生产环境和课本上的排序算法区别
- 生产环境:面对不同的场景,更加注重实践性能
- 理论算法:注意理论性能,例如时间,空间复杂度
-
Go语言(<=1.18)的排序算法是快速排序么?
- 一直都是混合排序算法,主体是快速排序
- 区别:fallback时机,pivot选择策略,是否有针对不同pattern优化
6. 作业
完成 PPT 中 pdqsort v1 版本,可以正确对元素排序
- 根据stl的IntroSort算法进行优化
STL的sort算法的优化策略:
1、 数据量大时采用QuickSort,分段递归排序。
2、 一旦分段后的数据量小于某个门槛,为避免Quick Sort的递归调用带来的额外负荷,就改用Insertion Sort。
3、 如果层次过深,还会改用HeapSort
4、 “三点中值”获取好的分割
- Version1:短序列插入,长序列快速,快速表现不佳,使用堆排序
const threshold = 24
func BenchmarkTestIntroSort(b *testing.B) {
demo := []int{1, 3, 5, 7, 9, 8, 2}
introSort(demo, len(demo))
//fmt.Println(demo)
}
type sortSlice []int
func (a sortSlice) Len() int { return len(a) }
func (a sortSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortSlice) Less(i, j int) bool { return a[i] < a[j] }
func BenchmarkTestSort(b *testing.B) {
demo := []int{1, 3, 5, 7, 9, 8, 2}
sort.Sort(sortSlice(demo))
//fmt.Println(demo)
}
func introSort(array []int, len int) {
if len != 1 {
introSortLoop(array, 0, len-1, lg(len)*2)
insertionSort(array, len)
}
}
// 第一层函数
//计算最大容忍的递归深度
func lg(n int) int {
var k int
for k = 0; n > 1; n >>= 1 {
k++
}
return k
}
//递归的对数组进行分割排序
func introSortLoop(array []int, begin, end, depthLimit int) {
for (end - begin + 1) > threshold { //子数组数据量大小,则交给后面的插入排序进行处理
if depthLimit == 0 { //递归深度过大,则由堆排序代替
heapSort(array, begin, end)
return
}
depthLimit--
//使用quick sort进行排序
cut := partition(array, begin, end, median3(array, begin, begin+(end-begin)/2, end))
introSortLoop(array, cut, end, depthLimit)
end = cut //对左半段进行递归的sort
}
}
//快速排序用到的辅助函数
//对数组分割
func partition(array []int, left, right, p int) int {
index := left //选择最右侧的元素作为分割标准
swap(array, p, right)
pivot := array[right]
//将所有小于标准的点移动到index的左侧
for i := left; i < right; i++ {
if array[i] < pivot {
swap(array, index, i)
index++
}
}
//将标准与index指向的元素交换,返回index,即分割位置
swap(array, right, index)
return index
}
//三点中值
func median3(array []int, first, median, end int) int {
if array[first] < array[median] {
if array[median] < array[end] {
return median
} else if array[first] < array[end] {
return end
} else {
return first
}
} else if array[first] < array[end] {
return first
} else if array[median] < array[end] {
return end
} else {
return median
}
}
//堆排序用到的辅助函数
func parent(i int) int {
return (int)((i - 1) / 2)
}
func left(i int) int {
return 2*i + 1
}
func right(i int) int {
return 2*i + 2
}
func heapShiftDown(heap []int, i, begin, end int) {
l := left(i-begin) + begin
r := right(i-begin) + begin
largest := i
//找出左右字节点与父节点中的最大者
if l < end && heap[l] > heap[largest] {
largest = l
}
if r < end && heap[r] > heap[largest] {
largest = r
}
//若最大者不为父节点,则需交换数据,并持续向下滚动至满足最大堆特性
if largest != i {
swap(heap, largest, i)
heapShiftDown(heap, largest, begin, end)
}
}
//自底向上的开始建堆,即从堆的倒数第二层开始
func buildHeap(heap []int, begin, end int) {
for i := int(begin+end) / 2; i >= begin; i-- {
heapShiftDown(heap, i, begin, end)
}
}
//堆排序
func heapSort(heap []int, begin, end int) {
buildHeap(heap, begin, end)
for i := end; i > begin; i-- {
swap(heap, begin, i)
heapShiftDown(heap, begin, begin, i)
}
}
// 插入排序
func insertionSort(array []int, len int) {
var i, j, temp int
for i = 1; i < len; i++ {
temp = array[i] //store the original sorted array in temp
for j = i; j > 0 && temp < array[j-1]; j-- { //compare the new array with temp(maybe -1?)
array[j] = array[j-1] //all larger elements are moved one pot to the right
}
array[j] = temp
}
}
func swap(s []int, i, j int) {
s[i], s[j] = s[j], s[i]
}
参考链接
快排的优化:blog.csdn.net/weixin_4290…
快速排序的几种方法:zhuanlan.zhihu.com/p/419568256