Наиболее эффективный способ сортировки массива и сохранения соответствующих исходных индексов.
Я хочу отсортировать массив целых чисел в C#, но также сохранить исходные индексы, соответствующие каждому элементу в массиве.
Моя первая мысль - преобразовать в объект Dictionary с ключом в качестве индекса и значением в качестве значения; а затем сортировать по значению с помощью linq. Я не думаю, что это работает очень хорошо. Какие еще решения возможны? Производительность является ключевым здесь.
Это кажется хорошим и простым решением; но это самый быстрый способ сделать это?
4 ответа
Если вы говорите о производительности во времени, вы можете скопировать массив во второй массив, отсортировать второй массив, а затем использовать два массива для отдельной функциональности. Это даст вам O(1)
доступ к необходимым элементам.
Если вы говорите о производительности с точки зрения пространства, ваш подход с помощью словаря является лучшим, поскольку он будет содержать только 1 копию элементов, что приводит к O(n)
пространство.
Как обычно, не оптимизируйте, пока не столкнетесь с проблемой производительности.
Для этого есть набор специальных встроенных функций .NET. Ищите перегрузки Array.Sort, которые принимают TKey[]
аргумент. Существует несколько перегрузок, которые позволяют указать поддиапазон для сортировки или пользовательский IComparer<TKey>
, Секрет в том, чтобы передать исходный массив как keys
аргумент и массив идентификаторов (0, 1, 2,... n-1
) для items
аргумент. Следующая функция сделает всю работу за вас:
/// sort array 'rg', returning the original index positions
static int[] SortAndIndex<T>(T[] rg)
{
int i, c = rg.Length;
var keys = new int[c];
if (c > 1)
{
for (i = 0; i < c; i++)
keys[i] = i;
System.Array.Sort(rg, keys /*, ... */);
}
return keys;
}
Опять же, с Array.Sort
Обратите внимание, что мы осторожны с возможными путаницами имен параметров. Мы передаем наши элементы как первый параметр (который называется "ключами"), а наш индекс (который больше похож на ключи) передается как второй параметр (называемый "элементы").
Использование довольно очевидно:
var rgs = new[] { "xyz", "a", "", "bb", "pdq" };
int[] idx = SortAndIndex(rgs); // rgs: { "", "a", "bb", "pdz", "xyz" }
// idx: { 2, 1, 3, 4, 0 }
Это касается случая ОП, когда вы действительно хотите, чтобы исходные данные были отсортированы. Если это то, что вам нужно, вы можете перестать читать здесь.
Но связанная с этим проблема заключается в том, что, если вам нужны такие же признаки сортировки, но вы не хотите изменять исходный массив? Как мы можем получить индексы сортировки без изменения порядка оригинальных предметов?
Лучший способ, который я нашел, - это использовать описанную выше процедуру для сортировки данных и получения индекса, но затем использовать этот индекс сортировки, чтобы восстановить отсортированные элементы в их первоначальном порядке.
Вероятно, есть несколько способов сделать это, но поскольку в этом вопросе упоминается эффективность, я могу показать некоторый код, который гарантированно выполнит минимальное количество обменов оригинальных элементов, используя только один T
элемент хранения, для того чтобы восстановить элементы в исходном, несортированном порядке:
static unsafe void RevertSortIndex<T>(T[] rg, int[] keys)
{
int i, k, c;
int* rev = stackalloc int[c = rg.Length];
for (i = 0; i < c; i++)
rev[k = keys[i]] = k != i ? i : -1;
do
if ((i = rev[--c]) != c && i >= 0)
{
T t = rg[k = c];
do
{
rg[k] = rg[i];
rev[k] = -1;
}
while ((i = rev[k = i]) != c);
rg[k] = t;
rev[k] = -1;
}
while (c > 0);
}
Для того, чтобы просто использовать один T
элемент для обмена, а также переместить каждый элемент только один раз в его окончательное положение, вы должны сделать обмен в очень конкретном порядке, определяемом данными. Выяснение этого упрощается с помощью временного обратного индекса (rev
), который легко создать из keys
, Здесь он отображается как stackalloc, но если вы не хотите идти по этому пути, вы можете легко заменить его на управляемый int[]
распределение.
Не вдаваясь в подробности, любой индекс сортировки содержит одну или несколько "цепочек" элементов, которые связаны между собой, и следование каждой цепочке дает вам оптимальный порядок, в котором вы можете восстановить эти элементы в исходное положение, сохраняя при этом только один временный T
, Это то, что внутреннее do...while
петля делает.
Внешний while...
Цикл необходим для сканирования дополнительных цепочек, потому что индекс сортировки в целом может иметь несколько независимых цепочек, и все они должны соблюдаться. Важно отметить, что для получения правильных результатов каждая цепочка должна обрабатываться ровно один раз и не более. Таким образом, чтобы выяснить, был ли обработан какой-либо данный своп, его запись в rev
временный обратный индекс установлен в -1
, Это указывает на то, что соответствующий T
элемент в rg
уже был перемещен (как часть предыдущей цепочки).
Вот полный пример использования:
var rgs = new[] { "xyz", "a", "", "bb", "pdq" };
int[] idx = SortAndIndex(rgs);
// rgs: { "", "a", "bb", "pdz", "xyz" }
// idx: { 2, 1, 3, 4, 0 }
RevertSortIndex(rgs, idx);
// rgs: { "xyz", "a", "", "bb", "pdq" }
// idx: { 2, 1, 3, 4, 0 } (unchanged)
Последнее замечание будет заключаться в том, что сочетание SortAndIndex
с RevertSortIndex
может дать внешний вид, который rgs
не изменяется, но на это нельзя полагаться в целях параллелизма. Если rgs
одновременно виден из другого места, промежуточные состояния будут видны.
В то время как для старой школы и нетипизированного Array.Sort(ключи массива, элементы массива) он лучше, чем LINQ для отслеживания индекса.
Начало реализации Array:
- C# Github исходный код для массива
- CPP Части реализации платформы
- Мэтт Уоррен - если вы действительно хотите понять Array
Array.Sort vs Linq
[GlobalSetup]
public virtual void Setup()
{
data = new T[N];
indexes = new int[N];
for (var cc = 0; cc < N; cc++)
{
data[cc] = GetRandom();
indexes[cc] = cc;
}
}
// Clone is nessesary as Array.Sort is done in place, ie the next call will be incorrectly given a pre-sorted list
private T[] GetTestData() => (T[]) data.Clone();
private int[] GetTestDataIndex() => (int[])indexes.Clone();
[Benchmark]
public virtual void Sort()
{
Array.Sort(GetTestData());
}
[Benchmark]
public virtual void SortMaintainIndex()
{
Array.Sort(GetTestData(), GetTestDataIndex());
}
[Benchmark]
public virtual void SortWithLinq()
{
int cc = 0;
var withIndex = GetTestData()
.Select(x => (cc++, x))
.OrderBy(x => x.x)
.ToArray();
}
С точки зрения скорости нет никакого сравнения: источник здесь https://gist.github.com/guylangston/cd9a0719d467f020eba46c6d0beb0584
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-3930K CPU 3.20GHz (Ivy Bridge), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=2.1.300
[Host] : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
DefaultJob : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
Method | N | Mean | Error | StdDev | Median |
------------------ |------ |------------:|-----------:|-----------:|------------:|
Sort | 1000 | 35.85 us | 0.3234 us | 0.2700 us | 35.76 us |
SortMaintainIndex | 1000 | 60.82 us | 0.2280 us | 0.1780 us | 60.76 us |
SortWithLinq | 1000 | 172.26 us | 3.3984 us | 3.7773 us | 170.75 us |
Sort | 10000 | 611.82 us | 13.8881 us | 18.0584 us | 602.77 us |
SortMaintainIndex | 10000 | 889.25 us | 18.6503 us | 28.4810 us | 874.06 us |
SortWithLinq | 10000 | 2,484.35 us | 57.8378 us | 54.1015 us | 2,476.72 us |
Вы можете создать массив KeyValuePairs, а затем отсортировать по значению:
Array.Sort(array, (left, right) => left.Value.CompareTo(right.Value))
Но Array.Sort(Array, Array) тоже выглядит хорошо.