经典排序算法
经典的排序算法有三个,分别为插入排序、快速排序和堆排序,首先我们来了解一下三者的实现。
插入排序(InsertionSort)
如下图所示将元素不断插入已经排序好的array中
从数组的第二个元素开始向后遍历,保证遍历位置的左边是有序序列,遍历到的数字若小于其左边的数则交换两数,直到其左边的数小于或等于它,以此方式插入到有序序列中对应的位置。
其时间复杂度最好的情况下能达到O(n),平均和最坏的情况下都是O(n^2)
快速排序(QuickSort)
采用分治思想,不断分割序列直到序列整体有序
实现步骤:
1、从数列中挑出一个元素,称为“基准”(pivot)
2、重新排列数列,比pivot小的放在它左边,反之放右边,相同大小的可放于任意一边,以基准为分界线,将数列分为了两块,称为分区操作(partition)
3、递归地对于两个分区重复第二步2操作,直至排序完成
go实现快速排序代码如下:
func quickSort(arr []int) []int {
return _quickSort(arr, 0, len(arr)-1)
}
func _quickSort(arr []int, left, right int) []int {
if left < right {
partitionIndex := partition(arr, left, right)
_quickSort(arr, left, partitionIndex-1)
_quickSort(arr, partitionIndex+1, right)
}
return arr
}
func partition(arr []int, left, right int) int {
pivot := left
index := pivot + 1
for i := index; i <= right; i++ {
if arr[i] < arr[pivot] {
swap(arr, i, index)
index += 1
}
}
swap(arr, pivot, index-1)
return index - 1
}
func swap(arr []int, i, j int) {
arr[i], arr[j] = arr[j], arr[i]
}
快速排序对于最好和平均的情况时间复杂度可以达到O(n*logn)
堆排序(HeapSort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
堆排序无论在什么情况下时间复杂度都是O(logn),是一种稳定的算法,这既是他的优点也是他的缺点。
经典算法小结
插入排序只有在于他而言最好的情况下能达到O(n)的时间复杂度,情况稍一复杂时间复杂度就是O(n^2);快速排序的性能中规中矩,在平均情况下时间复杂度为O(nlogn)优于插入排序;堆排序性能稳定,无论什么情况下时间复杂度都是O(nlogn)。
插入排序在短序列以及基本有序的序列中速度最快,中长序列中快速排序的速度是最快的并且大部分情况下快速排序都有较好的综合性能,堆排序的速度无论在什么情况下都与最快算法差距不大,比较稳定。
pdqsort
pdqsort(pattern-defeating-quicksort)是一种不稳定的混合排序算法,对于不同性质的序列,根据三种经典算法的性质选择最优的排序算法。
version1
- 对于短序列(12~24)使用插入排序
- 其他情况使用快速排序
- 当快速排序表现不佳时,即最终pivot的位置距离两端很近(距离小于length/8)时判定其表现不佳,这种情况出现次数达到limit时,切换堆排序保证时间复杂度仍为O(n*logn)
算法流程图如下:
tip:尽量使QuickSort的pivot为序列的中位数,保证QS的性能从而提升pdqsort的性能。
version2
那么如何选出中位数的pivot呢?遍历数组代价过高,性能并不好;使用第一个数字作为中位数虽然免除了遍历数组的高代价,但是实际使用效果必然差强人意。
于是我们提出了一种优化策略:
- 短序列(<=8),选择固定元素
- 中序列(<=50),采样三个元素
- 长序列(>50),采样九个元素
采样可以让我们知道序列的状态,若采样的元素都是顺序(或逆序)的,则说明这一小段的序列可能已经顺序(逆序)了,不再需要采用快速排序,转而采用插入排序(若逆序则先反转整个序列)。
final version
针对version2中的采样判断序列情况的策略,若序列中有很多重复元素不一定能够检测出来。
优化:当检测到此时的pivot与上次相同时,使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰。