基数排序为负整数

时间:2013-03-09 03:04:28

标签: sorting language-agnostic radix-sort radix

我正在尝试为整数实现基数排序,包括负整数。对于非负的int,我计划为数字0-9创建一个10个队列的队列,并实现LSD算法。但我对负整数感到困惑。我现在想的是,继续为他们创建10个队列的另一个队列并分别对它们进行排序,然后在最后,我将给出2个列表,一个包含负的整数排序,另一个包含非负的整数。最后我会合并它们。

您如何看待这个?是否有更有效的方法来处理负整数?

谢谢!

10 个答案:

答案 0 :(得分:25)

您可以将标志视为一种特殊的数字。你对单位进行排序,然后对数十等进行排序,最后在标志上进行排序。这确实会产生负面的反转顺序,然后您只需反转该存储桶的内容即可。这是机械卡分拣机的工作原理。

答案 1 :(得分:5)

请注意,符号位是有符号整数中的最高位,但默认情况下,所有数字都被基数排序视为无符号整数。因此,您需要告诉算法负数小于正数。在32位有符号整数的情况下,您可以先排序三个较低的字节,然后对符号位反转的第四个(较高)字节进行排序,这样0将用于负数而不是1,因此它们将首先出现。

我强烈建议逐字节而不是十进制数字对数字进行排序,因为机器拾取字节要比提取数字容易得多。

答案 2 :(得分:3)

另一个解决方案是将负整数与数组分开,使它们为正数,使用基数排序为正值,然后将其反转并使用已排序的非负数组追加。

答案 3 :(得分:3)

绝对!当然,你必须要把积极的东西分开,但幸运的是这很容易。在排序算法的开始,您所要做的就是围绕值0对数组进行分区。然后,在分区下方和上方对基数进行排序。

这是实践中的算法。我从Kevin Wayne和Bob Sedgewick的MSD基数排序中得出了这个:http://algs4.cs.princeton.edu/51radix/MSD.java.html

private static final int CUTOFF = 15;
private static final int BITS_PER_INT = 32;
private static final int BITS_PER_BYTE = 8;
private static final int R = 256;

public void sort(int[] a){
    int firstPositiveIndex = partition(0, a, 0, a.length-1);
    int[] aux =new int[a.length];
    if(firstPositiveIndex>0){
        recSort(a, firstPositiveIndex, a.length-1, 0,aux);
        recSort(a, 0, firstPositiveIndex-1, 0,aux);
    }else{//all positive
        recSort(a, 0, a.length-1, 0, aux);
    }
}

private void recSort(int[] a, int lo, int hi, int d, int[] aux){
    if(d>4)return;
    if(hi-lo<CUTOFF){
        insertionSort(a,lo, hi);
        return;
    }

    int[] count = new int[R+1];

    //compute counts
    int bitsToShift = BITS_PER_INT-BITS_PER_BYTE*d-BITS_PER_BYTE;
    int mask = 0b1111_1111;
    for(int i = lo; i<=hi; i++){
        int c = (a[i]>>bitsToShift) & mask;
        count[c+1]++;
    }

    //compute indices
    for(int i = 0; i<R; i++){
        count[i+1]=count[i]+count[i+1];
    }

    //distribute
    for(int i = lo; i<=hi; i++){
        int c = (a[i]>>bitsToShift) & mask;
        aux[count[c]+lo] = a[i];
        count[c]++;
    }
    //copy back
    for(int i = lo; i<=hi; i++){
        a[i]=aux[i];
    }

    if(count[0]>0)
        recSort(a, lo, lo+count[0]-1, d+1, aux);
    for(int i = 1; i<R; i++){
        if(count[i]>0)
            recSort(a, lo+count[i-1], lo+count[i]-1, d+1, aux);
    }
}

// insertion sort a[lo..hi], starting at dth character
private void insertionSort(int[] a, int lo, int hi) {
    for (int i = lo; i <= hi; i++)
        for (int j = i; j > lo && a[j] < a[j-1]; j--)
            swap(a, j, j-1);
}


