【问题标题】:Radix Sort for Negative Integers负整数的基数排序
【发布时间】:2013-03-09 03:04:28
【问题描述】:

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

您对此有何看法?有没有更有效的方法来处理负整数?

【问题讨论】:

    标签: sorting language-agnostic radix-sort radix


    【解决方案1】:

    接受的答案需要多过一次。

    只需翻转符号位。

    这假设您正在使用二进制补码表示,这对我们 99% 的人来说都是正确的。

    下表演示了简单地翻转符号位将导致二进制补码整数在按字典顺序排序时正确排序。

    第一列给出了一个 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。这使您可以读取字节而不是十进制数字。]

    【讨论】:

      【解决方案2】:

      另一种解决方案是从数组中分离出负整数,使其为正数,使用基数排序为正值,然后将其反转并附加已排序的非负数组。

      【讨论】:

        【解决方案3】:

        您可以将符号视为一种特殊的数字。您将一堆堆放在单位上,然后是十位,等等,最后在标志上。这确实会为否定产生相反的顺序,然后您只需反转该存储桶的内容即可。这就是老式机械卡片分类器的工作原理。

        【讨论】:

        • 这需要多过一次。排序前只需翻转符号位。
        • 非常聪明,但这不需要两次通过(一次翻转位,然后一次翻转它们之后)?当然,您不需要移动任何数据,这是一个优势。
        【解决方案4】:

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

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

        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】:

        如果您不使用“bitshift”和“bitwise AND”进行基数计算,您的基数排序不会比著名的比较排序快。

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

        例如
        436163157(作为 32 位数)= 00011001 11111111 01010010 01010101
        -436163157(作为 32 位数)= 11100110 00000000 10101101 10101011

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

        0 表示为 = 00000000 00000000 00000000 00000000
        最大负值 = 10000000 00000000 00000000 00000000

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

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

        这些值可以通过这段代码得到

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

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

        代码见Radix Sort Optimization
        可以使用相同的概念构建64位版本

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

        【讨论】:

          【解决方案6】:

          您还可以对最高有效字节(包含有符号位)以不同方式解释直方图 (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 occurrences 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 ascending 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;
          }
          

          【讨论】:

            【解决方案7】:

            这里提出的所有解决方案都意味着性能损失:

            • 在分组阶段通过 (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
            

            【讨论】:

              【解决方案8】:

              这可以在不需要分区或实际上反转 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;
                  }
              }
              

              【讨论】:

                【解决方案9】:

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

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

                【讨论】:

                • 除了反转符号位,您还可以以不同的顺序组合存储桶。如果您使用 16 个存储桶,则从存储桶 8 - 15 开始,然后返回执行 0 - 7,这将产生相同的排序。
                【解决方案10】:

                绝对!当然,您确实必须注意将负面因素与正面因素分开,但幸运的是这很容易。在排序算法开始时,您所要做的就是围绕值 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;
                }
                

                【讨论】:

                  【解决方案11】:

                  您可以在下面看到 JS 中正整数和负整数的基数排序实现。

                  const getDigit = (num, place) => {
                    return  Math.floor(Math.abs(num) / Math.pow(10, place)) % 10;
                  }
                  
                  
                  const maxDigitNumber = arr => {
                  
                      const digitCount = (num) =>  {
                          return Math.abs(num).toString().length;
                      }
                  
                      let maxDigit = digitCount(arr[0]);
                      for(let num of arr) {
                          const digits = digitCount(num);
                          if(maxDigit < digits) maxDigit = digits;
                      }
                      return maxDigit;
                  }
                  
                  const radixSort = arr => {
                      const maxDigit = maxDigitNumber(arr);
                      digitIteration:
                      for(let d = 0; d < maxDigit; d++) {
                          const bucket = {};
                          arrIteration:
                          for(let i = 0; i < arr.length; i++) {
                              const number = arr[i];
                              const digitValue = getDigit(number, d);
                              if(!bucket[digitValue]) bucket[digitValue] = [];
                              if(number > 0) bucket[digitValue].push(number);            
                              else bucket[digitValue].unshift(number);            
                          };
                          const newArr = [];
                          for(let obj in bucket) {
                              bucket[obj].map(el => {
                                  if(el < 0) newArr.unshift(el);
                                  else newArr.push(el);
                              });
                          }
                          arr = newArr;        
                      }
                      return arr;
                  }
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-05-25
                    • 2020-11-12
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多