Go Book / 5 Data Structure & Algorithms / 11 基本排序算法:桶排序与计数排序

11 基本排序算法:桶排序与计数排序

一、桶排序

原理

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

基本思想

桶排序的思想近乎彻底的分治思想。 桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。 然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。 接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。

补充: 映射函数一般是 f = array[i] / k; k^2 = n; n是所有元素个数

为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

实现逻辑
  • 设置一个定量的数组当作空桶子。
  • 寻访序列,并且把项目一个一个放到对应的桶子去。
  • 对每个不是空的桶子进行排序。
  • 从不是空的桶子里把项目再放回原来的序列中。
动画演示过程

9.桶排序.gif

Go语言描述

桶排序:通过开辟另外空间并分配+收集的排序方法

  • 平均时间复杂度:O(n*k)
  • 最坏时间复杂度:O(n²)
  • 最好时间复杂度:O(n)
  • 空间复杂度:O(n*k)
  • 稳定性:稳定
func BucketSort(list []int, size int) {

	min := list[0]
	max := list[0]

	for i := 0; i < len(list); i++ {
		if min > list[i] {
			min = list[i]
		}
		if max < list[i] {
			max = list[i]
		}
	}

	// 桶切片初始化
	count := make([][]int, (max-min)/size+1)

	// 数据入桶
	for i := 0; i < len(list); i++ {
		count[(list[i]-min)/size] = append(count[(list[i]-min)/size], list[i])
	}

	key := 0
	// 对每个桶进行排序
	for _, bucket := range count {
		if len(bucket) <= 0 {
			continue
		}
		// 插入排序
		InsertionSort(bucket)
		for _, value := range bucket {
			list[key] = value
			key = key + 1
		}
	}
	return
}

二、计数排序

计数排序不是比较数值排序,是记录数据出现次数的一种排序算法。它的原理有点类似桶排序算法,可以看似特殊的桶排序算法。

原理

算法解析:

  • 找出待排数组中最大值;
  • 额外一个数组记录待排数组值出现的次数;
  • 循环打印存储数值次数的数组下标值;
动画演示过程

计数排序.gif

Go语言描述
  • 计数排序 counting sort
  • 平均时间复杂度:O(n*k)
  • 最坏时间复杂度:O(n²)
  • 最好时间复杂度:O(n)
  • 空间复杂度:O(n*k)
  • 稳定性:稳定
package sort

func CountingSort(list []int) {
	// 初始化一个最大值,默认第一元素
	max := list[0]

	l := len(list)
	// 先找出最大值
	for i := 1; i < l; i++ {
		if max < list[i] {
			max = list[i]
		}
	}

	// 记录该值的出现次数
	arr := make([]int, max+1)
	for j := 0; j < l; j++ {
		arr[list[j]]++
	}
	// fmt.Println("arr:",arr)

	pos := 0
	for index, v := range arr {
		for x := 0; x < v; x++ {
			list[pos] = index
			pos++
		}
	}
}

由于计数排序依赖于额外数组保存整数元素出现的次数,需要开辟额外空间存储损耗较大(典型的空间换时间),不适合太过大量和离散的值,只适合小而紧凑的序列值的排序。