十大常见的排序算法有:
-
冒泡排序(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 }
总结
为避免篇幅过长,简单的介绍了前五种排序算法,总结如下:
参考: