【问题标题】:LSD radix sort for negative integers without queue没有队列的负整数的 LSD 基数排序
【发布时间】:2018-08-07 08:09:35
【问题描述】:

首先,我知道这里有一个类似的问题: Radix Sort for Negative Integers

但它与这个不重复。

我正在研究基数排序,并且有一个关于 Sedgewick 教授和韦恩教授的 LSD 基数排序实现的问题。

public static void sort(int[] a) {
    final int BITS = 32;                 // each int is 32 bits 
    final int R = 1 << BITS_PER_BYTE;    // each bytes is between 0 and 255
    final int MASK = R - 1;              // 0xFF
    final int w = BITS / BITS_PER_BYTE;  // each int is 4 bytes

    int n = a.length;
    int[] aux = new int[n];

    for (int d = 0; d < w; d++) {         

        // compute frequency counts
        int[] count = new int[R+1];
        for (int i = 0; i < n; i++) {           
            int c = (a[i] >> BITS_PER_BYTE*d) & MASK;
            count[c + 1]++;
        }

        // compute cumulates
        for (int r = 0; r < R; r++)
            count[r+1] += count[r];

        // for most significant byte, 0x80-0xFF comes before 0x00-0x7F
        if (d == w-1) {
            int shift1 = count[R] - count[R/2];
            int shift2 = count[R/2];
            for (int r = 0; r < R/2; r++)
                count[r] += shift1;
            for (int r = R/2; r < R; r++)
                count[r] -= shift2;
        }

        // move data
        for (int i = 0; i < n; i++) {
            int c = (a[i] >> BITS_PER_BYTE*d) & MASK;
            aux[count[c]++] = a[i];
        }

        // copy back
        for (int i = 0; i < n; i++)
            a[i] = aux[i];
}

最高有效字节发生了什么?它比我想出的任何东西都要优雅。

我对自己解释该代码块的能力没有信心,很明显它处理的是负数,但我不确定如何。

谁能更详细地解释这段代码?

更新

我想我对变量 shift1shift2 的命名也感到困惑。如果我们稍微重命名一下,并添加一两条评论:

 if (d == w-1) {
            int totalNegatives= count[R] - count[R/2];
            int totalPositives= count[R/2];
            for (int r = 0; r < R/2; r++)
                // all positive number must come after any negative number
                count[r] += totalNegatives;
            for (int r = R/2; r < R; r++)
                // all negative numbers must come before any positive number
                count[r] -= totalPositives;
        }

这变得更容易理解。

这个想法是第一个正数只能在最后一个负数之后的位置,并且所有正数必须按排序顺序在负数之后。因此,我们只需将总负数的计数添加到所有正数中,以确保正数确实会出现在负数之后。 负数的类比相同。

【问题讨论】:

  • 评论 - 建议改进,如果 count 是一个矩阵:count[w][R+1],那么只需要一次读取即可生成所有计数,然后生成索引(累积总和)。 a[] 和 aux[] 也是引用,因此 a[] 和 aux[] 可以交换(使用类似 int[] tmp 的东西)而不是复制回来。只要基数排序通过的次数是偶数,那么排序的结果将最终回到 a[] 中。这些建议也适用于 C/C++(假设指针用于引用数组)。
  • 如果 java 不能很好地优化矩阵的使用,可以使用 w 计数实例为 w 的特定实例硬编码排序,对于 w == 4,然后: count0[], count1[], count2[], count3[].
  • @rcgldr 很棒的建议!

标签: java algorithm sorting


【解决方案1】:

基本算法

让我们从忽略最高位的块开始,并尝试理解其余代码。

算法逐字节处理整数。每个字节可以有 256 个不同的值,分别计算。这就是在第一个区块中发生的事情。之后

int[] count = new int[R+1];
for (int i = 0; i < n; i++) {           
    int c = (a[i] >> BITS_PER_BYTE*d) & MASK;
    count[c + 1]++;
}

每个count[i]a 中在dth 字节中具有i-1 值的元素的数量(注意它们使用count[c + 1]++,所以count[0] == 0

然后算法继续计算累积计数

for (int r = 0; r < R; r++)
    count[r+1] += count[r];

在此之后,每个count[i] 都是该存储桶的第一个元素应该在(中间)输出中结束的索引。 (注意count的长度为257(R + 1),所以可以忽略累积数组的最后一个元素。我会在下面的例子中把它放在括号中。)让我们看一个有4个值的例子(而不是256,保持简洁):

考虑一个具有字节值[0, 3, 3, 2, 1, 2] 的数组。这给出了计数[0, 1, 1, 2, 2] 和累积计数[0, 1, 2, 4, (6)]。这些正是排序数组中第一个0123 的索引(即[0, 1, 2, 2, 3, 3])。

现在算法使用这些累积计数作为(中间)输出中的索引。每当它从桶中复制一个元素时,它都会增加桶的索引,因此来自同一桶的元素会被复制到连续的点。

for (int i = 0; i < n; i++) {
    int c = (a[i] >> BITS_PER_BYTE*d) & MASK;
    aux[count[c]++] = a[i];
}

for (int i = 0; i < n; i++)
    a[i] = aux[i];

处理符号位

最高位有点特殊,因为在two's complement 中它是符号,1 表示负数,0 表示正数。所以最后一步的累积数组count 是不正确的。最高有效位为 0(正数)的值的计数位于数组的前半部分,最高有效位为 1(负数)的值的计数位于数组的后半部分。因此,数组的前半部分和后半部分必须“翻转”。

这是通过将 counts 数组后半部分中的元素总数添加到 counts 数组前半部分中的每个元素来实现的。并通过从 counts 数组后半部分中的每个元素中减去 counts 数组前半部分中的元素总数。如前所述,counts 数组的长度为 257,因此前 128 个元素 (257 / 2) 是前半部分,其余 129 个元素是后半部分。

让我们看一个新示例,现在使用两位有符号值,即-2-101。这些的二进制表示是10110001,因此分别映射到无符号数2301

考虑将a 排列为[0, -1, -1, -2, 1, -2]。转换为无符号:[0, 3, 3, 2, 1, 2]。应用算法来获取累积计数:[0, 1, 2, 4, (6)]。如果我们不进行翻转,我们最终会得到排序后的无符号数组[0, 1, 2, 2, 3, 3],它等价于有符号数组[0, 1, -2, -2, -1, -1]。排序不正确。

现在,让我们对有符号字节应用额外的步骤。我们将累积的counts 数组分成两半:[0, 1][2, 4, (6)]。前半部分有 2 (2 - 0) 个元素,后半部分有 4 (6 - 2) 个元素。因此,我们将前半部分的每个元素加 4:[4, 5],并从后半部分的每个元素中减去 2:[0, 2, (4)]。将两半合并为[4, 5, 0, 2, (4)]

如果我们现在将这些计数用作最终无符号数组中的索引,我们会得到[2, 2, 3, 3, 0, 1](第一个 0 在索引 4 处,第一个 1 在索引 5 处,依此类推)。将其转换回有符号值会得到[-2, -2, -1, -1, 0, 1],这确实是正确的。


可能的混淆:该算法中令人困惑的部分之一是counts 数组用于两个不同的目的。首先它用于计算单独的出现次数,后来它用于计算累积出现次数。单独计数时,不使用数组的第一个元素。累加计数时,不使用数组的最后一个元素。

我认为如果使用两个单独的数组,算法会更简单。

【讨论】:

  • 非常感谢您的详细解答。您能否详细说明一下翻转是如何解决问题的,这背后的原理是什么?
  • @John 我试图让这个例子更明确一点。希望有帮助。如果您还没有,请检查二进制补码,因为这是翻转是必要的原因,也是该算法有效的原因。
  • 非常感谢您。是的,我同意你的困惑部分,也注意到了最后一部分/第一部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-09
  • 1970-01-01
  • 2019-09-22
  • 1970-01-01
  • 1970-01-01
  • 2012-09-26
相关资源
最近更新 更多