pdqsort 算法 | 青训营

504 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第 4 篇笔记

pdqsort 算法,即 pattern defeating quicksort,是一种不稳定的混合排序算法。

特征

  • 对于短序列(小于一定长度,一般为 12~32),使用插入排序

  • 其他情况,使用快速排序

  • 快速排序表现不佳时,使用堆排序来保证最坏情况下,时间复杂度仍然为 O(n*log n)

    • 当最终 pivot 的位置离序列两端很接近时(距离小于 length/8),判定其表现不佳。当这种情况达到 limit(bits.Len(length)),切换到堆排序。

快速排序关于 pivot 的选择

  • 选取首个元素作为 pivot,是最简单的方案,但在序列有序的情况下,性能很差。

  • 遍历数组,找中位数作为 pivot,但是多一次遍历,性能不好

  • 寻找近似中位数

    • 对于短序列(<=8),选取固定元素
    • 对于中序列(<=50),采样三个元素(一般是前中后三个),选取中位数
    • 对于长序列(>50),采样九个元素

扩展

插入排序

插入排序(InsertionSort),一般也被称为 直接插入排序。直接插入排序优化后,又可分为 折半插入排序希尔排序。这里的示例代码用的是直接插入排序。

package main
 
import (
   "fmt"
)
 
​
func InsertionSort(arr []int,n int) {
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
        for j >= 0 && arr[j] > key {
            arr[j + 1] = arr[j]
            j--
        }
        arr[j + 1] = key
        fmt.Println(arr)
    }

快速排序

快速排序的大致流程:选取某个元素作为 pivot,将它移动到一个位置,这个位置的左边都是比它小的数,右边都是比它大的数。然后再将左右序列分别排序,最后合并成一个排序好的序列。快速排序是一种 不稳定 的排序算法。

package main
 
import (
   "fmt"
)
​
func QuickSort(left int, right int, array *[9]int) {
   l := left
   r := right
   // pivot 是中轴, 支点
   pivot := array[(left+right)/2]
   temp := 0
   // for 循环的目标是将比 pivot 小的数放到左边,比 pivot 大的数放到右边
   for l < r {
      // 从 pivot 的左边找到大于等于pivot的值
      for array[l] < pivot {
         l++
      }
      // 从 pivot 的右边边找到小于等于pivot的值
      for array[r] > pivot {
         r--
      }
      // 1 >= r 表明本次分解任务完成, break
      if l >= r {
         break
      }
      // 交换
      temp = array[l]
      array[l] = array[r]
      array[r] = temp
      // 优化
      if array[l] == pivot {
         r--
      }
      if array[r] == pivot {
         l++
      }
   }
   // 如果  1== r, 再移动下
   if l == r {
      l++
      r--
   }
   // 向左递归
   if left < r {
      QuickSort(left, r, array)
   }
   // 向右递归
   if right > l {
      QuickSort(l, right, array)
   }
}

堆排序

堆排序是一种不稳定的排序方法。实现思路:

  • 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  • 将堆顶元素与末尾元素交换,将最大元素放到数组末端;
  • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
func heapSort(arr []int){ //arr.length = n
    //构建大根堆
    buildMaxHeap(arr)
    //排序 n-1 次
    for(i:=arr.length - 1; i > 0; i--){
        swap(arr, 0, i) // 将最大值交换到数组最后
        maxHeapify(arr, 0, i); //调整交换后的结点
    }
}
构建大根堆 代码:
func buildMaxHeap(arr []int){
    从最后一个非叶子节点开始调整大根堆
    最后一个非叶子节点为 arr.length/2 - 1
    for (int i = arr.length / 2 - 1; i >= 0; i--) {
        maxHeapify(arr, i, arr.length);
    }
}
调整大顶堆 代码:
func maxHeapify(arr []int, i, haepSize int){ //i:要进行调整为堆的非叶子节点结点,heapSize:堆的大小
    //左子树下标
    l := 2 * i + 1
    //右子树下标
    r := l + 1
    // 记录根结点、左子树结点、右子树结点三者中的最大值下标
    largest := i
    if(l < heapSize && arr[l] > arr[largest]){
        largest = l  
    }
    if(r < heapSize && arr[r] > arr[largest]){
        largest = r   
    }
    if(i != largest){
        //交换位置
        swag(arr, i, largest)
        // 再次调整交换数字后的大顶堆
        maxHeapify(arr, largest, heapSize)
    }
}
func swap(arr []int, int i, int j) {
    temp := arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

pdqsort

package sortlearning
​
import (
    "math/bits"
​
    "golang.org/x/exp/constraints"
)
​
func PDQsortV1[T constraints.Ordered](v []T) {
    recurseV1(v, bits.Len(uint(len(v))))
}
​
func recurseV1[T constraints.Ordered](v []T, limit int) {
    const maxInsertion = 24 // slices of up to this length get sorted using insertion sort.var (
        // True if the last partitioning was reasonably balanced.
        wasBalanced = true
    )
​
    for {
        length := len(v)
​
        // Very short slices get sorted using insertion sort.
        if length <= maxInsertion {
            InsertionSort(v)
            return
        }
​
        // If too many bad pivot choices were made, simply fall back to heapsort in order to
        // guarantee `O(n log n)` worst-case.
        if limit == 0 {
            HeapSort(v)
            return
        }
​
        if !wasBalanced {
            limit--
        }
​
        // Choose a pivot and try guessing whether the slice is already sorted.
        pivotidx := choosePivotV1(v)
​
        // Partition the slice.
        mid := partitionv1(v, pivotidx)
​
        left, right := v[:mid], v[mid+1:]
        if len(left) < len(right) {
            wasBalanced = len(left) >= len(v)/8
            recurseV1(left, limit)
            v = right
        } else {
            wasBalanced = len(right) >= len(v)/8
            recurseV1(right, limit)
            v = left
        }
    }
}
​
func partitionv1[T constraints.Ordered](v []T, pivotidx int) int {
    pivot := v[pivotidx]
    v[0], v[pivotidx] = v[pivotidx], v[0]
    i, j := 1, len(v)-1
​
    for {
        for i <= j && v[i] < pivot {
            i++
        }
        for i <= j && v[j] >= pivot {
            j--
        }
        if i > j {
            break
        }
        v[i], v[j] = v[j], v[i]
        i++
        j--
    }
    v[j], v[0] = v[0], v[j]
    return j
}

\