我正在尝试实现基数排序算法。 我使用链表来存储要排序的元素,然后我将每个元素都抛到它的桶中。这个桶只是一个指针,它将链接属于其存储桶的元素列表。
我正在区间[0,1000000]中测试10.000.000和100.000.000个整数。这个数字可以是新月,渐弱和随机顺序。
新月和新月订单中100.000.000个数字的运行时间约为20秒。但对于具有随机顺序的相同数量的元素 运行时间约为110秒。
据我了解,该算法对于任何质量都具有相同的复杂性 要排序的数据。
任何人都知道为什么会这样吗?
这是我的代码:
void radix(Number** numbers)
{
unsigned int i, k, e = 1;
Number* bucket[10];
Number* tail[10];
Number* index;
for(k = 0; k < 7; k++, e *= 10)
{
for(i = 0; i < 10; i++) bucket[i] = tail[i] = NULL;
index = *numbers;
while(index != NULL)
{
i = (index->value / e) % 10;
if(tail[i] == NULL)
bucket[i] = index;
else
tail[i]->next = index;
tail[i] = index;
index = index->next;
}
for(i = 0; i < 10; i++)
{
if(tail[i] != NULL)
{
*numbers = bucket[i];
index = tail[i];
for(i++; i < 10; i++)
{
if(tail[i] != NULL)
{
index->next = bucket[i];
index = tail[i];
}
}
}
}
index->next = NULL;
}
}
其中Number
是:
typedef struct number
{
unsigned int value;
struct number* next;
} Number;
答案 0 :(得分:2)
答案可能与内存访问和引用位置有关。
升序/降序有一个规则的模式,它可能具有更大的时间局部性,而不是关于桶,但可能更多关于链表节点用于数字的方式(特别是如果它们不是不连续的。
例如,如果我们接受输入:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ...
我们从桶0循环到桶9然后回到桶0.当我们回到桶0时,我们正在访问最近访问过的number
节点(仅仅9次迭代)可能会被缓存到更快,更小的内存中。
如果我们使用随机排序,谁知道我们何时会回到0?因此,在我们回到用于任何给定存储桶头部的数据的内存之前,我们有更长的时间将数据从DRAM移动到缓存。结果可以转化为更多此类前number
节点从缓存中逐出,并且当我们回到这样的存储桶时会有更多缓存未命中。
对于不规则排序,分支误预测也可能消耗一些时间。分析应该有助于缩小原因。
如果您确实存在内存瓶颈,可以尝试的一件事就是将您的存储桶转换为,例如,您将深度复制数字的展开列表。这将使你不再访问以前插入的数字的内存,这些数字可能已被插入多次迭代(由于随机排序而成为一个潜在的大变量)。有了这个,我们开始回到一些时间局部性(如果数字是连续分配的话可能是空间局部性),否则我们将失去这种链表表示。然后,重新使用桶的连续内存(其中只有10个)而不是桶之间的元素,其间具有可变步幅。我们还通过展开的表示在桶内获得空间局部性。
但如果是相同的数据,只是订单不同,这会影响到 很多?对于相同的数据,20到110秒太多了。
内存效率可以在数量级上产生差异。 http://lwn.net/Articles/250967/
我不是这个主题的专家(更多的是“简介它并尝试根据指南进行优化”类型),但根据过去的结果我从内存优化中获得,我会经常把它们放在一边在算法优化的效果方面。例外情况是复杂性的差异是粗略的(例如,线性与二次),但即使是线性算法也非常可行地击败具有非常大输入的线性算法,如果前者对缓存更友好的话。