Lista possibili argomenti raffa: - Alberi binari (dx-sx, child-sibiling), Alberi binari di ricerca - Liste (singole, doppie, con sentinella) - Pile, Code - Visite albero (in profondità(con stack), in ampiezza(con coda)) - Vettori (dividi-et-impera) Consigli: Liste: se si ha a che fare con una lista, tenere sempre conto della sua struttura. Se e' doppia, c'è sempre un qualche trucco per sfruttare la cosa a proprio vantaggio nelle ricerche e simili. Idem per la sentinella. Vettori: se c'è un vettore di mezzo, è quasi sempre collegato ad un maledettissimo algoritmo dividi et impera. Sempre. Se così non è, allora c'è il trucco per fare tutto in tempo O(n). Alberi binari (dx-sx) e/o di ricerca: per scorrerli usare sempre il caro vecchio metodo ricorsivo: Albero SX, Albero DX, Radice. Il 90% dei problemi sugli alberi sono risolvibili ricorsivamente usando come casi base l'albero nullo e la foglia. Tener conto sempre che l'algoritmo da implementare necessiterà sicuramente di dare in uscita più output, ovvero dover passare tra una chiamata e l'altra più dati (nel caso del C coi puntatori). Addendum per gli alberi di ricerca: se i dati che si cercano sono legati all'ordinamento, sfruttare ovviamente questa informazione. Addendum2: se si vede che il problema non è risolvibile ricorsivamente per un albero binario di ricerca (vedi per esempio contare tutti i valori duplicati nell'albero) usare le funzioni successivo() e/o precedente() con l'uso di un ciclo while. Definizioni di albero completo e bilanciato. Completo: tutti i lv sono "pieni". Solo l'ultimo LV è composto da foglie. Entrambi i sotto-alberi di ogni nodo hanno la stessa altezza. Bilanciato: tutti i nodi (a eccezione delle foglie) hanno due sottoalberi dx e sx il cui numero di figli o è uguale, o differenziano al massimo di 1. Rivedere i conti su h, numero foglie e altro negli alberi binari bilanciati. Alberi binari (child-sibiling): scorrevoli con un while e una ricorsione. Il ciclo while scorre i fratelli, mentre la ricorsione scorre i figli. Raramente li mette, ma ogni tanto compaiono. Pile, Code: sui compiti più recenti mai comparsi, nei compiti vecchi si. push mette in testa, e pop toglie dalla testa. LIFO! enqueue toglie dalla coda e queue dalla testa. FIFO! Visite albero: pre-ordine: Root, SX, DX (RDS) (detta anche anticipata) in ordine: SX, Root, DX (SRD) (detta anche simmetrica) post-ordine: SX, DX, Root (SDR)(detta anche posticipata) in profondità: SX, DX, Root (detta anche DFS, Deep First Search) in ampiezza: LV0, LV1, LV2, ... LVN (detta anche BFS, Breadth First Search) TRUCCO PER LA RICORSIONE (SU LE ORRECCHIETTE!!) Basandomi sui consigli della raffa e su esperienza personale, il miglior modo per risolvere un problema ricorsivo è dividere il problema in tre parti: - come parto (casi base) - come itero (ricorsione) - come finisco (risultato da ritornare) Conviene partire col secondo, come itero, ovvero ad ogni giro di iterazione, che dati mi servono? Capiti i dati, bisogna domandarsi: quali sono i casi base? Decisi i casi base, si decidono i valori di base, che saranno "le fondamenta" della ricorsione. Infine, sistemare la funzione, aggiustandola qui e la. Quando si vede che funziona, scrivere come invocarla per il primo step, quello che lancerà l'iterazione, e se necessario creare una funzione ausiliaria di start, ovvero quella che farà iniziare la ricorsione. Esempio: dato un albero SX, DX, INFO dire se e' completo. Albero completo, come visto sopra, è un albero nel quale solo l'ultimo LV è composto di foglie e per cui tutti i nodi hanno dei figli la cui altezza è uguale. Da questo sappiamo quindi che la nostra chiamata ricorsiva ha bisogno 2 valori di output: PSEUDO: = complete() C: is_complete = complete(&height); Ovvero quello che vogliamo sapere è: - se il nodo su cui ricorriamo è completo - e la sua altezza, che ci servirà per fare il controllo per determinare se è completo La chiamata quindi come input almeno un nodo dovrà averlo, per poter determinare se quel nodo è completo o no. PSEUDO: = complete(n) C: is_complete = complete(n, &height); Come faremo la chiamata ricorsiva? Si tratta di un albero binario i cui figli sono altrettanti alberi binari con un figlio dx e un figlio sx. Chiameremo quindi la chiamata ricorsiva almeno due volte (uno per sx, uno per dx). PSEUDO: PSEUDO: = complete(n.left) = complete(n.right) C: is_complete_lt = complete(n->left, &height_lt); is_complete_rt = complete(n->right, &height_rt); Con la chiamata ricorsiva abbiamo grosso modo idea di cosa ci serve sia come input che come output, possiamo quindi definire la firma PSEUDO: complete(Node n): C: int complete(Node n, int* height) Possiamo sviluppare la funzione e poi riempire quanto manca coi vari passaggi: PSEUDO: complete(Node n): //CASI BASE //CHIAMATA RICORSIVA = complete(n.left) = complete(n.right) //ELABORAZIONE //RETURN C: int complete(Node n, int* height){ //CASI BASE //CHIAMATA RICORSIVA int is_complete_lt, is_complete_rt; int *height_lt, *height_rt; is_complete_lt = complete(n->left, &height_lt); is_complete_rt = complete(n->right, &height_rt); //ELABORAZIONE //RETURN } Ora possiamo cominciare a lavorare sui casi base. Beh.. l'albero completo dice che solo l'ultimo livello sono foglie e tutti i nodi hanno altezza uguale tra loro. Si sa che l'albero nullo ha altezza 0, dunque una foglia ha due figli le cui altezze sono entrambe 0, quindi uguali, e quindi verificano la proprietà dell'albero completo. Sarà quindi il nostro caso base l'albero nullo. PSEUDO: if n == NIL: return C: if(n == NULL) { *height = 0; return 1; } Immettiamo questi dati all'interno della nostra funzione. PSEUDO: complete(Node n): //CASI BASE if n == NIL: return //CHIAMATA RICORSIVA = complete(n.left) = complete(n.right) //ELABORAZIONE //RETURN C: int complete(Node n, int* height){ //CASI BASE if(n == NULL) { *height = 0; return 1; } //CHIAMATA RICORSIVA int is_complete_lt, is_complete_rt; int *height_lt, *height_rt; is_complete_lt = complete(n->left, &height_lt); is_complete_rt = complete(n->right, &height_rt); //ELABORAZIONE //RETURN } Siamo arrivati all'ultimo punto, cosa tornare? Beh, i dati ce li abbiamo, dobbiamo solo capire come usarli. Innanzitutto possiamo calcolarci l'altezza che torneremo come risultato in questa chiamata ricorsiva. L'altezza del nodo che stiamo esaminando è naturalmente l'altezza più grande tra l'albero dx e l'albero sinistro + 1 (il +1 è la radice, ovvero il nodo che stiamo esaminando ora). Già questo dovrebbe farvi pensare a un modo di come controllare la completezza dell'albero. Intanto facciamo questo calcolo PSEUDO: height = max(height_lt, height_rt) + 1 C: *height = max(height_lt, height_rt) + 1; La cosa più semplice da assumere è che se un albero ha un figlio che non è completo sicuramente anch'esso non è completo. Questo è già un punto importante da tener conto. Si noti in C che non serve tornare height, essendo height passato per puntatore e quindi il suo valore già calcolato e tornato direttamente con il dereferenziamento. PSUEDO: if is_complete_lt and is_complete_rt: return C: if(is_complete_lt && is_complete_rt) return 1; Questo non basta per definire se l'albero è completo. Un albero è completo se le sue due altezze sono uguali! Se non lo fossero, non andrebbe bene. Uno potrebbe obbiettare: bisogna contare anche i nodi. E' vero, si sarebbe potuto fare anche così, ma guardando le altezze si trova subito se l'albero non è completo perché sicuramente un nodo ha un solo figlio, e quindi le due altezze non combaciano nei due sottoalberi dx e sx, rendendo quindi falsa la proprietà. Modifichiamo l'if aggiungendo questa condizione: PSUEDO: if is_complete_lt and is_complete_rt and (height_lt == height_rt): return else: return C: if(is_complete_lt && is_complete_rt && (height_lt == height_rt)) return 1; else return 0; Aggiungiamo tutto ciò alla nostra funzione: PSEUDO: complete(Node n): //CASI BASE if n == NIL: return //CHIAMATA RICORSIVA = complete(n.left) = complete(n.right) //ELABORAZIONE height = max(height_lt, height_rt) + 1 //RETURN if is_complete_lt and is_complete_rt and (height_lt == height_rt): return else: return C: int complete(Node n, int* height){ //CASI BASE if(n == NULL) { *height = 0; return 1; } //CHIAMATA RICORSIVA int is_complete_lt, is_complete_rt; int height_lt, height_rt; is_complete_lt = complete(n->left, &height_lt); is_complete_rt = complete(n->right, &height_rt); //ELABORAZIONE *height = max(height_lt, height_rt) + 1; //RETURN if(is_complete_lt && is_complete_rt && (height_lt == height_rt)) return 1; else return 0; } La nostra funzione necessita di una funzione che la faccia partire, visto il fatto che sicuramente l'utente si aspetta di passare solo un nodo e non anche un'altezza. Questo punto vale solo per il C, visto l'uso de puntatori. Dunque: C: int is_complete(Node n){ int h; return complete(n, &h); } Bene, abbiamo finito. XD