Algortimi di ricerca
- Ricercare elementi all'interno di una struttura dati,
- Si usa il suo valore e un proprietà che lo identifica.
- Si può usare un accesso casuale o sequenziale.
Tuttavia, si richiede che equal rappresenti una relazione di equivalenza:
- riflessiva: equal (a, a)
- simmetrica: equal (a, b) = ⇒ equal (b, a)
- transitiva: equal (a, b) ∧equal (b, c) = ⇒ equal (a, c)
Ricerca lineare - Linear Search
Complessità: O(n)
- I valori nella stuttura dai non importa che siano ordinati.
- Scorre in modo sequenziale la struttura dati fino a trovare l'elemento cercato.
Esempio:
Indovia il numero che sto pensando in un range da 1 a 5?
Ipotizziamo 5 come la soluzione e vediamo quanti tentativi ci vogliono per indovinare.
- Step 1: 1 sbalgiato
- Step 2: 2 sbalgiato
- …
- Step 5: 5 corretto
Codice di esempio in python:
def linear_search(array, elem, eq = lambda x, y: x == y):
for i in range(len(array)):
if eq(array[i],elem):
return i
return -1
Andiamo a vedere altri 2 esempi calati nel codice
/*
Caso 1
Struttura dati = [31, -7, 2, 8, 44, 0, -1, 5]
Cerca il valore 8
Controlla: 31 → -7 → 2 → 8 ✓
Trova 8 in posizione 3
Restituisce 3
Caso 2:
Cerca il valore 99
Controlla: 31 → -7 → 2 → 8 → 44 → 0 → -1 → 5
Non trova 99 dopo aver controllato tutto l'array
Restituisce None
*/
Andiamo a vedere il codice in python con l'utilizzo della ricorsione
def linear_search_rec(array, x, start=None, to=None, eq = lambda x, y: x == y):
if start is None or to is None:
start = 0
to = len(array)-1
if start > to: return -1
if eq(array[start], x): return start
return linear_search_rec(array, x, start+1, to, eq)
Ricerca binaria
La ricerca binaria:
- Complessità: O(log n) -> più efficiente della ricerca lineare O(n) per grandi dataset.
- Richiede un array ordinato
Flusso:
- Divide l'array a metà ad ogni passo
- Confronta l'elemento centrale con il valore cercato
- Scarta metà dell'array ad ogni iterazione
- (L'algoritmo dimezza l'area di ricerca ad ogni passo, da cui la complessità O(log n).)
- Con elementi duplicati si può restituire qualsiasi indice corrispondente all'elemento cercato e non necessariamente il primo o l'ultimo.
Esempio
indovia il numero che sto pensando in un range da 1 a 100 Ipotizziamo 100 come la soluzione e vediamo quanti tentativi ci vogliono per indovinare.
- Step 1: 50 sbalgiato è piu alto
- Step 2: 75 sbalgiato è piu alto
- Step 3: 88 sbalgiato è piu alto
- Step 4: 94 sabgliato è piu alto
- Step 5: 97 sabgliato è piu alto
- Step 6: 99 sbalgiato è piu alto
- Step 7: 100 corretto
In questo caso il lineare avrebbe impiagato 100 step invece questo binario solamente 7
Codice di esempio in python:
def binary_search_iter(array, x, start, to, eq = lambda x, y: x == y, less = lambda x, y: x < y):
while start <= to:
m = (start + to) // 2
if eq(array[m], x): return m
elif less(array[m], x): start = m+1
else: to = m-1
return -1
def binary_search(array, x):
return binary_search_iter(array, x, 0, len(array)-1)
Altri esempi
/*
strutturaDati = [1, 3, 5, 7, 9, 11, 13]
Se cercassimo 7:
1. Prima iterazione:
- left = 0, right = 6
- mid = (0 + 6) // 2 = 3
- arr[3] = 7 → trovato! Ritorna 3
Se cercassimo 11:
1. Prima iterazione:
- mid = 3
- arr[3] = 7 < 11
- left = mid + 1 = 4
2. Seconda iterazione:
- left = 4, right = 6
- mid = (4 + 6) // 2 = 5
- arr[5] = 11 → trovato! Ritorna 5
*/
Andiamo a vedere il codice in python con l'utilizzo della ricorsione
def binary_search_recur(array, x, start, to, eq = lambda x, y: x == y, less = lambda x, y: x < y):
if to < start: return -1
if start == to:
return start if eq(array[start], x) else -1
mid = start + (to - start)//2
print(f"from={start}; to={to}; mid={mid}")
if eq(array[mid], x): return mid
elif less(array[mid], x): return binary_search_recur(array, x, mid+1, to)
else: return binary_search_recur(array, x, start, mid-1)
Ricerca per iterpolazione
- Similare al binary search,
- Invece di dividere a metà usa una formula di interpolazione per stimare la posizione
- Assume distribuzione uniforme dei dati
Complessità:
- O(1) caso migliore
- O(log log n) caso medio con dati uniformi
- O(n) caso peggiore
Algoritmi di ordinamento
Selection Sort
- Opera trovando ripetutamente il minimo elemento nella parte non ordinata dell'array e posizionandolo all'inizio.
complessita:
- Caso medio/peggiore/migliore: O(n²)
- Spazio: O(1)
Codice di esempio in python:
def min_index(a, index_from, index_to):
min_idx = index_from
for i in range(index_from+1, index_to):
if a[i] < a[min_idx]:
min_idx = i
return min_idx
def selection_sort(a):
for i in range(len(a)-1):
min_idx = min_index(a, i, len(a))
a[i], a[min_idx] = a[min_idx], a[i]
Esempio
/*
StrutturaDati = [64, 25, 12, 22, 11]
Passi dell'esempio:
[64, 25, 12, 22, 11] → [11, 25, 12, 22, 64] (trova 11)
[11, 25, 12, 22, 64] → [11, 12, 25, 22, 64] (trova 12)
[11, 12, 25, 22, 64] → [11, 12, 22, 25, 64] (trova 22)
[11, 12, 22, 25, 64] → [11, 12, 22, 25, 64] (trova 25)
Output: [11, 12, 22, 25, 64]
Complessità: O(n²) in tutti i casi.
*/
Insertion Sort
- Funziona inserendo sequenzialmente ogni elemento nella posizione corretta all'interno della porzione già ordinata dell'array.
complessita:
- Caso medio/peggiore (array ordinato al contrario): O(n²)
- Caso migliore (array già ordinato): O(n)
- Spazio: O(1)
Codice di esempio in python:
def insert_in_order(arr, n, e):
pos = n
while pos > 0 and arr[pos-1] > e:
arr[pos] = arr[pos-1]
pos -= 1
arr[pos] = e
def insertion_sort(a):
for i in range(1,len(a)):
insert_in_order(a, i, a[i])
Esempio
/*
strutturaDati = [64, 25, 12, 22, 11]
Passi dell'esempio:
[64, 25, 12, 22, 11] → [25, 64, 12, 22, 11] (inserisce 25)
[25, 64, 12, 22, 11] → [12, 25, 64, 22, 11] (inserisce 12)
[12, 25, 64, 22, 11] → [12, 22, 25, 64, 11] (inserisce 22)
[12, 22, 25, 64, 11] → [11, 12, 22, 25, 64] (inserisce 11)
Output: [11, 12, 22, 25, 64]
*/
Bubble Sort
- confronta ripetutamente coppie di elementi adiacenti
- li scambia se sono nell'ordine sbagliato.
Complessità:
- Best case (array ordinato): O(n)
- Worst case: O(n²)
Info:
- La presenza di elementi grandi all’inizio non è problematica in quanto questi raggiungo la parte finale dell’array in poche scansioni
- La presenza di elementi piccoli alla fine invece è più problematica, in quanto questi si sposteranno di poco ad ogni scansione
Codice di esempio in python:
def bubble_sort(a):
swap = True
n = len(a)
i = 0
while swap and i < n-1:
swap = False
for j in range(n-1-i):
if a[j] > a[j+1]:
swap = True
a[j], a[j+1] = a[j+1], a[j]
i += 1
Codice Versione ricorsiva
def bubble_sort_recursive(arr, n=None): # Inizializzazione della dimensione al primo chiamata
if n is None:
n = len(arr)
# Caso base: se la dimensione è 1 o meno, l'array è ordinato
if n <= 1:
return
# Fase di "bolla": fa "galleggiare" l'elemento più grande fino alla fine
for i in range(n-1):
if arr[i] > arr[i+1]:
arr[i], arr[i+1] = arr[i+1], arr[i]
# Chiamata ricorsiva sulla porzione non ordinata (tutti tranne l'ultimo)
bubble_sort_recursive(arr, n-1)
Esempio
strutturaDati = [64, 34, 25, 12, 22]
Passaggi degli esempi [64, 34, 25, 12, 22]:
[34, 25, 12, 22, 64] # 64 "galleggia" alla fine
[25, 12, 22, 34, 64] # 34 trova la sua posizione
[12, 22, 25, 34, 64] # 25 trova la sua posizione
[12, 22, 25, 34, 64] # Ultima passata per verificare
Output [12, 22, 25, 34, 64]
Merge Sort
Il merge sort è un algoritmo di ordinamento che usa la strategia divide-et-impera:
- Divide l'array a metà ricorsivamente finché non ottiene singoli elementi
- Merge (fonde) le parti ordinate in modo crescente
- Opera out-of-place usando array temporanei per il merge
Caratteristiche principali:
- Stabile (mantiene ordine relativo di elementi uguali)
- Complessità sempre O(n log n)
- Richiede memoria extra O(n)
- Efficiente su input molto grandi
- Parallelizzabile
Codice esempio con ricorsione in python
def merge(a, b, r):
i, j, k = 0, 0, 0
# print(f"merge {a} and {b} into {r}")
while i < len(a) and j < len(b):
if a[i] <= b[j]:
r[k] = a[i]
i = i + 1
else:
r[k] = b[j]
j = j + 1
k = k + 1
for i in range(i,len(a)):
r[k] = a[i]
k = k + 1
for i in range(j,len(b)):
r[k] = b[i]
k = k + 1
def merge_sort(a):
if len(a)<=1: return
m = len(a)//2
left = a[:m] # beware: it's a copy
right = a[m:] # beware: it's a copy
merge_sort(left)
merge_sort(right)
merge(left, right, a)
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
Esempio
Esempio passo-passo con [38, 27, 43, 3]:
Divide: [38, 27] [43, 3]
Divide ancora: [38] [27] [43] [3]
Merge: [27, 38] [3, 43]
Merge finale: [3, 27, 38, 43]
Output: [3, 27, 38, 43]
Quick Sort
Funzionamento QuickSort:
- Scelta del pivot (solitamente primo elemento, elemento centrale o casuale)
- Partizionamento dell'array intorno al pivot
- Ricorsione sulle due parti (sinistra e destra del pivot)
Complessità:
-
Caso medio: O(n log n)
-
Caso peggiore: O(n²) quando:
-
Array già ordinato e pivot sempre minimo/massimo
-
Array ordinato al contrario
-
Tutti elementi uguali
Caso migliore: O(n log n)
def partition(a,start,to):
pivot = a[start]
k = start+1
for i in range(start+1,to):
if a[i] < pivot:
a[i], a[k] = a[k], a[i]
k += 1
a[start] = a[k-1]
a[k-1] = pivot
return k-1
# sort array
def quick_sort(array, start=None, to=None):
start = 0 if start==None else start
to = len(array) if to==None else to
if start < to:
pivot_index = partition(array,start,to)
quick_sort(array,start,pivot_index)
quick_sort(array,pivot_index+1,to)
return quick_sort(lesser) + equal + quick_sort(greater)
// Esempio di utilizzo
arr = [3, 6, 8, 10, 1, 2, 1]
print("Array originale:", arr)
sorted_arr = quick_sort(arr)
print("Array ordinato:", sorted_arr)
// Processo di partizionamento per arr = [3,6,8,10,1,2,1]:
// Pivot = 3
// lesser = [1,2,1] # elementi < 3
// equal = [3] # elementi = 3
// greater = [6,8,10] # elementi > 3
// Ricorsione su [1,2,1]:
// Pivot = 1
// lesser = []
// equal = [1,1]
// greater = [2]
// Ricorsione su [6,8,10]:
// Pivot = 6
// lesser = []
// equal = [6]
// greater = [8,10]
Il pivot è un elemento dell'array scelto come riferimento per il partizionamento. Intorno ad esso, l'array viene riorganizzato in modo che:
elementi minori del pivot vadano a sinistra elementi maggiori del pivot vadano a destra
Altro esempio
/*
Passo 1: Pivot = 7
[7] 2 1 6 8 5 3 4
Partizionamento:
[2,1,6,5,3,4] [7] [8]
Passo 2: Lavoro sulla parte sinistra, pivot = 2
[2] 1 6 5 3 4
Partizionamento:
[1] [2] [6,5,3,4]
Passo 3: Lavoro sulla parte [6,5,3,4], pivot = 6
[6] 5 3 4
Partizionamento:
[5,3,4] [6] []
Passo 4: Lavoro su [5,3,4], pivot = 5
[5] 3 4
Partizionamento:
[3,4] [5] []
Passo 5: Lavoro su [3,4], pivot = 3
[3] 4
Partizionamento:
[] [3] [4]
Risultato finale:
[1] [2] [3] [4] [5] [6] [7] [8]
*/
Taballa comparativa tra i vari algoritmi visti finora
### **See you in the next post! Stay Tuned!**