Algoritmi di ricerca e ordinamento

Vediamo degli algoritmi non banali:

Sono esempi di algoritmi


Problema della ricerca

Problema: decidere se un intero si trova in un vettore

Soluzione: controllare ogni elemento.

  public static boolean Cerca(int v[], int x) {
    boolean trovato;
    int i;

    trovato=false;

    for(i=0; i<v.length; i++)
      if(v[i]==x)
        trovato=true;

    return trovato;
  }

Parte inutile della ricerca

Quando ho trovato un elemento, il metodo continua ugualmente a verificare gli altri.

È un problema se:

Soluzione alternativa: quando trovo l'elemento, mi fermo.

  public static boolean cerca(int v[], int x) {
    int i;

    for(i=0; i<v.length; i++)
      if(v[i]==x)
        return true;

    return false;
  }

Se l'elemento non c'è, finisco con il fare tutta la scansione del vettore.

In generale, questo è inevitabile.


Vettore ordinato

Il vettore è ordinato se il primo elemento è minore del secondo, che è minore del terzo, ecc.

Vettori ordinati:

-1 0 3 10 60 120 900
-100 4 20
0 1 2 3 4 50

Vettori non ordinati:

-1 0 3 10 9 120 900
-100 4 20 0
1 0 1 2 3 4 50

Ricerca in vettore ordinato

Come si fa?

Posso usare gli stessi metodi dei vettori non ordinati.

Posso sfruttare l'ordinamento.

Domanda: che vantaggio ho?


Vantaggio dell'ordinamento

Se cerco 4, e trovo un elemento maggiore, so che è inutile andare avanti:

-2 -1 3 5 ...

Quando arrivo al 5, so che il 4 non lo trovo dopo, perchè altrimenti il vettore non sarebbe ordinato.

Esercizio: modificare il programma di ricerca per tenere conto di questo caso.


Ricerca in un vettore ordinato: algoritmo

Dato un vettore v e un intero x, per ogni elemento del vettore:

  1. se coincide con x, ritorna true
  2. se è maggiore di x, ritorna false
  3. se è minore, continua la scansione

Se si arriva alla fine, vuol dire che tutti gli elementi del vettore sono minori di x, e quindi si ritorna false


Ricerca in un vettore ordinato: programma

Si tratta solo di implementare l'algoritmo:

  public static boolean cercaOrdinato(int v[], int x) {
    int i;

    for(i=0; i<v.length; i++) {
      if(v[i]==x)
        return true;
      if(v[i]>x)
        return false;
    }

    return false;
  }

Esercizio

Trovare un vettore non ordinato su cui il metodo non funziona.


Soluzione

Se cerco 4 e trovo 5 mi fermo.

Se il vettore non è ordinato, il 4 può trovarsi dopo.

1 5 4

La ricerca binaria

Si tratta ancora di trovare un elemento in un vettore ordinato.

Idea: invece di procedere sequenzialmente, parto da metà.

Tipo di ricerca ``elenco telefonico''


Ricerca binaria: algoritmo

Parto da metà vettore

Se l'elemento è quello da cercare, ritorno true

Se l'elemento da cercare è minore di quello a metà del vettore, cerco nella prima metà

Se è maggiore, cerco nella seconda metà


Ricorsione sui vettori

