桶排序
桶排序是一种分布排序,将元素数组分到多个桶内,然后每个桶再分别进行排序。
其计算复杂度取决于对桶内排序所用算法、使用桶数量以及输入均匀度。
主要流程如下
- 建立空桶数组
- 将原始数组发布到各桶中
- 对非空桶进行排序
- 按照顺序从非空桶里面收集元素得到排序后的数组
func bucketSort(arr []int, numBuckets int) []int {if len(arr) <= 1 {return arr}// 根据数据最大最小值,确定桶范围minVal, maxVal := findMinMax(arr)bucketRange := (maxVal - minVal + 1) / numBuckets// 初始化桶buckets := make([][]int, numBuckets)for i := range buckets {buckets[i] = make([]int, 0)}// 分布元素到桶中for _, num := range arr {bucketIndex := (num - minVal) / bucketRangeif bucketIndex == numBuckets {bucketIndex-- // Adjusting for the last bucket}buckets[bucketIndex] = append(buckets[bucketIndex], num)}// 桶内排序for _, bucket := range buckets {bucket = insertionSort(bucket)}// 连接桶元素为排序数组sortedArr := make([]int, 0, len(arr))for _, bucket := range buckets {sortedArr = append(sortedArr, bucket...)}return sortedArr
}func findMinMax(arr []int) (int, int) {minVal, maxVal := arr[0], arr[0]for _, num := range arr {if num < minVal {minVal = num}if num > maxVal {maxVal = num}}return minVal, maxVal
}func insertionSort(arr []int) []int {for i := 1; i < len(arr); i++ {key := arr[i]j := i - 1for j >= 0 && arr[j] > key {arr[j+1] = arr[j]j--}arr[j+1] = key}return arr
}
时间复杂度分析
最坏情况下如果数据分散聚集分布,那么就很多集中在极少量的桶内,这就会使得复杂度完全取决于桶内排序算法
在均匀分布情况
-
初始化桶,并找到最大值
O(n)
-
分布到桶
O(n)
-
若桶内排序位插入排序
O ( n 2 k + n ) O(\frac{n^2}{k}+n) O(kn2+n),其中k为桶数量
-
聚合成排序数组
O(k)
优化
一种常见的优化是先将桶中未排序的元素放回原始数组中,然后对整个数组运行插入排序。因为插入排序的运行时间是基于每个元素与最终位置的距离,所以比较的次数仍然相对较少,并且通过将列表连续存储在内存中可以更好地利用内存层次结构
如果输入分布是已知的或可以估计的,通常可以选择包含恒定密度的桶(而不仅仅是具有恒定大小)。这便能在即使分布非均匀情况下,也能保证性能
基数排序
基数排序可以理解为一种特别的桶排序,桶分布的规则是根据数字或者映射后数字的位数来决定的,而桶内排序则是由所有位的位数排序来达成的
时间复杂度为O(nw),n为数据量,w则为最长位数
- 将桶分为10个
- 从最低位开始,根据位的数字分配到相应桶
- 这样从最低位到最高位排序完成之后就得到一个有序数组。
由于基数排序是根据之前排序完成之后的顺序去进行下一位的排序,因此在下一位相同的情况下,之前位的顺序依然是相对一致的。
代码实现如下
func radixSort(nums []int) []int {mx := getMax(nums)exp := 1// 从低位到高位进行排序for mx/exp > 0 {nums = countSort(nums, exp)exp *= 10}return nums
}func countSort(nums []int, exp int) []int {n := len(nums)output := make([]int, n)count := make([]int, 10)// 统计当前位各个数字的个数for i := 0; i < n; i++ {count[nums[i]/exp%10]++}// 计算小于等于当前数字的总个数for i := 1; i < 10; i++ {count[i] += count[i-1]}// 根据之前小于等于的总个数获取到在当轮基数排序中的位置for i := n - 1; i >= 0; i-- {j := nums[i] / exp % 10output[count[j]-1] = nums[i]count[j]--}// 更新排序后的数组for i := 0; i < n; i++ {nums[i] = output[i]}return nums
}func getMax(nums []int) int {mx := nums[0]for i := 1; i < len(nums); i++ {if nums[i] > mx {mx = nums[i]}}return mx
}
基数排序在元素范围较小时非常快,但要求更多的空间,以及缺少更多的灵活性
一种特别的应用场景是适合于顺序访问
MSD
MSD(Most Significant Digit)相比于上面的LSD(Least SIgnificant Digit)提出了新的思路。其从高位到低位进行递归排序
主要思路就是
- 根据当前位分到不同桶
- 然后对于大于2的桶,递归下一个低位进行排序
- 最后将所有排序好的桶组合到返回值
相比于LSD,MSD适合不定长的变量,但不稳定(也可以稳定,但实现更为复杂),性能更差些
另一个MSD独特的优势点就是MSD能够更好利用并发
func MSDRadixSort(nums []int, d int) {// 若已经到达最低位,或者桶内元素低于两个,直接返回if len(nums) <= 1 || d <= 0 {return}// 根据当前位放置到桶里buckets := make([][]int, 10)for i := 0; i < len(buckets); i++ {buckets[i] = make([]int, 0)}for _, num := range nums {digit := num / power(10, d-1) % 10buckets[digit] = append(buckets[digit], num)}// 对每个桶往下位递归基数排序var i intfor _, bucket := range buckets {MSDRadixSort(bucket, d-1)copy(nums[i:], bucket)i += len(bucket)}
}func power(base, exp int) int {ans := 1for i := 0; i < exp; i++ {ans *= base}return ans
}
应用场景
- 适用于对空间不太敏感的场景且数据分布不是特别聚集
- 对于大量数据难以一次在内存中排序的
Ref
- https://en.wikipedia.org/wiki/Bucket_sort
- https://en.wikipedia.org/wiki/Radix_sort