文章

桶排序Bucketsort和基数排序Radixsort

在学习完Mergesort和Quicksort之后,我们会发现他们的时间复杂度似乎已经突破不了 $\Theta(nln(n))$ ,这其实是这类排序算法的局限。

这类排序算法的本质其实是通过元素间的移动、交换来最终实现排序。理论已经证明了基于元素交换的排序算法存在速度上限 $\Theta(nln(n))$ ,因此我们无法尝试再去在元素交换的基础上去提高速度。

能不能换种方法继续提高速度?

答案是可以的,但是我们必须要牺牲算法的适用性。当我们给某个排序算法做出一些假设时,可以更加快速的实现排序。

桶排序Bucketsort

假设数据(如整数)落在某个已知的特定范围内,然后我们为这个范围内可能存在的数据都创建一个bucket(桶),用于储存 bit 值(0 or 1)。整个数据存储为一个 bitsVector

具体的排序流程: P355

  • 根据数据范围创建每个值对应的桶;
  • 过一遍数据,并在对应的桶(索引)进行标注,通过位运算将相应位置设置为1;
  • 然后遍历所有桶,输出存在的值,完成排序;

但是这样还存在一个严重的问题,简单的位向量(每个桶只能表示“有”或“没有”)无法正确处理重复项。

那么将其改进为 Countingsort,计数排序。每个桶添加:

  • 一个计数器
  • 一个链表 计数器用于记录该数据的数量,链表则可以更加准确的区分这些重复元素(如区分 $a_1,a_2,a_3 = 1$ )

通过这样简单的两个过程就实现了排序,这两个算法的时间复杂度都是 $\Theta(n+m)$ 。 $n$ 是元素个数,$m$ 是创建的桶的数量(即数据范围)(过一遍已有数据+遍历所有桶)

但是对于Bucketsort,$m$ 不可能小于 $n$,因为不允许有重复项。

并且对于桶排序,通常要求数据分布比较均匀和稠密,因为他是根据数据范围来创建的; 试想一下:排序一个电话号码 [0,1,2,100000000] ,这样岂不是要创建0-10000000个桶?这对空间的占用和浪费是非常巨大的。

基数排序 (Radix Sort)

如果直接对 10 位数字使用桶排序,将需要 $10^{10}$ 个桶,这在内存上(约 9 GiB)是不切实际的,而基数排序则通过逐位进行桶排序来解决这个问题。

流程: P372 例子

  • 创建 $k$ 个队列(桶),$k$ 是基数(例如,十进制为 10,二进制为 2);
  • 从最低位开始向上迭代(从个位开始);
  • 每次迭代都是直接对当前位使用桶排序,按顺序输出一个新的列表;
  • 重复直到处理完最高位;

为什么可以这样做?

  • 队列保证了当前位一样的元素都可以被放进桶中,每个队列都要有 $n$ 个位置;
  • 如果当前位的数不一样(例如 $a_i < b_i$),那么在桶排序的作用下,$a$ 会被放入较低的桶中,因此会比 $b$ 先输出,新的输出列表相当于根据当前位被重新桶排序了;

为什么要从最低位开始?能不能从最高位开始?

必须从最低位开始! 因为不断往上进位再桶排序的过程实际上是在寻找决定性的数位。 比如 309 和 209 进行比较,即使前两位相同,但是百位的 $3>2$ 彻底杀死比赛,桶排序时309一定比209靠后。如果从最高位开始往最低位走,那么如果两个数的各位不同(206,581),就会因为最后 $6>1$ 而导致206在581后。

基数排序的时间复杂度在全部情况下都是 $\Theta(n \ln(m))$ ,$n$ 的数据量,$m$ 数据范围(对于范围 $m$,位数大约是 $\log(m)$(例如 $\log_2(m)$ 或 $\log_{10}(m)$)),全部位都要走一遍。

十进制好还是二进制好?

  • 十进制会浪费 $10n-n=9n$ 的空间;
  • 二进制浪费 $2n-n=n$ 的空间; 二进制会更好。

对于二进制来说,只有两个桶(队列),0和1;并且每个队列的位置都有 n 个。 但是这样还是会有 n 的空间被浪费,那么此时我们只需要将队列改为双端队列(Deque) 即可实现通过一个队列放置0和1的队列。 双端队列实现radix

本文由作者按照 CC BY 4.0 进行授权