8 Go排序算法 | 青训营笔记

106 阅读5分钟

Go排序算法


排序算法

常见的排序算法:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。

2023-06-05-23-56-52.png

2023-06-05-23-56-39.png

  • 排序稳定性 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的;否则称为不稳定的。 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
  • In-place:占用常数内存,不占用额外内存 Out-place:占用额外内存

插入排序 insert sort

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置(需要遍历)并插入。初始序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

  • 在数组长度较短(依场景16 ~ 32)的情况下,在实际应用中拥有良好的性能表现。
  • 在已经有序的情况下最快

快速排序 quick sort

partition-exchange sort,分治法(Divide and conquer),不断分割序列直到整体有序。选定基准,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。 流程:

  1. 数列中挑出一个元素,称为基准(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。称为分区(partition)操作
  3. ​递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
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

堆排序是一种利用堆的概念来排序的选择排序。

堆:子结点的键值或索引总是小于(或者大于)它的父节点的完全二叉树。大顶堆(升序排列)、小顶堆(降序排列)

流程:

  1. 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
  2. 将其与末尾元素进行交换,此时末尾就为最大值。
  3. 然后将剩余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来排序
  • 2023-06-07-11-55-23.png

改进:

  • ver2
    • 根据序列长度选择pivot策略 近似中位数 短:固定元素 <=8 中:采样三个 长:采样九个 >50
    • 采样元素逆序-可能逆序-翻转整个序列
    • 采样元素顺序-可能基本有序-插入排序
    • 2023-06-07-12-07-29.png
  • final
    • 重复元素较多:pivot与上次相同,使用partitionEqual将重复元素提前放到一起,因为多次选定重复元素作为 pivot 会使得 partition 的效率较低。
    • 当 pivot 选择策略表现不佳时,随机交换元素
    • 2023-06-07-12-14-13.png

2023-06-07-12-15-01.png