Извлечь k максимальных элементов списка
скажем, у меня есть коллекция какого-то типа, например
IEnumerable<double> values;
Теперь мне нужно извлечь k наивысших значений из этой коллекции, для некоторого параметра k. Это очень простой способ сделать это:
values.OrderByDescending(x => x).Take(k)
Однако это (если я это правильно понимаю) сначала сортирует весь список, а затем выбирает первые k элементов. Но если список очень большой, а k сравнительно небольшой (меньше, чем log n), это не очень эффективно - список сортируется в O (n * log n), но я считаю, что выбираю k самых высоких значений из списка должно быть больше похоже на O (n * k).
Итак, есть ли у кого-нибудь предложения по улучшению и эффективному способу сделать это?
Ответы
Ответ 1
Это немного увеличивает производительность. Обратите внимание, что он возрастает, а не убывает, но вы должны иметь возможность перепрофилировать его (см. Комментарии):
static IEnumerable<double> TopNSorted(this IEnumerable<double> source, int n)
{
List<double> top = new List<double>(n + 1);
using (var e = source.GetEnumerator())
{
for (int i = 0; i < n; i++)
{
if (e.MoveNext())
top.Add(e.Current);
else
throw new InvalidOperationException("Not enough elements");
}
top.Sort();
while (e.MoveNext())
{
double c = e.Current;
int index = top.BinarySearch(c);
if (index < 0) index = ~index;
if (index < n) // if (index != 0)
{
top.Insert(index, c);
top.RemoveAt(n); // top.RemoveAt(0)
}
}
}
return top; // return ((IEnumerable<double>)top).Reverse();
}
Ответ 2
Рассмотрим приведенный ниже метод:
static IEnumerable<double> GetTopValues(this IEnumerable<double> values, int count)
{
var maxSet = new List<double>(Enumerable.Repeat(double.MinValue, count));
var currentMin = double.MinValue;
foreach (var t in values)
{
if (t <= currentMin) continue;
maxSet.Remove(currentMin);
maxSet.Add(t);
currentMin = maxSet.Min();
}
return maxSet.OrderByDescending(i => i);
}
И тестовая программа:
static void Main()
{
const int SIZE = 1000000;
const int K = 10;
var random = new Random();
var values = new double[SIZE];
for (var i = 0; i < SIZE; i++)
values[i] = random.NextDouble();
// Test values
values[SIZE/2] = 2.0;
values[SIZE/4] = 3.0;
values[SIZE/8] = 4.0;
IEnumerable<double> result;
var stopwatch = new Stopwatch();
stopwatch.Start();
result = values.OrderByDescending(x => x).Take(K).ToArray();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch.Restart();
result = values.GetTopValues(K).ToArray();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
На моих машинных результатах 1002 и 14.
Ответ 3
Другой способ сделать это (не было вокруг С# в течение многих лет, поэтому псевдокод, извините):
highestList = []
lowestValueOfHigh = 0
for every item in the list
if(lowestValueOfHigh > item) {
delete highestList[highestList.length - 1] from list
do insert into list with binarysearch
if(highestList[highestList.length - 1] > lowestValueOfHigh)
lowestValueOfHigh = highestList[highestList.length - 1]
}
Ответ 4
Я бы ничего не говорил о производительности без профилирования. В этом ответе я просто попытаюсь реализовать подход O(n*k)
take-one-enumeration-for-one-max-value. Лично я считаю, что порядок заказов выше. В любом случае:
public static IEnumerable<double> GetMaxElements(this IEnumerable<double> source)
{
var usedIndices = new HashSet<int>();
while (true)
{
var enumerator = source.GetEnumerator();
int index = 0;
int maxIndex = 0;
double? maxValue = null;
while(enumerator.MoveNext())
{
if((!maxValue.HasValue||enumerator.Current>maxValue)&&!usedIndices.Contains(index))
{
maxValue = enumerator.Current;
maxIndex = index;
}
index++;
}
usedIndices.Add(maxIndex);
if (!maxValue.HasValue) break;
yield return maxValue.Value;
}
}
Использование:
var biggestElements = values.GetMaxElements().Take(3);
Недостатки:
- Метод предполагает, что источник IEnumerable имеет порядок
- Метод использует дополнительную память/операции для сохранения используемых индексов.
Преимущество:
- Вы можете быть уверены, что для получения следующего максимального значения требуется одно перечисление.
Посмотрите, как работает