经典排序算法 | 青训营笔记

124 阅读4分钟

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

一、InsertionSort插入排序

示意图:

image.png

简析:

将元素不断插入已经排序好的array中

起始只有一个元素5,其本身就是一个有序数列

后续元素插入有序序列中,即不断交换,知道找到第一个比其小的元素

时间复杂度:

最好平均最坏
O(n)O(n^2)O(n^2)

优点:最好情况时间复杂度为O(n)

缺点:平均和最坏情况的时间复杂度高达O(n^2)

代码:

// InsertionSort 插入排序
func InsertionSort(arr []int) {
    for i := 1; i < len(arr); i++ {
        insertNum := (arr)[i]
        insertIndex := i - 1 // 下标
        // 从小到大
        for insertIndex >= 0 && arr[insertIndex] > insertNum {
            arr[insertIndex+1] = arr[insertIndex] // 数据后移
            insertIndex--                         // 插入位置前移
        }
        // 插入
        if insertIndex+1 != i {
            arr[insertIndex+1] = insertNum
        }
    }
}

二、QuickSort快速排序

示意图:

image.png

简析:

分治思想,不断分割序列直到序列整体有序

选定一个pivot(轴点)

使用pivot分割序列,分成元素比pivot大的和元素比pivot小的两个序列

时间复杂度:

最好平均最坏
O(n*logn)O(n*logn)O(n^2)

优点:平均情况时间复杂度为O(n*logn)

缺点:最坏情况的时间复杂度高达O(n^2)

代码:

// QuickSort 快速排序
// 1 left 表示数组左边的下标
// 2 right 表示数组右边的下标
// 3 array 表示要排序的数组
func QuickSort(left int, right int, array []int) {
    l := left
    r := right
    // pivot 是中轴, 支点
    pivot := array[(left+right)/2]
    // 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
        }
        // 交换
        array[l], array[r] = array[r], array[l]
        // 优化
        if array[l] == pivot {
            r--
        }
        if array[r] == pivot {
            l++
        }
    }
    // 如果  l== r, 再移动下
    if l == r {
        l++
        r--
    }
    // 向左递归
    if left < r {
        QuickSort(left, r, array)
    }
    // 向右递归
    if right > l {
        QuickSort(l, right, array)
    }
}

三、HeapSort堆排序

示意图:

image.png

简析:

利用堆的性质形成的排序算法

构造一个大顶堆

将根节点(最大元素)交换到最后一个位置,调整整个堆,如此反复

时间复杂度:

最好平均最坏
O(n*logn)O(n*logn)O(n*logn)

优点:平坏情况时间复杂度为O(n*logn)

缺点:最好情况的时间复杂度高达O(n*logn)

代码:

// HeapSort 堆排序
func HeapSort(array []int) {
    // 1、构建堆(这里用大顶堆构建升序)
    // 2、调整堆,把堆顶元素和第i-1个元素交换,这样0....i-2就又成为一个堆,继续对这个堆进行构建,调整
    Hepify(array, len(array)) // 先构建n个元素的大顶堆
    for i := len(array) - 1; i >= 0; i-- {
        array[i], array[0] = array[0], array[i] // 调整堆顶元素,把堆顶元素和最后一个元素交换
        Hepify(array, i)
    }
}
​
// Hepify 构建堆,一般从最后一个非叶子节点开始构建,即从下往上调整,从下往上能让最大(小)值元素转移到堆顶
func Hepify(nums []int, unsortCapacity int) {
    for i := (unsortCapacity / 2) - 1; i >= 0; i-- { // 非叶子节点的i范围从0...(n/2-1)个
        // 调整左子树
        leftIndex := 2*i + 1
        if leftIndex < unsortCapacity && nums[i] < nums[leftIndex] {
            nums[i], nums[leftIndex] = nums[leftIndex], nums[i] // 左孩子值大于父节点,交换
        }
        // 调整右子树
        rightIndex := 2*i + 2
        if rightIndex < unsortCapacity && nums[i] < nums[rightIndex] {
            nums[i], nums[rightIndex] = nums[rightIndex], nums[i] // 右孩子值大于父节点,交换
        }
    }
}

四、经典算法理论映像

最好平均最坏
InsertionSortO(n)O(n^2)O(n^2)
QuickSortO(n*logn)O(n*logn)O(n^2)
HeapSortO(n*logn)O(n*logn)O(n*logn)

插入排序平均和最坏情况时间复杂度都是O(n^2),性能不好

快速排序整体性能处于中间层次

堆排序性能稳定,“众生平等”

五、实际场景

插入排序在短序列中速度最快

快速排序在其他情况中速度最快

堆排序速度与最快算法差距不大

image.png

六、pdqsort

全称:pattern-defeating-quicksort

pdqsort是一总不稳定的混合算法

1、pdqsort-version1

image.png

对于短序列(<=24)使用插入排序

其他情况使用快速排序(选择首个元素作为pivot)来保证整体性能

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

2、pdqsort-version2

image.png

Version1升级到version2优化总结

升级pivot 选择策略(近似中 位数)

发现序列可能逆序,则翻转序列——>应对reverse场>景

发现序列可能有序,使用有限插入排序 ——>应对sorted场景

3、pdqsort-final version

image.png

优化-重复元素较多的情况(partitionEqual)

当检测到此时的pivot和上次相同时(发生在leftSubArray),使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰

优化-当pivot选择策略表现不佳时,随机交换元素

避免一些极端情况使得QuickSort总是表现不佳, 以及一些黑客攻击情况

官方源码:github.com/golang/go/b…

#如有不足,欢迎指正