我有一组字符串。其中 90% 是以 . 开头的 URL "http://www."
。我想按字母顺序对它们进行排序。
目前我使用 C++ std::sort()。但是 std::sort 是基于比较的快速排序的一种变体,比较两个具有长公共前缀的字符串效率不高。但是(我认为)基数排序也不起作用,因为大多数字符串都放在同一个桶中,因为公共前缀很长。
对于这个问题,有没有比普通快速排序/基数排序更好的算法?
我有一组字符串。其中 90% 是以 . 开头的 URL "http://www."
。我想按字母顺序对它们进行排序。
目前我使用 C++ std::sort()。但是 std::sort 是基于比较的快速排序的一种变体,比较两个具有长公共前缀的字符串效率不高。但是(我认为)基数排序也不起作用,因为大多数字符串都放在同一个桶中,因为公共前缀很长。
对于这个问题,有没有比普通快速排序/基数排序更好的算法?
我怀疑,当您考虑 URL 的平均长度时,您尝试利用每个 URL 大约 10 个字符的公共前缀所花费的处理时间甚至不会为自己付出代价。
只需尝试完全标准的排序。如果这还不够快,请考虑并行化或分发完全标准的排序。这是一种可行的简单方法。
Common Prefixes 似乎自然地暗示了 trie 数据结构可能是有用的。所以想法是构建所有单词的 trie,然后对每个节点进行排序。排序应该是特定节点的子节点驻留在列表中并进行排序。这可以很容易地完成,因为在特定节点上我们只需要对子节点进行排序,因此自然会出现递归解决方案。更多灵感请参见:http: //goanna.cs.rmit.edu.au/~jz/fulltext/acsc03sz.pdf
最后我发现三元快速排序效果很好。我在www.larsson.dogma.net/qsufsort.c找到了该算法。
这是我修改后的实现,具有与 std::sort 类似的接口。它比我的机器和数据集上的 std::sort 快约 40%。
#include <iterator>
template<class RandIt> static inline void multiway_qsort(RandIt beg, RandIt end, size_t depth = 0, size_t step_len = 6) {
if(beg + 1 >= end) {
return;
}
struct { /* implement bounded comparing */
inline int operator() (
const typename std::iterator_traits<RandIt>::value_type& a,
const typename std::iterator_traits<RandIt>::value_type& b, size_t depth, size_t step_len) {
for(size_t i = 0; i < step_len; i++) {
if(a[depth + i] == b[depth + i] && a[depth + i] == 0) return 0;
if(a[depth + i] < b[depth + i]) return +1;
if(a[depth + i] > b[depth + i]) return -1;
}
return 0;
}
} bounded_cmp;
RandIt i = beg;
RandIt j = beg + std::distance(beg, end) / 2;
RandIt k = end - 1;
typename std::iterator_traits<RandIt>::value_type key = ( /* median of l,m,r */
bounded_cmp(*i, *j, depth, step_len) > 0 ?
(bounded_cmp(*i, *k, depth, step_len) > 0 ? (bounded_cmp(*j, *k, depth, step_len) > 0 ? *j : *k) : *i) :
(bounded_cmp(*i, *k, depth, step_len) < 0 ? (bounded_cmp(*j, *k, depth, step_len) < 0 ? *j : *k) : *i));
/* 3-way partition */
for(j = i; j <= k; ++j) {
switch(bounded_cmp(*j, key, depth, step_len)) {
case +1: std::iter_swap(i, j); ++i; break;
case -1: std::iter_swap(k, j); --k; --j; break;
}
}
++k;
if(beg + 1 < i) multiway_qsort(beg, i, depth, step_len); /* recursively sort [x > pivot] subset */
if(end + 1 > k) multiway_qsort(k, end, depth, step_len); /* recursively sort [x < pivot] subset */
/* recursively sort [x == pivot] subset with higher depth */
if(i < k && (*i)[depth] != 0) {
multiway_qsort(i, k, depth + step_len, step_len);
}
return;
}
创建两个组:有前缀的和没有前缀的。对于第一组删除前缀,排序并添加前缀。对于第二组只是排序。之后将第二组分为前前缀和后前缀。现在连接三个列表(list_2_before、list_1、list_2_after)。
您可以编写自己的自定义代码,在前缀之后开始比较(即在比较时忽略前缀部分),而不是删除和添加第一个列表的前缀。
附录:如果您有多个公共前缀,您可以进一步使用它们来加快速度。最好创建一个带有非常常见前缀的浅树并将它们连接起来。
如果在开始快速排序之前找出向量中的最小值和最大值,那么您总是知道每次调用 partition() 的值的范围,因为分区值要么是最小值,要么是最大值(或至少接近每个子范围的最小值/最大值)和包含分区的最小值和最大值是每个子范围的另一端。
如果子范围的最小值和最大值共享一个公共前缀,那么您可以从公共前缀后面的字符位置开始进行所有分区比较。随着快速排序的进行,范围越来越小,因此它们的公共前缀应该越来越长,并且在比较时忽略它们将节省越来越多的时间。多少,我不知道;你必须对此进行基准测试,看看它是否真的有帮助。
无论如何,额外的开销是相当小的;一次遍历向量以找到最小和最大字符串,每个字符串花费 1.5 次比较 (*),然后检查每个分区以找到该分区的最大共享前缀;检查相当于比较,它可以从包含前缀的最大共享前缀开始,所以它甚至不是一个完整的字符串比较。
令人惊讶的是,工程并行字符串排序论文在 URL 数据集上对大量单线程算法进行了基准测试(参见第 29 页)。Rantala 的带有缓存的多键快速排序变体领先;您可以在本文引用的这个存储库中测试 multikey_cache8 。
我在那篇论文中测试了数据集,如果有任何迹象表明你在前十个字符中几乎看不到一点熵,并且区分了 100 个字符范围内的前缀。执行 100 次基数排序将破坏缓存而几乎没有什么好处,例如,对一百万个 url 进行排序意味着您要在每个键中寻找约 20 个可区分位,代价是约 100 次缓存未命中。
虽然一般基数排序在长字符串上表现不佳,但 Kärkkäinen 和 Rantala 在Engineering Radix Sort for Strings中描述的优化对于 URL 数据集来说已经足够了。特别是他们提前读取了 8 个字符并用字符串指针缓存它们;对这些缓存值进行排序会产生足够的熵来解决缓存未命中问题。
对于较长的字符串,请尝试该存储库中的一些基于 LCP 的算法;根据我的经验,URL 数据集大约处于高度优化的基数类型排序和基于 LCP 的算法之间的收支平衡点,这些算法在较长的字符串上渐进地做得更好。