strutture dati in c - oil.di.univaq.it · strutture dati in c dispense del corso di laboratorio di...

34
Strutture dati in C dispense del corso di Laboratorio di Algoritmi e Strutture Dati A.A. 2001/2002 prima parte (versione molto ma molto draft) Gianfranco Ciaschetti 1 27 maggio 2002 1 Dipartimento di Matematica Pura e Applicata, Universitμ a degli Studi di L'Aquila, via Vetoio, Coppito, I-67010 L'Aquila; e-mail: [email protected]

Upload: vuhanh

Post on 15-Feb-2019

217 views

Category:

Documents


0 download

TRANSCRIPT

Strutture dati in C

dispense del corso diLaboratorio di Algoritmi e Strutture Dati

A.A. 2001/2002

prima parte(versione molto ma molto draft)

Gianfranco Ciaschetti 1

27 maggio 2002

1Dipartimento di Matematica Pura e Applicata, Universitµa degli Studi diL'Aquila, via Vetoio, Coppito, I-67010 L'Aquila; e-mail: [email protected]

Indice

1 Insiemi e oggetti 2

2 Liste 62.1 Inserimento . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72.2 Ricerca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92.3 Cancellazione . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

3 Code e Pile 133.1 Implementazione con array . . . . . . . . . . . . . . . . . . . . 13

3.1.1 Pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153.1.2 Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

3.2 Implementazione con liste . . . . . . . . . . . . . . . . . . . . 183.2.1 Pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183.2.2 Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

4 Alberi 224.1 Visita di un albero in profonditµa . . . . . . . . . . . . . . . . 244.2 Visita di un albero in ampiezza . . . . . . . . . . . . . . . . . 274.3 Inserimento e cancellazione . . . . . . . . . . . . . . . . . . . . 30

1

Capitolo 1

Insiemi e oggetti

Nella progettazione di algoritmi spesso si ha bisogno di rappresentare oggettie insiemi di oggetti. Ogni oggetto µe descritto all'interno di un calcolatoreda un set di informazioni che ne rappresentano le proprietµa e/o le caratter-istiche. Possiamo allora pensare a un oggetto come a una generica porzionedi memoria in cui le sue informazioni sono archiviate. A seconda del numeroe del tipo di informazioni che intendiamo associare a ogni oggetto, esso puµooccupare piµu o meno spazio in memoria. L'oggetto x puµo essere indicatotramite il contenuto o l'indirizzo delle locazioni di memoria in cui le sue in-formazioni sono memorizzate. In C, nel primo caso l'oggetto µe rappresentatoda una variabile, nel secondo caso da un puntatore.

Il linguaggio C mette a disposizione un certo numero di tipi prede¯nitiper la rappresentazione di oggetti con una singola informazione (interi, reali,caratteri, ecc.), oltre a un tipo array per l'aggregazione di oggetti dello stessotipo e un tipo struct per l'aggregazione di oggetti di tipo diverso. Sia gli arrayche le strutture hanno l'e®etto di de¯nire locazioni di memoria contigue nellequali gli oggetti sono memorizzati.

Ad esempio, se vogliamo de¯nire un oggetto di tipo intero basta dichiarareuna variabile di tipo intero o puntatore a intero, come

int o;

int *po;

oppure, se vogliamo de¯nire un insieme omogeneo di 10 interi, dichiariamouna variabile

int A[10];

2

o ancora, per realizzare una struttura con un intero e un carattere,

struct ascii

{

int code;

char c;

}

I nomi o, po, A e ascii sono nomi di variabili, e come tali possono esserede¯niti come stringhe arbitrarie.

Array e strutture sono collezioni statiche di oggetti, in quanto non perme-ttono di inserire o cancellare altri oggetti oltre quelli speci¯cati all'atto delladichiarazione (o dell'allocazione, se un array µe de¯nito come un puntatore).

Tuttavia molto spesso si richiede di disporre di una struttura dati cherappresenti un insieme dinamico, il cui numero di elementi puµo variare neltempo. Per incrementare il numero n di elementi di un'array, una volta cheesso µe allocato in memoria, occorre allocare una nuova porzione di memoriaper n + 1 oggetti e copiare l'intero contenuto dell'array precedente nel nuo-vo. Ad esempio, per poter aggiungere un elemento all'array A dell'esempioprecedente, si puµo , de¯nita una funzione Reallocate che prende in ingressoun array di interi (tramite due parametri, il nome dell'array e la sua dimen-sione) e il numero di elementi da aggiungere, e®ettuare la seguente chiamatadi funzione:

