Ricerca dei dati

Una delle operazioni più frequentemente eseguita su una collezione di dati è la ricerca di un particolare valore, al fine di determinarne la presenza o meno nella collezione.

Se non si ha nessuna informazione in merito alla disposizione dei dati, l'unica strategia di ricerca possibile è la scansione lineare o ricerca sequenziale.
Tale algoritmo prevede la scansione in sequenza di tutti gli elementi della collezione, per verificare la presenza del valore da cercare.
La ricerca si arresta quando il valore viene trovato oppure quando si esaurisce la sequenza.
In caso la collezione di elementi fosse notevolmente numerosa, una tale strategia di ricerca sarebbe poco efficiente e molto onerosa in termini di tempo.

Se invece la collezione di elementi fosse ordinata (in modo crescente o decrescente), si potrebbe attuare una strategia molto più efficiente, nota come ricerca binaria o dicotomica.

Se, in aggiunta all'ordinamento, si avessero anche delle informazioni sulla distribuzione dei valori all'interno della sequenza, la strategia risolutiva potrebbe essere ancora più ottimizzata ed eseguire una ricerca simile a quella binaria, nota come ricerca interpolata.

Vediamo un po' più nello specifico le principali strategie citate.

Ricerca sequenziale

Come indicato poc'anzi, se non si hanno informazioni in merito alla disposizione dei dati, l'unica strategia di ricerca attuabile è la scansione lineare, ossia l'attraversamento della collezione, dal primo elemento all'ultimo, fino a che si trova l'elemento cercato o si esaurisce la sequenza.

L'algoritmo può essere implementato in due varianti, la prima, che restituisce semplicemente un valore boolean per indicare la presenza o meno del valore cercato nella collezione, la seconda, che restituisce la posizione del valore cercato nella sequenza, oppure il valore -1, nel caso in cui esso non fosse presente.

L'implementazione in Java di entrambe le varianti, per la ricerca di un valore stringa in una collezione di stringhe, potrebbe essere la seguente.

// prima variante
// cerca un valore nella collezione e restituisce 
// true se esso è presente, false in caso contrario
public static boolean cerca(String val, String[] collezione) {
    if (val == null) {
        return false;
    }
        
    for (int i = 0; i < collezione.length; i++) {
        if (val.equals(collezione[i])) {
            return true;
        }
    }
    return false;
}         

// seconda variante
// cerca un valore nella collezione e restituisce 
// la sua posizione oppure -1 se esso non è presente
public static int cerca(String val, String[] collezione) {
    if (val == null) {
        return -1;
    }
        
    for (int i = 0; i < collezione.length; i++) {
        if (val.equals(collezione[i])) {
            return i;
        }
    }
    return -1;
}

Tale tipo di ricerca richiede, nel caso peggiore, ossia qualora l'elemento da cercare fosse l'ultimo della collezione, oppure non fosse presente, un numero massimo di tentativi (confronti) pari al numero di elementi della collezione.
Nel caso migliore, ossia se l'elemento da cercare fosse il primo della sequenza, allora il numero di confronti sarebbe pari ad uno.
In media, si può concludere che occorrono n/2 confronti, dove n rappresenta il numero degli elementi della collezione.
Si dice che un tale algoritmo ha una complessità computazionale lineare e si indica con O(n), ad indicare che il tempo necessario per cercare un elemento cresce in modo lineare con il numero degli elementi della collezione.

Ricerca binaria o dicotomica

Nei casi in cui occorre cercare un elemento all'interno di una sequenza ordinata, si possono mettere in atto strategie più efficienti rispetto alla banale ricerca sequenziale.
La ricerca binaria o dicotomica è possibile su una struttura che consente l'accesso diretto ai suoi elementi (es. un vettore).

Essa verifica la presenza dell'elemento da cercare secondo la logica che segue:

  • l'elemento da cercare viene confrontato con l'elemento centrale della sequenza, se i due elementi sono uguali, la ricerca termina con successo
  • se l'elemento centrale della sequenza è successivo (maggiore) all'elemento cercato, la ricerca viene ripetuta con la stessa logica sulla sottosequenza degli elementi a sinistra (minori) di quello centrale
  • se l'elemento centrale della sequenza è precedente (minore) all'elemento cercato, la ricerca viene ripetuta con la stessa logica sulla sottosequenza degli elementi a destra (maggiori) di quello centrale
  • la ricerca termina con esito negativo quando si giunge ad una sequenza di un solo elemento e questi non coincide con il valore da cercare

Il termine binaria o dicotomica sta infatti ad indicare che, ad ogni passo, la ricerca viene ristretta ad un numero di elementi dimezzato rispetto al passo precedente.

Tale algoritmo richiede, nel caso peggiore, un numero di tentativi pari al logaritmo in base 2 della dimensione della sequenza in cui cercare.
Nel caso migliore, invece, se l'elemento da cercare fosse quello centrale della sequenza, il numero di tentativi richiesti sarebbe pari ad uno.
In media, quindi, occorrerebbero log2n tentativi per verificare la presenza di un valore in una sequenza di lunghezza pari ad n.
Si può, quindi, concludere affermando che la complessità di questo algoritmo è pari ad O(log2n), ossia il tempo necessario per cercare un elemento cresce in modo logaritmico rispetto alla lunghezza della sequenza in cui cercare.
Esso risulterebbe, quindi, decisamente più efficiente del precedente algoritmo di ricerca sequenziale.

