Обновление:
Меня очень заинтересовала эта тема, поэтому я сел и реализовал ее (используя это очень быстрая и консервативная реализация ). Я также прочитал этот (спасибо 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 radix, из-за копирования массива в начале и конце функции, где значения с плавающей точкой поразрядно копируются в 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
Итак, я выполнил тест с огромным массивом чисел 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));
И вывод был ( update: теперь запускался с выпуском сборки, а не отладкой ):
RadixSort: 00:00:03.9902332
Linq OrderBy: 00:00:17.4983272
Array.Sort: 00:00:03.1536785
примерно в четыре раза быстрее, чем Линк. Это не плохо. Но все еще не так быстро, как 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 тыс. Элементов, даже если не очень много. Оси диаграмм логарифмированы:
сравнительная таблица http://daubmeier.de/philip/stackoverflow/radixsort_vs_arraysort.png