//returns the index of the partition or to the right of where it should be if the pivot is not in the array 
public int partition(int pivot, int[] a, int lo, int hi){
    int curLo = lo;
    int curHi = hi;
    while(curLo<curHi){
        while(a[curLo]<pivot){
            if((curLo+1)>hi)return hi+1;
            curLo++;
        }

        while(a[curHi]>pivot){
            if((curHi-1)<lo)return lo-1;
            curHi--;
        }
        if(curLo<curHi){
            swap(a, curLo, curHi);
            if(a[curLo]!=pivot)curLo++;
            if(a[curHi]!=pivot)curHi--;             
        }
    }
    return curLo;
}


private void swap(int[] a, int i1, int i2){
    int t = a[i1];
    a[i1]=a[i2];
    a[i2]=t;
}

答案 4 :(得分:1)

处理有符号值的最简单方法可能是在最高有效数字上操作时偏移累加的起始位置(即产生位置偏移)。转换输入以使所有数字都可以被视为无符号也是一个选项,但是需要对值数组应用一次操作至少两次(一次准备输入并再次恢复输出)。

这使用第一种技术以及字节大小的数字(字节访问通常更有效):

void lsdradixsort(int* a, size_t n)
{
    // isolate integer byte by index.
    auto bmask = [](int x, size_t i)
    {
        return (static_cast<unsigned int>(x) >> i*8) & 0xFF;
    };

    // allocate temporary buffer.
    auto m = std::make_unique<int[]>(n);
    int* b = m.get();

    // for each byte in integer (assuming 4-byte int).
    for ( size_t i, j = 0; j < 4; j++ ) {
        // initialize counter to zero;
        size_t h[256] = {}, start;

        // histogram.
        // count each occurrence of indexed-byte value.
        for ( i = 0; i < n; i++ )
            h[bmask(a[i], j)]++;

        // accumulate.
        // generate positional offsets. adjust starting point
        // if most significant digit.
        start = (j != 3) ? 0 : 128;
        for ( i = 1+start; i < 256+start; i++ )
            h[i % 256] += h[(i-1) % 256];

        // distribute.
        // stable reordering of elements. backward to avoid shifting
        // the counter array.
        for ( i = n; i > 0; i-- )
            b[--h[bmask(a[i-1], j)]] = a[i-1];
        std::swap(a, b);
    }
}

注意:代码未经测试。对任何错误/错别字道歉。

答案 5 :(得分:1)

如果您不使用“bitshift”和“按位AND”进行基数计算,您的基数排序将不会比着名的比较排序更快。

计算机使用2的补码来表示带符号的数字,这里的符号位位于二进制数字的最左端,在内存表示中

例如
 436163157(作为32位数字)= 0 0011001 11111111 01010010 01010101
-436163157(作为32位数字)= 1 1100110 00000000 10101101 10101011

1(32位数字)= 0 0000000 00000000 00000000 00000001
-1(作为32位数字)= 1 1111111 1111111 1111111 11111111

0表示为= 0 0000000 00000000 00000000 00000000
最高负值= 1 0000000 00000000 00000000 00000000

所以你看,数字变得越负,它会失去那么多1,一个小的负数就有很多1,如果你只将符号位设置为0,它就会变成一个非常大的正数。反之亦然,一个小的正数会变成一个大的负数。

在基数排序中,排序负数的关键是如何处理最后8位,对于负数,至少最后一位必须为1,在32位方案中,它必须来自 1 0000000 00000000 00000000 00000000这是从零到最远的最负值 1 1111111 11111111 11111111 11111111,即-1。如果你看最左边的8位,幅度范围从10000000到11111111,即从128到255。

这些值可以通过此代码获得

V = ( A[i] >> 24 ) & 255

对于负数,V总是从128到255.对于正数,它将从0到127.如前所述,M的值对于-1为255,对于32位的最高负数为128方案。像往常一样构建直方图。然后从索引128到255执行累加和,然后将频率255添加到0,并继续从0到索引127的累积和。按常规执行排序。这种技术在理论上和实践中都是最佳的,快速的,优雅的和整洁的。排序后不需要任何单独的列表也不需要顺序反转,也不需要将所有输入转换为正数,这使得排序变得缓慢而混乱。

