Vous êtes sur la page 1sur 117

1

Introduction

La structure et le principe de fonctionnement des ordinateurs a peu évolué depuis les premières machines programmables construites dans les années 40. Ces machines sont com- posées des éléments suivants:

• Processeur: il effectue les calculs et autres traitements sur les données stockées dans la mémoire. Il suit un programme qui détermine la séquence des opérations à effectuer.

• Mémoire: selon l’architecture définie par John von Neuman on utilise la même mém- oire pour stocker les données et les programmes qui vont traiter ces données. Pour des raisons physiques et économiques, on a en général une mémoire centrale rapide mais de taille limitée et un ensemble de mémoires auxiliaires (disques magnétiques, disques optiques, etc.) moins rapides mais de plus grande capacité et assurant une meilleure persistance des données. La mémoire centrale est en général structurée en cellules composées d’un nombre fixe de bits que l’on peut mettre dans deux états distincts(0/1).

• Systèmes de communication (entrées/sorties): ils assurent la transmission d’informa- tions entre l’ordinateur et son environnement, qui est en général soit un être humain (transmission par écran, claviers, souris, etc.) soit d’autres ordinateurs (transmission à travers un réseau de télécommunication) soit des appareils (moteurs, capteurs, etc.). Le traitement de l’information par une machine impose de coder l’information sous forme binaire pour pouvoir la stocker dans une mémoire. On parle alors de données. Lors de tout développement informatique il faudra donc trouver des structures de données qui

1. permettent de représenter en mémoire toutes les informations à traiter;

2. soient efficaces du point de vue de l’occupation de la mémoire et des temps d’exécu-

tion des algorithmes. Ce deuxième point montre que l’étude des structures de données est indissociable de celle des algorithmes qui vont travailler sur ces données. C’est pourquoi nous allons introduire quelques notions de base concernant les algorithmes et leurs liens avec les structures de données.

1.1 Expression des algorithmes

Pour étudier d’un point de vue général et théorique les notions d’algorithme, de calculabil- ité, de complexité, etc, on a inventé divers modèle abstraits ou idéaux de machines: p.ex. les ma- chines de Turing ou les machines RAM (Random Access Machine) définie en annexe. Si ces modèles de machines sont simples et permettent l’étude théorique du comportement des ma- chines, elles sont cependant peu utilisables en pratique car l’expression d’un algorithme, même simple, requiert un grand nombre d’instructions et sa lecture est difficile. C’est pourquoi nous utiliserons un langage de plus haut niveau qui ressemble aux langages de programmation procé- duraux modernes (Pascal, C, Java). Ce langage comprend des instructions dites de haut niveau mais que l’on peut toujours traduire dans les modèles de base (Turing, RAM).

Expression des algorithmes en pseudo-langage

La mémoire est composée d’ objets 1 qui peuvent contenir des données de différents tpyes (nombres entiers, chaînes de caractères, valeurs booléennes (vrai, faux), etc.). Les objets occu- pent un nombre de cellules mémoire qui dépend de leur type.

1. Il ne s’agit pas d’objets au sens de la programmation orientée-objet.

2

Introduction

Nous utiliserons des variables pour désigner (nommer) les objet mémoire qu’un algorithme utilise. Ceci nous permettra de faire référence à la mémoire non pas par des adresse numériques mais par des noms, ce qui souvent beaucoup plus parlant.

Affectation d’une valeur à une variable

Cette instruction a pour but de stocker une valeur, en général le résultat d’un calcul, dans une variable. C’est l’instruction de base pour modifier l’état de la mémoire.

Syntaxe:

variable expression

Sémantique:

Évalue expression et stocke le résultat dans variable .

Pour exprimer plus formellement l’effet d’une instruction nous utiliserons la notation sui- vante :

[[ pré-condition ]] instruction [[ post-condition ]]

qui signifie que si la pré-condition est vraie avant l’exécution de l’instruction alors la post-con- dition sera vraie après. On peut aussi voir ces conditions comme l’expression de ce qu’on sait sur l’état de la mémoire à un instant donné.

Exemples:

[[ vrai ]] (aucune pré-condition particulière)

X 2

[[ X = 2 ]]

[[ X = 3 ]]

Y X + 8

[[ X = 3 et Y = 11]]

[[ X = 5 et Y = 11 ]]

X X * Y

[[ X = 55 et Y = 11 ]]

[[ vrai ]]

Z "allo"

[[ Z = "allo" (une chaîne de caractères) ]]

[[Z = "allo" et Y = 11 ]]

* X Z + Y n’a pas de sens : on ne peut additionner une chaîne de caractères et un entier

Evaluation des expressions

Une expression comprend des variables, des constantes, des opérateurs, des fonctions et des parenthèses. Le résultat d’une expression est obtenu en appliquant les opérateurs sur les con-

G. Falquet, CUI - Uni. Genève, 2001

Introduction

3

stantes et les valeurs des variables. Les opérateurs s’appliquent selon les règles de priorité habi- tuelles : * et / sont plus prioritaires que + et –. Les sous-expressions entre parenthèses sont évaluées en premier. Nous utiliserons tous les opérateurs habituellement définis pour les types de données traités : +, –, * (multiplication), /, mod (reste de la divistion) pour les nombres en- tiers, , , , pour les ensembles, etc.

Il faut prendre garde au fait que la signification des opérations dépend du type des arguments

:

2.3 + 5.66

addition des réels

6

+ 2

addition des entiers

6

/ 4

"bon"+"jour"

= 1 si l’on reste dans les entiers (et non pas 1.5) concaténation des chaînes

L’un des objets de ce cours est précisément de définir et étudier la signification des opérations sur différents type de données simples ou structurés (entiers, réels, listes, ensembles, séquences, arbres, graphes, etc.)

Séquence d’instruction

Syntaxe:

{ instruction1 ; instruction2 ; …}

Sémantique:

Exécuter instruction1 , puis instruction2 , et ainsi de suite. La post-condition de instruction1 devient la pré-condition de instruction2 , etc.

Exemples:

1. [[vrai]] { X 2; Z X + 1; U X – Z;} [[ X = 2 et Z = 3 et U = –1 ]]

car

[[vrai]]

X 2

[[ X = 2 ]]

Z X + 1

[[ X = 2 et Z = 3 ]]

U X – Z

[[ X = 2 et Z = 3 et U = –1 ]]

2. [[ X = 6 ]] {X X + 3; X X + 3; X X + 3 } [[ X = 15 ]]

Attention: l’ordre des instructions dans une séquence est important !

On a :

[[vrai]] { X 2; Z 2*X ; X X+1 } [[ X = 3 et Z = 4, ]]

et

[[vrai]] { X 2; X X+1; Z 2*X } [[ X = 3 et Z = 6 ]]

G. Falquet, CUI - Uni. Genève, 2001

4

Introduction

Exécution conditionnelle:

Syntaxe:

si ( expression ) instruction1 sinon instruction2

ou bien

si ( expression ) instruction1

Sémantique:

Si l’évaluation de expression donne vrai on exécute instruction1 si elle donne faux on exé- cute instruction2 (si elle existe). L’expression doit fournir un résultat de type booléen { vrai , faux }

Exemples:

[[…]]

{

A 2;

B 1;

si (A > B) W A – B sinon W 0

}

[[ A = 2, B = 1 et W = 1 ]] (car 2 > 1).

[[ vrai ]] si (X > 0) Y X sinon Y –X [[ Y = la valeur absolue de X ]].

Itération

Syntaxe:

tant que ( expression ) instruction

Sémantique:

l’expression est évaluée, si le résultat est vrai l’instruction est exécutée, puis on réévalue l’expression, et ainsi de suite tant que expression donne un résultat vrai.

Exemples:

[[ vrai ]]

{

R 10;

S 0;

tant que (R > 0) { S S + R; R R – 1 }

}

[[ S = 10+9+8+…+2+1 = 55 ]]

Lorsque on veut effectuer un nombre déterminé à l’avance d’itérations on pourra utiliser la syntaxe abrégée

pour ( variable de expression1 à expression2 ) instruction

qui correspond à :

G. Falquet, CUI - Uni. Genève, 2001

Introduction

5

variable expression1 tant que ( variable expression2 ) { instruction ; variable variable + 1

}

Toutes ces instructions peuvent être combinées entre elles pour former des instructions plus complexes.

.

Exemple. Calcul de r = p q

r 1; n q;

tant que (n > 0) {

r

r * p;

n

n – 1;

}

On peut se convaincre que l’algorithme est correct en regardant l’effet de chaque instruction sur l’état de la mémoire :

r 1; n 0;

[[ r = p n ]]

tant que (n q) { [[ r = p n ]]

r r * p;

[[ r = p n+1 ]]

n n + 1;

[[ r = p n ]]

}

[[ r = p n et n = q ]] => [[ r = p q ]]

Sous-programmes: procédures et fonctions

Les procédures sont des abstractions qui permettent d’«emballer» et de nommer un algorithme en vue de sa réutilisation future; ce nommage est aussi nécessaire pour écrire des algorithmes récursifs.

La déclaration d’une procédure prend la forme

procedure nom ( liste de paramètres ) { séquence d’instructions }

Par exemple :

procedure MAX(x, y) {

G. Falquet, CUI - Uni. Genève, 2001

6

Introduction

si (x > y) retourne x sinon retourne y

}

Cette procédure s’appelle MAX, elle traite deux paramètres et retourne un résultat (instruction retourne ). Une procédure qui retourne un résultat s’appelle une fonction et on peut l’utiliser dans les expressions selon la notation habituelle des fonctions. Par exemple

z 3 + MAX(5, b)

w MAX(b, MAX(h, 2*t)) Les expressions entre parenthèses sont les valeurs passées comme arguments (ou paramètres ef- fectifs) à la procédure. Nous prendrons la convention que les paramètres sont passés par référence . Ce qui veut dire que le nom d’un paramètre est une référence à la variable passée com- me argument, toute affectation faite à un paramètre formel dans la procédure va donc modifier la variable passé comme argument. Si l’argument est une expression (a+b) ou une constante (123) on suppose que sa valeur est tout d’abord copiée dans une variable interne à la procédure et qu’on travaille sur cette variable interne.

L’effet d’une procédure peut être de retourner un résultat, comme dans le cas de MAX, ou de modifier les variables passées comme paramètres. Par exemple, si on déclare la procédure

procedure ECHANGE(x, y) { t x; x y; y t;

}

Son utilisation dans un programme (ou dans une autre procédure) aura les effets suivants :

a 34; b 41;

[[ a = 34 et b = 41 ]] ECHANGE(a, b); [[ a = 41 et b = 34 ]]

Si l’argument donnée au moment de l’appel de la procédure n’est pas une variable, nous considérerons que l’ affectation au paramètre correspondant est sans effet à l’extérieur de la procédure (une procédure ne peut modifier une constante ou une expression).

Attention . Dans les langages Pascal, C, Java et C++, les paramètres sont souvent passés par valeur et non par référence. Ceci signifie qu’une copie des argument est faite dans la procé- dure et que la procédure travaille sur cette copie. Dans ce cas notre procédure ECHANGE n’aurait aucun effet visible dans le programme qui l’a appelée.

Méthodes

Dans la programmation orientée objet les sous-programmes s’appellent en général méthodes. Une méthode est toujours liée à un objet particulier (dont elle peut modifier l’état). On utilise une notation pointée pour l’appel des méthodes, le premier paramètre joue un rôle particulier et il est placé avant le nom de la méthode :

objet . méthode ( autres paramètres )

Exemples:

s

"Les copains d’abord";

n

s.longeur

la méthode longueur calcul la taille de la chaîne s

G. Falquet, CUI - Uni. Genève, 2001

Introduction

7

Après l’exécution on aura n = 19.

s.insère(11, "et les amis")

insère "et les amis" à la position 11 (on compte depuis 0)

produire s = "Les copains et les amis d’abord".

Une méthode s’écrit comme une procédure, le premier paramètre représente l’objet sur lequel s’applique la méthode (voir chapitre sur la programmation des classes), on doit obliga- toirement donner son type.

Définition:

methode augmenteDe(entier X, entier Y) { X X + Y

}

Utilisation de la méthode:

A 15

A.augmenteDe(12)

[[ A = 27 ]]

Dans une optique algorithmique, une méthode n’est rien d’autre qu’une autre manière d’écr- ire et d’appeler une procédure. Par contre en programmation il y a une nette différence entre méthode et procédure simple au niveau de l’appel (voir le concept de liaison dynamique).

Variables tableaux

Un tableau est un objet qui contient un nombre fixé d’objets du même type. Lorsqu’un tab- leau contient des objets simples de taille fixe (nombres entiers ou réels, caractères, booléens, références à des objets), ceux-ci seront stockés de manière contigüe dans la mémoire.

