4

我只是在阅读以下问题: 基数排序最重要或最不重要,哪个更快?

接受答案的作者建议 MSD 基数排序确实更快。但是我不明白为什么。

我已经实现了 LSD 和 MSD(基于二进制的移位操作),LSD 是迭代的,只需要一个桶数组,而 MSD 是递归的,每次递归调用都需要一个单独的桶数组。

如果您创建一个包含 1000 万个整数的随机数组,我看不出 MSD 将如何比 LSD 快,因为您每次重新进入函数时都会分配额外的桶数组,而且您将不得不面对递归调用的开销他们自己。

我可以看到 MSD 和 LSD 的组合如何提供全面的提升(对前几位运行 MSD,对其余位运行 LSD 以减少缓存未命中),但是单独的 MSD 如何比 LSD 更有效考虑到它的递归性质以及每次递归调用都需要一个新的桶数组这一事实,这与 LSD 不同,它既是迭代的,而且整个排序过程只需要一个桶数组?

4

2 回答 2

5

回答

MSD 基数中的迭代次数取决于输入大小,而 LSD 基数排序中的迭代次数取决于密钥长度。这通常导致 MSD 基数排序比 LSD 基数排序需要更少的迭代,因此速度更快。

内存分配不是问题,因为 MSD 基数排序可以很容易地就地实现。

基本原理

我已经为 LSD 和 MSD 基数排序做了一个实现,所以我可以看到它们具有哪些属性使 MSD 基数排序比 LSD 基数排序更快。

我已经将它们的速度与 std::sort 在 100.000.000 个随机正 63 位整数数组上进行了比较(我使用了 std::sort 的结果,我也用于验证已排序的数组)并得到以下结果:

  • 纯LSD排序:10.5s
  • 标准::排序:9.5s
  • 纯 MSD 排序:9.3s
  • MSD 排序 + 插入排序:7.6 秒

所以,它比 std::sort 稍微快一点,如果叶子是用 insert_sort 排序的,它会快很多。

为什么 MSD 基数排序可能比 LSD 基数排序快?

  • 有缓存局部性,尽管我怀疑这是否真的很重要,因为 LSD 基数排序也会扫描数组,而不是执行随机访问。
  • 可以实现 MSD 基数排序,使其空间复杂度为 O(dk),因此仅取决于基数d和项目k的长度。这可以在堆栈上分配,几乎是免费的。因此,它基本上是一种就地排序算法。
  • 可以修剪底层。即当一个桶只包含1个元素时,它已经排序,因此不需要在那个桶上递归。因此,MSD 基数排序只需要执行大约 log(n)/log(d) 迭代。而 LSD 基数排序总是必须执行 k 次迭代。

我相信最后一点是 MSD 基数排序通常比 LSD 基数排序更快的原因。如果输入数据是均匀随机分布的,那么预期的运行时间是 O(n log(n)/log(d)),而 LSD 基数排序的运行时间是 O(nk)。通常 n 比 k^d 小很多。只有当 n = o(k^d) 时,LSD 基数排序才会更快。但是,在这种情况下,也可以使用计数排序(k=1 的基数排序)。

实施

inline void insertion_sort(int64_t * array, int n) {
  for (int i=1; i<n; i++) {
    int64_t val = array[i];
    int j = i;
    while (j>0 && array[j-1] > val) {
      array[j] = array[j-1];
      j--;
    }
    array[j] = val;
  }
}

void msd_sort(int64_t * array, int n, int64_t bit=60) {
  const int64_t mask = INT64_C(7);
  // Count bucket sizes
  int count[9]={};
  for (int i=0; i<n; i++) {
    count[((array[i]>>bit) & mask)+1]++;
  }
  // Create holes.
  int loc[8];
  int64_t unsorted[8];
  int live = 0;
  for (int i=0; i<8; i++) {
    loc[i] = count[i];
    count[i+1]+=count[i];
    unsorted[live] = array[loc[i]];
    if (loc[i] < count[i+1]) {
      live++;
    }
  }
  live--;
  // Perform sort
  for (int i=0; i<n; i++) {
    int64_t val = unsorted[live];
    int64_t d = (val>>bit) & mask;
    array[loc[d]] = val;
    loc[d]++;
    unsorted[live] = array[loc[d]];
    if (loc[d] == count[d+1]) {
      live--;
    }
  }
  if (bit>0) {
    for (int i=0; i<8; i++) {
      n = count[i+1] - count[i];
      if (n > 20) { // If replaced by n > 1, insertion_sort is not needed.
        msd_sort(array + count[i], n, bit-3);
      } else {
        insertion_sort(array + count[i], n);
      }
    }
  }
}

void lsd_sort(int64_t * array, int n) {
  const int64_t mask = INT64_C(7);
  std::vector<int64_t> buffer(n);
  for (int64_t bit=0; bit<63; bit+=3) {
    // Copy and count
    int count[9]={};
    for (int i=0; i<n; i++) {
      buffer[i] = array[i];
      count[((array[i]>>bit) & mask) + 1]++;
    }
    // Init writer positions
    for (int i=0; i<8; i++) {
      count[i+1]+=count[i];
    }
    // Perform sort
    for (int i=0; i<n; i++) {
      int64_t val = buffer[i];
      int64_t d = (val>>bit) & mask;
      array[count[d]] = val;
      count[d]++;
    }
  }
}
于 2015-03-08T18:56:30.713 回答
1

您所指的问题是仅对单个位执行排序。因此,每次递归仅将数组分成两组。因此,递归时实际上不需要存储太多内容。

您下降的较小组 - 较大的组,您放入堆栈以供稍后执行。在最坏的情况下,w堆栈中最多有元素,其中w是数据类型的宽度(以位为单位)。

现在,已经表明递归在这种情况下并没有那么糟糕,Niklas B.的论点为什么 MSD 有机会运行得更快(即缓存局部性)。

于 2015-03-08T08:14:13.330 回答