Есть ли хорошая реализация radixsort для поплавков в С#

У меня есть структура данных с полем типа float. Набор этих структур должен быть отсортирован по значению с плавающей запятой. Есть ли для этого реализация сортировки по основанию.

Если нет, есть ли быстрый способ получить доступ к экспоненте, знаку и мантиссе. Потому что, если вы сначала отсортируете поплавки по мантиссе, экспоненте и экспоненте в последний раз. Вы сортируете поплавки в O (n).


person Willem Van Onsem    schedule 21.04.2010    source источник
comment
Разве radixsort концептуально не предназначен для целых чисел или, по крайней мере, любых чисел в десятичной системе? помните: поплавки хранятся внутри двойной системы.   -  person Philip Daubmeier    schedule 21.04.2010
comment
действительно, но, как я описываю, вы можете это сделать. Сначала вы сортируете его по мантиссе (видя мантисса как целое число без использования знака). После этого вы сортируете их по показателю степени (также целое число со знаком). Вы заключаете, сортируя их по знаку (логическому). Запустив три раза алгоритм сортировки по основанию, вы можете сортировать поплавки.   -  person Willem Van Onsem    schedule 21.04.2010
comment
Я вижу вашу точку зрения. Однако алгоритм сортировки O(n) может быть намного медленнее стандартной сортировки O(nlogn) в большинстве случаев, если n никогда не превышает точку безубыточности.   -  person Philip Daubmeier    schedule 21.04.2010
comment
Имейте в виду также накладные расходы памяти для сортировки по основанию в этом большом домене. Или уменьшение объема памяти также увеличивает время сортировки. Прямо сейчас у вас есть сортировка O (kn), где k уже равно как минимум 3. В зависимости от того, как вы настроите систему счисления, она может быть двузначной. Добавьте любой код преобразования float/double в int, и n должно быть довольно большим, чтобы превзойти стандартную сортировку nlogn.   -  person Michael Dorgan    schedule 21.04.2010
comment
Я должен сказать, после всей работы, это действительно стоило попробовать. Спасибо, что задали этот вопрос, без него я бы никогда не попробовал это :)   -  person Philip Daubmeier    schedule 22.04.2010


Ответы (5)


Обновление:

Меня очень заинтересовала эта тема, поэтому я сел и реализовал ее (используя это очень быстро и консервативная реализация памяти). Я также прочитал это (спасибо celion) и обнаружил, что вам даже не нужно разбивать поплавки на мантиссу и экспоненту, чтобы отсортировать их. Вам просто нужно взять биты один к одному и выполнить сортировку int. Вам просто нужно позаботиться об отрицательных значениях, которые нужно поставить обратно перед положительными в конце алгоритма (я сделал это за один шаг с последней итерацией алгоритма, чтобы сэкономить время процессора).

Итак, вот моя поразрядная сортировка с плавающей запятой:

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, из-за копирования массива в начале и в конце функции, где числа с плавающей запятой побитно копируются в целые числа и обратно. Тем не менее, вся функция снова равна 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

Поэтому я провел тест с огромным массивом из 10 миллионов чисел:

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. Это неплохо. Но все же еще не так быстро, как 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 итерации), у вас огромные накладные расходы памяти при сортировке небольших массивов, но вы можете превзойти Array.Sort, если речь идет о массивах размером более 100 тыс. элементов, пусть и не очень много. Обе оси диаграммы логарифмированы:

сравнительная таблица
(источник: daubmeier.de )

person Philip Daubmeier    schedule 21.04.2010
comment
Кстати, этот алгоритм также можно использовать для массивов double, просто замените float на double, int на long, ToInt32 на ToInt64, .ToSingle на .ToDouble и измените int bitLength = 32; на 64. - person Philip Daubmeier; 22.04.2010
comment
Отличная работа! Я не ожидал, что кто-то решит эту проблему. Очень хороший код и анализ. :D - person Willem Van Onsem; 22.04.2010
comment
@Philip Daubmeier Можете ли вы проверить мою версию с улучшенной производительностью? - person IvoTops; 22.02.2019

Здесь есть хорошее объяснение того, как выполнить сортировку с плавающей запятой: http://www.codercorner.com/RadixSortRevisited.htm

Если все ваши значения положительны, вы можете обойтись без двоичного представления; ссылка объясняет, как обрабатывать отрицательные значения.

person celion    schedule 21.04.2010

Благодаря причудливому преобразованию и замене массивов вместо копирования эта версия становится в 2 раза быстрее для 10 млн чисел по сравнению с оригиналом Филиппа Добмайера с параметром grouplength, равным 8. Это в 3 раза быстрее, чем Array.Sort для такого размера массива.

 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;
                }
            }
        }
person IvoTops    schedule 22.02.2019

Вы можете использовать блок unsafe для memcpy или псевдоним float * для uint * для извлечения битов.

person MSN    schedule 21.04.2010

Я думаю, что ваш лучший выбор, если значения не слишком близки и есть разумные требования к точности, вы можете просто использовать фактические цифры с плавающей запятой до и после десятичной точки для сортировки.

Например, вы можете просто использовать первые 4 десятичных знака (будь то 0 или нет) для сортировки.

person Blindy    schedule 21.04.2010