2014-09-03 6 views
5
IEnumerable<int> list = new[] { 1, 2, 7, 3, 11, 5 }; 
int item = (from x in list where (x == list.Max()) select x).First(); 
IEnumerable<int> above = from x in list where list.ToList().IndexOf(item) > list.ToList().IndexOf(x) select x; 
IEnumerable<int> below = from x in list where list.ToList().IndexOf(item) < list.ToList().IndexOf(x) select x; 

vorrei trovare un elemento in un IEnumerable e dividere il IEnumerable in IEnumerable s che non contiene l'elemento che ho trovato . Il codice sopra illustra il risultato che voglio raggiungere, tuttavia ho dovuto convertire l'oggetto IEnumerable in un elenco.Split IEnumerable in tre parti: "al di sopra", "voce", "sotto" con efficienza

Mi sembra che ci debba essere un modo per farlo usando solo LinQ e IEnumerable. Come si può fare?

+2

Prima di tutto si potrebbe semplicemente usare 'int item = list.Max()'. – Vache

+3

In secondo luogo, sembra che si desideri utilizzare ['Except'] (http://msdn.microsoft.com/en-us/library/vstudio/bb300779%28v=vs.100%29.aspx)? – Default

+0

@Default ho letto su Tranne - sembra che rimuova un elemento da un elenco - non riesco a vedere come dividerei il risultato in due elementi IEnumerable nello splitpoint. – Johannes

risposta

14

Quindi abbiamo diversi sotto-problemi qui. Il primo problema è restituire l'elemento in una raccolta con il valore più alto di una proiezione di quell'articolo. Max confronta solo l'elemento stesso o, se fornita una proiezione, restituisce il risultato di tale proiezione.

public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source 
    , Func<TSource, TKey> selector 
    , IComparer<TKey> comparer = null) 
{ 
    if (comparer == null) 
    { 
     comparer = Comparer<TKey>.Default; 
    } 
    using (IEnumerator<TSource> iterator = source.GetEnumerator()) 
    { 
     if (!iterator.MoveNext()) 
     { 
      throw new ArgumentException("Source was empty"); 
     } 

     TSource maxItem = iterator.Current; 
     TKey maxValue = selector(maxItem); 

     while (iterator.MoveNext()) 
     { 
      TKey nextValue = selector(iterator.Current); 
      if (comparer.Compare(nextValue, maxValue) > 0) 
      { 
       maxValue = nextValue; 
       maxItem = iterator.Current; 
      } 
     } 
     return maxItem; 
    } 
} 

Questo ci permette di molto più efficiente ottenere l'indice dell'elemento con il valore più grande:

var splitPoint = list.Select((index, number) => new { index, number }) 
    .MaxBy(pair => pair.number) 
    .index; 

Accanto a dividere la collezione si può semplicemente utilizzare Skip/prendere:

var firstHalf = list.Take(index); 
var secondHalf = list.Skip(index + 1); 

Ci sono una serie di diversi problemi con il codice che hai risolto qui.

di calcolare il valore Max per ogni singolo articolo nella query per ottenere item, invece di calcolare una volta e utilizzando quel valore calcolato.

Quindi, per ogni articolo nell'elenco, e copiare tutti gli elementi in un nuovo elenco, due volte, cercare in quell'elenco per cercare di trovare la posizione dell'elemento massimo, quindi provare a trovare la posizione dell'elemento corrente. Quindi fai tutto questo due volte. Ciò significa che copi l'intero array in un elenco quattro volte per ogni elemento, cerca la posizione dell'elemento massimo quattro volte per elemento nell'insieme e cerca linearmente l'elenco per trovare l'indice dell'elemento corrente (qualcosa puoi calcolare in un tempo non breve contando semplicemente) due volte per ogni oggetto. Questo si ridurrà ... male, man mano che aumenta il numero di elementi.

Il codice qui trova l'indice della voce massima in un singolo passaggio della raccolta e quindi crea sequenze che rappresentano ciascuna metà che non hanno praticamente nessun sovraccarico oltre al semplice iterare attraverso tali elementi.

+2

+1 per 'Take()' e 'Skip()'. Sebbene la creazione di un'estensione possa sembrare eccessiva. Vorrei invece usare la risposta di Filipe e usare 'Take()' e 'Skip()'. – rcdmk

+0

