2013-02-26 13 views
20

diciamo che ho una collezione di qualche tipo, ad es.Estrai i k elementi massimi di una lista

IEnumerable<double> values; 

Ora ho bisogno di estrarre i k valori più alti da quella collezione, per un certo parametro k. Questo è un modo molto semplice per farlo:

values.OrderByDescending(x => x).Take(k) 

Tuttavia, questo (se ho capito bene) ordina prima l'intero elenco, quindi raccoglie i primi k elementi. Ma se la lista è molto grande, e k è relativamente piccolo (più piccolo di log n), questo non è molto efficiente - la lista è ordinata in O (n * log n), ma immagino di selezionare i k valori più alti da una lista dovrebbe essere più simile a O (n * k).

Quindi, qualcuno ha qualche suggerimento per un modo migliore, più efficiente per farlo?

+6

Questo è noto come algoritmo di selezione. Vedi http://en.wikipedia.org/wiki/Selection_algorithm (si dice "K il più piccolo" ma puoi trovare il "K più grande" invertendo il confronto degli ordini, ovviamente). "Ordinamento parziale" è un caso speciale, che è più quello che vuoi: http: //en.wikipedia.org/wiki/Partial_sorting –

+1

Correlati: [Algoritmo veloce per calcolare percentili per rimuovere i valori anomali] (http://stackoverflow.com/questions/3779763/fast-algorithm-for-computing-percentiles-to-remove-outliers) – sloth

+0

Immagino un'altra soluzione sarebbe quella di ordinarlo ** quando gli articoli vengono aggiunti ** (invece di quando si accede). In questo modo, eviti di doverlo ordinare. – Default

risposta

6

Questo dà un po 'di un aumento delle prestazioni. Si noti che è ascendente, piuttosto che scendere, ma si dovrebbe essere in grado di riutilizzare (vedi commenti):

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(); 
} 
+0

Potrebbe anche essere un metodo di estensione per "lavorare con LINQ", per così dire. – Default

+0

E poi non è 'O (n * k)' è 'O (n * k * k * logk)' qualcosa –

+0

@Default Whoops sì, non mi preoccupo mai di battere queste cose insieme e ho dimenticato di metterlo in :) – Rawling

0

Un altro modo di fare questo (non sono stati in giro C# per anni, in modo pseudo-codice che è, mi spiace) sarebbe:

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] 
    } 
1

Si consideri il seguente metodo:

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); 
} 

E il programma di test:

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); 
} 

Sui miei risultati macchina sono e .

+0

Questo non funzionerà con i numeri negativi. – sloth

+0

@DominicKexel: Sì, ma i numeri naturali non sono mai negativi. –

+0

@DominicKexel: ho usato numeri naturali per non oscurare l'algoritmo. –

0

Non vorrei dire nulla sulle prestazioni senza la creazione di profili. In questa risposta cercherò di implementare l'approccio O(n*k) take-one-enumeration-for-one-max-value. Personalmente ritengo che l'approccio all'ordine sia superiore. Comunque:

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; 
     } 
    } 

utilizzati:

var biggestElements = values.GetMaxElements().Take(3); 

Inconvenienti:

  1. metodo presuppone che fonte IEnumerable ha un ordine
  2. metodo utilizza memoria aggiuntiva/operazioni per salvare indici utilizzati.

Vantaggio:

  • Si può essere certi che ci vuole un'enumerazione per ottenere successivo valore max.

See it running

Problemi correlati