最高有效位基数排序如何比最低有效位基数排序更有效?
how is the most significant bit radix sort more efficient than the least significant bit radix sort?
我刚刚在阅读以下问题:
Radix sort most significant first or least significant, which is faster?
接受答案的作者暗示 MSD 基数排序确实更快。但是我不明白为什么。
我已经实现了 LSD 和 MSD(通过移位操作基于二进制),LSD 是迭代的,只需要一个桶数组,而 MSD 是递归的,每次递归调用都需要一个单独的桶数组。
如果你创建一个包含 1000 万个整数的随机数组,我看不出 MSD 会比 LSD 快多少,因为你每次重新输入函数时都会分配额外的桶数组,而且你将不得不面对递归调用本身的开销。
我可以看到 MSD 和 LSD 的组合如何提供整体提升(运行 MSD 用于前几位,LSD 用于其余位以减少缓存未命中),但是如何考虑到 MSD 的递归性质以及每次递归调用都需要一个新的桶数组这一事实,单独 MSD 预计会比 LSD 更有效,这与 LSD 不同,LSD 是迭代的并且只需要一个桶数组来完成整个排序过程?
您提到的问题是仅对单个位执行排序。出于这个原因,每次递归只将数组分成两组。因此,在递归时实际上不需要存储太多内容。
你下降的较小的组 - 较大的组,你放入堆栈以供稍后执行。在最坏的情况下,堆栈中最多有 w
个元素,其中 w
是数据类型的宽度(以位为单位)。
现在,已经证明递归并不是那么糟糕在这种情况下,argumentation of Niklas B. 为什么 MSD 有机会运行 更快(即缓存地点)代表。
回答
MSD基数排序的迭代次数取决于输入大小,而LSD基数排序的迭代次数取决于密钥长度。这通常会导致 MSD 基数排序所需的迭代次数明显少于 LSD 基数排序,因此速度更快。
内存分配不是问题,因为 MSD 基数排序可以很容易地就地实现。
理由
我已经实现了 LSD 和 MSD 基数排序,所以我可以看到它们具有哪些属性使 MSD 基数排序比 LSD 基数排序更快。
我在 100.000.000 个随机正 63 位整数数组上将它们的速度与 std::sort 进行了比较(我使用 std::sort 的结果我也用于验证排序数组)并得到以下结果:
- 纯 LSD 排序:10.5 秒
- std::sort : 9.5s
- 纯 MSD 排序:9.3s
- MSD 排序 + insertion_sort : 7.6s
所以,它比std::sort稍微快一点,如果叶子用insertion_sort排序,它会快很多。
为什么 MSD 基数排序可能比 LSD 基数排序更快?
- 有缓存局部性,虽然我怀疑这是否真的很重要,因为 LSD 基数排序也会扫描数组,而不是执行随机访问。
- MSD 基数排序可以实现为 space 复杂度为 O(d·k),因此仅取决于基数 d 和项目的长度k。这个可以在栈上分配,几乎是免费的。因此它基本上是一种就地排序算法。
- 可以修剪底层。 IE。当一个桶只包含 1 个元素时,它已经排序,因此不需要对该桶进行递归。因此,MSD 基数排序只需要执行大约 log(n)/log(d) 次迭代。而 LSD 基数排序总是必须执行 k 次迭代。
我相信最后一点是 MSD 基数排序通常比 LSD 基数排序快的原因。如果输入数据是均匀随机分布的,那么预期的运行时间是O(n log(n)/log(d)),而LSD基数排序的运行时间是O(n k)。通常 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]++;
}
}
}
我刚刚在阅读以下问题: Radix sort most significant first or least significant, which is faster?
接受答案的作者暗示 MSD 基数排序确实更快。但是我不明白为什么。
我已经实现了 LSD 和 MSD(通过移位操作基于二进制),LSD 是迭代的,只需要一个桶数组,而 MSD 是递归的,每次递归调用都需要一个单独的桶数组。
如果你创建一个包含 1000 万个整数的随机数组,我看不出 MSD 会比 LSD 快多少,因为你每次重新输入函数时都会分配额外的桶数组,而且你将不得不面对递归调用本身的开销。
我可以看到 MSD 和 LSD 的组合如何提供整体提升(运行 MSD 用于前几位,LSD 用于其余位以减少缓存未命中),但是如何考虑到 MSD 的递归性质以及每次递归调用都需要一个新的桶数组这一事实,单独 MSD 预计会比 LSD 更有效,这与 LSD 不同,LSD 是迭代的并且只需要一个桶数组来完成整个排序过程?
您提到的问题是仅对单个位执行排序。出于这个原因,每次递归只将数组分成两组。因此,在递归时实际上不需要存储太多内容。
你下降的较小的组 - 较大的组,你放入堆栈以供稍后执行。在最坏的情况下,堆栈中最多有 w
个元素,其中 w
是数据类型的宽度(以位为单位)。
现在,已经证明递归并不是那么糟糕在这种情况下,argumentation of Niklas B. 为什么 MSD 有机会运行 更快(即缓存地点)代表。
回答
MSD基数排序的迭代次数取决于输入大小,而LSD基数排序的迭代次数取决于密钥长度。这通常会导致 MSD 基数排序所需的迭代次数明显少于 LSD 基数排序,因此速度更快。
内存分配不是问题,因为 MSD 基数排序可以很容易地就地实现。
理由
我已经实现了 LSD 和 MSD 基数排序,所以我可以看到它们具有哪些属性使 MSD 基数排序比 LSD 基数排序更快。
我在 100.000.000 个随机正 63 位整数数组上将它们的速度与 std::sort 进行了比较(我使用 std::sort 的结果我也用于验证排序数组)并得到以下结果:
- 纯 LSD 排序:10.5 秒
- std::sort : 9.5s
- 纯 MSD 排序:9.3s
- MSD 排序 + insertion_sort : 7.6s
所以,它比std::sort稍微快一点,如果叶子用insertion_sort排序,它会快很多。
为什么 MSD 基数排序可能比 LSD 基数排序更快?
- 有缓存局部性,虽然我怀疑这是否真的很重要,因为 LSD 基数排序也会扫描数组,而不是执行随机访问。
- MSD 基数排序可以实现为 space 复杂度为 O(d·k),因此仅取决于基数 d 和项目的长度k。这个可以在栈上分配,几乎是免费的。因此它基本上是一种就地排序算法。
- 可以修剪底层。 IE。当一个桶只包含 1 个元素时,它已经排序,因此不需要对该桶进行递归。因此,MSD 基数排序只需要执行大约 log(n)/log(d) 次迭代。而 LSD 基数排序总是必须执行 k 次迭代。
我相信最后一点是 MSD 基数排序通常比 LSD 基数排序快的原因。如果输入数据是均匀随机分布的,那么预期的运行时间是O(n log(n)/log(d)),而LSD基数排序的运行时间是O(n k)。通常 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]++;
}
}
}