Trovare l'intersezione di insiemi coinvolgerebbe qualcosa come un accesso all'hash . Mentre gran parte del setup può essere precalcolato e conservato in memoria, devi comunque eseguire tutte le seguenti operazioni:
- Supponiamo di cercare la lista
N
per le cifre a
e b
.
- Passare a N, che ha ~ un milione di numeri
- Per ogni numero nell'elenco:
- Cerca nella tabella hash la cifra
a
per vedere se esiste N[i]
- Scansiona i puntatori hash per il bucket hash giusto
- Scansiona il bucket hash per vedere se
N[i]
esiste
- Cerca nella tabella hash la cifra
b
per vedere se esiste N[i]
- Scansiona i puntatori hash per il bucket hash giusto
- Scansiona il bucket hash per vedere se
N[i]
esiste
D'altra parte, se hai saltato le tabelle e hai scritto un algoritmo efficace per il controllo sul posto, potresti:
- Supponiamo di cercare la lista
N
per le cifre a
e b
.
- Passare a N, che ha ~ un milione di numeri
- Per ogni numero nell'elenco:
- Per ogni cifra nel numero
- Dividi per 10
- Confronta il resto con
a
e b
Quindi per l'hash join, hai diverse complicate funzioni di ricerca e localizzazione, per ogni N[i]
, tutte richiedono operazioni di lettura, incremento e confronto della memoria. Ciò significa {numero di numeri} x {numero di hashtables} x {righe nella ricerca hash + righe nel bucket hash}. Si moltiplicano tutti! Nel frattempo, per una scansione lineare, hai due operazioni di basso livello (un'operazione di divisione + modulo e un confronto) per numero. Questo è molto, molto meno elaborazione.
È difficile valutare le prestazioni da soli, ma suppongo che non otterrete molte prestazioni, se ve ne sono, da una soluzione di hash join. In realtà potrebbe essere peggiore a causa della bassa selettività - dato un numero compreso tra 1 e 1.000.000, circa il 50% di loro avrà una data cifra. Se quel numero fosse molto più piccolo, una tabella hash aumenterebbe un po 'le prestazioni, ma se stai ritirando la metà o più dei dati, una scansione inizia ad apparire sempre migliore. Se consideri l'aumento dell'utilizzo della memoria (e quindi l'aumento del working set) necessario per supportare le tabelle hash, ho intenzione di scommettere che le prestazioni del design hash / intersezione sarebbero peggiori.
Ecco un semplice codice che controlla in modo efficiente la presenza di un set di cifre. Ho usato questo algoritmo sul mio portatile Dell Precision e sono riuscito a scansionare 1.000.000 di numeri in 0.0120 secondi. Vorrei solo eseguire questa funzione, per numero nell'elenco, quando necessario.
int ContainsDigits(int numberToCheck, int digitsToFind[], int digitCount)
{
int result;
int digits[10];
memcpy(digits, digitsToFind, digitCount * sizeof(int));
while (numberToCheck > 0)
{
std::div_t result = std::div(numberToCheck, 10);
for (int i = digitCount -1; i >= 0; i--)
{
if (result.rem == digits[i])
{
if (!--digitCount) return 1;
digits[i] = digits[digitCount];
}
}
numberToCheck = result.quot;
}
return 0;
}
L'algoritmo controlla la cifra meno significativa (data da n % 10
) in un ciclo, quindi sposta il numero a destra (equivalente a n / 10
). Possiamo ottenere il modulo e il quoziente in un'unica operazione ( std::div
).
Le cifre ricercate sono memorizzate in un array. Quando viene trovata una cifra, la matrice viene abbreviata di un elemento, con l'elemento finale spostato nella posizione della cifra scoperta, mantenendo così la lista compattata. Quando la matrice è vuota, tutte le cifre sono state trovate. Se il ciclo termina e non tutte le cifre sono state trovate, la funzione restituisce false.