www.caosfera.it

annuncio pubblicitario
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.
Scarica