代码之家  ›  专栏  ›  技术社区  ›  willeM_ Van Onsem

C中是否有一个很好的浮点RadixSort实现#

  •  9
  • willeM_ Van Onsem  · 技术社区  · 16 年前

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

    如果没有,是否有一种快速的方法来访问指数、符号和尾数。 因为如果在尾数、指数和上次指数上先对浮点数排序。你在O(N)中排序浮动。

    5 回复  |  直到 7 年前
        1
  •  18
  •   Community Mohan Dere    9 年前

    更新:

    我对这个主题很感兴趣,所以坐下来实现了它(使用 this very fast and memory conservative implementation )我也读 this one (谢谢) celion )发现你甚至不必把浮点数分成尾数和指数来排序。你只需要把这些位一对一地进行整型排序。你只需要关心负数,在算法结束时,负数必须反放在正数前面(我在算法的最后一次迭代中做了一个步骤,以节省一些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;
    }
    

    它比int基数排序稍微慢一点,因为在函数的开始和结束处都有数组复制,其中浮点按位复制到int并返回。不过,整个函数又是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,min/max value,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));
    

    结果是( 更新:现在使用发布版本运行,而不是调试 ):

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

    大约是Linq的四倍多。这还不错。但还没有那么快 数组排序 ,但也没那么糟。但我真的很惊讶这个:我希望它在非常小的数组上比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个元素,即使不是很多。图表轴均为对数:

    comparison chart http://daubmeier.de/philip/stackoverflow/radixsort_vs_arraysort.png

        2
  •  1
  •   celion    16 年前

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

    如果所有值都是正值,则可以使用二进制表示;该链接说明如何处理负值。

        3
  •  1
  •   MSN    16 年前

    你可以用 unsafe 块到memcpy或别名a float * 到A uint * 去提取碎片。

        4
  •  1
  •   IvoTops    7 年前

    通过做一些有趣的转换和交换数组而不是复制这个版本,对于10万个数字来说,比philip daubmeiers原来的grouplength设置为8快了2倍。它比数组快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;
                    }
                }
            }
    
        5
  •  0
  •   Blindy    16 年前

    我想你最好的办法是如果数值不是太接近,并且有一个合理的精度要求,你可以只使用小数点前后的实际浮点数来进行排序。

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