Si A désigne un tableau, A[i] désignera le i e élément du tableau. On verra qu’il est commode de numéroter les cellules d’un tableau de taille N de 0 à N–1 plutôt que de 1 à N.

1.2

V A[i] A[i] E

stocke la valeur de A[i] dans V stocke la valeur de E dans A[i]

Complexité

On appelle complexité d’un algorithme les ressources en temps (nombre d’instructions exécutées) et en espace (nombre de cellules mémoire utilisées) qui lui sont nécessaires pour résoudre un problème d’une taille n fixée. Si l’algorithme exécute toujours les mêmes instruc- tions, quelles que soient les données, il aura une complexité en temps constante. C’est le cas de notre programme qui calcule la somme de trois nombres. Par contre, si l’on observe notre pro- gramme de calcul de p q , on s’aperçoit qu’il exécute les instructions entre test et fini exactement q fois. Sa complexité en temps est donc proportionnelle à q . Sa complexité en espace est, quand à elle, constante.

G. Falquet, CUI - Uni. Genève, 2001

8

Introduction

Si les nombres que l’on manipule dans un programme ne dépassent pas une taille maximum fixée à l’avance, il est raisonnable de dire que chaque instruction s’exécute en un temps constant et donc que le temps d’exécution du programme sera proportionnel au nombre d’instructions exécutées. Cela ne s’applique plus si l’on se met à manipuler des nombres arbitrairement grands.

D’un point de vue pratique, l’analyse de la complexité des algorithmes est essentielle, car elle permet de mesurer si les ressources dont on dispose sont suffisante pour résoudre les problèmes auxquels on veut s’attaquer; ou inversement, de calculer la taille maximum des problèmes qu’on peut attaquer étant donné les ressources dont on dispose. Dans le tableau ci- dessous, on suppose qu’on a cinq algorithmes A1, …A5 de complexités différentes pour résoudre le même problème. Le tableau illustre l’importance de la recherche d’algorithmes aussi efficaces que possible.

 

Complexité

Taille maximum du problème que l’on peut résoudre en

Algorithme

en temps

1 seconde

1 minute

1 heure

A1

n

1000

60’000

3’600’000

A2

n log(n)

140

4893

200’000

A3

n

2

31

244

1897

A4

n

3

10

39

153

A5

2

n

9

15

21

Ordres de grandeur

Comme il est en général difficile de calculer exactement le nombre d’instructions qui seront exécutées, on s’intéresse plutôt à l’ordre de grandeur de ce nombre et à son évolution en fonction de n. On utilisera la notation O(f) (prononcer «grand O de f») pour représenter les ordres de gran- deur. Si g(n) est le nombre d’instructions exécutées pour résoudre un problème de taille n, on dira que g(n) O(f) s’il existe un n 0 et un c tels que g(n) cf(n) dès que n n 0 . La fonction f est donc une borne supérieure du nombre d’instruction exécutées.

Exemple. Si g(n) = 6n 2 + 3n – cos(n)

g(n) O(n 2 ) car à partir de n = 5 on a 7n 2 > 6n 2 + 3n – cos(n) (la constante c est 7)

par contre

g(n) O(n) car quelle que soit la constante c, aussi grande qu’on veut, si n est suffisamment grand on finit toujours par avoir 6n 2 > cn (il suffit de prendre n > c).

En général on utilisera des fonctions f simples telles que 1 (constante), n (linéaire), n 2 , n 3 , (polynomial)., 2 n (exponentiel), log(n), pour exprimer l’ordre de grandeur de la complexité.

Il est utile de se souvenir que

g

( n ) = constante O(1)

g

( n ) = an + b O( n ) ( a , b des constantes)

g

( n ) = an 2 + bn + c O( n 2 )

G. Falquet, CUI - Uni. Genève, 2001

Introduction

9

g

( n ) = a k n k + a k 1 n k 1 + … + a 1 n + a 0 O( n k )

g

( n ) = n q O(2 n ) quel que soit q

L’utilisation des ordres de grandeur sert surtout à nous économiser la peine de calculer ex- actement combien d’instructions exécutera un algorithme. En général on pourra assez facilement trouver un ordre de grandeur satisfaisant.

Cas de complexité d’un algorithme

Il faut encore remarquer qu’un algorithme peut avoir des comportement différents suivant les données à traiter. Imaginons que l’on cherche un nom dans un annuaire téléphonique et que notre algorithme consiste à lire tous les noms depuis le début jusqu’à ce qu’on trouve le bon. Si on a beaucoup de chance, le nom cherché est le premier. Si, au contraire, on n’a pas de chance du tout, le nom cherché se trouvera être le dernier. On peut dire qu’ «en moyenne» on trouvera le nom après avoir parcouru environ la moitié des pages de l’annuaire. On doit donc distinguer le meilleur des cas, le cas moyen et le pire des cas pour une taille n fixée. Pour notre recherche dans l’annuaire, si on considère que lire un nom est une opération on aurait pour un annuaire de n noms:

meilleur cas : O(1)

pire cas : O( n )

cas moyen : O( n /2)

En général l’analyse du cas moyen est la plus difficile.

1.3 Structure des machines réelles

Les machines réelles actuelles correspondent à peu près au modèle RAM, avec une différence importante: le programme est stocké dans la mémoire.

Structure de la mémoire

La mémoire possède pratiquement toujours une structure composée d’un ensemble de cel- lules toutes identiques et repérées par un numéro appelé adresse. Chaque cellule est elle-même composée d’un nombre fixe d’unités, appelées bits, que l’on peut mettre dans deux états dif- férents généralement désignés par 0 et 1. Si les cellules d’une mémoire sont composées de 8 bits (ce qui est un cas fréquent), le contenu d’une cellule sera une suite de 8 valeurs binaires, par ex- emple, 10011010. Un groupe de 8 bits est appelé octet (ou byte en anglais).

Pour des raisons d’efficacité “électronique”, les octets de la mémoire sont en général groupés pour former des mots de 16, 32 ou 64 bits.

Processeur et mémoire

En général le processeur contient lui-même quelques cellules mémoires très rapides, appelées registres, qui servent à stocker temporairement les résultats des calculs (comme l’ac- cumulateur de la machine RAM).

On s’est vite aperçu (Turing et Von Neumann 1945) qu’il était avantageux, pour des raisons pratiques, de stocker le programme à exécuter dans la même mémoire que les données. Il suffit pour cela d’attribuer un code binaire à chaque opération, au mode d’adressage utilisé (immédiat, direct, indirect, etc.), au registre(s) à utiliser, et à l’adresse mémoire référencée par l’instruction.

G. Falquet, CUI - Uni. Genève, 2001

10

Introduction

Chaque instruction du programme occupera alors un nombre, en général fixe, de cellules mém- oire. Les instructions d’une programme constituent donc un ensemble de données semblables aux autres et manipulable par un programme (ce qui n’est pas possible dans le modèle RAM). Le processeur contient toujours un registre spécial, le compteur d’instruction, qui indique l’adresse mémoire de la prochaine instruction à exécuter.

Le schéma général de fonctionnement du processeur consiste à

1. lire les cellules contenant la prochaine instruction à exécuter

2. décoder cette instruction (voir de quoi il s’agit)

3. exécuter l’opération entre le(s) registre(s) et les cellules mémoires contenant l’opérande

4. s’il s’agit d’une opération de branchement, modifier le compteur de programme en conséquence, sinon faire passer le compteur à l’instruction qui suite dans la mémoire

Les entrées/sorties

En général le système d’entrée/sorties de base est très rudimentaire. Par exemple, pour af- ficher quelque chose sur un écran il faut placer dans une zone de la mémoire des nombres qui seront interprétés comme les couleurs à donner à chaque point de l’écran. Il faut donc dessiner, sous forme de points, les lettres ou chiffres qui doivent apparaître. De même, le fait d’enfoncer une touche du clavier envoie le numéro de la touche, qu’il faut ensuite interpréter en fonction du type de clavier utilisé (suisse-romand, français, américain, etc.).

On voit qu’il y a là un énorme travail de programmation à faire. Heureusement, des pro- grammes déjà tout faits sont fournis avec les machines, ils font partie du système d’exploitation

Exemple: l’architecture Intel

Cette architecture, qui est actuellement la plus répandue dans le monde, trouve son origine dans le processeur Intel 8086 sorti en 1978. Elle a peu à peu évolué, par ajouts successifs, jusqu’au processeurs actuels Pentium II et III. La mémoire est composée d’octets (8 bits) et peut s’étendre jusqu’à 2 36 –1 octets, soit environ 64 gigaoctets. Il y a 8 registres de 32 bits, six regis- tres de 16 bits, un registre d’indicateurs de 32 bits et un compteur d’instruction de 32 bits.

Le jeu d’instruction est composé de 36 instructions de mouvement de données (entre la mé- moire et les registres ou entre registres); 12 instructions d’arithmétique entière; 6 instructions d’arithmétique décimale codée binaire; 4 instructions logiques; 9 instructions de décalage et ro- tation; 23 instructions de traitement des octets et bits; 31 instructions de branchement et branche- ment conditionnel; 24 instructions de traitement des chaînes de caractères; 11 instructions de contrôle des indicateurs; 10 instructions de contrôle des segments mémoire et autres; 49 instruc- tions MMX de traitement de paquets d’octets; 92 instructions de calcul flottant; 30 instructions de gestion système; 57 instructions SIMD (traitement en parallèle de plusieurs données).

Références

[AHU 74]

Aho, A., Hopcroft, J., Ullman, J. The Design and Analysis of Computer Algorithms, Addison Wes- ley, Reading MA, 1974. (nouvelles éditions sous d’autres titres)

www.intel.com

G. Falquet, CUI - Uni. Genève, 2001

Introduction

11

Annexe: La machine RAM

La machine RAM (pour Random Access Memory) est un modèle abstrait de calcul qui est relativement proche des modèles concrets que l’on trouve dans les microprocesseurs. Contraire- ment à la machine de Turing, la machine RAM peut accéder immédiatement à n’importe quelle cellule de la mémoire, d’où son nom, sans avoir besoin de déplacer une tête de lecture/écriture jusqu’à la cellule voulue. Une machine RAM est composée de quatre éléments:

• une bande d’entrée qui est une séquence de cases contenant chacune un nombre entier, cette bande est lue séquentiellement par la machine;

• une bande de sortie sur laquelle la machine écrit séquentiellement des nombres entiers;

• une mémoire composée d’une séquence de cellules pouvant contenir chacune un nom- bre entier;

• une programme composé d’une séquence d’instructions semblables à celles qu’on trouve dans les machines réelles. Les instructions sont composées d’un code d’opération et d’un argument. L’argument est soit un opérande soit un numéro d’instruction. On a trois type d’opérandes:

1. =i, désigne l’entier i lui-même

2. i, désigne le contenu de la cellule numéro i de la mémoire

3. *i, adressage indirect, désigne le contenu de la cellule dont le numéro est l’entier trou- vé dans la cellule numéro i. De plus on utilise la première cellule de la mémoire, désignée par r0, comme “accumula-

teur” ou zone de travail.

Voici un exemple standard de types d’instructions [AHU 74]

code

argument

effet

opéra-

tion

chg

opérande

met l’opérande dans r0

sto

opérande

met le contenu de r0 dans l’opérande (ne peut être de la forme =i)

add

opérande

ajoute l’opérande à r0

sous

opérande

soustrait l’opérande à r0

mul

opérande

multiplie r0 par l’opérande

div

opérande

divise r0 par l’opérande

lit

opérande

met le contenu de la case courante de la bande d’entrée dans opérande et avance d’une case

ecrit

opérande

écrit l’opérande dans la case courante de la bande de sortie et avance d’une case

saut

numéro

la prochaine instruction à exécuter sera celle désignée par ce numéro

G. Falquet, CUI - Uni. Genève, 2001

12

Introduction

code

argument

effet

opéra-

tion

ssgz

numéro

saute à l’instruction désignée si r0 > 0

ssz

numéro

saute à l’instruction désignée si r0 = 0

halte

 

arrête le programme

La machine exécute une programme en commençant par sa première instruction. A tout mo- ment un “compteur de programme” P indique quelle sera la prochaine instruction à exécuter. Après chaque instruction P est mis à P+1, sauf dans le cas de SAUTE, SSGZ, SSZ et HALT.

Exemple: calculer la somme de trois nombres

lit

1

lit

2

lit

3

chg

1

add

2

add

3

sto

4

ecrit

4

Exemple: calcule la valeur absolue de la différence de deux nombres

1. lit 1

2. lit 2

3. chg 1

4. sous 2

5. ssgz 9

6. sto 3

7. chg =0

8. sous 3

9. sto 4

10. ecrit 4

Exemple: calcul de p q (q 0) par multiplications successives (on a remplacé les numéros d’instructions par des étiquettes, pour simplifier)

 