int* Reallocate(int *p, int cur_dim, int grow_factor)

{

int *A_primo = (int*)malloc((cur_dim + grow_factor)*sizeof(int));

for (int i=0; i<cur_dim; i++)

A_primo[i] = A[i];

return A_primo;

}

A = Reallocate(A, 10, 1);

Gli insiemi dinamici di oggetti hanno diverse caratteristiche a seconda deltipo di operazioni che su di esso si intendono fare. Un tipo di dato astratto(TDA) rappresenta un insieme dinamico e il set di operazioni che su di essosi intendono eseguire. Le operazioni tipiche sono di interrogazione (ricercadi un oggetto, numero di oggetti presenti, ecc.) o di modi¯ca dell'insieme

(inserimento, cancellazione, modi¯ca di un oggetto, ecc.). Il mantenimen-to in memoria di un TDA per la rappresentazione di insiemi dinamici puµorichiedere che in un oggetto siano presenti, oltre alle informazioni che lo iden-ti¯cano, un certo numero di informazioni aggiuntive che permettono la suaaggregazione nell'insieme. Ad esempio, la struttura

struct nodo

{

int info;

struct nodo *next;

}

puµo essere usata per realizzare una lista lineare di oggetti, ognuno collegatoal proprio successore nella lista. Le liste saranno presentate nel x??.

La scelta del TDA da utilizzare per rappresentare insiemi dinamici dipendeprincipalmente dal tipo di operazione che si intende fare sull'insieme, e hain°uenza sull'e±cienza di un eventuale algoritmo che deve usare tale in-sieme di informazioni. Ad esempio, la ricerca di un elemento speci¯co in unalista richiede che tutti gli elementi vengano esaminati linearmente, e perciµorichiede un tempo computazionale pari a O(n). Se l'operazione di ricercadi un elemento µe ripetuta molte volte in un algoritmo, si potrebbe sceglieredi utilizzare strutture (TDA) piµu e±cienti per questa operazione, come adesempio alberi bilanciati. In questo contesto, a meno che non sia esplicita-mente detto il contrario, utilizziamo un'analisi asintotica del caso peggioreper determinare l'e±cienza di un'operazione in una struttura dati.

Riprendendo l'esempio precedente, se anzich¶e un solo intero volessimomemorizzare per ogni oggetto della lista tutte le informazioni relative a unimpiegato, potremmo dichiarare un oggetto del tipo:

struct object

{

int matricola;

char nome[20];

char cognome[20];

char sesso;

long stipendio;

struct object *next;

};

Alternativamente, per mantenere la struttura lista collegata piµu leggera,possiamo memorizzare in essa solo il puntatore all'oggetto anzich¶e l'oggettostesso. In questo caso, dovremmo de¯nire l'oggetto impiegato con le suesole informazioni, cio¶e senza il campo next, e dichiarare un nuovo oggettoper la nostra lista

struct my_object

{

struct object* o;

struct my_object *next;

};

Le liste che vengono realizzate con le due diverse dichiarazioni sono rap-presentate in ¯gura 1.1.

nomecognome

sessostipendio

nomecognome

sessostipendio

nomecognome

sessostipendio

nomecognome

sessostipendio

nomecognome

sessostipendio

nomecognome

sessostipendio

nomecognome

sessostipendio

nomecognome

sessostipendio

Figura 1.1: Una lista di oggetti di tipo impiegato una di puntatori a oggetti ditipo impiegato

Capitolo 2

Liste

Una lista (o lista collegata o lista lineare) µe una struttura dati in cui glioggetti sono organizzati in un ordine lineare. Ogni oggetto della lista contieneinformazioni proprie piµu un puntatore all'oggetto successivo.

A di®erenza degli array, la dimensione di una lista non µe nota a priori(negli array, ricordiamo, la dimensione µe speci¯cata all'atto della dichiarazioneo dell'allocazione esplicita di memoria), ma varia nel tempo man mano chegli oggetti sono inseriti o cancellati dalla lista.

