【问题标题】:Is there a good radixsort-implementation for floats in C#C# 中的浮点数是否有良好的基数排序实现
【发布时间】:2011-02-10 17:14:46
【问题描述】:

我有一个带有浮点类型字段的数据结构。这些结构的集合需要按浮点值排序。是否有一个基数排序实现。

如果没有,是否有快速访问指数、符号和尾数的方法。 因为如果您首先根据尾数、指数和指数对浮点数进行排序。您在 O(n) 中对浮点数进行排序。

【问题讨论】:

  • 从概念上讲,radixsort 不是针对整数,或者至少是十进制系统中的任何数字吗?请记住:浮点数在内部存储在双系统中。
  • 确实如此,但正如我所描述的,您可以做到。您首先对尾数进行排序(将尾数视为整数,不使用符号)。之后,您按指数(也是有符号整数)对它们进行排序。最后按符号(布尔值)对它们进行排序。通过运行 3 次基数排序算法,您可以对浮点数进行排序。
  • 我明白你的意思。然而,在大多数情况下,如果 n 永远不会超过某个收支平衡点,则 O(n) 排序算法可能比 O(nlogn) 标准排序慢。
  • 请记住在这么大的域上进行基数排序的内存开销。或者减少内存占用也会增加排序时间。现在,你有一个 O(kn) 排序,其中 k 至少已经是 3。根据您设置基数的方式,它可能会达到两位数。添加任何 float/double 到 int 部分的转换代码,n 必须非常大才能击败标准的 nlogn 排序。
  • 不得不说,经过这么多工作,真的值得一试。感谢您提出这个问题,如果没有它,我永远不会尝试 :)

标签: c# algorithm sorting floating-point radix-sort


【解决方案1】:

更新:

我对这个话题很感兴趣,所以我坐下来实现了它(使用this very fast and memory conservative implementation)。我还阅读了this one(感谢celion),发现您甚至不必将浮点数拆分为尾数和指数来对其进行排序。您只需要一对一地获取位并执行 int 排序。您只需要关心负值,必须在算法结束时将其反向放在正值之前(我在算法的最后一次迭代中一步完成以节省一些 cpu 时间)。

这是我的浮点基数排序:

public static float[] RadixSort(this float[] array)
{
    // temporary array and the array of converted floats to ints
    int[] t = new int[array.Length];
    int[] a = new int[array.Length];
    for (int i = 0; i < array.Length; i++)
        a[i] = BitConverter.ToInt32(BitConverter.GetBytes(array[i]), 0);

    // set the group length to 1, 2, 4, 8 or 16
    // and see which one is quicker
    int groupLength = 4;
    int bitLength = 32;

    // counting and prefix arrays
    // (dimension is 2^r, the number of possible values of a r-bit number) 
    int[] count = new int[1 << groupLength];
    int[] pref = new int[1 << groupLength];
    int groups = bitLength / groupLength;
    int mask = (1 << groupLength) - 1;
    int negatives = 0, positives = 0;

    for (int c = 0, shift = 0; c < groups; c++, shift += groupLength)
    {
        // reset count array 
        for (int j = 0; j < count.Length; j++)
            count[j] = 0;

        // counting elements of the c-th group 
        for (int i = 0; i < a.Length; i++)
        {
            count[(a[i] >> shift) & mask]++;

            // additionally count all negative 
            // values in first round
            if (c == 0 && a[i] < 0)
                negatives++;
        }
        if (c == 0) positives = a.Length - negatives;

        // calculating prefixes
        pref[0] = 0;
        for (int i = 1; i < count.Length; i++)
            pref[i] = pref[i - 1] + count[i - 1];

        // from a[] to t[] elements ordered by c-th group 
        for (int i = 0; i < a.Length; i++){
            // Get the right index to sort the number in
            int index = pref[(a[i] >> shift) & mask]++;

            if (c == groups - 1)
            {
                // We're in the last (most significant) group, if the
                // number is negative, order them inversely in front
                // of the array, pushing positive ones back.
                if (a[i] < 0)
                    index = positives - (index - negatives) - 1;
                else
                    index += negatives;
            }
            t[index] = a[i];
        }

        // a[]=t[] and start again until the last group 
        t.CopyTo(a, 0);
    }

    // Convert back the ints to the float array
    float[] ret = new float[a.Length];
    for (int i = 0; i < a.Length; i++)
        ret[i] = BitConverter.ToSingle(BitConverter.GetBytes(a[i]), 0);

    return ret;
}