lit

1

lit

2

charge =1

stocke

3

test

charge

2

ssz

fini

soustr =1

stocke

2

charge

3

mult

1

saute

test

fini

ecrit

3

halt

G. Falquet, CUI - Uni. Genève, 2001

Introduction

13

Traduction de langage de haut niveau en RAM

variable expression

X Y + 2*Z

p.ex. se traduit en RAM de la manière suivante :

chg

Z

mult

=2

add

Y

sto

X

(où X, Y et Z représentent des numéros de cellules différents, p.ex. 1, 2, et 3)

3. Ecriture d’une valeur

écrire expression

4. Séquence d’instruction

{ instruction ; instruction ; …}

p.ex.

{ X Z+1; U X–K; …}

5. Exécution conditionnelle:

si ( condition ) instruction1 sinon instruction2

p.ex.

si (A > B) W A–B sinon W 0

peut se traduire en RAM par

chg

A

sous

B

ssgz

suite

chg

=0

suite: sto

W

G. Falquet, CUI - Uni. Genève, 2001

14

Introduction

G. Falquet, CUI - Uni. Genève, 2001

2

Types de données primitifs

2.1 Représentation des nombres entiers

Le principe de représentation de données en mémoire consiste à coder les valeurs à représenter sous forme binaire et à stocker ces valeurs sous forme d’une séquence de bits d’une ou plusieurs cellules de la mémoire.

Nombres entiers de 0 à 2 n

Pour écrire un entier inférieur à 2 n il faut n chiffres binaires. Si une cellule mémoire est com- posée de n bits, elle peut contenir la représentation binaire de n’importe quel entier compris entre 0 et 2 n – 1. P.ex. dans un cellule de 8 bits le nombre décimal 23, qui s’écrit 10111 en binaire, sera représenté par la configuration de bits [00010111].

Pour stocker un nombre plus grand que 2 n on utilisera un nombre suffisant de cellules adja- centes. P.ex. pour des nombres compris entre 0 et 2’000’000’000 il faut 4 cellules de 8 bits.

Entiers relatifs (nombres négatifs)

La technique la plus usitée pour représenter un nombre négatif consiste à prendre son com- plément à 2. Pour représenter la valeur –k on utilisera les n derniers bits de 2 n – k.

Exemple (n = 8), pour représenter –6 on fait

 

(b8)

b7

b6

b5

b4

b3

b2

b1

b0

 

1

0

0

0

0

0

0

0

0

 

0

0

0

0

0

1

1

0

=

0

1

1

1

1

1

0

1

0

Par conséquent, l’utilisation de n bits permet de représenter les entiers compris

entre –2 n1 et 2 n1 – 1.

Opérations sur les entiers

Etant donné que la représentation des entiers que nous avons choisie est limitée, nous ne pouvons pas utiliser la définition standard des opérations d’addition, soustraction, multiplication des entiers. Si le plus grand entier représentable est 2 7 – 1 = 127, que vaut 127 + 3 ?

Le choix “standard” consiste à utiliser l’arithmétique modulaire, c’est-à-dire à prendre le reste de la division par 2 n après chaque opération. En d’autre termes, on ne considère que les n premiers bits du résultat et on laisse tomber les autres.

Exemple, calcul de 127 + 3 :

 

(b8)

b7

b6

b5

b4

b3

b2

b1

b0

   

0

1

1

1

1

1

1

1

+

 

0

0

0

0

0

0

1

1

=

 

1

0

0

0

0

0

1

0

16

Types de données primitifs

donc 127 + 3 = –126.

Tout se passe comme si les nombres étaient arrangés sur un cercle :

. . . 3 125 2 126 1 127 0 –128 –1 –2 –127 –126
.
.
.
3
125
2
126
1
127
0
–128
–1
–2
–127
–126
–3
.
.
.

L’opération de division doit également être redéfinie de manière à fournir un nombre entier com- me résultat. En général il s’agit de l’entier inférieur le plus proche. Par exemple: 7/3 = 2 ou 39/ 20 = 1. La division par 0 est considérée comme une erreur.

Nombres entiers illimités

Il est bien sûr possible de représenter des nombres arbitrairement grands, plûtot que de se limiter à n bits de représentation. Il suffit pour cela d’attribuer à chaque nombre une quantité de mém- oire suffisante, sous forme de cellules contigües.

Cependant, la plupart des processeurs ne sont pas prévus pour traiter ce type de représenta- tion. Il faut donc écrire des programmes spécifiques qui utilisent l’arithmétique modulaire du processeur pour traiter les nombres par morceaux de n bits. Le traitement des nombres illimités est de ce fait beaucoup moins efficace que celui des nombres limités.

Les modèles courants de nombres entiers proposés par les processeurs

Ces modèles utilisent en général un multiple de 8 bits, on trouve:

le byte: entier de 8 bits, de -128 à +127

le mot: entier de 16 bits, de -32768 à +32767

le double mot: entier de 32 bits, de -2 31 à 2 31 -1

le quadruple mot: entier de 64 bits, de -2 63 à 2 63 -1

Les modèles proposés par les langages de programmation

1. Dans certains langages le modèle d’entiers est calqué sur celui du processeur sous-jacent: C,

Pascal, etc.

2. Un langage comme Ada permet de définir des types d’entiers

p.ex.

type MesNombres is -2

+3000

3. Le langage Java propose des types standards indépendants du processeur:

G. Falquet, CUI - Uni. Genève, 2001

Types de données primitifs

17

int -> entier de 32 bits

long -> entier de 64 bits

byte -> entier de 8 bits

short -> entier de 16 bits

4. D’autres langages proposent des nombres illimités

p.ex. Smalltalk, Maple, Mathematica

ou des nombres de taille fixe mais aussi grande que l’on veut

p.ex. SQL, COBOL

2.2 Les nombres flottants

Les nombres flottants (ou nombres à virgule flottante) sont des représentations de certains nom- bres réels. Etant donné qu’il existe une infinité de nombres réels, et qu’entre deux réels quelconques il y a une infinité d’autres réels, il est bien évident qu’on ne peut avoir une représen- tation de taille finie pour chaque réel. La représentation d’un nombre réel sera donc une approx- imation, c’est à dire la représentation d’un autre nombre, suffisament proche.

Par exemple, sur une calculette à dix chiffres on représente le nombre π par 3.141 592 653, c’est à dire par le nombre rationnel 3 141 592 653 / 1 000 000 000.

Nous nous intéresserons ici à la représentation des réels par des séquences de bits de taille finie et fixée. Le format classique des nombres flottants est composé d’une mantisse, qui est un nombre entre 1.0 et 1.5, d’un exposant et d’un signe.

Par exemple, le standard IEEE-754 définit le format suivant:

bit no.

31

30

 

23

22

 

0

 

signe

 

exposant+127

 

mantisse

Selon cette norme, une séquence de bits [s e 7 e 6 … e 0 m 23 m 22 …m 1 m 0 ] représente le nombre

Exemple

sgn

(s)

×

1

,

m

23

m

22

m

1

m

0

×

2 e 7 e 6 e 0 – 127

a) [1 10000000 010010000000000000000000 ] représente

– 1.01001 × 2 10000000127 = –(1 + 1/2 2 + 1/2 5 ) × 2 128127 = –(2 + 1/2 + 1/2 4 ) = 2.5625

b) Représentation de 1/3

1/3 = 0.010101010101010101010

en binaire

= 1.01010101010101010

donc:

signe: 0

exposant: 127–2 = 125 = 01111101 en binaire

× 2 2

G. Falquet, CUI - Uni. Genève, 2001

18

Types de données primitifs

mantisse: 01010101010101010101010

Le nombre 1/3 est donc représenté par la séquence de 32 bits

[0 01111101 01010101010101010101010]

Le format double précision est le suivant:

bit no.

63

62

 

52

51

 

0

 

signe

 

exposant+1023

 

mantisse

Le plus petit nombre strictement positif que l’on peut représenter est

en simple précision: 1.4 x 10 45

en double précision: 4.9 x 10 324

Le plus grand nombre positif que l’on peut représenter est

en simple précision: 3.4 x 10 38

en double précision: 1.8 x 10 308

De plus, des configuration de bits réservées à cet effet permettent de représenter l’infini positif, l’infini négatif (résultats de divisions par 0, p.ex.), ainsi que l’indéterminé (0/0).

Précision de la représentation

La précision de la représentation est l’erreur que l’on peut commt en représentant un nom-

bre.

Par exemple, la représentation de 1/3, ci-dessus, néglige les chiffres après la 23 ème posi-

tion. L’erreur est donc 0.0

Si l’on prend le cas des nombres flottants en simple précisions, la plus grande erreur arrive lorsque les chiffres négligés sont tous des 1. Dans ce cas, pour un nombre d’exposant exp, l’er- reur est:

(23 fois 0)

01010101010 × 2 2 1.5 × 10 8 .

0.0

Le rapport entre l’erreur et le nombre représenté est donc :

(2 –23 × 2 exp–127 ) / (1.m 23 m 22 m 1 m 0 × 2 exp–127 ) 2 –23 10 –7

(23

fois)

011111111

× 2 exp–127 = 2 –23 × 2 exp–127 .

Cela signifie que l’erreur relative maximum pour la représentation d’un nombre réel en sim- ple précision est inférieure à 10 7 ; en d’autres termes, les 7 premiers chiffres sont représentés correctement.

Dans le cas de la double précision l’erreur relative est inférieure à 2 52 10 16 .

2.3 Précision des calculs et analyse numérique

La représentation des nombres n’étant pas exacte, le résultat d’un calcul ne correspond pas forcément à la valeur mathématique exacte. Si en théorie on a

a + b = c

sur la machine on a

G. Falquet, CUI - Uni. Genève, 2001

Types de données primitifs

19

a + b = c (1 + ε ab )

Le terme ε ab représente l’erreur d’arrondi, il dépend de a et de b.

L’une des conséquence de ce fait est que sur une machine

(a + b) + c a + (b + c) en général.

Par exemple. On veut calculer

s

=

100000

n = 1

1

-----

n 2

on peut effectuer le calcul en commençant par les premiers termes :

s1 = (…((((1+1/4)+1/9)+1/16)+1/25)+…)+1/10000000000

ou par les derniers

s2 = 1+(1/4+(1/9+(1/16+(1/25+(…+1/10000000000)…))))

un calcul théorique montre que

| s – s1 | 1.6 10 5 ε

| s – s2 | 9.2 ε

où e est la précision de représentation des nombres (p.ex. 10 7 pour les oat ).

Si l’on effectue le calcul avec un programme Java en utilisant des nombres de type float on obtient

s1 = 1.64473

s2 = 1.64493

alors que le s exact est 1.64492407…

La propagation des erreurs peut devenir énorme dans certains cas. Prenons par exemple le calcul de la fonction exponentielle avec la formule de Taylor:

e x

=

1

+

x

+

x 2

-----

2!

x 3

----- x 4

+++

-----

3!

4!

+ ----- x n! n +

On utilise l’algorithme suivant pour calculer une approximation de ex en prenant les 20 pre- mier termes de la série de Taylor:

1. t 1; e 1;

2. pour n de 1 à 20 {

3. t t * x / n

4. e e + t

5. }

En programmant cet algorithme en Java avec des nombres de type float on obtient les résultats suivants:

G. Falquet, CUI - Uni. Genève, 2001

20

Types de données primitifs

x

e x calculé

e x exact

-1

0.367879

0.367879…

-5

0.00670682

0.0067379…

-10

-27.7064

0.0000454…

-20

-2.1866 10 7

2.0612 10 9

La branche des mathématique qui s’intéresse aux algorithmes numériques et aux problèmes de précision de calculs s’appelle l’ analyse numérique . Les travaux en analyse numétique on mis en évidence deux notions fondamentales que nous allons présenter brièvement ci-dessous.

Problèmes mal conditionnés

Il existe des problèmes pour lesquels on ne peut trouver de méthodes de calcul qui donnent des

résultats précis (sauf en utilisant des nombres de taille illimitée !).

Exemple

On souhaite calculer

x 1, 2

=

a

±

2 a – b
2
a
– b

pour a = 1000 et b = 999999.987654321 avec une machine qui a 8 chiffres de précision. On

a :

a 2 1000000.00 -b 999999.99 =

0.01

donc a +/- (a 2 – b) 1/2 = 1000 +/- 0.01 1/2 = 1000.1 et 999.9 alors que les vraies valeurs sont 1000.111111… et 999.888888. Il ne reste donc que 4 à 5 chiffres corrects. Ceci vient de la sous- traction qui nous a fait perdre beaucoup de précision. C’est un exemple de problème mal condi- tionné.

Un problème consistant à calculer y = F(x) est mal conditionné si une petite variation de la valeur de x entraine une grande variation de y. Le nombre de condition C d’un problème y = F(x), pour une valeur b du paramètre x est défini comme

C

=

bF'(b)

----------------

F(b)

Si C est grand le problème est dit “mal conditionné”.

Justification: si b est la valeur théorique, la valeur effectivement représentée en mémoire est b(1+ ε ). L’erreur de calcul est donc F(b(1+ ε )) – F(b). F(b(1+ ε )) = F(b + b ε) est approximative- ment égal à F(b)+F’(b)b ε . L’erreur ralative est donc |(F(b)+F’(b)b ε – F(b)) / F(b) |.

G. Falquet, CUI - Uni. Genève, 2001

Types de données primitifs

21

Stabilité numérique

Le fait qu’un problème soit bien conditionné ne veut pas dire que tout algorithme donnera une réponse précise. On dira qu’un algorithme est numériquement stable si la valeur F calc (b) cal- culée par l’algorithme (à la place de la vraie valeur F(b)) est la solution d’un problème proche. C’est à dire que F calc (b) = F(b + ε ) avec ε petit.

L’algorithme de calcul de F(b) = e b que nous avons présenté plus haut n’est pas stable, en effet: pour b = –10 il donne F calc (b) = –27.7064, or il n’existe pas d’ ε , même assez grand, tel que

e 10+ ε = –27.7064.

Cet algorithme est par contre stable pour la valeurs de b comprises entre –1 et +1. On en déduit un nouvel algorithme:

Soit y la partie entière de x et z = x–y donc e x = e y+z = e y e z

1. calculer e y par multiplications (et inversion à la fin si y < 0)

2. calculer e z par l’algorithme précédent

3. multiplier les deux résultats

Cette brève incursion dans l’analyse numérique avait essentiellement pour but de montrer que l’arithmétique des nombres flottants ne doit surtout pas être assimilée à l’arithmétique des nombres réels “intuitifs” ou tels qu’ils sont définis en analyse mathématique. Il n’y a pas de cor- respondance bi-univoque entre l’arithmétique en virgule flottante des machines et celle du corps R des réels.

2.4 Conversions de type

En mathématique on a les inclusions naturelles N Z Q R (les entiers font partie des entiers relatifs, qui font partie des rationnels, qui font partie des réels). Il n’en va pas de même entre les types numériques. Par exemple, un entier sur 64 bits ne peut pas être représenté exact- ement par un flottant sur 32 bits. Il faut une opération de conversion qui trouve le flottant le plus proche de l’entier à représenter. Par exemple, l’entier 123 456 789 012 345 sera converti en 1.234568E+14., c’est à dire 123456800000000.

Il existe cependant des conversions, dites élargissement, qui ne posent pas de problèmes, on peut par exemple toujour passer d’un entier sur n bits à un entier sur m bits lorsque m > n. De même on peut passer directement d’un flottant avec k bits de mantisse et t bits d’exposants à un flottant à k' bits de mantisse et t' bits d’exposant si k' k et t' t.

Par contre, la conversion en sens inverse (rétrécissement) pose des problèmes. Différentes approches existent pour les résoudre.

En Java: la conversion d’un type entier à n bits vers un type à m bits (m < n) consiste sim- plement à prendre les m premiers bits du nombre. Ce qui fait que le nombre int (32bits) 65537 devient 1 lorsqu’il est converti en short (16bits). Pour éviter de trop mauvaises surprises une telle conversion doit être écrite explicitement dans un programme

int i = 65537; // short s1 = i --- ERREUR, affectation interdite short s2 = (int)i // prend les 16 premiers bits de i et affecte à s2

G. Falquet, CUI - Uni. Genève, 2001

22

Types de données primitifs

Dans d’autres langages la sémantique peut être différente. On peut par exemple avoir une erreur à l’exécution si un dépassement de capacité se produit.

Il est utile de rappeler ici que l’échec du premier tir de la fusée Ariane 5 a été causé par une erreur du système informatique de guidage. Cette erreur est survenue lors d’une conversion de type qui a causé un dépassement de capacité d’une variable. Parmi les recommandations émises suite à cet accident on notera :

Identifier toutes les hypothèses implicites faites par le code et ses documents de justi- fication sur les paramètres fournis par l'équipement. Vérifier ces hypothèses au regard des restrictions d'utilisation de l'équipement.

Vérifier la plage des valeurs prises dans les logiciels par l'une quelconque des variables internes ou de communication.

2.5 Les Caractères

La représentation des caractèes est une convention qui associe à chaque caractère de l’alphabet un nombre binaire, c-à-d une configuration de bits. Il s’agit forcément d’une convention car il n’y a pas d’ordre canonique des caractères (sauf pour les lettres). La représentation des car- actères a évolué au cours du temps, en fonction du type d’utilisateurs de l’informatique et de leurs besoins spécifiques. On est passé progressivement de codages sur 6 bits au codage actuel sur 16 bits, comme le montre le tableau suivant :

nb.

nb. caractères

   

bits

représentables

nom

remarques

6

64

 

Permet de représenter les 26 lettres majus-

cules latines, les chiffres 0

9,

les symboles

de ponctuation: , : ; . ( ) etc. Suffisant pour écrire des programmes et imprimer des résultats.

7

128

ASCII

Standard adapté à la langue américaine :

lettres majuscules et minuscules sans accents, chiffres, symboles de ponctuation.

8

256

IBM-PC

Représentation des lettres latines maj. et min. y compris les lettres accentuées, rendu nécessaire pour le développement des logiciels de traitement de texte.

8

256

Apple

Idem chez Apple avec l’arrivée du Macin- tosh

8

256

ISO-xxx

Ensemble de standards de représentation adaptés à différents groupes linguistiques (p.ex. ISO-Latin-1 permet la représenta- tions des lettres utilisées dans les langues latines)

G. Falquet, CUI - Uni. Genève, 2001

Types de données primitifs

23

nb.

nb. caractères

   

bits

représentables

nom

remarques

16

65536

Unicode

Un standard unique pour représenter tous les caractères utilisés dans le monde, y compris les idéogrammes.

2.6 Le Standard Unicode

Il est intéressant d’étudier le nouveau standard Unicode pour voir que la création d’un code de représentation des caractères n’est pas simplement technique mais également conceptuel et lin- guistique.

Les principales caractéristiques d’Unicode sont :

• la capacité d’encoder tous les caractères du monde ;

• le codage sur 16 bits ;

• un mécanisme d’extension (UTF-16) pour coder un million de caractères supplémentaires si nécessaire

Les alphabets traités sont :

• Latin, Greek, Cyrillic, Armenian, Hebrew, Arabic, Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada, Malayalam, Thai, Lao, Georgian, Tibetan, Japanese Kana, modern Korean Hangul, Chinese/Japanese/Korean (CJK) ideographs. Et bientôt: Ethiopic, Canadian Syllabics, Cherokee, additional rare ideographs, Sinha- la, Syriac, Burmese, Khmer, and Braille.

• Ponctuation, diacritiques, mathématique, technique, flèches, dingbats

• Signes diacritiques de modification de prononciation (n + ‘~’ = ñ)

• 18,000 codes en réserve

Eléments de texte, caractères et glyphes

D’un point de vue conceptuel il n’est pas évident de définir ce qu’est un caractère. Un élément de texte peut être composé de plusieurs caractères. Par exemple, en espagnol le double l “ll” compte comme un seul élément, comme s’il s’agissait d’une lettre spéciale. Les concepteurs d’Unicode ont choisi de définit des éléments de code (caractères) plutôt que des éléments de texte.

• p.ex. l’élément de texte “ll” est donc traité comme deux codes: ‘l’ + ‘l’

• chaque lettre majuscule et minuscule est un élément de code

D’autre part il faut distinguer le caractère (la valeur d’un code) et son affichage sur l’écran ou sur le papier. On a donc une notion de caractère abstrait , par exemple :

• "LATIN CHARACTER CAPITAL A"

• "BENGALI DIGIT 5"

et une notion de glyph qui est la marque faite sur un écran ou sur papier pour représenter visuelle- ment un caractère, par exemple

• A A A A A

G. Falquet, CUI - Uni. Genève, 2001

24

Types de données primitifs

sont des glyphs qui représentent le caractère "LATIN CHARACTER CAPITAL A". Unicode ne definit pas les glyphs et ne spécifie donc pas la taille, forme, orientation des caractères sur l’éc- ran.

Il existe également une notion de caractères composites (p.ex. â) qui est formé de

• une lettre de base (qui occupe un espace) "a"

• un ou plus marques (rendus sur le même espace) "^"

Unicode spécifie

• l’ordre des caractères pour créer un composite

• la résolution des ambigüités

• la décomposition des caractères précomposés "ü" peut être encodé par le code U+00FC 1 (un seul caractère de 16-bits) ou bien décomposé en U+0075 + U+0308 ("u"+"¨"). l’encodage en un seul caractère assure la compatibilité avec le standard ISO-Latin-1.

2.7 Définition des codes

Lors de l’attribution des codes aux caractères on a veillé à assurer l’inclusion de standards précé-

dents (0

caractères utilisés par plusieurs langues. Ceci afin d’éviter les duplification. Par exemple, le chi- nois, le japonais et le coréen utilisent tous le même script (nommé CJK) car ils ont plusieurs mil- liers de caractères en commun.

Comme on peut s’y attendre, un texte est une séquence de codes correspondant à l’ordre de frappe au clavier des caractères. Cependant toutes les langues ne s’écrivent pas dans la même direction, il existe donc des caractères spéciaux de changement de direction.

L’attribution des codes obéit aux principes suivants :

Un nombre de 16 bits est assigné à chaque élément de code du standard. Ces nombres sont ap- pelés les valeurs de code

FF = Latin-1). De plus on a défini une notion de script qui est un système cohérent de

U+0041 = nombre hexadécimal 0041 = décimal 65 représente le caractère "A" .

Chaque caractère reçoit un nom

U+0041 s’appelle "LATIN CAPITAL LETTER A."

U+0A1B s’appelle "GURMUKHI LETTER CHA."

(standard ISO/IEC 10646)

Des blocs de codes de taille variable sont aloués aux scripts en fonction de leur nombre de car- actères. L’espace des codes est actuellement aloués selon la séquence suivante : [standard ASCII (Latin-1)] - [Greek] - [Cyrillic] - [Hebrew] - [Arabic] - [Indic] - [other scripts] - [symbols and punctuation] - [Hiragana] - [Katakana] - [Bopomofo] - [unified Han ideographs] - [modern Hangul] - [surrogate characters] - [reserved for private use (never used)] - [compatibility char- acters].

L’ordre “alphabétique” à l’intérieur d’un script est si possible maintenu

1. La notation U+dddd signifie qu’il faut lire le nombre dddd comme un code Unicode en base 16.

G. Falquet, CUI - Uni. Genève, 2001

Types de données primitifs

25

Base de donnée de codes

Il existe une base de données de codes qui définit, outre le code et le nom de chaque caractère, d’autres attributs utiles pour le traitement des textes. Pour chaque caractère on a les quatorze champs

0 Code value

1 Character Name.

2 General Category

3 Canonical Combining Classes

4 Bidirectional Category

5 Character Decomposition

6 Decimal digit value : lorsque le caractère représente un chiffre, p.ex. le " V " romain

7 Digit value

8 Numeric value

9 "mirrored" (pour l’écritures bidirectionnelles)

10 Unicode 1.0 Name

11 10646 Comment field

12 Upper case equivalent mapping

13 Lower case equivalent mapping

14 Title case equivalent mapping

Quelques exemples tirés de la base ftp://ftp.unicode.org/Public/2.1-Update3/UnicodeData- 2.1.8.txt :

0041;LATIN CAPITAL LETTER A ;Lu;0;L;;;;;N;;;;0061; 005E;CIRCUMFLEX ACCENT;Sk;0;ON;<compat> 0020 0302;;;;N;SPACING CIRCUMFLEX;;;; 0F19;TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS; Mn;220;ON;;;;;N;;dong tsu;;; 112C;HANGUL CHOSEONG KAPYEOUNSSANGPIEUP; Lo;0;L;<compat> 1107 1107

110B;;;;N;;;;;

1EE4;LATIN CAPITAL LETTER U WITH DOT BELOW; Lu;0;L;0055

0323;;;;N;;;;1EE5;

FC64;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH REH FINAL FORM;Lo;0; R;<final> 0626 0631; ;;;N;;;;;

Formes d’encodage

Il faut faire la différence entre la définition des codes Unicode pour la représentation des car- actères et la manière dont ces caractères sont stockés sur les supports physiques (p.ex. dans des fichiers). Il existe deux modes principaux d’encodage :

UTF-16

• caractères 16-bits

• paires de 2 x 16-bits pour extension

UTF-8

• codage à longueur variable

0x00

0x7F ==> 1 byte (même codes que ASCII)

0x80

0x3FF ==> 2 bytes

0x400

0xD7FF, 0xE000

0xFFFF ==> 3 bytes

G. Falquet, CUI - Uni. Genève, 2001

26

Types de données primitifs

0x10000

0x10FFF ==> 4 bytes

• conversion sans perte vers et de UTF-16

2.8 Le type caractère

Tout ce que nous venons de présenter montre que le type caractère n’est pas aussi simple qu’il en a l’air au premier abord. Il existe de nombreuses opérations, parfois complexes, sur ce type. Si l’on considère que les deux opérations de base sur les caractères sont l’encodage et le dé- codage (passer d’un caractère à l’entier correspondant et réciproquement), on peut énumérer d’autres opérations telles que :

• trouver l’équivalent majuscule d’un caractère

• trouver l’équivalent minuscule d’un caractère

• encoder un caractère sous forme UTF-8

• composer un caractère à partir d’un caractère de base et de marques

• etc.

Si l’on prend le standard Unicode, ces opérations nécessitent une consultation de la base de don- nées des caractères ainsi que l’application de règles de compositions non triviales.

G. Falquet, CUI - Uni. Genève, 2001

Chaines de caractères

27

3 Chaines de caractères

3.1 Vision abstraite des chaînes

Commençons par considérer les chaînes de caractères indépendemment de toute représentation de celles-ci dans une machine. Dans cette vision abstraîte on s’intéresse à caractériser les chaînes au niveau de leur structure (de quoi sont-elles faites ?) et des opérations que l’on peut effectuer avec ce type de données

Une vision orientée modèle (structure)

Lorsqu’on parle de chaîne de caractères, la première idée qui vient à l’esprit est qu’il s’agit d’une séquence de caractères. Cette idée correspond à un point de vue ensembliste qui peut se résumer de la manière suivante :

Une chaîne de caractères est une séquence <c 1 , c 2 , …, c n > de caractères.

Le type de données «chaîne de caractères» est formé de l’ensemble de toutes les séquences pos- sibles (y compris la séquence vide <>).

Sur la base de cette définition on peut définir les principales opérations sur les séquences

• concaténation <c 1 , …, c n > + <d 1 , …, d k > <c 1 , …, c n , d 1 , …, d k >

• élément à la position i <c 1 , …, c n > c i

• modifier l’élément à la position i <c 1 , …, c i , …, c n > <c 1 , …, c i ', …, c n >

• insérer/retirer à la position i <c 1 , …, c i , …, c n > <c 1 , …, c i , d, c i+1 ,…, c n > <c 1 , …, c i , …, c n > <c 1 , …, c i1 , c i+1 ,…, c n >

Egalité des chaînes

On peut définir différentes notions d’égalité sur les chaînes. L’égalité stricte est définie par

<c 1 , …, c n > = <d 1 , …, d k > si

• n = k

• c i = d i (i = 1, n)

D’après cette définition

"swing " "swing"

"swing" = "swing"

"swing" "Swing"

"mel" "mél"

Dans certaines applications cette notion d’égalité est cependant trop stricte. On peut lui préférer l’égalité par degré définie comme

<c 1 , …, c n > = <d 1 , …, d k > si

G. Falquet, CUI - Uni. Genève, 2001

28

Chaines de caractères

• n = k

• c i = degré d i (i = 1, n)

où = degré est un égalité sur les caractères définie comme

• = 0 : égalité stricte

• = 1 : des caractères différents selon = 0 sont considérés égaux p.ex. "e" = 1 "é" ==> "fréquence" = "frequence"

• = 2 : encore plus large p.ex. "e" = 2 "E" ==> "Fréquence" = "frequence"

• etc.

Cette notion d’égalité dépend bien entendu de la langue, ou même de la région! (voir: classe ja- va.text.Collator)

Comparaison des chaînes

On utilise généralement l’ordre lexicographique qui est basé sur l’ordre des caractères dans l’al- phabet (ou le script Unicode). Le principe consiste à comparer les caractères des deux chaînes à partir de la gauche jusqu’à ce qu’on trouve un caractère différent ou qu’on ait atteint le bout d’une des chaînes.

On a

<c 1 , …, c n > <d 1 , …, d k > si

p . p min(n, k) et c 1 = d 1 , …, c p1 = d p1 et [ c p < d p ou ( p = n et n k) ]

Donc

"xyZot" "xyaot"

("Z" < "a" dans le code Unicode ou ISO ou ASCII)

"truc" "truc

" (et "truc" "truc

")

"1205" "205"

On peut également instaurer des degrés dans la comparaison en considérant l’égalité par degré des caractères. La relation < n’est plus définie entre caractères mais entre groupes de caractères. Par exemple {"A", "a", "à", "À"} < {"b", "B"} < {"c", "C", "ç", "Ç"} < {"d", "D"} < {"E", "È", "É", "Ê", "e", "è", "é", "ê"} < … Ce qui donnerait

"patate" < "pâté" mais

"pâté" = "pâte" = "Pate"

Vision abstraite algébrique

Une autre manière de voir les chaînes consiste à se concentrer sur les opérations entre chaînes plutôt qu’à l’ensemble des valeurs possibles. Dans cette vision algébrique on peut considérer le type chaîne comme le monoïde (S, +) formé de

• l’ensemble S des chaînes

• l’opération + de concaténation, qui possède les propriétés

a) ( s + t ) + u = s + ( t + u )

b) ( s + "") = s = "" + s

(associativité)

(la chaîne vide "" est l’élément neutre)

G. Falquet, CUI - Uni. Genève, 2001

Chaines de caractères

29

Il est possible de construire toutes les chaînes de S par concaténation à partir des chaines prim-

itives formées d’un seul caractère :

"A" "B" … "Z" "a"

"z"

On peut également donner une définition algébrique des autres opérations en se basant sur des équations.

longueur

• longueur("") = 0

• longueur("c") = 1

• longueur( s + t ) = longueur( s ) + longueur( t ) element no. i

• element( i , s + t )

= element( i , s ) si longueur( s ) i

= element( i – longueur( s ), t ) sinon

• element(1, "c") = "c"

Nous reviendrons sur cette définition algébrique lorsque nous parlerons de la spécification ab- straite des types de données.

3.2 Représentation concrète des chaînes

La représentation des chaînes de caractères en mémoire se base évidemment sur la représenta- tion des caractères. Si une cellule mémoire peut contenir un caractère (ou un demi en Unicode), on peut simplement dire qu’une chaîne de m caractères est un objet composé d’une séquence de

m cellules. Un premier problème se pose ici : comment un programme reconnaîtra-t-il l’endroit

où se termine une chaîne ? Il n’y a pas de marque de début et de fin d’objet dans la mémoire. La solution est simple : il suffit d’utiliser les premières cellules mémoire de l’objet pour stocker un nombre entier qui représentera la longueur de la chaîne, puis de placer les caractères après cet indicateur de longueur. Une autre technique consiste à indiquer la fin d’une chaîne avec un car- actère de code 0.

Un problème plus dur se pose cependant à nous : une variable du langage algorithmique sert à nommer un objet de la mémoire. Si l’on veut qu’une variable de type chaîne puisse recevoir différentes valeurs, quelle taille faut-il réserver pour l’objet correspondant ?

On pourrait tout d’abord imaginer de fixer une taille unique pour les objets chaînes, cette taille devrait correspondre à la plus grande chaîne que l’on veut pouvoir traiter. Mais alors, quelle taille maximum choisir ? Et à supposer qu’on ait pu faire ce choix, le gaspillage d’espace mémoire serait énorme puique même une chaîne d’un seul caractère occupera le même espace que la plus grande chaîne admise.

Une solution plus générale consiste à introduire un niveau d’indirection 1 . L’objet correspon- dant à un variable de type chaîne S ne contiendra pas la chaîne elle même mais une référence (adresse mémoire) à un objet qui contient la chaîne. L’objet S occupera un nombre fixe de cel-

1. Selon l’adage informatique « il n’y a pas de problème de structure de données qu’on ne puisse résoudre en ajoutant un ou plusieurs niveaux d’indirection ».

G. Falquet, CUI - Uni. Genève, 2001

30

Chaines de caractères

lules, suffisant pour stocker une adresse mémoire. Au cours du temps, la variable pourra faire référence à différents objets mémoire, comme illustré sur le schéma.

(1). S "abc"

(2). S "hahahabc"

S (de type Chaîne)

8, h a h a h a b c 3, a b c
8, h a h a h a b c 3, a b c
8, h a h a h a b c 3, a b c

8, h a h a h a b c

8, h a h a h a b c 3, a b c

3, a b c

8, h a h a h a b c 3, a b c
(2) (1)
(2)
(1)

Dans ce contexte, il existe deux grandes catégories de représentation : la représentation im- muable et la représentation mutable.

Représentation immuable

Dans une représentation immuable, l’objet qui contient les caractères d’une chaîne ne peut changer de valeur au cours du temps. Par conséquent les résultat d’une opération, p.ex. de con- caténation, est toujours un nouvel objet.

Par exemple :

(1) x "cos" [[ x fait référence à un objet o 1 qui contient "cos" ]] (2) y x [[ y fait référence au même objet o 1 ]] (3) x x + "mos" [[ x fait référence à un nouvel objet o 2 qui contient "cosmos" ]]

X

Y

3, m o s

3, m o s 6, c o s m o s 3, c o s
3, m o s 6, c o s m o s 3, c o s
3, m o s 6, c o s m o s 3, c o s

6, c o s m o s

3, m o s 6, c o s m o s 3, c o s

3, c o s

3, m o s 6, c o s m o s 3, c o s
3, m o s 6, c o s m o s 3, c o s
(3) (1)
(3)
(1)
(2)
(2)

Cette représentation possède les propriétés suivantes :

• (bonne) les objets qui contiennent les chaînes sont partageables entre variables (écon- omie de place sans problèmes d’alias, voir ci-dessous) ;

• (moins bonne) il peut y avoir une grande consommation de ressources (allocation/ désallocation) si un programme effectue de nombreuses opérations sur les chaînes.

G. Falquet, CUI - Uni. Genève, 2001

Chaines de caractères

31

Représentation mutable

Une représentation mutable signifie que l’objet qui représente une chaîne peut changer de valeur au cours du temps, suite à des opérations. Tant que l’opération n’agrandit pas la chaîne on utilise

le même objet pour stocker les caractères. On aura par exemple :

1) x "hop et boum" [[ x fait référence à un objet o 1 qui contient "hop et boum" ]] (2) y x [[ y fait référence au même objet o 1 ]] (3) x "zip" [[ x fait toujours référence à o 1 qui contient maintenant "zip" ]]

[[ y vaut aussi "zip" -- effet d’alias]]

remarque: à ce point, l’objet o 1 contient des cellules inutilisées.

Cette représentation présente les caractéristiques suivantes :

• moins de consommation de ressource (allocation/déallocation)

• il faut prévoir une stratégie d’extension

• des problèmes d’alias peuvent apparaître

Problème des alias

Ce problème survient lorsqu’il existe dans un programme deux manières différentes de désigner

le

même objet. Dans l’exemple précédent, après l’instruction (2) x et y désignent tous les deux

le

même objet o 1 qui contient la chaîne "hop et boum". Avec l’exécution de l’instruction suivante

(3) x "zip",

on a modifié cet objet, et donc simultannément modifié la valeur de y sans le dire explicitement. Ce phénomène tend évidemment à rendre le programme moins lisible et donc plus sujet à er- reurs. Il est clair que ce problème ne se pose pas pour des objets immuables.

En général les langages de programmation utilisent des objets immuables pour représenter les chaînes de caractères littérales. Ainsi on est sûr qu’après l’exécution de la séquence

A = "encore";

B = "encore";

A = A + " vous";

A vaudra "encore vous" et B vaudra "encore".

Allocation et désallocation de la mémoire

La mémoire étant une ressource limitée il convient de la gérer pour éviter de la saturer. L’opéra- tion d’allocation consiste à trouver dans la mémoire une zone inutilisée suffisamment grande pour contenir un nouvel objet (ou un tableau, ou un enregistrement, etc.).

L’opération de désallocation signale au contraire qu’une zone de la mémoire n’est plus utilisée

et peut être allouée à d’autres objets.

Les langages diffèrent dans leur mode d’allocation et surtout de désallocation de la mémoire. Des langages comme Pascal, C, C++, Ada demandent une déallocation explicite. Tant que le pro- gramme n’a pas désalloué une zone celle-ci est considérée comme occupée. Des langages tels que Smalltalk, Eiffel, Java, Lisp utilisent un système automatique qui repère les objets jetables

G. Falquet, CUI - Uni. Genève, 2001

32

Chaines de caractères

et les désallouent. Un objet est considéré comme jetables s’il est plus accessible par un pro-

