08-02-08 Algoritmi di ordinamento (continua) Insertion sort: molto

08-02-08
Algoritmi di ordinamento (continua)
Insertion sort: molto simile all'algoritmo che
usiamo comunemente
per ordinare le carte da gioco in ordine
crescente.
- date n carte gia' ordinate, riceviamo la carta
n+1 e
la inseriamo nella posizione che le compete,
confrontandola
con la piu' piccola, inserendola alla sua
sinistra se minore, oppure
confrontandola via via con le successive, se
maggiore....e poi cosi' proseguendo con le
carte successive che riceviamo.
Sulle liste:
- una lista di 0 oppure 1 elemento e' gia'
ordinata;
- altrimenti, data lista l di lunghezza almeno 2,
ordiniamo
(cdr l) richiamando ricorsivamente il
programma, e alla fine
procediamo a inserire nella posizione corretta
(car l).
[ (accennato una delle lezioni scorse)
Selection sort: opera un numero di confronti
proporzionale al quadrato degli elementi ]
Costo computazionale di insertion sort:
si comporta in modo differente a seconda di
come gli elementi
sono distribuiti all'interno della lista.
Se la lista l e' "quasi ordinata", insertion sort
opera un numero di confronti proporzionale a
n.
Se la lista e' ordinata in ordine inverso, (car l)
deve essere confrontato
con tutti gli altri elementi (gia' ordinati) per
essere collocato in fondo alla lista formata
fino a quel momento. In questo caso il
numero di confronti diventa proporzionale al
quadrato di n (quindi inefficiente). Nel caso
medio insertion sort ordina una lista con un
numero di confronti proporzionale al quadrato
di n. Quindi non e' un algoritmo di
ordinamento particolarmente appetibile.
(define insertion-sort
(lambda (lista)
(cond ((<= (length lista) 1) lista)
(else (let ((lista2 (insertion-sort (cdr
lista))))
(if (< (car lista) (car lista2))
(cons (car lista) lista2)
(cons (car lista2)
(insertion-sort
(cons (car lista)
(cdr lista2))))))))))
MERGE-SORT: codice finale
- Gia' vista: procedura merge che fonde due
liste gia' ordinate.
- gia' viste: procedura che creano la prima
meta' e la seconda meta'
di una lista data: sottolista_sx e sottolista_dx.
Algoritmo di merge-sort.
- Se la lista e' una lunga 0 o 1, e' ordinata.
- altrimenti, la dividiamo in due parti di
lunghezza (quasi) uguale,
che vengono prima ordinate ricorsivamente
richiamando il programma,
e poi fuse usando merge.
vengono comunque riportati tutti i codici per
comodita' di lettura:
(define merge
(lambda (L1 L2)
(cond ((or (null? L1) (null? L2)) (append
L1 L2))
((< (car L1) (car L2))
(cons (car L1) (merge (cdr L1)
L2)))
(else
(cons (car L2) (merge L1 (cdr
L2)))))))
(define sottolista_sx
(lambda (L i)
(if (= i 0)
()
(cons (car L) (sottolista_sx (cdr L) (- i
1))))))
(define sottolista_dx
(lambda (L i)
(if (= i 0)
L
(sottolista_dx (cdr L) (- i 1)))))
Ecco infine il programma merge-sort
(define merge-sort
(lambda (L)
(if (< (length L) 2)
L
(let ((pos_media (quotient (length L)
2)))
(merge (merge-sort (sottolista_sx L
pos_media))
(merge-sort
(sottolista_dx L pos_media)))))))
Costo computazionale di merge-sort (ossia:
numero di confronti
che vengono fatti):
qualche esempio, su liste lunghe 2^n.
Se la lista e' lunga 1 = 2^0, si fanno 0
confronti.
Altri casi
lunga 2: 1 confronto
lunga 4: 2 * 1 + 3 confronti = 5 confronti
lunga 8: 2 * 5 + 7 = 17
lunga 16: 2 * 17 + 15 = 49
Sia L lunga 2^n, per cui le sue meta' L1 e L2
sono lunghe
2^{n-1}.
Sia C(k) il numero di confronti fatti da
merge-sort
per ordinare una lista lunga k. Se k = 2^n,
C(k) sara'
il numero di confronti utilizzati da merge-sort
per ordinare L,
mentre C(k/2) e' il numero di confronti
utilizzati per ordinare
ciascuna delle due meta' L1 ed L2. Abbiamo
questa formula:
C(k) = 2 * C(k/2) + k - 1
giustificazione della formula: il numero di
confronti fatti per ordinare L, e' pari alla
somma di tutti in confronti fatti per ordinare,
prima,
le due sottoliste L1 ed L2 ( C(k/2) confronti
per lista,
quindi 2 * C(k/2) in tutto. A questa somma va
ancora aggiunto
il numero di confronti che la procedura merge
deve fare per
comporre correttamente la lista finale
ordinata, a partire
dalle due sottoliste L1 ed L2 ordinate.
Ora, siccome merge, ad ogni passo, sceglie
un elemento
(il piu' piccolo dei due car)
e non lo considera piu' per confronti
successivi, e' immediato
vedere che il numero di confronti che merge
utilizza e'
pari a k-1, se, complessivamente, ci sono k
elementi
da fondere (distribuiti sulle due liste).
Resta da capire come cresca la funzione
definita induttivamente
(sulle potenze di 2) dalla formula sopra:
C(k) = if k = 1 then 0 else 2 * C(k/2) + k -1.
La risposta e' che la crescita di C(k) e'
proporzionale
a k * log(k) [log = logaritmo in base 2].
La giustificazione verra' data in altri corsi.
=========
Riassunto: visti: bubble-sort, selection-sort,
insertion-sort, merge-sort,
(non visti: heap sort, quick-sort, counting-cort,
library sort.......).
bubble,selection,insertion - sort sono
quadratici nel numero di confronti, merge-sort
segue invece l'andamento (ottimale) k *
log(k).
===========
Alberi (BINARI).
Come le liste, sono strutture ricorsive. Ad
esempio "questo e' un albero binario di interi"
5
/ \
7
9
/\
/\
3 21 8
/
9
\
13
Definizione di alberi binari di numeri interi:
insieme T.
Definizione induttiva dell'insieme T:
T
=
{ () }
U
NxTxT
ossia:
un albero binario di interi e':
caso 1: un albero vuoto oppure
caso 2: una tripla costituita da:
- un numero
- un sottoalbero sinistro (che
DEVE essere un albero)
- un sottoalbero destro (idem
come sopra)
Alcune definizioni:
radice dell'albero (non vuoto): prima
componente della tripla
che lo rappresenta.
nodo di un albero: consiste dell'informazione
relativa
a una posizione nell'albero (da descrivere in
qualche modo)
e l'elemento ivi contenuto. Ad esempio
nell'albero sopra al numero
8 corrisponde un (unico) nodo, descritto dalla
posizione "scendi due
volte a destra a partire dalla radice", e dal
numero 8 stesso.
profondita' di un nodo: numero di archi che lo
collegano alla radice.
altezza di un albero: massimo fra tutte le
profondita' dei nodi.
foglia: nodo terminale (non ha sotto altri nodi).
Ad esempio:
5
T=
/ \
7
9
/\
/\
3 21 8
/
9
\
13
la radice e' 5, l'altezza dell'albero e' 3, il nodo
dove e' posizionato
il numero 1 ha profondita' 2.
T viene descritto come tripla nel modo
seguente
(resta inteso: la descrizione sotto e'
insoddisfacente:
e' per meta' formale, per meta' grafica)
7
T
)
= ( 5,
9
/\
/ \
2,
3
1
8
/
\
9
13
Osservazione: a loro volta i sottoalberi sx e dx
sono formalmente
delle triple
7
/\
3
(lft T) =
3 2
2)
/
= (7,
/
,
9
9
Attenzione alla tripla a destra: il "2" che
compare come terza
componente e' un albero, NON un numero!
(chiaramente
la scrittura sopra e' insoddisfacente!! Vedi il
seguito per pervenire
a una scrittura soddisfacente, utilizzando le
espressioni di Scheme).
sarebbe piu' preciso, pur usando questo
sistema di scrittura "misto",
scrivere esplicitamente gli alberi vuoti con cui
termina ogni
albero: questo eviterebbe ambiguita' di
scrittura. Quindi
7
/ \
3 2
/\ /\
9 () () ()
e' una scrittura piu' soddisfacente di
7
/ \
3 2
/
9
Ricordare, come regola aurea, questo punto:
ogni albero non vuoto e' una tripla, e in
particolare la seconda e la terza componente
sono
a loro volta SEMPRE alberi
proviamo a scrivere correttamente, usando le
triple, questo albero
binario
3
/
9
NON COSI' : (3, 9, () ) non va bene perche'
9 non e' un albero//
Ma se riempiamo la scrittura sopra con gli
impliciti alberi vuoti,
perverremo alla rappresentazione corretta.
3
/\
9 ()
/\
() ()
e quindi la tripla corretta e' questa:
(3,
).
radice
(9, (), ()),
()
sottoalbero_sx
sottoalbero_dx
Se un numero n lo vogliamo interpretare
come un albero, allora
viene descritto dalla tripla (n, (), () ).
Esercizio: usare rappresentazione degli alberi
che utilizza le liste.
Noi usiamo questa rappresentazione:
- un albero vuoto e' la lista vuota.
- un albero non vuoto e' una COPPIA la cui
prima componente
e' il numero che sta alla radice dell'albero e la
seconda componente
e' una COPPIA, il cui car e' il sottoalbero
sinistro, e il cui cdr e' il sottoalbero destro.
Esempio: rappresentare l'albero
7
/\
3 2
/
9
(cons 7 (cons (sottoalbero sx) (sottoalbero
dx))) =
(cons 7 (cons (cons 3 (cons (sottoalbero sx)
(sottoalbero dx)))
(cons 2 (cons () ()))
(cons 7 (cons (cons 3 (cons (cons 9 (cons ()
())) ()))
(cons 2 (cons () ())))
come si vede, la rappresentazione e' piuttosto
laboriosa
e per niente intuitiva. Si osservi tuttavia che
NON useremo,
di fatto, questo schema costruttivo per definire
alberi, in quanto
potremo fra poco sfruttare le funzioni del
protocollo degli alberi.
non e' detto che gli alberi debbano
necessariamente essere
rappresentati come sopra: potremmo ad
esempio usare
delle stringhe, oppure delle liste, a nostra
discrezione.
Rappresentazione alternativa con le liste
(cenno)
albero = lista vuota oppure lista di 3 elementi
costuita da
radice, sottoalbero sx e sottoalbero dx.
Se si sceglie questa rappresentazione,
l'albero sotto...
7
/\
3 2
/
9
....viene cosi' rappresentato.
(list 7 (list 3 (list 9 () ()) ()) (list 2 () ())).
Scegliamo ora, definitivamente, la
rappresentazione che impiega
la coppia la cui prima componente e' la
radice, e la seconda
componente la coppia sottoalbero_sx e
sottoalbero_dx, e implementiamo le funzioni
del protocollo.
una struttura dati (qualunque essa sia) si
caratterizza per questi
elementi base:
- i costruttori: come costruire gli oggetti della
struttura.
- i "metodi" che agli oggetti possono essere
applicati per utilizzarli
(vedi, in seguito, programmazione JAVA)
Sugli alberi binari (di interi) occorrono queste
operazioni:
COSTRUZIONE:
albero vuoto;
crescita dell'albero a partire da una radice, e
due sottoalberi
sinistro e destro.
(define make_empty_tree
(lambda ()
null)
(define grow_tree
(lambda (radice left_subtree right_subtree)
(cons radice (cons left_subtree
right_subtree))))
"DISTRUTTORI" dell'albero non vuoto
(operazioni utili per modificarli):
vogliamo essere in grado di
- estrarre la radice di un albero
root
- estrarre il sottoalbero sinistro
lft
- estrarre il sottoalbero destro
rgt
(define root car) <==== non e' una
definizione oziosa:
rende la funzione root del protocollo
indipendente dalla rappresentazione
(define lft
(lambda (t) ; albero non vuoto
(car (cdr t))))
(define rgt
(lambda (t)
(cdr (cdr t))))
ultima operazione richiesta
TEST: un albero e' vuoto?
(define empty_tree? null?)
================
Esercizio: scrivere un programma che, dato
un albero,
conta il numero dei suoi nodi.
(define conta_nodi
(lambda (t)
(if (empty_tree? t)
0
(+ (conta_nodi (lft t)) (conta_nodi (rgt
t)) 1))))
ESERCIZIO:
dato un albero binario di interi t, e un numero
n, stabilire se
n compare da qualche parte nell'albero
(belongs? t1 3) = #t
(belongs? t1 4) = #f
dove
(define t1
(grow_tree 1 (grow_tree 2 () ()) (grow_tree
3 () ())))
ESERCIZIO2: Scrivere un programma che
calcola l'altezza
di un albero (possibile partire da qua: albero
vuoto ha altezza -1)
(height (grow_tree 1 () ())) = 0