有关代码,请参阅Radix Sort Optimization
可以使用相同的概念构建64位版本

进一步阅读:
http://codercorner.com/RadixSortRevisited.htm
http://stereopsis.com/radix.html

答案 6 :(得分:1)

此处提出的所有解决方案均暗含性能损失:

  • 在分组阶段通过(a [i] XOR 0x8000000000000000)翻转最高位;
  • 将符号位处理为基数并使用额外的传递,并按其排序;
  • 从数组中分离负数;
  • 使用特殊的位掩码等

您不需要全部。使用常规的基数排序。在最后一次迭代中,您将数组项分为0..255组。示例项目: 1 50 200 -500 -300 -2 -1

唯一需要调整的是我们如何将这些组复制回原始数组。我们应该开始复制签名的128..255组(实际上是-128 ..- 1),然后再复制0..127。

结果: -500 -300 -2 -1 1 50 200

在PHP 7.4中测试。常规的基数排序实现比QuickSort快2-2.5倍。 添加额外的异或运算会将结果减慢到1.7-1.8倍。使用上述方法完全不会降低性能。

代码:


function sortRadix (array &$arr) {
  static $groups;
  isset($groups) or $groups = [];

  $numRadix = 8;
  $arrSize  = count($arr);
  $shift    = 0;

  for ($i = 0; $i < $numRadix; $i++) {
    // Cleaning groups
    for ($j = 0; $j < 256; $j++) { 
      $groups[$j] = [];
    }

    // Splitting items into radix groups
    for ($j = 0; $j < $arrSize; $j++) {
      $currItem = $arr[$j];
      $groups[(($currItem >> $shift) & 0xFF)][] = $currItem;
    }

    // Copying sorted by radix items back into original array
    $arrPos = 0;

    // Treat the last radix with sign bit specially
    // Output signed groups (128..256 = -128..-1) first
    // Other groups afterwards. No performance penalty, as compared to flipping sign bit
    // via (($currItem ^ 0x8000000000000000) >> $shift) & 0xFF)
    if ($i === 7) {
      for ($j = 128; $j < 256; $j++) { 
        foreach ($groups[$j] as $item) {
          $arr[$arrPos++] = $item;
        }
      }

      for ($j = 0; $j < 128; $j++) { 
        foreach ($groups[$j] as $item) {
          $arr[$arrPos++] = $item;
        }
      }
    } else {
      foreach ($groups as $group) {
        foreach ($group as $item) {
          $arr[$arrPos++] = $item;
        }
      }
    }

    // Change shift value for next iterations
    $shift += 8;
  } // .for
} // .function sortRadix

答案 7 :(得分:0)

这可以在不需要分区或必须实际反转MSB的情况下完成。这是Java中的工作解决方案:

public class RadixSortsInterviewQuestions {
    private static final int MSB = 64;

    static Map.Entry<Integer, Integer> twoSum(long[] a, long sum) {
        int n = a.length - 1;
        sort(a, MSB, 0, n);

        for (int i = 0, j = n; i < j; ) {
            long t = a[i] + a[j];
            if (t == sum) {
                return new SimpleImmutableEntry<>(i, j);
            } else if (t < sum) {
                i++;
            } else {
                j--;
            }
        }
        return null;
    }

    // Binary MSD radix sort: https://en.wikipedia.org/wiki/Radix_sort#In-place_MSD_radix_sort_implementations
    private static void sort(long[] a, int d, int lo, int hi) {
        if (hi < lo || d < 1) return;

        int left = lo - 1;
        int right = hi + 1;

        for (int i = left + 1; i < right; ) {
            if (isBitSet(a[i], d)) {
                swap(a, i, --right);
            } else {
                left++;
                i++;
            }
        }
        sort(a, d - 1, lo, left);
        sort(a, d - 1, right, hi);
    }

    private static boolean isBitSet(long x, int k) {
        boolean set = (x & 1L << (k - 1)) != 0;

        // invert signed bit so that all positive integers come after negative ones
        return (k == MSB) != set;
    }

    private static void swap(long[] a, int i, int j) {
        long tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }
}

答案 8 :(得分:0)

接受的答案需要比通过更多的通行证。

