基数排序模板

基础概念

不了解概念的话参考:

  • OI Wiki:基数排序
  • 菜鸟教程:基数排序

基数排序是桶思想排序的一种。

基数排序分:

  • LSD(Least Significant Digit first)低位优先(以下只围绕 LSD 展开)。
  • MSD(Least Significant Digit first) 高位优先。

针对于 MSD 理解上没有 LSD 直观,所以下文只分析 LSD。

核心思想

将要排序的元素按一定规则分桶(桶是有序的),通过从低位到高位循环入桶和出桶实现最终排序。

基数排序能实现排序的关键在于:桶是有序的,入桶和出桶是有序的。

核心步骤

  1. 先按最低位分桶。(桶之间是有序的,放入也是有序的)
  2. 依次从桶中取出元素排列出来。(桶中取数据也是有序的)
  3. 变换 1 步骤的次低位循环 1~2 步,直到最高位完成。

复杂度

  • 要分桶,分桶个数为基数范围的个数,如 0~9 为 10 个,a~z 为 26 个(常数)。
  • 每个桶中的元素数量不固定,所有桶的所有元素个数为 n。
  • 放桶和从桶中取都是基于元素个数的,所以操作次数也为 n 次。

由上,所以,空间复杂度为 O(n),时间复杂度为 O(n),代码上该如何设计呢?

注意:桶思想排序是 O(n) 复杂度的,如果题目中需要 O(n) 复杂度实现排序,应该就要想到桶思想(桶排序、计数排序、基数排序)。

设计考虑

有以下几点考虑:

  • 分桶方法。
  • 桶是有序的。(这个不用多说)
  • 向桶中放数据和取数据是有序的(LSD 下必须是 FIFO 的,才能保证最终排序)。

针对于第一条可能会想到用二维切片来实现,同样看第三条正好每个桶对应一个切片会很合适。
但由于桶内元素是伸缩变化的,如果每个桶用单独的切片来存的话,底层空间的使用量不好控制。

所以可以这样设计:

  • 新分配出和原数列一样长度的空间作为 buf
  • 按 buf 的范围区间进行切分桶
  • FIFO 的放桶取桶顺序可以用下标来控制

代码步骤(LSD)

以对一个数值数组进行排序为例,主要逻辑:

  1. 循环数列找到最大数,根据数位个数确定循环次数
  2. 从低位开始执行下方步骤
  3. 循环原数组,每个元素计算桶号,计数对应桶容量 cnt (注意在下一次循环之间重置)
  4. 确定各个桶在 buf 数组上的区间范围(buf 是原数组的一个等空间数组)
  5. 循环原数组,依次计算桶号,并放入 buf 对应桶内
  6. 替换 buf 和原数组,跳转到 2 步骤开始下一次循环

在第 4 步中获得 buf 中每个桶的最末位置比较容易,所以在第 5 步中从桶的后面向前放元素比较容易

上面的逻辑太细,以至于不好记,记下面的流程(啰嗦一下):

  1. 确定循环次数
  2. 在同等空间的数列上划分桶(包含计数及切分区间)
  3. 再循环原数组,依次将对应桶数据放入桶(注意顺序)
  4. 进行下次循环

注意:不要想着把好几个 for range nums 的循环合并成 1 个,因为这里多少个 n 次的循环是个常数量,所以量级还是 O(n)。

go 代码实现

(当前此模板只支持正数,可进行正负分类去做)

func sortByRadix(nums []int) []int {
    n := len(nums)

    maxVal := getMax(nums...)                  // 获取最大值,用于计算循环次数
    times := 0                                 // 循环次数(最大数值的位数)
    for temp := maxVal; temp > 0; temp /= 10 { // 计算循环次数
        times++
    }

    buf := make([]int, n) // 容器 buf,用于存储每次循环后的元素顺序状态
    division := 1         // 除数,用于计算当前基数值
    cnt := [10]int{}      // 桶标号 => 前期存计数,后期作为对应桶末尾位置的偏移量

    // 核心逻辑,按次数循环
    for i := 1; i <= times; i++ {

        // 计算每个桶内元素的数量
        for _, num := range nums {
            b := (num / division) % 10
            cnt[b]++
        }

        // 转换为桶末尾在这个数列的偏移位置
        for j := 1; j < 10; j++ {
            cnt[j] += cnt[j-1]
        }

        // 将桶内元素依次放回 buf 对应位置
        for j := len(nums) - 1; j >= 0; j-- {
            b := (nums[j] / division) % 10
            cnt[b]--
            buf[cnt[b]] = nums[j]
        }

        // 重置 cnt
        for j := 0; j < 10; j++ {
            cnt[j] = 0
        }

        division *= 10
        nums, buf = buf, nums
    }

    return nums
}

func getMax(a ...int) int {
    max := a[0]
    for _, v := range a[1:] {
        if v > max {
            max = v
        }
    }
    return max
}
// 做第二道题时,再次调整了一下逻辑,代码相同
func radixSort(nums []int) {
    n := len(nums)
    if n <= 1 {
        return
    }

    max := nums[0]// 最大值
    for _, num := range nums[1:] {
        if max < num {
            max = num
        }
    }
    times := 0 // 循环次数
    for max > 0 {
        max /= 10
        times++
    }

    bCnt := [10]int{}
    division := 1
    buf := make([]int, n)
    for t := 1; t <= times; t++ {
        // 重置 bCnt
        for i := 0; i < len(bCnt); i++ {
            bCnt[i] = 0
        }
        // 桶内元素计数
        for _, num := range nums {
            b := num / division % 10
            bCnt[b]++
        }
        // 前缀和,转为偏移量
        for i := 1; i < len(bCnt); i++ {
            bCnt[i] += bCnt[i-1]
        }

        // 放桶,从后往前取 nums 数,从后往前放 buf 内的桶
        for i := n - 1; i >= 0; i-- {
            b := nums[i] / division % 10
            bCnt[b]--
            buf[bCnt[b]] = nums[i]
        }

        // 更新到原数组
        copy(nums, buf)

        division *= 10
    }
}

力扣练习

  • 164. 最大间距 (题解)
  • 912. 排序数组(题解:基数排序练习)

你可能感兴趣的:(基数排序模板)