Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Algoritmi di Ordinamento: Analisi di Insertion, Selection, Bubble, Merge, Counting e Radix, Appunti di Algoritmi E Strutture Di Dati

Appunti del primo anno della triennale in informatica, relativi a: -Introduzione generale sugli algoritmi -Studio della complessità temporale (O grande, omega, theta) -Algoritmi di ordinamento (Insertion sort, selection sort, quick-sort, merge-sort, bubble-sort) -Teorema dell'esperto -Strutture dati: liste doppiamente e singolarmente concatenate, pile, code, implementazione mediante array, alberi binari di ricerca, visita preordine, postordine, inordine -Heap (Heapify, Build-heap, heap-sort)

Tipologia: Appunti

2021/2022

In vendita dal 26/07/2022

silvia-cambiago
silvia-cambiago 🇮🇹

4.7

(3)

8 documenti

1 / 31

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Algoritmi e Strutture Dati
1
Cambiago Silvia Anno Accademico 2021-2022 Prof. Alberto Dennunzio
ALGORITMO
Un algoritmo è una sequenza finita di istruzioni non ambigue che, eseguita a partire da un
insieme di dati, produce altri dati in un numero finito di passaggi.
I dati devono essere rappresentabili in modo finito, utilizzando quindi una quantità finita di
informazione. Non si possono quindi ricevere in input dati non rappresentabili con un numero
finito di cifre, come ad esempio π. Ciò però non va a limitare l’insieme dei possibili input, il
quale può essere anche costituito da un numero infinito di elementi (es , ). Non tutto
l’insieme ℝ è, invece, un input idoneo, in quanto contiene anche i numeri irrazionali.
I dati prodotti sono in relazione con i dati a partire dai quali l’esecuzione viene applicata
mediante una funzione (l’algoritmo deve essere quindi deterministico):
f: I ® O (I = input, O = output)
PROBLEMA COMPUTAZIONALE
Un problema computazionale prevede la presenza di un’istanza, ossia l’input necessario alla
computazione della soluzione, e di quest’ultima, quindi l’output. Il testo del problema descrive
in termini appartenenti al linguaggio comune la funzione che lega input e output.
Esempio:
Problema: Eseguire l’ordinamento di un array di n elementi interi.
Istanza: x
ℤ*
Soluzione:
𝑓(𝑥)&=&
(
()&&&&&&&𝑥&=&()
𝑦&&&&&&&&𝑥&&()&
() = sequenza vuota
# y deve essere una permutazione di x e yn+1 > yn
n
{1,…,n-1}
NOTAZIONI ASINTOTICHE
Sia g una funzione asintotica non negativa.
Il limite asintotico superiore, od O grande, è definito come:
𝑂(𝑔(𝑛))&=&{𝑓&|&∃&𝑐,𝑛!&>&0&|&∀𝑛>𝑛!&0&𝑓(𝑛)&𝑐&𝑔(𝑛)}
A partire da n0, c
g(n) è
sempre più grande di f(n).
g(n) è limite asintotico
superiore per f(n), il che
vuol dire che f(n) cresce al
più come g(n) a meno di un
fattore costante. La funzione
f(n) è detta O grande di
g(n).
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f

Anteprima parziale del testo

Scarica Algoritmi di Ordinamento: Analisi di Insertion, Selection, Bubble, Merge, Counting e Radix e più Appunti in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

Algoritmi e Strutture Dati

Cambiago Silvia Anno Accademico 2021- 2022 Prof. Alberto Dennunzio

ALGORITMO

Un algoritmo è una sequenza finita di istruzioni non ambigue che, eseguita a partire da un

insieme di dati, produce altri dati in un numero finito di passaggi.

I dati devono essere rappresentabili in modo finito, utilizzando quindi una quantità finita di

informazione. Non si possono quindi ricevere in input dati non rappresentabili con un numero

finito di cifre, come ad esempio π. Ciò però non va a limitare l’insieme dei possibili input, il

quale può essere anche costituito da un numero infinito di elementi (es ℕ, ℤ). Non tutto

l’insieme ℝ è, invece, un input idoneo, in quanto contiene anche i numeri irrazionali.

I dati prodotti sono in relazione con i dati a partire dai quali l’esecuzione viene applicata

mediante una funzione (l’algoritmo deve essere quindi deterministico):

f: I ® O (I = input, O = output)

PROBLEMA COMPUTAZIONALE

Un problema computazionale prevede la presenza di un’istanza, ossia l’input necessario alla

computazione della soluzione, e di quest’ultima, quindi l’output. Il testo del problema descrive

in termini appartenenti al linguaggio comune la funzione che lega input e output.

Esempio:

Problema: Eseguire l’ordinamento di un array di n elementi interi.

Istanza: x ∈ ℤ*

Soluzione: 𝑓(𝑥) = (

() = sequenza vuota

y deve essere una permutazione di x e y

n +

> y n

n ∈ {1,…, n - 1}

NOTAZIONI ASINTOTICHE

Sia g una funzione asintotica non negativa.

Il limite asintotico superiore, od O grande, è definito come:

!

!

A partire da n 0

, c ⋅ g(n) è

sempre più grande di f(n).

g(n) è limite asintotico

superiore per f(n), il che

vuol dire che f(n) cresce al

più come g(n) a meno di un

fattore costante. La funzione

f(n) è detta “O grande” di

g(n).

L’O grande è solitamente impiegato per descrivere la complessità del caso peggiore di un

algoritmo, prendendo l’ordine più grande di una funzione polinomiale ignorando le costanti

(trascurabili per n ® ∞).

Il limite asintotico inferiore, od omega, è definito come:

!

!

A partire da n 0

, c ⋅g(n) è

sempre più piccola di

f(n). g(n) è limite asintotico

inferiore per f(n), il che

vuol dire che f(n) cresce

almeno come g(n) a meno

di un fattore costante. La

funzione f(n) è detta

“omega” di g(n).

Questa notazione è utilizzata per descrivere la complessità del caso migliore di un algoritmo.

Il limite asintotico stretto, o theta, è definito come:

θ(𝑔(𝑛)) = {𝑓 | ∃ 𝑐 "

!

!

"

A partire da n 0

, f(n) è

sempre compresa tra

c 1

⋅g(n) e c 2

⋅g(n). g(n) è

limite asintotico stretto per

f(n), il che vuol dire che

f(n) cresce esattamente

come g(n) a meno di un

fattore costante. La

funzione f(n) è detta

“theta” di g(n).

Questa notazione è utilizzata per descrivere la complessità di tutti i casi di un algoritmo

compresi tra quello migliore e quello peggiore.

PSEUDOCODICE

Tipicamente, gli algoritmi vengono scritti in pseudocodice, linguaggio che esprime il

significato ed il senso dell’algoritmo rimanendo indipendente dalla macchina su cui dovrà

essere implementato, pur essendo sintatticamente simile ai principali linguaggi di

programmazione.

Nella scrittura dello pseudocodice è necessario rispettare determinate convenzioni:

Nel caso migliore, quello in cui k sia il primo elemento di A, la complessità è 𝜃( 1 ). Nel caso

peggiore, quando l’elemento chiave è l’ultimo della sequenza oppure non è presente, la

complessità è 𝜃(𝑛), in quanto n sono i confronti da effettuare.

Per ottenere la complessità nel caso medio, si somma il numero di confronti da eseguire caso

per caso:

e lo si divide per la lunghezza dell’array n , ne risulta:

La complessità nel caso medio è quindi 𝜃( n /2) = 𝜃( n ).

Una rappresentazione grafica della ricerca sequenziale è la seguente:

RICERCA DICOTOMICA

L’algoritmo di ricerca dicotomica è impiegato nella ricerca di un numero k all’interno di un

array ordinato V di n elementi. Il processo di ricerca è effettuato dividendo ripetutamente a

metà la sequenza. Se il numero cercato è minore dell’elemento in mezzo all’array, allora

l’operazione viene ripetuta valutando la prima metà, se è maggiore verrà considerata la

seconda.

Di seguito è mostrato lo pseudocodice della ricerca dicotomica:

Caso migliore

1 1 1 1 0 0 / 0 0 1 1 / 0

Caso peggiore

1

1

1

log

!

𝑛 + 1

log

!

𝑛

log

!

𝑛

/

log

!

𝑛

log

!

𝑛

1

0

/

1

Vengono memorizzati gli elementi iniziale e finale della sequenza che si sta analizzando e si

trova il centro ad ogni iterazione facendo la media tra essi.

Il caso migliore si ha quando l’elemento chiave si trova al centro e si effettua una sola

iterazione.

Il tempo di esecuzione è quindi:

= 1 + 1 + 1 + 1 + 1 + 1 = 6 = θ

Il caso peggiore si verifica invece quando l’elemento cercato non è presente nella sequenza.

La lunghezza dell’array si dimezza ad ogni iterazione. Detto x il numero di iterazioni, la

lunghezza dell’array rimanente è

$

!

. Nel caso peggiore tale valore è 1 , quindi:

%

Þ 𝑙𝑜𝑔

Þ 𝑥 = 𝑙𝑜𝑔

Il numero di iterazioni diventa quindi 𝑙𝑜𝑔

ed il tempo di esecuzione rimane:

  • 6 = θG𝑙𝑜𝑔

H

La complessità, di conseguenza, è θ(𝑙𝑜𝑔

Di seguito un esempio grafico del funzionamento:

Caso migliore

1

n

n – 1

" +𝑛 − 𝑖 + 1.

! & '

𝑖 # '

" +𝑛 − 𝑖.

! & '

𝑖 # '

0

n – 1

0

Caso peggiore

1

n

n – 1

" +𝑛 − 𝑖 + 1.

! & '

𝑖 # '

" +𝑛 − 𝑖.

! & '

𝑖 # '

" [𝑛 − ( 2 𝑖 − 1 )

! & '

% # '

]

n – 1

3(n – 1 )

%

𝑖 − 1 =

(

) *!

𝑛

( 𝑛 + 1

)

2

− 1 − (𝑛 − 1 ) =

𝑛

( 𝑛 + 1

)

2

− 𝑛

Il tempo totale di esecuzione del caso peggiore risulta:

𝑇(𝑛) = 1 + n + n − 1 + 𝑛 − 1 +

2

= θ

Nella figura seguente è mostrato un esempio grafico di implementazione dell’insertion-sort:

SELECTION-SORT

Un algoritmo di ordinamento alternativo all’insertion-sort è il selection-sort, che ordina l’array

cercando l’elemento minimo, piazzandolo all’inizio, e restringendo ad ogni iterazione la

porzione di array in cui esegue la ricerca e la selezione. Lo pseudocodice è il seguente:

Le sommatorie descritte da parte alla figura valgono:

" (𝑛 − 𝑖 + 1 ) = (𝑛 − 1 )𝑛 −

(𝑛 − 1 )𝑛

2

  • 𝑛 − 1

$ % &

' ( &

=

1

2

𝑛(𝑛 − 1 ) + 𝑛 − 1

" (𝑛 − 𝑖)

$ % &

' ( &

=

1

2

𝑛(𝑛 − 1 )

" [𝑛 −

( 2 𝑖 − 1

)

$ % &

' ( &

] = 𝑛(𝑛 − 1 ) − 2 ⋅

1

2

𝑛

( 𝑛 − 1

)

  • 𝑛 − 1 = 𝑛 − 1

Nel caso migliore, il tempo di esecuzione è:

= 1 + n + n − 1 +

= θ(𝑛

Nel caso peggiore risulta invece:

𝑇(𝑛) = 1 + n + n − 1 +

= θ(𝑛

Questo algoritmo non presenta differenze notevoli tra caso migliore e peggiore, in quanto non

è in grado di individuare un array già ordinato, ma esegue comunque tutti i cicli.

Un esempio grafico di funzionamento è:

BUBBLE-SORT

Il bubble-sort è un ulteriore algoritmo di ordinamento che, dato un vettore A di lunghezza n ,

scambia, ad ogni iterazione, ogni elemento con il successivo nel caso in cui non siano ordinati.

Lo pseudocodice è:

Ad ogni iterazione, l’algoritmo assicura che l’elemento massimo venga posizionato

correttamente: alla prima esecuzione pone l’elemento più grande nella n - esima posizione, alla

seconda, controllando fino alla posizione n - 1, si assicurerà che il massimo della sequenza

rimanente finisca in posto n - 1 e così via.

L’algoritmo non riconosce se il vettore sia già ordinato o meno, ma esegue ogni volta i cicli

indipendentemente dall’input, risultando quindi inefficiente nel caso migliore.

L’unica differenza tra caso migliore e peggiore sta nell’esecuzione dell’istruzione di scambio.

Le sommatorie descritte da parte alla figura valgono:

Caso migliore

1

n

" (𝑛 − 𝑖 + 1 )

! & '

% # '

" (𝑛 − 𝑖)

! & '

% # '

0

Caso peggiore

1

n

" (𝑛 − 𝑖 + 1 )

! & '

% # '

" (𝑛 − 𝑖)

! & '

% # '

3 *(precedente)

MERGE-SORT

Il merge-sort, dato un array A di n elementi, opera come di seguito al fine di ottenere una

sequenza ordinata:

Divide l’array di partenza in due sotto-array ognuno di lunghezza n /2;

Usando lo stesso algoritmo merge-sort, ordina i sotto-array ricorsivamente;

Combina i sotto-array ordinati per ottenere la soluzione al problema iniziale.

Il caso base è quello in cui la sequenza da ordinare ha dimensione 1 , e risulta quindi già

ordinata.

Lo pseudocodice è il seguente ( l è l’indice del primo argomento ed r quello dell’ultimo):

Lo pseudocodice della procedura merge è:

Caso base

1

0

0

0

0

Se l = r l’array A [ lr ] è già ordinato e l’algoritmo non deve eseguire alcuna istruzione.

Altrimenti:

Si divide A [ lr ] in A [ lm ] e A [ m +1… r ], dove m è la media tra l ed r ;

Si riapplica il merge-sort sui due array ottenuti;

Si combinano A [ lm ] e A [ m +1… r ].

Il merge-sort è un algoritmo stabile, ossia mantiene inalterato l’ordine relativo di elementi

uguali. Non è invece in loco, in quanto non utilizza un numero costante di variabili

indipendentemente dall’input (per la presenza dell’array di appoggio nel merge, di grandezza

direttamente proporzionale a quella dell’input).

Nel calcolo della complessità del merge-sort non si effettua distinzione tra caso peggiore,

medio e migliore in quanto indipendente da come si presenta l’array dato in input vengono

eseguite comunque le stesse istruzioni, le quali a loro volta non presentano differenza in termini

di theta tra caso migliore, medio e peggiore. Si distingue invece tra caso base e passo ricorsivo.

Nel caso base, infatti, l’algoritmo non esegue alcuna istruzione, ad eccezione del primo if per

accertarsi che la lunghezza sia 1. Il tempo di esecuzione T( n ) sarà quindi:

= 1 = θ

Nel passo ricorsivo vengono eseguite la prima istruzione di controllo e l’inizializzazione di m ;

viene poi eseguita una chiamata ricorsiva alla funzione di merge-sort sulla porzione di array

che va da 1 a M

&

N (tetto di

&

, ossia il più piccolo intero maggiore di

&

). In seguito ad essa viene

eseguita un’ulteriore chiamata sulla parte che rimane della sequenza, che ha dimensione O

&

P

(base di

&

, ossia il più grande intero minore di

&

). C’è poi la procedura merge, che ha

complessità θ

. Il tempo di calcolo totale T( n ) è quindi espresso in maniera ricorsiva:

𝑇(𝑛) = Q

θ

𝑇 RM

NS + 𝑇 RO

PS + θ

Questa è un’equazione di ricorrenza, ossia un’espressione che contiene se stessa ma con un

argomento più piccolo di quello iniziale. Volendo risolvere questa ricorrenza si può ricorrere

all’”albero delle chiamate”:

Assumo che n sia una potenza di 2;

Assumo che il costo dell’istruzione merge sia c ⋅ n.

Valutando il passo ricorsivo, il tempo di calcolo si può quindi riscrivere come:

= 𝑇 R

S + 𝑇 R

S + 𝑐 ⋅ 𝑛 = 2 𝑇 R

S + 𝑐 ⋅ 𝑛

Da qui, T(

$

) vale:

𝑇 R

S = 2 𝑇 R

S + 𝑐 ⋅

E T(

$

'

) corrisponde a:

𝑇 R

S = 2 𝑇 R

S + 𝑐 ⋅

Dato che n è una potenza di 2, la diramazione proseguirà fino ad ottenere T(

$

$

) = T(1), ma T(1)

è un valore noto, pari a 1. Arrivati a questo punto i T(1) da sommare saranno n.

Passo 2:

Ordina ricorsivamente V[ lq ] e V[ q +1… r ] eseguendo i tre passi sui due sottoarray.

Passo 3:

Combina i sottoarray.

Lo pseudocodice è il seguente:

Per quanto riguarda la procedura partition, ne esistono due: quella di Hoare e quella di Lomuto.

Quella di Hoare funziona come di seguito:

Sceglie il primo elemento dell’array o del sottoarray, quindi V[ l ], come pivot;

Scansiona V a partire dall’ultimo elemento e si ferma quando trova un elemento ≤

pivot;

Scansiona V a partire dal primo elemento e si ferma quando trova un elemento ≥ pivot;

Scambia i due elementi.

A livello di pseudocodice l’implementazione è la seguente:

dove il valore dx che viene restituito corrisponde a q nella procedura del quick-sort.

Il tempo di esecuzione del quick-sort su un vettore V di n elementi è determinato dalla seguente

equazione di ricorrenza:

𝑇(𝑛) = 𝑇(𝑞) + 𝑇(𝑛 − 𝑞) + θ(𝑛)

in quanto la procedura partition ha complessità θ(𝑛), alla quale vanno aggiunti il tempo di

esecuzione del quick-sort sul primo sottoarray di dimensione q ed il tempo di esecuzione del

secondo, di dimensione nq.

Nel caso base, ossia quando l’array ha lunghezza 1, l’unica istruzione da eseguire è il primo

controllo, quindi il tempo di esecuzione è:

T(𝑛) = 1 = θ( 1 )

Nel caso il vettore abbia lunghezza > 1, considero ora il caso peggiore, ossia degli input V tali

per cui il partizionamento, per ogni chiamata ricorsiva, sia sempre il più sbilanciato possibile,

avendo quindi un solo elemento da una parte e tutti i restanti dall’altra. Il valore q varrebbe

quindi 1 e l’equazione di ricorrenza assumerebbe la forma:

  • θ

Per trovare T( n - 1) si può sostituire tale espressione a T( n ), ottenendo quindi:

𝑇(𝑛 − 1 ) = 𝑇( 1 ) + 𝑇(𝑛 − 2 ) + θ(𝑛 − 1 )

quindi, unendola alla precedente, rimane:

𝑇(𝑛) = 𝑇( 1 ) + 𝑇( 1 ) + 𝑇(𝑛 − 2 ) + θ(𝑛 − 1 ) + θ(𝑛)

ma tale sostituzione si può applicare anche a T( n – 2), che diventa:

  • θ
  • θ
  • θ
  • θ

e così via, per n – 1 volte, fino ad arrivare a T( n – (n – 1 )), quindi a θ( 1 ).

Si arriva quindi ad un’equazione della forma:

𝑇(𝑛) = 𝑛 + θ ]^ 𝑖

$

0 2 #

` = 𝑛 + θ a

− 1 b = 𝑛 + θ(𝑛

) = θ(𝑛

Consideriamo ora il caso migliore, quindi la condizione in cui, per ogni chiamata ricorsiva,

l’array sia perfettamente bilanciato, quindi il partizionamento è tale che 𝑞 =

  1. 4

. L’equazione

di ricorrenza diventa dunque:

𝑇(𝑛) = 2 ⋅ 𝑇 R

S + θ(𝑛)

Questa equazione è analoga a quella del merge-sort, e si è dimostrato nel paragrafo dedicato

che ha complessità θ(𝑛 ⋅ 𝑙𝑜𝑔(𝑛)).

Nonostante la complessità sia analoga o maggiore di quella del merge-sort, il quick-sort ha

costanti (c 1

e c 2

) più piccole nel calcolo di θ rispetto a quelle del merge-sort, risultando quindi

più efficiente, a meno del caso peggiore. Inoltre, a differenza di quest’ultimo, il quick-sort è in

loco, in quanto la procedura partition necessita di un numero di variabili aggiuntive costante

ed indipendente dalla dimensione dell’input.

Si contano le occorrenze di ogni valore del range e si mette questo conteggio in un

vettore C ausiliario, in modo da sapere il numero di elementi che precedono ogni valore

del range nell’ordine (quindi il numero di elementi minori o uguali all’elemento stesso);

Si modifica C sommando, per ogni casella, quella corrente a quella precedente;

Si scorrono gli elementi del vettore di partenza A, a partire dall’ultima posizione,

inserendo ogni elemento nella posizione corretta in un altro array B di dimensione

analoga ad A in base alle informazioni ottenute nello step precedente, inserite in C.

Graficamente, il funzionamento del counting sort è:

RADIX SORT

Il radix sort è un algoritmo di ordinamento non basato sul confronto, ma:

Parte dall’analisi della cifra meno significativa e raggruppa gli elementi all’interno di

contenitori (bucket), in base al valore della cifra che si sta analizzando (bucket 0…9).

Procederà poi, nelle successive iterazioni, ad analizzare la seconda cifra meno

significativa e così via;

Gli elementi vengono inseriti all’interno del bucket in ordine di apparizione nell’array;

Ad ogni iterazione si ricostruisce l’array partendo dal bucket 0 ed inserendo ogni

elemento di ogni bucket nell’ordine in cui è stato inserito al passo precedente;

Effettua un numero di passaggi pari al numero di cifre dell’elemento maggiore;

Risulta vantaggioso quando l’array da ordinare è di grandi dimensioni.

Graficamente, il funzionamento del radix sort è:

PILE (STACK)

Una pila (stack) è un insieme dinamico, quindi variabile nel tempo, definito su un dominio D.

Può essere identificata come una sequenza S dove:

S = <> (sequenza vuota).

oppure

S = < a

"

$

0

∈ 𝐷, dove a n

è l’elemento in cima alla

pila S.

Questi insiemi dinamici vengono manipolati ed utilizzati dagli algoritmi. La manipolazione

avviene tramite operazioni dette di modifica (come inserimento e cancellazione), ma ne

esistono altre che non vanno a modificare l’insieme dinamico, nonostante siano anch’esse a

disposizione degli algoritmi; queste operazioni sono dette interrogazioni o query (come i vari

controlli). Le operazioni di inserimento e cancellazione effettuate su una pila S qualunque sono

tali che l’ultimo elemento inserito in S è il primo ad essere cancellato, secondo la politica LIFO.

OPERAZIONI

Sia 𝒮 l’insieme di tutte le possibili pile su un dominio D. Le operazioni su 𝒮 di cui un algoritmo

può disporre sono le seguenti:

Push (inserimento):

𝑃𝑈𝑆𝐻: 𝒮 × 𝐷 → 𝒮

∀(𝑆, 𝑥) ∈ 𝒮 × 𝐷

o Se S = <> 𝑃𝑈𝑆𝐻

o Se S = < a

"

$

= < a

"

$."

PUSH(𝑆, 𝑥) richiede in input una coppia: una pila ed un elemento del dominio.

Pop (cancellazione):

𝑃𝑂𝑃: 𝒮 {<>} → 𝒮 × 𝐷

∀ 𝑆 ∈ 𝒮 con 𝑆 ≠ <>

o 𝑃𝑂𝑃(𝑆) = G< 𝑎

"

($,")

$

H

Quindi POP(𝑆) è la pila ottenuta da 𝑆 rimuovendo a n

Essendo l’output di POP(𝑆) un elemento di 𝒮 × 𝐷, dovrà essere una coppia.

Stack-empty (interrogazione utilizzata per verificare se la pila considerata è vuota):

o 𝑆𝑇𝐴𝐶𝐾 − 𝐸𝑀𝑃𝑇𝑌(𝑆) = (

𝑡𝑟𝑢𝑒 S = <>

Top (interrogazione che restituisce l’elemento in cima alla pila):

∀ 𝑆 ∈ 𝒮 con 𝑆 ≠ <>

o 𝑇𝑂𝑃(𝑆) = 𝑎

$

o 𝐸𝑀𝑃𝑇𝑌 − 𝑄𝑈𝐸𝑈𝐸

𝑡𝑟𝑢𝑒 Q = <>

Graficamente, le operazioni ENQUEUE() e DEQUEUE() funzionano come di seguito:

IMPLEMENTAZIONE DI PILE MEDIANTE ARRAY

Sia data una pila S = < a

"

$

>, con al più m elementi ( nm ).

Si utilizza A[1… m ] array con m = A.length;

A.top è l’indice dell’ultimo elemento inserito di S;

A[1…top] memorizza S;

A[1] è l’elemento in fondo; A[A.top] è l’elemento in cima;

Se A.top = 0, allora S = <>.

IMPLEMENTAZIONE DI CODE MEDIANTE ARRAY

Sia data una coda Q = < a

"

$

>, con al più m - 1 elementi ( nm - 1 ).

Si utilizza A[1… m ] array con m = A.length;

A.head è l’indice del primo elemento inserito in Q;

A.tail è l’indice del prossimo elemento che verrà aggiunto in Q;

A[A.head…A.tail- 1 ] memorizza Q (può essere che A.tail- 1 ≤ A.head);

A[A.head] è l’elemento in testa;

A[A.tail-1] è l’elemento in coda;

La posizione 1 segue la posizione m secondo un ordine circolare:

Se A.head = A.tail, allora Q = <>;

Se A.head = 1 e A.tail = A.length oppure A.head = A.tail +

allora la coda è piena.

Quindi, dato un array Q[1…12] come quello

in figura, riempito dalla posizione 7 alla 11,

Q.head vale 7, mentre Q.tail, l’indice del

prossimo elemento che verrà aggiunto, vale

  1. A mano a mano che la coda viene riempita,

se Q.tail raggiunge l’ultima casella dell’array,

torna in posizione 1 , continuando l’incremento

da lì, in maniera circolare. La coda è piena nel

momento in cui Q.tail+1 coincide con Q.head.

Nel terzo passaggio è stata applicata la

procedura DEQUEUE(Q), portando Q.head

ad incrementarsi e l’elemento in posizione 7

(ossia 15) ad essere rimosso dalla coda Q.

LIMITI INFERIORI PER L’ORDINAMENTO

Si stabilisce un limite inferiore Ω

per gli algoritmi di ordinamento basati su

confronti del tipo:

a i

≤ a j

a i

< a j

a i

≥ a j

a i

> a j

Poiché inoltre si suppone che tutti gli elementi in input siano distinti, saranno sufficienti test

soltanto del tipo a i

≤ a j

(o, analogamente, a i

< a j

Un albero di decisione è un albero binario pieno che rappresenta i confronti fra elementi che

vengono effettuati da un determinato algoritmo di ordinamento che opera su un input di una

data dimensione.

Ogni nodo interno è rappresentato da un’espressione i : j, con 1 ≤ i, j ≤ n (test a

i

≤ a j

Ogni foglia è rappresentata da una permutazione < π( 1 ), … , π(𝑛) > dell’input.

Dimostrazione:

Dato n ∈ N

, l’albero di decisione T n

corrisponde ad un dato algoritmo 𝒜 di ordinamento ed

alla dimensione n si avranno n! foglie.

Poiché un albero binario di altezza K ha al massimo 2

K

foglie, posto h = height(T n

𝒜), si ha:

7

≥ 𝑛! ⟹ ℎ ≥ 𝑙𝑜𝑔(𝑛!) = ΩG𝑛 ⋅ 𝑙𝑜𝑔(𝑛)H

Infatti:

𝑙𝑜𝑔(𝑛!) = ^ 𝑙𝑜𝑔(𝑖) ≥ ^ 𝑙𝑜𝑔(𝑖)

$

0 2 8

$

9

$

0 2 "

^ 𝑙𝑜𝑔 R

S

$

0 2 8

$

9

= R𝑛 − M

N + 1 S ⋅ 𝑙𝑜𝑔 R

S ≥

⋅ 𝑙𝑜𝑔 R

S =

= ΩG𝑛 ⋅ 𝑙𝑜𝑔(𝑛)H

Corollario: heap-sort e merge-sort sono algoritmi di ordinamento asintoticamente uguali.