只需翻转符号位。

这基本上是punpcklbw发布的答案,但是有一个小小的警告需要解决。具体来说,这假设您正在使用二进制补码表示形式,这对我们99.999%的用户而言是正确的。例如,Java和Rust都指定有符号整数使用二进制补码。 C和C ++规范不需要任何特定格式,但是MSVC,GCC和LLVM都不支持其他表示形式。在汇编中,几乎您要处理的所有CPU都是2的补码,并且您肯定会知道其他情况。

下表演示了按字典顺序排序时,简单地翻转符号位将导致二进制补码整数正确排序。第一列给出二进制值,第二列给出将这些位解释为4位有符号整数,第三列给出对那些位进行高位翻转的解释。

Binary    | 2s-comp  | Flip sign
----------+----------+----------
0000      | 00       | -8
0001      | +1       | -7
0010      | +2       | -6
0011      | +3       | -5
0100      | +4       | -4
0101      | +5       | -3
0110      | +6       | -2
0111      | +7       | -1
1000      | -8       | 00
1001      | -7       | +1
1010      | -6       | +2
1011      | -5       | +3
1100      | -4       | +4
1101      | -3       | +5
1110      | -2       | +6
1111      | -1       | +7

punpcklbw给出的答案建议仅在查看最高字节时才翻转该位,但是我的直觉告诉我,每次拔出该字节之前先翻转最高位会更快看着。那是因为每次执行一次xor翻转位的速度要比每次执行分支以决定是否翻转位的速度快。

[要提到的一个重要细节,有些教科书未能正确解决,它是真正的实现应按字节而不是十进制数字排序。显然这仍然是正确的,因为您只是按256的基数而不是10进行排序,但是以这种方式考虑将导致更好的实现。]

答案 9 :(得分:0)

对于最高有效字节(包含有符号位),您也可以不同地解释直方图(count [])。这是C语言的解决方案:

#include <stdint.h>
#include <stdlib.h>
#include <string.h>

static void sortbno(const int32_t* tab, // table of entries
                    int tabsz,    // #entries in tab
                    int bno,      // byte number in T
                    int* inidx,   // current sorted index before this byte
                    int* outidx)  // indices after sorting this byte
{
    int count[256];
    memset(count, 0, sizeof(count));

    // count occurences of each byte value
    for (int i = 0; i < tabsz; i++) {
        int32_t x = tab[i];
        int v = (x >> (8 * bno)) & 0xff;
        count[v]++;
    }

    // change count[i] so it now reflects the actual
    // position of this byte value in outidx
    if (bno == sizeof(tab[0]) - 1) {
        /* account for signed bit for most-significant-byte */
        for (int i = 129; i < 256; i++) {
            count[i] += count[i - 1];
        }
        count[0] += count[255];
        for (int i = 1; i < 128; i++) {
            count[i] += count[i - 1];
        }
    } else {
        for (int i = 1; i < 256; i++) {
            count[i] += count[i - 1];
        }
    }

    // fill outidx[]
    for (int i = tabsz - 1; i >= 0; i--) {
        int in = inidx[i];
        int32_t x = tab[in];
        int v = (x >> (8 * bno)) & 0xff;
        outidx[--count[v]] = in;
    }
}

/**
 *  Sort tab[].
 *  Return the indices into tab[] in asc order.
 */
int* rsort(const int32_t* tab, int tabsz)
{
    int* r[2];

    r[0] = malloc(tabsz * sizeof(*r[0]));
    r[1] = malloc(tabsz * sizeof(*r[1]));
    if (! (r[0] && r[1]))
        goto bail;

    // Artificially assign indices to items
    for (int i = 0; i < tabsz; i++) {
        r[0][i] = i;
    }

    // Sort byte by byte. byte #0 is x & 0xff.
    int bin = 0;
    for (int i = 0; i < (int)sizeof(tab[0]); i++) {
        sortbno(tab, tabsz, i, r[bin], r[1-bin]);
        bin = !bin;
    }

    free(r[1-bin]);
    return r[bin];

    bail:
    if (r[0]) free(r[0]);
    if (r[1]) free(r[1]);
    return 0;
}