这是我参与「第三届青训营 -后端场」笔记创作活动的的第五篇笔记
一、InsertionSort插入排序
示意图:
简析:
将元素不断插入已经排序好的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快速排序
示意图:
简析:
分治思想,不断分割序列直到序列整体有序
选定一个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堆排序
示意图:
简析:
利用堆的性质形成的排序算法
构造一个大顶堆
将根节点(最大元素)交换到最后一个位置,调整整个堆,如此反复
时间复杂度:
| 最好 | 平均 | 最坏 |
|---|---|---|
| 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] // 右孩子值大于父节点,交换
}
}
}
四、经典算法理论映像
| 最好 | 平均 | 最坏 | |
|---|---|---|---|
| InsertionSort | O(n) | O(n^2) | O(n^2) |
| QuickSort | O(n*logn) | O(n*logn) | O(n^2) |
| HeapSort | O(n*logn) | O(n*logn) | O(n*logn) |
插入排序平均和最坏情况时间复杂度都是O(n^2),性能不好
快速排序整体性能处于中间层次
堆排序性能稳定,“众生平等”
五、实际场景
插入排序在短序列中速度最快
快速排序在其他情况中速度最快
堆排序速度与最快算法差距不大
六、pdqsort
全称:pattern-defeating-quicksort
pdqsort是一总不稳定的混合算法
1、pdqsort-version1
对于短序列(<=24)使用插入排序
其他情况使用快速排序(选择首个元素作为pivot)来保证整体性能
当快速排序表现不佳时(limit==0),使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)
2、pdqsort-version2
Version1升级到version2优化总结
升级pivot 选择策略(近似中 位数)
发现序列可能逆序,则翻转序列——>应对reverse场>景
发现序列可能有序,使用有限插入排序 ——>应对sorted场景
3、pdqsort-final version
优化-重复元素较多的情况(partitionEqual)
当检测到此时的pivot和上次相同时(发生在leftSubArray),使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰
优化-当pivot选择策略表现不佳时,随机交换元素
避免一些极端情况使得QuickSort总是表现不佳, 以及一些黑客攻击情况
#如有不足,欢迎指正