Vous êtes sur la page 1sur 160

Structures de données

notes de cours
G. Falquet, 1999

Table des matières


1 Types de données primitifs

2 Chaines de caractères

3 Les types abstraits

4 Les Types Abstraits Collection

5 Arbres

6 Graphes

7 Implémentation des Types Collection

8 Algorithmique

9 Données persistantes

10 Niveau physique des bases de données

http://cui.unige.ch/~falquet/std/notes/index.html [25-10-2001 20:18:31]


1 Types de données primitifs

[Remonter] [Precedent] [ Suivant]

1 Types de données primitifs


La structure des ordinateurs (processeur et mémoire) impose que toute information soit codée pour
pouvoir être traitée par une machine. Nous allons voir comment représenter l'information numérique
(nombres entiers et réels) et les signes alphabétiques (caractères). Pour chaque type de données nous
étudierons les différentes opérations qui lui sont associées.

1.1 Structure des machines

1.2 Représentation des nombres entiers

1.3 Les nombres flottants

1.4 Précision des calculs et analyse numérique

1.5 Conversions de type

1.6 Les Caractères

1.7 Le Standard Unicode

1.8 Définition des codes

1.9 Le type caractère

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.1.html [25-10-2001 20:18:36]


1 Types de données primitifs

http://cui.unige.ch/~falquet/std/notes/p1.html [25-10-2001 20:18:38]


1.1 Structure des machines

[Remonter] [Precedent] [ Suivant]

1.1 Structure des machines


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 composées des éléments suivants:
● processeur: effectue les calculs et autres traitements sur les données

● mémoire centrale: sert d'une part à stocker le programme à exécuter et d'autre part à stocker les
données à traiter
● mémoires auxiliaires: ces mémoires, de type disque dur, disque optique, etc.servent
essentiellement à augmenter la capacité de la mémoire centrale, mais également à assurer la
permanence des données et éventuellement leur transmission (par transport physique des disques).
● systèmes de communication (entrées/sorites): assurent la transmission d'informations 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.).
On retrouve une structure similaire dans les machines abstraites telles que les machines de Turing ou les
machines RAM (Random Acccess Machine) qui servent à étudier, d'un point de vue théorique, la notion
d'algorithme, de calculabilité, etc.
Nous considérerons donc qu'un ordinateur est un automate capable de manipuler des symboles (par
exemple des 0 et des 1) dans une mémoire.

Structure de la mémoire
Pour qu'un ordinateur puisse traiter une information il faut que celle-ci soit stockée, sous une forme
codée dans la mémoire. On parlera alors de donnée.
La mémoire possède pratiquement toujours une structure composée d'un ensemble de cellules 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 diffé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 exemple, 10011010. Un groupe de 8 bits est appelé
octet (ou byte en anglais).
Pour des raisons d'efficacité "électronique", les octes 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
Le schéma général de fonctionnement du processeur consiste à
1. "lire" le contenu des cellules mémoires contenant les données à traiter et les stocker
temporairement dans ses cellules mémoire locales appelées registres

http://cui.unige.ch/~falquet/std/notes/p1.2.html (1 of 2) [25-10-2001 20:18:43]


1.1 Structure des machines

2. effectuer les traitements sur les registres


3. "écrire" les résultats dans des cellules mémoire.
L'opération lire consiste à fournir une adresse à la mémoire, qui répond par le contenu de la cellule située
à cette adresse.
L'opération écrire consiste à fournir à la mémoire une adresse et une valeur, la mémoire va alors stocker
cette valeur dans la cellule située à l'adresse fournie.

Exemple d'une séquence d'instructions:


LIRE le contenu de la cellule 456665 dans le registre A1
LIRE le contenu de la cellule 56633 dans le registre A2
ADDITIONNER A1 et A2 et mettre le résultat dans A3
ECRIRE le contenu de A3 dans la cellule 771118

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.2.html (2 of 2) [25-10-2001 20:18:43]


1.2 Représentation des nombres entiers

[Remonter] [Precedent] [ Suivant]

1.2 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 composé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 adjacentes. 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 complé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 n-1 et 2 n-1 - 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.

http://cui.unige.ch/~falquet/std/notes/p1.3.html (1 of 3) [25-10-2001 20:18:59]


1.2 Représentation des nombres entiers

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

donc 127 + 3 = -126.


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

L'opération de division doit également être redéfinie de manière à fournir un nombre entier comme
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émoire suffisante, sous
forme de cellules contigües.
Cependant, la plupart des processeurs ne sont pas prévus pour traiter ce type de représentation. 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

http://cui.unige.ch/~falquet/std/notes/p1.3.html (2 of 3) [25-10-2001 20:18:59]


1.2 Représentation des nombres entiers

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:


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

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.3.html (3 of 3) [25-10-2001 20:18:59]


1.3 Les nombres flottants

[Remonter] [Precedent] [ Suivant]

1.3 Les nombres flottants


Les nombres flottants (ou nombres à virgule flottante) sont des représentations de certains nombres 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ésentation de taille finie pour chaque
réel. La représentation d'un nombre réel sera donc une approximation, c'est à dire la représentation d'un
autre nombre, suffisament proche.
Par exemple, sur une calculette à dix chiffres on représente le nombre p 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
a) [1 10000000 010010000000000000000000 ] représente
- 1.01001 ¥ 2 10000000-127 = -(1 + 1/2 2 + 1/2 5 ) ¥ 2 128-127 = -(2 + 1/2 + 1/2 4 ) = 2.5625
b) Représentation de 1/3
1/3 = 0.010101010101010101010... en binaire
= 1.01010101010101010... ¥ 2 -2
donc:
signe: 0
exposant: 127-2 = 125 = 01111101 en binaire
mantisse: 01010101010101010101010
Le nombre 1/3 est donc représenté par la séquence de 32 bits

http://cui.unige.ch/~falquet/std/notes/p1.4.html (1 of 2) [25-10-2001 20:19:07]


1.3 Les nombres flottants

[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 nombre.
Par exemple, la représentation de 1/3, ci-dessus, néglige les chiffres après la 23 ème position. L'erreur est
donc 0.0....(23 fois 0) ...01010101010 ¥ 2 -2 @ 1.5 ¥ 10 -8 .
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'erreur est:
0.0...(23 fois)...011111111... ¥ 2 exp-127 = 2 -23 ¥ 2 exp-127 .
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

Cela signifie que l'erreur relative maximum pour la représentation d'un nombre réel en simple 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 .
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.4.html (2 of 2) [25-10-2001 20:19:07]


1.4 Précision des calculs et analyse numérique

[Remonter] [Precedent] [ Suivant]

1.4 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
a + b = c (1 + e ab )

Le terme e 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

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 e
| s - s2 | £ 9.2 e
où e est la précision de représentation des nombres (p.ex. 10 -7 pour les float ).
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

http://cui.unige.ch/~falquet/std/notes/p1.5.html (1 of 4) [25-10-2001 20:19:22]


1.4 Précision des calculs et analyse numérique

fonction exponentielle avec la formule de Taylor:

On utilise l'algorithme suivant pour calculer une approximation de ex en prenant les 20 premier 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:
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

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

http://cui.unige.ch/~falquet/std/notes/p1.5.html (2 of 4) [25-10-2001 20:19:22]


1.4 Précision des calculs et analyse numérique

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 soustraction
qui nous a fait perdre beaucoup de précision. C'est un exemple de problème mal conditionné.
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

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+ e ).
L'erreur de calcul est donc F(b(1+ e )) - F(b). F(b(1+ e )) = F(b + b e) est approximativement égal à
F(b)+F'(b)b e . L'erreur ralative est donc |(F(b)+F'(b)b e - F(b)) / F(b) |.

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) calculé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 +
e ) avec e 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' e , même assez grand, tel que

e -10+e = -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 correspondance bi-univoque
entre l'arithmétique en virgule flottante des machines et celle du corps R des réels.

http://cui.unige.ch/~falquet/std/notes/p1.5.html (3 of 4) [25-10-2001 20:19:22]


1.4 Précision des calculs et analyse numérique

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.5.html (4 of 4) [25-10-2001 20:19:22]


1.5 Conversions de type

[Remonter] [Precedent] [ Suivant]

1.5 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é exactement 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 simplement à
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

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 justification 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.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.6.html [25-10-2001 20:19:26]


1.6 Les Caractères

[Remonter] [Precedent] [ Suivant]

1.6 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 caractè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. bits nb. caractères représentables nom remarques
Permet de représenter les 26 lettres majuscules
latines, les chiffres 0..9, les symboles de ponctuation:
6 64
, : ; . ( ) etc. Suffisant pour écrire des programmes et
imprimer des résultats.
Standard adapté à la langue américaine : lettres
7 128 ASCII majuscules et minuscules sans accents, chiffres,
symboles de ponctuation.
Représentation des lettres latines maj. et min. y
8 256 IBM-PC 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 Macintosh
Ensemble de standards de représentation adaptés à
différents groupes linguistiques (p.ex. ISO-Latin-1
8 256 ISO-xxx
permet la représentations des lettres utilisées dans les
langues latines)
Un standard unique pour représenter tous les
16 65536 Unicode caractères utilisés dans le monde, y compris les
idéogrammes.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.7.html [25-10-2001 20:19:30]


1.7 Le Standard Unicode

[Remonter] [Precedent] [ Suivant]

1.7 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 linguistique.
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, Sinhala, 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 visuellement un
caractère, par exemple
● AAAAA

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'écran.

http://cui.unige.ch/~falquet/std/notes/p1.8.html (1 of 2) [25-10-2001 20:19:34]


1.7 Le Standard Unicode

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.

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

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.8.html (2 of 2) [25-10-2001 20:19:34]


1.8 Définition des codes

[Remonter] [Precedent] [ Suivant]

1.8 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 ..
FF = Latin-1). De plus on a défini une notion de script qui est un système cohérent de caractères utilisés
par plusieurs langues. Ceci afin d'éviter les duplification. Par exemple, le chinois, le japonais et le coréen
utilisent tous le même script (nommé CJK) car ils ont plusieurs milliers 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 appelés les
valeurs de code
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 caractè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 characters].
L'ordre "alphabétique" à l'intérieur d'un script est si possible maintenu

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

http://cui.unige.ch/~falquet/std/notes/p1.9.html (1 of 2) [25-10-2001 20:19:40]


1.8 Définition des codes

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 caractè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

● 0x10000 .. 0x10FFF ==> 4 bytes

● conversion sans perte vers et de UTF-16

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.9.html (2 of 2) [25-10-2001 20:19:40]


1.9 Le type caractère

[Remonter] [Precedent] [ Suivant]

1.9 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 données des
caractères ainsi que l'application de règles de compositions non triviales.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p1.a.html [25-10-2001 20:19:45]


2 Chaines de caractères

[Remonter] [Precedent] [ Suivant]

2 Chaines de caractères
2.1 Vision abstraite des chaînes

2.2 Représentation concrète des chaînes

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p2.1.html [25-10-2001 20:19:51]


2.1 Vision abstraite des chaînes

[Remonter] [Precedent] [ Suivant]