Vediamo come esso procederebbe attraverso un esempio grafico.
Supponiamo di dover cercare il valore 5 nel vettore seguente.

1 2 3 4 5 6 7 8 9 10

Il valore 5 verrebbe confrontato con l'elemento centrale della sequenza.

1 2 3 4 5 6 7 8 9 10

Tale valore risulta successivo al valore cercato, pertanto la ricerca può continuare, con la stessa logica, solo nella parte sinistra (elementi minori) della sequenza.

1 2 3 4 5 6 7 8 9 10

Si procede quindi confrontando l'elemento da cercare con l'elemento centrale di questa sottosequenza.

1 2 3 4 5 6 7 8 9 10

Tale valore risulta minore del valore cercato, per cui la ricerca può continuare, con la stessa logica, solo nella parte destra (elementi maggiori) della sequenza.

1 2 3 4 5 6 7 8 9 10

A questo punto il valore cercato coincide con quello centrale della sequenza, pertanto la ricerca termina con successo.

1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10

L'algoritmo che utilizza questa strategia risolutiva viene sviluppato principalmente in forma ricorsiva.
Anche questo algoritmo di ricerca può essere implementato in due varianti, la prima, che restituisce semplicemente un valore boolean per indicare la presenza o meno del valore cercato nella collezione, la seconda, che restituisce la posizione del valore cercato nella collezione, oppure il valore -1 nel caso in cui esso non fosse presente.

L'implementazione in Java di entrambe le varianti, per la ricerca di un valore stringa in una collezione di stringhe potrebbe essere la seguente.

// prima variante
// cerca un valore nella collezione e restituisce 
// true se esso è presente, false in caso contrario
// funzione principale che richiama la funzione ricorsiva che segue
public static boolean ricercaBinaria(String val, String[] collezione) {
    if (val == null) {
        return false;
    }

    return ricercaBinariaR(val, collezione, 0, collezione.length - 1);
}

// funzione ricorsiva richiamata anche dalla funzione precedente
public static boolean ricercaBinariaR(String val, String[] collezione, int iIniz, int iFin) {
    if (iIniz > iFin) {
        return false;
    }

    int iMed = (iIniz + iFin) / 2;
    if (val.equals(collezione[iMed])) {
        return true;
    } else if (val.compare(collezione[iMed]) < 0) {
        return ricercaBinariaR(val, collezione, iIniz, iMed - 1);
    } else {
        return ricercaBinariaR(val, collezione, iMed + 1, iFin);
    }
}

// seconda variante
// cerca un valore nella collezione e restituisce 
// la sua posizione oppure -1 se esso non è presente
// funzione principale che richiama la funzione ricorsiva che segue
public static int ricercaBinaria(String val, String[] collezione) {
    if (val == null) {
        return -1;
    }

    return ricercaBinariaR(val, collezione, 0, collezione.length - 1);
}

// funzione ricorsiva richiamata anche dalla funzione precedente
public static int ricercaBinariaR(String val, String[] collezione, int iIniz, int iFin) {
    if (iIniz > iFin) {
        return -1;
    }

    int iMed = (iIniz + iFin) / 2;
    if (val.equals(collezione[iMed])) {
        return iMed;
    } else if (val.compare(collezione[iMed]) < 0) {
        return ricercaBinariaR(val, collezione, iIniz, iMed - 1);
    } else {
        return ricercaBinariaR(val, collezione, iMed + 1, iFin);
    }
}

Ricerca interpolata

L'algoritmo di ricerca interpolata è un perfezionamento dell'algoritmo di ricerca binaria, utilizzabile nelle situazioni in cui, oltre alla presenza di una collezione ordinata, si abbiano anche informazioni sulla distribuzione degli elementi all'interno della collezione.

Per chiarire meglio il procedimento possiamo pensiamo al modo in cui solitamente procediamo per cercare un termine in un dizionario.

Osservando la prima lettera del termine da cercare, apriamo il dizionario in un punto che non corrisponde sempre alla sua pagina mediana, ma è spostato verso l'inizio o verso la fine del dizionario a seconda che tale prima lettera sia più prossima all'inizio o alla fine dell'alfabeto.

Ad esempio, se il termine da cercare iniziasse con la lettera B, apriremmo il dizionario in un punto prossimo all'inizio, viceversa, se il termine da cercase iniziasse con la lettera T, apriremmo il dizionario in un punto più prossimo alla fine dello stesso.

In termini pratici, la posizione dell'elemento da confrontare con il valore da cercare non corrisponde sempre alla posizione mediana della sequenza, ma bensì è determinato attraverso una funzione di interpolazione, che si basa sulle informazioni note in merito alla distribuzione degli elementi all'interno della collezione.

Non approfondiremo tale algoritmo di ricerca con la sua implementazione in Java.
Per gli scopi di questo sito è sufficiente conoscere che le sue prestazioni possono essere in alcuni casi migliori di quelle della ricerca binaria, infatti, la complessità computazionale di questo algoritmo può essere sintetizzata con O(log2log2n), dove n rappresenta sempre la lunghezza della collezione in cui cercare.