LINQ in SortedList
-
04-10-2019 - |
Domanda
Sono un novizio completo LINQ, quindi non so se il mio LINQ non è corretta per quello che ho bisogno di fare o se le mie aspettative di prestazioni sono troppo alte.
Ho un SortedList di oggetti, digitato da int; SortedList al contrario di SortedDictionary perché sarò popolano la raccolta con i dati pre-ordinato. Il mio compito è quello di trovare sia la chiave esatta o, se non c'è una chiave precisa, quella con il successivo valore più alto. Se la ricerca è troppo alta per la lista (per esempio tasto più alto è di 100, ma la ricerca di 105), di ritorno nullo.
// The structure of this class is unimportant. Just using
// it as an illustration.
public class CX
{
public int KEY;
public DateTime DT;
}
static CX getItem(int i, SortedList<int, CX> list)
{
var items =
(from kv in list
where kv.Key >= i
select kv.Key);
if (items.Any())
{
return list[items.Min()];
}
return null;
}
Dato un elenco di 50.000 record, chiamando getItem 500 volte richiede circa un secondo e mezzo. Chiamarlo 50.000 volte dura più di 2 minuti. Questa performance sembra molto povera. È il mio LINQ male? Sto aspettavo troppo? Dovrei essere a rotazione la mia funzione di ricerca binaria?
Soluzione
Scrivi ricerca binaria sul proprio può essere difficile.
Fortunatamente, Microsoft ha già scritto una bella robusta uno: Array.BinarySearch<T>
. Questo è, infatti, il metodo che utilizza SortedList<TKey, TValue>.IndexOfKey
internamente . L'unico problema è che ci vuole un argomento T[]
, invece di qualsiasi IList<T>
(come SortedList<TKey, TValue>.Keys
).
Sai una cosa, però? C'è questo grande strumento chiamato Riflettore che ti permette di guardare il codice sorgente di .NET .. .
Check it out:. Un metodo di estensione BinarySearch
generica su IList<T>
, presi direttamente dal codice riflessa di implementazione Array.BinarySearch<T>
di Microsoft
public static int BinarySearch<T>(this IList<T> list, int index, int length, T value, IComparer<T> comparer) {
if (list == null)
throw new ArgumentNullException("list");
else if (index < 0 || length < 0)
throw new ArgumentOutOfRangeException((index < 0) ? "index" : "length");
else if (list.Count - index < length)
throw new ArgumentException();
int lower = index;
int upper = (index + length) - 1;
while (lower <= upper) {
int adjustedIndex = lower + ((upper - lower) >> 1);
int comparison = comparer.Compare(list[adjustedIndex], value);
if (comparison == 0)
return adjustedIndex;
else if (comparison < 0)
lower = adjustedIndex + 1;
else
upper = adjustedIndex - 1;
}
return ~lower;
}
public static int BinarySearch<T>(this IList<T> list, T value, IComparer<T> comparer) {
return list.BinarySearch(0, list.Count, value, comparer);
}
public static int BinarySearch<T>(this IList<T> list, T value) where T : IComparable<T> {
return list.BinarySearch(value, Comparer<T>.Default);
}
Questo vi permetterà di chiamare list.Keys.BinarySearch
e ottenere il complemento bit a bit negativo dell'indice che si desidera nel caso in cui il tasto desiderato non si trova (il sotto è tratto sostanzialmente retta dalla risposta di tzaman):
int index = list.Keys.BinarySearch(i);
if (index < 0)
index = ~index;
var item = index < list.Count ? list[list.Keys[index]] : null;
return item;
Altri suggerimenti
In primo luogo, la query viene valutato due volte (una volta per Any
, e una volta per Min
). In secondo luogo, Min
richiede che iterate su tutta la lista, anche se il fatto che si tratta di mezzi ordinati che la prima voce sarà il minimo. Dovreste essere in grado di cambiare questo:
if (items.Any())
{
return list[items.Min()];
}
Per questo:
var default =
(from kv in list
where kv.Key >= i
select (int?)kv.Key).FirstOrDefault();
if(default != null) return list[default.Value];
return null;
Aggiorna
Perché si sta selezionando un tipo di valore, FirstOrDefault
non restituisce un oggetto annullabile. Ho modificato la query per il cast del valore selezionato a un int?
invece, permettendo al valore risultante da controllare per null
. Auspico, pertanto, questo rispetto all'uso di ContainsKey
, come sarebbe tornare true
se l'elenco contiene un valore per 0
. Per esempio, supponiamo di avere i seguenti valori
0 2 4 6 8
Se si dovesse passare a qualcosa di meno o uguale a 8, poi si otterrebbe il valore corretto. Tuttavia, se si dovesse passare a 9, si otterrebbe 0 (default(int)
), che è nella lista, ma non un risultato valido.
Utilizzando LINQ su un SortedList
non vi darà il beneficio del genere.
Per ottenere prestazioni ottimali, si dovrebbe scrivere il proprio binario di ricerca.
OK, giusto per dare a questo un po 'più di visibilità - Ecco una versione più concisa della risposta di Adam Robinson:
return list.FirstOrDefault(kv => kv.Key >= i).Value;
La funzione FirstOrDefault
ha un sovraccarico che accetta un predicato, che seleziona il primo elemento che soddisfa una condizione - è possibile utilizzare che per ottenere direttamente l'elemento che si desidera, o null
se non esiste.
Perché non usare il BinarySearch
che è costruito nella classe List
?
var keys = list.Keys.ToList();
int index = keys.BinarySearch(i);
if (index < 0)
index = ~index;
var item = index < keys.Count ? list[keys[index]] : null;
return item;
Se la destinazione di ricerca non è nella lista, BinarySearch
restituisce il complemento bit per bit della voce immediatamente superiore; possiamo utilizzare che per ottenere direttamente ciò che vuoi da ri-integrando il risultato se è negativo. Se diventa uguale al Count
, la vostra chiave di ricerca era più grande di qualsiasi altra cosa nella lista.
Questo dovrebbe essere molto più veloce di fare un
Come commenti hanno sottolineato, la chiamata where
LINQ, dal momento che è già ordinato ... ToList
costringerà una valutazione di tutta la lista, quindi questo è utile solo se si fa più ricerche senza alterare il SortedList
sottostante, e si mantiene l'elenco keys
intorno separatamente.
Utilizzando OrderedDictionary in PowerCollections è possibile ottenere un enumeratore che inizia dove si chiave sono alla ricerca di dovrebbe essere ... se non è lì, si otterrà il nodo successivo più vicino e può quindi passare avanti / indietro da quella in O (log n) per ogni chiamata nav.
Questo ha il vantaggio di non dover scrivere il proprio di ricerca o anche gestire le proprie ricerche sulla cima di una SortedList.