快速对字节数组进行原地排序

5
我是一个有用的助手,可以为您翻译内容。
我遇到了一个小问题,并且找不到一个令人满意的解决方案。有一个字节数组,我需要按高7位对这些字节进行排序,同时保留低位的顺序。
所以最初它看起来像这样:
// sort buf[N] to tmp[N]
uint offs[128+1]; uint c,i,s;
for( i=0; i<128; i++ ) offs[i]=0;
for( i=0; i<l; i++ ) offs[buf[i]>>1]++;
for( i=0,s=0; i<128; i++ ) c=offs[i], offs[i]=s, s+=c; offs[i]=s;

byte* tmp = new byte[N];
for( i=0; i<N; i++ ) c=buf[i], tmp[offs[c>>1]++]=c; // sort

但是这些块足够大(目前为8M),我想使用多个线程,每个线程额外使用8M的内存会很明显。

因此,我尝试使用一些简单的基数排序:

void radix( byte* buf, uint h, uint l, uint mask ) {
  uint p = (h+l)>>1, q = h; 
  uint i = offs[h], j = offs[l]-1; h = offs[p]; 
  if( (i<h) && (j>=h) ) {
    byte c = buf[i], d = buf[j];
    while( (i<h) && (j>=h) ) {
      while( (c&mask)==0 ) c = buf[++i]; // find value with bit 1
      while( (d&mask)!=0 ) d = buf[--j]; // find value with bit 0
      buf[i]=d; buf[j]=c; // swap 1-0 -> 0-1
      c = buf[++i]; d = buf[--j];
    }
    if( mask>=4 ) {
      radix( buf, q,p, mask>>1 );
      radix( buf, p,l, mask>>1 );
    }
  }
}

但是它会改变这些低位的顺序,导致无法使用。

实际上,一些更简单的方法,如冒泡排序,可以做到我想要的效果,但它们速度较慢,而速度也是一个问题。

因此,目前我通过临时缓冲区对较小的块进行排序,然后使用索引表按顺序访问部分排序的块:

struct tmpsort {

  enum{ blocksize = (1<<16)-1 };

  unsigned short ofs[(max_quants+blocksize-1)/blocksize][probN];

  tmpsort( byte* buf, uint f_len ) {
    uint i,j,k;
    uint freq[2*probN]; // prob freqs
    byte tmp[blocksize+1];

    for( k=0,j=0; k<f_len; k+=blocksize,j++ ) {
      uint l = Min(k+blocksize,f_len)-k;
      byte* p = &buf[k];

      // compute offsets of sorted chunks
      for( i=0; i<2*probN; i++ ) freq[i]=0;
      for( i=0; i<l; i++ ) freq[p[i]]++;
      for( i=0; i<probN; i++ ) freq[i+1]=freq[2*i+0]+freq[2*i+1]; // 1=0+1, 2=2+3, 3=4+5
      freq[0] = 0;
      for( i=0; i<probN; i++ ) freq[i+1]+=freq[i];
      for( i=0; i<probN; i++ ) ofs[j][i]=freq[i+1];

      // sort the block via tmp
      for( i=0; i<l; i++ ) { byte c=p[i]; tmp[freq[c>>1]++]=c; }
      for( i=0; i<l; i++ ) p[i]=tmp[i];
    }
  }

};

[...]

tmpsort ts( buf, f_len );
for( i=0; i<probN; i++ ) {
  for( k=0,j=0; k<f_len; k+=ts.blocksize,j++ ) {
    uint x = i>0 ? ts.ofs[j][i-1] : 0;
    for(; x<ts.ofs[j][i]; x++ ) putc( buf[k+x],g );
  }
}

但是tmp[]和ofs[]数组使用了太多的堆栈空间,而且它并不是一个完整的排序,所以我一直在想是否有什么巧妙的解决方法。

这里提供了数据样本和我的实现代码: http://nishi.dreamhosters.com/u/tmpsort_v0.rar

4个回答

1

使用基数排序的一种版本,对每个重要的7位进行稳定排序,从最不重要的位到最重要的位,可以在略多于O(n log n)的时间内通过相对简单的代码实现。与稳定的原地归并排序相比,这种技术的优点在于,如果您自己编写所有代码,那么代码会简单得多。

下面是执行指定位的原地稳定排序的函数。为了简单起见,此处使用递归方式编写,使用O(lg n)的堆栈空间(如果您想使用for循环来组织分治方法,则可以消除此堆栈空间使用):

