这是一篇很长的文字.请多多包涵.简而言之,问题是:是否有可行的就地基数排序算法?
我有很多小的固定长度字符串,只使用字母"A","C","G"和"T"(是的,你猜对了:DNA)我想要排序.
目前,我使用的是在STL的所有常见实现中std::sort
使用introsort.这非常有效.但是,我相信,基数排序适合我的问题完全设置,并应工作多在实践中更好.
我已经用非常天真的实现来测试这个假设,并且对于相对较小的输入(大约10,000),这是真的(好吧,至少快两倍以上).但是,当问题规模变大(N > 5,000,000)时,运行时会出现严重下降.
原因很明显:基数排序需要复制整个数据(实际上我的天真实现不止一次).这意味着我已经将~4 GiB放入我的主内存中,这显然会影响性能.即使它没有,我也承担不起使用这么多内存,因为问题规模实际上变得更大.
理想情况下,对于DNA和DNA5(允许额外的通配符"N"),甚至DNA与IUPAC 模糊代码(导致16个不同的值),此算法应该适用于2到100之间的任何字符串长度.但是,我意识到所有这些情况都无法涵盖,所以我对任何提速都感到满意.代码可以动态决定要分派的算法.
不幸的是,关于基数排序的维基百科文章是没用的.关于就地变体的部分是完全垃圾.关于基数排序的NIST-DADS部分几乎不存在.有一篇名为Efficient Adaptive In-Place Radix Sorting的有前途的论文描述了算法"MSL".不幸的是,这篇论文也令人失望.
特别是,有以下几点.
首先,该算法包含几个错误,并留下了许多无法解释的问题.特别是,它没有详细说明递归调用(我只是假设它递增或减少一些指针来计算当前的移位和掩码值).同时,它采用了功能dest_group
并dest_address
没有给出定义.我没有看到如何有效地实现这些(即在O(1)中;至少dest_address
不是微不足道的).
最后但并非最不重要的是,该算法通过使用输入数组内的元素交换数组索引来实现就地.这显然只适用于数值数组.我需要在字符串上使用它.当然,我可以只是强调打字并继续进行,假设内存可以容忍我存储一个不属于它的索引.但这只有在我将字符串压缩到32位存储器(假设32位整数)时才有效.这只有16个字符(暂时忽略16> log(5,000,000)).
其中一位作者的另一篇论文根本没有给出准确的描述,但它给出了MSL的运行时为次线性,这是错误的.
回顾一下:是否有希望找到一个工作的参考实现或至少一个良好的伪代码/描述工作就地基数排序,适用于DNA字符串?
好吧,这是DNA的MSD基数排序的简单实现.它是用D语言编写的,因为这是我最常使用的语言,因此最不可能犯下愚蠢的错误,但它很容易被翻译成其他语言.它就位,但需要2 * seq.length
通过阵列.
void radixSort(string[] seqs, size_t base = 0) {
if(seqs.length == 0)
return;
size_t TPos = seqs.length, APos = 0;
size_t i = 0;
while(i < TPos) {
if(seqs[i][base] == 'A') {
swap(seqs[i], seqs[APos++]);
i++;
}
else if(seqs[i][base] == 'T') {
swap(seqs[i], seqs[--TPos]);
} else i++;
}
i = APos;
size_t CPos = APos;
while(i < TPos) {
if(seqs[i][base] == 'C') {
swap(seqs[i], seqs[CPos++]);
}
i++;
}
if(base < seqs[0].length - 1) {
radixSort(seqs[0..APos], base + 1);
radixSort(seqs[APos..CPos], base + 1);
radixSort(seqs[CPos..TPos], base + 1);
radixSort(seqs[TPos..seqs.length], base + 1);
}
}
显然,这是一种特定的DNA,而不是一般的,但它应该是快速的.
编辑:我很好奇这段代码是否真的有效,所以我在等待自己的生物信息学代码运行时测试/调试它.现在上面的版本实际上已经过测试和运行.对于每个5个碱基的1000万个序列,它比优化的内含子快约3倍.
I've never seen an in-place radix sort, and from the nature of the radix-sort I doubt that it is much faster than a out of place sort as long as the temporary array fits into memory.
Reason:
The sorting does a linear read on the input array, but all writes will be nearly random. From a certain N upwards this boils down to a cache miss per write. This cache miss is what slows down your algorithm. If it's in place or not will not change this effect.
I know that this will not answer your question directly, but if sorting is a bottleneck you may want to have a look at near sorting algorithms as a preprocessing step (the wiki-page on the soft-heap may get you started).
That could give a very nice cache locality boost. A text-book out-of-place radix sort will then perform better. The writes will still be nearly random but at least they will cluster around the same chunks of memory and as such increase the cache hit ratio.
I have no idea if it works out in practice though.
Btw: If you're dealing with DNA strings only: You can compress a char into two bits and pack your data quite a lot. This will cut down the memory requirement by factor four over a naiive representation. Addressing becomes more complex, but the ALU of your CPU has lots of time to spend during all the cache-misses anyway.
您当然可以通过以位为单位对序列进行编码来降低内存要求.您正在考虑排列,因此,对于长度为2,"ACGT"为16状态或4位.对于长度3,这是64个状态,可以以6位编码.因此,对于序列中的每个字母,它看起来像2位,或者像你说的那样,对于16个字符,大约32位.
如果有办法减少有效"单词"的数量,则可以进一步压缩.
因此,对于长度为3的序列,可以创建64个桶,可能是uint32或uint64.将它们初始化为零.迭代你非常大的3个字符序列列表,并按上述方式对它们进行编码.使用此作为下标,并增加该桶.
重复此操作,直到处理完所有序列.
接下来,重新生成列表.
按顺序迭代64个桶,对于在该桶中找到的计数,生成该桶所代表的序列的许多实例.
当所有桶都被迭代后,你就有了你的排序数组.
4的序列,加2位,所以会有256个桶.5的序列,加2位,所以会有1024个桶.
在某些时候,桶的数量将接近你的极限.如果从文件中读取序列,而不是将它们保留在内存中,则可以为存储桶提供更多内存.
我认为这比在原地进行排序要快,因为铲斗可能适合您的工作组.
这是一个显示该技术的黑客
#include
#include
#include
using namespace std;
const int width = 3;
const int bucketCount = exp(width * log(4)) + 1;
int *bucket = NULL;
const char charMap[4] = {'A', 'C', 'G', 'T'};
void setup
(
void
)
{
bucket = new int[bucketCount];
memset(bucket, '\0', bucketCount * sizeof(bucket[0]));
}
void teardown
(
void
)
{
delete[] bucket;
}
void show
(
int encoded
)
{
int z;
int y;
int j;
for (z = width - 1; z >= 0; z--)
{
int n = 1;
for (y = 0; y < z; y++)
n *= 4;
j = encoded % n;
encoded -= j;
encoded /= n;
cout << charMap[encoded];
encoded = j;
}
cout << endl;
}
int main(void)
{
// Sort this sequence
const char *testSequence = "CAGCCCAAAGGGTTTAGACTTGGTGCGCAGCAGTTAAGATTGTTT";
size_t testSequenceLength = strlen(testSequence);
setup();
// load the sequences into the buckets
size_t z;
for (z = 0; z < testSequenceLength; z += width)
{
int encoding = 0;
size_t y;
for (y = 0; y < width; y++)
{
encoding *= 4;
switch (*(testSequence + z + y))
{
case 'A' : encoding += 0; break;
case 'C' : encoding += 1; break;
case 'G' : encoding += 2; break;
case 'T' : encoding += 3; break;
default : abort();
};
}
bucket[encoding]++;
}
/* show the sorted sequences */
for (z = 0; z < bucketCount; z++)
{
while (bucket[z] > 0)
{
show(z);
bucket[z]--;
}
}
teardown();
return 0;
}
如果您的数据集太大,那么我认为基于磁盘的缓冲区方法最好:
sort(Listelements, int prefix) if (elements.Count < THRESHOLD) return InMemoryRadixSort(elements, prefix) else return DiskBackedRadixSort(elements, prefix) DiskBackedRadixSort(elements, prefix) DiskBackedBuffer [] buckets foreach (element in elements) buckets[element.MSB(prefix)].Add(element); List ret foreach (bucket in buckets) ret.Add(sort(bucket, prefix + 1)) return ret
我还会尝试分组到更多数量的存储桶,例如,如果您的字符串是:
GATTACA
第一个MSB调用将返回GATT的桶(总共256个桶),这样就可以减少基于磁盘的缓冲区的分支.这可能会也可能不会提高性能,所以请尝试一下.
我打算走出去,建议你切换到堆/ 堆实现.这个建议有一些假设:
您可以控制数据的读取
一旦您开始对其进行排序,您就可以对已排序的数据执行有意义的操作.
堆/堆排序的优点在于,您可以在读取数据时构建堆,并且可以在构建堆时开始获取结果.
我们退一步吧.如果您是如此幸运,您可以异步读取数据(也就是说,您可以发布某种读取请求并在准备好某些数据时收到通知),然后您可以在等待时创建堆的一大块即将从磁盘进入的下一个数据块.通常,这种方法可以将一半排序的大部分成本埋在获取数据所花费的时间之后.
读取数据后,第一个元素已经可用.根据您发送数据的位置,这可能很棒.如果要将其发送到另一个异步读取器或某个并行"事件"模型或UI,则可以随时发送块和块.
也就是说 - 如果你无法控制如何读取数据,并且它是同步读取的,并且在完全写出之前你没有使用已排序的数据 - 忽略所有这些.:(
查看维基百科文章:
堆排序
二进制堆