algo-桶排序

本文详细介绍了桶排序的工作原理,包括其在不同情况下的时间复杂度分析,以及基数排序和MSDRadixSort的区别。重点讲解了如何优化桶排序以应对不同输入分布,以及适用的场景和局限性。
摘要由CSDN通过智能技术生成

桶排序

桶排序是一种分布排序,将元素数组分到多个桶内,然后每个桶再分别进行排序。

其计算复杂度取决于对桶内排序所用算法、使用桶数量以及输入均匀度。

主要流程如下

  1. 建立空桶数组
  2. 将原始数组发布到各桶中
  3. 对非空桶进行排序
  4. 按照顺序从非空桶里面收集元素得到排序后的数组
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) / bucketRange
		if 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 - 1
		for j >= 0 && arr[j] > key {
			arr[j+1] = arr[j]
			j--
		}
		arr[j+1] = key
	}
	return arr
}

时间复杂度分析

最坏情况下如果数据分散聚集分布,那么就很多集中在极少量的桶内,这就会使得复杂度完全取决于桶内排序算法

在均匀分布情况

  1. 初始化桶,并找到最大值

    O(n)

  2. 分布到桶

    O(n)

  3. 若桶内排序位插入排序

    O ( n 2 k + n ) O(\frac{n^2}{k}+n) O(kn2+n),其中k为桶数量

  4. 聚合成排序数组

    O(k)

优化

一种常见的优化是先将桶中未排序的元素放回原始数组中,然后对整个数组运行插入排序。因为插入排序的运行时间是基于每个元素与最终位置的距离,所以比较的次数仍然相对较少,并且通过将列表连续存储在内存中可以更好地利用内存层次结构

如果输入分布是已知的或可以估计的,通常可以选择包含恒定密度的桶(而不仅仅是具有恒定大小)。这便能在即使分布非均匀情况下,也能保证性能

基数排序

基数排序可以理解为一种特别的桶排序,桶分布的规则是根据数字或者映射后数字的位数来决定的,而桶内排序则是由所有位的位数排序来达成的

时间复杂度为O(nw),n为数据量,w则为最长位数

  1. 将桶分为10个
  2. 从最低位开始,根据位的数字分配到相应桶
  3. 这样从最低位到最高位排序完成之后就得到一个有序数组。

由于基数排序是根据之前排序完成之后的顺序去进行下一位的排序,因此在下一位相同的情况下,之前位的顺序依然是相对一致的。

代码实现如下

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 % 10
		output[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)提出了新的思路。其从高位到低位进行递归排序

主要思路就是

  1. 根据当前位分到不同桶
  2. 然后对于大于2的桶,递归下一个低位进行排序
  3. 最后将所有排序好的桶组合到返回值

相比于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) % 10
		buckets[digit] = append(buckets[digit], num)
	}

	// 对每个桶往下位递归基数排序
	var i int
	for _, bucket := range buckets {
		MSDRadixSort(bucket, d-1)
		copy(nums[i:], bucket)
		i += len(bucket)
	}
}

func power(base, exp int) int {
	ans := 1
	for i := 0; i < exp; i++ {
		ans *= base
	}
	return ans
}

应用场景

  • 适用于对空间不太敏感的场景且数据分布不是特别聚集
  • 对于大量数据难以一次在内存中排序的

Ref

  1. https://en.wikipedia.org/wiki/Bucket_sort
  2. https://en.wikipedia.org/wiki/Radix_sort
  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值