@rcdmk Questo aggiunge complessità in cambio sia di un utile metodo generico con ampie applicazioni, ma rimuove anche la necessità di materializzare l'intera raccolta in un elenco e quindi di iterare quell'elenco solo per trovare l'elemento che hai già trovato in precedenza, ancora . Se la raccolta sembra essere abbastanza piccola da non costituire un problema, allora va bene, ma il punto centrale di questa domanda è ottimizzare la query per evitare operazioni non necessarie come quella. – Servy

+0

@Servy Buon punto. L'hai vinto. Pensando a un set di dati di grandi dimensioni e modo ottimizzato. – rcdmk

4

Provare a utilizzare i metodi di estensione in cui è possibile utilizzare l'indice. Guarda il campione qui sotto con i commenti.

// define the list 
IEnumerable<int> list = new[] { 1, 2, 7, 3, 11, 5 }; 

// define some value (max in your sample) 
int value = list.Max(); 

// get the index of the value you want 
int indexValue = list.ToList().IndexOf(value); 

// find collections 
IEnumerable<int> above = list.Where((value, index) => index < indexValue); 
IEnumerable<int> below = list.Where((value, index) => index > indexValue); 
+0

Non è questo ciò che l'OP sta già facendo? – Mrchief

+0

@Mrchief: No. L'OP cercava l'indice per * ogni singolo elemento *. Questo fornisce l'indice come argomento ed è più efficiente. –

+0

Questo verrà comunque delineato dal compilatore in modo da non dover sostenere il costo di ricerca ogni volta. – Mrchief

3

In primo luogo è necessario trovare il (primo) indice dell'elemento massimo. Come variazione della risposta di Servy, questo può essere fatto usando Select e Aggregate. Poi enumerare sugli elementi prima e dopo che l'indice:

 var indexOfMax = list 
      .Select((value, index) => new KeyValuePair<int, int>(index, value)) 
      .Aggregate(new KeyValuePair<int, int>(-1, -1), (min, cur) => 
       { 
        if (min.Key == -1 || cur.Value > min.Value) 
         return cur; 
        return min; 
       }).Key; 

     var beginning = list.Take(indexOfMax); 
     var end = list.Skip(indexOfMax + 1); 
+2

Invece di usare 'int' e usare' -1' come valore pseudo-null, puoi semplicemente usare 'int?' E in realtà usare 'null' per rappresentare non avere un valore per qualche intero. 'KeyValuePair' è anche specifico per oggetti che rappresentano una mappatura da un valore all'altro. Per una coppia in generale che non ha la relazione chiave/valore, in genere si dovrebbe usare 'Tupla'. – Servy

+1

Si noti inoltre che sebbene questo usi meno codice rispetto alla creazione di un metodo 'MaxBy' generico, come nella mia risposta, trovo che il metodo sia abbastanza utile e le sue applicazioni siano abbastanza comuni che vale la pena di rifare la logica in il proprio metodo, piuttosto che ripetere l'implementazione della logica ogni volta che si presenta il problema. La soluzione consente di ottenere meno codice in una risposta, ma può facilmente generare più codice e meno codice gestibile, nel corso di una base di codice più ampia. – Servy

+0

@Servy - Ho usato KeyValuePair per due motivi. In primo luogo, per Tuples estremamente piccoli preferisco usare le struct alle classi per motivi di prestazioni. In secondo luogo, in questo caso KeyValuePair rappresenta davvero una mappatura, poiché la chiave è l'indice della lista. -1 come significante per un indice non valido o non inizializzato è piuttosto convenzionale. Per quanto riguarda la creazione del proprio metodo 'MaxBy()', fallo sicuramente se riutilizzerai il codice; il riutilizzo del codice è sempre buono. – dbc

0

In primo luogo, è necessario garantire che il vostro oggetto IEnumerable<T> contiene articoli in un ordine specifico. Questa operazione può essere eseguita con IOrderedEnumerable<T> anziché con il numero non ordinato IEnumerable<T> o con IList<T> che può fare riferimento agli elementi per indice.

Quando hai capito, la suddivisione diventa piuttosto banale: scorrere gli elementi per indice o per ordine e aggiungerli a IList<T> above finché non trovi il tuo elemento. Salta quell'elemento e poi continua a scorrere tra gli elementi aggiungendoli a IList<T> below.

Problemi correlati