十大常见的排序算法有:
-
冒泡排序(Bubble Sort)
-
选择排序(Selection Sort)
-
插入排序(Insertion Sort)
-
希尔排序(Shell Sort)
-
归并排序(Merge Sort)
-
桶排序(Bucket Sort)
-
快速排序(Quick Sort)
-
堆排序(Heap Sort)
-
计数排序(Counting Sort)
-
基数排序(Radix Sort)
一个一个说
冒泡排序
冒泡排序是一种简单的排序算法,通过重复比较相邻元素并交换位置从而确定一个元素的位置,直到没有需要交换的元素
大体有两种写法,一种是从前往后比,每次确定一个较大值;一种是从后往前比,每次确定一个较小值
func BubbleSort(arr []int) {
n := len(arr)
// 1、每次确定一个较大值的位置
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
// 如果当前值大于下一位,就交换
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
// 2、每次确定一个较小值的位置
for i := 0; i < n-1; i++ {
for j := n - 1; j > i; j-- {
if arr[j] < arr[j-1] {
// 当前值小于上一位,就交换
arr[j], arr[j-1] = arr[j-1], arr[j]
}
}
}
return arr
}
冒泡排序是稳定的,稳定是指相同元素在排序完后相对位置不变,例如切片[2, 1, 3, 1, 5],经过排序后第一个 "1" 还是在第二个 "1" 之前
冒泡排序的平均时间复杂度是O(n^2),在元素近似有序的情况下,会有很多没有必要的比较和交换,一种优化思路是使用标志位,如果一次循环没有发生任何元素交换,就表示切片已经有序了,直接退出就可以,不需要再往下比较,这在元素近似有序的情况下会很省时间
func BubbleSort(arr []int) {
n := len(arr)
// 使用标志位和缩小比较范围优化
for i := 0; i < n-1; i++ {
swapped := false // 标志位,记录这次外层循环中是否有过交换
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
swapped = true
}
}
// 如果一次交换都没有,说明数组已经有序了,没有必要再循环下去
if !swapped {
break
}
}
return arr
}
选择排序
选择排序的思路是,反复选择未排序部分的最小(大)元素,把它放到已排序部分的末尾
func selectionSort(arr []int) {
n := len(arr)
for i := 0; i < n-1; i++ {
// 最小元素下标
minIndex := i
for j := i + 1; j < n; j++ {
// 找出待排序元素中最小的一个
if arr[j] < arr[minIndex] {
minIndex = j
}
}
// 把找出来的最小元素放到排序好的元素末尾
arr[i], arr[minIndex] = arr[minIndex], arr[i]
}
return arr
}
特点:时间复杂度是 O(n^2)、不稳定的(比如待排序元素为 [5, 5, 3],把 3 和 5 交换就完成了排序,但是原先在之前的 5 变成了后面)、没有明显的优化思路,
插入排序
插入排序的大概思路是,构建一个排序好的数据,把新的元素插入到适当的位置中去
func insertionSort(arr []int) {
n := len(arr)
for i := 1; i < n; i++ {
// 待排序元素
compare := arr[i]
// 排序好的元素右边界值,因为 arr[i] 是待比较元素,所以 [0,i-1] 这一段数据是已经排好序的
j := i - 1
// 如果元素大于待排序元素就往右挪 腾出位置
for j >= 0 && arr[j] > compare {
arr[j+1] = arr[j]
j--
}
// 把元素放到合适的位置
arr[j+1] = compare
}
}
特点:时间复杂度为 O(n^2)、稳定,适合在元素基本有序时使用,有序时只需要少量判断就可以确定正确的位置
归并排序
归并排序是一种分治算法,把数组逐次对半分,分别对两半进行排序,然后把排序好的两半合并在一起
// 递归地拆分数组并进行归并
func mergeSort(arr []int, l, r int) {
// l == r 时退出 因为此时只有一个元素
if l < r {
// 求中间值
m := (r-l)/2 + l
// 递归左半边
mergeSort(arr, l, m)
// 递归右半边
mergeSort(arr, m+1, r)
// 把排好序地两半合并
merge(arr, l, m, r)
}
}
// 把两个已经排好序的子数组合并成一个有序数组
func merge(arr []int, l, m, r int) {
// 创建切片存放左右两半元素
leftSize := m - l + 1 // 把中间值归左半边
rightSize := r - m
leftArr := make([]int, leftSize)
rightArr := make([]int, rightSize)
// 复制元素 注意是左开右闭区间
copy(leftArr, arr[l:m+1])
copy(rightArr, arr[m+1:r+1])
// 遍历左右半边元素 挑出小的写回到待排序数组中
i, j, k := 0, 0, l
for i < leftSize && j < rightSize {
if leftArr[i] <= rightArr[j] {
arr[k] = leftArr[i]
i++
} else {
arr[k] = rightArr[j]
j++
}
k++
}
// 复制剩余元素 不处理两半不均等时会漏元素
for i < leftSize {
arr[k] = leftArr[i]
i++
k++
}
for j < rightSize {
arr[k] = rightArr[j]
j++
k++
}
}
单看代码有点难理解,用一个例子来看排序的整个过程
比如现在有切片 arr = [2, 3, 1, 5]
通过 mergeSort(arr, 0, len(arr) - 1)
排序,整个流程为:
看图示流程就会比较清晰,先把左半边元素拆到底,逐个合并,使左半边元素有序,再处理右半边,把右半边也处理完,再合并左右半边,最终排序完成
特点:时间复杂度为 O(n log n)、稳定
改进思路:在元素量较小的时候,比如 10 或 20 个元素,使用插入排序比双指针要稍快一点,可以在排序过程中增加一个阈值,当子切片的长度小于该值时,使用插入排序
桶排序
大概思路是生成一些桶,把元素分布到不同的桶中,对每个桶再单独进行排序(可以使用任何排序方法,选择排序、插入排序等等),再将元素依次取出,需要注意,后一个桶中的每一个元素都要大于前一个桶中所有数据
核心点有两个:怎么确定桶的个数?哪个元素放哪个桶如何确定,即桶的索引如何确定?
对于第一个问题,桶排序适用的场景是分布均匀的浮点数,因为浮点数计算桶的索引方便一点,如果分布不均匀,可能会造成某些桶包含大量元素,某些桶只包含很少的元素,导致算法效率降低,一般来说,根据元素个数 n,桶的个数一般在 n^1/2 - n 之间
第二个问题,桶的索引计算一般基于最小值和最大值来完成,一个简单的索引计算方法:
index := int((value - minValue) / (maxValue - minValue) * float64(bucketCount))
-
value
: 当前要处理的元素 -
minValue
: 在整个数组中的最小值 -
maxValue
: 在整个数组中的最大值 -
bucketCount
: 总的桶的数量
简单解释这条公式,(value - minValue) / (maxValue - minValue)
意思是当前值减去最小值并除以范围(最大值和最小值的差值),把元素的值定位到 [0, 1] 的范围中,这样就可以获得元素相对于整个数据的位置,拿这个值乘以桶的总数,然后用 int()
转换为 int 类型作为桶的下标索引,这样就完成了简单的定位操作
完整代码:
func bucketSort(arr []float64, bucketCount int) []float64 {
if len(arr) == 0 {
return arr
}
// 找最大值和最小值
minValue, maxValue := arr[0], arr[0]
for _, value := range arr {
if value < minValue {
minValue = value
}
if value > maxValue {
maxValue = value
}
}
// 初始化桶 用切片表示
buckets := make([][]float64, bucketCount)
// 把元素分配到桶里
for _, value := range arr {
// 用公式计算桶的索引
index := int((value - minValue) / (maxValue - minValue) * float64(bucketCount))
// 如果计算出的索引越界了 手动减一
if index >= bucketCount {
index = bucketCount - 1
}
// 把元素追加到对应的桶里
buckets[index] = append(buckets[index], value)
}
// 对每个桶进行排序 用哪种排序方法都可以 这里用自带的排序函数
sortedArr := make([]float64, 0)
for _, bucket := range buckets {
sort.Float64s(bucket)
sortedArr = append(sortedArr, bucket...)
}
return sortedArr
}
总结
为避免篇幅过长,简单的介绍了前五种排序算法,总结如下:
参考: