Go排序算法
排序算法
常见的排序算法:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
- 排序稳定性 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的;否则称为不稳定的。 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
- In-place:占用常数内存,不占用额外内存 Out-place:占用额外内存
插入排序 insert sort
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置(需要遍历)并插入。初始序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 在数组长度较短(依场景16 ~ 32)的情况下,在实际应用中拥有良好的性能表现。
- 在已经有序的情况下最快
快速排序 quick sort
partition-exchange sort,分治法(Divide and conquer),不断分割序列直到整体有序。选定基准,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。 流程:
- 数列中挑出一个元素,称为基准(pivot)
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。称为分区(partition)操作
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
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]
}
快速排序通常明显比其他 Ο(nlogn) 算法更快.在多数情况下拥有最好的表现
quicksort 的性能关键点在于选定 pivot,选择 pivot 的好坏直接决定了排序的速度。如果每次 pivot 都被选定为真正的 median(中位数),此时快排的效率是最高的。
- pivot如果选定为中位数,则大部分情况下每次 partition 都会形成两个长度基本相同的 sub-arrays,我们只需要 logn 次 分区就可以使得 array 完全有序,此时时间复杂度为 O(n* logn)。在最坏情况下,我们需要 n-1 次 partition (每次将长度为 L 的 array 分为长度为 1 和 L - 1 的两个 sub-arrays)才能使得 array 有序,此时时间复杂度为 O(n^2)。
- 最简单的方法:选取 array 的首个元素作为 pivot。这种简单的方法在面对极端情况时效果并不好
- 取样再取中间值作为pivot: 取最左边、最右边、中间三个值,然后选出其中间值作为 pivot 将array分成若干个子串,取子串的midian,再取这些midian中的 median作为pivot
堆排序 heap sort
堆排序是一种利用堆的概念来排序的选择排序。
堆:子结点的键值或索引总是小于(或者大于)它的父节点的完全二叉树。大顶堆(升序排列)、小顶堆(降序排列)
流程:
- 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
func heapSort(arr []int) []int {
arrLen := len(arr)
buildMaxHeap(arr, arrLen)
for i := arrLen - 1; i >= 0; i-- {
swap(arr, 0, i)
arrLen -= 1
heapify(arr, 0, arrLen)
}
return arr
}
func buildMaxHeap(arr []int, arrLen int) {
for i := arrLen / 2; i >= 0; i-- {
heapify(arr, i, arrLen)
}
}
func heapify(arr []int, i, arrLen int) {
left := 2*i + 1
right := 2*i + 2
largest := i
if left < arrLen && arr[left] > arr[largest] {
largest = left
}
if right < arrLen && arr[right] > arr[largest] {
largest = right
}
if largest != i {
swap(arr, i, largest)
heapify(arr, largest, arrLen)
}
}
func swap(arr []int, i, j int) {
arr[i], arr[j] = arr[j], arr[i]
}
几乎在任何情况下,堆排序的表现稳定 O(n*logn)
pdqsort
pattern-defeating quicksort,一种混合排序算法,会在不同情况下切换到不同的排序机制,是 C++ 标准库算法 introsort 的一种改进(C++ boost)。 不断判定目前的序列情况,然后使用不同的方式和路径达到最优解:
- 短序列: 使用插入排序来进行排序后直接返回 24
- 发现quicksort效果不佳(最终pivot离序列两端很近,次数达到limit)则后续排序都使用堆排序来保证最坏情况时间复杂度为 O(n*logn)。
- 对于其他情况使用改进的quicksort来排序
改进:
- ver2
- 根据序列长度选择pivot策略 近似中位数 短:固定元素 <=8 中:采样三个 长:采样九个 >50
- 采样元素逆序-可能逆序-翻转整个序列
- 采样元素顺序-可能基本有序-插入排序
- final
- 重复元素较多:pivot与上次相同,使用partitionEqual将重复元素提前放到一起,因为多次选定重复元素作为 pivot 会使得 partition 的效率较低。
- 当 pivot 选择策略表现不佳时,随机交换元素