gramme, c’est-à-dire s’il n’existe plus aucune variable qui permet d’y accéder. Par exemple

A = "salut" // allocation de objet1("salut")

B = "oh oh" // allocation de objet2("oh oh")

A = A + " les potes" // allocation de objet3("salut les potes")

Après l’exécution de cette séquence objet1 n’est plus référencé par A ni par B, il est donc jetable.

Il faut se souvenir que ces opérations d’allocation et désallocation sont relativement coûteuses

en temps puisqu’il faut d’une part trouver des espaces libres et d’autre part repérer les objets jeta- bles. Elles impliquent également que le système d’allocation maintienne une "carte" des zones mémoire occupées et inoccupées.

Allocation et extensibilité

Un objet n’est en général pas extensible «sur place» ; si l’objet a besoin de plus de place on ne peut pas agrandir sa zone mémoire sans entrer en conflit avec les zones attribuées à d’autres ob- jets. Il faut donc trouver d’autres stratégies pour étendre les objets. On a deux types de stratégies qui consistent à

• allouer une nouvelle zone mémoire suffisament grande et recopier l’objet dans cette zone (stratégie contigüe) ;

• allouer une ou des autres zones et répartir l’objet (les sous-objets) dans ces zones, l’in- tégrité de l’objet étant assurée par des références (pointeurs) entres zones (stratégie non contigüe).

A partir de ces stratégies de bases on peut imaginer toutes sortes de variantes, comme, par

exemple : allouer une zone plus grande que nécessaire en prévision des extensions futures ; uti- liser une stratégie non contigüe mais «compacter» l’objet de temps en temps, etc.

La performance de ces stratégies dépend énormément du type d’opération qu’on effectue sur les

objets. Il n’est donc pas possible de déterminer une stratégie optimale.

Nous reviendrons en détail sur ce point dans le chapitre sur la représentation des collections.

Mutabilité et performances

Dans de nombreux cas la représentation immuable des chaînes peut entraîner des temps de calcul importants car toute opération nécessite la recopie de la chaîne. Si x est une chaîne de 5000 car- actères, le simple remplacement d’un caractère de la chaîne entraînera la recopie des 5000 caracètres dans un nouvel objet. Les opérations itérées sont évidemment encore plus coûteurses.

Considérons le cas où l’on veut mettre dans S une chaîne contenant 10000 fois le caractère "*". On utilise l’algorithme suivant :

S "*"; pour i de 2 à 10000 { S S + "*"

}

Dans le cas d’une implémentation immuable, l’exécution de l’algorithme va entraîner 10000 créations et recopie d’objets qui seront elles-même de plus en plus longues.

G. Falquet, CUI - Uni. Genève, 2001

Chaines de caractères

33

Dans le cas d’une implémentation mutable ce nombre sera considérablement réduit si l’ex- tension se fait de manière «prévoyante», c’est-à-dire si quand un objet devient trop petit on en alloue un qui soit suffisamment grand pour supporter d’autres agrandissements. Par exemple, si on alloue systématiquement 10 cellules de plus que nécessaire, on n’aura plus besoin que de 1000 allocations. Si l’on double l’espace alloué à chaque fois on n’aura besoin que de 14 allo- cations.

En résumé, si l’on veut pouvoir analyser précisément la complexité en temps d’un algo- rithme qui utilise intensivement les chaînes de caractères, il est nécessaire de savoir si l’on parle de chaînes mutables ou non et de connaître la stratégie d’extension des chaînes mutables.

Opérations d’insertion et de suppression de caractères

Quelle que soit la stratégie choisie (mutable ou non), la représentation d’une chaîne par une séquence contigüe de caractères pose des problèmes de performance pour les opérations d’in- sertion et de suppression de caractères.

Insérer un caractère au début d’une chaîne mutable implique de décaler en mémoire tous les caractères de la chaîne. Le temps de calcul est donc proportionnel à la longueur de la chaîne. Il en va de même pour l’opération de suppression.

Dans ces conditions, on immagine bien qu’un traitement de texte ne pourrait pas être effi- cace s’il gardait tout le texte sous forme d’une seule chaîne, le temps d’insertion d’un caractère au début d’un document de 1mio de caractères serait prohibitif. La représentation des textes né- cessite donc d’autres structures de données plus efficaces.

G. Falquet, CUI - Uni. Genève, 2001

34

Chaines de caractères

G. Falquet, CUI - Uni. Genève, 2001

4

Les types abstraits

4.1 Motivation

Considérons un programme qui doit traiter deux types de données: des dates et des poids. Une donnée de chacun de ces type peut s’exprimer par un nombre entier:

• le nombre de jours écoulés entre le 1.1.1800 et cette date;

• le nombre de grammes de ce poids.

Si ces deux types utilisent bien le même domaine de valeurs, ils diffèrent cependant sur les opérations appicables à ces valeurs. En effet, s’il est légitime de soustraire deux dates pour trou- ver le nombre de jours qui les séparent, on imagine difficilement l’intérêt d’additionnner deux dates. Par contre on peut bien calculer la différence entre deux poids ou la somme de deux poids. Pour le type date on pourra avoir une opération jour-de-la-semaine qui donne comme résultat l’une des chaînes de caractères “dimanche”, “lundi”, “mardi”, etc. suivant le jour de la semaine correspondant à cette date. Une telle opération n’a évidemment pas de sens avec un poids.

Il est donc normal de considérer qu’un type de données n’est pas seulement un ensemble de valeurs mais un ensemble de valeurs muni d’un ensemble d’opérations (de même qu’en mathé- matiques un groupe est un ensemble muni d’une opération, un anneau est un ensemble muni de deux opérations, etc.).

On peut même aller plus loin en disant qu’il suffit pour décrire un type de données, de décrire très précisément ses opérations, sans donner explicitement l’ensemble de ses valeurs. On parle dans ce cas de type abstrait. L’avantage de la définition abstraite est qu’elle se concentre sur ce qu’on peut faire avec des objets de ce type, indépendamment de la manière dont ces objets sont représentés. Par exemple, dans la définition abstraite d’un type date on n’a pas besoin de spéci- fier si les dates sont représentées par des entiers, des triplets (jour, mois, année) ou des chaînes de caractères. Ce n’est qu’au moment de l’implémentation concrète que l’on choisira une représentation en fonction de critères tels que la performance, l’économie de place mémoire, la simplicité de programmation, etc.

4.2 Exemples informels

On peut définir un type Date par les opréations suivantes, dont on donne les types des paramètres et du résultat :

différence: (Date, Date) Nombre entier (en nombre de jours) jour de la semainre: Date Chaîne de caractères en jours : Date Entier (la date exprimée en nb. de jours depuis le 1.1.1900) ajouter jours: (Date, Entier) Date (la date n jours après) année : Date Entier (année de cette date) mois : Date Entier (no. du mois) jour : Date Entier (no. du jour) définir une date : Entier, Entier, Entie Date (créer une date à partir de jour, mois, année)

Si les définitions ci-dessus nous disent bien quelles opérations on peut effectuer sur ce type de données, elles ne sont pas suffisante pour décrire précisément l’effet de ces opérations. Une manière de décrire cet effet consiste à écrire des équations qui mettent en rapport les opérations. Par exemple :

36

Les types abstraits

différence(d1, d2) = en jours(d1) – en jours(d2) année(définir une date(j, m, a)) = a en jours(définir une date(j, m, a)) = une fonction assez compliquée de j, m et a Faisons de même pour un type Disque défini par les coordonnées de son centre et son rayon et dont on s’intéresse à la surface.

surface : Disque Nombre rayon : Disque Nombre définir un disque : Nombre, Nombre, Nombre Disque coord x du centre : Disque Nombre coord y du centre : Disque Nombre

Quelques équations

coord x du centre(définir un disque(u, v, r)) = u coord y du centre(définir un disque(u, v, r)) = v surface(d) = rayon(d) * rayon(d) * Pi

4.3 Spécification algébrique des types abstraits

L’approche algébrique consiste à pousser le plus loin possible l’affirmation «les types ne sont pas des ensembles». Plutôt que de définir un type de données par la construction d’un en- semble de valeurs, on s’intéresse uniquement aux opérations sur ces valeurs et aux propriétés de ces opérations. L’idée est que la définition précise des opérations va conduire inéluctablement à une définition précise de l’esemble des valeurs. On considère en général qu’un approche qui se concentre sur les opérations (ou sur les fonctions) est plus abstraite qu’une approche qui cherche à décrire les éléments d’un ensemble.

Décrire algébriquement des types de données consiste à définir trois ensembles: les sortes , la sig- nature des opérations et les axiomes . Les sortes ne sont rien d’autre que des noms servant à représenter des ensembles de valeurs sur lesquels vont porter les opérations. Par exemple

naturel, booleen, entier, cercle, rectangle, personne, avion, …

On ne dit rien de plus sur ces ensembles que leur nom.

La signature d’une spécification définit pour chaque opération les sortes de ses paramètres et la sorte du résultat. Par exemple

addition: naturel, naturel -> naturel

spécifie que l’addition est une opération qui a deux paramètres de sorte naturel qui produit un résultat également naturel.

est-pair: naturel -> booleen

l’opération est-pair prend un paramètre naturel et rend un résultat booléen.

zero: -> naturel

zero est une opération constante qui n’a pas de paramètre et retourne un naturel, qui sera toujours le même.

La forme générale de la signature d’une opérations n-aire est:

G. Falquet, CUI - Uni. Genève, 2001

Les types abstraits

37

opération : sorte 1 , sorte 2 , …, sorte n -> sorte r .

Une expression construite à l’aide des opérations et de variables et qui respecte la signature est appelée un terme . Par exemple:

zero est-pair(zero) addition(Y, addition(X, zero))

Parmi les opérations certaines sont appelées génératrices , ce sont celles qui serviront à constru- ire les valeurs d’une sorte. Par exemple, l’opération “sucesseur” permet de construire tous les entiers à partir de la constante zéro.

Les axiomes décrivent les propriétés des opérations sous forme d’équivalences entre termes. Par exemple

addition(X, Y) == addition(Y, X)

signifie que pour toute valeur des variables X et Y, si l’on change l’ordre des paramètres de l’opération d’addition on obtient une expression équivalente, c-à-d que l’addition est commuta- tive.

addition(X, zero) == X

signifie que l’addition de la constante zero à n’importe quelle valeur donne la valeur elle-même.

Le symbole “==” doit se lire comme «est équivalent à», il n’a pas de direction privilégiée et sig- nifie qu’on peut remplacer ce qui est à gauche par ce qui est à droite et réciproquement.

L’application des axiomes à des termes et des sous-termes permet d’obtenir d’autres expressions équivalentes. Par exemple, l’expression

addition(zero, X)

est équivalente à

addition(X, zero)

par application du premier axiome. L’application du second axiome nous donne ensuite

X.

Nous avons donc prouvé que addition(zero, X) == X .

En général trouver la valeur d’une expression consiste à la réduire, grâce aux axiomes, à une ex- pression équivalente qui ne contient que des constantes et des opérations génératrices.

La forme générale des axiomes est:

terme g1 == terme d1 et … et terme gn == terme dn => terme 1 == terme 2

La partie avant le “=>” restreint l’application de l’équivalence à terme 1 et terme 2 , c-à-d terme 1 est équivalent terme 2 seulement si l’on peut auparavant prouver toutes les équivalences qui se trouvent avant le signe “=>”. Pour alléger l’écriture, on remplacera souvent les deux équations

b

== vrai => t1 == t2 (où b est un terme de sorte booléen)

b

== faux => t1 == t3

par un équation compacte

G. Falquet, CUI - Uni. Genève, 2001

38

Les types abstraits

t1 == si b alors t2 sinon t3

ou encore par

t1 == t2 si b, == t3 sinon

4.4 Exemple: spécification d’un type Cercle

Reprenon le type cercle esquissé précédemment et formalisons sa définition (tout en lui ajoutant les opérations de calcul du périmètre et de translation horizontale).

SPECIFICATION Cercle SORTES entier, rationnel, cercle OPERATIONS perimetre : cercle -> rationnel rayon : cercle -> entier cercle-unité : -> cercle centre-x : cercle -> entier centre-y : cercle -> entier translater : cercle, entier, entier -> cercle elargir : cercle, entier -> entier cercle : entier, entier, entier -> cercle cercle : entier -> cercle AXIOMES VARIABLES X, Y, Z, DX, DY: entier; C: cercle;

centre-x(cercle-unité) == 0 centre-y(cercle-unité) == 0 rayon(cercle-unité) == 1

rayon(cercle(X,Y,Z)) == Z centre-x(cercle(X,Y,Z)) == X centre-y(cercle(X,Y,Z)) == Y

centre-x(translater(C, DX)) == centre-x(C)+DX centre-y(translater(C, DY)) == centre-y(C)+DY rayon(translation-horizontale(C, DX)) == rayon(C)

centre-x(elargir(C, D)) == centre-x(C) centre-y(elargir(C, D)) == centre-y(C) rayon(elargir(C, D)) == rayon(C)+D

PRE elargir(C, X) == rayon(C) + X 0

Les trois premiers axiomes définissent ce qu’est un cercle unité : un cercle centré en (0, 0) et de rayon 1. Les trois axiomes suivants établissent quelques évidences, du genre, le rayon d’un cer- cle centré en (X, Y) et de rayon Z vaut Z, etc. On a ensuite trois axiomes qui nous disent qu’après une translation la coordonnée X du centre est modifiée, de même que la coordonnée Y alors que le centre reste le même. Finalement les trois derniers axiomes définissent l’opération élargir qui ne modifie par le centre mais uniquement le rayon.

À la suite des axiomes, la spécification contient une précondition sur l’opération elargir. Celle-ci spécifie que l’opération ne peut être utilisée que si ses opérandes C et X satisfont la con- dition rayon(C) + X 0. Cette précaution nous permet de garantir que le rayon d’un cercle est toujours un nombre positif ou nul. L’opération elargir n’est donc pas une fonction totale, il y a des valeurs de C et X pour lesquelles elle n’est pas définie.

G. Falquet, CUI - Uni. Genève, 2001

Les types abstraits

39

4.5 Spécification de quelqus types élémentaires

Si l’on veut construire complètement un ensemble de types abstraits algébriques il faut comenc- er par définir les “briques de base” que sont les nombres naturels et les valeurs booléennes. Com- mençons par une spécification des booléens:

SPECIFICATION Booléens SORTES bool OPERATIONS vrai

:

:

-> bool;

faux

non

: bool

-> bool; -> bool;

_et_

: bool, bool

-> bool; // notation infixée

_ou_

: bool, bool

-> bool;

AXIOMES VARIABLES X : bool;

[1]

non(vrai) == faux;

[2]

non(faux) == vrai;

[3]

vrai et X == X;

[4]

faux et X == faux;

[5]

vrai ou X == vrai;

[6]

faux ou X == X;

Ces axiomes permettent de réduire à vrai ou faux toute expression formée de et, ou et non et des constantes vrai et faux. Par exemple:

non(vrai ou (faux et non(vrai))) == non(vrai ou (faux et faux)) (par [1]) == non(vrai ou faux) (par [4]) == non(vrai) (par [5]) == faux (par [1]).

À partir de cette spécification on peut construire les entiers naturels

SPECIFICATION Nat UTILISE Bool SORTES nat OPERATIONS 0 : -> nat; succ : nat -> nat; _+_ : nat, nat -> nat; _-_ : nat, nat -> nat; _*_ : nat, nat -> nat; _^_ : nat, nat -> nat; _=_ : nat, nat -> bool; AXIOMES VAR X, Y : nat;

[1] X + 0 == X;

[2] X + succ(Y) == succ(X + Y);

[3] 0 - X == 0;

-- convention pour éviter de sortir des entiers

-- naturels.

[4] X - 0 == X; [5] succ(X) - succ(Y) == X - Y;

[6] X * 0 == 0;

G. Falquet, CUI - Uni. Genève, 2001

40

Les types abstraits

[7] X * succ(Y) == X + (X * Y); [8] X ^ 0 == succ(0); [9] X ^ succ(Y) == X * (X ^ Y); [10] 0 = 0 == vrai; [11] succ(X) = 0 == faux; [12] 0 = succ(X) == faux; [13] succ(X) = succ(Y) == X = Y

Calculons 2+1:

succ(succ(0)) + succ(0) == succ(succ(succ(0)) + 0) -- [par 2 en prenant X=succ(succ(0)) et Y=0] == succ(succ(succ(0))) -- [par 1]

4.6 Spécification des tableaux

Un tableau est une collection indexée d’éléments du même type. Chaque élément du tableau est repéré par son indice. On peut accéder à l’élément no. i et le modifier. La spécification ci- dessous décrit un tableau dont les éléments sont d’une sorte t quelconque définie dans une spéci- fication T. T est mis entre parenthèse pour indiquer qu’il s’agit d’un paramètre interchangeable, c’est-à-dire qu’on peut remplacer T par n’importe quelle spécification conforme à T (voir plus bas). Les noms bizarres des opérations ont été choisis de façon à correspondre à la syntaxe qu’on trouve dans certaines langages de programmation.

SPECIFICATION Tableau(T) UTILISE Bool, Nat SORTES tab OPERATIONS init(_) : t -> tab; _[_] : tab, nat -> t; _[_] _ : tab, nat, t -> tab;

AXIOMES VAR A, B : tab; I : nat; X : t

init(X)[I] == X (A[J] X)[I] == si I = J alors X sinon A[I]

Le premier axiome indique qu’après avoir initialisé un tableau avec la valeur X, l’accès à n’importe quel élément I donne X comme valeur. Le second dit que si l’on donne la valeur X au Jème élément et qu’on demande la valeur du Jème élément alors on trouve bien X, et les autres éléments restent inchangés. On notera que les tableaux ainsi défini sont de taille illimitée.

Notre spécification de tableau est générique, on peut l’utiliser pour faire des tableaux de booléens, de naturels, etc. Il suffit pour cela de remplacer T par une spécification existante. Par exemple :

SPECIFICATION TableauNat = Tableau(T Nat avec t nat)

T est remplacée par Nat et la sorte t par la sorte nat de Nat.

Voyons le fonctionnement de notre spécification TableauNat sur quelques expressions :

(((init(a)[3] b)[2]

== ((init(a)[3] b)[2] c)[3] == (init(a)[3] b)[3]

c)[4] b)[3]

G. Falquet, CUI - Uni. Genève, 2001

Les types abstraits

41

== b ((((init(a)[3] b)[2]

== (((init(a)[3] b)[2] c)[3] w)[3] == w

c)[3] w)[4] b)[3]

Ce dernier cas montre que la valeur trouvée à l’indice 3 est bien la dernière qu’on y a mise. Le w a remplacé le b qu’on avait mis au début.

((((init(a)[3] b)[2] c)[3] w)[4] b)[8] ==

Dans ce cas on trouve la valeur d’initialisation car on n’a rien mis à l’indice 8.

Tableaux de taille fixée

== init(a)[8] == a

Dans ce cas on veut que le tableau soit constitué d’un nombre d’élément fixe, défini au mo- ment de son initialisation. On ajoute donc un paramètre pour fixer la longueur à l’opératin init et on ajoute une opération longueur pour accéder à la longueur du tableau.

SPECIFICATION Tableau(T) UTILISE Bool, Nat SORTES tab OPERATIONS init(_, _) : T, nat -> tab; _[_] : tab, nat -> T; _[_] _ : tab, nat, T -> tab; longueur(_) : tab -> nat;

AXIOMES VAR A, B : tab; I, N : nat; X : T

init(X, N)[I] == X (A[J] X)[I] == si I = J alors X sinon A[I]

longueur(init(A, N)) == N; longueur(A[I] X) == longueur(A);

PRE A[I] X == I < longueur(A); PRE A[I] == I < longueur(A);

Les deux préconditions sont là pour éviter tout accès à un indice hors du tableau (pour un tableau de taille N les indices vont de 0 à N–1).

4.7 Les types produits cartésiens

Le produit cartésien de n ensembles E 1 , E 2 , …, E n est formé de tous les n-tuples (a 1 , a 2 , …, a n ) où chaque a i est un élément de E i . Les opérations qui nous intéressent sur les n-tuples sont :

accéder au i e composant

modifier le i e composant

On peut définir une structure générale pour la spécification de type produit cartésien, selon le schéma suivant :

SPECIFICATION Prod(S1, S2, …, Sn) SORTES : n-tuple OPERATIONS :

(_, _, …, _): s1, s2,

, sn n-tuple

G. Falquet, CUI - Uni. Genève, 2001

42

Les types abstraits

a1 : n-tuple s1

an : n-tuple sn m1 : s1, n-tuple produit

mn : sn, n-tuple produit

Les opérations a1 à an servent à accéder aux composants alors que m1 à mn servent à mod- ifier les composants d’un produit. Les schémas d’axiomes sont :

AXIOMES:

-- n axiomes de la forme

