Introduzione
Benvenuti!
Come promesso iniziamo una piccola serie di articoli per approfondire, e proporre adeguate soluzioni, riguardo un problema apparentemente semplice/banale, ma che nasconde caratteristiche estremamente interessanti.
Il problema è molto semplice: data una lista di stringhe, convertirle in maiuscolo in modo alterno (la prima, la terza, etc…). Ovviamente cercando di risolvere il problema con algoritmi “funzionali”, cercando quindi, per quanto possibile, di staccarci dagli algoritmi e funzioni tipiche dei linguaggi imperativi.
Facciamo un esempio (in Pure Basic, perchè molto semplice e di facile comprensione):
dim myList$(6) myList$(1) = "string 1" myList$(2) = "string 2" myList$(3) = "string 3" myList$(4) = "string 4" myList$(5) = "string 5" myList$(6) = "string 6" for i=1 to 6 step 2 myList$(i) = uppercase(myList$(i)) next
Analizziamo il problema…
Il programma sopra mostrato, in Pure Basic, è semplice e veloce. Provare però ad affrontarlo senza l’uso di un ciclo for… step può nascondere difficoltà non previste. Talvolta il problema non si pone nel trovare un algoritmo in sè, ma quanto alla sua ottimizzazione, in termini di eleganza / performance / compattezza del codice / etc…
Il problema è riuscire a trovare quale sia la posizione dell’elemento che siamo analizzando, per decidere poi se convertirlo oppure no.
Ciò di cui abbiamo bisogno allora è:
1) capire quali elementi dobbiamo convertire. E’ facile constatare che, nel nostro caso, si tratta di elementi posizionati in “caselle” dispari (posizione 1, posizione 3, posizione 5, etc…). Dobbiamo quindi trovare un algoritmo per “scoprire” quali numeri sono dispari.
Dopo alcuni giorni di brain-storming sul forum di newLisp (vedi qui: http://www.alh.net/newlisp/phpbb/viewtopic.php?t=2574&postdays=0&postorder=asc&start=0) sono venute fuori diverse soluzioni. Vediamole una per una, verificando anche altri fattori come, ad esempio, le performance.
ALGORITMO -1-(define (even? x)
(= (/ x 2) (div x 2) ) )
(define (odd? x)
(!= (/ x 2) (div x 2) ) )
ALGORITMO -2-
(define (odd? num)
( (bits num true) 0) )
(define (even? num)
(not ( (bits num true) 0) ) )
ALGORITMO -3- (versione semplificata dell’ALGORITMO 2); Bit test per posizione.
(define (bit? pos num)
(bits num true) pos)
(define (odd? num)
(bit? 0 num) )
(define (even? num)
(not (bit? 0 num) ) )
ALGORITMO -4-
(define (odd? num)
(= (& num 1) 1) )
(define (odd? num)
(= (& num 1) 0) )
Ok, già da questa prima lista possiamo vedere come, per un’attività semplice come verificare se un numero è pari o dispari si possano adottare diverse tecniche. Il secondo algoritmo (ed il terzo) sono interessanti perchè mettono in evidenza un fatto: un algoritmo può essere un pò più lento (il terzo) ma anche più leggibile.
Facciamo un controllo preliminare sulle performance. Ho usato questo piccolo script newLisp (potete usarlo per fare i vostri tests):
(define (bit? pos num) (bits num true) pos) (define (even-1? x) (= (/ x 2) (div x 2) ) ) (define (even-2? num) (not ( (bits num true) 0) ) ) (define (even-3? num) (not (bit? 0 num) ) ) (define (even-4? num) (= (& num 1) 0) ) ;============================================================== (setq start (time-of-day)) (dotimes (x 1000000) (even-1? x) ) (println "Risultato 1: " (- (time-of-day) start) ) (setq start (time-of-day)) (dotimes (x 1000000) (even-2? x) ) (println "Risultato 2: " (- (time-of-day) start) ) (setq start (time-of-day)) (dotimes (x 1000000) (even-3? x) ) (println "Risultato 3: " (- (time-of-day) start) ) (setq start (time-of-day)) (dotimes (x 1000000) (even-4? x) ) (println "Risultato 4: " (- (time-of-day) start)
Questi sono i risultati che ho avuto sul mio PC (ho lanciato i tests tre volte, solo sulle funzioni per il calcolo dei numeri pari):
Risultato 1: 459 / 445 / 437 – MEDIA = 447Risultato 2: 855 / 857 / 870 – MEDIA = 860
Risultato 3: 1022 / 976 / 976 – MEDIA = 991
Risultato 4: 298 / 301 / 294 – MEDIA = 297
Mi ha colpito il primo risultato: pensavo che l’uso della divisione avrebbe alzato i tempi di calcolo, invece si è rivelato un algoritmo piuttosto efficiente. Il migliore però è l’ultimo: il calcolo di un AND logico tra i bit è velocissimo! Al contrario mi sarei aspettato buone performance dall’algoritmo 2, mentre è risultato molto lento!
Chiudiamo quindi questa prima parte del workshop, valutando i seguenti punti:
- Quali algoritmi sono più leggibili, quindi comprensibili a chi non li conosce?
- Quali operazioni sono risultate più veloci?
- Alcuni algoritmi portano a risultati inaspettati.
- Notate che alcuni algoritmi forniranno risultati differenti per la funzione (odd?) e per la (even?), in quanto non usano le stesse istruzioni (un esempio è l’algoritmo 2, in quanto uno include anche un not).
Per ora è tutto. Questo articolo introduttivo vi offre un assaggio di quello che saranno i prossimi workshop: vi garantisco che questo è solo l’inizio, e per i prossimi articoli ci sarà molta carne al fuoco, molti algoritmi, performance molto diverse, algoritmi più o meno comprensibili ed eleganti (alcuni degni di un’opera d’arte!).
Alla prossima!
Translate To English!
Commenti Recenti