它比整数基数排序稍慢,因为在函数的开头和结尾复制数组,其中浮点数按位复制到整数并返回。尽管如此,整个函数还是 O(n)。无论如何,都比您建议的连续排序 3 次要快得多。我认为优化空间不大,但如果有人这样做:请随时告诉我。

要降序排序,请在最后更改此行:

ret[i] = BitConverter.ToSingle(BitConverter.GetBytes(a[i]), 0);

到这里:

ret[a.Length - i - 1] = BitConverter.ToSingle(BitConverter.GetBytes(a[i]), 0);

测量:

我设置了一些简短的测试,包含浮点数的所有特殊情况(NaN、+/-Inf、最小/最大值、0)和随机数。它的排序顺序与 Linq 或 Array.Sort 排序浮点数完全相同:

NaN -> -Inf -> Min -> Negative Nums -> 0 -> Positive Nums -> Max -> +Inf

所以我对包含 1000 万个数字的庞大数组进行了测试:

float[] test = new float[10000000];
Random rnd = new Random();
for (int i = 0; i < test.Length; i++)
{
    byte[] buffer = new byte[4];
    rnd.NextBytes(buffer);
    float rndfloat = BitConverter.ToSingle(buffer, 0);
    switch(i){
        case 0: { test[i] = float.MaxValue; break; }
        case 1: { test[i] = float.MinValue; break; }
        case 2: { test[i] = float.NaN; break; }
        case 3: { test[i] = float.NegativeInfinity; break; }
        case 4: { test[i] = float.PositiveInfinity; break; }
        case 5: { test[i] = 0f; break; }
        default: { test[i] = test[i] = rndfloat; break; }
    }
}

并停止不同排序算法的时间:

Stopwatch sw = new Stopwatch();
sw.Start();

float[] sorted1 = test.RadixSort();

sw.Stop();
Console.WriteLine(string.Format("RadixSort: {0}", sw.Elapsed));
sw.Reset();
sw.Start();

float[] sorted2 = test.OrderBy(x => x).ToArray();

sw.Stop();
Console.WriteLine(string.Format("Linq OrderBy: {0}", sw.Elapsed));
sw.Reset();
sw.Start();

Array.Sort(test);
float[] sorted3 = test;

sw.Stop();
Console.WriteLine(string.Format("Array.Sort: {0}", sw.Elapsed));

输出是(update: now run with release build, not debug):

RadixSort: 00:00:03.9902332
Linq OrderBy: 00:00:17.4983272
Array.Sort: 00:00:03.1536785

大约是 Linq 的四倍多。这还不错。但是仍然没有Array.Sort那么快,但也没有那么糟糕。但我真的对此感到惊讶:我预计它在非常小的阵列上会比 Linq 稍微慢一些。但后来我只用了 20 个元素进行了测试:

RadixSort: 00:00:00.0012944
Linq OrderBy: 00:00:00.0072271
Array.Sort: 00:00:00.0002979

甚至这次我的 Radixsort 比 Linq 快,但 方式 比数组排序慢。 :)

更新 2:

我进行了更多测量并发现了一些有趣的事情:更长的组长度常数意味着更少的迭代和更多的内存使用。如果您使用 16 位的组长度(仅 2 次迭代),则在对小型数组进行排序时会产生巨大的内存开销,但如果涉及大于约 100k 个元素的数组,即使不是很多,您也可以击败 Array.Sort。图表轴都是对数化的:


(来源:daubmeier.de

【讨论】:

  • 顺便说一下,该算法也可用于double 数组,只需将float 替换为doubleint 替换为longToInt32 替换为ToInt64、@ 987654345@ .ToDouble 并将 int bitLength = 32; 更改为 64。
  • 干得好!我没想到有人会解决这个问题。非常好的代码和分析。 :D
  • @Philip Daubmeier 你能验证我的性能改进版本吗?
【解决方案2】:

这里有一个很好的解释如何对浮点数执行基数排序: http://www.codercorner.com/RadixSortRevisited.htm

如果你所有的值都是正数,你可以不使用二进制表示;该链接解释了如何处理负值。

【讨论】:

    【解决方案3】:

    通过执行一些花哨的转换和交换数组而不是复制此版本,对于 10M 数字,此版本的速度是 Philip Daubmeiers 原始的 2 倍,其中 grouplength 设置为 8。对于该数组大小,它比 Array.Sort 快 3 倍。

     static public void RadixSortFloat(this float[] array, int arrayLen = -1)
            {
                // Some use cases have an array that is longer as the filled part which we want to sort
                if (arrayLen < 0) arrayLen = array.Length;
                // Cast our original array as long
                Span<float> asFloat = array;
                Span<int> a = MemoryMarshal.Cast<float, int>(asFloat);
                // Create a temp array
                Span<int> t = new Span<int>(new int[arrayLen]);
    
                // set the group length to 1, 2, 4, 8 or 16 and see which one is quicker
                int groupLength = 8;
                int bitLength = 32;
    
                // counting and prefix arrays
                // (dimension is 2^r, the number of possible values of a r-bit number) 
                var dim = 1 << groupLength;
                int groups = bitLength / groupLength;
                if (groups % 2 != 0) throw new Exception("groups must be even so data is in original array at end");
                var count = new int[dim];
                var pref = new int[dim];
                int mask = (dim) - 1;
                int negatives = 0, positives = 0;
    
                // counting elements of the 1st group incuding negative/positive
                for (int i = 0; i < arrayLen; i++)
                {
                    if (a[i] < 0) negatives++;
                    count[(a[i] >> 0) & mask]++;
                }
                positives = arrayLen - negatives;
    
                int c;
                int shift;
                for (c = 0, shift = 0; c < groups - 1; c++, shift += groupLength)
                {
                    CalcPrefixes();
                    var nextShift = shift + groupLength;
                    //
                    for (var i = 0; i < arrayLen; i++)
                    {
                        var ai = a[i];
                        // Get the right index to sort the number in
                        int index = pref[( ai >> shift) & mask]++;
                        count[( ai>> nextShift) & mask]++;
                        t[index] =  ai;
                    }
    
                    // swap the arrays and start again until the last group 
                    var temp = a;
                    a = t;
                    t = temp;
                }
    
                // Last round
                CalcPrefixes();
                for (var i = 0; i < arrayLen; i++)
                {
                    var ai = a[i];
                    // Get the right index to sort the number in
                    int index = pref[( ai >> shift) & mask]++;
                    // We're in the last (most significant) group, if the
                    // number is negative, order them inversely in front
                    // of the array, pushing positive ones back.
                    if ( ai < 0) index = positives - (index - negatives) - 1; else index += negatives;
                    //
                    t[index] =  ai;
                }
    
                void CalcPrefixes()
                {
                    pref[0] = 0;
                    for (int i = 1; i < dim; i++)
                    {
                        pref[i] = pref[i - 1] + count[i - 1];
                        count[i - 1] = 0;
                    }
                }
            }
    

    【讨论】:

      【解决方案4】:

      您可以使用unsafe 块来memcpy 或将float * 别名为uint * 以提取位。

      【讨论】:

        【解决方案5】:

        如果值不是太接近并且有合理的精度要求,我认为最好的选择是,您可以使用小数点前后的实际浮点数进行排序。

        例如,您可以只使用前 4 位小数(无论是否为 0)进行排序。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-19
          • 2017-03-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多