ai((x1, x2,

-- n axiomes ai(mi(x, p)) == x (i = 1, 2, -- n x (n-1) axiomes ai(mj(x, p)) == ai(p) (i = 1,

,

xn) == xi (i = 1, 2,

, n)

, n)

, -- spécifie que la modification du composant i n’affecte pas -- les autres composants

n; j = 1,

, n; i j)

À partir d’une telle spécification générique on pourra définir des produits sur des types con- nus. Par exemple, on peut représenter les informations qu’on a au sujet d’une personne (nom, prénom, age) par des triplets (n, p, a) obéissant à la spécification

SPECIFICATION Personne = Produit(Chaine, Chaine, Nat)

Pour rendre l’usage de cette spécification plus explicite on peut renommer les opérations de la manière suivante :

SPECIFICATION Personne = Produit(Chaine, Chaine, Nat) avec a1 renommé nom, a2 renommé prénom, a3 renommé age, m1 renommé m-nom, m2 renommé m-prénom, m3 renommé m-age

Remarque: les types produit cartésien existent dans la plupart des langages de programmation, sous différentes appellations : record en Pascal, struct en C, classes internes en Java, etc. Les composants des tuples s’appellent souvent des champs.

4.8 Spécification du type Chaînes de caractères

Le type chaîne de caractère se distingue d’un tableau de caractères par le fait que la taille de la chaîne à un instant donné est connue et par les opérations de concaténation et d’égalité qu’on peut appliquer aux chaînes.

SPECIFICATION CCar UTILISE Bool, Nat SORTES ccar OPERATIONS "" : -> ccar; "_" : car -> ccar; _+_ : ccar, ccar -> ccar; _=_ : ccar, ccar -> bool; _=_depuis_ : ccar, ccar, nat -> bool longueur : ccar -> nat; _[_] : ccar, nat -> car;

- - op. auxiliaire pour ax.

G. Falquet, CUI - Uni. Genève, 2001

Les types abstraits

43

AXIOMES VAR X, Y : ccar; I : nat;

""+X == X; X+"" == X; longueur("") == 0; longueur("C") == 1; longueur(X+Y) == longueur(X)+longueur(Y) "C"[0] == C

X+Y[I] == si I < longueur(X) alors X[I] sinon Y[I-longueur(X)]

X

= Y depuis I == si I longueur(X) alors longueur(X) = longueur(Y) sinon X[I] = Y[I] et X = Y depuis I+1

X

= Y == X = Y depuis 0

PRE X[I] == I < longueur(X)

Explications:

longueur: la chaîne vide est de longueur nulle, une chaîne composée d’un seul caractère est de longueur 1, deux chaînes concaténées par l’opération + additionnent leur longueurs.

accès aux éléments: l’élément 0 d’une chaîne composée d’un seul caractère est ce caractère; pour trouver l’élément i dans la concaténation de deux chaînes x et y, si i est plus petit que la longueur de x il faut prendre le i e caractère de x, sinon il faut prendre le (i-longeur de x) e car- actère de y.

L’égalité est plus difficile à définir. On utilise une opération auxiliaire x = y depuis i qui donne vrai si les chaînes x et y ont les même caractères à partir de la position i.

4.9 Introduction à l’implémentation orientée-objet des types

Cette section présente les concepts généraux de l’implémentation orientée-objet des types de données. Les techniques d’implémentation proprement dites seront abordées dans le chapitre suivant la présentation des collections, des arbres et des graphes.

Dans un système à objets (orienté-objet), un objet est une paire ( oid , v ) où oid est l’identité de l’objet et v est sa valeur. L’identité d’un objet sert à repérer celui-ci, elle est invariable au cours du temps. Un objet peut par contre changer de valeur (à condition qu’il soit mutable ). De plus, plusieurs objets peuvent avoir la même valeur.

La valeur d’un objet est représentée par une ou plusieurs variables d’instance de divers types élémentaires (entiers, réels, …) ou complexes. Implémenter un type abstrait consiste alors à

• définir la structure interne des objets, c’est-à-dire les variables d’instance qui vont représenter les valeurs de ce type et

• définir les algorithmes nécessaires pour réaliser les opérations du type.

Pour implémenter une type abstrait T on procédera par agrégation, c’est à dire qu’on construira un objet de type T en utilisant un des objets de types T 1 , T 2 , …, T k déjà implémentés. Attention, il ne faut pas confondre cette technique avec l’héritage multiple. De même, l’implémentation de chacun des T i se base sur d’autres types déjà implémentés, et ainsi de suite. Il y a donc construc- tion d’une hiérarchie d’abstraction de types (qui n’est pas la hiérarchie des sous-classes) qui re- pose sur les types les plus simples ou types de base.

G. Falquet, CUI - Uni. Genève, 2001

44

Les types abstraits

Suivant le langage et l’environnement utilisé, les types de base peuvent varier. Nous con- sidérerons comme types de base :

• les type élémentaire (entier, flottant, caractères, booléen)

• la référence à un objet (identité d’un objet, pointeur)

• les tableaux de valeurs élémentaires ou de références.

Un exemple: implémentation du type Cercle

Nous avons précédemment défini un type abstrait cercle muni des opérations cercle-unité , translater , elargir , rayon , centre-x et centre-y .

Une implémentation possible de ce type consiste à définir une classe d’objets Cercle avec comme structure interne les trois variables :

entier x

// coordonnée x du centre

entier y

// coordonnée y du centre

entier r

// rayon

L’opération translater sera implémentée par la procédure

procedure translater(Cercle c, entier dx, entier dy) { c.x c.x + dx; c.y c.y + dy;

}

L’opération cercle-unité est un peu particulière car elle doit créer un nouveau cercle à partir de rien. Pour cela nous supposeerons qu’il existe toujours une procédure new T qui produit un nouvel objet du type T dont les variables ne sont pas initialisées.

procédure cercle_unité { c new Cercle; c.x 0; c.y 0:

c.r 1; retourne c

}

L’opération new nous permettra également de réaliser des implémentation immuables qui ne changent jamais la valeur d’un objet mais en crée un nouveau. Un implémentation immuable de translater pourrait être :

procedure translater(Cercle c, entier dx, entier dy) { res = new Cercle; // crée un nouvel objet res.x x + dx; res.y y + dy; retourne res;

}

Propriétés de l’implémentation

On s’intéressera particulièrement à deux propriétés d’une implémentation d’un type :

• la correction (obligatoire): les opérations implémentées doivent satisfaire les contraint- es de la spécification du type abstrait: types des paramètres et résultats, équations, in- variants.

G. Falquet, CUI - Uni. Genève, 2001

Les types abstraits

45

• la complexité (mesure): pour chaque implémentation on veut connaaître la complexité en temps de chaque opération (évolution du temps de calcul en fonction de la taille des objets traités) et la complexité en espace (mémoire occupée par la structure de don- nées). Une implémentation peut aussi posséder d’autres propriétés moins formellement définies:

• simplicité : il est clair que plus une implémentation est simple dans sa structure et ses algorithmes, plus il sera facile de la vérifier, de la corriger et de la maintenir à jour.

• utilisabilité : l’implémentation n’a pas besoin de définir exactement les mêmes opéra- tions que le type abstrait. Dans l’exemple du cercle, on pourrait définir une procédure translateEtAgrandit(x, y, r) qui fait à la fois une translation et un agrandissement du cer- cle. Ou au contraire on pourrait implémenter l’opération translater par deux procé-

dures : translaterHorizontalement(x) et translaterVerticalement(y) .Un bon choix des

procédures qui constituent l’interface du type peut grandement simplifier son utilisa- tion.

• extensibilité et réutilisabilité : dans quelle mesure les choix qui ont été faits du point de vue de l’interface et de l’implémentation des procédures aident-ils ou empêchent-ils la réutilisation de ce travail pour définir d’autres types ?

G. Falquet, CUI - Uni. Genève, 2001

46

Les types abstraits

G. Falquet, CUI - Uni. Genève, 2001

5

Les Types Abstraits Collection

Les types abstraits qui représentent des collections de données jouent un rôle important dans la modélisation de l’information et dans la programmation des algorithmes. Un objet d’un type collection est un conteneur d’objets qui possède un protocole particulier pour l’ajout, le retrait et la recherche d’éléments. Dans ce chapitre nous explorerons les types de collections les plus utilisés. Pour chacun nous donnerons une spécification sous forme d’un type abstrait algébrique, qui définira la sémantique des opérations.

5.1

Piles

Une pile est une collection d’objets qui obéit au protocole FILO (First In Last Out), on ne peut accéder et retirer de la pile que le dernier élément qu’on y a mis. On appelle cet élément le sommet de la pile.

5.1.1 Le type abstrait: spécification algébrique

Les éléments qu’on met dans une pile n’ont aucune condition particulière à remplir, il suffit qu’ils obéissent à la spécification triviale

SPECIFICATION Element SORTES elem

On définit ensuite une notion générique de pile dont les éléments sont de la sorte elem.:

SPECIFICATION Pile(Element) UTILISE Bool, Nat SORTES pile OPERATIONS -- constructeurs vide : pile; empiler : elem, pile pile; depiler : pile pile -- sélecteurs sommet : pile elem; est-vide : pile bool; AXIOMES VAR E, E1, E2 : elem; P : pile depiler(empiler(E,P)) == P

sommet(empiler(E,P)) == E

est-vide(vide) == true

.

est-vide(empiler(E,P)) == false

PRE depiler(P) == non est-vide(P)

// empiler puis dépiler laisse la pile dans l’état initial

// après avoir empilé E, il se trouve au sommet de la pile

// une pile vide est vide

// après avoir empilé un élément la pile n’est pas vide

Exemple 1. Algorithme de vérification de l’équilibrage des parenthèses dans un texte.

Il s’agit de vérifier qu’un texte qui contient des caractères standard, des parenthèses ou- vrantes "(", "[", ou "{" et des parenthèses fermantes ")", "]" ou "}" est syntaxiquement correct du point de vue des parenthèses. Cela signifie qu’à toute parenthèse ouvrante doit correspondre, plus loin dans le texte, une parenthèse fermante du même type. Le texte compris entre ces deux

48

Les Types Abstraits Collection

parenthèses doit également être correct : une parenthèse ouverte doit y être refermée. L’algo- rithme ci-dessous utilise une pile de caractères, de la sorte pile de Pile(Element Caractère) pour mémoriser les ouvertures de parenthèses.

Entrée: un tableau c de N caractères qui contient le texte Sortie: "OK" si les parenthèses sont bien placées, un message d’erreur sinon avec Pile(Caractère) pile p vide; pour i de 0 à N-1 { si (c[i] = <<une parenthèse ouvrante>>) p empiler(p, c[i]) sinon si (c[i] <<est une parenthèse fermante>>) { si (est-vide(p)) retourne "ERREUR: il manque une parenthèse ouvrante" si (sommet(p) <<est du même type que>> c[i]) p depiler(p) sinon retourne "ERREUR: parenthèses de types différents"

}

}

si (est-vide(p)) retourne "OK" sinon retourne "ERREUR: il manque au moins une parenthèse fermante"

Pseudo-code

Dans cet algorithme, les parties entre << et >> n’ont pas été définies formellement. Il faud- rait les écrire dans le langage algorithmique pour obtenir un véritable algorithme. Cependant on se persuade aisément que leur écriture ne poserait pas de problème particulier, il n’est donc pas nécessaire de les détailler ici. Dans ce cas on parle d’expression en pseudo-code . Le pseudo-code n’est acceptable que si les parties informelles sont soit évidentes à formaliser, soit formalisée séparément dans le langage algorithmique ou en pseudo-code. Par exemple, le fragment in- formel

sommet(p) <<est du même type que>> c[i] peut se formaliser en

(sommet(p) = "(" et c[i] = ")") ou (sommet(p) = "{" et c[i] = "}") ou (sommet(p) = "[" et c[i] = "]")

L’emploi du pseudo-code est souvent un bon moyen de concevoir les algorithmes ou les pro- grammes de manière “top-down”, c’est-à-dire en commençant par les grandes lignes puis en définissant peu à peu les détails laissés en suspens.

Notation orientée-objet

Dans l’algorithme de vérification de parenthèses on utilise dans l’instruction

p empiler(p, c[i])

qui modifie la valeur de la variable p. Si l’on veut marquer le fait que p est implémentée par un objet mutable, on employera la notation pointée

p.empiler(c[i])

G. Falquet, CUI - Uni. Genève, 2001

Les Types Abstraits Collection

49

qui montre bien qu’on agit sur l’objet p (ou l’objet référencé par p). On gardera la notation de l’affectation ( ) pour les objets non mutables ou pour montrer qu’on donne comme valeur à la variable un nouvel objet de la mémoire, comme c’est le cas avec p vide. Par souci d’unifor- mité on emploiera aussi cette notation pour les autres opération : p.sommet à la place de som-

met(p) .

5.1.2 Polymorphisme

Les langages à objets possèdent une notion de sous type, ou d’extension de type. Un sous- type U dans type T possède les mêmes opérations que T, plus des opérations propres 1 . Donc un objet de type U peut être utilisé partout où un objet de type T est requis (selon le principe “qui peut le plus peut le moins”). Donc si l’on a un type Forme muni des sous-types Rectangle, Carre et Cercle, il sera possible de placer dans une pile de Forme des objets de n’importe lequel de ces trois types. On obtiendra ainsi une pile polymorphe constituée d’objets de différents types (mais tous sous-types de Forme).

Dans beaucoup de systèmes à objets on a un type Objet dont tous les autres types sont des sous types. Donc si l’on crée une pile d’Objets on pourra mettra dedans n’importe quel objet de n’importe quel type. C’est la pile la plus polymorphe qu’on puisse créer.

5.2

Files

Une file («queue» en anglais) est une collection qui obéit au protocole FIFO (First In First Out). L’ajout d’un élément se fait à l’arrière de la file alors que le retrait se fait à l’avant. Les éléments restent dans le même ordre tant qu’ils sont dans la file (il est interdit de dépasser !). On appelle premier élément celui qui est devant tous les autres.

5.2.1 Spécification algébrique

SPECIFICATION File(Elem) UTILISE Bool, Nat SORTES file OPERATIONS -- constructeurs vide : -> file; entrer : elem, file -> file; sortir : file -> file -- sélecteurs premier : file -> elem; est-vide : file -> bool; longueur : file -> nat; AXIOMES VAR X : elem; F : file

1. longueur(vide) == 0

// La longueur d’une file vide est zéro

2. longueur(entrer(X,F)) == longueur(F) + 1 // Entrer un nouvel élélment augmente la longueur d’une unité

3. longueur(sortir(F)) == longueur(F) - 1 // Sortir un élément diminue la longueur d’une unité

4. premier(entrer(X,F))

== X

si longueur(F) = 0

== premier(F)

sinon

1. Il existe différentes définitions, plus ou moins sophistiquées, de la notion de sous-type. Nous nous con- tenterons de cette définition simple.

G. Falquet, CUI - Uni. Genève, 2001

50

Les Types Abstraits Collection

// Quand la file est vide, l’élément qu’on y entre devient le premier. // Quand elle n’est pas vide, le premier reste le même.

5. sortir(entrer(X,F))

==

F

si longueur(F) = 0

==

entrer(X,sortir(F))

sinon

// Si on entre un élément derrière une file vide et qu’on sort le premier // on retrouve une file vide. // Si la file n’est pas vide, on peut inverser les deux opérations.

PRE sortir(F) == non est-vide(F) PRE premier(F) == non est-vide(F)

Utilisation de l’axiome 5:

premier(sortir(sortir(entrer(11,entrer(7,entrer(4,vide)))))) == premier(sortir(entrer(11,sortir(entrer(7,entrer(4,vide)))))) == premier(sortir(entrer(11,entrer(7,sortir(entrer(4,vide)))))) == premier(sortir(entrer(11,entrer(7,vide)))) == premier(entrer(11,sortir(entrer(7,vide)))) == premier(entrer(11,vide)) == (par 4)

11

5.2.2 Utilisation des files

La notion de file est utilisée à chaque fois qu’il s’agit de gérer l’allocation d’une ressource à plusieurs «clients». Dans le cas le plus simple on procède sur la base du “premier arrivé pre- mier servi”. On peut créer des files plus sophistiquées dans lesquelles les clients ont des priorités différentes qui leur permettent de dépasser les clients moins prioritaires. De tels problèmes ap- paraîssent dans les systèmes d’exploitation d’ordinateurs, dans les réseaux de télécommunic- tions, ou dans les programmes dea gestion du courrier électronique.

Les files servent également de support au protocole «producteur consommateur» entre deux processus asynchrones. Dans ce cas un processus P produit des données qui sont envoyées à un processus C. Il faut passer par l’intermédiaire d’une file pour gérer les périodes pendant lesquelles P produit plus vite que C ne peut consommer.

Les files informatique peuvent également servir à simuler les files d’attentes bien réelles qui se créent dans diverses situations (guichets, trafic automobile, etc.).

5.3

Séquence

Une séquence est une collection d’objets du même type qui sont placés selon un ordre. Chaque objet possède donc une position. Le modèle mathématique d’une liste <a 1 , a 2 , …, a n > d’élé- ments de type T est la fonction {1 a 1 , 2 a 2 , …, n a