Una lista µe univocamente determinata mediante una variabile di tipopuntatore che contiene l'indirizzo del primo oggetto della lista. Gli altrioggetti possono essere individuati scorrendo la lista mediante i puntatori aglielementi successivi. In quanto segue, supponiamo senza perdita di generalitµache ogni oggetto contenga una sola informazione di tipo intero, come nelladichiarazione che segue:

struct elem

{

int info;

struct elem *next;

}

Per il TDA lista lineare, de¯niamo le seguenti operazioni:

² inserimento

² cancellazione

² ricerca

6

Ognuna di queste operazioni µe implementata in modo diverso a secondache la lista sia mantenuta ordinata (rispetto al campo info oppure no.

2.1 Inserimento

Se la lista non µe ordinata, scegliamo di inserire un elemento in testa alla lista,in modo da minimizzare il numero di operazioni elementari da compiere.Siano de¯nite, oltre all'oggetto elem, le seguenti variabili:

elem *piniz; /* puntatore al primo oggetto della lista */

elem *p; /* puntatore a un oggetto generico */

15 10 7 21

12

piniz

15 10 7 21piniz

12

p

Figura 2.1: Inserimento in testa a una lista lineare

Tutto quello che occorre fare, una volta creato il nuovo elemento da in-serire, e allocata memoria per esso, µe aggiornare opportunamente i puntatoricome mostrato in ¯gura 2.2, cio¶e eseguire le seguenti istruzioni:

/* alloca memoria per il nuovo oggetto e inserisci dati di informazione */

p = (elem*) malloc (sizeof(elem));

p->info = 12;

/* aggiorna puntatori */

p->next = piniz;

piniz = p;

Se invece la lista µe ordinata, allora l'inserimento richiede che il nuovooggetto venga collocato nella posizione opportuna nella lista. Senza perdi-ta di generalitµa , supponiamo che la lista non possa presentare ripetizioni(elementi con stessa informazione).

3 10 13 21

12

piniz

3 10 13 21piniz

12

r q

p

Figura 2.2: Inserimento in una lista lineare ordinata

Bisogna prima trovare il punto di inserimento del nuovo oggetto (imme-diatamente prima dell'elemento che contiene l'informazione maggiore dellasua) e poi e®ettuare la modi¯ca dei puntatori.

/* alloca memoria per il nuovo oggetto e inserisci dati di informazione */

p = (elem*) malloc (sizeof(elem));

p->info = 12;

/* trova il punto di inserimento */

struct elem *q = piniz, *r = piniz;

while (q->info < p->info)

{

r = q;

q = q->next;

}

/* aggiorna puntatori */

/* r e q sono il predecessore e il successore del nuovo elemento p*/

r->next = p;

p->next = q;

Si noti che abbiamo dovuto utilizzare due variabili di tipo puntatoreperch¶e altrimenti, una volta identi¯cato l'elemento q, non abbiamo mododi recuperare la locazione di memoria dove scrivere il puntatore all'elementoda inserire. Questo problema non si presenta se al posto di liste lineari uti-lizziamo liste doppie, contenenti sia il puntatore all'oggetto successivo sia ilpuntatore all'oggetto precedente. In questo caso, la struttura dell'oggettodiventa la seguente:

struct elem

{

int info;

struct elem *next;

struct elem *prev;

}

e le istruzioni per l'inserimento in una lista ordinata diventano le seguenti (siveda la ¯gura 2.3):

struct elem *q = piniz;

/* trova il punto di inserimento */

while (q->info < p->info)

q = q->next;

/* aggiorna puntatori */

q->prev->next = p;

p->prev = q->prev->next;

p->next = q;

q->prev = p;

A partire da una lista doppia, si possono de¯nire liste circolari facendopuntare il predecessore del primo elemento all'ultimo elemento della lista, eil successore dell'ultimo al primo.

2.2 Ricerca

La ricerca di un elemento in una lista viene fatta scandendo tutti gli elementidella lista ¯no a trovare quello che contiene l'informazione interessata, seesiste, o ¯no alla ¯ne della lista. Nel codice che segue, supponiamo ancora chep sia il puntatore all'elemento da inserire, e piniz il puntatore all'elementoiniziale della lista.

12

piniz

piniz

3 10 13 21

3 10 13 21

12

qq->prev

p

Figura 2.3: Inserimento in una lista doppia ordinata

struct elem* Ricerca(int k)

{

struct elem *q = piniz;

while (q != NULL)

if (q->info != k)

q = q->next;

else

break;

return q;

La procedura di ricerca restituisce un puntatore all'elemento cercato, se es-so µe presente nella lista, altrimenti il puntatore nullo NULL. Qui descrivi-amo la procedura generale nel caso delle liste lineari, lasciando per eserciziol'implementazione dell'operazione di cancellazione nel caso delle liste doppieo circolari.

2.3 Cancellazione

La cancellazione di un elemento, come per l'inserimento in una lista ordi-nata, prevede due fasi: prima si deve identi¯care l'elemento da cancellare,e poi modi¯care opportunamente i puntatori (ed eventualmente cancellarel'elemento dalla memoria, se non serve piµu ). Il procedimento µe illustrato inFugura 2.4

void Delete(int k)

{

/* trova l'elemento da cancellare */

struct elem *q = piniz, *r = piniz;

while (q->info != p->info)

{

r = q;

q = q->next;

}

/* aggiorna puntatori */

r->next = q;

/* cancella oggetto dalla memoria */

free p;

3 10 13 21piniz

3 10 13 21piniz

r q

p

Figura 2.4: Cancellazione di un elemento da una lista lineare

La procedura di cancellazione descritta non considera il caso in cui l'elementoda cancellare non µe presente nella lista. Si lascia allo studente per esercizioil compito di descrivere la procedura completa.

Solitamente, si usa inserire all'inizio della lista un oggetto ¯ttizio (dum-my) per evitare di eseguire i controlli necessari per i casi particolari incui l'elemento da cancellare si trovi all'inizio o alla ¯ne della lista. Tut-tavia, la presenza di un oggetto dummy non riduce la complessitµa asintotivadell'operazione.

A titolo di esempio, presentiamo il codice per la costruzione di una listalineare di 10 elementi.

#include<stdio.h>

#include<stdlib.h>

struct elem {

int info;

struct elem *next;

}

void main()

{

struct elem *p, *piniz;

int k, i;

for(i=0; i<10; i++)

{

printf("\n%d -esimo elemento", i);

scanf("%d", &k);

p = (struct elem *)malloc(sizeof(elem));

p->info = k;

p->next = piniz;

piniz = p;

}

}

Capitolo 3

Code e Pile

Code e pile sono particolari struture dati astratte che permettono di inserireed estrarre elementi solo in determinate posizioni. In particolare, una codaµe gestita in modo FIFO (¯rst-in-¯rst-out), mentre una pila µe getita in modoLIFO (last-in-¯rst-out).

Le operazioni consentite in queste strutture sono le seguenti:

push inserimento

pop estrazione

Per le code l'inserimento avviene in coda alla struttura, e l'estrazione in testa,mentre per le pile si inserisce e si estrae sempre in testa alla struttura.

Code e pile possono essere realizzate sia tramite array che tramite liste.Discuteremo l'implementazione delle funzioni push e pop in entrambi i casi.

3.1 Implementazione con array

Se pile e code vengono implementate tramite array, occorre che il numero dielementi presenti nella pila o nella coda sia sempre inferiore alla dimensionedell'array, a meno di riallocazioni di memoria. Come mostrato in ¯gura 3.2,occorre mantenere due indici head e tail per rappresentare la coda con unarray, mentre basta solo l'indice head per la pila.

13

push poppop

push

LIFO FIFO

Figura 3.1: Pile e Code

top

head

tail

pile code

134811

1

411

357

Figura 3.2: Implementazione di pile e code tramite array

3.1.1 Pile

Per descrivere le operazioni di inserimento e cancellazione in una pila, sup-poniamo di costruirire una pila utilizzando un array A con 100 posizioni, e dichiamare top l'indice di testa della pila. Vediamo di seguito le istruzioni Cche creano l'array, allocano memoria per esso, inizializzano l'indicett top, e realizzano le funzioni push e pop.

int A[100];

int top;

for(int k=0; k<100; k++)

A[k] = 0;

top = -1;

void push(int i)

{

if (top < 99)

A[++top] = i;

else

printf("errore: pila piena");

}

int pop()

{

if (top > 0)

return A[top--];

else

printf("errore: pila vuota");

}

Si puµo osservare che sia l'operazione di inserimento che quella di cancellazioneaggiornano l'indice top al valore precedente o successivo. Se si vuole scandirel'intera pila, basta e®etture un ciclo for partendo da 0 ¯no al valore di top.Se la lista µe vuota, nessuna istruzione interna al ciclo viene eseguita.

for(int k=0; k<top; k++)

...

3.1.2 Code

Anche per una coda, l'inserimento di un elemento comporta l'aggiornamentodegli indici. In particolare, un inserimento aggiorna l'indice di testa, men-tre l'estrazione aggiorna l'indice di coda. Per descrivere queste oeprazioni,supponiamo di costruire una coda utilizzando un array A con 100 posizioni.Vediamo di seguito le istruzioni C che creano l'array, allocano memoria peresso, inizializzano gli indici head e tail, e realizzano le funzioni push e pop.

int A[100];

int tail, head;

for(int k=0; k<100; k++)

A[k] = 0;

head = -1;

tail = -1;

void push(int i)

{

if (tail < 99)

A[++tail] = i;

}

int pop()

{

if (head > 0)

return A[--head];

}

Se si vuole scandire una coda, basta partire da uno dei due indici (adesempio quello di testa) e raggiungere l'altro in un ciclo while, come nellaseguente istruzione:

while(head < tail)

{

int p = head;

...

p++;

}

Un modo pratico di realizzare liste µe tramite array circolari. In un arraycircolare, la coda della struttura coda rappresenta il primo elemento liberodell'array, secondo l'ordine circolare, come mostrato in ¯gura 3.3.

1 5 3 8

11

4

0

1

2

4 5 6

7

8

9

1011

head = 3 3

tail = 9

Figura 3.3: Implementazione di code tramite array circolari

In questo caso, supponendo che l'array A ha n posizioni, le operazionipush e pop possono essere codi¯cate come segue:

head = 0;

tail = 0;

void push(int i)

{

if (head == (tail+1)%n)

printf("errore: coda piena");

else

{

A[tail] = i;

tail = (tail+1)%n;

}

}

int pop()

{

if (head == tail)

printf("errore: coda vuota");

else

{

int k = A[head];

head = (head+1)%n;

return head;

}

}

3.2 Implementazione con liste

Se invece che interi volessimo realizzare una pila o una coda di oggetti genericicon piµu di un'informazione, ci potrebbe essere utile realizzare pile e code condelle liste collegate. Ogni elemento della lista contiene tutte le informazionirelative all'oggetto, e un puntatore all'oggetto successivo. Senza perdita digeneralitµa , supponiamo per ora che ci sia solo un'informazione di tipo interoassociata a ogni oggetto.

3.2.1 Pile

Una pila realizzata tramite una lista ha il puntatore top corrispondente alpuntatore iniziale della lista, e le operazioni di inserimento e cancellazionevengono e®ettuate solo in testa alla lista.

struct elem {

int info;

struct elem *next;

}

struct elem *top; /* come piniz */

void create()

{

top = NULL;

}

void push(struct elem *p)

{

p->next = top;

top = p;

}

struct elem *pop()

{

if (top != NULL)

{

struct elem *p = top;

top = top->next;

return p;

else

printf("errore: pila vuota");

}

Si noti che nella implementazione di pile tramite liste non occorre e®etuareil controllo di pila piena, in quanto usiamo una struttura dati dinamica.

3.2.2 Code

Una coda realizzata tramite una lista ha il puntatore head corrispondenteal puntatore iniziale della lista, mentre il puntatore tail punta all'ultimoelemento della lista.

struct elem {

int info;

struct elem *next;

}

struct elem *head, *tail;

void create()

{

head = NULL;

tail = NULL;

}

void push(struct elem *p)

{

if (head == NULL)

head = p;

else

tail->next = p;

tail = p;

}

struct elem *pop()

{

if (head == NULL)

printf("errore: coda vuota");

else

{

struct elem *p = head;

head = head->next;

return p;

}

}

Come nell'implementazione tramite array, µe possibile de¯nire liste circo-lari come quella rappresentata in ¯gura 3.4, dove si illustra l'inserimento diun elemento.

In questo caso, le inizializzazioni sono le stesse che per le liste lineari,mentre le operazioni push e pop vanno riscritte come segue:

void push(struct elem *p)

{

if (head == NULL)

head = p;

else

{

p->next = head;

tail->next = p;

tail = p;

}

}

15 10 7 21

tail head

8

Figura 3.4: Inserimento di un elemento in una coda implementata con listacircolare

struct elem *pop()

{

if (head == NULL)

printf("errore: coda vuota");

else

{

struct elem *p = head;

head = head->next;

tail->next = head;

return p;

}

}

Capitolo 4

Alberi

Un albero rappresenta la generalizzazione di una lista lineare, in cui un el-emento puµo avere piµu di un successore. Ogni elemento si chiama nodo edispone dei puntatori ai suoi successori, ed eventualmente al predecessore.Seguendo una terminologia genealocica, il predecessore del nodo in un alberosi chiama padre, i successori ¯gli, e cosµ³ via. Il nodo che non ha predecessori µedetto la radice dell'albero, mentre quelli che non hanno successori sono dettifoglie. In un albero ogni elemento ha un solo predecessore.

Un albero µe detto n-ario se ogni elemento ha al piµu n ¯gli. In fugura 4.1sono mostrati un esempio di albero binario (4.1-a) e uno n-ario generico (4.1-b). Un albero µe identi¯cato per mezzo della propria radice, ossia il puntatoreall'oggetto che µe in tale posizione.Se l'albero µe binario, allora basta usare due variabili di tipo puntatore permemorizzare i ¯gli di ogni nodo. Supponendo ancora, senza perdita digeneralitµa , di memorizzare una sola informazione per nodo, descriviamol'implementazione del tipo nodo.

struct nodo {

int info;

struct nodo *left;

struct nodo *right;

struct nodo *parent;

}

Per ogni nodo, il puntatore left punta al suo ¯glio sinistro (eventual-mente NULL se si tratta di una foglia), right al suo ¯glio destro e parent

al genitore. L'informazione sul genitore non µe sempre necessaria.

22

(a)

(b)

Figura 4.1: Alberi binari e n-ari

Se invece l'albero puµo avere piµu successori per ogni nodo, e il numero µevariabile, dobbiamo memorizzare i puntatori ai ¯gli in un array e speci¯carnele dimensioni. La dichiarazione del tipo nodo sarµa allora del tipo:

struct nodo {

int info;

struct nodo **sons;

int numsons;

}

Si potrµa in questo caso indicare l'i-esimo ¯glio del nodo puntato da p conl'espressione p->sons[i-1].

Si de¯nisce altezza di un nodo x dell'albero il numero massimo di archi(puntatori) che occorre percorrere, a partire da x, per raggiunge una suafoglia. Si de¯nisce inoltre altezza dell'albero l'altezza della sua radice.

In generale, gli alberi sono usati perch¶e , a di®erenza delle liste, per-mettono di avere piµu successori per ogni elemento, ma anche perch¶e , seorganizzati opportunamente, danno luogo a operazioni piµu e±cienti. In par-ticolare, un albero bilanciato di n elementi ha altezza log2n e si vedrµa comeµe possibile in alberi bilanciati realizzare operazioni di ricerca, inserimento ecancellazione in un tempo proporzionale all'altezza dell'albero, cio¶e in O(n).

Se l'albero non gode di particolari proprietµa , occorre in genere visitarel'intero albero per cercare un dato elemento. Esistono due modi diversi pervisitare un albero:

² visita in profonditµa

² visita in ampiezza

4.1 Visita di un albero in profonditµa

La visita di un albero in profonditµa prevede che, dato un nodo corrente x,si visiti successivamente il proprio sottoalbero sinistro (l'albero che ha comeradice il ¯glio sinistro di x, x->left) e poi il proprio sottoalbero destro(l'albero che ha come radice il ¯glio destro di x, x->right). Nel caso dialberi n-ari, si visitano nell ordine i ¯gli del nodo corrente (x), x->sons[0],x->sons[1], ..., x->sons[numsons-1].

Questa strategia si puµo applicare ricorsivamente, a partire dalla radice,per tutti i nodi dell'albero. Mostriamo di seguito il codice per ricerca di unelemento di un albero binario e n-ario mediante visita in profonditµa . Percompletezza, presentiamo sia la versione ricorsiva che quella iterativa.

struct b_nodo {

int info;

struct b_nodo *left;

struct b_nodo *right;

}

struct n_nodo {

int info;

struct n_nodo *left;

struct n_nodo **sons;

int numsons;

}

/* creazione degli alberi */

...

struct b_nodo *b_root;

struct n_nodo *n_root;

...

struct b_nodo* BRecDepthFirstSearch(int key)

{

struct nodo *p = b_root;

if (p->info == key)

return p;

if (p->left != NULL)

DepthFirstSearch(p->left);

if (p->right != NULL)

DepthFirstSearch(p->right);

}

struct n_nodo* NRecDepthFirstSearch(int key)

{

int i;struct nodo *p = b_root;

if (p->left != NULL)

for (i=0; i<p->numsons; i++)

DepthFirstSearch(p->sons[i]);

if (p->info == key)

return p;

}

struct b_nodo* BDepthFirstSearch(int key)

{

char found = 0;

struct nodo *p = b_root;

if ((p->info == key) && (found != 0))

{

found = 1;

return p;

}

while ((p->left != NULL) && (found != 0))

{

p = p->left;

if (p->info == key)

{

found = 1;

return p;

}

}

while ((p->right != NULL) && (found != 0))

{

p = p->right;

if (p->info == key)

{

found = 1;

return p;

}

}

}

struct b_nodo* NDepthFirstSearch(int key)

{

char found = 0;

int i;

struct nodo *p = b_root;

if ((p->info == key) && (found != 0))

{

found = 1;

return p;

}

for (i=0; i<p->numsons; i++)

{

p = p->sons[i];

if ((p->info == key) && (found != 0))

{

found = 1;

return p;

}

}

}

A titolo di esempio, si riporta in ¯gura 4.2 la sequenza di visita in pro-fonditµa prodotta con le due funzioni date. Come si puµo notare, la ver-sione ricorsiva della visita in ampiezza µe piµu agevole, poich¶e la strutturaoggetto-puntatore lista successivi si presta naturalmente alla de¯nizionedi funzioni ricorsive.

1

4

85

3

9

1, 3, 9, 4, 5, 8

1

510

7

11

8 2

13 63

1, 7, 11, 3, 13, 6, 8, 2, 10, 5

Figura 4.2: Visita in profonditµa di un albero

4.2 Visita di un albero in ampiezza

La visita di un albero in ampiezza procede, a partire dalla radice (livello 0),visitando in sequenza tutti i nodi di uno stesso livello, per poi passare a tuttiquelli del livello successivo. Nella ¯gura 4.3 si mostra la sequenza di visitain ampiezza di un albero.

Per realizzare questa operazione abbiamo bisogno, ogni volta che ci trovi-amo a un nodo corrente x, di memorizzare gli elementi successivi da visitare.Mentre si visita il nodo padre di x, possiamo memorizzare i puntatori ai¯gli in una sequenza, che verrµa visitata solo dopo che tutti i nodi al livellodi x siano stati visitati. Mediante l'uso di una coda si realizza facilmentequesta operazione: l'elemento corrente x µe in testa alla coda, poi ci sonotutti gli elementi dello stesso livello, e in¯ne vengono accodati i ¯gli di x. Ilprocedimento µe illustrato in ¯gura 4.3 per alberi binari.

Di seguito presentiamo il codice per la visita in ampiezza di un alberobinario e un albero n-ario, nella sola versione iterativa.

struct b_nodo {

int info;

struct b_nodo *left;

struct b_nodo *right;

}

1

4

85

3

9

1, 3, 4, 9, 5, 8

1

1 3 4

3 4

3 4 9

4 9

4

9

9 5 8

5 8

5 8

8

Figura 4.3: Visita in ampiezza di un albero

struct n_nodo {

int info;

struct n_nodo *left;

struct n_nodo **sons;

int numsons;

}

/* creazione degli alberi */

...

struct b_nodo *b_root;

struct n_nodo *n_root;

...

/* costruzione delle code */

struct b_nodo *b_queue[100];

struct n_nodo *n_queue[100];

int b_head = -1;

int n_head = -1;

int b_tail = -1;

int n_head = -1;

/* inserisci la radice nella coda */

b_queue[0] = b_root;

n_queue[0] = n_root;

b_head = 0;

b_tail = 0;

n_head = 0;

n_tail = 0;

/* visita in ampiezza */

struct b_nodo* BBreadthFirstSearch(int key)

char found = 0;

while (b_head <= b_tail)

{

/* controlla elemento corrente */

if (b_queue[head]->info == key)

{

found = 1;

return b_queue[head];

}

/* inserisci figli */

if (b_queue[head]->left != NULL)

push(b_queue, b_queue[head]->left;

if (b_queue[head]->right != NULL)

push(b_queue, b_queue[head]->right;

/* elimina nodo corrente */

pop (b_queue);

}

struct n_nodo* BBreadthFirstSearch(int key)

char found = 0;

int i;

while (b_head <= b_tail)

{

/* controlla elemento corrente */

if (b_queue[head]->info == key)

{

found = 1;

return b_queue[head];

}

/* inserisci figli */

for (i=0; i<b_queue[head]->numsons; i++)

push(b_queue, b_queue[head]->sons[i];

/* elimina nodo corrente */

pop (b_queue);

}

Nel codice proposto mancano i controlli di coda vuota e coda piena. Inoltre,si µe supposto di aver ride¯nito le operazioni push e pop in modo che essepossano lavorare con entrambi i tipi di strutture (array di puntatori a b nodo

e array di puntatori a n nodo). Si µe supposto inoltre che gli aggiornamenti deipuntatori alla testa e alla coda sono e®ettuati direttamente dalle operazionipush e pop.

4.3 Inserimento e cancellazione

Se l'albero non richiede particolari organizzazioni dei dati, occorre speci¯careil punto di inserimento di un nuovo elemento. La procedura di inserimentorichiede allora due parametri: il puntatore al nuovo nodo da inserire e ilpuntatore al padre. Riprendendo parte delle de¯nizioni del listato precedente,descriviamo l'operazione di inserimento per i due diversi alberi.

void BInsert(struct elem *p, struct elem *par)

{

if (par->left != NULL) && (par->right != NULL)

printf("genitore errato");

if (par->left == NULL)

par->left = p;

else

par->right = p;

}

void NInsert(struct elem *p, struct elem *par)

{

/* rialloca memoria per l'array dei figli */

p->sons = Reallocate(p->sons, numsons, 1);

par->sons[numsons] = p;

par->numsons++;

}

Nel codice per l'inserimento abbiamo supposto di aver de¯nito una funzioneReallocate che rialloca array di puntatori a nodo, anzich¶e interi come quelladescritta nel x1.

Per quanto riguarda la cancellazione, il procedimento µe piµu complesso.Innanzitutto, se µe nota la chiave key dell'elemento da cancellare, occorrecercare il suo puntatore x nell'albero. A questo punto possono presentarsitre diverse situazioni:

1. x µe una foglia

2. x ha solo un ¯glio

3. x ha entrambi i ¯gli

In tutti i casi occorre mantenere un puntatore al padre di x. Ciµo µe facilese si utilizza il campo parent, altrimenti occorre modi¯care l'operazione diricerca in modo che restituisca oltre al nodo cercato anche il proprio genitore.

void BDelete(int key)

{

struct elem *p = BSearch(key);

if (p == NULL)

printf("errore: elemento non presente");

/* caso 1 */

if (p->left == NULL) && (p->right == NULL)

{

/* p e' figlio destro o sinistro */

if (p == p->parent->left)

p->parent->left = NULL;

else

p->parent->right = NULL;

return;

}

/* caso 2 */

if (p->right == NULL)

/* p e' figlio destro o sinistro? */

if (p == p->parent->left)

p->parent->left = p->left;

else

p->parent->right = p->left;

if (p->left == NULL)

/* p e' figlio destro o sinistro? */

if (p == p->parent->left)

p->parent->left = p->right;

else

p->parent->right = p->right;

/* caso 3 */

struct elem *q = FindSucc(p);

/* copia il successore in p */

p->info = q->info;

/* elimina successore */

if (q == q->parent->left)

q->parent->left = NULL;

else

q->parent->right = NULL;

}

Abbiamo ipotizzato, nel terzo caso, di eliminare una foglia dell'albero, edesattamente la foglia piµu a sinistra del sottoalbero destro di p. Come ve-dremo negli alberi binari di ricerca (seconda parte delle dispense), quandoun ordinamento dei nodi dell'albero deve essere mantenuto, la funzione Find-Succ trova l'elemento successivo di p nell'albero. In questo contesto, la sceltadel successivo µe assolutamente inin°uente.

struct nodo* FindSucc(struct nodo *p)

{

struct nodo *q = p->right;

while (q->left != NULL)

q = q->left;

return q;

}

Si lascia per esercizio al lettore de¯nire la procedura di cancellazione inun albero n-ario.