2.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 possibles (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 i-1 , 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"

http://cui.unige.ch/~falquet/std/notes/p2.2.html (1 of 3) [25-10-2001 20:19:58]


2.1 Vision abstraite des chaînes

"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
● 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
java.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'alphabet (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 p-1 = d p-1 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

http://cui.unige.ch/~falquet/std/notes/p2.2.html (2 of 3) [25-10-2001 20:19:58]


2.1 Vision abstraite des chaînes

{"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

1. (s + t) + u = s + (t + u) (associativité)
2. (s + "") = s = "" + s (la chaîne vide "" est l'élément neutre)
Il est possible de construire toutes les chaînes de S par concaténation à partir des chaines primitives
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 abstraite des
types de données.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p2.2.html (3 of 3) [25-10-2001 20:19:58]


2.2 Représentation concrète des chaînes

[Remonter] [Precedent] [ Suivant]

2.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ésentation des
caractères. Mais d'autres questions entrent également en jeu du fait que
● les chaînes peuvent avoir différentes tailles (il n'y a en théorie pas de taille maximum)

● l'espace mémoire à disposition n'est pas infini

Parmi ces questions on trouve le choix entre une représentation immuable ou mutable.

Représentation immuable
Dans ce qui suit nous utilisons le terme objet pour désigner aussi bien un objet d'un langage OO qu'une
zone de la mémoire (tableau, structure, etc.) d'un langage non OO.
Dans une représentation immuable, une chaîne est un objet qui ne peut changer de valeur au cours du
temps. Par conséquent les résultat d'une opération, p.ex. de concaténation, est toujours un nouvel objet.
Par exemple :
objet 1 ("cos") + objet 2 ("mos") Æ objet 3 ("cosmos")

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


● objets partageables entre variables (économie de place sans problèmes d'alias, voir ci-dessous) ;

● pas besoin de prévoir une structure de stockage extensible ;

● il peut y avoir une grande consommation de ressources (allocation/désallocation) si un programme


effectue de nombreuses opérations sur les chaînes.

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éréations. On aura par exemple :
objet 1 ("cos") .ajoute( objet 2 ("mos") ) Æ objet 1 ("cosmos")

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


● moins de consommation de ressource (allocation/déallocation) si l'implémentation est bien prévue

● l'objet de stockage doit être extensible

● des problèmes d'alias peuvent apparaître

http://cui.unige.ch/~falquet/std/notes/p2.3.html (1 of 3) [25-10-2001 20:20:05]


2.2 Représentation concrète des chaînes

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. Par exemple, supposons que A et B désignent tous les deux le même objet en mémoire qui contient
la chaîne "Encore". Si l'on modifie cet objet à travers une opération sur A
A.ajoute(" vous!")
on a simultannément modifié B sans le dire explicitement, ce qui tend à rendre le programme moins
lisible et donc plus sujet à erreurs. 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.ajoute(" vous");
B vaudra "encore" et A vaudra "encore vous".

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ération
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 programme 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 et les désallouent. Un objet est
considéré comme jetables s'il est plus accessible par un programme, 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 jetables.

http://cui.unige.ch/~falquet/std/notes/p2.3.html (2 of 3) [25-10-2001 20:20:05]


2.2 Représentation concrète des chaînes

Allocation et extensibilité
En général un objet n'est 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 objets. 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'inté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 ; utiliser 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.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p2.3.html (3 of 3) [25-10-2001 20:20:05]


3 Les types abstraits

[Remonter] [Precedent] [ Suivant]

3 Les types abstraits


3.1 Motivation

3.2 Exemples informels

3.3 Spécification algébrique des types abstraits

3.4 Exemple: spécification d'un type Cercle

3.5 Spécification des types élémentaires

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

3.7 Spécification d'un type collection: Liste

3.8 Les types produits cartésiens

3.9 Spécification des Interfaces

3.10 Expression de la spécification d'un interface

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.1.html [25-10-2001 20:20:07]


3.1 Motivation

[Remonter] [Precedent] [ Suivant]

3.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 trouver 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écifier 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.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.2.html [25-10-2001 20:20:10]


3.2 Exemples informels

[Remonter] [Precedent] [ Suivant]

3.2 Exemples informels


Date
On peut définir le 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 (nb. 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 :
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

Disque
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

http://cui.unige.ch/~falquet/std/notes/p3.3.html (1 of 2) [25-10-2001 20:20:14]


3.2 Exemples informels

coord y du centre(définir un disque(u, v, r)) = v


surface(d) = rayon(d) * rayon(d) * Pi

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.3.html (2 of 2) [25-10-2001 20:20:14]


3.3 Spécification algébrique des types abstraits

[Remonter] [Precedent] [ Suivant]

3.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 ensemble 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 signature
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:
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 à construire les
valeurs d'une sorte. Par exemple, l'opération "sucesseur" permet de construire tous les entiers à partir de
la constante zéro.

http://cui.unige.ch/~falquet/std/notes/p3.4.html (1 of 2) [25-10-2001 20:20:19]


3.3 Spécification algébrique des types abstraits

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 commutative.
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 signifie 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 expression
é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
t1 == si b alors t2 sinon t3
ou encore par
t1 == t2 si b, == t3 sinon

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.4.html (2 of 2) [25-10-2001 20:20:19]


3.4 Exemple: spécification d'un type Cercle

[Remonter] [Precedent] [ Suivant]

3.4 Exemple: spécification d'un type Cercle


SPECIFICATION Cercle
SORTES
naturel, rationnel, cercle
OPERATIONS
perimetre : cercle -> rationnel
rayon : cercle -> naturel
cercle-unité : -> cercle
centre-x : cercle -> naturel
centre-y : cercle -> naturel
translation-horizontale : cercle -> cercle
cercle : naturel, naturel, naturel -> cercle
cercle : naturel -> cercle
AXIOMES
VARIABLES X, Y, Z, DX: naturel; 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(translation-horizontale(C, DX)) == centre-x(C)+DX
centre-y(translation-horizontale(C, DX)) == centre-y(C)
rayon(translation-horizontale(C, DX)) == rayon(C)

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.5.html [25-10-2001 20:20:22]


3.5 Spécification des types élémentaires

[Remonter] [Precedent] [ Suivant]

3.5 Spécification des types élémentaires


Si l'on veut construire complètement un ensemble de types abstraits algébriques il faut comencer par
définir les "briques de base" que sont les nombres naturels et les valeurs booléennes. Commençons par
une spécification des booléens:
SPECIFICATION Booléens
SORTES
bool
OPERATIONS
vrai : -> bool;
faux : -> bool;
non : 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

http://cui.unige.ch/~falquet/std/notes/p3.6.html (1 of 2) [25-10-2001 20:20:27]


3.5 Spécification des types élémentaires

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;
[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]
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.6.html (2 of 2) [25-10-2001 20:20:27]


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

[Remonter] [Precedent] [ Suivant]

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


SPECIFICATION CCar
UTILISE Bool, Nat
SORTES ccar
OPERATIONS
"" : -> ccar;
"_" : car -> ccar;
_+_ : ccar, ccar -> ccar;
_=_ : ccar, ccar -> bool;
AXIOMES
VAR X, Y : ccar;

""+X == X;
X+"" == X;
vide("") == vrai;
vide("C") == faux;
vide(X+Y) == vide(X) et vide(Y);
premier("C") = C
premier(X+Y) = si vide(X) alors premier(Y) sinon premier(X)
reste("C") = ""
reste(X+Y) = si vide(X) alors reste(Y) sinon reste(X) + Y
"" = "" == vrai;
"" = X == si vide(X) alors vrai sinon faux
X = "" == si vide(X) alors vrai sinon faux
premier(X) = premier(Y) == vrai => X=Y == reste(X) = reste(Y)
premier(X) = premier(Y) == faux => X=Y == faux

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.7.html [25-10-2001 20:20:31]


3.7 Spécification d'un type collection: Liste

[Remonter] [Precedent] [ Suivant]

3.7 Spécification d'un type collection: Liste


On distingue en général divers types de collections d'objets dont les listes, les ensembles, les
multi-ensembles, les tableaux, les applications, les piles, les files.
Une liste est une collection d'objets du même type qui sont placés selon un ordre. Chaque objet possède
donc une position. Mathématiquement une liste <a 1 , a 2 , ..., a n > d'éléments de type T est l'application
{1 Æ a 1 , 2 Æ a 2 , ..., n Æ a n } de {1, ..., n} dans {a 1 , a 2 , ..., a n }.

Une spécification
On peut spécifier un type liste en se basant sur l'idée qu'un liste est soit vide soit composée d'un premier
élément (la tête) suivi d'une liste (le reste). Nous donnons ci dessous une spécification générique où elem
, le type des éléments, peut être n'importe quel type pourvu qu'il possède l'opération " = ".
SPECIFICATION Liste
UTILISE Bool, Nat
SORTES liste, elem
OPERATIONS
-- constructeurs
vide : -> liste;
cons : elem, liste -> liste;
-- sélecteurs
tete : liste -> nat;
reste : liste -> liste;
est-vide : liste -> bool;
element : elem, liste -> bool;
position : elem, liste -> nat;
AXIOMES
VAR E, E1, E2 : elem; L : liste
[1] tete(cons(E, L)) == E;
[2] reste(cons(E, L)) == L;
[3] est-vide(vide) == true;
[4] est-vide(cons(E, L)) == false;
[5] element(E, vide) == false;
[6] element(E1, cons(E2, L)) ==
if E1 = E2 then true else element(E1, L);

http://cui.unige.ch/~falquet/std/notes/p3.8.html (1 of 3) [25-10-2001 20:20:44]


3.7 Spécification d'un type collection: Liste

[7] position(N, vide) == zero;


[8] position(E1, cons(E2, L)) ==
if E1 = E2 then succ(zero) else
if element(E1, L) then succ(position(E1, L)) else zero;
Exemples:
Construire la liste <6, 55, 444>:
cons(6, cons(55, cons(444, vide)))

Tête de la liste <6, 55>:


tete(cons(6, cons(55, vide))) == 6 (par l'axiome 1)

L'élément 22 se trouve-t-il dans la liste <3, 1, 22, 7> ?


element(22, cons(3, cons(1, cons(22, cons(7, vide)))))
== element(22, cons(1, cons(22, cons(7, vide)))) [par 6]
== element(22, cons(22, cons(7, vide))) [par 6]
== true [par 6]

Extension
À partir de la spécification ci dessus on peut définir de nouvelles opérations telles que la concaténation
de listes, l'ajout et la suppression d'éléments au début, à la fin ou à une certaine position, etc. On définit
ainsi une nouvelle spécification qui étend la première.
Ajouter un élément au début d'une liste:
ajout-d: elem, liste -> liste
axiomes:
ajout-d(E, L) == cons(E, L)
Ajouter un élément à la fin:
ajout-f: elem, liste -> liste
axiomes:
ajout-f(E, vide) == cons(E, vide)
ajout-f(E, cons(E', L)) == cons(E', ajout-f(E, L))
Ajouter à la position i:
ajout-pos: elem, liste, nat -> liste
axiomes:
ajout-pos(E, L, 1) == cons(E, L)

http://cui.unige.ch/~falquet/std/notes/p3.8.html (2 of 3) [25-10-2001 20:20:44]


3.7 Spécification d'un type collection: Liste

ajout-pos(E, cons(E', L), succ(I)) = cons(E', ajout-pos(E, L, I))


Supprimer à la position i
supp-pos: liste, nat -> liste
axiomes:
supp-pos(cons(E, L), 1) == L
supp-pos(cons(E, L), succ(I)) == cons(E, supp-pos(L, I))
Changer l'élément à la position i
chg-pos: elem, liste, nat -> liste
axiomes:
chg-pos(E, L, I) == supp-pos(ajout-pos(E, L, I), succ(I))
On remarquera que ces opérations n'introduisent pas de nouveaux constructeurs, c'est-à-dire qu'une
expression de type liste "correcte" peut toujours se ramener à une suite de cons et vide .

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.8.html (3 of 3) [25-10-2001 20:20:44]


3.8 Les types produits cartésiens

[Remonter] [Precedent] [ Suivant]

3.8 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
En général on préfère désigner les composant par un nom plutôt que par leur numéro d'ordre, pour rendre
plus évidente la sémantique des spécifications et programmes.
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
UTILISE: ...
SORTES: produit
OPERATIONS:
cons : s1, s2, ..., sn -> produit
comp1 : produit -> s1
...
compn : produit -> sn
m-comp1 : s1, produit -> produit
...
m-compn : sn, produit -> produit

Les opérations comp1 à compn servent à accéder aux composants alors que m-comp1 à m-compn servent
à modifier les composants d'un produit. Les schémas d'axiomes sont :
AXIOMES:
-- n axiomes de la forme
compi(cons(x1, x2, ..., xn) == xi (i = 1, 2, ..., n)
-- n axiomes
compi(m-compi(x, p)) == x (i = 1, 2, ..., n)
-- n x (n-1) axiomes
compi(m-compj(x, p)) == compi(p) (i = 1, ..., n; j = 1, ..., n; i j)
-- spécifie que la modification du composant i n'affecte pas
-- les autres composants

http://cui.unige.ch/~falquet/std/notes/p3.9.html (1 of 2) [25-10-2001 20:20:49]


3.8 Les types produits cartésiens

Remarque: les types produit cartésien existent dans la plupart des langages de programmation, sous
différentes appellations : record en Pascal, struct en C, etc. Les composants des tuples s'appellent
souvent des champs.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.9.html (2 of 2) [25-10-2001 20:20:49]


3.9 Spécification des Interfaces

[Remonter] [Precedent] [ Suivant]

3.9 Spécification des Interfaces


Dans les langages de programmation modulaires et orientés objets habituels (Smalltalk, C++, etc.)
l'interface d'une classe (sa vue externe) est composée de ce qui est strictement nécessaires aux autres
classe pour y accéder. C'est à dire:
1. le nom de la classe
2. le nom de chaque opération, le nombre de paramètres requis et le type des paramètres pour
les langages typés statiquement (C++, Eiffel)
Il est bien évident qu'une telle spécification est très pauvre sémantiquement, en particulier le
comportement des opérations n'est pas défini. C'est pourquoi la vue externe doit être complétée par une
description précise du sens de chaque opération.

Spécification des opérations par pré et post conditions


Nous avons déjà vu qu'il était possible de spécifier la sémantique des opérations d'un type à l'aide
d'équations (axiomes). Mais ce genre de spécification n'est pas directement utilisable dans les langages
orientés objet que nous utilisons car les opérations (méthodes Smalltalk ou fonctions C++) ne sont pas de
pures fonctions mathématiques. Non seulement elles calculent un résultat, mais elles peuvent également
modifier l'état de la mémoire. C'est pourquoi nous utiliserons la technique des pré et post conditions tout
en nous appuyant sur les spécifications algébriques pour ce qui est des valeurs d'objets.
La technique des pré et post conditions consiste à que si les paramètres d'une opération satisfont une
condition donnée (la pré condition) au moment du début de l'exécution de l'opération alors la post
condition portant sur le résultat les paramètres sera vraie juste après l'exécution. Par exemple
opération division
paramètres: x et y
résultat: r
pré condition: x et y des entiers positifs et y non nul
post condition: (ry £ x) et ((r + 1)y > x) (autrement dit r est le plus grand entier tels que ry £ x)
On voit sur cet exemple qu'il faut un langage, ici l'arithmétique entière, pour exprimer les conditions.
Nous utiliserons comme langage l'ensemble des opérations définies dans des spécifications algébriques.
Dans le cas ou la pré-condition n'est pas vraie au début de l'opération peut théoriquement faire n'importe
quoi, y compris arrêter le programme, car elle n'est pas tenue de remplir la post-condition à la fin.
Lorsque nous avons défini des opérations algébriquement nous n'avons pas traité explicitement les cas
d'erreur. Par exemple, que donne l'opération tete appliquée à une liste vide? Il existe des moyens de
traiter ces cas particuliers directement dans la spécification algébrique, p.ex. en introduisant des valeurs

http://cui.unige.ch/~falquet/std/notes/p3.a.html (1 of 2) [25-10-2001 20:20:53]


3.9 Spécification des Interfaces

spéciales. Mais il est souvent plus simple de le faire à l'aide de pré-conditions. La pré-condition, ou une
partie de celle-ci, peut donc servir de barrière d'entrée et déclencher une erreur du programme si jamais
elle n'est pas vraie.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.a.html (2 of 2) [25-10-2001 20:20:53]


3.10 Expression de la spécification d'un interface

[Remonter] [Precedent] [ Suivant]

3.10 Expression de la spécification d'un


interface
Pour rester général nous spécifierons l'interface d'un type abstrait plutot que celle d'une classe. Ceci nous
laissera la liberté d'implanter ensuite un type avec une ou plusieurs classes, ce qui arrive souvent dans la
pratique. La spécification de l'interface d'un type sera composée de:
● son nom

● du nom de la spécification algébrique de référence

● d'une correspondance entre les sortes et les types utilisés

● de la spécification de chaque méthode comprenant:

● son nom

● les types des paramètres et du résultat

● une pré et une post condition

Une méthode agit toujours sur un objet (appelé cible). La spécification algébrique va servir à écrire les
pré et post conditions qui portent sur la valeur de l'objet cible avant et après l'exécution de la méthode.

Exemple:
Interface d'une classe Liste d'éléments de type T
type Liste
specification Liste(Liste pour liste, T pour elem)
à chaque sorte de la spécification algébrique on fait correspondre un type du système en cours de
définition.
méthodes
vide
post self-post = vide
self-post représente la valeur de l'objet cible après l'exécution de la méthode et self-pre sa valeur avant.
Dans ce cas la postcondition signifie qu'après l'exécution de la méthode la liste est vide (plus exactement,
la valeur de l'objet self est une liste vide).
Il n'y a pas de précondition car la méthode peut s'appliquer sur n'importe quelle liste.
cons: t: T

http://cui.unige.ch/~falquet/std/notes/p3.b.html (1 of 2) [25-10-2001 20:20:59]


3.10 Expression de la spécification d'un interface

post self-post = cons(t, self-pre)


tete
retourne t: T
pre est-vide(self-pre) = false
post t = tete(self-pre)
reste
retourne l: Liste
pre est-vide(self-pre) = false
post l = reste(self-pre)
estVide
retourne b: Booleen
post b = est-vide(self)
element : t: T
retourne b: Boolean;
post b = element(t, self)
position : t: T
retourne i: Naturel;
post i = position(t, self)
Lorsque l'on parle de la valeur d'un objet dans une telle spécification il faut bien voir que cette valeur
sera représentée par une structure de données en mémoire qui peut être complexe et mettre en jeu
plusieurs autres objets. Par exemple, dans le cas de la liste la valeur d'un objet de la classe liste pourra
être une chaîne d'objets d'une classe ElementListe liés entre eux par une variable d'instance suivant .
C'est au niveau de l'implantation (vue interne) que se décidera finalement la représentation adoptée pour
les valeurs d'un type. Pour que l'implantation soit correcte il faudra vérifier qu'il y a bien correspondance
entre les valeurs abstraites, construites par des termes algébriques, et les structures de données en
mémoire.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p3.b.html (2 of 2) [25-10-2001 20:20:59]


4 Les Types Abstraits Collection

[Remonter] [Precedent] [ Suivant]

4 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, et un type d'objet correspondant à une manière de voir chaque type.

4.1 Piles

4.2 Files

4.3 Séquence

4.4 Listes "binaires"

4.5 Ensembles et multi-ensembles

4.6 Les itérateurs sur les ensembles

4.7 Fonctions

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.1.html [25-10-2001 20:21:03]


4.1 Piles

[Remonter] [Precedent] [ Suivant]

4.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.

4.1.1 Le type abstrait: spécification algébrique


SPECIFICATION Pile
UTILISE Bool, Nat
SORTES pile, elem
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

4.1.2 Un type pile orienté objet


Les opérations primitives d'une pile d'objets de type T sont décrites dans la table ci-dessous:
résultat méthode paramètres description
crée une pile vide
Pile new Pile PRE: true
POST: est-vide(this_post)
dépose l'élément e au sommet de la pile
Pile empiler T e
POST: this_post = empiler(e, this_pre)

http://cui.unige.ch/~falquet/std/notes/p4.2.html (1 of 4) [25-10-2001 20:21:23]


4.1 Piles

enlève l'élément qui se trouve au sommet de la pile


Pile depiler PRE: est-vide(this) = false
POST: this_post = depiler(this_pre)
retourne l'élément qui est au sommet de la pile
T sommet PRE: est-vide(this) = false
POST: result = sommet(this)
retourne vrai si la pile est vide
boolean estVide
POST: result = est-vide(this)

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 ouvrantes de quatre
types : (, [, { et <, et des parenthèses fermantes des mêmes types : ), ], } et > 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 parenthèses doit
également être correct : une parenthèse ouverte doit y être refermée. L'algorithme ci-dessous utilise une
pile pour mémoriser les ouvertures de parenthèses :
Données: un tableau c de N caractères qui contient le texte
p = new Pile()
pour i de 0 à N-1 {
si (c[i] est une parenthèse ouvrante) p.empiler(c[i])
sinon si (c[i] est une parenthèse fermante) {
si (p.estVide()) retourne "ERREUR il manque une parenthèse ouvrante"
si (p.sommet()) est du même type que c[i]) p.depiler()
sinon retourne "ERREUR: parenthèses de types différents"
}
}
si (p.estVide()) retourne "OK"
sinon retourne "ERREUR: il manque au moins une parenthèse fermante"

Remarque. Nous exprimerons les algorithmes dans un pseudo-langage orienté-objet inspiré du langage
Java. Nous négligerons en général les déclarations de variables et nous écrirons en français les parties de
l'algorithme qui ne posent pas de problème particulier.

http://cui.unige.ch/~falquet/std/notes/p4.2.html (2 of 4) [25-10-2001 20:21:23]


4.1 Piles

4.1.3 L'opération de copie


Les opérations que nous avons décrites sont suffisantes pour effectuer la copie d'une pile. La méthode est
cependant complexe. Etant donné une pile p, pour créer une pile q qui soit une copie de p il faut
employer la méthode suivante :
temp = new Pile(); q = new Pile();
tant que (non p.estVide()) {
temp.empiler(p.sommet());
p.dépiler();
}
tant que (non temp.estVide()) {
p.empiler(temp.sommet()); q.empiler(temp.sommet());
temp.dépiler()
}

La boucle 2. sert à copier p dans temp, mais à l'envers, la boucle 3. recopie temp sur p et q.
Si la pile est implémentée à l'aide d'un tableau on voit qu'il serait plus rapide de copier directement ce
tableau. Ceci est vrai quelle que soit la structure utilisée pour l'implémentation. Pour des raisons de
performance il est sera donc souhaitable de définir une opération copier() qui recopie directement la
structure interne de la pile, sans passer par des empiler(), dépiler().

4.1.4 Généricité
Dans la définition que nous avons donnée nous n'avons pas précisé quel devait être le type T des élément
d'une pile. Notre type abstrait Pile est donc générique. Cette généricité pourra être conservé ou non au
niveau de l'implémentation du type. Si les algorithmes utilisés pour implémenter la pile font appel à des
opérations sur les éléments, par exemple une opération de test d'égalité ou de comparaison, cette
implémentation réduira la généricité du type en imposant des contraintes sur le type des éléments.
On appelle instantiation d'un type générique le fait de fixer le type T. On pourra, par exemple, créer une
pile de String (T = String), un pile de Rectangles (T = Rectangle), etc.

4.1.5 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).

http://cui.unige.ch/~falquet/std/notes/p4.2.html (3 of 4) [25-10-2001 20:21:23]


4.1 Piles

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.

1. Il existe différentes définitions, plus ou moins sophistiquées, de la notion de sous-type. Nous nous
contenterons de cette définition simple.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.2.html (4 of 4) [25-10-2001 20:21:23]


4.2 Files

[Remonter] [Precedent] [ Suivant]

4.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 !).

4.2.1 Spécification algébrique


SPECIFICATION File
UTILISE Bool, Nat
SORTES file, elem
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
2. longueur(entrer(X,F)) == longueur(F) + 1
3. longueur(sortir(F)) == longueur(F) - 1
4. premier(entrer(X,F)) == X si longueur(F) = 0
== premier(F) sinon
5. sortir(entrer(X,F)) == F si longueur(F) = 0
== entrer(X,sortir(F)) sinon
Utilisation de cette dernière équation:
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)))) ==

http://cui.unige.ch/~falquet/std/notes/p4.3.html (1 of 3) [25-10-2001 20:21:30]


4.2 Files

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

4.2.2 Un type file orienté objet


Ce type reprend les opérations de la spécification algébrique, avec le type T pour elem
résultat opération paramètres effet
crée une file vide
File new File
POST: est-vide(this_post)
ajoute l'élément e à l'arrière de la file
File entrer Te
POST: this_post = entrer(e, this_pre)
sort l'élément qui se trouve à l'avant
File sortir PRE: non est-vide(this)
POST: this_post = sortir(this_pre)
retourne l'élément qui se trouve en premère position
T premier PRE: non est-vide(this)
POST: result = premier(this)
teste si la file est vide
boolean estVide
POST: result = est-vide(this)
donne la longueur de la file
int longueur
POST: result = longueur(this)

4.2.3 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é premier 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 apparaîssent dans les systèmes
d'exploitation d'ordinateurs, dans les réseaux de télécommunictions, 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

http://cui.unige.ch/~falquet/std/notes/p4.3.html (2 of 3) [25-10-2001 20:21:30]


4.2 Files

dans diverses situations (guichets, trafic automobile, etc.).

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.3.html (3 of 3) [25-10-2001 20:21:30]


4.3 Séquence

[Remonter] [Precedent] [ Suivant]

4.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 n } de {1, ..., n} dans {a 1 , a 2 , ..., a n }.

4.3.1 Spécification algébrique


SPECIFICATION Sequence
UTILISE Bool, Nat
SORTES seq, elem
OPERATIONS
-- constructeurs
vide : -> seq;
inserer : elem, nat, seq -> seq;
supprimer : nat, seq -> seq;
remplacer : elem, nat, seq -> seq;
-- sélecteurs
element : nat, seq -> elem;
indice : elem, seq -> nat;
indice-apres : elem, nat, seq -> nat;
est-vide : seq -> bool;
longueur : seq -> nat;
AXIOMES
VAR E : elem; S : seq; I : nat;

1. supprimer(I, inserer(E, I, S) == S;
2. element(J, inserer(E, I, S) == element(J, S) si J < I
== E si J = I
== element(J-1, S) sinon
3. element(J, supprimer(I, S)) == element(J, S) si J < I
== element(J+1, S) sinon
4. element(J, remplacer(I, E, S)) == E si J = I
== element(J, S) sinon
5. longueur(vide) == 0

http://cui.unige.ch/~falquet/std/notes/p4.4.html (1 of 4) [25-10-2001 20:21:39]


4.3 Séquence

6. longueur(inserer(E, I, S) == longueur(S)+1;
7. longueur(supprimer(I, S) == longueur(S)-1;
8. longueur(remplacer(E, I, S) == longueur(S);
9. indice(E, vide) == 1;
10. indice(E, S) == 0 si element(0, S) = E
== indice(E, supprimer(0, S))+1 sinon
N.B. si e ne se trouve pas dans l, le résultat est l.longueur()+1
11. indice(E, I, S) == indice(E, I-1, supprimer(0, S)) si I > 0
== indice(E, S) sinon;
12. est-vide(S) == longueur(S) = 0
13. est-vide(vide) == vrai
Exemples d'application des équations:
a.
element(1, inserer(A, 0, inserer(B, 0, S)))
== (par 2.) element(0, inserer(B, 0, S))
== (par 2.) B
b.
element(0, supprimer(0, inserer(A, 0, inserer(B, 0, S))))
== (3) element(1, inserer(A, 0, inserer(B, 0, S)))
== (2) element(0, inserer(B, 0, S))
== (2) B

Certaines expressions ne sont pas complètement évaluable, c'est-à-dire qu'on ne peut les réduire à des
expressions ne faisant intervenir que les constructeurs et les constantes. Par exemple :
element(6, inserer(a, 0, inserer(b, 0, vide))) ==
element(5, inserer(a, 0, vide)) ==
element(4, vide)
Aucun axiome ne permet de réduire cette dernière expression

element(2, inserer(a, 6, (inserer(b, 12, vide))) ==


element(2, (inserer(b, 12, vide)) ==
element(2, vide)
La dernière expression est irréductible.

http://cui.unige.ch/~falquet/std/notes/p4.4.html (2 of 4) [25-10-2001 20:21:39]


4.3 Séquence

4.3.2 Le type concret d'un objet Sequence:


Ce type reprend les opérations de la spécification algébrique et ajoute des pré-conditions de manière à ce
que toute expression satisfaisant les pré-condition s'évalue complètement jusqu'à des constantes ou des
constructeurs sur des constantes.
résultat opération paramètres effet
crée une liste vide
Liste new Liste
POST: this_post = vide
insère elem à la position i
Liste insérer int i, T elem PRE: 0 £ i £ longueur(this)
POST: this_post = inserer(T, i, this_pre)
remplace le i-ième élément par elem
Liste remplacer int i, T elem PRE: 0 £ i < longueur(this)
POST: this_post = remplacer(T, i, this_pre)
supprime l'élément à la position i
Liste supprimer int i PRE: 0 £ i < longueur(this)
POST: this_post = supprimer(i, this_pre)
indice de la première occurence de elem
int indice T elem
POST: result = indice(T, this)
indice de la première occurence de elem après i
int indice T elem, int i
POST: result = indice(T, i, this)
element se trouvant à la position i
T element int i
POST: result = element(i, this)
longueur de la liste
int longueur
POST: result = longueur(T)
la liste est-elle vide ? (longueur = 0)
boolean estVide
POST: result = est-vide(this)

4.3.3 Problème de l'égalité et de la copie


Considérons deux listes d'objets l1 et l2. Que signifie "l1 est égale à l2" ? On peut trouver au moins deux
interprétations différentes de l'égalité. Ces différentes interprétations viennent de la distinction qui existe
entre l'identité d'un objet et sa valeur. Nous avons déjà vu qu'un objet est une paire (identité, valeur).
Deux objets distincts ne peuvent avoir la même identité, par contre ils peuvent avoir la même valeur..

http://cui.unige.ch/~falquet/std/notes/p4.4.html (3 of 4) [25-10-2001 20:21:39]


4.3 Séquence

On peut donc donner une première définition de l'égalité:


Définition 1. Deux listes sont égales si elles contiennent des objets identiques (même identité) placés
dans le même ordre.
Si l'on ne considère plus l'identité des objets mais simplement l'égalité de leur valeurs on peut donner une
seconde définition:
Définition 2. Deux listes sont égales si elles contiennent des objets de même valeur placés dans le même
ordre.
Exemple. Supposons que le type Rectangle possède un constructeur Rectangle(int x, int y, int largeur, int
hauteur).
r = new Rectangle(0, 0, 6, 8);
s = new Rectangle(0, 1, 12, 15);
t = new Rectangle(0, 0, 6, 8);
u = new Rectangle(0, 1, 12, 15);
v = new Rectangle(7, 7, 89, 0);
l1 = new Liste();
l1.inserer(0, r); l1.inserer(1, s); l1.inserer(2, v);
l2 = new Liste();
l2.inserer(0, t); l2.inserer(1, u); l2.inserer(2, v);

Les listes l1 et l2 ne sont pas égales au sens de la première définition car l'élément no. 1 de l1 est l'objet s
alors que l'élément no. 1 de l2 est u. Par contre elles sont égales au sens de la seconde définition car les
objets s et u ont les mêmes valeurs (0, 1, 12, 15) et sont donc égaux (mais pas identiques).
Il faut noter que la seconde définition est récursive car la notion d'égalité des valeurs des objets peut
impliquer la comparaison d'autres objets (composantes). Par exemple, si l'on crée une liste dont les
éléments sont eux-même des listes.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.4.html (4 of 4) [25-10-2001 20:21:39]


4.4 Listes "binaires"

[Remonter] [Precedent] [ Suivant]

4.4 Listes "binaires"


On peut définir une liste de manière récursive en disant qu'elle est soit vide, soit composée d'une tête de
liste et d'un reste qui est lui-même une liste.

4.4.1 Une spécification algébrique


SPECIFICATION Liste
UTILISE Bool, Nat
SORTES liste, elem
OPERATIONS
-- constructeurs
vide : -> liste;
cons : elem, liste -> liste;
-- sélecteurs
tete : liste -> elem;
reste : liste -> liste;
est-vide : liste -> bool;
element : elem, liste -> bool;
position : elem, liste -> nat;
AXIOMES
VAR E, E1, E2 : elem; L : liste
[1] tete(cons(E, L)) == E;
[2] reste(cons(E, L)) == L;
[3] est-vide(vide) == vrai;
[4] est-vide(cons(E, L)) == faux;
[5] element(E, vide) == faux;
[6] element(E1, cons(E2, L)) == vrai si E1 = E2,
== element(E1, L) sinon;
[7] position(N, vide) == 0;
[8] position(E1, cons(E2, L)) == 1 si E1 = E2
== position(E1, L) + 1 sinon

Exemples:
Construire la liste <6, 55, 444>:

http://cui.unige.ch/~falquet/std/notes/p4.5.html (1 of 5) [25-10-2001 20:21:56]


4.4 Listes "binaires"

cons(6, cons(55, cons(444, vide)))

Tête de la liste <6, 55>:


tete(cons(6, cons(55, vide))) == 6 (par l'axiome 1)

L'élément 22 se trouve-t-il dans la liste <3, 1, 22, 7> ?


element(22, cons(3, cons(1, cons(22, cons(7, vide)))))
== element(22, cons(1, cons(22, cons(7, vide)))) [par 6]
== element(22, cons(22, cons(7, vide))) [par 6]
== true [par 6]

4.4.2 Type Liste


Comme d'habitude, on ajoute des pré-conditions pour être sûr d'arriver à des résultats évaluables.
résultat opération paramètres effet
crée une liste vide
Liste new Liste
POST: this = vide
crée une liste avec t comme tête et r comme reste
Liste new Liste T t, Liste r
POST: this = cons(t, r)
modifie la tête de la liste qui devient t
Liste mTete Tt PRE: non est-vide(this)
POST: this_post = cons(t, reste(this_pre))
modifie le reste qui devient la liste r
Liste mReste Liste r PRE: non est-vide(this)
POST: this_post = cons(tete(this_pre, r)
donne la tête de la liste
T tete PRE: non est-vide(this)
POST: result = tete(this)
donne le reste de la liste
Liste reste PRE: non est-vide(this)
POST: result = reste(this)
la liste est-elle vide ?
boolean estVide
POST: result = est-vide(this)

http://cui.unige.ch/~falquet/std/notes/p4.5.html (2 of 5) [25-10-2001 20:21:56]


4.4 Listes "binaires"

Exemples d'utilisation de ces opérations:


Créer une liste <a, b, c, d>
new Liste(a, new Liste(b, new Liste(c, new Liste(d, new Liste()))))

Obtenir le cinquième élément d'une liste z


z.reste().reste().reste().reste().tete()

Trouver le dernier élément d'une liste k non vide


r = k;
while (! r.reste().estVide()) r = r.reste();
dernier = r.tete();

Inséerer un élément w en quatrième position dans une liste z


quatreEtSuite = z.reste().reste().reste();
troisEtSuite = z.reste().reste();
nouveauQuatreEtSuite = new Liste(w, quatreEtSuite);
troisEtSuite.mReste(nouveauQuatreEtSuite);

Ce genre de liste est utilisé dans les langages tels que LISP et PROLOG. En LISP la liste est la structure
de données de base, elle sert non seulement à représenter les données mais également les instructions
d'une programme. Les expressions qui forment un programme LISP sont des listes dont la tête est un
opérateur et le reste les arguments sur lesquels porte l'opération. Par exemple:
(+ 6 4) calcule 6 + 3
(* 8 (- 12 x)) calcule 8 * (12 - x)
(cond ((eq a 5) (+ 3 b)) ((eq a 7) x))
== si a = 5 le résultat est b+3 et si a = 7 le résultat est x

Ceci permet de traiter les programmes comme des données. On peut donc écrire facilement des
programmes qui construisent des expressions et les évaluent (méta-programmes).
L'implémentation de ce genre de liste est immédiat: il suffit de définir des objets de type Liste muni de
deux variables d'instance, l'un faisant référence à l'objet tête et l'autre faisant référence à l'objet Liste qui
constitue le reste.
class Liste {
Object tete; Liste reste;

http://cui.unige.ch/~falquet/std/notes/p4.5.html (3 of 5) [25-10-2001 20:21:56]


4.4 Listes "binaires"

...
}

La programmation des méthodes est également immédiate, il ne s'agit que d'affectations. Ce genre de
liste est également appelé liste simplement liée.

4.4.3 Extension aux séquences


À partir de la spécification ci dessus on peut définir de nouvelles opérations telles que la concaténation
de listes, l'ajout et la suppression d'éléments au début, à la fin ou à une certaine position, etc. On définit
ainsi une nouvelle spécification qui étend la première et qui rejoint la définition des séquences.
Ajouter un élément au début d'une liste:
ajout-d: elem, liste -> liste
axiomes:
ajout-d(E, L) == cons(E, L)
Ajouter un élément à la fin:
ajout-f: elem, liste -> liste
axiomes:
ajout-f(E, vide) == cons(E, vide)
ajout-f(E, cons(E', L)) == cons(E', ajout-f(E, L))
Ajouter à la position i:
ajout-pos: elem, liste, nat -> liste
axiomes:
ajout-pos(E, L, 1) == cons(E, L)
ajout-pos(E, cons(E', L), succ(I)) = cons(E', ajout-pos(E, L, I))
Supprimer à la position i
supp-pos: liste, nat -> liste
axiomes:
supp-pos(cons(E, L), 1) == L
supp-pos(cons(E, L), succ(I)) == cons(E, supp-pos(L, I))
Changer l'élément à la position i
chg-pos: elem, liste, nat -> liste
axiomes:
chg-pos(E, L, I) == supp-pos(ajout-pos(E, L, I), succ(I))

http://cui.unige.ch/~falquet/std/notes/p4.5.html (4 of 5) [25-10-2001 20:21:56]


4.4 Listes "binaires"

On remarquera que ces opérations n'introduisent pas de nouveaux constructeurs, c'est-à-dire qu'une
expression de type liste "correcte" peut toujours se ramener à une suite de cons et vide .
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.5.html (5 of 5) [25-10-2001 20:21:56]


4.5 Ensembles et multi-ensembles

[Remonter] [Precedent] [ Suivant]

4.5 Ensembles et multi-ensembles


Un ensemble est une collection d'objets distincts, non ordonnés. Les objets qui forment un ensemble sont
appelés ses éléments. Etant donné un objet quelconque, il est soit élément de l'ensemble soit non élément.
Un ensemble A est sous ensemble de B si tous ses éléments sont également éléments de B.
La cardinalité d'un ensemble est le nombre de ses éléments.
Nous parlerons d'ensemble homogène pour désigner un ensemble dont les éléments ont tous un caractère
commun, p.ex. l'appartenane à une même classe, à un même type, etc.

4.5.1 Spécification algébrique


Pour construire un ensemble on partira de l'ensemble vide auquel on ajoutera successivement des
éléments, dans n'importe quel ordre. L'opérateur de retrait d'un élément sera également utile, bien que
formellement pas nécessaire.
SPECIFICATION Ensemble
UTILISE Bool, Nat
SORTES elem, ens
OPERATIONS
-- constructeurs
vide : -> ens;
ajoute: elem, ens -> ens;
retire: elem, ens -> ens;
-- sélecteurs
est-vide : ens -> bool;
element : elem, ens -> bool;
taille : ens -> nat;
_=_ : ens, ens -> bool;
_union_ : ens, ens -> ens;
AXIOMES
VAR E, E1, E2 : elem; S, S1, S2: ens
[1] retire(E, vide) == vide;
[2] retire(E1, ajoute(E2, S) ==
if E1 = E2 then retire(E1, S) else ajoute(E2, retire(E1, S);
[3] est-vide(vide) == true;
[4] est-vide(cons(E, S)) == false;

http://cui.unige.ch/~falquet/std/notes/p4.6.html (1 of 4) [25-10-2001 20:22:10]


4.5 Ensembles et multi-ensembles

[5] element(E, vide) == false;


[6] element(E1, ajoute(E2, S)) ==
si E1 = E2 alors vrai sinon element(E1, S);
[7] taille(vide) == zero;
[8] taille(ajoute(E, S)) ==
si element(E, S) alors taille(S) sinon succ(taille(S))
[9] vide = vide == true;
[10] vide = ajoute(E, S) == false;
[11] ajoute(E, S) = vide == false;
[12] ajoute(E, S1) = S2 == element(E, S2) et S1 = retire(E, S2);
[13] S1 union ajoute(E, S2) == ajoute(E, S1) union S2;
[14] S inter vide == vide
[15] S1 inter ajoute(E, S2) ==
si element(E, S1) alors ajouter(E, S1 inter S2) sinon S1 inter S2

Remaques sur la spécification.


L'axiome [2] permet de simplifier toute expression qui contient des retire de façon à ne laisser que des
ajoute .
De plus, quand E1 = E2 on ne peut pas simplement dire que le résultat est S car S peut être construit en
ajoutant plusieurs fois E2 . Par exemple, retire(3, ajoute(3, ajoute(3, vide))) doit donner vide et non
ajoute(3, vide) .
L'axiome [8] prend en compte le fait qu'une expression peut contenir plusieurs fois l'ajout du même
élément, il faut faire attention à ne le compter qu'une seule fois, c-à-d que taille(ajoute(3, ajoute(2,
ajoute(3, vide)))) = 2 et non pas 3.

Exemple 1. Réduction d'une expression composée de ajoute et retire à une expression composée
uniquement de ajoute
r(33, a(4, a(33, r(1, a(20, a(1, vide))))))
== r(33, a(4, a(33, a(20, r(1, a(1, vide))))))
== r(33, a(4, a(33, a(20, r(1, vide)))))
== r(33, a(4, a(33, a(20, vide))))
== a(4, r(33, a(33, a(20, vide))))
== a(4, r(33, a(20, vide)))
== a(4, a(20, r(33, vide)))
== a(4, a(20, vide))

http://cui.unige.ch/~falquet/std/notes/p4.6.html (2 of 4) [25-10-2001 20:22:10]


4.5 Ensembles et multi-ensembles

Exemple 2. Calcul de l'appartenance d'un élément à un ensemble


element(0, a(99, a(1, a(0, vide))))
== element(0, a(1, a(0, vide)))
== element(0, a(0, vide))
== vrai

Exemple 3. Calcul de la cardinalité d'un ensemble


taille(a(1, a(0, a(1, vide)))
== taille(a(0, a(1, vide)) (car appartient(1, ...) == vrai
== succ(taille(a(1, vide))) (car appartient(0, ...) == faux
== succ(succ(taille(vide)) (car appartient(1, ...) == faux
== succ(succ(0))

Le calcul de la cardinalité tient compte du fait qu'on peut ajouter plusieurs fois le même élément à un
ensemble sans que l'ensemble ne change de cardinalité. On voit que cette opération est assez complexe
car elle fait intervenir à chaque étape la fonction appartient .

4.5.2 Spécification du type d'objets


Les opérations sur les objets correspondent aux opérations de la spécification. On a ajouté aux opérations
ajoute et retire un résultat booléen qui indique si l'opération a eu un effet ou non.
résultat opération parmètres description
créer un nouvel ensemble vide
Ensemble new Ensemble
post: this = vide
ajoute l'élément t à l'ensemble
booléen ajoute Tt post: this = ajoute(t, this_pre) et result = non
element(t, this_pre)
retire l'élément t de l'ensemble
booléen retire Tt
post: this = retire(t, this_pre) et result = element(t,
this_pre)
Ensemble union/intersection/différence Ensemble e post: this = this_pre union/inter/diff e
int cardinalite result = cardinalite(this)
boolean estVide result = est-vide(this)
boolean appartient Tt result = appartient(t, this)

http://cui.unige.ch/~falquet/std/notes/p4.6.html (3 of 4) [25-10-2001 20:22:10]


4.5 Ensembles et multi-ensembles

teste si l'ensemble e est inclus dans cet ensemble


boolean inclus Ensemble e
result = (this union e = this)

De même que la pile, l'ensemble a une structure monolithique, car il est composé d'éléments et non pas
de sous-ensembles.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.6.html (4 of 4) [25-10-2001 20:22:10]


4.6 Les itérateurs sur les ensembles

[Remonter] [Precedent] [ Suivant]

4.6 Les itérateurs sur les ensembles


Contrairement aux structures que nous avons vues précédemment, les ensembles n'ont pas d'ordre parmi
les éléments. Si l'on veut parcourir tous les éléments d'un ensemble, p.ex. pour les imprimer, il nous faut
un mécanisme qui nous fournisse successivement tous les éléments, dans un ordre quelconque. Ce genre
de mécanisme est appelé itérateur.

4.6.1 Spécification algébrique


Pour spécifier algébriquement un itérateur sur un type d'ensemble on utilise deux opérations : a-voir et
deja-vu qui représentent respectivement l'ensemble des éléments restant à parcourir et l'esemble des
éléments déjà parcourus. On doit également supposer qu'il existe une fonction choix qui choisit un
élément arbitraire dans un ensemble.
SPECIFICATION Iterateur
UTILISE Bool, Nat, Ensemble
SORTES iter
OPERATIONS
-- constructeurs
initial: ens -> iter;
avancer: iter -> iter;
-- sélecteurs
deja-vu : iter -> ens;
a-voir : iter -> ens;
courant : iter -> elem;
encore : iter -> bool;

AXIOMES:

deja-vu(initial(E) == vide;
a-voir(inital(E)) == E;
courant(avancer(I)) == choix(a-voir(I))
deja-vu(avancer(I)) == ajouter(choix(a-voir(I),deja-vu(I))
a-voir(avancer(I)) == retirer(choix(a-voir(I),a-voir(I))
encore(avancer(I)) == non est-vide(a-voir(I))

http://cui.unige.ch/~falquet/std/notes/p4.7.html (1 of 3) [25-10-2001 20:22:23]


4.6 Les itérateurs sur les ensembles

4.6.2 Spécification du type d'objets.


résultat opération paramètres description
crée un nouvel itérateur sur l'ensemble e
Iterateur Iterateur Ensemble e
POST: this_post = initial(e)
passer à l'élément suivant
Iterateur avancer PRE: encore(this) = vrai
POST: this_post = avancer(this_pre)
élément courant
T courant
POST: result = courant(this)
vrai s'il reste des éléments à voir
boolean encore
POST: result = encore(this)

On remarque que les opérations deja-vu et a-voir de la spécification algébrique ne sont que des
auxiliaires pour la définition, elles n'ont pas d'utilité pratique pour le type concret.

4.6.3 Itérateur généraux sur les collections


La notion d'itérateur peut s'appliquer à n'importe quelle collection: pile, liste, file, ensemble, etc. Dans le
cas de collections comme les listes, files ou les piles il existe un ordre de parcours naturel (du début à la
fin, du premier au dernier, du sommet au fond) alors que pour des collections comme les ensembles ou
les fonctions l'ordre est choisi par l'itérateur.
On peut ajouter à la définition ci-dessus des opérations d'insertion ou de suppression d'éléments d'une
collection. C'est particulièrement utile dans le cas des itérateurs sur les listes et files.
Du point de vue de la conception des applications l'itérateur est intéressant car il perment de parcourir
une collection non naturellement ordonnée sans avoir à connaître sa représentation interne.

4.6.4 Multi-ensembles
Dans un multi ensemble on a un certain nombre d'occurences de chaque élément, c-à-d chaque élément
peut apparaitre une ou plusieurs fois.
La cardinalité unique d'un multi-ensemble est le nombre de ses éléments distincts.
Les opérations sur les multi-ensembles sont celles des ensembles plus la cardinalité unique et le nombre
d'occurences d'un élément dans le multi-ensemble.
int cardinaliteUnique()
int nombreOccurences(T elem)

http://cui.unige.ch/~falquet/std/notes/p4.7.html (2 of 3) [25-10-2001 20:22:23]


4.6 Les itérateurs sur les ensembles

Pour les multiensembles les équations de calcul de la cardinalité, de la cardinalité unique et du nombre
d'occurences sont:
taille(ajoute(E, S)) == succ(taille(S));

card-unique(ajoute(E, S)) == si element(E, S) alors card-unique(S) sinon


succ(card-unique(S));

nb-occurences(E, vide) == 0;

nb-occurences(E, ajoute(F, S)) == si E = F alors succ(nb-occurences(E, S)) sinon


nb-occurences(E, S);
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.7.html (3 of 3) [25-10-2001 20:22:23]


4.7 Fonctions

[Remonter] [Precedent] [ Suivant]

4.7 Fonctions
Une fonction partielle d'un ensemble A de départ dans un ensemble d'arrivée B associe à chaque élément
a de A au plus un élément b de B (éventuellement aucun). L'élément b est appelé image de a. Si un
élément possède une image on dira qu'il est lié .
Une fonction F est donc un ensemble de paires (a -> b) qui satisfait la contrainte de fonctionnalité
suivante:
si (a -> b) appartient à F alors il ne peut pas y avoir dans F une autre paire (a -> b2) telle que b est
différent de b2.
Remarque. Attention à ne pas confondre ce type abstrait fonction, qui est une collection de données, avec
la notion de fonction que l'on trouve dans les langages de programmation et qui correspond à une suite
d'instructions qui calculent un résultat à partir des paramètres fournis. Il s'agit bien de fonctions puisqu'en
fin de compte elles créent des paires (paramètres, résultat) mais ce paires ne sont pas des données
stockées dans une collection, elles sont calculées à chaque fois. De plus, ces fonctions ne sont pas
modifiables (sauf si le langage permet de modifier des instructions en cours d'exécution).

4.7.1 Spécification algébrique


SPECIFICATION Fonction
UTILISE Bool, Nat
SORTES fct, dep, arr
OPERATIONS
vide: -> fct
lie: dep, arr, fct -> fct
delie: dep, fct -> fct
-- Sélecteurs
cardinalite: fct -> nat
est-vide: fct -> bool
est-lie: dep, fct -> bool
image: dep, fct -> arr
AXIOMES
delie(D1, lier(D2, A, P)) ==
if D1 = D2 then delie(D1, P) else lie(D2, A, delie(D1, P));
delie(D, vide) == vide;
image(D1, lie(D2, A, P)) ==
if D1 = D2 then A else image(D1, P);

http://cui.unige.ch/~falquet/std/notes/p4.8.html (1 of 3) [25-10-2001 20:22:35]


4.7 Fonctions

est-lie(D, vide) == false


est-lie(D1, lie(D2, A, P)) ==
if D1 = D2 then true else est-lie(D1, P);
est-vide(vide) == true;
est-vide(lie(A, D, P)) == false;
cardinalite(vide) == zero;
cardinalite(lie(A, D, P)) ==
if est-lie(A, P) = true then cardinalite(P)
else succ(cardinalite(P))

On remarquera que ces équations sont très proches de celles des ensembles. Ce n'est pas un hasard car on
peut considérer une fonction P comme un ensemble de paires (d Æ a) qui satisfait la contrainte de
fonctionnalité: si (d Æ a) appartient à P alors il ne peut pas y avoir une autre paire (d Æ a') avec a
différent de a' dans P. Cette contrainte est représentée par le fait que l'axiome pour image "s'arrête" au
premier lien trouvé, donc l'opération lie remplace un éventuel lien précédent:
image(6, lie(6, 3, lie(6, 4, lie(6, 2, lie(8, 1)))))
== 3 -- [par l'axiome 3]
Dans cet exemple la fonction est construite en liant 8 à 1 puis 6 successivement à 2, 4 et 3; la fonction
résultante est {8 Æ 1, 6 Æ 3}.

4.7.2 Type Fonction


résultat opération paramètres effet
crée une fonction vide
Fonction Fonction
post: this = vide
ajoute la paire (d Æ a) à la fonction
lier TD d, TA a
post: this = lie(d, a, this_pre)
supprime la paire dont l'élément de départ est d, retourne
TA delier TD d l'ancienne image de d.
post: this = delie(d, this_pre) et result = image(d, this_pre)
int cardinalite result = cardinalite(this)
boolean estVide result = est-vide(this)
boolean estLie TD d result = est-lie(d,this)
pre: est-lie(d, this)
TA image TD d
post: result = image(d, this)

http://cui.unige.ch/~falquet/std/notes/p4.8.html (2 of 3) [25-10-2001 20:22:35]


4.7 Fonctions

4.7.3 Utilité des fonctions


On peut se demander pourquoi il est nécessaire de définir une type fonction alors qu'il suffirant, pour lier
des objets d'une classe A à des objets d'une classe B, d'ajouter une variable d'instance f à A pour stocker
l'image de chaque objet de A par la fonction f. Cette solution n'est cependant pas la meilleure pour
plusieurs raisons:
● elle nécessite une modification de la structure interne de la classe A (l'ajout d'une variable
d'instance). On romp ainsi le principe de modularité.
● les opérations globales (comme estVide ou cardinalité) ne sont pas réalisables facilement dans les
langages à objets car l'ensemble des instances d'une classe n'est en général pas traité comme une
collection.
● il n'est pas possible de retrouver les objets de départ liés à un objet d'arrivée, sauf en ajoutant une
variable preImage dans la classe d'arrivée.
● la fonction ne peut plus être traitée comme un objet

4.7.4 Fonctions et tableaux


Un tableau de taille N dont les éléments sont de type T peut être vu comme une fonction de l'ensemble
{0, 1, ..., N-1} vers l'ensemble des objets de type T. Un tableau est donc un cas particulier de fonction.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p4.8.html (3 of 3) [25-10-2001 20:22:35]


5 Arbres

[Remonter] [Precedent] [ Suivant]

5 Arbres
Les structures d'arbres possèdent un double intérêt: d'une part les données qui interviennent dans de
nombreux problèmes sont naturellement structurées en arbres (hiérarchies d'objets, choix et décisions,
arbres syntaxiques, etc.), d'autre part elle permettent de représenter efficacement des ensembles d'objets
ou des applications, on parlera dans ce cas d'arbres de recherche (search trees).

5.1 Définitions

5.2 Une vue polylithique des arbres

5.3 Parcours d'un arbre

5.4 Représentation linéaire des arbres

5.5 Exemples d'utilisation des arbres

5.6 Implémentation des arbres

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.1.html [25-10-2001 20:22:39]


5.1 Définitions

[Remonter] [Precedent] [ Suivant]

5.1 Définitions
Un arbre avec racine est composé de deux ensembles N et A appelés respectivement l'ensemble des
noeuds et l'ensemble des arcs et d'un noeud particulier r appelé racine de l'arbre. Les éléments de A sont
des paires ( n1 , n2 ) d'éléments de N . Une arc ( n1 , n2 ) établit une relation entre n1 , appelé noeud
parent, et n2 , appelé noeud enfant de n1 , A doit être tel que chaque noeud, sauf la racine, a exactement
un parent.
On appelle feuille de l'arbre les noeuds qui n'ont pas d'enfant et noeud intérieur les noeuds qui ne sont ni
des feuilles ni la racine. Le degré d'un noeud est le nombre de ses enfants et le niveau d'un noeud est le
nombre d'arc qu'il faut remonter pour atteindre la racine depuis ce noeud (la racine est donc de niveau 0).
La hauteur de l'arbre est le plus grand degré qu'on trouve parmi les noeuds.
Si l'ordre entre les sous-arbre enfants est pris en compte on parlera d'arbre ordonné (à ne pas confondre
avec un arbre trié).
On peut fixer un degré maximum pour chaque arbre. On parlera alors d'arbre unaire, binaire, ternaire, etc.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.2.html [25-10-2001 20:22:42]


5.2 Une vue polylithique des arbres

[Remonter] [Precedent] [ Suivant]

5.2 Une vue polylithique des arbres


En tant que structure on peut définir un arbre comme
● un arbre vide

● un noeud racine uniquement

● ou bien un noeud racine qui possède une valeur et qui est lié à 0, 1 ou plusieurs sous-arbres
éventuellement vides
On peut définir un type abstrait arbre dont les valeurs sont de type T à l'aide des opérations ci-dessous
type résultat opération paramètres description
fournit un arbre formé uniquement d'un noeud racine qui a la
Arbre new Arbre T val
valeur val
insère b comme n-ième enfant de la racine
insère int n, Arbre b Précondition: n doit être compris entre 0 et le nombre d'enfants
de la racine.
supprime int n supprime le n-ième enfant
modifVal T val la racine de l'arbre prend la valeur val
T valeur
int nbEnfants nombre d'enfants de la racine
Arbre enfant int i fournit l'arbre qui est le n-ième enfant de la racine
boolean estVide vrai si l'arbre n'a aucun noeud

Exemple. Construction d'un arbre avec ces opérations :

new Arbre("haut")
.insere(0, new Arbre("milieu-1"))
.insere(1, new Arbre("milieu-2")
.insere(0, new Arbre("bas-1"))
.insere(1, new Arbre("bas-2"))
);

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.3.html [25-10-2001 20:22:47]


5.3 Parcours d'un arbre

[Remonter] [Precedent] [ Suivant]

5.3 Parcours d'un arbre


On utilisera la méthode enfant() pour parcourir récursivement un arbre à partir de sa racine.
Contrairement à la structure de liste ou de file, il n'y a pas d'ordre canonique pour parcourir un arbre,
même ordonné. On distingue deux grandes catégories d'odre de parcours: le parcours en profondeur et le
parcours en largeur.

5.3.1 Parcours en profondeur


Dans le parcours en profondeur, à partir de la racine on descend dans un sous-arbre, qu'on explore
complètement, puis on passe à un autre sous-arbre, et ainsi de suite jusqu'au dernier sous-arbre de la
racine. Le principe de parcours est le suivant:
méthode parcourirEnProfondeur() de Arbre a{
parcourir la racine de a
pour chaque enfant e de a
e.parcourirEnProfondeur()
}
Exemple: impression du contenu d'un arbre:
On effectue un parcours en profondeur d'un arbre pour l'imprimer sous forme parenthésée:
méthode imprime() de Arbre a {
print("(");
print(a.valeur());
pour i=0 .. nbEnfants()-1 {
print(" ");
a.enfant(i).imprime();
}
print(") ");
}

Si l'arbre haha est construit comme :


haha = new Arbre("haut")
.insere(0, new Arbre("milieu-1"))
.insere(1, new Arbre("milieu-2")
.insere(0, new Arbre("bas-1"))
.insere(1, new Arbre("bas-2"))

http://cui.unige.ch/~falquet/std/notes/p5.4.html (1 of 2) [25-10-2001 20:22:55]


5.3 Parcours d'un arbre

);

Le résultat de haha.imprime() sera:


(haut (milieu-1) (milieu-2 (bas-1) (bas-2) ) )

Cas particulier du parcours des arbres binaires


Dans le cas des arbre binaires on distingue trois sortes de parcours en profondeur:
● pré ordre: on parcourt la racine, puis le sous-arbre gauche, puis le sous-arbre droit

● post ordre: on parcourt le sous-arbre gauche, puis le sous-arbre droit, puis la racine

● en ordre: on parcourt le sous-arbre gauche, puis la racine, puis le sous-arbre droit

5.3.2 Parcours en largeur


Dans le parcours en largeur on commence par parcourir tous les noeuds de niveau 1 (les enfants de la
racine), puis on parcours tous les noeuds de niveau 2 (les petits-enfants), puis ceux de niveau 3, etc.
L'algorithme général est:
méthode parcourirEnLargeur de Arbre a {
ce_niveau = {a}
tant que ce_niveau est non vide {
niveau_inferieur = {}
pour chaque arbre s de ce_niveau {
parcourir la racine de s
niveau_inferieur = niveau_inferieur » les enfants de s
}
ce_niveau = niveau_inferieur
}
}
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.4.html (2 of 2) [25-10-2001 20:22:55]


5.4 Représentation linéaire des arbres

[Remonter] [Precedent] [ Suivant]

5.4 Représentation linéaire des arbres


Le parcours en profondeur de la fonction imprime, ci-dessus, montre que l'on peut représenter tout arbre
sous forme d'une séquence de symboles comprenant des parenthèses. Le principe consiste à représenter
un arbre sous la forme
( racine enfant0 enfant1 ... enfantN )
Les enfants étant eux-mêmes des arbres, on obtient une séquence avec des parenthèses imbriquées.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.5.html [25-10-2001 20:23:04]


5.5 Exemples d'utilisation des arbres

[Remonter] [Precedent] [ Suivant]

5.5 Exemples d'utilisation des arbres


5.5.1 Les arbres à lettre
Un arbre à lettre est une manière compacte de représenter un ensemble de mot (p.ex. un lexique). L'idée
est de regrouper tous les mots en un arbre dont chaque arc représente une lettre. Un mot est représenté
par un chemin de la racine à un noeud portant l'étiquette "fin de mot". Par exemple, prenons les mots
syndrome
syntaxe
sommaire
somme
sommet
sot
L'arbre à mot correspondant sera:

(Le lecteur averti aura sans doute remarqué qu'il ne s'agit de rien d'autre qu'un automate à état fini qui
reconnaît le langage formé des mots donnés).
L'intérêt de l'arbre à lettres réside dans la rapidité de la reconnaissance d'un mot. Pour tester si un mot
formé des lettres c1, c2, ..., ck appartient au dictionnaire on part de la racine et on suit les arcs indexés
par les lettre c1, c2, etc. Le mot appartient au dictionnaire si et seulement si l'on arrive sur un noeud de
fin de mot. Le temps de calcul est donc proportionnel à la longueur du mot à reconnaître et est
indépendant de la taille du dictionnaire !

http://cui.unige.ch/~falquet/std/notes/p5.6.html (1 of 3) [25-10-2001 20:23:25]


5.5 Exemples d'utilisation des arbres

5.5.2 La représentation et la manipulation d'expressions


On peut représenter une expression arithmétique par un arbre dont la racine contient un opérateur et les
sous-arbres les opérandes. Par exemple:

Une telle représentation permet tout sortes de manipulations des expressions:

Evaluation:
L'évaluation consiste à remplacer un (sous-)arbre par la valeur de l'expression qu'il représente. Ainsi,
l'arbre A1 ci-dessus sera remplacé par l'arbre constitué du seul noeud (10); pour l'arbre A2 on évalue
d'abord le sous-arbre (- 4 6) qui donne -2, puis (* 3 -2) qui donne -6.

Simplification:
Si l'expression contient des variables comme opérandes, on ne peut pas l'évaluer complètement. Par
contre, on peut appliquer des règles de simplification du genre:
● remplacer (* 1 expr ) ou (* expr 1) par ( expr )

● remplacer (+ 0 expr ) ou (+ expr 0) par ( expr )

● remplacer (+ (* expr1 x ) (* expr2 x )) par (* (+ expr1 expr2 ) x )

http://cui.unige.ch/~falquet/std/notes/p5.6.html (2 of 3) [25-10-2001 20:23:25]


5.5 Exemples d'utilisation des arbres

Dérivation:
On peut calculer la dérivé (selon x) d'une formule en appliquant des transformations telles que:
d(* cte x) = (cte)
d(** x n) = (* n (** x (- n 1)))
d(+ f g) = (+ d(f) d(g))
d(* f g) = (+ (* d(f) g) (* f d(g)))
etc.
Cette représentation, avec quelques variantes, est la base des systèmes de manipulation et de résolution
symbolique d'équations mathématiques (Mathematica, Maple, etc.). Elle peut également être utilisée dans
les compilateurs pour vérifier le bon typage des expressions et pour les optimiser. Bien que nous n'ayons
montré que des opérations mathématiques, cette représentation peut aussi s'appliquer à d'autres domaines
où la notion d'opération et d'opérandes existe.

5.5.3 Documents structurés


Une document structuré est composé d'une hiérarchie d'éléments qui forme un arbre. Les feuilles de
l'arbre sont des éléments atomiques qui contiennent le texte lui-même. Les autres éléments sont
composés de sous-éléments.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.6.html (3 of 3) [25-10-2001 20:23:25]


5.6 Implémentation des arbres

[Remonter] [Precedent] [ Suivant]

5.6 Implémentation des arbres


On peut réaliser l'implémentation d'une structure d'arbre de manière très simple et directe. Il suffit de
définir une classe Noeud dont les objets représentent chacun un noeud de l'arbre et les liens vers les
enfants de ce noeud. On obtient le type concret suivant :
type variable description
T valeur la valeur stockée dans ce noeud, d'un type T quelconque
Liste<Noeud> ou
Tableau<Noeud> enfants la liste des noeuds racine des sous-arbres enfants
(c.f. ci-dessous)

Si le nombre maximum d'enfants est fixé on peut utiliser pour la variable enfants un tableau de Noeuds,
pour un arbre binaire on utilisera deux variables gauche et droite de type Noeud, dans le cas général on
utilisera une liste de Noeuds.
N.B. Nous noterons Liste<Noeud> le type Liste que nous avons défini précédemment où le type T de
éléments est Noeud. De même pour les ensembles, files, etc.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p5.7.html [25-10-2001 20:23:30]


6 Graphes

[Remonter] [Precedent] [ Suivant]

6 Graphes
Le graphe est une structure que l'on trouve dans la modélisation d'un grand nombre de situations très
diverses (réseaux de transports et de communications, ordonnancement de tâches, relations entre
personnes ou institutions, etc.). En fait, dès qu'intervient une relation binaire entre des objets d'un même
ensemble on a une structure de graphe. De ce fait, de nombreux problèmes peuvent se ramener à des
problèmes classiques de la théorie des graphes : recherche de chemins (à coût minimal), détection de
cycles, arbres de recouvrement, coloration, etc.

6.1 Définitions

6.2 Type abstrait graphe

6.3 Parcours d'un graphe

6.4 Implémentation

6.5 Algorithmes sur les graphes

6.6 Problèmes NP-complets dans les graphes

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.1.html [25-10-2001 20:23:33]


6.1 Définitions

[Remonter] [Precedent] [ Suivant]

6.1 Définitions
Graphes non orientés
Un graphe est composé de deux ensembles V et E appelés respectivement l'ensemble des sommets (ou
noeuds ou points) et l'ensemble des arêtes. Les éléments de E sont des paires { n1 , n2 } d'éléments de V
.ou des singletons { n }. Une arête { n1 , n2 } représente un lien non orienté entre n1 et n2 ; un singletion
{ n } représente un lien de n avec lui-même (une boucle).
Le degré d'un sommet est le nombre d'arêtes qui contiennent ce sommet.
Deux sommet n1 , n2 sont adjacents s'il existe une arête { n1 , n2 }.
Un chemin non orienté de n 0 à n t (ou chaîne) est une séquence d'arêtes de la forme {n 0 , n 1 }, {n 1 , n 2
}, {n 2 , n 3 }, ..., {n t-2 , n t-1 }, {n t-1 , n t }. Un chemin est dit simple s'il ne rencontre pas deux fois le
même sommet. Un chemin de n 0 à n 0 est un cycle.

Un graphe est connexe si tous ses sommets sont reliés deux à deux par au moins un chemin.
Un sous-graphe d'un graphe G = (V, E) est un graphe G' = (V', E') tel que V' Õ V, E' Õ E.
Une composante connexe de G est un sous-graphe G' de G, tel que 1) G' est connexe, 2) il n'existe pas de
sous-graphe G'' de G tel que G' est un sous-graphe de G'' et G'' est connexe.

Graphes orientés
Un graphe orienté est composé d'un ensemble de sommets et d'arcs. Les arcs sont des paires ( n1 , n2 ).
Le premier sommets de la paire est appelé origine de l'arc et le second destination .
Un chemin de n 0 à n t est une séquence d'arcs de la forme (n 0 , n 1 ), (n 1 , n 2 ), (n 2 , n 3 ), ..., (n t-2 , n t-1
), (n t-1 , n t ). Un chemin est dit simple s'il ne rencontre pas deux fois le même sommet. Un chemin de n 0
à n 0 est un circuit.

On peut également associer une valeur à chaque sommet et à chaque arête (par exemple pour indiquer un
cout ou une distance). Comme dans le graphe ci-dessous.

http://cui.unige.ch/~falquet/std/notes/p6.2.html (1 of 2) [25-10-2001 20:23:43]


6.1 Définitions

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.2.html (2 of 2) [25-10-2001 20:23:43]


6.2 Type abstrait graphe

[Remonter] [Precedent] [ Suivant]

6.2 Type abstrait graphe


Nous commencerons par le cas des graphes orientés. Pour définir abstraitement un graphe orienté il nous
faut en fait trois types: Graphe, Sommet et Arc. Le type présenté ci-dessous est monolythique dans le
sens où un graphe n'est pas formé de sous-graphe. De plus, les sommets et arcs d'un graphe ne peuvent
être utilisés dans un autre graphe.
Le type abstrait graphe est essentiellement une combinaison de deux ensembles (sommets et arêtes/arcs).
type résultat opération paramètres description
Graphe new Graphe crée un graphe vide
ajoute Sommet s crée un sommet et l'ajoute au graphe
retire Sommet s retire le sommet s du graphe - et les arcs adjacents
crée un arc (s1, s2) et l'ajoute au graphe
ajoute Sommet s1, s2
PRE: s1 et s2 appartiennet au graphe
retire Arc a supprime un arc
boolean estVide teste si le graphe est vide
boolean appartient Sommet s teste si le sommet appartient à ce graphe
boolean appartient Arc a teste si l'arc appartient à ce graphe
Ensemble<Arc> entrants Sommet s fournit l'ensemble des arcs dont l'origine est s
Ensemble<Arc> sortants Sommet s fournit l'ensemble des arcs dont la destination est s

Remarques:
Pour éviter qu'un sommet puisse appartenir à deux graphes différents, c'est l'opération ajoute qui créee
elle-même un nouveau sommet. On ne peut donc pas prendre un sommet existant et l'ajouter au graphe. Il
en va de même pour l'ajout d'arcs.
Dans le cas d'un graphe non orienté on remplacera les opérations entrants(s) et sortants(s) par une seule
opération adjacents(c) (voir également le type Arc).

Le type Sommet
Le type somet possède les opérations nécessaires à sélectionner et modifier la valeur, de type VS, d'un
sommet.
type résultat opération paramètres description
Sommet new Sommet crée un sommet
metValeur VS v modifie la valeur du sommet
VS valeur donne la valeur du sommet

http://cui.unige.ch/~falquet/std/notes/p6.3.html (1 of 2) [25-10-2001 20:23:57]


6.2 Type abstrait graphe

Ensemble<Arc> sortants Sommet s fournit l'ensemble des arcs dont la destination est s

Le type Arc
Un arc possède un sommet origine, un sommet destination et une valeur de type VA
type résultat opération paramètres description
Arc new Arc Sommet s1, s2 crée un arc de s1 à s2
Arc metValeur VA v
Sommet origine sommet origine
Sommet destination sommet destination
VA valeur Arc a valeur de l'arc

Remarque
Le type arc ne possède pas d'opérations pour modifier l'origine et la destination, ceci pour garantir que
l'on ne puisse pas associer des sommets de deux graphes différents.
Dans le cas d'un arc non orienté (arête) on remplacera origine() et destination() par un opération
sommets() qui retourne les deux sommets sous forme d'un tableau (p.ex.).
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.3.html (2 of 2) [25-10-2001 20:23:57]


6.3 Parcours d'un graphe

[Remonter] [Precedent] [ Suivant]

6.3 Parcours d'un graphe


Parcourir un graphe consiste à visiter tous les sommets une et une seule fois. Comme dans le cas des
arbres on peut effectuer un parcours en profondeur ou en largeur à partir d'un sommet choisi.
L'algorithme général de parcours en profondeur s'écrit :

méthode parcourir de Graphe G = (V, E) {


Vus = {} "Cet ensemble sert à mémoriser les sommets déjà visités."
tant que (vus V) {
choisir un sommet s V - vus
G.parcourirDepuis(s, Vus)
}
}
méthode parcourirDepuis(sommet s, ensemble SV) de Graphe G {
visiter s
SV = SV » {s}
pour toute arête {s, s'} adjacente à s
si (s' SV) G.parcourirDepuis(s', SV)
}

On pourra effectuer un parcours en largeur manière analogue au parcours en largeur des arbres. Mais
comme dans l'algorithme ci-dessus, il faut mémoriser les sommets déjà vus pour éviter de les visiter
plusieurs fois et de boucler indéfiniment.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.4.html [25-10-2001 20:24:00]


6.4 Implémentation

[Remonter] [Precedent] [ Suivant]

6.4 Implémentation
En fonction du type d'algorithme que l'on veut utiliser on utilisera différentes implémentations des
graphes. Les techniques les plus classiques pour représenter un graphe sont:
● la liste de sommets et d'arêtes

● la liste de sommets et d'arêtes avec listes d'adjacence

● la matrice d'adjacence

Liste de sommets et d'arêtes avec listes d'adjacence


En principe il est suffisant de n'avoir qu'une liste de sommets et d'arêtes (une liste d'arêtes seule ne suffit
pas car il peut y avoir des sommets isolés qui n'appartiennent à aucune arête). Cependant de nombreux
algorithmes de traitememt des graphes ont besoin d'une opération qui fournit la liste des arêtes qui
touchent un sommet donné. Pour que ces algorithmes puissent s'exécuter dans des temps raisonnables il
vaut mieux associer à chaque somment une liste des arêtes qui le touchent. Pour le graphe précédent on
obtiendrait la structure suivante:

sommet: s1 s2 s3 s4 s5 s6
valeur: milano tokyo london lyon paris geneve
liste-adj.: (a1 a2 a3) (a1) (a2) (a3 a4 a5) (a4 a6) (a5 a6)

arc: a1 a2 a3 a4 a5 a6
source: s1 s1 s1 s5 s6 s6
destination: s2 s3 s4 s4 s4 s5
valeur: 401 501 601 201 301 101

Dans le cas des graphes orientés ont peut, si nécessaire, distinguer la liste des arcs entrants et sortants
pour chaque sommet. Dans l'implémentations des opérations il faudra tenir compte de l'existence de ces
listes qui créent de la redondance dans la structure. Par exemple, lors de l'ajout d'une nouvelle arête il
faudra non seulement mettre celle-ci dans la liste des arêtes mais également dans la liste d'adjacence des
deux sommets qu'elle relie. De même lors de la suppression.

Matrice d'adjacence
Si on a n sommets on construit une matrice n x n. La valeur de l'élément (i,j) de la matrice indique si les
sommets i et j sont adjacents (i.e. s'il existe un arc entre i et j). Dans le cas ou les ars sont valuées
l'élément (i,j) contiendra la valeur de l'arc et une valeur particulière (p.ex. l'objet Nil) i et j ne sont pas

http://cui.unige.ch/~falquet/std/notes/p6.5.html (1 of 2) [25-10-2001 20:24:16]


6.4 Implémentation

adjacents. Pour le graphe ci-dessus la matrice est la suivante:


ADJACENCE milano tokyo london lyon paris geneve
milano nil 401 501 601 nil nil
tokyo nil nil nil nil nil nil
london nil nil nil nil nil nil
lyon nil nil nil nil nil nil
paris nil nil nil 201 nil nil
geneve nil nil nil 301 101 nil

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.5.html (2 of 2) [25-10-2001 20:24:16]


6.5 Algorithmes sur les graphes

[Remonter] [Precedent] [ Suivant]

6.5 Algorithmes sur les graphes


6.5.1 Recherche du plus court chemin
A chaque arc est associé un coût.
Le coût d'un chemin est la somme des coûts des arcs qui le composent.
Le plus court chemin du sommet x au sommet y existe si
● il existe un chemin de x à y

● il n'y a pas de circuit absorbant (à coût négatif)

Algorithme de Disjkstra
Calcul du coût du chemin le plus court de s0 à chaque autre sommet du graphe G = (V, A) et
mémorisation des chemins les plus courts.
Principe:
● On part du sommet s0

● On construit itérativement un ensemble S de sommets pour lesquels on connait le plus court


chemin depuis s0.

Algorithme:

Soit c(s1, s2) le coût de l'arc (s1, s2) ou bien · s'il n'y a pas d'arc de s1 à s2
S = {}
D[s0] ¨ 0
pour chaque sommet s dans V - S { D[s] ¨ c(s0, s) }
tant que (S V) {

http://cui.unige.ch/~falquet/std/notes/p6.6.html (1 of 3) [25-10-2001 20:24:39]


6.5 Algorithmes sur les graphes

chercher le sommet s de V - S tel que D[s] est minimum


ajouter s à S
pour chaque v de V - S {
si (D[s] + c(s, v) < D[v]) {
D[v] ¨ D[s] + c(s, v)
P[v] ¨ s
}
}
}.

L'application de l'algorithme sur le graphe G ci-dessous, à partir du sommet 1, fournit les distances
minimales montrées sur le graphe MIN.

6.5.2 Recherche de composantes connexes


A partir de s on effectue un parcours en profondeur ou en largeur. L'ensemble des sommets atteints forme
la composante connexe Cs de s. Pour trouver toutes les cc on calcule itérativement les cc des sommets
qui n'ont pas encore été atteints lors des parcours précédents.

http://cui.unige.ch/~falquet/std/notes/p6.6.html (2 of 3) [25-10-2001 20:24:39]


6.5 Algorithmes sur les graphes

6.5.3 Arbre de recouvrement à coût minimal


On veut un sous ensemble Ar des arêtes de G = (S, A) tel que
1. (S, Ar) est un arbre
2. il n'existe pas (S, Ar') un autre arbre de coût inférieur

Algorithme de Kruskal
Ar := Ø; Reste := A;
tant que (S, Ar) ne couvre pas G {
choisir l'arête a de Reste de coùt minimal
supprimer a de Reste
si a ne crée pas de cycle avec Ar alors ajouter a à Ar
}

Test de création d'un cycle


Problème: étant donné un graphe G = (S, A), pas forcément connexe, on veut savoir si l'ajout d'une arête
a crée un cycle dans G.
Technique: On tient a jour une liste des composantes connexes de G: à chaque sommet on associe un
sommet représentant sa composante connexe.
● Si les deux extrémités de a sont dans la même cc alors il y a création d'un cycle.

● Sinon a relie deux cc qui maintenant n'en forment plus qu'une. Il faut mettre à jour les
représentants des sommets de l'une des cc.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.6.html (3 of 3) [25-10-2001 20:24:39]


6.6 Problèmes NP-complets dans les graphes

[Remonter] [Precedent] [ Suivant]

6.6 Problèmes NP-complets dans les graphes


La théorie des graphes est une source extrêmement riche de problèmes NP-complets. Rappelons que pour
l'instant les meilleurs algorithme connus pour résoudre ces problèmes ont un temps d'exécution
exponentiel par rapport à la taille du problème.

Couverture de sommets
Etant donné un entier K, existe-t-il un sous-ensemble V' des sommets de V tel que |V'| £ K et pour
chaque arête {s1, s2}, {s1, s2} « V' Ø ?

Clique
Etant donné un entier J, le graphe contient-il une clique de taille J ou plus ? Une clique est un
sous-ensemble des sommets tel que les membres de la cliques sont tous reliés par une arête.

Circuit Hamiltonien
Existe-t-il un circuit hamiltonien dans le graphe ? C'est-à-dire une séquence <s 1 , s 2 , ..., s n > des
sommets de G telle que {s i , s i+1 } (1 £ i < n) est une arête de G, de même que {s 1 , s n }.

Mais également:
L'isomorphisme de sous-graphes, l'arbre de recouvrement à degré limité, la coloration avec 3 couleurs,
etc..
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p6.7.html [25-10-2001 20:24:43]


7 Implémentation des Types Collection

[Remonter] [Precedent] [ Suivant]

7 Implémentation des Types Collection


7.1 Introduction

7.2 Tableaux extensibles

7.3 Listes liées

7.4 Techniques de hachage

7.5 Arbres de recherche

7.6 Arbres équilibrés (AVL)

7.7 B-arbres

7.8 Arbres multidimensionnels

7.9 Structures de données pour "recherche-union"

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.1.html [25-10-2001 20:24:46]


7.1 Introduction

[Remonter] [Precedent] [ Suivant]

7.1 Introduction
Implémenter un type abstrait consiste à
● définir la structure interne des objets et

● définir les algorithmes nécessaires pour réaliser les différentes opérations.

Structure interne
La structure interne d'un objet est composée d'un ensemble de variables d'instance de divers types. 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 construction d'une hiérarchie
d'abstraction de types (qui n'est pas la hiérarchie des sous-classes) qui repose sur les types les plus
simples ou types de base.
Suivant le langage et l'environnement utilisé, les types de base peuvent varier. Nous considé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.

Algorithmes
Nous définirons les algorithmes en pseudo-langage orienté-objet. En plus des instructions de contrôle (si
() ... sinon ... ; tant que {...}, etc.), de l'affectation et des invocations de méthodes nous utiliserons la
notation pointée <objet>.<varialbe d'instance> pour faire référence aux variables d'instance des objets.

Propriétés de l'implémentation
On s'intéressera plus 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 contraintes de la
spécification du type abstrait: types des paramètres et résultats, équations, invariants.
● 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 données).

http://cui.unige.ch/~falquet/std/notes/p7.2.html (1 of 2) [25-10-2001 20:24:51]


7.1 Introduction

Structures d'implémentation de base


Il existe un cerain nombre de structures de données que l'on retrouve fréquemment dans l'implémentation
de divers types abstraits. Ces structures forment en quelque sorte des briques de base pour réaliser les
implémentations. Il s'agit des tableaux dynamiques, des listes liées, des arbres de recherche et des tables
de hachage.
Nous allons examiner ces structures et voir comment elles peuvent servir à implémenter les types
abstraits collection.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.2.html (2 of 2) [25-10-2001 20:24:51]


7.2 Tableaux extensibles

[Remonter] [Precedent] [ Suivant]

7.2 Tableaux extensibles


Une variable d'instance de type tableau est une référence à une zone de la mémoire qui contient les
éléments du tableau (placés en général les uns à la suite des autres dans la mémoire). A un instant donné
le tableau a une taille fixe. Si l'on a besoin d'agrandir le tableau il est possible d'allouer une nouvelle
place mémoire, plus grande, et d'y recopier le tableau courant. Ensuite on utilise ce nouveau tableau à la
place de l'ancien.

7.2.1 Implémentation du type pile


On utilise deux variables:
type variable description
le nombre d'élément dans la pile == l'indice du prochain élément sur lequel on peut
entier longueur
empiler
T [ ] elements le tableau extensible des éléments

Réalisation des opérations:


opération implémentation complexité
new Pile elements = new T[N]
si (longueur=N) étendre elements; O(1+d)
empiler(T e)
elements[longueur] ¨ e; longueur++; (voir remarque)
depiler longueur ¨ longueur - 1 O(1)
sommet retourne elements[longueur-1] O(1)
estVide retourne (longueur = 0) O(1)

Remarque
Si l'extension de tableau n'intervient pas trop souvent, par exemple si l'on double à chaque fois la taille du
tableau, le temps moyen pour empiler est quasiment constant. Supposons que la taille de départ du
tableau soit N 0 , qu'on choisisse de doubler chaque fois que le tableau est plein, et qu'on fasse 2 t N 0
opérations empiler(). On devra étendre le tableau t fois, le nombre d'éléments à copier lors des extensions
sera: N 0 , 2N 0 , 4N 0 , ..., 2 t N 0 = (2 t+1 -1)N 0 . Le nombre moyen de recopies par opération sera donc
(2 t+1 -1)N 0 /2 t N 0 = 2 - 1/2 t . On voit que ce nombre est quasiment constant.

Si l'on choisit des extensions de taille fixe D on peut tomber dans des complexité bien plus grande. Le
plus mauvais choix étant bien sûr D = 1 qui oblige à étendre le tableau à chaque opération.

http://cui.unige.ch/~falquet/std/notes/p7.3.html (1 of 2) [25-10-2001 20:24:57]


7.2 Tableaux extensibles

7.2.2 Implémentation des listes:


On utilise la même structure que pour la pile.
Un tableau extensible permet d'implémenter efficacement (en temps constant) les opérations element(i)
d'accès au i-ième élément de la liste et remplacer(i, e) de remplacement d'un élémment par e.
Par contre, les opérations d'insertion et de suppression nécessitent le déplacement d'éléments du tableau,
leur complexité en temps est donc proportionnelle à la taille de la liste. De même, la recherche d'un
élément se fait en un temps linéaire par rapport à la taille de la liste.

7.2.3 Implémentation des ensembles


Contrairement à la liste, l'ajout et la suppression d'élément peuvent se faire en un temps quasi constant, il
suffit d'ajouter le nouvel élément à la fin du tableau (comme pour empiler() ). Lorsqu'on retire un
élément on le remplace par le dernier et on diminue la longueur de 1.
Ceci fonctionne pour autant qu'on n'essaye jamais d'ajouter deux fois le même élément. Pour rendre
l'implémentation robuste il faut vérifier avant chaque ajout que l'élément à ajouter ne soit pas déjà dans
l'ensemble. Cette opération requière un parcours séquentiel du tableau qui rend le temps d'exécution de
l'opération d'ajout proportionnel à la taille de l'ensemble.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.3.html (2 of 2) [25-10-2001 20:24:57]


7.3 Listes liées

[Remonter] [Precedent] [ Suivant]

7.3 Listes liées


7.3.1 Définition
Une liste liée est composée d'un ensemble de conteneur, appelés noeuds, qui contiennent chacun un
élément et sont liés les uns aux autres pour former une liste. Un noeud ne peut appartenir qu'à une seule
liste. La structure d'un noeud pour une liste liée d'éléments de type T est composé des deux variables:
T contenu l'élément contenu dans ce noeud
Noeud suivant le prochain noeud de la liste, nul si ce noeud est le dernier

Un objet de type ListeLiée est composé des variables:

Noeud début référence au premier noeud de la liste


entier longueur longueuer actuelle de la liste.

Cette dernière variable n'est pas strictement nécessaire, mais elle accélère les opérations. Sans elle il
faudrait à chaque fois parcourir toute la liste pour calculer sa longueur. La See Une liste liée montre
graphiquement les objets composant une liste liée.
On peut implémenter directement les opérations d'insertion et de suppression au début de la liste liée :
méthode insererDebut(T e) de ListeLiée {
nn ¨ new Noeud()
nn.contenu ¨ e

http://cui.unige.ch/~falquet/std/notes/p7.4.html (1 of 4) [25-10-2001 20:25:11]


7.3 Listes liées

nn.suivant ¨ premier
premier ¨ nn
longueur ¨ longueur+1}

méthode supprimerDebut() de ListeLiée {


PRECONDITION: premier nul
premier ¨ premier.suivant
longueur ¨ longueur-1 }

Contrairement au tableau, la liste liée n'offre pas de mécanisme d'accès direct à un élément. Pour accéder
au i-ième élément d'une liste l il faut suivre les référence suivant depuis le début de la liste.

n ¨ l . premier;
pour j de 1 à i-1 { n ¨ n . suivant }

Etant donné qu'il est coûteux d'atteindre une position donnée dans une liste liée on a tout avantage à
1. mémoriser la position atteinte, au cas ou plusieurs traitements sont à faire sur l'objet atteint.
2. organiser les traitements de manière à ce qu'ils accèdent séquentiellement aux éléments de la
liste plutôt que dans un ordre aléatoire.
Dans ces conditions il est avantageur de définir une structure d'itérateur qui représente un curseur se
déplaçant sur la liste. L'itérateur permet de se positionner à l'endroit voulu, de mémoriser la position et
d'y d'effectuer des opérations d'insertion ou de suppression. L'implémentation d'un itérateur sur une liste
liée est directe. La structure interne de l'intérateur est composée des variables
Liste liste la liste sur laquelle opère l'itérateur
Noeud position le noeud courant

Les opérations sont décrites dans la table ci-dessous.


opération description
position ¨ l.premier
initialiser(Liste l)
liste ¨ l
PRE: position nul
avancer()
position ¨ position.suivant
PRE: position nul
courant() -> Noeud
retourner position.contenu

http://cui.unige.ch/~falquet/std/notes/p7.4.html (2 of 4) [25-10-2001 20:25:11]


7.3 Listes liées

dansListe -> booléen retourner position nul


PRE: position nul
nn ¨ new Noeud()
insererApres(T e) nn.contenu ¨ e
nn.suivant ¨ position.suivant
position.suivant ¨ nn
PRE: position nul et
position.suivant nul
supprimerApres
courant.suivant ¨
courant.suivant.suivant

Pour accélérer certaines opérations comme insererALaFin() ou supprimerLeDernier on peut maintenir


dans la structure ListeLiée une variable qui fait référence au dernier noeud de la liste. Mais il faut alors
ajouter à chaque noeud une variable qui indique quel est le noeud précédent. La référence au précédent
permet également d'effectuer efficacement une opération comme insérerAvant() , supprimerAvant() et
reculer() .

7.3.2 Implémentation des piles


On peut implémenter un objet Pile à l'aide d'une seule variable elements de type ListeLiée qui contient les
éléments de la pile. Le dernier élément entré dans la pile est placé au début de la liste.
méthode empiler ( T e ) de Pile
{ element.insererDebut(e) }

Retirer un élément revient à remplacer le début de la liste par le noeud suivant le premier
méthode retirer() de Pile
{elements.supprimerDebut() }

7.3.3 Implémentation du type abstrait Liste


Contrairement à l'implémenation par tableau, l'accès au i-ième élément n'est plus direct mais nécessite le
parcours de i noeuds.
méthode insererA(entier i, T e) de Liste l{
si (i = 0) element.insererDebut(e)
sinon
IterateurListe it <- new IterateurListe(element) // itérateur sur cette liste
pour k de 1 à i { it.avancer() }
it.insererApres(e)
}

http://cui.unige.ch/~falquet/std/notes/p7.4.html (3 of 4) [25-10-2001 20:25:11]


7.3 Listes liées

Etant donné qu'il est coûteux d'accéder au i-ième élément d'une liste liée, mais qu'en revanche il est
rapide de passer d'un noeud au suivant, on a tout intérêt à organiser les traitements à effectuer sur la liste
de manière séquentielle et non pas aléatoire.

7.3.4 Implémentation des ensembles


Chaque élément de l'ensemble est stocké comme contenu d'un noeud de la liste liée. Les nouveaux
éléments sont ajoutés au début (ou à la fin, ... ou n'importe où ailleurs).
Comme nous l'avons remarqué dans la spécification abstraite il faut tenir compte du fait que l'ajout répété
du même objet doit être équivalent à un ajout unique, p. ex. l'ensemble {3, 3, 6, 3, 6} est le même que {3,
6}. Il y a (au moins) deux manières de traiter ce problème:
1. avant d'ajouter un élément on vérifie qu'il n'est pas déjà présent dans l'ensemble, s'il est présent on
ne fait rien; la cardinalité de l'ensemble est alors égale à la taille de la liste.
2. on ne fait aucun test à l'ajout mais c'est lors du calcul de la cardinalité qu'on ne doit compter qu'une
seule fois les éléments qui apparaissent plusieurs fois dans la liste. De manière analogue,
l'opération retire doit supprimer toutes les occurences de l'élément.
Si l'on choisit la première solution, l'opération d'ajout est plus complexe que pour une simple liste et plus
l'ensemble grandit plus l'ajout devient coûteux, puisqu'en moyenne il faut parcourir la moitié de la liste
des éléments pour vérifier si un élément est déjà dans la liste.
Quelle que soit la méthode choisie, l'opération de test d'appartenance d'un élément à l'ensemble prend un
temps proportionnel à la taille de l'ensemble. Si cette opération est fréquente dans l'application envisagée
il est nécessaire de disposer d'une structure qui permette de rechercher plus rapidement un élément. Les
structures les plus fréquemment utilisées dans ces cas sont les tables de hachage et les arbres de
recherche .

Opérations d'union, intersection, différence

La performance de ces opérations globales dépend fortement de la structure de données choisie. Prenons
l'exemple de l'union d'ensembles: avec une représentation par liste on ne peut pas simplement concaténer
les listes pour réaliser l'union car il y a risque de créations de doublons. L'opération nécessite une
vérification (relativement longue) pour éviter les doublons. Par contre, si l'univers des éléments
considérés est ordonné, il est avantageux de placer les éléments dans la liste en fonction de cet ordre.
L'union revient alors à une opération de fusion . Une représentation par liste triée est également plus
efficace pour l'intersection et la différence.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.4.html (4 of 4) [25-10-2001 20:25:11]


7.4 Techniques de hachage

[Remonter] [Precedent] [ Suivant]

7.4 Techniques de hachage


Le but des techniques de hachage est de déterminer l'emplacement de stockage d'un objet par un calcul
basé uniquement sur la valeur de cet objet. Dans le hachage ouvert les emplacement de stockage sont
extensibles (p.ex. des listes liées) alors que dans le hachage fermé on ne dispose que d'un nombre limité
d'emplacements ne pouvant recevoir qu'un seul objet.
Les techniques de hachages ne conviennent qu'à l'implémentation des types non ordonnés (ensembles,
multi-ensembles, fonctions) car, par définition, on ne peut placer un élément à une position définie à
l'avance.

7.4.1 Hachage ouvert


Principe
L'idée consiste à remplacer une liste liée par un tableau de N listes. On se donne également une fonction
h (dite de hachage) qui, à partir d'un objet o fournit un entier h(o) compris entre 1 et N. Si h(o) = i, l'objet
o est placée dans la i-ème liste.
Le hachage ne peut être utilisé pour représenter des piles, files ou listes car il est impossible de conserver
l'ordre d'ajout des éléments.

Structure interne:
On s'appuye sur l'implémentation des ensembles par les listes liées. La varialbe ll est un tableau de N
ensembles représentés par des listes.

Opérations:

résultat opération parmètres description


ll ¨ tableau de N Liste
Ensemble new Ensemble
pour i de 0 à N-1 {ll[i] ¨ new EnsListe()}
i ¨ t.h()
ajoute Tt
ll[ i] . ajoute(t)
i ¨ t.h()
retire Tt
ll[ i] . retire(t)
effectue l'union, l'intersection ou la différence
Ensemble union/intersection/différence Ensemble e
sous-liste par sous-liste
int cardinalite somme des ll[i].cardinalite()

http://cui.unige.ch/~falquet/std/notes/p7.5.html (1 of 3) [25-10-2001 20:25:21]


7.4 Techniques de hachage

boolean estVide
i ¨ t.h()
boolean appartient Tt
ll[ i] . appartient(t)
boolean inclus Ensemble e teste l'inclusion sous-liste par sous-liste

Complexité en temps
Si la fonction h répartit suffisament uniformément les valeurs entre 1 et N les listes auront une taille
moyenne de S/N où S est le nombre d'éléments de l'ensemble. Dans le pire des cas, si la fonction de
hachage est très mal choisie, tous les éléments vont se placer dans la même liste. On se retrouve dans la
situation d'une représentation par liste liée.
Dans le meilleur des cas il n'y a que zéro ou un élément par liste (ceci implique que le tableau de hachage
est plus grand que le nombre d'éléments). Le temps d'accès à un élément se résume alors à <temps de
calcul du h-code> + <accès au premier élément d'une liste>, ce temps est constant.
Dans un cas "moyen" les éléments seront uniformément répartis entre les N sous-listes. Le temps d'accès
sera alors divisé par N par rapport à la représentation par une seule liste.

Fonctions de hachage
La principale propriété requise pour la fonction de hachage est qu'elle répartisse le plus uniformément
possible les objets dans l'intervalle 1..N et qu'elle soit simple à calculer (sinon on perd tout le bénéfice de
la méthode). On peut immaginer différentes fonctions de hachage suivant le type de données. Pour des
données de type chaine de caractères on peut prendre par exemple:
● la somme modulo N des codes des caractères de la chaine;

● la somme module N de certains caractères (p.ex. le premier, le dernier et un caractère au milieu de


la chaine);
● etc.

Etant donné que la performance de la fonction de hachage dépend du domaine de valeur réellement
utilisé par l'application, il n'est pas possible de définir une fonction qui soit bonne en toutes
circonstances. La fonction de hachage doit donc être une opération du type T des objets à stocker 1 .

7.4.2 Forme bornée et hachage fermé


Si l'on peut déterminer à l'avance une taille maximale que l'application ne dépassera jamais alors on peut
utiliser la technique du hachage fermé. Celle-ci consiste à stocker les informations directement dans la
table de hachage plutot que dans des listes associées. Il faut évidemment que la table soit d'une taille
supérieure ou égale à la taille maximale prévue.
La hachage fermé présente le problème de la collision qui se produit lorsque deux éléments différents x
et y à placer dans la table ont la même valeur par la fonction h (h(x) = h(y)). On résoud ce problème en
cherchant une autre place pour le second élément. Il existe pour cela différentes méthodes:
● on place y dans la première position libre après h(x) (si on arrive au bout de la table on revient au

http://cui.unige.ch/~falquet/std/notes/p7.5.html (2 of 3) [25-10-2001 20:25:21]


7.4 Techniques de hachage

début)
● on cherche une place libre à h(x) + k, h(x) +2k, h(x) + 3k, etc. (modulo la taille de la table), si la
taille de la table est une puissance d'une nombre premier on est sûr d'explorer toutes les positions
possibles avec cette technique
● (déplacement quadratique) on cherche une place libre à h(x) + ai + bi**2 (i = 1, 2, 3, ...) (modulo
la taille de la table)

1. Dans un système à objet tel que Java la class Object possède un méthode hash() qui est héritée par
toutes les autres classes. Chaque classe peut redéfinir cette méthode en fonction des caractéristiques de
ses objets.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.5.html (3 of 3) [25-10-2001 20:25:21]


7.5 Arbres de recherche

[Remonter] [Precedent] [ Suivant]

7.5 Arbres de recherche


On appelle arbres de recherche des arbres dont l'organisation permet de retrouver efficacement une
valeur particulière (stockée dans un noeud).

7.5.1 Représentation des ensembles


Lorsque les valeurs que l'on donne aux noeuds d'un arbre sont tirées d'un ensemble qui possède une
relation d'ordre ( £ ), l'arbre de recherche fournit une structure efficace pour représenter un ensemble de
ces valeurs. Si l'on utilise des arbres binaires la technique est la suivante:
● la racine contient l'un des éléments de l'ensemble

● les noeuds du sous-arbre gauche ne contiennent que des éléments de valeur inférieure à celle de
l'élément racine
● les noeuds du sous-arbre droit ne contiennent que des éléments de valeur supérieure à celle de
l'élément racine
Nous supposerons que si les éléments considérés sont d'un type T qui possède les opérations de
comparaison
T.egal(T) -> booléen
T.inférieur(T) -> booléen.
L'implémentation de la structure de l'arbre binaire de recherche est immédiate, il suffit de définir une
classe Noeud qui contient trois variables d'instance :
T contenu // l'élément contenu dans ce noeud, de type T
Noeud gauche, droite // références aux racines des sous-arbres de droite et de gauche.
Un ensemble sera représenté par un objet contenant une variable d'instance arbre de type Noeud. Si arbre
= nul l'ensemble est vide, sinon arbre fait référence au noeud racine de l'arbre de recherche où sont
stockés les éléments de l'ensemble.

Algorithme de recherche d'un élément (opération appartient)


La recherche dans un arbre binaire de recherche procède par dichotomie : si l'élément cherché est
inférieur à l'élément racine on cherche dans le sous-arbre de gauche, s'il est supérieur on cherche dans
celui de droite et s'il est égal on a trouvé. L'algorithme de la méthode appartient peut s'écrire comme suit:

méthode appartient(T e) retourne booléen de Noeud {


si (contenu.egal(e)) retourner vrai
sinon
si (e. inferieur (contenu))

http://cui.unige.ch/~falquet/std/notes/p7.6.html (1 of 4) [25-10-2001 20:25:38]


7.5 Arbres de recherche

si (gauche = nul) retourner faux


sinon retourner gauche.appartient(e)
sinon
si (droite = nul) retourner faux
sinon retourner droite.appartient(e)

On remarque que le temps de recherche (le nombre de comparaison d'éléments à effectuer) dépend de la
hauteur de l'arbre, au pire on atteint la feuille la plus basse de l'arbre. Si l'arbre est complet (chaque noeud
intérieur a exactement 2 enfants) et équilibré (toutes les feuilles sont au même niveau), sa hauteur est
environ égale à log 2 (nombre de noeuds + 1) - 1.

Ajout d'un nouvel élément


On procède comme pour la recherche mais au cas où l'élément n'est pas trouvé on crée un nouveau noeud
pour le stocker.

méthode ajouter (T e) de Noeud


si (e.egal(contenu)) retourner // e déjà dans l'arbre
sinon
si (e.inférieur(contenu))
si (gauche = nul)
gauche <- new Noeud(e)
sinon gauche.ajouter(e)
sinon
si (droite = nul)
droite <- new Noeud(e)
sinon droite.ajouter(e)
}

Supprimer un élément
L'opération de suppression est un peu plus problématique. En particulier si l'élément à supprimer ne se
trouve pas dans une feuille de l'arbre. Dans ce dernier cas il faut restructurer l'arbre. L'aglorithme procède
de la manière suivante.

1. Chercher le sous-arbre dont la racine contient la valeur x à supprimer


2.
1. Si ce sous-arbre est une feuille, on l'enlève de l'arbre
2. Si ce sous-arbre n'a qu'un enfant, on le remplace dans l'arbre par cet enfant

http://cui.unige.ch/~falquet/std/notes/p7.6.html (2 of 4) [25-10-2001 20:25:38]


7.5 Arbres de recherche

3. Dans le cas où le sous-arbre a deux enfants ont ne peut rien faire directement, il faut
faire descendre x dans l'arbre par une série de rotations, jusqu'à ce qu'on se ramène au
cas See Si ce sous-arbre est une feuille, on l'enlève de l'arbre ou See Si ce sous-arbre
n'a qu'un enfant, on le remplace dans l'arbre par cet enfant .Si on fait une rotation à
gauche il faut ensuite supprimer l'élément du sous-arbre gauche du nouvel arbre.

Algorithme de rotation (à gauche) d'un arbre dont la racine est le noeud r:

r1 = r.droite
r.droite = r1.gauche
r1.gauche = r

La nouvelle racine est r1. On voit qu'il suffit de trois affectations pour effecture la rotation. La méthode
de suppression est alors la suivante:

méthode supprimer(T e) de Noeud n retourne Noeud


si (contenu.inférieur(e))
droite <- droite.supprimer (e)
retourner n
sinon si (contenu.supérieur(e))
gauche <- gauche.supprimer(e)
retourner n
sinon // cas où contenu = e
si gauche = nul retourner droite
si droite = nul retourner gauche
sinon retourner n.rotation_gauche().gauche.supprimer(e)

http://cui.unige.ch/~falquet/std/notes/p7.6.html (3 of 4) [25-10-2001 20:25:38]


7.5 Arbres de recherche

7.5.2 Représentation des fonctions


On utilise le même principe que pour la représentation des ensembles mais dans chaque noeud on aura
deux variabes : clé et valeur à la place de la variable contenu . Toutes les opérations de comparaison se
feront uniquement sur la variable clé . On se rappellera que par définition on ne peut avoir deux paires
(clé, valeur ) qui ont des clés égales et des valeurs différentes.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.6.html (4 of 4) [25-10-2001 20:25:38]


7.6 Arbres équilibrés (AVL)

[Remonter] [Precedent] [ Suivant]

7.6 Arbres équilibrés (AVL)


Suivant l'ordre dans lequel les éléments sont insérées dans un arbre de recherche binaire on obtiendra
différentes formes d'arbre. Dans le pire des cas, qui consiste à insérer les éléments en ordre croissant (ou
décroissant), l'arbre obtenu sera complètement linéaire et aura par conséquent une hauteur égale ou
nombre d'éléments insérés. Dans cette situation pathologique l'arbre sera réduit à une liste du point de
vue des performances de recherche.
La technique des arbre AVL consiste à maintenir vraie la condition suivante tout au long de l'existence
de l'arbre.
Condition AVL : pour tout sous-arbre de racine r, le sous-arbre gauche de r et le sous-arbre droit de r ont
des hauteurs qui diffèrent au plus d'une unité.
Pour maintenir cet invariant lors de l'insertion d'un nouvel élément il peut être nécessaire de procéder à
une simple ou double rotation selon la méthode suivante:
insertion de x dans a

si x > racine alors


insérer x dans le sous-arbre droit de a
si hauter(s-a droit) a augmenté et hauteur(s-a droit) - hauteur(s-a gauche) > 0 alors
si hauteur(s-a droit du s-a droit) > hauteur(s-a gauche du s-a droit) alors rotation simple vers
la gauche
sinon rotation double vers la gauche (voir See Rotation double à gauche - les sous-arbres de
C remontent d'un niveau et le sous-arbre a descend d'un niveau. )
si x < racine
-- même chose mais à gauche --

http://cui.unige.ch/~falquet/std/notes/p7.7.html (1 of 2) [25-10-2001 20:25:48]


7.6 Arbres équilibrés (AVL)

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.7.html (2 of 2) [25-10-2001 20:25:48]


7.7 B-arbres

[Remonter] [Precedent] [ Suivant]

7.7 B-arbres
Les B-arbres (B pour "balanced" = équilibré) sont des arbres multiples où chaque noeud peut avoir entre
0 et m+1 enfants et contenir entre 0 et m valeurs. Afin de maintenir l'arbre complètement équilibré on va
accepter que ses noeuds soient partiellement vides, c-à-d que les m places réservées pour stocker des
valeurs ne soient pas toutes occupées.
Par définition un B-arbre d'ordre m est un arbre tel que:
● La racine a au moins 2 descendants - sauf si c'est une feuille

● Chaque noeud intérieur a au maximum m descendants et au minimum [ m /2] descendants

● Chaque noeud contient entre [ m /2] et m valeurs

● Toutes les feuilles apparaissent au même niveau

Schématiquement un noeud de B-arbre se présente comme suit:

Le sous-arbre le plus à gauche contient toutes les valeurs inférieures à v1, le 2ème sous-arbre contient
celles comprises entre v1 et v2, et ainsi de suite.

Insertion
chercher le noeud où le nouvel élément doit être inséré
insérer l'élément dans son noeud
si débordement
partager le bloc en deux blocs à moitié pleins
si (le noeud n'est pas la racine)
insérer de la même manière la valeur médiane dans le noeud de niveau supérieur où elle
servira de critère de séparation (cette insertion peut elle-même créer une séparation dans les
noeuds supérieurs)
sinon créer un nouveau noeud racine pour y mettre cette valeur médiane.

http://cui.unige.ch/~falquet/std/notes/p7.8.html (1 of 3) [25-10-2001 20:26:03]


7.7 B-arbres

Suppression
chercher le noeud s où se trouve l'élément à supprimer
enlever l'élément du noeud
si le noeud devient moins qu'à moitié plein
si (nb. élt d'un noeud adjacent t + nb élt de s < m)
fusionner s, t et l'élément du noeud supérieur e qui les séparait en un noeud u
enlever e du noeud supérieur
remplacer les deux références à s et t par une seule référence à u
si (le noeud supérieur devient moins qu'à moitié plein)
appliquer récursivement le même traitement, sauf si c'est la racine
sinon
effectuer une répartition égale des éléments entre s et un noeud adjacent t,
mettre à jour l'élément séparateur dans le noeud supérieur.

http://cui.unige.ch/~falquet/std/notes/p7.8.html (2 of 3) [25-10-2001 20:26:03]


7.7 B-arbres

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.8.html (3 of 3) [25-10-2001 20:26:03]


7.8 Arbres multidimensionnels

[Remonter] [Precedent] [ Suivant]

7.8 Arbres multidimensionnels


Les éléments que l'on désire stocker dans un arbres peuvent être eux-mêmes composées de plusieurs
sous-éléments (ou composantes) sur lesquels ont veut pouvoir faire des recherches. Par exemple, si l'on
stocke des points de l'espace composés de 3 coordonnées on aimertait avoir une structure qui permette de
● rechercher un point particulier étant donné ses 3 coordonnées x, y et z (recherche exacte)

● rechercher tous les points qui ont une valeur donnée pour la coordonnée x (ou y ou z) (recherche
partielle)
Il est bien entendu possible de définir un ordre sur les éléments composés. Par exemple l'ordre
lexicographique sur des coordonnées 3-D est donné par
<x, y, z> < <x', y', z'> si (x < x') ou (x = x' et y < y') ou (x = x' et y = y' et z < z')
mais si cet ordre permet de construire un arbre (binaire ou autre) pour stocker les éléments de cet
ensemble, cet arbre n'est d'aucune utilité pour les recherches partielles. Si, par exemple, on veut trouver
tous les points dont la coordonnée z vaut 12, il n'y a pas d'autre solution que d'explorer tout l'arbre. Seule
la recherche sur la première coordonnée est efficace.

7.8.1 Arbres k-d


Un arbre k-d ("k-dimensional tree") est un arbre de recherche binaire où à chaque niveau on change de
dimension pour déterminer ce qui va dans le sous-arbre gauche et dans le sous-arbre droit. Par exemple,
dans un arbre 2-d les niveaux 1-3-5-7-... discriminent les valeurs de la première composante et les
niveaux 2-4-6-8-... discriminent celles de la deuxième composante.

Une recherche exacte dans un arbre k-d s'effectue comme dans un arbre binaire normal, mais en
comporant alternativement sur chacune des composantes.
Pour effectuer une recherche partielle, dans un arbre 2-d, on doit alternativment choisir le bon sous-arbre
ou explorer les deux sous-arbres du niveau courant.

http://cui.unige.ch/~falquet/std/notes/p7.9.html (1 of 2) [25-10-2001 20:26:14]


7.8 Arbres multidimensionnels

Algorithme
Recherche des noeuds qui ont la valeur v dans la i -ème composante
soit j le niveau de la racine du sous-arbre qu'on explore
si ( j mod k + 1 = i )
si ( v < la i -ème composante de la racine)
recherche dans le s-a gauche
sinon
si ( v > la i -ème composante de la racine)
recherche dans le s-a droit
sinon (* trouvé *) ajoute ce noeud à la liste des noeuds trouvés
sinon recherche dans le s-a gauche puis recherche dans le s-a droit

On remarque que plus k devient grand plus on explore une grande partie de l'arbre.

7.8.2 Quad-tree
Dans un quad-tree de dimension k, chaque noeud possède 2 k descendants. Chacun des descendants
correspond à un résultat de comparaison possible des composantes de l'élément à placer et des
composantes du noeud. Pour un quad-tree d'ordre 3 on aura:

Dans ce cas la recherche sur une composante, disons Y, nécessite l'exploration de 4 des 8 sous-arbres du
niveau inférieur.
Chaque noeud d'un quad-tree de dimension k correspond à une division de l'espace en 2 k sous-espaces.
Chacun de ses sous-espaces étant lui-même re-subdivisé au niveau suivant de l'arbre.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.9.html (2 of 2) [25-10-2001 20:26:14]


7.9 Structures de données pour "recherche-union"

[Remonter] [Precedent] [ Suivant]

7.9 Structures de données pour


"recherche-union"
Certains algorithmes traitent des données qui se présentent sous forme d'ensembles disjoints d'objets.

7.9.1 Exemples
Exemple 1. Déterminer les classes d'équivalence correspondant à une relation d'équivalence R donnée.

Rappel
R est une relation d'équivalence si
1. aRa pour tout a (réflexivité)
2. aRb => bRa pour tout a, b (symétrie)
3. aRb et bRc => aRc pour tout a, b, c (transitivité)
La classe d'équivalence d'un élément a, notée [a] est l'ensemble de tous les éléments x tels que aRx. Il est
facile de voir que si x est dans [a] alors [x] = [a] et que si x n'est pas dans [a] alors [a] et [x] sont
disjointes. Les classes d'équivalences sont donc des ensembles disjoints.

Algorithme.
On peut construire itérativement l'ensemble des classes d'équivalence d'une relation R sur l'ensemble S =
{s 1 , s 2 , ..., s n } de la manière suivante:
1. On commence par créer les ensembles C 1 = {s 1 }, C 2 = {s 2 }, ..., C n = {s n }.
2. On considère successivement toutes les paires (a, b) telles que aRb, deux situations peuvent se
présenter:
a) a et b se trouvent déjà dans la même classe C i , il n'y a rien à faire

b) a et b se trouvent dans des classes C i et C j différentes, le fait que aRb indique que a et b sont dans la
même classe d'équivalence, il faut donc faire l'union de C i et C j pour produire une nouvelle classe
d'équivalence C k qui remplace C i et C j .

Exemple 2. Détection de cycles dans la construction d'un arbre.


Dans un algorithme comme celui de Kruskal on construit un arbre en choisissant successivement des
arêtes dans un graphe. Si un arête choisie crée un cycle on la rejette sinon on l'inclut dans l'arbre.
Pour détecter si une arête {u, v} crée un cycle dans l'arbre A on effectue le test suivant:
● si u et v appartiennent à la même composante connexe C de A alors il y a création d'un cycle

http://cui.unige.ch/~falquet/std/notes/p7.a.html (1 of 4) [25-10-2001 20:26:28]


7.9 Structures de données pour "recherche-union"

● si u et v appartiennent à deux c.c. différentes C1 et C2, il n'y a pas de création de cycle, par contre
il faut fusionner C1 et C2 en une nouvelle c.c. C3
● si u appartient à une c.c. C1 et v n'appartient à aucune c.c., il ajouter v à C1, idem si c'est u qui
n'appartient à aucune c.c. et v appartient à C1
● si ni u ni v n'appartiennent à une c.c. il faut créer une nouvelle c.c. C = {u, v}

Caractéristique des algorithmes Union-Recherche

Les algortihmes ci-dessus présente les caractéristiques suivantes:


● on traite une collection d'ensembles

● ces ensembles sont tous disjoints deux à deux

● on doit pouvoir déterminer dans quel ensemble se trouve un objet (opération RECHERCHE)

● il faut faire l'union de deux ensembles disjoints (opération UNION)

7.9.2 Structures de données et algorithmes


Etant donné les caractéristiques des données à manipuler et des opérations à effectuer, on peut chercher
quels sont les meilleures structures de données et algorithmes pour réaliser ce type d'opérations.

1. Approche simple et directe


Dans la structure de chaque objet on met une variable d'instance 'ensemble' qui donne le numéro de
l'ensemble auquel appartient l'objet. Et on place tous les objets dans, disons, un tableau T.

Opération RECHERCHE

La réalisation de l'opération est triviale, il suffit de faire T[s].ensemble

Opération UNION

Pour faire l'union des ensembles i et j on parcourt tout le tableau T et pour chaque s tel que T[s] = j on
fait T[s] ¨ i.
Le temps d'exécution d'une opération UNION est donc proportionnel au nombre total d'objets.

2. Accélération de l'union
On peut représenter chaque ensemble par une liste liée dont les noeuds contiennent les objets. L'union
devient triviale, il suffit de connecter le dernier élément de C i au premier de C j .

Par contre la RECHERCHE se complique. Si l'on garde dans le dernier objet de chaque liste une
référence à l'ensemble qui correspond à cette liste, il faut parcourir en moyenne la moitié de la liste pour
atteindre ce dernier élément et savoir dans quel ensmble on se trouve. On pourrait bien sûr garder dans
avec chaque objet une référence à son ensmble, mais alors il faudrait mettre à jour cette référence à
chaque union et l'on perdrait le bénéfice de la méthode d'union rapide.

http://cui.unige.ch/~falquet/std/notes/p7.a.html (2 of 4) [25-10-2001 20:26:28]


7.9 Structures de données pour "recherche-union"

3. Des listes aux arbres


Pour accélérer la recherche on représente chaque ensemble par un arbre. A chaque objet on associe son
parent dans l'arbre. Si le parent est nul nous sommes à la racine de l'arbre.
Si l'on suppose que les objets dont on s'occupe sont numérotés de 0 à N-1 (p.ex. si les objets sont placés
dans une liste ou dans un tableau), on peut utiliser un tableau P (pour parent) de taille N pour représenter
tous ces arbres. Une élément P[i] contient le numéron du parent de l'objet numéro i. Si l'élément est à la
racine de son arbre on mettra P[i] à -1 (par exemple). Un ensemble est donc repéré par l'indice de son
élément racine.

Exemple

Les arbres de la figure suivante représentent les ensembles {0,, 1, 3, 5, 2, 6}, {4, 7, 8}, {12, 10, 9} et
{11}.

On les représentera dans un tableau P dont le contenu sera le suivant:


indice 0 1 2 3 4 5 6 7 8 9 10 11 12
contenu: 1 -1 5 1 8 1 5 8 -1 10 12 -1 -1

UNION

Pour unir les ensembles i et j, c-à-d les ensembles dont les racines sont aux positions i et j de P, on choisit
au hasard entre i et j, disons j et on "accroche" l'arbre sous P[j] à P[i] en faisant simplement P[j] ¨ i.

RECHERCHE

Pour trouver l'ensemble auquel appartient l'objet k on rechrche la racine de l'arbre auquel appartient k. Le
temps de recherche est proportionnel à la profondeur de l'objet dans son arbre.

Amélioration 1: union par taille

Le problème qui subsiste est que les arbres ainsi produits peuvent être très dégénérés et se comporter
comme des listes. Ce qui rend le temps de rechrche linéaire en fonction du nombre d'objets. Pour
améliorer la situation il suffit, lorsqu'on fait une union d'accrocher toujours le plus petit arbre à la racine
du plus grand.

http://cui.unige.ch/~falquet/std/notes/p7.a.html (3 of 4) [25-10-2001 20:26:28]


7.9 Structures de données pour "recherche-union"

Ainsi, si x appartient à un arbre de taille T, à chaque fois que son arbre devient sous-arbre d'un autre, et
donc que x s'éloigne d'un cran de la racine, l'ensemble auquel appartient x double au moins de taille. Un
ensemble ne peut doubler qu'au maximum log 2 (N) fois. Donc x se trouve au pire à une distance log 2
(N) de la racine.
Pour implémenter cette méthode il est nécessaire de connaître à tout moment la taille de chaque
ensemble, sans devoir la calculer. L'astuce consiste à garder l'inverse de la taille d'un arbre de racine i
dans P[i].

Amélioration 2: compression des chemins

On peut réduire progressivement la longueur du chemin à parcourir depuis un objet x jusqu'à la racine en
procédant de la manière suivante. Chaque fois qu'on parcourt le chemin de x jusqu'à la racine, on en
profite pour attacher les noeuds rencontrés directement à la racine.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p7.a.html (4 of 4) [25-10-2001 20:26:28]


8 Algorithmique

[Remonter] [Precedent] [ Suivant]

8 Algorithmique
Bien qu'il n'existe pas d'algorithme pour fabriquer un algorithme pour résoudre un problème donné, il
existe des techniques de conception des algorithmes. Parmi celles-ci on peut citer:
● diviser et conquérir

● la programmation dynamique

● les algorithmes récursifs et le retour arrière (backtracking)

● les algorithmes gloutons

● les algorithmes probabilistes

8.1 Diviser et conquérir

8.2 SOUS-SUITES MAXIMALES

8.3 Programmation dynamique

8.4 Retour arrière (backtracking)

8.5 Algorithmes gloutons

8.6 Algorithmes probabilistes

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.1.html [25-10-2001 20:26:34]


8.1 Diviser et conquérir

[Remonter] [Precedent] [ Suivant]

8.1 Diviser et conquérir


Le principe général consiste à diviser un problème en sous-problèmes que l'on peut résoudre séparément
puis à combiner les solutions partielles pour obtenir une solution globale. Nous illustrerons ce principe
sur deux exemples.

8.1.1 Problème du MINMAX.


Il s'agit de trouver le plus petit et le plus grand élément d'un ensemble S de taille n = 2 k .
L'algorithme itératif classique peut s'écrire comme suit:
MAX <- un élément quelconque y de S
MIN <- y
pour chaque élément x de S - {y} {
si (x > MAX) MAX <- x
si (x < MIN) MIN <- x
}
L'opération dominante, qui va déterminer le temps d'exécution, est la comparaison. Pour un ensemble de
taille n on effectue 2(n-1) comparaisons pour trouver le min et le max.
Prenon maintenant une approche "diviser et conquérir"
L'idée consiste à
diviser S en deux ensembles S1 et S2 de même taille, à chercher le min et le max dans S1 et S2, puis à
combiner les résultats en prenant le min des min et le max des max.
On peut écrire l'algorithme suivant:
opération MAXMIN(ensembleT S) retourne (T, T)
-- retourne le min et le max de S sous forme d'une paire
si (card(S)) = 2 et S = {x1, x2}
si (x1 < x2) retourne (x1, x2) sinon retourne (x2, x1)
sinon {
diviser S en S1 et S2 de même taille
(a1, b1) <- MAXMIN(S1)
(a2, b2) <- MAXMIN(S2)
retourne (min(a1, a2), max(b1, b2))
}
Le nombre de comparaison en fonction de la taille n de S est donné par

http://cui.unige.ch/~falquet/std/notes/p8.2.html (1 of 2) [25-10-2001 20:26:44]


8.1 Diviser et conquérir

T(n) = 1 si n = 2
= 2T(n/2) + 2 si n > 2
Cette équation a pour solution la fonction T: n -> 3n/2 - 2
On voit donc que cette algorithme est 25% meilleur que le précédent.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.2.html (2 of 2) [25-10-2001 20:26:44]


8.2 SOUS-SUITES MAXIMALES

[Remonter] [Precedent] [ Suivant]

8.2 SOUS-SUITES MAXIMALES


Etant donné une suite de nombres positifs nuls ou négatifs S = <s 1 , s 2 , ..., s n >, trouver i et j (1 £ i £ j
£ n) tels que S t=i..j (s t ) soit la plus grande possible.

Un algorithme exhaustif doit essayer toutes les valeurs possibles de i et j, ce qui donne n + (n-1) + (n-2)
+ ... + 2 + 1 = (n (n+1))/2 sommes. Chaque somme peut se calculer en une opération à partir de la somme
précédente, on doit donc effectuer (n (n+1))/2 additions et comparaisons.¨
Si l'on divise S en deux parties S1 = <s 1 , ..., s n/2 > et S2 = <s n/2+1 , ..., s n > on a trois cas possibles

soit la sous-suite max. se trouve dans S1; soit elle se trouve dans S2; soit elle commence dans S1 et finit
dans S2. Notons bien que dans ce dernier cas les éléments s n/2 et s n/2+1 appartiennent à la sous-suite.
L'algorithme s'écrit alors
entier MAXSOM(a, b)
si a = b
retourner s[a]
sinon {
d = (a+b)/2
max1 = MAXSOM(a, d)
max2 = MAXSOM(d+1, b)
-- calculer la plus grande somme qui commence en S1 et
-- finit en S2
-- 1. la plus grande somme qui commence en S1 et finit à d
m1 <- s[d]; somme <- s[d];
pour i de d-1 à a {
somme <- somme + s[i]
si (somme > m1) m1 <- t
-- 2. la plus grande somme qui commence en d+1 et finit dans S2
m2 <- s[d+1]; somme <- s[d+1];
pour i de d+1 à b {
somme <- somme + s[i]
si (somme > m2) m2 <- t
retourner max(max1, max2, m1+m2)
Le nombre de sommes et comparaisons est
T(n) = 1 si n = 1

http://cui.unige.ch/~falquet/std/notes/p8.3.html (1 of 3) [25-10-2001 20:26:53]


8.2 SOUS-SUITES MAXIMALES

= 2T(n/2) + 2n si n>1
Ce qui donne une complexité en O(n log n) (voir ci-après)

8.2.1 Complexité des algorithmes diviser-conquérir


En général les algorithmes d-c s'écrivent comme
algo(P)
1. si P est trivial retourner la réponse
sinon
2. diviser P en a sous problèmes P 1 , ..., P a de taille n/c
3. appeler algo(P i ) pour i = 1, a
4. calculer les cas qui recouvrent plusieurs sous-problèmes
5. combiner les résultats
En analysant ces algorithmes on tombe fréquemment sur un complexité qui s'écrit comme
T(n) = b si n = 1
= aT(n/c) + bn si n > 1
où a, b et c sont des constantes et n = c k
On peut montrer qu'alors
T(n) = O(n) si a < c
= O(n log n) si a = c
= O(n log c a ) si a > c

Preuve:
T(c k ) = aT(c k-1 ) +bc k = a(aT(c k-2 ) +bc k-1 )+bc k = ...
= a k b + a k-1 bc + a k-2 bc 2 + ... + abc k-1 + bc k
= bc k (a k /c k + a k-1 /c k-1 + ... + a/c + 1/1)
si a < c, la somme des a k /c k tend vers une limite constante r, donc T(n = c k ) £ bc k r
si a = c, la somme est égale à k+1 = log c n + 1, donc T(n) est de l'ordre de n log n

si a > c, par la formule


somme pour i = 0, k des u i = (u k+1 - 1)/(u - 1),
on trouve que la somme des a k /c k vaut ((a/c) k+1 - 1)/((a/c) - 1), ce qui est de l'ordre de a log c n =n log c a

http://cui.unige.ch/~falquet/std/notes/p8.3.html (2 of 3) [25-10-2001 20:26:53]


8.2 SOUS-SUITES MAXIMALES

8.2.2 Equilibrage des partitions


Tri de n nombres
Le tri par recherche du plus petit élément consiste à
● trouver le plus petit élément du tableau

● échanger le 1er avec le plus petit

● trier les n-1 éléments restants

C'est un peu comme si on divisait le tableau en une partie de taille 1 et une autre de taille n-1. On sait que
la complexité de ce tri est quadratique.
Le tri-fusion est une méthode par division équilibrée
● trier les éléments de 0 à n/2-1

● trier les éléments de n/2 à n

● fusionner les deux parties triées dans un nouveau tableau

Temps d'exécution:
T(n) = 0 si n = 1
= 2T(n/2) + n si n>1
d'après les formules ci-dessus: T(n) est O(n log n)

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.3.html (3 of 3) [25-10-2001 20:26:53]


8.3 Programmation dynamique

[Remonter] [Precedent] [ Suivant]

8.3 Programmation dynamique


Dans certains cas les algorithmes récursifs descendants sont inneficaces car ils recalculent plusieurs fois
la même chose.

8.3.1 Exemple.
Etant donné une monnaie qui possède des pièces de valeur v 1 , v 2 , ..., v k et une somme s, quel est le
nombre minimum de pièces pour obtenir cette somme ?
Un algorithme récursif descendant peut s'écrire comme
NMP(s) =
si s = 0 retourne 0
sinon retourne 1 + min{ NMP(s - v i ) | 1 £ i £ k et s - v i 0 }

Regardons ce qui se passe avec


s = 15 et v 1 = 1, v 2 = 2, v 3 = 7, v 4 = 10
NMP(15)
v 1 --> NMP(14)
v 1 --> NMP(13) ...
v 2 --> NMP(12) ...
v 3 --> NMP(7) ...
v 4 --> NMP(4) ...
v 2 --> NMP(13) -- déjà calculé dans NMP(14)
v 3 --> NMP(8)
v 1 --> NMP(7) -- déjà calculé...
v 2 --> NMP(6)
etc.
La complexité de cet algorithme est de l'ordre de k s/m (on explore un arbre dont chaque noeud a environ
k sous-arbres et de hauteur s/m)

8.3.2 Principe
L'idée consiste à stocker les résultats partiels déjà obtenus, de façon à éviter tout recalcul.
En général on procède de manière "bottom-up", c'est-à-dire qu'on commence par calculer les résultats

http://cui.unige.ch/~falquet/std/notes/p8.4.html (1 of 3) [25-10-2001 20:27:10]


8.3 Programmation dynamique

pour les cas les plus simples, puis on s'appuye sur ces résultats pour calculer des cas plus complexes, et
ainsi de suite.
Dans l'exemple précédent on aura:
NMP(0) = 0 -- cas trivial
NMP(1) = 1 + NMP(0) = 1
NMP(2) = 1 + min{NMP(1), NMP(0)} = 1
NMP(3) = 1 + min{NMP(2), NMP(1)} = 2
NMP(4) = 1 + min{NMP(3), NMP(2)} = 2
NMP(5) = 1 + min{NMP(4), NMP(3)} = 3
NMP(6) = 1 + min{NMP(5), NMP(4)} = 3
NMP(7) = 1 + min{NMP(6), NMP(5), NMP(0)} = 1
NMP(8) = 1 + min{NMP(7), NMP(6), NMP(1)} = 2
NMP(9) = 1 + min{NMP(8), NMP(7), NMP(2)} = 2
NMP(10) = 1 + min{NMP(9), NMP(8), NMP(3), NMP(0)} = 1
etc.
jusqu'à la valeur s
La complexité en temps est O(sk)

8.3.3 Exemple: multiplication de matrices


La multiplication de matrices étant associative, il y a plusieurs manière d'évaluer le produit de n matrices
M 1 , M 2 , ..., M n (pour que le produit soit possible il faut que les matrices soient de dimension r 0 ¥ r 1 ,
r 1 ¥ r 2 , ..., r n-1 ¥ r n .)

Par exemple, pour n = 4 on peut faire


M 1 * (M 2 * (M 3 * M 4 )) ou (M 1 * M 2 ) * (M 3 * M 4 ) ou ((M 1 * M 2 ) * M 3 ) * M 4 etc.

Le coût des multiplications n'est pas le même suivant l'ordre d'évaluation. Il s'agit donc de choisir le
meilleur ordre possible. Mais le nombre de possibilités est exponentiel en fonction de n. Il faut donc
éviter d'évaluer le coût de chaque ordre.
Lorsqu'on multiplie une matric p ¥ q par une matric q ¥ r on effectue O(pqr) opérations.
On peut établir la relation de récurrence:
soit m ij le coût de la multiplication (M i *...* M j )

m ik = 0 si i = j

= min k {m ik + m k+1j + r i-1 r k r j } sinon

L'algorithme dynamique consiste à calculer

http://cui.unige.ch/~falquet/std/notes/p8.4.html (2 of 3) [25-10-2001 20:27:10]


8.3 Programmation dynamique

m 1,1 = 0, m 2,2 = 0, m 3,3 = 0, ...

m 1,2 = f(m 1,1 , m 2,2 ), m 2,3 = f(m 2,2 , m 3,3 ), ...

m 1,3 = f(m 1,2 , m 3,3, m 2,3 , m 1,1 ), etc.

La complexité est O(n 3 )


[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.4.html (3 of 3) [25-10-2001 20:27:10]


8.4 Retour arrière (backtracking)

[Remonter] [Precedent] [ Suivant]

8.4 Retour arrière (backtracking)


Un algorithme avec retour arrière cherche à construire une solution itérativement, lorsqu'il arrive dans
une impasse il défait ce qui vient d'être fait et cherche dans une autre direction.
Ceci évite de construire complètement toutes les solutions potentielles et de les tester toutes.
Dans une approche orientée-objet on définira une classe Solution dont un objet représente une solution
en cours de construction. Cette classe sera munie d'une méthode récursive essayer() qui explore toutes les
solutions constructibles à partir de la solution partielle actuelle. Cette méthode se termine (revient en
arrière) quand elle à exploré toutes les voies qui peuvent conduire à une solution correcte.
classe Solution
// variable mémorisant la solution (partielle) actuelle
Description_solution s
méthode essayer()
si (s est une solution complète) { afficher s }
sinon {
pour toute solution partielle s' constructible à partir de s {
si (s' peut éventuellement mener à
une solution correcte) {
memoS ¨ s; s ¨ s'; essayer();
s ¨ memoS
}
}
}

8.4.1 Exemple. Le problème des huit reines


On dispose d'un échiquier de n ¥ n cases sur lequel on veut disposer n reines sans qu'elles se menacent
mutuellement. Pour cela on doit satisfaire les conditions suivantes:
● deux reines ne peuvent se trouver sur la même ligne

● deux reines ne peuvent se trouver sur la même colonne

● deux reines ne peuvent se trouver sur la même diagonale

On construit itérativement une solution en essayant de placer une reine dans la première colonne, puis
dans la deuxième, et ainsi de suite. Si, arrivé à la colonne k il n'y a plus de possibilité de placer une reine,

http://cui.unige.ch/~falquet/std/notes/p8.5.html (1 of 3) [25-10-2001 20:27:31]


8.4 Retour arrière (backtracking)

on revient à la colonne k-1 et on essaye de déplacer la reine qui s'y trouve. Si on peut le faire, on repart
en avant, à la colonne k, sinon on revient à la colonne k-2, et ainsi de suite.
Exemple sur un échiquier 4 ¥ 4:

Algortihme
méthode SolutionNReines
// mémorisation de la solution
int colonne[N] // ligne de chaque reine dans chaque colonne
// uniquement pour accélérer les tests
booléen rangee[N], diagonale1[2*N-1] , diagonale2[2*N-1]

essayer_colonne(j)
i¨ N
tant que (i > 0) {
si (case_non_menacée(i, j)) {
placer_reine(i, j)
si (j>1) essayer_colonne(j-1)
sinon afficher_échiquier
enlever_reine(i, j)
}

http://cui.unige.ch/~falquet/std/notes/p8.5.html (2 of 3) [25-10-2001 20:27:31]


8.4 Retour arrière (backtracking)

}
placer_reine(i, j)
colonne[j] ¨ i; rangée[i] ¨ vrai;
diagonale1[i+j] ¨ vrai; diagonale2[N+i-j] ¨ vrai
}
case_non_menacée(i, j)
retourne non rangee[i] et non diagonale1[i+j]
et non diagonale2[N+i-j]
[]

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.5.html (3 of 3) [25-10-2001 20:27:31]


8.5 Algorithmes gloutons

[Remonter] [Precedent] [ Suivant]

8.5 Algorithmes gloutons


Un algorithme glouton construit une solution à un problème en faisant à chaque étape le choix le plus
"facile".

8.5.1 Exemple 1.
Dans le problème de l'obtention d'une somme à l'aide du minimum de pièces de monnaie on pourrait
définir un algorithme glouton de la forme:
S : la somme à atteindre
A¨0
NP ¨ 0
tant que (A < S) {
choisir la pièce v i de plus grand valeur telle que A+v i £ S
A ¨ A + v i ; NP ¨ NP + 1
}
On peut assez facilement se convaincre que cet algorithme donne bien le nombre minimum de pièces
pour toute somme S dans le cas où v 1 = 1, v 2 = 2, v 3 = 5 et v 4 = 10.

Par contre il ne trouve pas la meilleure solution si v 1 = 1, v 2 = 4, v 3 = 6 et S = 8. En effet, l'algorithme


va choisir une pièce de 6, puis 1, puis 1, donc 3 pièces. Alors que la meilleure solution est 4 + 4.

8.5.2 Exemple 2.
L'algorithme de Kruskal pour la recherche de l'arbre de recouvrement à coût minimal est un algorithme
glouton.
On peut prouver qu'il donne effectivement le meilleur arbre.

8.5.3 Conception des algorithmes gloutons


Les algorithmes gloutons sont en général faciles à inventer, car ils correspondent à l'intuition.
Mais l'intuition peut se révéler fausse, il faut donc démontrer formellement que l'algorithme est correct.
Si l'on ne recherche pas la meilleure solution mais une solution "acceptable" on peut employer un
algorithme glouton non optimal.
Un algorithme glouton peut également servir à produire une première solution, non optimale, que l'on

http://cui.unige.ch/~falquet/std/notes/p8.6.html (1 of 2) [25-10-2001 20:27:35]


8.5 Algorithmes gloutons

peut ensuite améliorer par des techniques telles que TABOU ou le "recuit simulé".

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.6.html (2 of 2) [25-10-2001 20:27:35]


8.6 Algorithmes probabilistes

[Remonter] [Precedent] [ Suivant]

8.6 Algorithmes probabilistes


Contrairement aux autres algorithmes, les algorithmes probabilistes donnent une réponse avec une
probabilité p < 1 qu'elle soit correcte. Il ne s'agit donc pas d'algorithmes au sens strictement
Church-Turingiens du terme.

8.6.1 Exemple: tester si un nombres est premier


Nous avons besoins de deux résultats de théorie de nombres avant d'écrire notre algorithme.
1. Le petit théorème de fermat nous dit que si P est premier, pour tout nombre A non nul inférieur à P
A P-1 1 (mod P)
La preuve:
Si k < P, kA ne peut pas être 0 mod P, sinon kA serait égal à nP, ce qui est impossible si k et A sont
inférieurs à P et P est premier.
Par conséquent, si kA qA mod P, (k-q)A 0 mod P, ce qui n'est possible que si k q mod P.
Donc A, 2A, 3A, ..., (P-1)A parcourt tous les entiers 1, 2, ..., P-1.
Donc A 2A ... (P-1)A = A P-1 (P-1)! = (P-1)! donc A P-1 1 mod P

2. Si X 2 1 mod P alors X +/- 1 mod P

X 2 = 1 => X 2 - 1 = 0 => (X-1)(X+1) = 0 mod P, comme P est premier X = +/- 1

Témoins de non primalité


Etant donné un nombre N dont on ne sait pas s'il est premier ou non, si on a un nombre A tel que
A^{N-1} =/= 1 mod N
alors on est sur que N n'est pas premier, sinon on contredirait le petit thm de Fermat. Un tel nombre est
appelé 'témoin' de la non primalité de N.
Par contre, la réciproque n'est pas vraie, si A N-1 1 mod N on n'est pas sûr que N est premier. En effet, si
N est composite, les nombres entre 2 et N-1 ne sont pas tous des témoins.
Pour certains nombres il est difficile de trouver des témoins
On peut minimiser Amélioration de l'algorithme à l'aide du deuxième théorème.

http://cui.unige.ch/~falquet/std/notes/p8.7.html (1 of 3) [25-10-2001 20:27:43]


8.6 Algorithmes probabilistes

Pour calculer A^K on utilise la formule de récurence


A0=1
A K = (A K/2 ) 2 si K est pair
A K = A(A K/2 ) 2 si K est impair
au cours du calcul on est amené à évaluer des carrés. Si lors d'un calcul de X 2 mod N on a X 2 mod N 1
et X n'est pas +1 ou -1 alors on peut déjà dire que N n'est pas premier.
On peut montrer que si N est composite, on a au plus 1 chance sur 4 que le calcul retourne 1 alors que N
n'est pas premier. Donc l'algorithme
premier (N)
tirer A au hasard
calculer U = A^{N-1} mod N
retourner faux si le test du théorème 2 échoue au cours du calcul de U
si U 1 mod N retourner vrai
sinon retourner faux
se trompe 1 fois sur 4 en déclarant premier un nombre qui ne l'est pas. Si l'on applique deux fois
l'algorithme sur N et que les deux réponses sont 'vrai' on a 1/16 que le nombre ne soit pas premier. En
appliquant l'algorithme 10 fois on a 1 chance sur 1 048 576 de se tromper.

Algorithme:
entier témoin(a, i, n)
// calcule récursivement a^i mod n
// retourne 0 si le test du thm-2 échoue pendant le calcul
si i = 0 retourner 1
sinon
x <- témoin(a, i/2, n)
si x = 0 retourne 0 // n est composite
y = x^2 mod n
si (y = 1 et x =/= 1 et x =/= n-1) retourne 0 // échec thm-2
si i est impair (i mod 2 = 1)
retourne (a * y) mod n
sinon
retourne y
[]
booleen premier(n)
pour i de 1 à NB_ESSAIS

http://cui.unige.ch/~falquet/std/notes/p8.7.html (2 of 3) [25-10-2001 20:27:43]


8.6 Algorithmes probabilistes

r = nb. aléatoire
si témoin(r, n-1, n) != 1 retourne faux
[]
retourne vrai // avec une probabilité d'erreur de 1/4^NB_ESSAIS
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p8.7.html (3 of 3) [25-10-2001 20:27:43]


9 Données persistantes

[Remonter] [Precedent] [ Suivant]

9 Données persistantes
On peut classer les données en deux catégories en fonction de leur durée de vie. Les données volatiles ou
temporaires n'existent que pendant l'exécution d'un programme particulier. Ce sont des données qui
représentent l'état d'un processus, des résultats partiels de calculs, ou d'autres données mais sous une
forme différente. Les données persistantes existent indépendemment de l'exécution des programmes.
Elles représentent typiquement des informations du monde réel sur lesquelles le système d'information
doit travailler. On demande aux données persistantes plusieurs qualités :
● qu'elles survivent à l'exécution des programmes et restent disponibles pendant un temps
arbitrairement long ;
● qu'elles soient utilisables par plusieurs programmes (éventuellement en parallèle);

● qu'elles soient physiquement indépendantes des programmes

Contraintes matérielles

La distinction entre données persitantes et volatiles ne trouve pas son origine dans des motifs d'ordre
conceptuels mais dans des contraintes matérielles. En effet, pour obtenir des mémoires d'ordinateurs
rapides et pas trop chères, il faut utiliser des technologies qui nécessitent une alimentation électrique
permanente. Ces systèmes (mémoires RAM) ne peuvent donc assurer de manière fiable le stockage
permanent de l'information. Ils sont par contre nécessaire pour stocker les programmes et données en
cours de traitement si l'on veut obtenir de bonnes performances. Par conséquent, les objets manipulés par
les programmes résident en général en mémoire centrale (RAM) et disparaissent à la fin de l'exécution du
programme.
La garantie de la persistance requière donc l'utilisation d'une mémoire "externe" (p.ex. disque
magnétique) qui possède d'autres caractéristiques que la mémoire centrale. Il s'agira donc de
● gérer les transferts entre types de mémoires ;

● gérer le stockage externe (structure, repérage, nommage) en fonction de ses caractéristiques


propres.

Caractérisitques des supports physiques

Coûts

En l'état actuel de la technologie (1999) la persistence des données est principalement assurée par des
supports de type disque magnétique, essentiellement pour des questions de coût.
La mémoire centrale électronique rapide (RAM) est chère (env. SFr. 5.- / megabyte) donc inutilisable
pour de grandes quantités des données persistantes. De plus elle nécessite une alimentation électrique
permanente.
Les mémoire magnétiques (disques) sont meilleure marché: (env. SFr. 0.20 / megabyte) et plus
compactes.

http://cui.unige.ch/~falquet/std/notes/p9.1.html (1 of 3) [25-10-2001 20:27:52]


9 Données persistantes

Organisation par blocs

Sur les disques l'information est stockée sous forme de blocs d'octets de taille fixe (p.ex. 512 ou 1024
octets). La plus petite unité de lecture/écriture est le bloc. Il n'est pas possible de modifier directement un
octet particulier sur un disque. Pour y arriver il faudrait : lire tout le bloc en mémoire centrale ; modifier
l'octet en mémoire centrale ; récrire le bloc sur disque.

Temps d'accès

Le temps d'accès à un bloc (lecture ou écriture) étant très long (env. 10ms) par rapport à l'accès à une
cellule de la mémoire centrale (env. 80ns, donc 80 m s pour 1000 octets), il est impératif que les
traitements minimisent le nombre d'accès au disque. Ceci implique de
● regrouper toutes les opérations portant sur les octets d'un bloc pendant qu'il est en mémoire RAM.

● grouper par dans le même bloc les données qui sont en général manipulées ensemble.

Abstaction des contraintes physiques

Du point de vue du développement du logiciel on voit bien qu'il n'est pas simple de développer des
applications qui doivent travailler sur des données persistantes. Les différentes contraintes de temps
d'accès, d'organisation par blocs, de transfert entre types de mémoire vont considérablement compliquer
la tâche du concepteur et du développeur d'applications.
Comme d'habitude, la bonne idée consiste
1) à isoler et encapsuler dans des modules appropriés tout ce qui concerne la gestion de l'accès aux
supports physiques persistants ;
2) à définir des concepts abstraits sur lesquels pourra s'appuyer le développement d'applications à
données persistantes.
Parmi les abstractions qui ont eu le plus de succès pour la représentation des données persistantes on peut
citer :
les fichiers : des séquences d'octets persistants
les bases de données relationnelles : des tables persistantes, un élément d'une table est un n-tuple de
valeurs simples (nombres, chaînes de caractères, séquences d'octets, ...)
les bases de données "réseau" : des ensembles d'enregistrements de même type et des ensembles de liens
entre enregistrements ;
les bases de données hiérarchiques : des ensembles d'enregistrements et des liens hiérarchiques entre
enregistrements ;
objets persistants : les mêmes objets que ceux du programme mais persistants

http://cui.unige.ch/~falquet/std/notes/p9.1.html (2 of 3) [25-10-2001 20:27:52]


9 Données persistantes

9.1 Fichiers

9.2 Implémentation des fichiers

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p9.1.html (3 of 3) [25-10-2001 20:27:52]


9.1 Fichiers

[Remonter] [Precedent] [ Suivant]

9.1 Fichiers
Buts
Abstraction :
s'abstraire de la structure de blocs, voir les données sous forme d'objets de plus haut niveau.
Repérage :
nommer les données persistantes pour pouvoir les retrouver et y accéder à partir de n'importe quel
programme.

9.1.1 Type abstrait fichier


D'un point de vue abstrait un fichier est une séquences d'octets persistante. Un fichier est repéré par un
nom, lui-même enregistré dans un répertoire.
Un fichier possède les opérations primitives suivantes
● créer : nom Æ fichier

● supprimer : fichier

● repérer : nom Æ fichier

fournit le fichier qui porte le nom donné


● longueur : fichier Æ entier, data-modif, etc.

● lire: fichier, position, nb-octets Æ séquence d'octets

● écrire: fichier, séquence d'octets, position Æ fichier

En général il existe dans les langages de programmation un type Fichier . Un objet de ce type, appelé
fichier logique , sert à représenter un fichier du disque, appelé fichier physique .
Les opérations effectuées sur le fichier logique sont reportées sur le fichier physique (lecture, écriture).
Par contre la création ou la disparition d'un fichier logique n'entraine en général pas celle du fichier
physique correspondant.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p9.2.html [25-10-2001 20:27:55]


9.2 Implémentation des fichiers

[Remonter] [Precedent] [ Suivant]

9.2 Implémentation des fichiers


Il s'agit de stocker les octets du fichier dans des blocs du disque de manière à pouvoir effectuer aussi
efficacement que possible les opérations élémentaires. On peut imaginer toutes sortes de réalisation :
blocs contigus ; blocs chaînés ; index linéaires des blocs ; index arborescent des blocs. Nous en
présenterons quelques unes à titre d'exemple.
En plus de l'organisation interne du fichier il faut organiser le catalogue des noms de fichiers (repérage)
et l'allocation globale des blocs du disque (pour déterminer quels blocs sont libres ou occupés). La
technique des «bitmap» est un moyen efficace de gérer l'allocation des blocs.

9.2.1 Organisation - blocs contigues

Problème de création d'un fichier


Comment gérer plusieurs fichiers qui grandissent simultanément?
Nécessité de réserver un nombre fixe de blocs à la création.
Où allouer le premier bloc? Combien de blocs réserver?
Problème de fragmentation - trous - => impossible de trouver un espace libre suffisamment grand.

Exemples
Apple II
DEC PDP11 - RT11
IBM 3090 - VM (minidisques)

Avantage
Repérage immédiat du n-ième bloc

9.2.2 Organisation - indexes de blocs - FAT

http://cui.unige.ch/~falquet/std/notes/p9.3.html (1 of 4) [25-10-2001 20:28:10]


9.2 Implémentation des fichiers

Table d'allocation des blocs


Le répertoire contient le no. du premier bloc du fichier
On a une table d'allocation (FAT) qui contient un élément par bloc du disque.
FAT[i]
= <libre> si le bloc n'est utilisé par aucun fichier
= j si le bloc no. i est utilisé par un fichier f et j suit le bloc no. i dans f
= <fin> si le bloc no. i est utilisé par un fichier f et c'est le dernier bloc de f.
Pour que ce soit efficace il faut que la FAT soit copiée en mémoire centrale.

Problème
Pour le gros disques (1 gigaoctet) la FAT devient très grande
taille(FAT) = (nb. de blocs du disque) * [log 2 nb de blocs] bits

9.2.3 Organisation - index de blocs hiérarchisés


Un descripteur de fichier (i-node) contient (Unix)
● les no.s des 10 premiers blocs

● le no. d'un bloc d'index primaire, secondaire et tertiaire

Si le fichier fait plus de 10 blocs on alloue un bloc d'index primaire qui contient les nos des N blocs
suivants.
Si le fichier fait plus de N + 10 blocs on alloue un bloc secondaire qui contient les nos des N blocs
d'index primaire.
Si le fichier fait plus de N 2 + N + 10 = (N + 10)(N - 9) + 100 blocs on alloue un bloc tertiaire ...

http://cui.unige.ch/~falquet/std/notes/p9.3.html (2 of 4) [25-10-2001 20:28:10]


9.2 Implémentation des fichiers

Suivant où se trouvent les octets à lire il faut accéder à 0, 1, 2,ou 3 blocs d'index avant d'arriver au bloc
de données. Pour accélérer les accès on peut essayer de garder en mémoire centrale le plus possible de
blocs d'index.
On peut considérer cette structure comme la juxtaposition de B-arbres de hauteur 1, 2, 3 et 4
respectivement.

9.2.4 Carte des blocs alloués (bitmap)


Tableau de booléens tel que TB[i] = vrai <=> le bloc i du disque est alloué à un fichier, = faux s'il est
libre.
Un booléen occupe un bit

Taille de TB en bits:
nb. [octets du disque / nb. octets par bloc]
ex. 1000Mo / 1K = 1000000 bits

Taille de TB en blocs:
[taille de TB en bits / (8 x nb. octets par bloc)].
ex. 1000000 / (8 x 1024) = 123 blocs
soit 0.0123 % du disque

http://cui.unige.ch/~falquet/std/notes/p9.3.html (3 of 4) [25-10-2001 20:28:10]


9.2 Implémentation des fichiers

Ex. organisation d'un disque Unix

Les i-nodes sont des descripteurs de fichiers.


Les répertoires («directory») sont des fichiers normaux qui contiennent des références aux i-nodes de
leurs fichiers ou sous-répertoires.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p9.3.html (4 of 4) [25-10-2001 20:28:10]


10 Niveau physique des bases de données

[Remonter] [Precedent] [ Suivant]

10 Niveau physique des bases de données


Relations
- représentation des tuples et ensembles de tuples
- indexation
- opérations relationnelles
Objets persistants
- système O2
- système Gemstone
- système Exodus
- références aux objets en mémoire et sur disque

10.1 Stockage des tables relationnelles

10.2 Système INGRES (premières versions)

10.3 Organisation en tas

10.4 Fichier indexé

10.5 Système R: ancêtre d'Oracle


10.6 Représentation des objets persistants

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.1.html [25-10-2001 20:28:13]


10.1 Stockage des tables relationnelles

[Remonter] [Precedent] [ Suivant]

10.1 Stockage des tables relationnelles


Besoins:
● stocker les tuples d'une relation (rangées d'une table)

● opérations primitives de construction de relations:

● ajouter un tuple

● supprimer un tuple

● modifier un attribut d'tuple

● assurer un calcul efficace des opérations de l'algèbre relationnelle

● sélection : attribut = valeur, attribut <op> valeur, "et", "ou" logiques

● jointure : équijointure, theta-jointure

● maintenir certaines contraintes d'intégrité


● unicité des clés
● intégrité référentielle (clés étrangères)
● contraintes de domaines
● permettre les accès concurrents (verrous, etc.), la reprise après pannes, etc.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.2.html [25-10-2001 20:28:16]


10.2 Système INGRES (premières versions)

[Remonter] [Precedent] [ Suivant]

10.2 Système INGRES (premières versions)


1. un fichier logique par relation
2. trois types d'organisation
1. fichier organisé en tas
2. fichier h-codé
3. fichier indexé
3. accélérateurs
4. création d'index
5. les index sont des tables organisées en fichier indexé
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.3.html [25-10-2001 20:28:18]


10.3 Organisation en tas

[Remonter] [Precedent] [ Suivant]

10.3 Organisation en tas


Les tuples de la table sont représentés par des enregistrements du fichier

Insertion d'un tuple:


Ajout d'un nouvel enregistrement en fin de fichier

Suppression d'un tuple:


Mise à 0 du bit indicateur de l'enregistrement

Sélection:
Balayage séquentiel du fichier

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.4.html [25-10-2001 20:28:20]


10.4 Fichier indexé

[Remonter] [Precedent] [ Suivant]

10.4 Fichier indexé


Les enregistrements sont maintenus en ordre croissant de la valeur d'un champ clé qui correspond à un
attribut clé de la relation. Un fichier d'index (creux) contient des paires
(valeur clé premier enregistement de la page, no. page)

Insertion:
le champ clé de l'enregistrement à insérer détermine la page où il doit aller
s'il existe une place libre dans la page : insérer dans cette page
sinon
stratégie 1: essayer une re-répartiion avec la page suivante si elle a de la place, et ainsi de suite
● problème: les enregistrements bougent, il faut modifier l'index

stratégie 2: créer une page de débordement chaînée à la page prévue


● problème: accès lent s'il y a trop de débordements => nécessaire réorganisation du fichier

Sélection:
si la sélection porte sur l'attribut clé: recherche dichotomique dans l'index puis dans la page indiquée par
l'index; pour les autres attributs: balayage séquentiel.
[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.5.html [25-10-2001 20:28:22]


10.5 Système R: ancêtre d'Oracle

[Remonter] [Precedent] [ Suivant]

10.5 Système R: ancêtre d'Oracle


1. rid et tid
2. structure d'une page
3. stockage en tas partiellement remplis
4. regroupement physique des tuples
5. accélérateurs
6. index en B+-arbre

10.5.1 Stockage des tuples


Les tuples sont stockés comme des enregistrements dans des pages de fichiers.
Une même page peut contenir des tuples de différentes relations
● cela permet d'accélérer certaines opérations de jointure

● chaque tuple est suivi d'un "relation id" qui indique à quelle relation il appartient

Chaque tuple possède un "tuple identifier" (tid) qui est une paire (no. page, déplacement)

● une zone est réservée à la fin de la page pour les pointeurs de début de tuples, le déplacement est
relatif au premier pointeur (en partant de la fin)
● le tuple peut être déplacé dans la page sans changer son tid
A l'insertion on peut demander à placer un tuple le plus près possible d'un autre

10.5.2 Structure du B-arbre


Chaque noeud de l'arbre est une page physique
Les noeuds intérieurs de l'arbre ont une structure standard:
[ ptr-page-1 | clé-1 | ptr-page-2 | clé-2 | ... | clé-n | ptr-page-n+1 ]
Les nouds feuilles ont les particularités suivantes:
● ils sont chainés entre eux dans les deux sens

http://cui.unige.ch/~falquet/std/notes/p10.6.html (1 of 3) [25-10-2001 20:28:33]


10.5 Système R: ancêtre d'Oracle

● les pointeurs de page sont remplacés par des tid


● si plusieurs tuples ont la même valeur clé, la liste des tid suit la valeur de clé (ceci arrive lorsque
l'attribut "clé" n'est pas une clé au sens relationnel du terme)
● si la liste des tid pour une valeur donnée est trop grande pour la page, une page de débordement est
créée
ptr
clé clé nb
ptr feuille préc. ptr feuille suiv nb tup. tid ... tid ... tid ... tid page
1 n tup.
débord.

10.5.3 Opérations relationnelles


Sélection
1. prédicat du type (colonne = valeur)
s'il existe un index sur la colonne: chercher la valeur dans l'index
sinon parcours séquentiel
1. prédicat du type (colonne (<, >, £ , ) valeur)
si l'index est de type B-arbre on peut s'en servir car il est ordonné
un index en hachage est inutilisable
1. prédicat complex avec et/ou/négation
si le prédicat est de la forme ( C 1 = v 1 et C 2 = v 2 et ... et C k = v k ) et qu'il existe un index composite
sur ( C 1 , C 2 , ... , C k ) on peut s'en servir directement

si le prédicat est de la forme ( C 1 = v 1 et P' ) et qu'il existe un index sur C 1 : sélectionner par l'index le
sous-ensemble S des tuples qui satisfont C 1 = v 1 , puis sélectionner les tuples de S qui satisfont P' .

Principe général : décomposer la sélection en un ensemble de conjonctions et commencer par


sélectionner avec les conditions pour lesquelles on a une structure efficace (index).

10.5.4 Jointure
1. balayage imbriqué
Pour calculer R * A = B S on fait
pour chaque tuple r de R
pour chaque tuple s de S
si r.A = s.B alors ajouter (r, s) au résultat

http://cui.unige.ch/~falquet/std/notes/p10.6.html (2 of 3) [25-10-2001 20:28:33]


10.5 Système R: ancêtre d'Oracle

Le temps de calcul est proporitonnel à taille(R) * taille(S)


Amélioration:
Si S est indexée sur B la boucle interne peut être remplacée par une recherche indexée, le temps devient
O(taille(R)).

2. tri et composition
Trier R selon A et S selon B
Effectuer une fusion de R et S selon A et B.
Le temps de calcul est composé du temps de tri (au moins taille(rel)*log(taille(rel))) et du parcours des
deux relations (taille(R) + taille(S)).

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.6.html (3 of 3) [25-10-2001 20:28:33]


10.6 Représentation des objets persistants

[Remonter] [Precedent] [ Suivant]

10.6 Représentation des objets persistants


Gérer la persistance d'un objet consiste essentiellement à gérer ses déplacements entre la mémoire
centrale non permanente et limitée et la mémoire permanente (disque, réseau) de plus grande taille.
Le premier niveau de gestion de la persistance consiste à permettre l'écriture et la lecture explicite d'un
objet dans un fichier (p.ex. sérialisation en Java). Il y a plusieurs problèmes à résoudre:
● s'assurer que non seulement l'objet explicitement désigné soit écrit sur disque, mais également les
objets auxquels il fait référence ;
● transformer toutes les références à des objets en mémoire en des références à des objets sur disque,
et vice-versa. En effet, l'espace d'adressage du disque n'est pas le même que celui de la mémoire,
pour conserver l'intégrité du système il faut donc "suivre" l'objet dans ses déplacements ;
Le second niveau consiste à se passer de la notion de fichier. On dit simplement qu'on veut lire ou écrire
un objet, sans préciser dans quel fichier. Dans ce cas il faut
● donner un moyen de retrouver l'objet stocké sur disque.

● nommer les objets et maintenir un catalogue d'objets

● fournir un langage de sélection d'objets et l'intégrer élégamment dans le langage OO utilisé

● utiliser une base de données plutôt que des fichiers séquentiels

● définir des correspondances entre structure des objets et structure de la BD

10.6.1 Bases d'objets


Une approche plus sophistiquée consiste à laisser au système le soin de déplacer les objets entre disque et
mémoire au fur et à mesure des besoins, sans que le programmeur doive explicitement demander la
lecture ou l'écriture de l'objet. Le programme est écrit comme si tous les objets étaient disponibles à tout
instant dans la mémoire.
● gérer le déplacement (partiel) de gros objets

● déterminer quand les objets doivent être lus ou écrits

● garantir l'intégrité en cas de panne

● définir un langage complet de sélection et de manipulation d'objets (OQL)

● intégrer le style de programmation OO procédural et le style déclaratif/algébrique du langage de la


BD
● garantir de bonnes performances

● etc.

http://cui.unige.ch/~falquet/std/notes/p10.7.html (1 of 4) [25-10-2001 20:28:41]


10.6 Représentation des objets persistants

10.6.2 Méthode (inspirée du SGBD O2)


On suppose qu'il existe un système de fichiers, qui n'est pas forcément celui du système d'exploitation.
Chaque fichier contient des enregistrements repérés par leur position (en nb. d'octets depuis le début du
fichier).

Les objets "simples"


● Un objet avec des variables d'instance est stocké dans un enregistrement d'un fichier. Il est repéré
par l'adresse de cet enregistrement (fichier + position dans le fichier).
● L'oid est stocké au début de l'enregistrement.
● Les valeurs atomiques des variables d'instance sont stockées directement dans l'enregistrement
(entiers, réels, caractères, chaînes)
● Pour les variables contenant des références à d'autres objets on stocke l'adresse des enregistrements
contenant ces objets.

10.6.3 Les objets "collections"


Ensemble
Un ensemble est représenté par un B-arbre dont les éléments sont les adresse des enregistrements
représentant les objets de l'ensemble.
=> on peut retrouver efficacement un objet ou tester s'il appartient à l'ensemble

Liste
Une liste est représentée par un B-arbre dont les clés sont les positions des éléments dans la liste.
=> on peut retrouver efficacement le n-ième objet de la liste

Multi-ensemble
Un ensemble non unique (multi-ensemble) est représenté par
1) un B-arbre qui représente l'ensemble (unique) des éléments
2) un fichier séquentiel d'identificateurs des enregistrements du B-arbre

10.6.4 Stockage d'objets en Gemstone (modèle Smalltalk)


5 formats de base

http://cui.unige.ch/~falquet/std/notes/p10.7.html (2 of 4) [25-10-2001 20:28:41]


10.6 Représentation des objets persistants

self-identifying
pour les SmallInt, Character, Boolean

byte
pour les String, Float, etc.

named
pour accéder aux composantes d'un objet par leur nom (noms des variables d'instance)

indexed
accès aux composantes d'un objet par le numéro, p.ex. pour les Array

non-sequenceable collections
utilisé pour les Set, Bag, etc. dont les composantes ne sont ni nommées ni numérottées.

10.6.5 Gemstone: Stockage des gros objets


Lorsqu'un objet est plus grand qu'une page, il est découpé en morceaux et organisé sous forme d'un arbre
de pages.
● il n'est pas nécessaire d'amener en mémoire l'objet complet

● on peut modifier des parties de l'objet sans tout récrire

● l'objet peut grandir et rétrécir sans recopie complète

Le système utilise des pointeurs orientés-objet (OOP) pour référencer les objets et une table de
correspondance entre OOP et adresse physique.

10.6.6 Enregistrements extensibles (EXODUS)


Exodus est un "noyau" de base de données à objet
Les "objets de stockage" (OdS) sont les unités de données de base de EXODUS.
● Les petits OdS résidents entièrement sur une page, les grands occupent plusieurs pages.

● Les pages contenant de petits objets sont découpées en emplacements

● L'identificateur d'un OdS (oid) est une paire (no. page, no. emplacement).

● Pour les grands objet l'oid pointe vers vers une entête de grand objet (EGO) qui réside dans une
page découpée avec d'autre EGO et petits objets. L'EGO pointe vers d'autres pages qui servent à
représenter le grand objet.

http://cui.unige.ch/~falquet/std/notes/p10.7.html (3 of 4) [25-10-2001 20:28:41]


10.6 Représentation des objets persistants

10.6.7 Stockage des grands objets


1. Conceptuellement un grand objet est une séquence d'octets
2. La représentation est un B+ arbre qui sert d'index vers un ensemble de pages qui contiennent les
octets de l'objet.
3. Chaque page (noeud intérieur) de l'arbre est de la forme
[p 1 , n 1 , p 2 , n 2 , ..., p k , n k ]
● Les p i sont des pointeurs vers d'autres pages de l'arbre ou vers des pages de données.
● Les n i indiquent le dernier numéro d'octet stockés dans le sous-arbre dont la racine est indiquée
par p i .
● Cette représentation permet d'ajouter et de supprimer des octets n'importe où dans un OdS.

[Remonter] [Precedent] [ Suivant]

http://cui.unige.ch/~falquet/std/notes/p10.7.html (4 of 4) [25-10-2001 20:28:41]

Vous aimerez peut-être aussi