// sort array x from i to j by bit b
sort(x, i, j, b) {
  if (i >= j - 1) return;
  mid = (i + j) / 2;
  sort(x, i, mid, b);
  sort(x, mid, j, b);
  first1 = -1;
  last0 = -1;
  for (k = i; k < j; k++) {
    if (first1 < 0 && isSet(x[k], b)) first1 = k;
    if (!isSet(x[k], b)) last0 = k;
  }
  if (last0 < first1) return;

  // the sequence of bit b generally looks something like 0000011100000111111
  // so we reverse from the first 1 to the last 0
  reverse(x, first1, last0afterfirst1);
  newlast0 = first1;
  while (!isSet(x[++newlast0], b));
  newlast0--;

  // the elements in the range first1..last0 are in the wrong order, so reverse
  reverse(x, first1, newlast0);
  reverse(x, newlast0 + 1, last0);
}

isSet函数用于测试位是否被设置,reverse函数用于原地数组反转。上述排序子程序按以下方式对每个位进行调用(如基数排序):

sort(x) {
  for (b = 1; b < 8; b++) {
    sort(x, 0, n, b);
  }
}

总运行时间为“O(7 * n log n)”。如果将此算法推广,额外的因素7可能是可变的。


谢谢,但是从我的评论中可以看出来,我已经知道这个了,而且你的实现看起来比我想象的还要慢 :). 此外,在这种情况下,Nlog(N)是相当糟糕的,因为log2(8M)是23。事实上,7238M甚至比找到所有匹配键所需的1288M更糟糕。 - Shelwien
哦,好的,我以为你唯一的抱怨是它不是稳定排序。 - jonderry

1

为什么不使用任何标准的就地、稳定排序算法,例如插入排序,并实现一个适当的比较函数呢?


使用两个缓冲区的解决方案需要进行N次读取和N次写入。 我需要快速处理这里的问题,标准排序实现并不适用于字节排序。 - Shelwien

0

快速排序可以作为稳定排序的一种实现方式。从大O的角度来看,它并不比插入排序更好,但在实践中,它的表现要好得多。如果你硬编码排序网络,使其适用于大小为6或8的叶子节点,我认为这是实现稳定的原地排序所能达到的最佳性能。

实际上...据说有一种原地、稳定的归并排序。从理论上讲,它具有理想的特性——原地、真正的O(n log n)和稳定性,同时具备。但我怀疑它很难实现,并且有相当大的常数项与大O一起使用。


我认为这里只有128个不同的键非常重要。此外,我考虑在这里实现一种位合并排序(0(10)1 -> 0011通过xy=reverse(reverse(y)+reverse(x))),但与那个单行循环相比,它似乎太慢了... - Shelwien
顺便提一下,使用带有额外缓冲区的第一个版本处理100M文件需要15.610秒,而使用上述的“tmpsort”则需要17.594秒。 - Shelwien
是的,但你想要保持顺序的那些低位仍然包含了大量信息;保留它们并不是免费的。如果您不介意使用单独的输出缓冲区,我有一个快速算法,我会在另一个答案中发布。 - R.. GitHub STOP HELPING ICE
我只在输入字节为8M时不介意使用64k这样的大小。或者如果可以以排序的方式处理这些最低有效位,而无需实际存储它们,那么这种方法才比我的第一种解决方案更好,该方案需要额外的N字节缓冲区(实际上可以是N位,但仍然太大)。 - Shelwien
我可以看到用1MB的空间来处理8MB的输入数据(将最低有效位存储在位数组中)是可行的,但我没有立即看到这种小常数或log-N类型的空间如何帮助你节省任何东西。 - R.. GitHub STOP HELPING ICE
与有额外缓冲区且只进行2N读取+N写入的版本相比,几乎任何事情都会变得更慢...而10N则慢了10倍,所以这里很注重小常数-请注意,我的最终版本最多为3N+ 2N(如果我们忽略L1和内存之间的差异),即使如此速度也已降低了15%。 - Shelwien

0

如果你有额外的64kB,你可以(如你所见)以压缩形式存储一个512 kbit块(减去一些固定的索引数据)。遍历大块并将它们转换为它们的压缩排序形式,在整个数组的开头进行压缩。

现在将压缩形式合并成一个大的压缩形式(使用7M轻松完成)。然后将其解压回排序数组。

这是O(N),尽管常数看起来相当大,需要进行3次涉及一些非平凡位操作的传递。


谢谢,我真的很喜欢这个方法,值得一试。 - Shelwien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接