Avrei solo due argomenti (il vettore e l'intero da cercare).

Metto altri due argomenti (indice di inizio e fine ricerca).

Algoritmo:

È un tipico algoritmo ricorsivo.


Ricerca binaria: traduzione in programma

Trovare elemento in mezzo=divisione intera

Cercare nella prima metà=chiamata ricorsiva passando lo stesso indice di inizio e quello di mezzo meno uno.

Cercare nella seconda metà=...


Ricerca binaria: programma senza caso base

Metodo ricorsivo senza i casi base:

  public static boolean cercaBinario(int v[], int x, int inizio, int fine) {

    ...

    int mezzo=(inizio+fine)/2;

    if(v[mezzo]==x)
      return true;
    else if(v[mezzo]>x)
      return cercaBinario(v, x, inizio, mezzo-1);
    else
      return cercaBinario(v, x, mezzo+1, fine);
  }

Caso/casi base

Dove arrivo facendo le invocazioni ricorsive?

Prima o poi si arriva al caso: fine=inizio-1

Corrisponde alla ricerca in un vettore di zero elementi.

Si deve ritornare false


Ricerca binaria: metodo completo

Traduco in codice anche il passo base.

  public static boolean cercaBinario(int v[], int x, int inizio, int fine) {
    if(inizio>fine)
      return false;

    int mezzo=(inizio+fine)/2;

    if(v[mezzo]==x)
      return true;
    else if(v[mezzo]>x)
      return cercaBinario(v, x, inizio, mezzo-1);
    else
      return cercaBinario(v, x, mezzo+1, fine);
  }

Efficienza degli algoritmi

Spazio di memoria/tempo che richiedono.

Devo tenere conto che:

Per la ricerca in vettore ordinato ho la ricerca sequenziale e la ricerca binaria.

L'efficienza dipende dal vettore e dall'elemento da cercare.

Caso in cui la ricerca sequenziale è più veloce:

v[]={1, 2, 3, 4, 5, 6, 7, 8, 9};
x=1;

Caso in cui la ricerca binaria è più veloce:

v[]={0, 3, 4, 10, 12, 143, 159, 200};
x=10;

Valutazioni

  1. devo valutare quale algoritmo è migliore
  2. ma, questo dipende dai dati su cui lavorano

Si danno valutazioni complessive:

  1. caso migliore
  2. caso peggiore
  3. caso medio

In questo modo, posso dire quale algoritmo è il migliore complessivamente (senza specificare i dati di input).


Valutazioni

caso migliore
il minimo tempo che ci mette il programma (dati su cui ci mette di meno)
caso peggiore
il tempo che ci mette sui dati peggiori
caso medio
devo specificare una distribuzione di probabilità sui dati

Dimensione dei dati

Se il vettore ha quattro elementi, tutti gli algoritmi vanno bene.

L'efficienza è importante quando ci sono grandi quantità di dati (vettori grandi).

pochi dati poco tempo
tanti dati tanto tempo

Questa è una cosa naturale!


Modello di costo

Assumo che ogni istruzione richiede tempo 1

Tempo di esecuzione=numero di istruzioni eseguite

Valutazione in base al numero dei dati

n
dimensione dei dati (grandezza del vettore)
T(n)
tempo impiegato dal metodo su un vettore di grandezza n

Costo degli algoritmi di ricerca

Ricerca sequenziale fino alla fine: tempo T(n)=n in ogni caso.

Ricerca sequenziale in cui mi fermo quando trovo: T(n)=n nel caso peggiore, T(n)=1 nel caso migliore.

Ricerca binaria: T(n)=log(n) nel caso peggiore, T(n)=1 nel caso migliore.

Perchè?


Costo della ricerca binaria

Il costo è log(n), se n è la dimensione del vettore

Dimostrazione ``al contrario'': se ci vogliono x operazioni, quanto è grande il vettore?

  1. una operazione: vettore grande 1
  2. due operazioni: vettore grande 2
  3. tre operazioni: vettore grande 4

La ricerca richiede una operazione in più se il vettore è grande il doppio

Se la dimensione del vettore è esponenziale nel numero di operazioni, allora il numero di operazioni è logaritmico nella dimensione

Esempio: se ho 8 elementi alla prima invocazione riduco a 4, poi a 2 poi a 1, quindi servono 3 invocazioni. Se ho 16 elementi faccio 8, 4, 2, 1, ecc.


Notazione O

Assunzioni:

  1. ignoro le costanti moltiplicative, per cui 2n lo vedo come n
  2. ignoro i termini di ordine inferiore, per cui n2+3n+2 lo vedo come n2

Per me, n2+3n+2 è come n2.

Non uso = perchè questo indica che due grandezze sono uguali

Uso una notazione diversa per dire ``è più o meno come'':

n2+3n+2=O(n2)

Si dice che la complessità è O(n2)


Ordinamento

Caso che analizziamo noi: preso un vettore di interi metterli in ordine crescente.

Esempio: se ho il vettore che contiene:

9 4 3 8 1 5

Voglio il vettore che contiene gli stessi interi, ma in ordine:

1 3 4 5 8 9

Quindi, alla fine, v[0] deve essere minore di v[1] che deve minore di v[2], ecc


Ordinamento, in generale

Noi vediamo solo come si ordina un vettore di interi, ma le stesse cose si possono fare con:

Nell'ultimo caso, deve essere possibile confrontare due oggetti (primo oggetto deve essere "minore" del secondo).

L'ordinamento < fra oggetti non c'è

Esempio di ordinamento: oggetti che contengono un intero, confronto in base agli interi (il primo oggetto deve contenere un intero minore di quello del secondo, ecc)


Utilità dell'ordinamento


Algoritmo più semplice

Trovo il primo e lo metto in prima posizione, trovo il secondo e lo metto in seconda posizione, ecc.

Vediamo prima come si mette il minimo in prima posizione.


Il minimo in prima posizione

Se il primo lo metto in prima posizione, cancello quello che c'era prima: v[0]=minimo cancella l'intero che prima stava in v[0].

Si perde il valore che prima stava in v[0]

Invece, io voglio che alla fine il vettore abbia tutti i valori che aveva all'inizio, ma in una sequenza diversa.

Soluzione: in v[0] ci metto il minimo, e il valore che stava prima in v[0] lo metto dove stava prima il minimo.


Minimo in prima posizione: algoritmo

Si tratta di trovare il minimo in un vettore.

Dato che poi lo devo scambiare di posizione, devo anche sapere dove si trova.

Ricerca della posizione del minimo:

  1. la posizione del minimo è 0
  2. per ogni indice del vettore (da 0 a v.length-1)

Minimo in prima posizione

Se l'elemento più piccolo è v[minpos], allora lo devo mettere in v[0].

Se faccio v[0]=v[minpos], cancello il valore di v[0].

  1. salvo il valore di v[0] in una variabile
  2. faccio v[0]=v[minpos]
  3. in v[minpos] vi metto quello che stava prima in v[0], e che ora sta nella variabile temporanea

Il minimo in prima posizione

Trovo il valore di minpos tale che v[minpos] è l'elemento più piccolo del vettore.

L'intero che sta in v[minpos] lo metto in v[0] e viceversa.

  public static void inPrima(int v[]) {
    int i, minpos;

    minpos=0;

    for(i=0; i<v.length; i++)
      if(v[i]<v[minpos])
        minpos=i;

    int temp=v[0];
    v[0]=v[minpos];
    v[minpos]=temp;
  }

Nota: quando si passa un vettore a un metodo, questo equivale a copiare il suo indirizzo nella variabile locale (dato che è un oggetto).

Quando modifico il v locale, questo è lo stesso oggetto del vettore del programma principale.

La variabile del programma e la variabile del metodo sono due variabili diverse, ma contengono lo stesso indirizzo (le due freccie vanno verso lo stesso oggetto).


Ordinamento, ripetendo

So come mettere il minimo in prima posizione.

Cosa resta da fare? Ordinare il resto del vettore.

Tipico esempio di metodo ricorsivo:

ordina il vettore=metti il minimo all'inizio+ordina il resto del vettore


Ordinamento, con ricorsione sui vettori

Ricorsione sui vettori: serve un vettore senza il primo

Invece di creare un nuovo vettore, aggiungo dei parametri al metodo

Alla prima invocazione, devo ordinare tutto il vettore, alla seconda tutto tranne il primo, poi tutto tranne il secondo, ecc.

Aggiungo un parametro inizio:

public static void ordina(int v[], int inizio) { ... }

Ordina il vettore a partire dall'indice inizio fino alla fine.

Ossia: ordina la parte di vettore v[inizio], v[inizio+1], ... , v[v.length-1]


Ordinamento, ricorsivo

Metto il minimo in prima posizione, poi ordino quello che resta del vettore.

Non va fatto su tutto il vettore, ma solo da inizio alla fine!

Faccio la chiamata ricorsiva passando inizio+1


Il programma ricorsivo di ordinamento

Implementazione dell'algoritmo:

  public static void ordina(int v[], int inizio) {
    int i, minpos;

    minpos=inizio;

    for(i=inizio; i<v.length; i++)
      if(v[i]<v[minpos])
        minpos=i;

    int temp=v[inizio];
    v[inizio]=v[minpos];
    v[minpos]=temp;

    ordina(v, inizio+1);
  }

Faccio tutto sul vettore da inizio in poi, invece che su tutto il vettore (da 0 in poi)

Quindi, metto inizio dove prima c'era 0

Cosa manca?


Ordinamento: caso base

Manca il caso base.

Ogni volta che invoco il metodo, la parte di vettore da guardare ha un elemento in meno.

Alla fine, arrivo al vettore di un elemento

Questo succede quando inizio è l'ultimo elemento del vettore.

Il vettore di un elemento è già ordinato.

In altre parole: inizio aumenta di uno a ogni invocazione ricorsiva. Quando arriva alla fine, devo ordinare la parte di vettore con un elemento solo (un vettore di uno o zero elementi è sempre in ordine)

  public static void ordina(int v[], int inizio) {
    int i, minpos;

    if(inizio>=v.length-1)
      return;

    minpos=inizio;

    for(i=inizio; i<v.length; i++)
      if(v[i]<v[minpos])
        minpos=i;

    int temp=v[inizio];
    v[inizio]=v[minpos];
    v[minpos]=temp;

    ordina(v, inizio+1);
  }

Questo algoritmo di ordinamento si chiama selectionsort


Selectionsort: versione iterativa

Ricorsione ---> iterazione

Se la chiamata ricorsiva sta alla fine, è facile.

In questo caso, ripeto tutta la parte:

    minpos=inizio;

    for(i=inizio; i<v.length; i++)
      if(v[i]<v[minpos])
        minpos=i;

    int temp=v[inizio];
    v[inizio]=v[minpos];
    v[minpos]=temp;

Questa parte viene eseguita prima con inizio=0, poi con inizio=1, poi con inizio=2.

L'ultima volta che si esegue questa parte, inizio vale v.length-2: infatti, quando si fa la invocazione passando v.length-1, il metodo termina subito, prima di eseguire la ricerca del minimo.


Selectionsort con un ciclo

Faccio un ciclo in cui inizio va da 0 a v.length-2.

  public static void ordina(int v[]) {
    int i, minpos, inizio;

    for(inizio=0; inizio<=v.length-2; inizio++) {
  
      minpos=inizio;
  
      for(i=inizio; i<v.length; i++)
        if(v[i]<v[minpos])
          minpos=i;
  
      int temp=v[inizio];
      v[inizio]=v[minpos];
      v[minpos]=temp;
  
    }
  }

Complessità del SelectionSort

Quante operazioni vengono eseguite?

Considero un vettore di n elementi, senza specificare quali valori contiene.

Alla prima chiamata ricorsiva, faccio n iterazioni.

Alla seconda, faccio n-1 iterazioni, ecc.

Totale: n+(n-1)+(n-2)+...+2+1 = n(n+1)/2

Ignoro le costanti e i termini inferiori: viene n2, quindi dico che la complessità è O(n2)


Complessità, sulla versione iterativa

La complessità si valuta facilmente sulla versione iterativa:

Il ciclo esterno ha n iterazioni.

Il ciclo interno ha un numero di iterazioni da n a 1, di media n/2

L'istruzione dentro il ciclo interno viene eseguita n.n/2 volte

Metodo dell'istruzione dominante: vado a vedere quante volte si esegue l'istruzione dentro i cicli maggiormente nidificati.


SelectionSort: caso migliore o peggiore

Vengono eseguite O(n2) operazioni indipendentemente dai valori scritti nel vettore.

La complessità del caso migliore e peggiore coincidono: sono tutte e due O(n2)


Altro algoritmo di ordinamento

Vantaggio: più efficiente nel caso migliore.

  1. confronto il primo con il secondo; se non stanno nell'ordine giusto, il scambio
  2. confronto il secondo con il terzo; se non stanno nell'ordine giusto, il scambio
  3. confronto il terzo con il quarto; ...
  4. ...

Alla fine, l'elemento maggiore sta per forza nell'ultima posizione.

Infatti: quando arrivo all'elemento massimo, lo scambio con il successivo. A questo punto, confronto il successivo (che è ancora il massimo) con quello dopo, ecc:

1 10 3 2 4 5

Quando confronto 10 con 3, li scambio:

1 3 10 2 4 5

Ora il confronto è fra 10 e 2:

1 3 2 10 4 5

Il primo elemento del confronto è ancora il massimo

Il massimo elemento, essendo maggiore di tutti gli altri, non può venire ``fermato'' nel suo percorso verso la fine del vettore


Mettere il massimo alla fine: programma

Ripeto il confronto, con eventuale scambio, fra 0,1 fino a v.length-2, v.length-1

Nota: quando ci sono cicli con vettori, controllare gli indici dei vettori nella prima e nell'ultima iterazione:

Nel primo caso, certe operazioni da fare non vengono fatte; nel secondo caso, si genera un errore in esecuzione ArrayOutOfBound


Metodo che mette il massimo in fondo

Faccio un ciclo da 0 a v.length-2, ogni volta confronto ed eventualmente scambio un elemento con il successivo.

  public static void maxDopo(int v[]) {
    int i, temp;

    for(i=0; i<=v.length-2; i++)
      if(v[i]>v[i+1]) {
        temp=v[i];
        v[i]=v[i+1];
        v[i+1]=temp;
    }
  }

Se v[i]<=v[i+1] allora li lascio in pace, altrimenti li scambio.

Quindi, se v[i]>v[i+1] li scambio, altrimenti non faccio niente.

Per lo scambio, serve sempre la variabile temporanea.


Ordinamento a bolle

Quando eseguo il ciclo, il massimo "risale" fino alla cima del vettore.

Dopo aver fatto questo, resta da ordinare il resto del vettore (tutto tranne l'ultimo elemento).

Versione ricorsiva: invece di copiare il vettore, specifico dove il vettore termina (dove finisce la parte di vettore da ordinare)

public static void ordina(int v[], int fine) { ... }

Questo metodo ordina la parte di vettore da 0 a fine


BubbleSort ricorsivo

Faccio il ciclo di confronti, che mette il massimo alla fine.

Faccio la invocazione ricorsiva su tutto il vettore tranne il primo elemento.

  public static void ordina(int v[], int fine) {
    int i, temp;

    for(i=0; i<=fine-1; i++)
      if(v[i]>v[i+1]) {
        temp=v[i];
        v[i]=v[i+1];
        v[i+1]=temp;
    }

    ordina(v, fine-1);
  }

Cosa manca?


BubbleSort: caso base

La parte di vettore da ordinare diventa sempre più piccola a ogni invocazione ricorsiva.

In altre parole: alla prima invocazione fine vale v.length-1 poi diminusce di uno, poi di uno, ecc.

Quando fine==0 la parte di vettore da ordinare è il solo primo elemento.

Un vettore (o una parte di vettore) fatta di un solo elemento è già ordinata.

  public static void ordina(int v[], int fine) {
    int i, temp;

    if(fine==0)
      return;

    for(i=0; i<=fine-1; i++)
      if(v[i]>v[i+1]) {
        temp=v[i];
        v[i]=v[i+1];
        v[i+1]=temp;
    }

    ordina(v, fine-1);
  }

BubbleSort, versione iterativa

Questa parte viene ripetuta:

    for(i=0; i<=fine-1; i++)
      if(v[i]>v[i+1]) {
        temp=v[i];
        v[i]=v[i+1];
        v[i+1]=temp;
    }

La prima volta con fine pari a v.length-1, poi con v.length-2, ecc, fino a 1 (quando fine vale 0 questa parte non viene eseguita, perchè viene fatto subito return)

Faccio un ciclo con fine che va da v.length-1 a 1.

  public static void ordina(int v[]) {
    int i, temp, fine;

    for(fine=v.length-1; fine>=1; fine--) {
      for(i=0; i<=fine-1; i++)
        if(v[i]>v[i+1]) {
          temp=v[i];
          v[i]=v[i+1];
          v[i+1]=temp;
      }
    }
  }

Complessità del BubbleSort

Vettore grande n, di cui non specifico i valori.

Il ciclo esterno ha n iterazioni.

Il ciclo interno ha n iterazioni la prima volta, poi n-1, ecc: di media, ho n/2 iterazioni.

Totale: O(n2)

Questo vale sia nel caso migliore che nel caso peggiore.


Vantaggio del BubbleSort

Se faccio tutto il ciclo interno senza mai fare scambi, vuol dire che il vettore è ordinato.

Infatti, ho:

     for(i=0; i<=fine-1; i++)
        if(v[i]>v[i+1]) 
           scambia

Se non scambio, vuol dire che v[i]<v[i+1] su tutto il vettore.

In questo caso, non serve fare le iterazioni successive (il vettore è già ordinato)


BubbleSort con verifica di ordinamento

Prima di iniziare ogni catena di scambi, assumo che il vettore sia ordinato.

Se scambio degli elementi, allora non lo era.

Se alla fine non ho fatto scambi, termino senza andare avanti.

     boolean ordinato=true;

     for(i=0; i<=fine-1; i++)
        if(v[i]>v[i+1]) {
           ordinato=false;

           scambia
        }

    if(ordinato)
      return;

Come al solito, if(ordinato) è lo stesso di if(ordinato==true).

Nota: ordinato indica se il vettore era già ordinato prima di fare i confronti.


BubbleSort con verifica di ordinamento

Uguale a prima, ma ci metto anche la verifica.

  public static void ordina(int v[]) {
    int i, temp, fine;

    for(fine=v.length-1; fine>=1; fine--) {
      boolean ordinato=true;

      for(i=0; i<=fine-1; i++)
        if(v[i]>v[i+1]) {
          ordinato=false;

          temp=v[i];
          v[i]=v[i+1];
          v[i+1]=temp;
      }

      if(ordinato)
        return;
    }
  }

Complessità del BubbleSort

Caso peggiore: devo fare tutto come prima, quindi ho O(n2)

Caso migliore: il vettore è già ordinato

In questo caso, faccio una intera catena di confronti (eseguo una volta tutto il ciclo più interno), mi accorgo che trovato vale true, e termino.

Costo di caso migliore: O(n)

Il nuovo metodo ha la stessa complessità nel caso peggiore, ma minore nel caso migliore.


Complessità ottima

Nessun metodo di ordinamento può avere complessità minore di O(n)

Infatti, devo almeno verificare se il vettore è già ordinato.

Quindi, il BubbleSort è ha complessità ottima nel caso migliore.

Esistono algoritmi di ordinamento che impiegano O(n log n) nel caso peggiore.

Quindi: il BubbleSort è ottimo nel caso migliore, ma non nel caso medio.


Il mergesort

Basato sul fatto che si possono fondere (merge) due array ordinati in un unico array ordinato in tempo lineare

Algoritmo:


Prima implementazione

Tengo un indice sul primo e sul secondo vettore

A ogni passo, scelgo il più piccolo, e lo metto nel vettore nuovo

  public static int[] merge(int v[], int w[]) {
    int nuovo[]=new int[v.length+w.length];
    int i, j, z;

    i=0;
    j=0;
    for(z=0; z<v.length+w.length; z++) {
      if(v[i]<w[j]) {
        nuovo[z]=v[i];
        i++;
      }
      else {
        nuovo[z]=w[j];
        j++;
      }
    }

    return nuovo;
  }

Come funziona?


Esempio di esecuzione

Situazione iniziale:

Uso due freccie per indicare i valori di i e j

Dato che v[i]<v[j], metto v[i] nel vettore nuovo, e incremento i


Dopo il primo passo

Lascio bianca la prima posizione del primo vettore per chiarezza
(dentro c'è ovviamente ancora il valore 5)

Il più piccolo fra i due elementi puntati dalle freccie è quello di v: lo copio nel vettore nuovo


Terzo passo

Situazione attuale:

Questa volta è w[j] quello più piccolo: lo copio nel vettore nuovo


Quarto passo

Questa volta è di nuovo l'elemento di v quello più piccolo


Ecc.

Vado avanti fino alla fine


Perchè funziona?

Ad ogni passo, il vettore nuovo contiene un elemento in più

Ad ogni passo, l'elemento che viene messo nel vettore nuovo è:

Quindi, ad ogni passo viene sempre messo nel vettore nuovo l'elemento più piccolo fra quelli che mancano


Terminazione

Il programma di sopra funziona solo finchè i vettori non sono finiti

Andando avanti, si arriva a questo punto:

L'elemento di v è più piccolo per cui viene copiato


Dopo la copia dell'ultimo

Ora l'indice su v viene ancora incrementato, per cui va fuori:

Quando si va a fare il confronto fra v[i] e v[j], si genera un errore

Il problema è che i è troppo grande (non è un indice valido per l'array)


Soluzione

Modifico il codice: quando uno degli indici è fuori dal vettore, copio l'elemento dell'altro

Algoritmo: prendo sempre l'elemento più piccolo fra i due vettori e passo all'elemento successivo di quel vettore; quando non ci sono più elementi di uno dei due vettori, copio sempre l'altro

public static int[] merge(int v[], int w[]) {
  int nuovo[]=new int[v.length+w.length];
  int i, j, z;

  i=0;
  j=0;
  for(z=0; z<v.length+w.length; z++) {
    if(j>=w.length) {
      nuovo[z]=v[i];
      i++;
    }
    else if(i>=v.length) {
      nuovo[z]=w[j];
      j++;
    }
    else if(v[i]<w[j]) {
      nuovo[z]=v[i];
      i++;
    }
    else {
      nuovo[z]=w[j];
      j++;
    }
  }

  return nuovo;
}

Costo di esecuzione

Quante operazioni servono per eseguire il metodo di merge?

La parte principale del metodo è dentro un ciclo:

for(z=0; z<v.length+w.length; z++) {
  ...
}

All'interno del ciclo ho solo un numero costante di operazioni (al più, 6 operazioni

Il numero di operazioni è quindi proporzionale a v.length+w.length

Costo di esecuzione: O(n) dove n è la dimensione complessiva dei due vettori


Ordinamento per fusione

È un algoritmo ricorsivo

int [] mergeSort(int v[]) {
  ...
}

Per semplicità, realizziamo un metodo che ritorno un altro vettore (ordinato), invece di ordinare quello passato come parametro


Idea di base

Supponiamo che sia la prima metà che la seconda metà del vettore siano già ordinate:

Per ottenere un unico vettore ordinato, potrei usare il metodo di fusione


Metodo di fusione

Creo due vettori in cui copio le due parti, e poi faccio la fusione di vettori ordinati:


Condizione necessaria

Questo metodo funziona soltanto se le due metà del vettore sono già ordinate

Se non lo sono?

Le devo ordinare

Il metodo mergeSort(v) fa esattamente questo: ordina un vettore.

I due vettori più piccoli li posso ordinare con due invocazioni ricorsive


Caso base della ricorsione

Se il vettore è grande 0 oppure 1 elemento, restituisco un vettore uguale


Algoritmo complessivo


Codice del mergesort

Non dovete ricordare a memoria il metodo

L'algoritmo invece sí

  public static int[] mergeSort(int v[]) {
    int ordinato[]=new int[v.length];

    if(v.length==0)
      return ordinato;

    if(v.length==1) {
      ordinato[0]=v[0];
      return ordinato;
    }

    int primo[]=new int[v.length/2];
    int secondo[]=new int[(v.length+1)/2];

    System.arraycopy(v, 0, primo, 0, v.length/2);
    System.arraycopy(v, v.length/2, secondo, 0, (v.length+1)/2);

    int primo_ord[]=mergeSort(primo);
    int secondo_ord[]=mergeSort(secondo);

    return merge(primo_ord, secondo_ord);
  }

Nota

Perchè ho v.length/2 da una parte e (v.length+1)/2 dall'altra?

Se il vettore ha un numero dispari di elementi, allora facendo due parti grandi v.length/2 mi perdo l'ultimo elemento


Costo del mergesort

A ogni passo, ho un costo lineare (escludendo il costo delle invocazioni ricorsive)

Perè il costo è lineare nella dimensione del vettore passato, non in quella del vettore originario

In altre parole: anche se il vettore di partenza è grande 100, a un certo punto farò le invocazioni ricorsive con vettori grandi 10


Invocazioni ricorsive

Il problema si risolve osservando la sequenza delle invocazioni ricorsive

Le prime due invocazioni sono fatte su vettori grandi la metà:


Secondo passo

Ognuna delle due invocazioni ne fa altre due, su vettori grandi ancora la metà:


Altre chiamate

In ogni chiamata ricorsiva, il vettore viene ancora spezzato e vengono fatte due invocazioni:

ecc.


Osservazione

Contare le caselline di ogni riga

In ogni riga, ho sempre un numero di caselle uguale alla dimensione del vettore di partenza

Quindi?


Costo totale

Ogni sottovettore corrisponde a una invocazione ricorsiva

Ogni invocazione ricorsiva ha costo pari alla dimensione del vettore passato

Quindi, il totale di tutte le invocazioni ricorsive che corrispondono a una certa riga ha costo n (dimensione del vettore originario)


Costo totale

Ogni riga ha costo n

Ci sono log(n) righe.

Costo totale del mergesort: O(n log(n))

Si può dimostrare che non esistono algoritmi più efficienti