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.

  1. 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.
  2. Si trova il mediano all’interno di ciascuno di questi gruppi
  3. Si calcola il mediano dei mediani precedenti
  4. 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.