Per spiegare la tecnica usiamo come esempio il problema della selezione:
Data una lista A di interi distinti ed un intero con vogliamo sapere quale elemento occuperebbe la posizione se il vettore venisse ordinato.
Individuiamo alcuni casi particolari:
- Per abbiamo il minimo di
- Per avremo il massimo di
- Per avremo il mediano di
Un semplice algoritmo potrebbe essere:
def selezione1(A, k):
A.sort()
return A[k-1]
Che ha come costo .
Usando però il divide et impera avremo un costo di nel caso generale, dimostreremo quindi che il problema della selezione è computazionalmente più semplice di quello dell’ordinamento che richiede tempo
Approccio basato sul Divide et Impera:
- Scegli nella lista A l’elemento in posizione che chiamiamo perno
- A partire da costruisci due liste , la prima contiene gli elementi di minori del perno e la seconda i maggiori
- Dove si trova l’elemento di rango ?
- Se allora l’elemento di rango è nel vettore
- Se allora l’elemento di rango è proprio il perno
- Se allora l’elemento di rango è in , è l’elemento di rango in
Implementazione:
def selezione2(A,k):
if len(A) == 1:
return A[0]
perno = A[0]
A1, A2 = [], []
for i in range(len(A)):
if A[i] < perno:
A1.append(A[i])
else:
A2.append(A[i])
if len(A1) >= k:
return selezione2(A1, k)
elif len(A1) == k-1:
return perno
return selezione2(A2, k - len(A1) - 1)
Questa procedura che tripartisce la lista può però restituire una partizione massimamente sbilanciata in cui si ha ad esempio ovvero quando il perno è l’ultimo elemento della lista.
Se poi questo caso si ripete ad ogni partizione creata dall’algoritmo allora la complessità di questo sarebbe catturata da:
In generale la complessità superiore dell’algoritmo è data da:
Dove
Quindi se avessimo una scelta del perno che garantisca sempre un equilibrio fra le liste che crea l’algoritmo avremo che:
E quindi la complessità diventa:
Ovviamente avere sempre un perno che divide a metà le liste è troppo, osserviamo che in realtà possiamo accontentarci di una qualsiasi frazione anche piuttosto vicina ed ottenere sempre .
Ad esempio se abbiamo:
Proviamo quindi a scegliere il perno a caso in modo equiprobabile tra gli elementi della lista, in questo modo anche se la scelta non produce una lista perfettamente bilanciata avremo comunque una complessità lineare.
def selezione2R(A, k):
if len(A)==1
return A[0]
perno = A[randint(0, len(A) - 1)]
A1, A2 = [], []
for x in A:
if x < perno:
A1.append(x)
elif x > perno:
A2.append(x)
if len(A1) >= k:
return selezione2R(A1, k)
elif len(A1) == k - 1:
return perno
return selezione2R(A2, k - len(A1) - 1)
Con questo algoritmo abbiamo che con alta probabilità il costo è lineare mentre al caso peggiore, che accade con una probabilità molto piccola è di .
C’è in realtà un algoritmo deterministico che garantisce una complessità di anche al caso pessimo.
Questo modo noto come mediano dei mediani garantisce che il perno scelto produce sempre due sottoliste A1 e A2 ciascuna delle quali ha non più di elementi.
- Dividi l’insieme A da elementi in gruppi da 5, ovviamente l’ultimo potrebbe non averne 5. Si prendono in considerazione soltanto i primi gruppi.
- Si trova il mediano all’interno di ciascuno di questi gruppi
- Si calcola il mediano dei mediani precedenti
- Si usa come pivot per
def selezione(A, k):
if len(A) <= 120:
A.sort()
return A[k - 1]
B = [sorted(A[5*i:5*i+5])[2] for i in range(len(A)//5)]
perno = selezione(B, ceil(len(A)/10))
A1, A2 = [], []
for x in A:
if x < perno:
A1.append()
elif x > perno:
A2.append()
if len(A1) >= k:
return selezione(A1, k)
elif len(A1) == k - 1:
return perno
return selezione(A2, k - len(A1) - 1)
Questo algoritmo quindi risolve il problema in modo lineare anche al caso pessimo, però a causa delle grandi costanti nascoste nell’, in realtà l’algoritmo random di prima si comporta meglio.