www.caosfera.it creativitoria 100% MADE IN ITALY Segni Fabrizio Tagliaferro OTTIMIZZAZIONI ISBN copyright 2011, Caosfera Edizioni www.caosfera.it soluzioni grafiche e realizzazione Fabrizzio Tagliaferro OTTIMIZZAZIONI OTTIMIZZAZIONI Indice CAPITOLO 00 - Introduzione 01 - Come si disordina un array 02 - Analisi di un GNPC di riferimento 03 - Impostazione di un GNPC 04 - Progettazione DI UN gnpc 05 - Permutazioni 06 - Comportamento degli algoritmi 07 - Bubble Sort 08 - Digressione 09 - Tabelle prestazioni per Bubble Sort 10 - Bubble Sort Indiretto 11 - Bubble Sort con Scorrimento 12 - Bubble Sort con Attraversamento Dinamico 13 - Conclusioni riguardo Bubble Sort 14 - Ordinamento Caotico 15 - Selection Sort 16 - Selection Sort Indiretto 17 - Selection Sort con Attraversamento Dinamico 18 - Una Selezione... diversa 19 - Conclusioni riguardo Select Sort 20 - Insert Sort 21 - Insert Sort Indiretto 22 - Insert Sort con Destinazione Calcolata 23 - Insert Sort con Facilitazione Probabilistica 24 - Insert Sort con Attraversamento Dinamico 25 - Insert Sort Ricorsivo 26 - Fusione Easy & Dina 27 - Valutazioni riguardo Insert Sort 28 - Uccidi il Verme 29 - Balck Hole Sort 30 - Heap Sort 31 - Da Inserimento a Fusione 32 - Merge Sort Top Down 33 - Merge Sort Bottom Up 34 - Problemi con la Allocazione Dinamica 35 - Shell Sort - Primi Passi 36 - Shell Sort - Potenze e Numeri Primi 37 - Shell Sort - Altre Soluzioni Pag. Numero pagine p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX 11 8 4 10 15 9 3 6 3 8 6 5 10 1 8 10 4 9 3 1 12 3 10 15 15 9 2 1 9 10 12 4 11 7 4 14 8 6 38 - Shell Sort con Inserimento 39 - Shell Sort Bidimensionale 40 - Dobo-Comb-Nes Sort 41 - Miti sfatati 42 - Figure Retoriche 43 - Quick Sort - Primi Passi con BKRD 44 - Quick Sort - Approfondimenti con HSNW 45 - Quick Sort - Fino in fondo con RS 46 - Quick Sort - A3 M3 INS 47 - Quick Sort - Varie ed Eventuali 48 - Quick Sort Digitale 49 - Quick Sort - Prima del Fulmine 50 - Quick Sort - Stabilità 51 - Quick Sort - Partizioni Multiple 52 - Grid Sort - Distribuzione a Griglia 53 - Grid Sort - Target Sort 54 - Grid Sort - Alien Sort 55 - Grid Sort - Partizionamento Stimato 56 - Grid Sort - Array Esterno di Appoggio 57 - Valutazioni conclusive riguardo all'Inserimento 58 - Grid Sort - Worp Sort 59 - Grid Sort - Analisi Tempi Parziali 60 - Programma di test 61 - File sort_list.h Appendice 1 - Tabelle Lunghezza Verme Appendice 2 - Tabelle Facilitazione Probabilistica Appendice 3 - Tabelle Gaia Sort Appendice 4 - Tabelle Dimensione Media Ottimale per Worp p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX p. XX 7 5 10 3 6 10 4 5 16 11 9 11 9 11 11 5 3 2 9 3 6 9 7 2 3 1 9 5 478 TOTALE PAGINE 00 - Introduzione 1/11 00.0 - Un Aneddoto Mercoledì 30 settembre 2009, ore 23:20 circa. Sono appena uscito dalla scuola di balli caraibici, dove ho imparato i primi passi, (uno-dos-tres, cinco-sei-siete) e dove ora vado a dare una mano, ed anche un piede, se il numero di dame presenti ai corsi eccede il numero dei maschi. Lezione dimostrativa di inizio corso, prima per principianti e poi livello intermedio, per circa due ore (unodos-tres, cinco-sei-siete), ma uscendo non ne ho ancora abbastanza. Parcheggio la macchina nei pressi di un locale notturno ricavato ristrutturando una vecchia casa cantoniera, non lontano dalla scuola, ed avvicinandomi all’ingresso, dall’altra parte della strada, che attraversa in compagnia di una comune amica, anche lei ballerina e sua connazionale, vedo… Lei (Nota: ricordarsi mettere L maiuscola e punti di sospensione). Lei… va via presto, deve alzarsi di buon’ora per andare al lavoro, ma abbiamo il tempo di fare qualche ballo insieme, e di scambiare gomitate amichevoli (grrr!) e tacchi a spillo sulla caviglia (sticazz!) con i vicini di mattonella. Come sempre, ogni volta che mi avvicino ed allungo il braccio per ♫ invitarla in pista, ♪ si gira verso gli amici con cui parla del più e del meno, del per e del diviso, del modulo e della radice quadrata, e dice: «Vado a ♫ ballare col mio moroso» con il suo tipico accento bulgaro. Nell’accidentato percorso tra la sedia a bordo pista ♫ ed il centro ♫ del caos ♫, ho sovente il tempo per chiedermi quanto di quella frase sia “vero affetto”, e quanto invece sia “presa per i fondelli”, ma dopo pochi secondi siamo già in ♪ mezzo, e balliamo ♫ ♫. Durante una bachata ♪, quando siamo più stretti ♫, ed a distanza d’orecchio, le racconto che due giorni prima avevo finalmente concluso il corso IFTS di Logistica, con la valutazione scritta finale, ed avevo messo tutte le dispense in una scatola di cartone, occultandole poi dentro ad un armadio. Avevo di nuovo il tempo per riprendere in mano il libro che avevo cominciato mesi prima, e di cui le avevo già vagamente accennato settimane addietro. «Me lo ♪ dedichi? ♫» Ed io, sorpreso, considero in silenzio, con sottofondo di arpeggi ♫ di chitarra ♫ e voci ♪ melanconiche ♫, che scrivere un libro è leggermente più impegnativo dell’andare ♫ dal DJ a chiedere di ♪ mixare un pezzo ♫ con dedica al microfono ♫. «È informatica» la banale risposta che mi esce da sola, senza ottimizzare troppo i miei schemi mentali. Forse in questo momento penso che l’argomento del libro, non suscitando in… Lei (L maiuscola) un particolare interesse, potrebbe non essere altrettanto appetibile per una dedica. Mah, non saprei (♪ musica ♫ di sottofondo ♪). Poco più tardi, durante il nostro ultimo ballo ♫ prima della sua sparizione notturna, roteando ♪ ♪ ♪ in pista, accenna ad un nostro comune amico, che ha notato per la prima ♫ volta in quel momento, seduto per conto suo, da solo, in seconda fila. Dato che non lo aveva ancora salutato, e secondo lei aveva un aspetto ♫ triste, (cioè da ometto non più giovane, sposato, con figli già grandi ed autonomi, che non balla perché poco appetibile e quindi ignorato dalle giovani gagliarde ed intraprendenti animatrici del locale, e seduto in seconda fila), le dissi: «Vvaa beene, finita questa ♫ ♪ ♫ vai da lui a renderlo felice… MA NON TROPPO!» Dopo il casché finale ♫ ♫, raddrizzandosi, Lei aggiunge: «Vado a renderlo felice ♪ ♫, ma non troppo.» «Ok ♪ » Fatta la precedente ed inevitabile premessa, doverosamente valutati pro e contro, non è totalmente falso affermare/sospettare/insinuare/supporre, che esiste una probabilità P > 0, piccola ma non trascurabile, secondo cui il di Lei (L maiuscola) atteso e richiesto procedimento abbia avuto una tanto fortuita quanto sperata ma infinitesimale attuazione, ovvero è ragionevolmente possibile che il presente aneddoto sia dedicato a Maryiana, la mia “quasi morosa pseudo casuale”, per una questione di congruenza lineare… Mmh… Dunque, dov’ero rimasto? Ah, sì, adesso ricordo: il libro. 00 - Introduzione 2/11 00.1 - Premessa Diversi anni fa, ormai parecchi, quando attraversai una fase della vita che è comune a tanti, in cui si comincia a delineare un interesse per un percorso lavorativo e/o professionale, partecipai ad un corso di formazione per “Operatore CAD/CAM”. Durante una lezione sulle basi di programmazione, in linguaggio FORTRAN, avendo già superato la fase relativa alle assegnazioni di variabili, READ e WRITE, IF, DO, e così via, il docente propose, per una esercitazione, di ideare e scrivere su carta un programma, possibilmente di poche righe, in cui un array di 10 elementi veniva ordinato. Non possedevo ancora un personal computer, ma ricordavo di aver letto tempo prima su una rivista un algoritmo di ordinamento scritto in BASIC, e di averci perso del tempo a rileggerlo per decifrare le istruzioni, convinto di averlo compreso. Mi sforzai di ricordare ciò che avevo capito allora, e lo riprodussi, come potevo, convertendo mentalmente le istruzioni di un linguaggio che conoscevo vagamente, in un altro che stavo ancora iniziando ad imparare. Un dubbio. Mi avvicinai per chiedere al docente se l’ordinamento doveva essere crescente o decrescente, perché avrei dovuto modificare gli operatori di confronto. Stupito per il fatto che avevo già finito, volle vedere cosa avevo scritto. In realtà non avevo finito, e ciò che avevo scritto era un abbozzo di una versione personalizzata e piena di errori tra lo “SHELL sort” ed il “BUBBLE sort”, mi disse. Ma in compenso nessun altro partecipante al corso aveva ancora scritto qualcosa. Perché era sbagliato? Ad ogni passata, prima di ridurre la distanza fra i due indici (classicamente “I” e “J”), avrei dovuto mettere un flag. Non c’era garanzia infatti che una singola passata di confronti, ed eventuali scambi, sarebbe stata sufficiente ad effettuare il parziale ordinamento, e la passata andava ripetuta tale e quale, fino a che tutti gli elementi non si fossero trovati nella posizione migliore possibile, e non si sarebbe più verificata la condizione per uno scambio degli elementi dell’array. Feci qualche correzione, ma non andava ancora bene. Avevo dimenticato di aggiungere l’istruzione di azzeramento del flag di scambio, prima di iniziare la passata. Ma il tempo a disposizione era finito. Crescente o decrescente? Lo finisco a casa? Ma non aveva importanza, lo scopo era valutare quanto avevamo capito, fino a quel momento, delle logiche di programmazione. E nessun altro era arrivato ad abbozzare una soluzione coerente. Ancora qualche minuto, forse per dare il tempo a qualcuno di finire. Approfittai per “aggiungere” l’istruzione mancante. Poi fui chiamato per scriverlo sulla lavagna. Conclusione: l’istruzione che terminava l’esecuzione del programma, quando la distanza tra i due indici diventava minore di “1”, mancava, e la aggiunsi al momento sulla lavagna. Ma non era tutto. Anche se il linguaggio usato era il FORTRAN, invece del DO avevo usato il GOTO, ed a sinistra di ogni istruzione avevo scritto 10, 20, 30, come nel BASIC interpretato. Crescente o decrescente? Lezione finita, tutti in mensa. Prima ancora di accendere un terminale, il mio primo programma su carta è stato un algoritmo di ordinamento. Nel tempo, e con l’esperienza, ho compreso che quella prima esercitazione non era pseudo-casuale. Tre libri con nomi importanti, acquistati in seguito, (ed indicati con un asterisco nel prossimo paragrafo) riguardanti la programmazione, sottolineano l’importanza di questo genere di algoritmi, poiché sono sufficientemente brevi da potersi contenere, nella maggior parte dei casi, in una sola pagina di testo, quindi facili da capire per chi sta facendo i primi passi, ed utilizzando varie metodologie di trattamento dei dati ed alla implementazione pratica di procedimenti algoritmici e concetti matematici, si apre la strada verso la comprensione di problemi computazionali più complessi. PORZIONE DI PAGINA INTENZIONALMENTE VUOTA 00 - Introduzione 3/11 00.2 - Risorse disponibili • Pentium 4 (dual core) 3,20 Ghz • Niklaus Wirth, Algoritmi + Strutture Dati = Programmi, Tecniche Nuove, 2ª edizione, 1987 * • H. Schildt, Turbo C Programmazione Avanzata, Mc Grow-Hill Libri Italia, 1ª edizione, novembre 1988 * • Brian W. Kernighan, Dennis M. Ritchie, Linguaggio C, Jackson Libri, 2ª edizione, 1989 • Clovis L. Tondo, Scott E. Gimpel, Linguaggio C Il libro delle soluzioni, Gruppo Editoriale Jackson, 2ª edizione, 1990 • DEVCPP 4.9.9.2 GNU General Public License - 2/06/1991 scaricato da Internet • DOS IBM 5.0 Manuale utente, 1ª edizione, luglio 1991 • Robert Sedgewick, Algoritmi in C, Addison ~ Wesley Masson, 1ª edizione, marzo 1993 * • Connessione Internet • Appunti, fogli di quaderno, stralci di tabulato, per uno spessore di circa 3 cm (ovvero quanto resta e quanto ho ritrovato, dopo anni, del tempo libero dedicato agli algoritmi di ordinamento, lavorando su un PC IBM 80386, prima che un fulmine notturno si portasse via l’alimentatore e la scheda madre) PORZIONE DI PAGINA INTENZIONALMENTE VUOTA 00 - Introduzione 4/11 00.3 - Propositi Dopo aver visto passarmi davanti CD e DVD, che hanno surclassato e fatto dimenticare i floppy disk ed il vinile (o quasi); varie release di Windows, con le rincorse commerciali, i difetti, gli aggiornamenti, e qualche dietro-front; cellulari ipertecnologici che ti obbligano ad alcuni giorni di full immersion, solo per isolare, capire e personalizzare quelle poche funzioni che veramente servono, MP3, MP4, iPod… Ma c’era sempre quel mazzetto di vecchi appunti impolverati, in mezzo ad altro vecchiume, ed un senso di incompletezza. Chissà quando avrò tempo? Poi… La crisi dei mutui/obbligazioni USA finirà nel 2012 (da: Il Sole 24 Ore). Adesso, aprile 2009. Disoccupato. Tempo libero. Perché no? Quanto segue è un approccio personale alle problematiche di programmazione, limitatamente agli algoritmi di ordinamento. Ricostruendo in parte, grazie ai vecchi appunti, i percorsi ed i pensieri agli albori della mia esperienza informatica, strada facendo ho integrato altre nozioni per le quali ero allora immaturo. Non ho pretese di vastità ed esaustività, né di mantenere un profilo pseudoaccademico. Ciononostante mi auguro che questo tentativo di condivisione non rimanga fine a sé stesso, e di poter aggiungere qualche nuovo ed originale frammento di conoscenza, anche se in modesta misura, ad un argomento di cui si conosce già tanto. PORZIONE DI PAGINA INTENZIONALMENTE VUOTA 00 - Introduzione 5/11 00.4 - Titolo del libro OTTIMIZZAZIONI Ovvero: Tutto quello che avresti voluto sapere sugli algoritmi di ordinamento e non hai mai osato chiedere? No, non esattamente... OTTIMIZZAZIONI Ovvero: Tutto quello che avresti preferito non sapere sugli algoritmi di ordinamento! 00 - Introduzione 6/11 00.5 - Definizione di Array Dopo aver cercato sulla rete alcune definizioni, tutte ragionevolmente corrette, noto che ciascuna di esse rispecchia il pensiero di chi l’ha composta, oppure il contesto in cui è inserita, o lo scopo per cui è stata concepita. Ciascuna di esse gira intorno al concetto principale, esponendone alcune caratteristiche, ma lasciandosi dietro la sensazione di incompletezza o di non aver centrato l’obiettivo. Dato che non esiste una definizione univoca, tanto vale crearne una personale. La prima cosa che mi chiedo è la seguente: se da una parte mi ritrovo ad avere una certa idea soggettiva di cosa sia un array, senza averne una definizione rigorosa, e dall’altra esiste da tempo l’associazione tra il termine array ed un “insieme di dati da definire”, mi chiedo come sia avvenuta in passato tale associazione. Qual era il significato e l’uso della parola “array” prima che la matematica (prima) e l’informatica (dopo) se ne appropriassero? La traduzione di “array” che mi affascina maggiormente è “schieramento”. Ripetendo mentalmente il suono della parola, immagino me stesso in un campo, con lo sguardo all’orizzonte, e davanti a me dei pali di legno, in fila, che sostengono una rete di recinzione. Non importa cosa ci sia oltre la rete. Ogni palo di legno è un’entità a sé stante, ma la disposizione in fila mi permette di vederli tutti insieme, spostando lo sguardo da destra a sinistra e viceversa. La rete di recinzione che li collega mi fa intuire che tutti i singoli pali sono disposti in questo modo per mettere in pratica uno scopo comune. Ogni palo è stato conficcato nel terreno singolarmente, poi legato alla rete singolarmente, ma tutti insieme recintano un campo o delimitano una proprietà. SALTO. Davanti al televisore vedo l’obiettivo di una telecamera che inquadra, sul campo di gioco, uno dopo l’altro, i giocatori di una squadra, in raccoglimento, prima della partita. Ogni giocatore è una persona distinta dalle altre, con la sua vita, i suoi allenamenti, le sue motivazioni. Ogni giocatore è entrato in campo per proprio conto, si è disposto in fila, ed è stato inquadrato, puntato dalla telecamera, individualmente. Ma con l’inquadratura in campo lungo si vede che sono una squadra, con uno scopo comune. SALTO. Primo ottobre 331 a.C.. Gli eserciti schierati del macedone Alessandro il Grande e di Re Dario III di Persia si fronteggiano, a Gaugamela. SALTO. Sono di nuovo davanti al mio computer. Chiudo la pagina di Wikipedia e la finestra di Internet Explorer. Array = schieramento (schiera, dal latino scàra, corpo di soldati ordinato a scalare, affiancato, sopra una linea determinata), disposizione multipla di singoli elementi, dotati di caratteristiche omogenee, tra loro allineati o ordinati o disposti secondo criteri di regolarità, tali che un osservatore esterno possa vedere o riconoscere contemporaneamente A) ogni singolo elemento come a sé stante, B) l’intero raggruppamento dei singoli, come facenti parte di una unica entità di gruppo o di insieme e C) lo scopo per cui i singoli elementi ed il loro insieme vengono utilizzati. Array di dati = disposizione multipla di singoli elementi informativi (dati) di base, dotati di caratteristiche omogenee (numeriche, alfabetiche, alfanumeriche, ecc.) allocati (collocati) nella memoria volatile (RAM) di un microprocessore o altro dispositivo di memorizzazione e registrazione o altro sistema di elaborazione, organizzati secondo criteri di regolarità, tali che un programma (software) possa accedere, in lettura/scrittura contemporaneamente, A) ad ogni singolo dato (contenuto) in ogni singola cella o casella di memoria (contenitore), B) all’intero insieme dei dati e C) riconoscendone le caratteristiche e le proprietà per cui i singoli elementi ed il loro insieme vengono utilizzati. L’accessibilità ai singoli dati contenuti in un array di N elementi A {0,...,n-1} oppure A {1,...,n} avviene generalmente grazie al puntamento fisico o logico (indirizzamento) alla cella di memoria che si trova ad un estremo dell’insieme, detto primo elemento A[0] oppure A[1], utilizzato come riferimento primario per accedere a tutte le altre celle grazie ad un indice I di 00 - Introduzione 7/11 spiazzamento, nel modo seguente: A[I] (traducibile in: calcola/leggi/reperisci la posizione primaria di A, poi calcola la posizione distante I posizioni da A, poi accedi al contenuto di A[I]), ed agendo sul contenuto della singola cella puntata con appropriate operazioni di lettura e/o scrittura. L’indice di spiazzamento I è a sua volta una cella di memoria indipendente che può assumere valori interi contigui compresi tra 0 ed N-1 (oppure tra 1 ed N), per un array di N elementi. In matematica “array di dati” è sinonimo di “vettore di dati” o “matrice mono dimensionale” o “matrice ad una sola riga”. In informatica viene definito anche come “costruttore di tipo”, cioè un metodo per definire strutture di dati complessi partendo da singole definizioni di base preesistenti. Per “definizione di dato” si intende la “dimensione” del contenitore, cioè la quantità delle singole celle unitarie di memoria adiacenti che contengono per intero il singolo dato di base, ed il “tipo di dato”, cioè la metodologia di trattamento del contenuto. PORZIONE DI PAGINA INTENZIONALMENTE VUOTA 00 - Introduzione 8/11 00.6 - Definizione di Algoritmo di Ordinamento Dalla radice “or”, cioè nascita, inizio, origine di qualcosa, suscitare, mettere in movimento. Dalla desinenza “do”, cioè dare, attribuire. Dal latino “ordo”, cioè dare inizio, dare origine, dare movimento, ma anche maniera di procedere. Da “al-Khwarizmi”, matematico arabo del IX secolo, “algorismo”, “algoritmo”, cioè “procedimento di calcolo per raggiungere un risultato”. Il secondo termine, “algoritmo” è stato in seguito preferito, probabilmente per la somiglianza con il termine greco rithmòs, scorrimento cadenzato, movimento misurato, simmetria. Algoritmo di ordinamento = procedimento ripetitivo codificato che, se messo in atto su una qualsiasi permutazione o combinazione o disposizione di numeri o elementi contenuti nelle celle di un array (o su oggetti appartenenti ad una o più categorie con caratteristiche omogenee), permette di ottenere una ed una sola disposizione definitiva, tale che, alla fine del procedimento, tutti e solo gli elementi di partenza, cioè nessuno aggiunto e nessuno escluso, siano disposti in ordine crescente (o decrescente). Una sequenza numerica si definisce “crescente” se, dato un qualsiasi elemento I di un array o insieme A, A[I-1] ≤ A[I] ≤ A[I+1] (oppure decrescente se A[I-1] ≥ A[I] ≥ A[I+1]), con eccezione solo per i due elementi terminali dell’array o insieme, A[0] ed A[N-1] (oppure A[1] ed A [N]), che sono dotati di un solo elemento adiacente invece di due, cioè rispettivamente A[0] ≤ A[1] (oppure A[1] ≤ A[2]) e A[N-2] ≤ A[N-1] (oppure A[N-1] ≤ A[N]). Un algoritmo di ordinamento trova il suo punto di origine dal concetto astratto che sta alla base del procedimento. La sua implementazione prevede l’utilizzo di un linguaggio o forma di codifica, per formalizzarne e fissarne i passi elaborativi, di un insieme di dati da elaborare, e di un supporto fisico automatizzato, o meglio “informatizzato”, che li contenga entrambi. Il concetto astratto deve essere dotato della proprietà potenziale di codifica non limitata, indipendentemente cioè dal linguaggio di programmazione (possibilità di conversione di linguaggio) o dalle caratteristiche dei dati e dalla loro quantità, nonché dalle caratteristiche o potenza del sistema di elaborazione (portabilità). L’unico limite che si pone alla realizzazione è la fruibilità del prodotto finale, cioè una ragionevole combinazione tra A) facilità di comprensione e realizzazione, B) una dimensione contenuta per essere incorporato in programmi più grandi, e C) di un tempo di esecuzione più breve possibile, poiché le tre condizioni, se non ottimizzate, rappresentano un costo (economico per chi produce o utilizza il software e/o elaborativo per il computer). Un algoritmo di ordinamento deve essere strutturato secondo criteri, per lo più comuni alla maggior parte dei programmi: uno o più criteri di scorrimento e/o gestione dei dati lungo l’intera dimensione dell’array (indici, incrementi, puntatori, cicli nidificati, ricorsione); uno o più criteri di discriminazione per determinare e confrontare il valore numerico delle celle (“IF”, minore, maggiore, uguale, diverso); operazioni di spostamento dei valori tra le celle (assegnazione, scorrimento, scambio); una o più condizioni di termine (istruzioni di salto condizionato e incondizionato, fine ciclo, fine elaborazione), e di accorgimenti ausiliari di appoggio alle operazioni principali dell’algoritmo (variabili temporanee, flags, stacks, contatori, chiamate a funzione, ricorsione). Lo scopo principale di un algoritmo di ordinamento consiste nel disporre i dati disponibili in modo da predisporli a successive elaborazioni, o analisi, o ricerche, operazioni molto frequenti che vengono eseguite da altre componenti del software di gestione dei dati, le cui caratteristiche e competenze esulano dai compiti specifici per un algoritmo di ordinamento. 00 - Introduzione 9/11 00.7 - Catalogazione degli Algoritmi di Ordinamento Gli algoritmi di ordinamento possono essere catalogati secondo diversi tipi di categorie, tutte tra loro mutuamente non escludentesi (elenco non esaustivo). Per quantità di memoria utilizzata • “In Place”: utilizzano l’array stesso come punto di partenza e destinazione di scambi e/o spostamenti, utilizzando al più piccole quantità di variabili temporanee o ausiliarie. • Array duplicato o con array di appoggio: utilizzano una copia di parte o di tutto l’array da ordinare, aumentando l’utilizzo di memoria impiegata (esempio: merge_sort). Per metodo di analisi e ricerca dei dati Genericamente gli elementi di un array sono sottoposti a ripetitive operazioni di scansione per scorrimento, di confronto, e di scambio, a loro volta strutturate secondo i seguenti criteri elaborativi che possono coesistere nello stesso algoritmo: • A cicli iterativi nidificati, in cui l’intero array viene scandito grazie al posizionamento sulle singole celle grazie ad indici di spiazzamento o puntatori. • Partizionamento ed ordinamento parziale, in cui un insieme di elementi viene partizionato, cioè progressivamente suddiviso in parti più piccole, solitamente due, in cui solo una viene ordinata, e l’altra viene momentaneamente accantonata, per essere recuperata in una fase successiva. • Ricorsivi, cioè che fanno utilizzo di routines e/o funzioni che in determinate condizioni richiamano se stesse, sfruttando gli stessi meccanismi di partizionamento, ordinamento parziale e accantonamento in modo implicito. Per tempi di esecuzione Dato un parametro generico “p” dipendente dalle caratteristiche di un algoritmo di ordinamento • Esponenziali: eseguono l’ordinamento in un tempo proporzionale a p * KN per K ≥ 2. • Potenziali: eseguono l’ordinamento in un tempo circa proporzionale a p * Nk. • Quadratici: caso potenziale particolare per K = 2. • Cubici: caso potenziale particolare per K = 3. • Sub-Quadratici: eseguono l’ordinamento in un tempo proporzionale a p * NK, con 1 < K < 2. • Enne-garitmici: eseguono l’ordinamento in un tempo proporzionale a p * N * log(N). • Logaritmici: eseguono l’ordinamento in un tempo proporzionale a p * log(N). • Lineari: eseguono l’ordinamento in un tempo proporzionale a p * N. Nota: in un solo caso noto (quicksort digitale ottimizzato) presentato in questo libro, l’algoritmo sembra manifestare in determinate condizioni un tempo proporzionale a p * N / log(N). Per tipo di ottimizzazione • Senza ottimizzazione: illustra a fini didattici i criteri di base per un algoritmo di ordinamento. • Ottimizzati: versioni migliorate e più veloci, poiché hanno subito un processo di analisi e riprogettazione, che ha eliminato, dove possibile, i punti deboli e le inefficienze degli algoritmi di base (flags di riferimento temporaneo, riferimenti e spiazzamenti alleggeriti). • Ottimizzazione con struttura di supporto: ottengono prestazioni ulteriormente migliorare sfruttando strutture di appoggio quali attraversamento dinamico (dinacross), albero binario (heap), liste concatenate, partizionamento, telaio digitale (frame), griglia di distribuzione (grid). Per efficienza ed efficacia • Un algoritmo si dice molto efficace se un determinato elemento, dal punto in cui si trova in origine, subisce mediamente un numero molto basso di spostamenti o scambi, prima di arrivare 00 - Introduzione 10/11 alla posizione migliore possibile e quindi definitiva. • Un algoritmo si dice molto efficiente se, in funzione del numero di spostamenti o scambi che gli elementi di un array devono subire, la quantità media di operazioni accessorie (test, assegnazioni, contatori, stacks, chiamate ricorsive) per determinare se uno scambio o spostamento deve essere effettuato è molto bassa. • Le caratteristiche dei due punti precedenti possono coesistere e fondersi nello stesso algoritmo. Per stabilità • La stabilità di un algoritmo di ordinamento è determinata dal fatto che, se i dati ordinati mantengono un eventuale ordinamento precedente, cioè per esempio se si esegue un primo ordinamento secondo una chiave K1, ed eseguendo successivamente un secondo ordinamento con una chiave K2, dove la seconda chiave presenta una sequenza di elementi con valori identici, tali sottoinsiemi di dati hanno mantenuto la precedente proprietà di essere ordinati secondo la chiave K1. Per criterio discriminante • Un algoritmo si dice selettivo orientato al singolo elemento se il procedimento è concepito per identificare ed isolare un solo elemento (o pochi) per volta, grazie ad un criterio discriminante, portandolo a destinazione, con la conseguenza di spostare tutti gli altri elementi di una, nessuna, o poche posizioni verso la loro o al più di disinteressarsene. • Un algoritmo si dice selettivo per sottoinsiemi se il procedimento è concepito per separare porzioni significative di elementi, grazie ad un criterio discriminante, che porta tutti gli elementi appartenenti ai sottoinsiemi nella corretta porzione di array di destinazione, senza però effettuare una immediata associazione tra singoli elementi e singole posizioni di destinazione. Il partizionamento è per esempio un criterio selettivo orientato ai sottoinsiemi. • Un algoritmo non selettivo è invece orientato a muovere grandi quantità di elementi, in taluni casi anche allontanandoli provvisoriamente, durante i vari passaggi, dalla loro posizione definitiva, talvolta anche con procedimenti contro-intuitivi, provvedendo poi a degli aggiustamenti successivi, come per esempio lo shell_sort. Diretto o indiretto • L’ordinamento diretto provvede ad ordinare direttamente gli elementi contenuti nell’array. • L’ordinamento indiretto prevede l’utilizzo di un array ausiliario o di appoggio di puntatori con un numero di elementi di tipo “puntatore ad elemento” identico a quello dell’array da ordinare, oppure di un array ausiliario o di appoggio di indici di spiazzamento. L’ordinamento avviene ricombinando i puntatori o gli indici di spiazzamento, così che, facendo riferimento all’array primario attraverso i puntatori o indici del secondario, l’array primario è di fatto ordinato, anche se in modo “indiretto”. Gestione statica o dinamica • Un algoritmo si dice statico se i dati sono collocati stabilmente e solo all’interno di array la cui occupazione di memoria non varia durante l’intera elaborazione, a prescindere da quante operazioni di spostamento/scambio vengano effettuate sui dati stessi. • Un algoritmo dinamico prevede invece che, in funzione delle esigenze elaborative, i dati siano spostati su aree di memoria che vengono occupate (allocate) quando se ne presenta la necessità, e successivamente liberate (deallocate) quando tale necessità cessa. Successive allocazioni possono avvenire anche su aree di memoria già utilizzate in precedenza, a condizione che il programma abbia già effettuato al momento opportuno una corretta deallocazione. Le liste concatenate rappresentano l’esempio migliore per questo caso. 00 - Introduzione 11/11 Per comportamento • Un algoritmo di ordinamento ha un comportamento naturale di primo livello, come la variante base per l’inserimento, se il tempo dedicato alle operazioni di ordinamento variano in modo approssimativamente proporzionale in funzione della disposizione di partenza dei dati nell’array, cioè lavora di meno in presenza di dati già ordinati o quasi, ed aumenta gradualmente se tale disposizione mostra una distribuzione più vicina alla condizione peggiore, cioè con la massima dispersione, e peggiora ulteriormente in condizioni di parziale o totale rovesciamento dell’ordine. • Un algoritmo di ordinamento ha un comportamento naturale di secondo livello, come per alcune varianti ottimizzate del bubble_sort, se al precedente comportamento si aggiunge la capacità di approfittare della specularità nella disposizione iniziale, cioè secondo cui l’ultimo elemento in posizione N (oppure N-1) dista linearmente di N-1 posizioni dal primo elemento in posizione 1 (oppure 0), e contemporaneamente dista specularmente di 1 posizione (cioè di 1 riflessione) dallo stesso elemento. Per metodo di ordinamento • Si definisce un algoritmo di ordinamento per scambio quando l’operazione prevalente che avviene sui dati prevede che due dati adiacenti (o distanti k posizioni) si scambino di posto se non rispettano la condizione di ordinamento, cioè se l’ordinamento è crescente ed i dati A[j-1] > A[j] (oppure A[j-k] > A[j]), a prescindere da come tali dati si trovino rispetto a tutti gli altri. • Si definisce un algoritmo di ordinamento per selezione quando tutti i dati vengono presi in esame alla ricerca dell’elemento estremo (minore o maggiore di tutti gli altri), per poter essere scambiato una sola volta con l’elemento che ne occupa la posizione designata. • Si definisce un algoritmo di ordinamento per inserzione quando, partendo da un estremo dell’array con un solo elemento iniziale si crea un parziale ordinamento di tutti gli elementi già presi in considerazione, selezionando il primo elemento esterno adiacente e facendolo scorrere all’interno della parte già ordinata fino ad inserirlo temporaneamente nelle sua posizione corretta tale che A[j-1] ≤ A[j] < A[j+1]. • Esistono ulteriori modalità di ordinamento più complesse che sfruttano diverse combinazioni delle tre precedenti, sommate allo sfruttamento di molte tipologie di accorgimenti e soluzioni che ne rendono difficile, se non impossibile, la catalogazione nella casistica summenzionata. PORZIONE DI PAGINA INTENZIONALMENTE VUOTA 01 - Come si disordina un array 1/8 01.0 - Come si disordina un array - Approccio manuale su carta o foglio elettronico Come già accennato nell’introduzione, c’è stato un tempo in cui non possedevo ancora un personal computer, ma soddisfacevo alcune mie curiosità leggendo qualche rivista di settore. Sorvolando su quanto non pertinente e rimanendo “sul pezzo”, gli algoritmi di ordinamento, scritti in basic, erano accompagnati da schemi e disegni che riproducevano celle di memoria sotto forma di riquadri contenenti numeri disposti in modo casuale. Si parlava anche di generatori di numeri pseudocasuali. Quando si presentò l’occasione di dedicarmi agli algoritmi di ordinamento, adottai le funzioni che il linguaggio o l’interprete mi metteva a disposizione, ma in taluni casi non mi bastava scrivere il codice o convertirlo da altri linguaggi, poiché volevo capire nel dettaglio come funzionavano, passo per passo, riproducendo manualmente l’intero procedimento su un array di piccole dimensioni. 10, 15, 20 elementi, non di più, per esigenze di tempo, e per lo spazio a disposizione su un foglio di quaderno. Per esempio evidenziavo a biro alcuni contorni per disegnare delle celle di memoria ideali, in modo simile a questo: Posizione elemento 0 1 2 3 4 5 6 7 8 9 Contenuto celle [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] L’array doveva poi essere riempito con una sequenza di numeri che potesse essere ragionevolmente considerata come casuale. Potevo operare secondo due criteri complementari, mutuamente non escludenti. Potevo scegliere di accedere alle celle in modo sequenziale (0,1,2,3 …) determinando un procedimento che mi proponesse un valore da scrivere che non fosse altrettanto sequenziale, oppure mantenendo un conteggio crescente dei numeri da scrivere nelle celle, spostandomi all’interno dell’array in modo apparentemente casuale. Comprendendo che in un contenitore di soli dieci elementi, non era necessario determinare un procedimento con valori troppo “lontani”, poiché la velocità di esecuzione “manuale” era anche influenzata da quante cifre avrei dovuto riscrivere ad ogni passaggio. Dieci numeri con una sola cifra, da 0 a 9, erano quindi più pratici di dieci presi a caso tra 0 e 102 o tra 103 e 2x103. 01.1 - Primo caso • Valore iniziale scelto a caso, per esempio 5, somma 7, modulo 10 (esempio 5 + 7 = 12 % 10 = 2) • Cella iniziale 0, somma 1, modulo 10 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (0) Avvio sequenza: {CELLA;VALORE} ► {0;5} ► {1;2} ► {2;9} ► {3;6} ►{4;3} ► {5;0} ► {6;7} ► {7;4} ► {8;1} ► {9;8} ► {0;5} ► Fine sequenza Posizione elemento 0 1 2 3 4 5 6 7 8 9 Contenuto celle [5] [2] [9] [6] [3] [0] [7] [4] [1] [8] OK 01.2 - Secondo caso • Valore iniziale 0, somma 1, modulo 10 • Cella iniziale scelta a caso, per esempio 6, somma 7, modulo 10 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (6) Avvio sequenza: {CELLA;VALORE} ► {6;0} ► {3;1} ► {0;2} ► {7;3} ► {4;4} ► {1;5} ► {8;6} ► {5;7} ► {2;8} ► {9;9} ► {6;0} ► Fine sequenza Posizione elemento 0 1 2 3 4 5 6 7 8 9 Contenuto celle [2] [5] [8] [1] [4] [7] [0] [3] [6] [9] OK La scelta di utilizzare il numero 7 è stata dettata da una ovvia serie di motivi. Scegliendo un numero pari la sequenza avrebbe riproposto due volte gli stessi numeri pari, 01 - Come si disordina un array 2/8 escludendo tutti i numeri dispari, sia come valore del contenuto delle celle, sia come posizionamento di cella, cioè: 2-4-6-8-0-2-4-6-8-0 4-8-2-6-0-4-8-2-6-0 6-2-8-4-0-6-2-8-4-0 8-6-4-2-0-8-6-4-2-0 Scegliendo il numero 5, la sequenza sarebbe degenerata in una banale ripetizione di 5 - 0 Con altri numeri dispari la distribuzione non sarebbe stata sufficientemente disordinata, cioè: 1 - 2 - 3 - … è già ordinata in modo crescente 9 - 8 - 7 - … è già ordinata in modo decrescente Restano il 3 ed il 7 che restituiscono sequenze che possono essere considerare identiche eccetto una operazione di rovesciamento, cioè: 3-6-9-2-5-8-1-4-7-0 0-7-4-1-8-5-2-9-6-3 Ho notato che: • I due numeri, 3 e 7, sono primi • Rispetto al numero 10, entrambi i numeri, per completare un “giro”, il valore del modulo assume sempre tutti i possibili valori pari e dispari nell’intervallo considerato • Durante la distribuzione i valori si dispongono in piccole sequenze in cui tre celle contigue assumono valori crescenti o decrescenti, affiancati da altre microsequenze con le stesse caratteristiche, in cui il valore di ogni elemento è sostanzialmente equivalente al corrispondente elemento della microsequenza adiacente, a meno di una differenza di 1, in più o in meno Dovendo eseguire su queste sequenze un ordinamento manuale, allo scopo di poter comprendere meglio i meccanismi interni degli ordinamenti, considerai di aver trovato un metodo sufficiente allo scopo, ma per poterlo valutare in modo più approfondito, rimescolai le carte. 01.3 - Terzo caso • Valore iniziale scelto a caso, per esempio 4, somma 3, modulo 10 • Cella iniziale scelta a caso, per esempio 2, somma 7, modulo 10 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (2) Avvio sequenza: {CELLA;VALORE} ► {2;4} ► {9;7} ► {6;0} ► {3;3} ► {0;6} ► {7;9} ► {4;2} ► {1;5} ► {8;8} ► {5;1} ► {2;4} ► Fine sequenza Posizione elemento 0 1 2 3 4 5 6 7 8 9 Contenuto celle [6] [5] [4] [3] [2] [1] [0] [9] [8] [7] NO La sequenza è spezzata in due, 6-5-4-3-2-1-0 e 9-8-7, ciascuna delle due parti è già ordinata in modo decrescente. 01.4 - Quarto caso • Valore iniziale scelto a caso, per esempio 9, somma 7, modulo 10 • Cella iniziale scelta a caso, per esempio 3, somma 7, modulo 10 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (3) Avvio sequenza: {CELLA;VALORE} ► {3;9} ► {0;6} ► {7;3} ► {4;0} ► {1;7} ► {8;4} ► {5;1} ► {2;8} ► {9;5} ► {6;2} ► {3;9} ► Fine sequenza 01 - Come si disordina un array 3/8 Posizione elemento 0 1 2 3 4 5 6 7 8 9 Contenuto celle [6] [7] [8] [9] [0] [1] [2] [3] [4] [5] NO La sequenza è spezzata in due, 6-7-8-9 e 0-1-2-3-4-5, ciascuna delle due parti è già ordinata in modo crescente. L’utilizzo contemporaneo dei due valori di incremento, a mio avviso migliori, 3 e 7, (7 e 3, 7 e 7, 3 e 3) comportava una reciproca compensazione degli effetti della distribuzione, generando sequenze già ordinate. Sconsigliabile quindi l’uso contemporaneo. Analoghi tentativi con combinazioni di 7 o 3 con uno qualsiasi degli altri numeri portava a sequenze non idonee. Ho comunque voluto valutare se il criterio più semplice, cioè quello del solo 7, del primo e del secondo caso, poteva essere adattato ed applicato su array più grandi. Rispetto al 10, il numero 7 quali caratteristiche mostra? Uno pari, l’altro dispari. Uno prodotto di primi, 2x5, l’altro un primo, e reciprocamente non divisibili. E poi? Rispetto ad una dimensione di 20 celle, devo forse scegliere il 14, cioè esattamente il doppio di 7? È pari, quindi non va bene. I dispari più vicini sono 13 e 15. 15 è multiplo di 5, non va bene. Resta 13. Proviamo. 01.5 - Quinto caso • Valore iniziale 0, somma 1, modulo 20 • Cella iniziale scelta a caso, per esempio 9, somma 13, modulo 20 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (9) Avvio sequenza: {CELLA;VALORE} ► {9;0} ► {2;1} ► {15;2} ► {8;3} ► {1;4} ► {14;5} ► {7;6} ► {0;7} ► {13;8} ► {6;9} ► {19;10} ► {12;11} ► {5;12} ► {18;13} ► {11;14} ► {4;15} ► {17;16} ► {10;17} ► {3;18} ► {16;19} ► {9;0} ► Fine sequenza 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 [07][04][01][18][15][12][09][06][03][00][17][14][11][08][05][02][19][16][13][10] NO Quattro sub-sequenze decrescenti, 7-4-1, 18-15-12-9-6-3-0, 17-14-11-8-5-2, 19-16-13-10. 01.6 - Sesto caso • Valore iniziale 0, somma 3, modulo 20 • Cella iniziale scelta a caso, per esempio 11, somma 13, modulo 20 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (9) Avvio sequenza: {CELLA;VALORE} ► {11;0} ► {4;3} ► {17;6} ► {10;9} ► {3;12} ► {16;15} ► {9;18} ► {2;1} ► {15;4} ► {8;7} ► {1;10} ► {14;13} ► {7;16} ► {0;19} ► {13;2} ► {6;5} ► {19;8} ► {12;11} ► {5;14} ► {18;17} ► {11;0} ► Fine sequenza 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 [19][10][01][12][03][14][05][16][07][18][09][00][11][02][13][04][15][06][13][08] NO Le celle in posizione pari contengono valori dispari in ordine crescente, le celle in posizione dispari contengono valori pari in ordine crescente, con differenza sempre 11 rispetto alla cella adiacente destra e sempre 9 rispetto alla sinistra. 01.7 - Settimo caso • Valore iniziale 0, somma 7, modulo 20 • Cella iniziale scelta a caso, per esempio 4, somma 13, modulo 20 • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (4) 01 - Come si disordina un array 4/8 Avvio sequenza: {CELLA;VALORE} ► {11;0} ► {4;7} ► {17;14} ► {10;1} ► {3;8} ► {16;15} ► {9;2} ► {2;9} ► {15;16} ► {8;3} ► {1;10} ► {14;17} ► {7;4} ► {0;11} ► {13;18} ► {6;5} ► {19;12} ► {12;19} ► {5;6} ► {18;13} ► {11;0} ► Fine sequenza 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 [11][10][09][08][07][06][05][04][03][02][01][00][19][18][17][16][15][14][17][12] NO Utilizzando 7 e 13, la cui somma è 20, come il numero degli elementi dell’array, si ricade nello stesso problema della compensazione reciproca, già identificato in precedenza, e si genera una sequenza decrescente, spezzata in due. Il procedimento approssimativo utilizzato fino a quel momento sembra proporre per lo più risultati deludenti, e deve essere cambiato. Cominciai a sospettare che le limitazioni che mi ero posto fossero la causa principale. Disponendo di un numero limitato N di celle da riempire, numerate da 0 a N-1, e volendole riempire con gli stessi valori compresi tra 0 e N-1, è facile ricadere nelle distribuzioni che non si presentavano sufficientemente casuali. Tentai di escogitare un fattore di disturbo che alterasse il parziale ordinamento presente nelle distribuzioni tentate fino a quel momento. 01.8 - Ottavo caso • Cella iniziale scelta a caso, per esempio 17, somma 1, modulo 20 • Valore iniziale 0, somma 7, somma la posizione della cella corrente, modulo 20. Confronta il valore ottenuto con tutti i precedenti già calcolati, e se necessario somma 1, modulo 20, fino a trovare un valore non ancora utilizzato • Condizione di termine quando la posizione della cella corrente torna uguale alla posizione iniziale (17) Avvio sequenza: {CELLA;VALORE} ► {17;0} ► {18;0 + 7 + 18=25%20=5} ► {19;5 + 7 + 19=31%20=11} ► {0;11 + 7 + 0=18} ► {1;18 + 7 + 1=26%20=6} ► {2;6 + 7 + 2=15} ► {3;15 + 7 + 3=25%20=5 + 1 + 1=7} ► {4;7 + 7 + 4=18 + 1=19} ► {5;19 + 7 + 5=31%20=11 + 1=12} ► {6;12 + 7 + 6=25%20=5 + 1 + 1 + 1=8} ► {7;8 + 7 + 7=22%20=2} ► {8;2 + 7 + 8=17} ► {9;17 + 7 + 9=33%20=13} ► {10;13 + 7 + 10=30%20=10} ► {11;10 + 7 + 11=28%20=8 + 1=9} ► {12;9 + 7 + 12=28%20=8 + 1 + 1 + 1 + 1 + 1 + 1=14} ► {13;14 + 7 + 13=34%20=14 + 1 + 1=16} ► {14;16 + 7 + 14=37%20=17 + 1 + 1 + 1 + 1=21%20=1} ► {15;1 + 7 + 15=23%20=3} ► {16;3 + 7 + 16=26%20=6 + 1 ... =24%20=4} ► {17;…} ► Fine sequenza 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 [18][06][15][07][19][12][08][02][17][13][10][09][14][16][01][03][04][00][05][11] NO Ad esclusione delle sequenze 19-12-8-2 e 17-13-10-9, che presentano un ordinamento decrescente, la distribuzione sembra essere migliorata, anche se a destra ci sono prevalentemente numeri con valori bassi. Per verificare se il fatto sia casuale, riprovo cambiando solo la cella iniziale 01.9 - Nono caso • Tutto come per il caso precedente tranne cella iniziale 6 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 [10][18][12][16][17][19][00][14][09][05][02][01][03][04][06][08][11][15][07][13] ??? In questo caso sembra che i valori più bassi siano prevalentemente al centro dell’array. Potrebbe essere solo una coincidenza. Tento con altre distribuzioni cambiando la cella iniziale. Seguono altre prove con cella iniziale 3, 9, 11, 14. Per agevolarmi il compito, ed evitare di dilungarmi troppo in noiosi ed infruttuosi tentativi, utilizzo un foglio di calcolo. 01 - Come si disordina un array 5/8 01.10 - Foglio elettronico Per ogni fascia di tre colonne, in cui ogni fascia rappresenta un ulteriore caso (casi 10, 11, 12, 13), la prima colonna a sinistra rappresenta la lista dei valori da inserire nell’array. La parte più in alto colorata in grigio indica la verifica fatta, cambiando manualmente il formato colore riga per riga (anche se non necessario), che ogni nuovo elemento calcolato non sia già stato utilizzato in precedenza. La colonna centrale con cifre in grassetto rappresenta il valore reale contenuto in ogni cella. Eccettuata la prima, con valore 0, tutte le altre sono il risultato di una formula. La cella B5 contiene per esempio la seguente formula: =RESTO(RESTO((B4+7+A5);20)+C5;20). La colonna di destra senza contorni contiene il numero di incrementi (inseriti manualmente) del valore della cella per poter determinare un nuovo valore non ancora utilizzato. Per esempio C13 contiene 3. Il calcolo di B13 = B12 + A13 = (17 + 7 + 12) % 20 = 36 % 20 = 16, cioè un valore già utilizzato per B7, corrispondente all’elemento 6 dell’array. Aggiungendo 3 in posizione C3 il calcolo finale risulta 19, cioè il primo numero superiore a 16 non ancora utilizzato. Questo procedimento di verifica può essere utilizzato per array di qualsiasi dimensione, con qualsiasi valore di somma e con qualsiasi posizionamento e valore della cella iniziale, anche se 01 - Come si disordina un array 6/8 diventa poco agevole quando la dimensione dell’array supera i 50 elementi. Per aumentare il numero ad alcune centinaia, e per testare con rapidità la correttezza di un algoritmo di ordinamento, è consigliabile implementare il procedimento con un programma e verificarne l’output. La scelta dell’incremento 7 o 13 per un array di 20 elementi, oppure 3 o 7 per 10 elementi è dovuta alla necessità di determinare un cambiamento di valore o di cella sufficientemente ampio da generare da solo una distribuzione sufficientemente sparsa dei valori nelle celle. Se si suppone che i valori debbano essere scelti in modo che l’incremento non sia un sottomultiplo delle N celle dell’array, si può approssimare tale valore all’intero più vicino ad 1/3 o a 2/3 di N. Tale considerazione però può portare a situazioni anomale, se N é divisibile per 3. Per N = 15, i due possibili valori di incremento diventano 5 e 10, causando un ciclo infinito su valori/celle 0-5-10. Ho optato per il calcolo del valore di un intero prossimo alla sezione aurea, secondo cui, dato N, N / X = X / (N - X). Da cui X2 + NX - N2 = 0, quindi X = (- B ± √(B2 - 4AC)) / 2A. Risultato per N = 1, X1 = 0,6180, X2 = -1,6180. Scartando il valore negativo, dato un qualsiasi valore di N, la sezione aurea è circa il 62% di N. Per N = 10, X = 6,2. Si selezionano i due interi più vicini, arrotondando per difetto e per eccesso, cioè 6 e 7. Se N è pari si sceglie l’intero dispari, se N è dispari si sceglie quello pari. Alcuni esempi: N = 9 ► 5,6 ► 5-6 ► 6 (Caso Particolare, {9,5} dispari, {9,6} massimo comune divisore = 3) N = 15 ► 9,2 ► 9-10 ► 10 (C.P., {15,9} mcd = 3, {15,10} mcd = 5) N = 18 ► 11,1 ► 11-12 ► 11 N = 20 ► 12,4 ► 12-13 ► 13 N = 26 ► 16,1 ► 16-17 ► 17 N = 31 ► 19,2 ► 19-20 ► 20 N = 45 ► 27,8 ► 27-28 ► 28 Per tutti gli N ≤ 18 e multipli di 3, la sezione aurea di N e 2N / 3 hanno valori molto vicini e diventano quindi casi particolari, poiché almeno uno dei due valori interi, per difetto o per eccesso, è uguale a 2N / 3. Per N = 15, caso particolare, poiché 9, 10 o le differenze 6, 5, sono tutti multipli di 3 o di 5, la scelta di uno qualsiasi di questi valori porta sicuramente a distribuzioni con valori o posizioni cicliche. Pertanto l’utilizzo di un fattore di disturbo sulla generazione della sequenza diventa un elemento chiave per realizzare una distribuzione sufficientemente disordinata da sembrare casuale e per evitare le generazioni cicliche. 01.11 - Implementazione software #define ARRAY_SIZE 30 /* valore a discrezione, >50 sconsigliato */ #define MIN_SIZE 4 /* se <4 inutile effettuare distribuzioni */ int verif[ARRAY_SIZE]; /* per verificare valori già utilizzati */ int a2f[ARRAY_SIZE]; /* Array da riempire (Array 2 Fill) */ main() /* programma little_arrays */ { clock_t beg_time = clock(); int arr_ind; for(arr_ind=MIN_SIZE;arr_ind<=ARRAY_SIZE;arr_ind++) { int add_val; if(!((arr_ind + (add_val = (arr_ind*62)/100)) & 1)) add_val++; int pos_ind; for(pos_ind=0;pos_ind<arr_ind;pos_ind++) { int val_ind; for(val_ind=0;val_ind<arr_ind;val_ind++) { int i; for(i=0;i<arr_ind;i++) verif[i]=i; int cur_pos=pos_ind,cur_val=val_ind; i=0; while(i<arr_ind) { 01 - Come si disordina un array 7/8 while(verif[cur_val]==INT_MAX) cur_val=(cur_val+1)%arr_ind; verif[cur_val]=INT_MAX; a2f[cur_pos]=cur_val; i++; cur_pos=(cur_pos+1)%arr_ind; cur_val=(cur_val+add_val+cur_pos)%arr_ind; } } } } for(i=0;i<arr_ind;i++) printf("%2i ",a2f[i]); printf("\n"); } clock_t end_time = clock(); printf("\nsecondi=%.4f\n",((float)(end_time-beg_time))/CLOCKS_PER_SEC); return; C:\>little_arrays > distributions.txt C:\>type distributions.txt ARRAY_SIZE 30 - MIN_SIZE 4 Trattandosi di un generatore per piccoli array, a scopo prevalentemente dimostrativo, è più indicato per disporre di sequenze di 10-15 elementi da ordinare “manualmente”, ed inadatto per studiare i casi con centinaia o migliaia di elementi. L’algoritmo genera solo permutazioni, in cui cioè ogni elemento è presente una sola volta. Le permutazioni di 1, 2, 3, 4, 5, 6, …, n sono 1!, 2!, 3!, 4!, 5!, …, n!, cioè 1, 2, 6, 24, 120, …, 1 * 2 * … * (n - 2) * (n - 1) * n, mentre le sequenze prodotte dall’algoritmo sono n2, cioè rispettivamente 1, 4, 9, 16, 25, …, n2. Quindi il numero di sequenze generate per un numero di elementi n < 4 è maggiore delle permutazioni possibili, mentre per n ≥ 4 le sequenze sono minori delle permutazioni, ma con il difetto di generarne alcune in modo identico, indipendentemente dalla posizione o dal valore di inizializzazione della sequenza. Le due costanti utilizzate sono la diretta conseguenza di queste limitazioni. if(!((arr_ind + (add_val = (arr_ind*62)/100)) & 1)) add_val++; Lo scopo e la struttura di questa istruzione potrebbero risultare poco comprensibili ad un programmatore inesperto, poiché essa contiene diversi accorgimenti in una sola riga di codice. Dall’interno all’esterno: (arr_ind*62)/100 Serve a calcolare un intero prossimo alla sezione aurea del numero corrispondente agli elementi dell’array, ogni volta che l’algoritmo ne incrementa il valore. L’istruzione, matematicamente corretta, non necessiterebbe di parentesi per determinare eventuali priorità nei calcoli da eseguire. Se trasformata in formula Excel (=(riga:colonna)*62/100) restituisce il valore corretto anche cambiando di posizione ai vari componenti, poiché esegue implicitamente calcoli in virgola mobile, anche se il formato di visualizzazione è impostato su zero decimali. Un compilatore invece si attiene alle convenzioni stabilite in fase di 01 - Come si disordina un array 8/8 dichiarazione delle variabili. L’istruzione arr_ind*(62/100), per esempio, verrebbe eseguita in questo modo: 62 (intero) diviso 100 (intero) uguale 0 (intero), cioè (62 / 100) = 0, quindi arr_ind * 0 = 0. Per un risultato corretto occorre questa modifica: (int)(arr_ind*((float)62/100)), cioè: converti 62 in 62,0 (virgola mobile), converti 100 in 100,0 (virgola mobile), 62,0 diviso 100,0 uguale 0,62 (virgola mobile), converti arr_ind in valore equivalente in virgola mobile, moltiplicalo per 0,62, poi converti il risultato in intero (int). La formulazione utilizzata nell’istruzione if è l’unica che garantisce il risultato corretto, forzando il compilatore ad eseguire le operazioni nell’ordine voluto, a prescindere da quali siano le priorità di default, e senza dover convertire i vari elementi per il calcolo in virgola mobile (float). add_val = Il valore calcolato della sezione aurea determina anche il valore di incremento che sarà utilizzato più volte e deve essere mantenuto in una propria variabile. arr_ind + add_val La dimensione dell’array ed il valore di incremento devono essere mutuamente diversi, cioè sempre uno pari e l’altro dispari. La somma dei due, quindi sempre dispari. Se il risultato è pari, entrambi sono pari o entrambi dispari. (!( (arr_ind + add_val) & 1)) add_val++; Utilizzando l’operatore su bit &, è possibile isolare la cifra binaria meno significativa, per determinare se la somma dei due numeri è pari o dispari. Solo se è pari (cioè falsa), occorre modificare il valore di una delle due variabili, affinché la somma sia dispari. Grazie all’operatore logico not, corrispondente al punto escalmativo, si inverte il risultato del confronto if, determinando l’aumento di 1 di add_val. for(i=0;i<arr_ind;i++) verif[i]=i; Durante l’elaborazione occorre utilizzare un accorgimento per escludere tutti i valori già utilizzati, pertanto all’inizio di ogni ciclo l’array “verif” deve essere ogni volta re-inizializzato. while(verif[cur_val]==INT_MAX) cur_val=(cur_val+1)%arr_ind; Se il valore calcolato corrisponde ad un valore già utilizzato si esegue una ricerca, spostandosi da una posizione a quella adiacente, fino a trovarne uno nuovo. Il nuovo valore corrente potrebbe eccedere il massimo consentito, e se ne calcola il modulo, per rimanere entro i limiti massimi imposti. verif[cur_val]=INT_MAX; a2f[cur_pos]=cur_val; Il valore corrente viene inserito nell’array da riempire, ma contemporaneamente deve essere inserito nella lista di quelli già usati. Fatte le dovute considerazioni riguardo alle limitazioni sul tipo di approccio “manuale” e sui difetti e lacune del metodo illustrato, ritengo comunque di aver suggerito un metodo ragionevolmente interessante per produrre un certo numero di sequenze pseudo-casuali per piccoli array da utilizzarsi in esempi di ordinamento manuale o su carta. La scelta di quali sequenze scartare o utilizzare è discrezionale. Tale metodo può risultare un poco macchinoso, se confrontato con un semplice “scrivi a mano i numeri così come ti vengono in mente, e se non va bene li cancelli e li riscrivi spostati”, ma lo sforzo necessario ad individuare un procedimento deterministico generalizzato anche per casi relativamente semplici è pur sempre un utile apprendistato per comprendere le difficoltà di realizzare un generatore di numeri pseudo-casuali per array di grandi dimensioni. 02 - Analisi di un GNPC di riferimento 1/5 02.0 - Analisi di un generatore di numeri pseudo-casuali Nota: Non è nelle intenzioni di questo testo, orientato ad aspetti pratici, proporre una trattazione teorica completa dei criteri di valutazione per i generatori di numeri pseudo-casuali. Le argomentazioni seguenti hanno lo scopo di riproporre in modo personalizzato alcuni criteri di riferimento per elaborare un generatore pseudo-casuale ad hoc, finalizzato allo studio delle prestazioni degli algoritmi di ordinamento che saranno trattati nei successivi capitoli. La libreria standard ANSI C contiene queste due funzioni: unsigned long int next = 1; int rand(void) /* ritorna un numero pseudo-casuale compreso tra 0 e 32767 */ { next = next * 1103515245 + 12345; return (unsigned int) (next/65536) % 32768; } void srand (unsigned int seed) /* inizializza seme per rand */ { next = seed; } Tabella: riduzione a numeri primi dei numeri utilizzati dalla funzione rand() 1103515245 3 367838415 3 122612805 3 40870935 3 13623645 3 4541215 5 908243 7 129749 129749 1 12345 3 4115 5 823 823 1 Tali funzioni rispecchiano il metodo attualmente più diffuso per generare numeri pseudo-casuali. La formulazione generale è la seguente: V0 = Nini ► Vn = ((K * Vn-1) + D) mod M Ovvero: Valore iniziale (V0) = numero arbitrario (Nini) CICLO - Per ogni iterazione: • determinato un valore numerico V nella iterazione precedente (Vn-1) • si moltiplica V per una costante K • si somma il risultato ad un fattore di disturbo D • si divide il risultato per il modulo M (resto della divisione) • si colloca il risultato corrente Vn in V • Se non si è raggiunto il numero massimo di iterazioni richieste salta a CICLO. Fine generazione. Il procedimento indicato non è matematicamente ineccepibile, poiché una scelta non corretta dei valori K, D ed M può portare a risultati imprevedibili ed indesiderati, quali per esempio la ripetizione di cicli di numeri sempre uguali, in cui la quantità di numeri generati non copre mai una porzione ragguardevole dei numeri nell’intervallo considerato, oppure, dopo l’insorgenza di valori numerici particolari, la degenerazione in cicli più stretti e composti da una quantità di numeri molto inferiore a quanto ci si aspetti. Secondo alcune fonti, per ottenere una sequenza ampia, K deve essere molto grande. Secondo altre deve essere il valore M del modulo, ad essere molto grande, per garantire questo risultato. La mia esperienza personale mi porta a credere che i tre valori possano anche appartenere allo stesso ordine di grandezza, a condizione che non ci sia relazione di molteplicità tra loro, ovvero che, dividendo in virgola mobile ciascuno dei tre numeri con uno qualsiasi degli altri due, il risultato non abbia degli zeri dopo poche cifre decimali. Possibilmente il risultato deve contenere cifre decimali senza periodo o con periodo lungo. Dopodiché, occorre procedere per tentativi. 02 - Analisi di un GNPC di riferimento 2/5 02.1 - Programma di analisi statistica semplificata Ho elaborato un programma che permette di effettuare alcuni test statistici sulla funzione rand(). La funzione srand(), in tutte le prove i cui risultati sono riportati nelle tabelle seguenti, viene inizializzata a 1˙000, per permettere la riproducibilità dei risultati. Altri valori di inizializzazione portano comunque a risultati confrontabili con quelli indicati. #define SEED 1000 #define MAX_J 1 typedef unsigned long int uli; main(){ /* Programma prova_rand_ansi.c */ uli num_elem=RAND_MAX+1,cnt[num_elem],i,j; uli range=RAND_MAX+1,occurr=8192,repeat=range*occurr; uli min=INT_MAX,max=0,average=0,dif=0; float chi2=0.0; srand(SEED); for(i=0;i<num_elem;i++) cnt[i]=0; for(j=0;j<MAX_J;j++) for(i=0;i<repeat;i++) cnt[rand()]++; for(i=0;i<num_elem;i++) { average=average+abs(dif=cnt[i]-(occurr*MAX_J)); chi2=chi2+((float)(dif*dif)/(occurr*MAX_J)); if(cnt[i]<min) min=cnt[i]; else if(cnt[i]>max) max=cnt[i]; } printf("Su %.0f iteraz. m.a. occorr.=%i\n",(float)repeat*MAX_J,occurr*MAX_J); printf("Occorr. min=%li max=%li - campo var.=%li\n",min,max,max-min); printf("Scarto semplice medio=%.2f\n",(float)average/num_elem); printf("Sc. semp. medio su m.a.=%.2f%%\n",100.*average/num_elem/(occurr*MAX_J)); printf("Sc. semp. medio su c.v.=%.2f%%\n",100.*average/num_elem/(max-min)); printf("CHI2=%.2f CHI2 medio=%.5f\n",chi2,chi2/num_elem); } In tabella: m.a. = Media Attesa delle Occorrenze, cioè quante volte un numero dovrebbe riproporsi durante la generazione min.o. = Numero minimo delle occorrenze di un numero ottenuto per conteggio max.o. = Numero massimo delle occorrenze di un numero ottenuto per conteggio c.v. = Campo di Variazione (max-min) delle occorrenze ottenute per conteggio s.s.m. = Scarto Semplice Medio delle occorrenze s.m. / m.a. = Calcolo % del valore di Scarto Semplice Medio in rapporto alla Media Attesa s.m. / c.v. = Calcolo % del valore di Scarto Semplice Medio in rapporto al Campo Variazione chi2 = Valore totale di CHI Quadro su 32˙768 valori possibili chi2 md = Valore medio di CHI Quadro su 32˙768 valori possibili Tabella: prime 40 potenze di 2 2^0=1 2^1=2 2^2=4 2^3=8 2^4=16 2^5=32 2^6=64 2^7=128 2^8=256 2^9=512 2^10=1˙024 2^11=2˙048 2^12=4˙096 2^13=8˙192 2^14=16˙384 2^15=32˙768 2^16=65˙536 2^17=131˙072 2^18=262˙144 2^19=524˙288 2^20=1˙048˙576 2^21=2˙097˙152 2^22=4˙194˙304 2^23=8˙388˙608 2^24=16˙777˙216 2^25=33˙554˙432 2^26=67˙108˙864 2^27=134˙217˙728 2^28=268˙435˙456 2^29=536˙870˙912 2^30=1˙073˙741˙824 2^31=2˙147˙483˙648 2^32=4˙294˙967˙296 2^33=8˙589˙934˙592 2^34=17˙179˙869˙184 2^35=34˙359˙738˙368 2^36=68˙719˙476˙736 2^37=137˙438˙953˙472 2^38=274˙877˙906˙944 2^39=549˙755˙813˙888 Dalla tabella sottostante si notano subito alcune caratteristiche sul funzionamento della funzione. Quando il numero delle iterazioni è ancora basso (215), il Chi quadro ha già raggiunto un valore (0.98) tale da indicare che la sequenza fino al momento generata ha una elevata probabilità di essere considerata casuale, però, in riferimento alla Media Attesa delle Occorrenze, c’è un Campo di Variazione elevato, cioè una notevole quantità di numeri che vengono riproposti molte volte insieme ad altri che non vengono mai generati. La colonna percentuale di Scarto Semplice Medio su Media Attesa riporta valori molto elevati, che via via diminuiscono al crescere delle iterazioni. L’unico valore stabile sembra essere lo Scarto Semplice Medio, che si attesta intorno al 10% 02 - Analisi di un GNPC di riferimento 3/5 rispetto al Campo di Variazione. La vera sorpresa è rappresentata dalle ultime righe della tabella seguente, in cui il Valore Atteso delle Occorrenze è uguale al minimo e massimo riscontrati, con la conseguenza di azzerare lo Scarto Semplice Medio, il suo rapporto con il Campo Variazione, ed il valore di Chi quadro e Chi quadro medio. Tabella risultati per prova_rand_ansi.c: rand() - progressione geometrica del numero di iterazioni * 2 con m.a. da 1 a 131˙072. Altri valori non in progressione sono evidenziati in grassetto. Iterazioni 215 216 m.a. 20 21 min.o. 0 0 max.o. 8 10 c.v. 8 10 s.s.m. 0.73 1.08 s.m./m.a. % 72.97 % 54.25 s.m./c.v. % 9.12 % 10.85 chi2 32˙234.00 32˙823.00 chi2 md 0.98 1.00 217 218 219 22 23 24 0 0 2 15 22 35 15 22 33 1.56 2.24 3.17 % 39.05 % 28.04 % 19.81 % 10.41 % 10.20 % 9.61 32˙845.50 33˙007.25 32˙674.50 1.00 1.01 1.00 220 25 13 60 47 4.50 % 14.05 % 9.57 32˙737.94 1.00 221 26 34 101 67 6.36 % 9.94 % 9.49 32˙694.78 1.00 222 27 223 224 28 29 86 189 413 178 336 611 92 147 198 8.99 12.72 18.02 % 7.03 % 4.97 % 3.52 % 9.77 % 8.65 % 9.10 32˙558.69 32˙619.38 32˙738.69 0.99 1.00 1.00 225 210 905 1˙165 260 25.43 % 2.48 % 9.78 32˙452.70 0.99 226 211 1˙861 2˙236 375 35.71 % 1.74 % 9,52 31˙991.37 0.98 227 212 3˙843 4˙354 511 49.83 % 1.22 % 9.75 31˙284.97 0.95 228 213 7˙865 8˙554 689 68.15 % 0.83 % 9.89 29˙209.61 0.89 229 214 15˙937 16˙829 892 88.64 % 0.54 % 9.94 24˙745.35 0.76 230 215 229+230 214+215 231 216 32˙206 48˙707 65˙536 33˙330 49˙599 65˙536 1˙124 892 0 102.60 88.64 0.00 % 0.31 % 0.18 % 0.00 % 9.13 % 9.94 #DIV/0! 16˙545.29 8˙247.93 0.00 0.50 0.25 0.00 230+231 215+216 97˙742 98˙866 1˙124 102.60 % 0.10 % 9.13 5˙515.01 0.17 232 217 131˙072 131˙072 0 0.00 % 0.00 #DIV/0! 0.00 0.00 231+232 216+217 196˙608 196˙608 0 0.00 %0.00 #DIV/0! 0.00 0.00 Probabilmente, utilizzando l’istruzione repeat=range*occurr per facilitarmi il lavoro, dato che la variabile occurr contiene già il valore atteso delle occorrenze, senza doverlo calcolare, ho avuto la possibilità di notare con facilità qualcosa che sarebbe potuto sfuggirmi se il numero di iterazioni fosse stato determinato in modo diverso. La congettura da verificare quindi è la seguente: nella funzione rand() l’istruzione next = next * 1103515245 + 12345; esegue un ciclo chiuso con periodo 2˙147˙483˙648, e l’istruzione return (unsigned int) (next/65536) % 32768; ridistribuisce i valori in un range più piccolo, in cui i 32˙768 valori compresi tra 0 e 32˙767 si ripetono esattamente fino a 65˙536 volte, ma con un andamento asimmetrico, in cui alcuni valori si ripetono con maggiore frequenza all’inizio delle iterazioni, e tale asimmetria diventa speculare all’approssimarsi della fine del periodo, rendendo maggiormente frequenti quei valori che inizialmente lo erano di meno. Nelle ultime righe il valore del chi2 si abbassa fino a zero. Non disponendo di valori intermedi tra 32˙768 e 65˙536 iterazioni e oltre, ho verificato con valori intermedi in prossimità di 65˙536 e indicato in altre due tabelle se la tendenza manifestata è reale. 02 - Analisi di un GNPC di riferimento 4/5 Tabelle: rand() - progressione m.a. da 30˙000 a 80˙000 incremento 10˙000 + altro evidenziato rand() - progressione m.a. da 62˙000 a 68˙000 incremento 2˙000 + altro evidenziato Iterazioni 3 x 104 x 215 4 x 104 x 215 5 x 104 x 215 6 x 104 x 215 216 x 215 7 x 104 x 215 8 x 104 x 215 m.a. 3 x 104 4 x 104 5 x 104 6 x 104 216 7 x 104 8 x 104 min.o. 29˙440 39˙415 49˙570 59˙707 65˙536 69˙738 79˙563 max.o. 30˙501 40˙535 50˙435 60˙305 65˙536 70˙274 80˙413 c.v. 1˙061 1˙120 865 598 0 536 850 s.s.m. 102.35 99.44 87.02 56.63 0.00 51.85 85.01 s.m./m.a. % 0.35 % 0.25 % 0.17 % 0.09 % 0.00 % 0.07 % 0.11 s.m./c.v. % 9.65 % 8.88 % 10.06 % 9.47 #DIV/0! % 9.67 % 10.00 chi2 17˙972.23 12˙762.01 7˙801.83 2˙766.09 0.00 1˙982.27 4˙667.12 chi2 md 0.55 0.39 0.24 0.08 0.00 0.06 0.14 Iterazioni 62 x 103 x 215 64 x 103 x 215 216 x 215 66 x 103 x 215 68 x 103 x 215 m.a. 62 x 103 64 x 103 216 66 x 103 68 x 103 min.o. 61˙766 63˙818 65˙536 65˙900 67˙790 max.o. 62˙249 64˙140 65˙536 66˙091 68˙237 c.v. 483 322 0 191 447 s.s.m. 46.19 30.85 0.00 17.14 39.08 s.m./m.a. % 0.07 % 0.05 % 0.00 % 0.03 % 0.06 s.m./c.v. % 9.56 % 9.58 #DIV/0! % 8.98 % 8.74 chi2 1˙773.42 763.46 0.00 229.75 1˙158.78 chi2 md 0.05 0.02 0.00 0.01 0.04 Le prove eseguite confermano la tendenza ipotizzata. #define SEED 4000000000.0 /* valore arbitrario */ #define ARBT1 2100000000.0 /* valore arbitrario */ #define ARBT2 ARBT1 + INT_MAX + 1 typedef unsigned long int uli; uli i=INT_MAX,j=INT_MAX,val,next=(uli)SEED; uli ver_rand(void){return(uli)next=next*1103515245+12345;} void stampa(int test) { printf("test su valore arbitrario n. %i \n",test); printf("ind=%.0f ",(float)i); printf("mod(%.0f)=",(float) (((uli)INT_MAX)+1)); printf("%.0f\n",(float)(i% (((uli)INT_MAX)+1))); printf("num=%.0f mod(%i)=%.0f\n",(float)val,32768,(float)(val%32768)); return; } main() /* programma verifica_periodo_rand.c */ { while(1) { if((j=i) > ++i) printf("overflow %.0f %.0f\n",(float)i,(float)j); if(i==(uli)ARBT1) stampa(1); if(i==(uli)ARBT2) stampa(2); if( (val=ver_rand()) == SEED) break; } printf("indice=%.0f ultimo generato=%.0f\n",(float)(uli)i,(float)val); return; } La combinazione delle tre istruzioni #define SEED 4000000000.0, uli next=(uli)SEED; e uli ver_rand(void){return(uli)next=next*1103515245+12345;} rappresentano la riduzione ai minimi termini delle due funzioni rand() e srand(). Nel programma si è fatto ampio uso di variabili “unsigned long int” e variabili in virgola mobile, con conversioni di formato. La scelta è stata necessaria poiché l’utilizzo di numeri molto grandi comporta frequenti reazioni indesiderate, sia per gli indici, sia per i formati di output. Gli indici i e j vengono inizializzati ad un valore già alto, INT_MAX, cioè 231-1 “unsigned long int”, la cui mappatura binaria corrisponde al valore 0 per “long int”. L’originalità della soluzione è deliberata, poiché durante l’iterazione, gli indici arrivano al massimo valore possibile UINT_MAX, cioè 232-1, ed al passo successivo, con l’overflow non segnalato, gli indici assumono valore negativo INT_MIN, cioè -231 per “long int”, ovvero UINT_MIN, cioè “0” per “unsigned long int”, dopodiché il ciclo 02 - Analisi di un GNPC di riferimento 5/5 termina avendo nuovamente generato un numero identico al seme iniziale. Indipendentemente dalla scelta del valore arbitrario SEED, il ciclo di generazione si conclude dopo 232 iterazioni, cioè con un periodo esattamente doppio rispetto a quello previsto. Aggiungendo ARBT1 e ARBT2, corrispondenti rispettivamente ad un primo test arbitrario su una qualunque delle iterazioni, ed un secondo test esattamente 231 iterazioni dopo il primo, cioè mezzo periodo, si può notare che al primo periodo grande se ne sovrappone un altro con periodo 1/2 rispetto al precedente. Eseguendo nel programma le opportune operazioni di modulo su indice e valore generato, si può concludere che la funzione rand() genera nel primo “semiperiodo grande” numeri pseudocasuali con una distribuzione tale da aumentare progressivamente il Campo di Variazione delle Occorrenze, mentre nel secondo semiperiodo la “mappatura“ dei valori si inverte, causando la riduzione progressiva del Campo di Variazione fino al valore zero, ma tale distribuzione non è facilmente rilevabile dopo che la funzione rand() ha restituito il numero generato, cioè dopo aver diviso per 216 ed effettuato il modulo %215. PORZIONE DI PAGINA INTENZIONALMENTE VUOTA 03 - Impostazione di un GNPC 1/10 03.0 - Metodo “relativamente” semplice per impostare un generatore pseudo-casuale Con riferimento alla formula per un generatore pseudo-casuale già indicata, dove si applica il metodo della congruenza lineare, si propone la seguente variante, finalizzata ad un simulatore di generatore pseudo-casuale su foglio Excel: Vn = ((K * Vn-1 + D) mod M (formula base per la congruenza lineare), in cui D rappresenta l’incremento che genera un fattore di disturbo ed impedisce al ciclo iterativo di degenerare in un numero esiguo di valori. Si pone D = K * Di + De, spezzando il fattore di disturbo in due disturbi separati, uno “interno” che viene incorporato nella moltiplicazione, ed uno esterno che ne resta escluso, da cui Vn = ((K * Vn-1 + K * Di + De) mod M Supponendo inoltre di voler separare il numero degli stati possibili del generatore (modulo overflow), dal numero di stati su cui fare i test statistici (modulo M), la formula diventa V0 = Nini ► Vn = ((K * (Di + Vn-1) + De) mod OVF) mod M Le principali differenze: • si lavora in doppia precisione con segno positivo, invece che su interi senza segno a 32 bit. • Il valore calcolato precedentemente viene sommato ad un fattore di disturbo Di prima di essere moltiplicato. Si stabilisce quindi la distinzione tra fattori di disturbo interno ed esterno, Di e De. • Quando il valore calcolato in doppia precisione supera un determinato valore di overflow, si esegue una prima operazione di modulo OVF. Nella seguente tabella Excel viene illustrato un metodo per valutare con facilità e rapidamente una grande quantità di possibili valori per Nini, Vn, K, Di, De, OVF ed M. 03.1 - GNPC - impostazione manuale con formule su foglio Excel File: Generatore numeri pseudo-casuali.xls 03 - Impostazione di un GNPC 2/10 Nei riquadri di sinistra i dati di input • Overflow: a seconda che si desideri realizzare un programma che lavora su 2 o 4 bytes, con o senza segno. La scelta dipende dal tipo di variabili che si intende usare per implementare la versione software del generatore. • Seme: può essere modificato a piacere. Per evitare che i primi numeri generati nella colonna D non siano ottimali, si consiglia di iniziare con un numero piuttosto grande, dello stesso ordine di grandezza di “overflow”. Si consiglia di scegliere affinché alla prima iterazione seme * K > overflow. • K: moltiplicatore nella formula. • Disturbo interno ed esterno: come da formula. • Modulo: si è scelto di usare un modulo relativamente piccolo, poiché uno più grande avrebbe reso necessario l’utilizzo di più righe nelle colonne F, G, H, I, ed allungando i tempi per aggiornare il foglio di calcolo ad ogni cambio di un qualsiasi valore. Al centro le colonne di calcolo • Colonna D: calcolo del successivo. Ad eccezione della prima posizione (D5) che fa riferimento al seme (seed:B7), tutte le altre fanno riferimento alla cella soprastante. • Colonna E: output, cioè il numero intero che verrebbe restituito dalla funzione di un programma dopo aver eseguito l’operazione di modulo sul valore della cella in colonna D. • In generale, per le colonne D ed E, la scelta di quante celle utilizzare (104 piuttosto che tutte le 216 disponibili) dipende dal compromesso tra due esigenze: l’accuratezza delle valutazioni ed il tempo di aggiornamento. Ogni volta che si modifica uno qualunque dei valori (o celle) utilizzate nella formula, sul computer utilizzato per queste prove occorrono circa 15-18 secondi per aggiornare 216 righe, e circa 2,5-3 secondi per 104. • Colonna F: con modulo 103 i valori possibili variano da 0 a 103-1. • Colonna G: conta le occorrenze di un numero, cioè quante volte un numero tra 0 e 103-1 è presente nella colonna E. • Colonna H: la media delle occorrenze rappresenta il valore medio atteso delle occorrenze. Si calcola lo scarto semplice per ogni valore dell’intervallo considerato. • Colonna I: Chi Quadrato, cioè scarto semplice al quadrato diviso valore atteso delle occorrenze. A destra i riquadri con i risultati su cui fare le valutazioni • Totale Occorrenze: deve corrispondere al numero di celle utilizzate nella colonna D. • Media = Valore atteso delle occorrenze: Totale Occorrenze diviso Modulo. • Occorrenze: Minimo, Massimo, Campo di Variazione (Max-Min) • Scarto Semplice Medio: Sommatoria di tutti gli scarti semplici diviso Modulo. • Scarto semplice medio su Campo di Variazione. • Chi Quadrato medio: Sommatoria di tutti i Chi quadrato della colonna I diviso Modulo. Generatore numeri pseudo-casuali.xls - inserisci > nome > incolla > incolla elenco chi_quad chi2_col exp_val ext_dist int_dist K max_occ =GNPC!$L$12 =GNPC!$I$5:$I$1004 =GNPC!$L$6 =GNPC!$B$11 =GNPC!$B$10 =GNPC!$B$9 =GNPC!$L$8 obs_occ out_col overflow poss_val_col seed st_dev st_dev_ave =GNPC!$G$5:$G$1004 =GNPC!$E$5:$E$10004 =GNPC!$B$5 =GNPC!$F$5:$F$1004 =GNPC!$B$7 =GNPC!$H$5:$H$2004 =GNPC!$L$10 min_occ =GNPC!$L$7 tot_occ =GNPC!$L$5 mod =GNPC!$B$13 var_fie =GNPC!$L$9 03 - Impostazione di un GNPC 3/10 Nell’intento di facilitare la comprensione delle formule per i meno esperti, ho pensato di dare un nome ad alcune zone ed alcune celle, con riferimento all’uso delle variabili contenute nelle celle stesse. Per esempio: Foglio1 > rinomina > GNPC. File: Generatore numeri pseudo-casuali.xls - parte sinistra - formule evidenziate File: Generatore numeri pseudo-casuali.xls - parte destra - formule evidenziate 03.2 - Valutazioni e parametri Valutazioni: Se si desidera costruire un generatore di numeri pseudo-casuali moltiplicativo, cioè basato sul fattore K, i migliori sono quelli che danno un risultato di Chi Quadrato (cella chi2) prossimo a 1, compreso tra 0.99 e 1.01, poiché danno delle distribuzioni di numeri che hanno una “buona probabilità” di essere “considerate” casuali. Sono accettabili comunque valori compresi tra 0.90 e 1.10. Lo Scarto Semplice Medio deve avere valori che non si discostano oltre al il 10-15% del campo di variazione. Nella sequenza di colonna E (output), i numeri già disposti in ordine crescente o decrescente possono essere talvolta disposti in piccole sequenze che solo occasionalmente superano i 4-5 elementi. Occorre comunque effettuare diverse prove cambiando il valore del seme iniziale, prima di passare ad una implementazione software. Da evitare K intero pari. La sequenza degenera entro poche decine di passaggi verso un solo singolo numero che si ripete indefinitamente, anche se si utilizzano dist-int e dist-est. Esempio: K = 6 03 - Impostazione di un GNPC 4/10 Per K = 1 si ricade nel caso dei generatori additivi, descritti poco più avanti. Con K intero dispari diverso da 1, in alcuni casi occorre compensare sfruttando il dist-int e dist-est. Il valore di Chi Quadrato medio prevalentemente è fuori range, e la ricerca di fattori di disturbo che possano riportarlo a valori accettabili è complessa. Esempio: K = 87˙654˙321 L’utilizzo di almeno una cifra decimale in K genera buone sequenze, senza bisogno di dist-int e dist-est. Evitare i K con decimali ma con valori troppo vicini ad 1.0. Esempio: K = 1,00001 Se si vuole costruire un generatore di numeri pseudo-casuali di tipo additivo, ci si deve aspettare che la generazione non rispetti i canoni che si possono dare per scontati per la maggior parte dei generatori moltiplicativi. Si pone obbligatoriamente K = 1, affinché la porzione di formula K * (Di + Vn-1) + De possa essere semplificata in 1*(Di + Vn-1) + De cioè Vn-1 + Di + De, cioè Vn-1 + Di+e. Un qualsiasi valore posto indifferentemente in dist-int o dist-est provoca lo stesso risultato. Due valori distinti si sommano. I generatori migliori sono dotati di un fattore di disturbo N * modulo + X, in cui N * modulo è superfluo, poiché viene eliminato spontaneamente dalla successiva operazione di modulo, mentre per X il valore consigliato è compreso in una zona intermedia tra modulo/3 e modulo/2, oppure tra modulo/2 e modulo * 2/3. Usando i valori consigliati si producono piccole sequenze di 2-3 numeri crescenti nel primo caso (X ≈ modulo * 5/12), decrescenti nel secondo (X ≈ modulo * 7/12). L’alternanza di numeri alti e bassi ed apparentemente non correlati tra loro è paragonabile ad una sequenza pseudo-casuale. In un grafico su Excel in cui l’asse delle x rappresenta la posizione nell’array, l’asse delle y il suo valore, è chiaramente visibile una trama regolare a “trapunta” o con allineamenti periodici, da cui si deduce facilmente l’origine additiva del generatore. Evitare valori di disturbo troppo vicini a modulo/ 2, poiché si genera una alternanza di valori appartenenti a due sequenze, entrambe crescenti o decrescenti (es: 430-933-436-939-442-945-448-951-454-957-460). Esempio: K=1, dist_int = 489 03 - Impostazione di un GNPC 5/10 Evitare valori di disturbo troppo vicini al valore del modulo (2/3 < X < 1), o allo zero (0 < X <1/3), poiché si generano lunghe sequenze di numeri crescenti o decrescenti, la cui lunghezza è maggiore quanto più ci si approssima ai valori estremi. Esempio: K=1, dist_int = 301 Evitare sottomultipli esatti del modulo o valori con un comune massimo divisore molto alto. Per esempio, modulo 103 e disturbo 60 non va bene, poiché il massimo comune divisore è 20, troppo alto (con modulo 103 evitare tutti i multipli di 2 e 5). Nota: le versioni deliberatamente “difettose” possono essere utilizzate per testare algoritmi di ordinamento, per vedere come si comportano nella condizione anomala dovuta alla presenza di molte occorrenze di solo alcuni dei numeri nell’intervallo di interesse. Quando la scelta è relativamente azzeccata, nei riquadri di destra sul foglio Excel si notano queste caratteristiche: il minimo ed il massimo delle occorrenze sono uguali o molto vicini, il campo di variazione è zero o molto basso, ed il Chi Quadrato Medio è zero. Il grafico della distribuzione è sempre a “trapunta” (gli allineamenti avvengono per le soluzioni non azzeccate). L’utilizzo di cifre decimali nei fattori di disturbo, applicato anche nelle condizioni migliori, causa solo un leggero peggioramento sulla distribuzione delle occorrenze dei numeri generati (min, max, campo variazione), ed un miglioramento trascurabile nel Chi Quadrato Medio. Il metodo additivo può essere descritto anche dalla seguente formula generalizzata. Vn = ( (K1 * Vn-1) + (K2 * Vn-2) + … + (Kj * Vn-j) + D) mod M Dato l’insieme da un numero J di fattori di moltiplicazione K1, K2, …, Kj, per ciascuno dei valori Vn-1, Vn-2, …, Vn-j calcolati precedentemente, ponendo L fattori a 0, ed altri J-L = 1. Ogni valore numerico generato è quindi ottenuto ricalcolandolo in funzione della somma di una determinata quantità J-L di valori già determinati in precedenza. L’implementazione non è complessa, ma è costosa in tempo di elaborazione, poiché ad ogni valore (Kx * Vn-x) calcolato sono associate J-L moltiplicazioni e J-L addizioni. La scelta effettuata più avanti in questo testo è invece orientata alla realizzazione di procedimenti ottimizzati, quindi veloci, quindi con un numero limitato di operazioni e calcoli. 03.3 - Esempio con propagazione dei decimali In base alle considerazioni appena effettuate, la scelta delle caratteristiche del generatore pseudocasuale che verrà utilizzato in larga parte del presente testo, ricade su un generatore additivo con caratteristiche peculiari e fino a questo momento non ancora illustrato. Solo a scopo esemplificativo e per completezza del metodo, si propone un generatore moltiplicativo senza disturbo esternointerno, e con valore K in virgola mobile, con almeno una cifra decimale. La formula è la seguente: Vn = ((K * Vn-1) + ▼) mod M, in cui “▼” non è un numero, ma rappresenta un disturbo intrinseco di approssimazione per arrotondamento, cioè generato dalla progressione dei decimali, che, raggiunto il limite dei bit disponibili nella mantissa, subiscono un processo forzato di arrotondamento da parte del processore che esegue le operazioni in virgola mobile, rendendo 03 - Impostazione di un GNPC 6/10 parzialmente imprevedibile il comportamento del generatore, con la conseguenza di rendere potenzialmente “indeterminata” la lunghezza del periodo, a prescindere dal seme iniziale o dal valore di K. File: Generatore numeri pseudo-casuali.xls - foglio “Propagazione decimali” Nell’esempio precedente, nelle colonne a sinistra, il seme 1 viene ripetutamente moltiplicato per 5.1. Inizialmente i decimali aumentano, ma quando la parte intera del valore calcolato supera in grandezza una determinata soglia, le cifre meno significative vengono via via sacrificate facendo nuovamente aumentare le cifre decimali con valore 0. La linea verticale evidenzia la posizione della virgola. Nelle colonne di destra, grazie all’operazione modulo 104, il valore calcolato resta sufficientemente basso da mantenere un elevato numero di cifre decimali, con i conseguenti errori di arrotondamento, che si ripercuotono sui numeri successivi. È doveroso fare alcune precisazioni. Gli arrotondamenti effettuati da un foglio di calcolo non sono necessariamente equivalenti a quelli effettuati con variabili in virgola mobile in singola o doppia precisione di un programma, quindi la simulazione su Excel ed una implementazione software possono dare risultati diversi. 03.4 - Applicazione software con propagazione dei decimali #define #define #define #define #define typedef OVF 2147483648.0 K 1.61 MOD 32768 SEED 17482248.0 MAX_J 1 unsigned long int uli; double next = 1; void init_my_rnd_1k61 (double seed) /* inizializza seme per my_rnd_1k61 */ { next = seed; } uli my_rnd_1k61(void) /* ritorna numero pseudo-casuale tra 0 e 32767 */ { return ((uli)(next=(fmod(next*K,OVF)))) %MOD; } main() /* Programma prova_my_rnd_1k61.c */ 03 - Impostazione di un GNPC 7/10 { uli num_elem=RAND_MAX+1,cnt[num_elem],i,j; uli range=RAND_MAX+1,occurr=8192,repeat=range*occurr; uli min=INT_MAX,max=0,average=0,dif=0; float chi2=0.0; init_my_rnd_1k61(SEED); for(i=0;i<num_elem;i++) cnt[i]=0; for(j=0;j<MAX_J;j++) for(i=0;i<repeat;i++) cnt[my_rnd_1k61()]++; for(i=0;i<num_elem;i++) { average=average+abs(dif=cnt[i]-(occurr*MAX_J)); chi2=chi2+((float)(dif*dif)/(occurr*MAX_J)); if(cnt[i]<min) min=cnt[i]; else if(cnt[i]>max) max=cnt[i]; } printf("Su %.0f iteraz. m.a. occorr.=%i\n",(float)repeat*MAX_J,occurr*MAX_J); printf("Occorr. min=%li max=%li - campo var.=%li\n",min,max,max-min); printf("Scarto semplice medio=%.2f\n",(float)average/num_elem); printf("Sc. semp. medio su m.a.=%.2f%%\n",100*average/num_elem/(occurr*MAX_J)); printf("Sc. semp. medio su c.v.=%.2f%%\n",100*average/num_elem/(max-min)); printf("CHI2=%.2f CHI2 medio=%.2f\n",chi2,chi2/num_elem); } Si può notare l’analogia con il programma prova_rand_ansi.c, con le opportune modifiche per implementare il generatore moltiplicativo con K appartenente al campo dei numeri razionali (con cifre decimali) e senza disturbo interno/esterno. Dopo alcune prove la scelta è ricaduta su K = 1.61, cioè un valore simile ad una delle due soluzioni per l’equazione di secondo grado usata per il calcolo della sezione aurea (X1 = 0.6180, X2 = -1.6180), ma con cambio di segno. Come per la versione di test per la funzione rand(), si è proceduto raddoppiando ogni volta il numero delle iterazioni, ed il valore (media) atteso delle occorrenze. La funzione rand() inizia a mostrare segni di saturazione delle occorrenze già quando nella colonna m.a. si raggiunge il valore 4˙096, per poi degenerare man mano che si avvicina al periodo di ampiezza inferiore. Nel caso della funzione my_rnd_1k61() le anomalie si presentano tre righe più in basso, al valore 32˙768. Al di sotto del miliardo di iterazioni il valore medio di Chi Quadro si mantiene costantemente attorno al valore ottimale 1, ed al di sopra del miliardo il Chi Quadro Medio tende ad assumere valori proporzionali al campo di variazione ed allo Scarto Semplice Medio, rispettivamente in ragione dell’1‰ e dell’1%. Tabella: my_rnd_1k61() - progressione potenze di 2, da 0 a 17 Iterazioni 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 m.a. 20 21 22 23 24 25 26 27 28 29 210 211 212 213 214 215 216 217 min.o. 0 0 0 0 2 8 33 87 198 421 894 1˙861 3˙843 7˙856 15˙911 31˙706 63˙292 126˙488 max.o. 7 10 14 23 36 58 100 182 325 603 1˙148 2˙272 4˙389 8˙552 16˙882 33˙818 67˙670 135˙438 c.v. 7 10 14 23 34 50 67 95 127 182 254 411 546 696 971 2˙112 4˙378 8˙950 s.s.m. 0.73 1.08 1.56 2.23 3.15 4.48 6.33 9.03 12.72 17.98 25.46 36.03 50.92 71.89 101.29 200.88 420.14 895.14 s.m./m.a. % 73.47 % 53.86 % 39.03 % 27.86 % 19.68 % 14.00 % 9.89 % 7.06 % 4.97 % 3.51 % 2.49 % 1.76 % 1.24 % 0.88 % 0.62 % 0.61 % 0.64 % 0.68 s.m./c.v. % 10.50 %10.77 %11.15 %9.69 % 9.26 % 8.96 % 9.45 % 9.51 % 10.02 % 9.88 % 10.02 % 8.77 % 9.33 %10.33 % 10.43 % 9.51 % 9.60 % 9.60 chi2 chi2 md 32˙848.00 1.00 32˙620.00 1.00 32˙672.50 1.00 32˙690.75 1.00 32˙436.38 0.99 32˙580.75 0.99 32˙541.88 0.99 32˙834.80 1.00 32˙575.86 0.99 32˙652.17 1.00 32˙640.79 1.00 32˙745.68 1.00 32˙583.43 0.99 32˙215.15 0.98 32˙273.84 0.98 63˙522.21 1.94 138˙804.55 4.24 290˙777.30 8.87 03 - Impostazione di un GNPC 8/10 03.5 - Altri confronti tra rand() e my_rnd_1k61() Tabella: rand() - progressione multipli di 213 da 1 a 16 Iterazioni 1 x 228 2 x 228 3 x 228 4 x 228 5 x 228 6 x 228 7 x 228 8 x 228 9 x 228 10 x 228 11 x 228 12 x 228 13 x 228 14 x 228 15 x 228 16 x 228 m.a. 1 x 213 2 x 213 3 x 213 4 x 213 5 x 213 6 x 213 7 x 213 8 x 213 9 x 213 10 x 213 11 x 213 12 x 213 13 x 213 14 x 213 15 x 213 16 x 213 min.o. 7˙865 15˙937 24˙022 32˙206 40˙347 48˙707 56˙952 65˙536 73˙401 81˙473 89˙558 97˙742 105˙883 114˙243 122˙448 131˙072 max.o. 8˙554 16˙829 25˙133 33˙330 41˙492 49˙599 57˙686 65˙536 74˙090 82˙365 90˙669 98˙866 107˙028 115˙135 123˙222 131˙072 c.v. 689 892 1˙111 1˙124 1˙145 892 734 0 689 892 1˙111 1˙124 1˙145 892 734 0 s.s.m. 68.15 88.64 99.14 102.60 98.77 88.64 67.29 0.00 68.15 88.64 99.14 102.60 98.77 88.64 67.29 0.00 s.m./m.a. % 0.83 % 0.54 % 0.40 % 0.31 % 0.24 % 0.18 % 0.12 % 0.00 % 0.09 % 0.11 % 0.11 % 0.10 % 0.09 % 0.08 % 0.05 % 0.00 s.m./c.v. % 9.89 % 9.94 % 8.92 % 9.13 % 8.63 % 9.94 % 9.17 #DIV/0! % 9.89 % 9.94 % 8.92 % 9.13 % 8.63 % 9.94 % 9.17 #DIV/0! chi2 29˙209.61 24˙745.35 20˙681.16 16˙545.29 12˙292.55 8˙247.93 4˙090.68 0.00 3˙245.51 4˙948.61 5˙640.18 5˙515.01 4˙728.14 3˙535.08 1˙908.78 0.00 chi2 md 0.89 0.76 0.63 0.50 0.38 0.25 0.12 0.00 0.10 0.15 0.17 0.17 0.14 0.11 0.06 0.00 In questa tabella per rand(), invece di procedere al raddoppiamento delle iterazioni ad ogni riga, si è utilizzato un multiplo, da 1 a 16, di 8˙192 (come valore atteso delle occorrenze). Le sfumature di grigio evidenziano il periodo e le ripetizioni dei valori indicati. Tabella: my_rnd_1k61() - progressione multipli di 213 da 1 a 16 Iterazioni 1 x 228 2 x 228 3 x 228 4 x 228 5 x 228 6 x 228 7 x 228 8 x 228 9 x 228 10 x 228 11 x 228 12 x 228 13 x 228 14 x 228 15 x 228 16 x 228 m.a. min.o. max.o. c.v. s.s.m. 13 1x2 7˙856 8˙552 696 71.89 2 x 213 15˙911 16˙882 971 101.29 3 x 213 23˙786 25˙356 1˙570 153.94 4 x 213 31˙706 33˙818 2˙112 200.88 5 x 213 39˙609 42˙235 2˙626 257.47 6 x 213 47˙485 50˙762 3˙277 310.48 7 x 213 55˙443 59˙250 3˙807 364.16 8 x 213 63˙292 67˙670 4˙378 420.14 9 x 213 71˙220 76˙172 4˙952 472.49 10 x 213 79˙122 84˙593 5˙471 529.36 11 x 213 86˙986 93˙073 6˙087 583.71 12 x 213 94˙946 101˙610 6˙664 638.97 13 x 213 102˙784 110˙024 7˙240 694.75 14 x 213 110˙714 118˙530 7˙816 748.13 15 x 213 118˙610 126˙957 8˙347 804.75 16 x 213 126˙488 135˙438 8˙950 895.14 s.m./m.a. % 0.88 % 0.62 % 0.63 % 0.61 % 0.63 % 0.63 % 0.64 % 0.64 % 0.64 % 0.65 % 0.65 % 0.65 % 0.65 % 0.65 % 0.65 % 0.68 s.m./c.v. %10.33 % 10.43 % 9.81 % 9.51 % 9.80 % 9.47 % 9.57 % 9.60 % 9.54 % 9.68 % 9.59 % 9.59 % 9.60 % 9.57 % 9.64 % 9.60 chi2 chi2 md 32˙215.15 0.98 32˙273.84 0.98 49˙589.50 1.51 63˙522.21 1.94 83˙342.67 2.54 101˙130.63 3.09 119˙274.02 3.64 138˙804.55 4.24 156˙404.34 4.77 176˙434.98 5.38 195˙193.69 5.96 214˙290.98 6.54 233˙762.09 7.13 251˙980.47 7.69 271˙903.63 8.30 290˙777.30 8.87 Nella corrispondente tabella per my_rnd_1k61() si conferma l’andamento del Chi2 medio già rilevato in precedenza. 03 - Impostazione di un GNPC 9/10 Grafici relativi a Chi2 medio 03.6 - Prestazioni dei GNPC e periodo typedef unsigned long int uli; #define LIM 100000000 main() /* programma confronto_velocita.c */ { int get_rand; double get_my_rnd_1k61; uli get_uli,i; clock_t dt1,dt2,dt3,dt4,dt5,dt6; dt1 = clock(); srand(1000); for(i=0;i<LIM;i++) get_rand=rand(); dt2 = clock(); printf("secondi=%.4f\n",((float)dt2-dt1)/CLOCKS_PER_SEC); dt3 = clock(); init_my_rnd_1k61(1000); for(i=0;i<LIM;i++) get_my_rnd=my_rnd_1k61(); dt4 = clock(); printf("secondi=%.4f\n",((float)dt4-dt3)/CLOCKS_PER_SEC); printf("rapporto velocita' = %.4f\n",((float)dt4-dt3)/(dt2-dt1)); } dt5 = clock(); init_my_rnd_1k61(1000); for(i=0;i<LIM;i++) get_uli=((uli)(next=(fmod(next*K,OVF))))%MOD; dt6 = clock(); printf("secondi=%.4f\n",((float)dt6-dt5)/CLOCKS_PER_SEC); printf("rapporto velocita' = %.4f\n",((float)dt6-dt5)/(dt2-dt1)); Eseguendo un confronto delle velocità tra rand() e my_rnd_1k61(), è emerso quanto segue: • La funzione my_rnd_1k61() è circa 6 volte più lenta rispetto alla funzione di libreria e circa 40 volte più lenta se la stessa funzione standard viene riprodotta in codice sorgente. La causa più probabile è l’utilizzo dell’istruzione fmod (next * K, OVF). • Se l’istruzione viene scritta in modo che il valore restituito non venga assegnato a niente, oppure ad un (unsigned) int o un (unsigned) long, è leggermente più lenta, ma se viene assegnato ad una variabile double si ottiene il miglior risultato, poiché non c’è conversione di formato. • Se il generatore viene convertito in una istruzione di una sola riga senza chiamata a funzione, l’operazione è leggermente più lenta, ma se si riavvia il computer in modalità emergenza (SAFEBOOT con il comando MSCONFIG), il generatore “in una sola linea” è leggermente più veloce della funzione. Probabilmente in modalità di emergenza vengono a mancare alcune ottimizzazioni di sistema per le chiamate a funzioni. 03 - Impostazione di un GNPC 10/10 Eseguendo invece un test relativo al tempo necessario per completare un periodo, cioè, dato un qualsiasi numero prodotto dal generatore, dopo quanto tempo lo stesso numero si ripresenta nuovamente, sembra che il periodo, anche nella ipotesi più ottimistica, si completi nel giro di alcune settimane. main() { /* Programma verifica_periodo_my_rnd_1k61.c */ init_my_rnd_1k61(SEED); clock_t dt1 = clock(); int i; for(i=0;i<100000000;i++) my_rnd_1k61(); printf("secondi=%.4f\n",((float)(clock()-dt1))/CLOCKS_PER_SEC); return; } Fissando una condizione di termine ad un campione di un miliardo di numeri generati, il tempo di elaborazione, con il computer utilizzato, è di circa 150 secondi (due minuti e mezzo). Tenendo conto del fatto che un numero binario in doppia precisione viene rappresentato con un numero in base 10 composto da diciassette cifre, il rapporto tra 1017 su 109 è 108, cioè dieci milioni. Il rapporto tra INT_MAX su 109 è circa 2.147, da cui un calcolo approssimativo del tempo: 150 secondi per 2.147 per 108 ≈ 322˙050˙000 secondi ≈ 5˙367˙500 minuti ≈ 89˙458 ore ≈ 3˙727 giorni ≈ 10 anni. Supponendo che la stima sia errata per eccesso, che le operazioni in virgola mobile, a causa degli arrotondamenti, non permettano di realizzare tutte le possibili combinazioni di decimali, che alcuni numeri possano essere prodotti con una maggiore frequenza rispetto ad altri, e quindi di poter ridurre molto ottimisticamente di un fattore 102 il tempo di esecuzione, il risultato sarebbe comunque di poco superiore a 5 settimane, prima di completare l’intero periodo. 03.7 - Altre prove e considerazioni Sono state fatte ulteriori prove utilizzando le seguenti istruzioni: #define DIST 1789578777 void init_my_rnd_5d12(unsigned long int seed) {next_5d12=seed;} unsigned long int my_rnd_5d12(void) {return(next_5d12=next_5d12-DIST)%MOD;} La costante DIST, cioè il fattore di disturbo esterno/interno, rappresenta un numero primo molto prossimo ai 5/12 di 232. Si ottiene una funzione con un tempo di esecuzione migliore del 25% circa rispetto alla funzione rand() implementata direttamente nel sorgente e circa 13 volte più veloce rispetto alla equivalente funzione standard di libreria, ma come già detto in precedenza, la distribuzione è statisticamente pessima, ed il grafico mostra il tipico disegno a trapunta. Ciononostante, la facilità di realizzazione della funzione, il periodo di 232 numeri e le brevi sequenze di solo 2-3 numeri già ordinati, la rendono consigliabile nelle situazioni in cui non c’è la possibilità o il tempo di realizzare e testare generatori pseudo-casuali più sofisticati. È doveroso concludere che, in presenza di decimali nel generatore, sebbene il fattore di disturbo generato è spesso causa di miglioramenti a livello statistico, i GNPC che ne fanno uso non godono di una delle caratteristiche richieste per questi algoritmi, cioè la prevedibilità e ripetibilità dei risultati se applicati su elaboratori di tipo diverso, a causa di metodologie di arrotondamento implementate in hardware sui co-processori matematici che possono effettuare arrotondamenti con criteri corretti ma non identici per tutti. Dato lo spazio utilizzato finora per i GNPC, ho ritenuto superfluo ripetermi nel descrivere altri tentativi ed analisi effettuate, del tutto analoghi ai precedenti. Tutte le considerazioni fatte fino a questo punto ed i programmi riportati, sebbene “elementari” per i veri conoscitori della materia, possono comunque fornire un utile approccio per chi cerca informazioni al riguardo al di fuori delle usuali fonti didattiche.