Algo Prog With Python 01 12 2023 LMD
Algo Prog With Python 01 12 2023 LMD
Algo Prog With Python 01 12 2023 LMD
1 Introduction 7
1.1 A qui s’adresse ce cours . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Pré-requis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3 Connaissances et compétences des étudiants au terme de ce cours 7
1.4 Méthodologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Références . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6 Note spéciale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.7 Auteur de ces notes de cours . . . . . . . . . . . . . . . . . . . . 8
1.8 Passage au système LMD . . . . . . . . . . . . . . . . . . . . . . 9
1.9 Introduction générale . . . . . . . . . . . . . . . . . . . . . . . . . 10
1
2 TABLE DES MATIÈRES
3 Introduction à l’algorithmique 75
3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.2 Les fondements mathématiques de l’algorithmique . . . . . . . . 76
3.2.1 La fonction constante . . . . . . . . . . . . . . . . . . . . 77
3.2.2 La fonction logarithme . . . . . . . . . . . . . . . . . . . . 78
3.2.3 La fonction linéaire . . . . . . . . . . . . . . . . . . . . . . 78
3.2.4 La fonction n log2 n . . . . . . . . . . . . . . . . . . . . . . 78
3.2.5 La fonction quadratique . . . . . . . . . . . . . . . . . . . 79
3.2.6 La fonction cubique et les autres polynomes . . . . . . . . 79
3.2.7 La fonction exponentielle . . . . . . . . . . . . . . . . . . 80
3.2.8 Quelques sommations . . . . . . . . . . . . . . . . . . . . 80
3.3 Efficacité d’un algorithme . . . . . . . . . . . . . . . . . . . . . . 81
3.4 Algorithmes et autres technologies . . . . . . . . . . . . . . . . . 82
3.5 Conception et analyse des algorithmes . . . . . . . . . . . . . . . 82
3.5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 82
3.5.2 Conception d’un algorithme . . . . . . . . . . . . . . . . . 83
3.5.3 Analyse expérimentale . . . . . . . . . . . . . . . . . . . . 86
3.5.4 Analyse théorique . . . . . . . . . . . . . . . . . . . . . . 88
3.5.5 Notation assymptotique . . . . . . . . . . . . . . . . . . . 94
4 La Récursivité 99
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
4.2 Quelques exemples illustrant la récursivité . . . . . . . . . . . . . 99
4.2.1 La fonction factorielle . . . . . . . . . . . . . . . . . . . . 99
4.2.2 La recherche binaire . . . . . . . . . . . . . . . . . . . . . 100
4.2.3 Le système de fichiers . . . . . . . . . . . . . . . . . . . . 103
4.3 Analyse d’un algorithme récursif . . . . . . . . . . . . . . . . . . 105
4.3.1 Calcul du temps d’exécution de factoriel(n) . . . . . . . . 105
4.3.2 Calcul du temps d’exécution de la recherche binaire . . . 106
TABLE DES MATIÈRES 3
Introduction
1.2 Pré-requis
Les étudiants sont censés avoir suivi un cours de programmation. Ils ont
donc des connaissances élémentaires en ce qui concerne la programmation. Ils
ont également été préparés à suivre un cours d’algorithmique. Ils devraient dans
la mesure du possible avoir suivis une introduction au langage de programmation
Python.
7
8 CHAPITRE 1. INTRODUCTION
Ils connaissent les structures des données les plus populaires et probablement les
plus utilisées. Il s’agit entre autre : des tableaux, des listes chainées, des piles,
des files d’attente, des arbres et des graphes. Pour chacune de ces structures
de données ils savent produire deux ou trois implémentations en Python. Le
cours se termine par la réalisation de deux projets dans lesquels les structures
de données étudiées au cours sont mises en oeuvre dans des problèmes réels.
1.4 Méthodologie
Pour atteindre ces objectifs, nous pronnons et pratiquons l’enseignement
par la pratique. Une partie du cours théorique est consacrée à la maitrise des
concepts de base par la pratique. En bref, la maitrise des notions fondemen-
tales d’algorithmique et de programmation par la pratique commence au cours
théorique pour s’intensifier aux travaux pratiques.
1.5 Références
[1] T.H. Cormen, C.E. Leiserson, R.L. Rivest, C. Stein, ”Algorithmique. Cours
avec 957 exercices et 158 problèmes.”, Dunod, 3ième édition, Juin 2010.
et une partie logicielle (software) qui assure les fonctions logiques nécessaires
aux différents traitements et au stockage de l’information.
ner savamment ces deux éléments et les exploiter par les bons soins des ma-
chines appropriés, ou des ressources humaines ayant les compétences requises.
Par exemple pour pouvoir confectionner un livre (Je parle ici de la saisie des
informations et de leur mise en forme), il faut disposer d’un ordinateur et d’un
programme de traitement de texte (MS word de Microsoft pour le commun des
mortels ou LaTeX pour des personnes un petit peu mieux outillées en matière
de connaissances en informatique) et d’un secrétaire qualifié.
Windows 2000, Windows XP, Windows 2010, linux, MSWord, LaTeX sont des
logiciels. Ils ont été conçus, mis au point et distribués par des acteurs du secteur
logiciel de l’industrie informatique. Windows 2000, Windows XP, Windows 2010
et linux sont des systèmes d’exploitation alors que MSWord et LaTeX sont des
logiciels d’application.
Il convient de signaler ici qu’ au fil des années, les logiciels ont évolués dans
plusieurs directions :
les logiciels utilitaires. Ce sont des logiciels qui aident à développer les
applications (logiciels d’application) ; ce sont les compilateurs, les in-
terpréteurs, les assembleurs, les éditeurs des liens ; les chargeurs et les
débogueurs. Ils comprennent aussi d’autres outils tels que des outils gra-
phiques, des outils de communication, etc ;
Comme vous le devinez déjà, mettre au point un logiciel n’est pas chose aisée.
Pour preuve toute une discipline de l’informatique s’attelle à cela : c’est le génie
logiciel. Sans entrer dans les détails le processus de mise au point d’un logiciel
peut être décomposé en 7 étapes :
l’expression des besoins. C’est la phase ou l’on décrit les fonctions que le
logiciel doit effectuer, les conditions d’exploitation, le contrat de service,
la qualité requise, en faisant abstraction le plus possible de la façon dont
ces différentes fonctions vont être effectivement réalisées. On considère
le logiciel comme une boite noire dont on ne connait que les entrées et
les sorties mais que l’on veut voir se comporter d’une certaine façon en
termes de fonctionalités, de performances (consomation de ressources,
temps de réponses, débit d’informations) et de sûreté de fonctionnement
(disponibilité du système, sécurité) ;
la conception. Cette phase a comme objectif de définir de façon très
précise les fonctions et l’architecture du logiciel, à partir des besoins ex-
primés et des contraintes générales définies dans les phases précédentes. A
l’issue de cette phase, tous les choix techniques ont été effectués, les fonc-
tions sont spécifiées, les regroupements en modules sont connus, les algo-
rithmes à mettre en oeuvre sont identifiés et caractérisés de façon quan-
titative (temps de réponse, ressources consommées), les données essen-
tielles sont répertoriées et les évenèments qui déclenchent le séquencement
des opérations explicités ;
la programmation et les tests unitaires. Cette phase correspond à la pro-
grammation proprement dite des fonctions sur la base des informations
précises venant de la phase de conception. Les fonctions sont traduites
dans le ou les langages de programmation qui ont été adoptés, en respec-
tant les normes de qualité définies dans le plan de qualité du logiciel. A
l’issue de cette phase, tous les modules doivent compiler sans erreur ;
l’intégration et les tests de qualification. Cette phase correspond au re-
groupement progressif de tous les modules de façon à garantir la vérification
et la validation progressive du logiciel, jusqu’à pouvoir le faire fonction-
ner dans son environnement réel. A l’issue de cette phase on doit être
en mesure de s’assurer par l’exécution du logiciel et à l’aide de mesures
appropriées, que le logiciel satisfait aux besoins spécifiés au moment de
l’expression des besoins ;
l’installation. Cette phase correspond à la mise en fonctionnement opérationnel
du logiciel dans le contexte du client ;
l’exploitation et la maintenance. Cette phase correspond à la mise à la
disposition des utilisateurs du logiciel et ceci sans restriction.
Un algorithme est une procédure de calcul bien définie qui prend en entrée
une valeur, ou un ensemble de valeurs, et qui donne en sortie une valeur, ou un
ensemble de valeurs. Un algorithme est donc une séquence d’étapes de calcul
qui transforment une entrée en une sortie.
Rappels sur la
programmation et le
langage Python
2.1 Introduction
Le langage Python a été développé au début des années 1990s par Guido
van Rossum. Il est devenu depuis un des langages de programmation les plus
utilisés par l’industrie et le monde de l’éducation. Python 2 a été délivré en
2000. La troisième version de Python a été délivrée quand à elle en 2008. Il faut
noté qu’il y a des fortes incompatibilités entre Python 2 et Python 3. Ces notes
de cours sont basées sur Python 3 et les versions utltérieures (la plus récente
version de Python au moment de la rédaction de ces notes est Python 3.9.0 ;
la version d’Octobre 2020). Il est possible de télécharger cette dernière version
ainsi que sa documentation sur www.python.org.
Dans ces notes nous supposons que les étudiants ont eu un premier contact
avec Python. Ces notes ne donnent donc pas une description complète de Py-
thon. Nous nous attachons plutôt à introduire tous les aspects de Python qui
apparaissent dans les illustrations de ce cours ”d’Algorithmique et Programma-
tion”.
17
18CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
temperature = 98.6
temperature est ici une variable. A cette variable est associée l’objet située du
coté droit du signe égal. Ici il s’agit d’un objet de type float dont la valeur est
98.6. En Python les variables respectent les majuscules et les miniscules. Ainsi,
Figure 2.1 – A la variable tempertaure est associée l’objet float dont la valeur
est 98.6
Figure 2.2 – Les variables tempertaure et original sont des alias. Ils font
références tous les deux à un même objet. Il s’agit ici d’un objet de type float
dont la valeur est 98.6
Les changements qui sont apportés à notre objet float par le biais de la
variable temperature sont visibles sur la variable original. Par contre si on
affecte à une des variables de l’alias un autre objet, cela n’a aucun effet sur
l’objet sur lequel pointe l’alias. Cette opération brise l’alias.
temperature = temperature+5.0
Dans l’expression ci-dessus une nouvelle valeur float a été affectée à la variable
temperature. Cela n’a aucun effet sur la variable original comme on peut le
voir sur la figure 3.3. Il y a juste que l’alias est brisé.
w = Widget()
d = Widget(a,b,c)
20CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Figure 2.3 – Une nouvelle valeur float a été affectée à la variable tempera-
ture. Cette opération n’affecte pas la valeur référencée par la variable original,
elle brise juste l’alias.
Dans la première instruction un objet de type Widget est crée, par un construc-
teur sans argument, puis affecté à la variable w. Dans la deuxième instruction,
le contructeur reçoit trois arguments pour créer un objet de type Widget qui
est ensuite affecter à la variable d.
Plusieurs classes internes de Python acceptent la création des nouvelles ins-
tances sous forme de littéral. Par exemple l’instruction temperature = 98.6
consiste en une création d’une nouvelle instance de type float, sous forme littéral
(le littéral ici est 98.6). Ensuite l’objet crée de cette manière est affecté à la va-
riable temperature.
La classe bool
Elle est utilisée pour manipuler des booléens. Il existe seulement deux ins-
tances de cette classe qui sont exprimées comme des littéraux True et False.
Le contructeur par défaut retourne False. Toutefois il n’y a pas lieu de re-
courir à cette syntaxe dans la mesure ou il suffit d’affecter True où False à
une variable pour avoir un objet de type booléen. Python permet la création
d’un objet booléen à partir d’un type non booléen. En utilisant la syntaxe
bool(foo), si foo = 0 l’expression vaut False, dans le cas contraire elle vaut
True. Les séquences et les autres conteneurs sont évalués à False s’ils sont vides
et True s’ils ne sont pas vides. Une importante application de cette situation
est l’utilisation des valeurs non booléennes comme condition dans les structures
de contrôle.
La classe list
Une instance liste stocke une séquence d’objets. Une liste stocke une séquence
de références (voir figure 2.4). Les éléments d’une liste sont des objets arbitraires
(y compris l’objet particulier None). Les listes sont des séquences basées sur
Figure 2.4 – Représentation interne d’une liste en Python. Cette liste a été
instanciée par l’instruction primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]. L’indice
implicite de chaque élément dans la liste est donnée en-dessous du tableau.
les tableaux. Les indices des éléments d’une liste commencent à 0. Ainsi les
indices des éléments d’une liste de n éléments vont de 0 à n-1. Python utilise
les caractères [ ] comme délimiteurs pour une liste des litéraux. Ainsi [’rouge’,
’vert’, ’bleu’] est une liste contenant trois instances de chaines de caractères. [
] est une liste vide. Le constructeur list(), produit une liste vide par défaut,
toutefois le constructeur accèpte n’importe quel paramètre d’un type itérable.
La classe tuple
La classe tuple fourni une version non modifiable d’une séquence. Les
parenthèses sont utilisées pour délimiter un tuple. (17, ) est un tuple à un
élément alors que (17) représente tout simplement l’entier 17 entre parenthèses.
La classe str
La classe str de Python permet de représenter une séquence non modifiable
de caractères basés sur l’Unicode. Une chaine de caractère peut être notée ’hello’
ou ”hello”. Part contre il faut obligatoirement utilisé le double quote quand le
simple quote est utilisé comme un simple caractère. Exemple ”Don’t worry”. Il
est néamoins possible d’utiliser le backslash pour se défaire du problème. Python
supporte aussi ””” ou ””” pour commencer et finir une chaine de caractères.
Cette façon de faire permet de générer automatiquement une ligne blanche
2.3. EXPRESSIONS ET OPÉRATEURS 23
La classe dict
Opérateurs logiques
Opérateur Description
not opérateur unaire de négation
and et logique
or ou logique
Les opérateurs and et or sont des opérateurs court-circuit dans la mesure où
ils n’évaluent pas la seconde opérande si le résultat de l’opération peut être
déterminer par la première opérande.
Opérateurs d’égalité
Python supporte les opérateurs d’égalité suivants :
Opérateur Description
is True si les opérandes ont la même identité
is not True si les opérandes n’ont pas la même identité
== True si les opérandes sont équivalents
!= True si les opérandes ne sont pas équivalents
L’espression a is b est évaluée à True si a et b sont les alias d’un même objet.
L’expression a == b teste une notion plus générale, celle de l’équivalence. Si a et
b font reférence à un même objet, alors a == b est True. a == b est également
True si a et b font référence à des objets différents mais dont les valeurs sont
les mêmes. La notion précise d’équivalence dépend du type de la donnée. Par
exemple deux chaines de caractères sont équivalentes si elles sont les mêmes
caractère par caractère. Deux ensembles sont équivalents s’ils contiennent les
mêmes éléments.
Opérateurs de comparaison
Les structures des données définissent des relations d’ordre à partir des
opérateurs suivants :
Opérateur Description
< plus petit que
<= plus petit ou égal
> plus grand que
>= plus grand ou égal
Opérateurs Arithmétiques
Python supporte les opérateurs arithmétiques suivants :
Opérateur Description
+ addition
- soustraction
* multiplication
/ simple division
// division entière
% modulo
2.3. EXPRESSIONS ET OPÉRATEURS 25
Opérateur Description
/ 27/4 = 6.75 : simple division
// 27//4 = 6 division entière
% 27%4 = 3 modulo : reste de la division entière
Opérateur Description
∼ complément
& et
— ou
ˆ ou exclusif bit à bit
<< shift à gauche en ajoutant des 0
>> shift à droite en ajoutant des 0
Opérateur Description
s[j] élément dont l’indice est j
s[start :stop] éléments de s dont les indices
vont de start à stop-1
s[start :stop :step] éléments de s dont les indices vont de start
stop-step par pas de step
s+t concatenation des séquences
k∗s une façon d’écrire s+s+s+... (k fois)
val in s vérifie si s contient val
val not in s vérifie si s ne contient pas val
L’indice des éléments d’une séquence commence à 0. Ainsi les éléments d’une
séquence de n éléments sont indexés de 0 à n-1. On peut faire du ”slicing”.
Exemple data[3 :8] est une sous-séquence de la séquence data dont les indices
sont : 3, 4, 5, 6, 7. La notation val in s peut être utilisée pour n’importe quelle
séquence pour vérifier si la séquence s contient l’élément val.
Opérateur Description
key in s vérifie si s contient key
key not in s vérifie si s ne contient pas key
s1 == s2 vérifie si s1 est équivalent à s2
s1 <= s2 vérifie si s1 est un sous-ensemble de s2
s1 peut être égal à s2
s1 < s2 vérifie si s1 est un sous-ensemble de s2
s1 ne peut pas être égal à s2
s1 est strictement inclu dans s2
s1 >= s2 vérifie si s1 est un ensemble contenant l’ensemble s2
s1 > s2 vérifie si s1 est un ensemble contenant l’ensemble s2
mais pas égal à lui
s1|s2 l’union de s1 et s2
s1&s2 l’intersection de s1 et s2
s1 − s2 l’ensemble des éléments de s1
qui ne sont pas dans s2
s1ŝ2 les éléments qui sont soit dans s1 est soit dans s2
mais pas dans les deux
Opérateur Description
alpha = [1, 2, 3] affectation de la liste [1, 2, 3] à alpha
beta = alpha beta devient un alias de alpha
beta +=[4, 5] étend la liste existante avec deux nouveaux
éléments. alpha et beta sont modifiés
beta =beta + [6, 7] réaffectation à beta
d’une nouvelle liste [1, 2, 3, 4, 5, 6, 7].
L’alias est cassé. alpha n’est pas modifié
print(alpha) imprime [1, 2, 3, 4, 5]
Les langages de programmation doivent disposer des règles claires selon les-
quelles les expressions composées telles que 5+2∗3 doivent être évaluées. L’ordre
des priorités à suivre pour l’évaluation des expressions est donné dans le tableau
ci-dessous :
2.4. LES STRUCTURES DE CONTRÔLE 27
if first_condition:
28CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
first_body
elif second_condition:
second_body
elif third_condition:
third_body
else:
fourth_body
Chaque condition est une expression booléenne et chaque body contient une
ou plusieurs instructions à exécuter. Si la première condition est True, le pre-
mier body est exécuté. Aucune autre condition ne sera évaluée dans ce cas. Si
la première condition est false, alors le processus d’évaluation des conditions
se poursuit avec l’évalution de la deuxième condition. L’exécution de toute la
structure if vera un et un seul body s’exécuté. Il peut y avoir 0 ou plusieurs
clauses elif. La clause finale else est optionelle.
Un exemple simple d’utilisation de l’instruction if est la logique d’un robot
contrôleur :
if door_is_closed:
open_door()
advance()
Il faut noter que l’instruction finale advance() n’est pas indentée et ne fait par
conséquent pas partie du code body du if. Elle sera exécutée que la porte soit
ouverte ou fermée. Les if peuvent être imbriqués les uns dans les autres. On
peut illuster ceci avec notre robot.
if door_is_closed:
if door_is_locked:
unlock_door()
open_door()
advance()
Boucle while
La syntaxe de la boucle while est la suivante :
2.4. LES STRUCTURES DE CONTRÔLE 29
while condition:
body
Comme dans le cas de l’instruction if condition est une expression booléenne
et body est le block de code à exécuté lorsque la condition est True. Après
chaque exécution de body la condition est réevaluée. Si elle est True le body
est exécuté. Lorsque la condition devient False, le programme sort de la boucle
et continue son exécution avec l’instruction juste après la boucle. Ci-dessous
l’exemple d’une boucle qui fait avancer un indice dans une chaine de caractère
jusqu’à ce qu’elle rencontre la lettre ’X’ ou la fin de la séquence.
j=0
while j<len(data) and data[j] != ’X’:
j +=1
La boucle for
La syntaxe de la boucle for de Python est plus appropriée que celle de la
boucle while lorsqu’il s’agit de parcourir les éléments d’une séquence. La boucle
for sera donc utilisée pour le traitement des structures iterable telles que list,
tuple, str, set, dict, ou file. Sa syntaxe générale est :
30CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
found = False
for item in data:
if item==target:
found = True
break
Ce bout de code termine la boucle for lorsqu’une valeur target est atteinte. Py-
thon supporte également l’instruction continue qui termine l’actuelle itération
du body d’une boucle, avec un déroulement normal pour les prochaines itérations
de la boucle. Ces deux instructions doivent être utilisée avec parcimonie.
2.5. LES FONCTIONS 31
L’instruction return
L’instruction return est utilisée dans le body (coprs) d’une fonction pour
indiquer que la fonction doit immédiatement terminé son exécution et qu’une
valeur passée comme argument à l’intruction return doit être retounée à l’ap-
pelant de la fonction. Si l’instruction return est exécutée sans argument, alors
la valeur None est retournée automatiquement. De même None sera retourné
si la fin de la fonction est atteinte sans qu’aucune instruction return n’aie été
exécutée. Il peut exister plusieurs instructions return dans une fonction. Les
conditions désigneront laquelle des instructions return sera exécutée. Exemple :
def contains(data, target):
for item in target:
return True
return False
32CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Si la condition dans la boucle for est satisfaite, item in target est True
indiquant que le ”target” a été atteinte. item == target est True, et le
return True est exécuté, ce qui termine la fonction. Par contre si la boucle
atteint la fin sans que item in target ne soit True alors le return False est
exécuté.
data = grades
target=’A’
Elle a trois arguments. Les deux derniers arguments offrent des valeurs par
défaut. Un appellant peut appeler la fonction avec 3 arguments, comme foo(4,12,8).
Dans ce cas les valeurs par défaut des deux derniers arguments ne sont pas uti-
lisées. Si par contre l’appelant appelle la fonction en lui passant un et un seul
argument comme foo(4), la fonction s’exécutera avec les arguments a = 4,
b = 15, c = 27.
Si un appelant appelle la fonction avec deux arguments comme foo(8,20),
la fonction s’exécutera avec les arguments a = 8, b = 20, c = 27. Cependant,
il est illégal de définir une fonction avec une signature telle que bar(a, b=15,
c) où b possède une valeur par défaut et c pas. Si on définit une valeur par
défaut pour un argument, des valeurs par défaut doivent être définies pour tous
les arguments qui suivent.
Commentons ici un exemple interessant de fonction polymorhe en l’occurence
la fonction range. Techniquement il s’agit en fait du constructeur de la classe
range, mais pour des raisons pédagogiques nous allons la traiter ici comme
une simple fonction. Trois appels différents sont possibles pour cette fonction.
L’appel avec un seul paramètre range(n), qui génère une séquence d’entiers
de 0 à n-1. Un appel à deux paramètres range(start, stop) qui génère des
entiers de start à stop non inclu. Et finalement un appel à trois paramètres
range(start,stop,step) qui génère la même chose que range(start, stop),
mais avec un incrément valant step au lieu de 1.
Cette combinaisons de formes de la fonction semble violer la règle sur les
arguments par défaut. En parliculier lorsqu’un seul argument est passé à la
fonction comme range(n). Le n sert ici comme valeur du stop qui est le sécond
34CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
argument. La valeur du start est 0 dans ce cas. Toutefois, cet effet peut être
obtenu par un tour de passe passe comme ci-dessous :
En choisissant les arguments nous utilisons les variables x, y, z pour des numériques
arbitraires, k pour un entier et a, b, c pour des types comparables arbitraires.
Nous utilisons la variable ”iterable” pour représenter une instance de n’importe
quel itérable (list, tuple, set, dict). Les fonctions dans le tableau peuvent être
regroupées en 5 catégories :
— Entrées et Sorties : print, input, et open.
— Encodage de caractères : ord et char relient les caractères avec leurs
codes entiers. Par exemple ord(’A’) vaut 65 et char(65) vaut ’A’).
— Mathématiques : abs, divmod, pow, round et sum offrent des fonc-
tionalités mathématiques usuelles.
— Ordering : max et min sont utilisables par toutes les données suppor-
tant la notion de comparaison et à toute collection. sorted peut être
utilisé pour produire une liste ordonnée, triée.
— Collections et itérations : range génère une nouvelle séquence de
nombres. len calcule la taille d’une collection (nombre d’éléments). Les
fonctions reversed, all, any et map opèrent sur des itérations arbi-
traires. iter, next sont utilisés dans les itérations.
chaine.
— Par défaut, la fonction print envoie sa sortie à la console. Toutefois cette
sortie peut être dirigée vers un fichier.
La fonction input
Exemple de programme :
L’instruction la plus élémentaire pour lire à partir d’un fichier est la méthode
read. Si elle est invoquée sous la forme suivante fp.read(k) l’instruction retour-
neles k prochains bytes du fichier. La lecture commence à partir de la position
actuelle. Sans argument fp.read() retourne le restant ou le contenu du fichier.
Pour des raisons de commodité, le fichier peut être lu ligne par ligne en utilisant
readline ou la méthode readlines pour lire le reste des lignes du fichier d’un
coup.
Lorsqu’un proxy de fichier permet l’écriture, du texte peut être écrit dans
ce fichier par les méthodes write ou writelines. Exemple : si nous définissons
fp=open(’results.txt’, ’w’), la syntaxe fp.write(”Hello World. ”) écrit
dans le fichier une ligne avec la chaine de caractère passée en argument. Toute
exception doit être prise en charge. Si elle n’est pas prise en charge elle va
entrainer l’arrêt de l’interpréteur.
2.7. LA GESTION DES EXCEPTIONS 39
Classe Description
Exception Une classe de base pour
la plupart des types d’erreurs
AttributeError Déclenchée par la syntaxe obj.foo
si obj n’a pas de membre foo
EOFError Déclenché lorsque la fin
du fichier est atteinte sur la console
ou dans un fichier input-output
IOError Déclenchée lorsque
une opération I/O se plante
IndexError Déclenchée si un index
parcourant une séquence
est en dehors des limites
KeyError Déclenchée si une clé
non existante est requise pour
un ensemble ou un dictionnaire
KeyboardInterrupt Déclenchée si l’utilisateur tape CTRL-C
NameError Déclenchée si une variable inexistente est utilisée
StopIteration Déclenchée si next(iterator)
est utilisé alors qu’il n’y a plus d’éléments
TypeError Déclenchée lorsqu’un
mauvais type de paramètre
est envoyé à une fonction
ValueError Déclenchée lorsqu’un
argument a une mauvaise valeur
ZeroDivisionError Déclenchée pour la division
par 0 (tout opérateur de division)
40CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
def sqrt(x):
if not isinstance(x, (int,float)):
raise TypeError(’x must be numeric’)
elif x<0:
raise ValueError(’x can not be negative’)
# le vrai travail se fait ici
def sum(values):
if not isinstance(values, collections.Iterable):
raise TypeError(’parameter must be an iterable type’)
total = 0
for v in values:
if not isinstance(v, (int, float)):
raise TypeError(’elements must be numeric’)
total = total +v
return total
collections.Iterable est une classe abstraite de Python qui inclu tous les conte-
neurs itérable de python. Une implémentation plus simple de la fonction sum
pourait prendre la forme suivante
def sum(values):
total = 0
for v in values:
total += v
return total
2.8. ITERATEUR ET GÉNÉRATEUR 41
if y != 0:
ratio = x/y
else:
# faire autre chose ici
Une seconde philosophie, souvent mise en oeuvre par les programmeurs Python
est que ” it is easier to ask for forgiveness than it is to get permission” ;
Grace Hopper (Il est plus facile de présenter des excuses que d’obtenir la
permission). En Python cette philosophie est implémenttée par l’usage de la
paire de mots try-except. Selon cette philosophie, le bout de code ci-dessus
peut être récrit de la manière suivante :
try:
ratio = x/y
except ZeroDivisionError:
# faire quelque chose d’autre ici
En Python il existe plusieurs types d’objets qui sont des iterable. Il s’agit
de list, tuple, set par exemple. Toutefois une chaine de caractère effectue des
itérations sur ces caractères. Des variables définies par les utilisateurs peuvent
également supporter les itérations. En Python le mécanisme des itérations est
basé sur les conventions suivantes :
— Un iterator est un objet qui peut effectuer une itération à travers une
série de valeurs. Si la variable i, identifie un iterator, alors chaque appel
de la fonction interne next avec l’argument i sous la forme next(i) pro-
duit un élément de la série avec une Exception StopIteration déclenchée
pour indiqué qu’il n’ y a plus d’éléments.
— Un iterable est un objet, obj, qui produit un iterator via la syntaxe
iter(obj).
42CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Partant de ces définitions une instance d’une liste est un iterable, mais pas
un iterator. Avec data = [1, 2, 3, 4] il n’est pas possible de faire un appel
next(data). Toutefois, il est possible il est possible de crée un iterator avec
i = iter(data). Il devient alors possible d’utilisser l’iterator de sorte que next(i)
retourne un élément de la liste. On peut créer plusieurs iterator à partir d’un
seul objet iterable.
2.8.1 Générateur
Il est possible de créer une classe dont les instances sont des itertor. Tou-
tefois la meilleure façon de créer des itertor en Python est de recourir à des
générateurs. Un générateur est implémenté avec une syntaxe très proche
de celle d’une fonction. Toutefois un générateur ne retourne pas de valeur, il
exécute plûtot une instruction yield pour indiquer chaque élément de la série.
A titre d’exemple, considérons que l’on souhaite trouver les facteurs d’un
entier positif. Le nombre a comme facteurs 1, 2, 3, 4, 5, 10, 20, 25, 50, 100.
Une fonction traditionnelle peut retourner la liste de tous les facteurs si elle est
implémentée comme ci-dessous :
Par contre une implémentation d’un générateur pour génér les facteurs se
présenterais comme ci-dessous :
def factors(n):
for k in range (1, n+1):
if n%k == 0
yield k
def factors(n):
k=1
while k*k < n:
if n%k == 0
yield k
yield n//k
k+=1
if k+k == m
2.9. QUELQUES AUTRES FONCTIONALITÉS DE PYTHON 43
result = [ ]
for value in iterable:
if condition:
result.append(expression)
Plus concrètement, la liste des carrés des nombres de 1 à n qui est [1, 2, 4, 9,
16, 25,..., n2 ] , peut être produite de manière traditionnelle comme ci-dessous :
squares=[ ]
for k in range(1, n+1):
squares.append(k*k)
Si nous recherchons les facteurs d’un entier. Par la comprehension nous avons
Expression Explication
data = 2, 4, 6, 8 affectation à la variable
data du tuple (2, 4, 6, 8)
return x,y Un seul objet contenant
le tuple (x,y) est retourné
a, b, c, d = range(7, 11) affectation à a, b, c,d des
valeurs 7, 8, 9, 10
a = 7, b = 8, c = 9, d = 10
quotient, remainder = divmod(a, b) retourne le couple de valeurs
(quotient = a//b, remainder = a%b)
for x, y in [ (7,2), (5,8), (6,4)] trois itérations
x = 7, y = 2; x = 5, y = 8; x = 6, y = 4
for k, v in mapping.items() : itération par key value
le tuple (x,y) est retourné
data = 2, 4, 6, 8 affectation à la variable
data du tuple (2, 4, 6, 8)
x, , y , z = 6, , 2 , 5 affectation collective
x = 6, y = 2, z = 5
j, k, = k, j un swap plus élaboré
autre fichier .py dans le même projet. Si le fichier count.py se trouve dans le
module utility.py on peut alors importer la fonction en utilisant from utility
import count. Il existe une construction spéciale qui permet de placer dans un
module des commandes qui seront exécutées directement lorsque le module est
invoqué comme un script, mais pas lorsque le module est invoqué à partir d’un
autre script. Ces instructions doivent être placées dans le corps d’une instruction
conditionnelle if.
if __name__ == ’__main__’:
Il est possible de démontrer que les séquences générées par cette technique
sont uniformes du point de vue statistique. Ce qui est bon pour la plupart des
applications. Puisque le prochain nombre d’un générateur de pseudo-nombres
aléatoires dépend des nombres précédement générés, un tel générateur a toujours
besoin d’un point de départ que l’on appelle seed.
La séquence de nombres générés pour un seed donné est toujours la même.
On conseille pour obtenir des séquences différentes chaquefois que le programme
tourne d’utiliser un seed qui sera différent pour chaque exécution du pro-
gramme. La classe Random est la classe utilisée pour les services relatifs aux
nombres aléatoires. Toutes les méthodes supportées par la classe Random sont
aussi supportées par les fonctions stand-alone du module random. Ci-dessous
quelques unes de ces fonctions :
Syntaxe Description
seed(hashable) Initialise le générateur
des pseudo-nombres aléatoires
random() Retourne un pseudo nombre aléatoire à
virgule flottante dans l’intervalle [0.0, 1.0]
randint(a,b) Retourne un pseudo nombre aléatoire
dans l’intervalle fermé [a, b]
randrange(start, stop, step) Retourne un pseudo nombre aléatoire
dans l’intervalle commençant à start et
se terminant à stop par pas de step
choice(seq) Retourne un élément d’une séquence
choisie de manière pseudo-aléatoire
shuffle(seq) Réordonne les éléments d’une
certaine séquence pseudo-aléatoire.
Robustesse
Tout bon programmeur veut écrire des programmes qui sont corrects. C’est-
à-dire que le programme produit une bonne sortie pour chaque entrée prévue.
En plus nous voulons que le programme soit robuste, il s’agit ici du fait qu’il
soit capable de géré des entrées non prévues. Par exemple lorsqu’un programme
attend un entier positif et qu’à la place, il reçoit comme entrée un entier négatif,
le programme doit pouvoir gérer correctement cette situation.
Plus important dans les applications où se présentent des situations de vie
où de mort, où une erreur dans un programme peut donné lieu à des blessures
où à des morts d’homme. Un logiciel qui n’est pas robuste pourrait être mortel.
Cette situation s’est présentée dans les accidents de Therac-5, une machine de
radio-thérapie qui a injecté une overdose de radiation à 6 patients entre 1985 et
1987, dont quelques uns sont décédés. Ces six accidents étaient tous dus à une
erreur dans un programme.
Adaptabilité
Les logiciels modernes tels que les navigateurs web et les moteurs de re-
cherche sur Internet impliquent des larges programmes qui sont utilisés durant
plusieurs années. Les logiciels doivent donc pouvoir évoluer pendant des nom-
breuses années au gré des demandes des utilisateurs. Ainsi une importante qua-
lité d’un logiciel est son adaptabilité, sa capacité à évoluer. A cela il faut ajouté
le concept de portabilité qui est son habilité à tourner sur différentes plate-
formes (Système d’exploitation par exemple) sans changement ou avec très peu
de modifications. Le langage Python assure la portabilité des programmes.
50CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Réutilisabilité
En parallèle avec l’adaptabilité nous avons la notion de réutilisabilité. Un
programme doit pouvoir être utilisé comme composant de différents systèmes
dans différentes applications. Il faut toutefois veiller à ce que cette volonté de
réutiliser du code ne nous conduise à des erreurs. D’aprés les enquêtes, les acci-
dents de Therac-c sont dus à la réutilisation du code.
Modularité
Les logiciels modernes consistent en plusieurs modules qui doivent interagir
correctement entre eux pour un fonctionnement harmonieux du logiciel. La mo-
dularité est un principe d’organisation selon lequel différentes composantes d’un
logiciel sont divisées en unités fonctionnelles. Un exemple de la modularité dans
le monde réel est celui d’une maison qui peut être regardée comme composée
de plusieurs unités fonctionnelles : électricité, chauffage, plomberie, et structure
(ou gros oeuvre). En Python nous avons les modules qui sont des collections
des fonctions liées les unes aux autres et des classes qui sont définies dans une
même unité de code à savoir un fichier.
Abstraction
La notion d’abstraction repose sur le fait de décomposé un système complexe
jusqu’à ses composants les plus fondamentaux. Partant de là on peut enfermer
certaines parties dans des blocs dont on assure la communication avec l’extérieur
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 51
par des interfaces simples. Typiquement décrire les parties d’un système re-
vient à les nommés, à expliquer leurs fonctionalités. Appliqué l’abstraction à
la conception des structures des données donne naissances aux structures des
données abstraites (ADT=Abstract Data Type). Un ADT spécifie ce que chaque
opération fait, mais pas comment il le fait. On voit apparaitre la notion d’inter-
face public d’ADT. Python s’appuie sur le mécanisme connu comme abstract
base class (ABC) pour utiliser les ADT. Un ABC ne peut pas être instancié,
mais il défini une ou plusieurs méthodes que toutes les implémentations de l’abs-
traction devront avoir. Un ABC peut consiter en une ou plusieurs classes.
Encapsulation
Un autre principe important de la conception orienté objet est l’encapsula-
tion. Ici il s’agit du fait que différents composants d’un software ne doivent pas
révéler les détails internes de leurs implémentations. Le plus important avantage
de l’encapsulation est qu’elle donne au programmeur la liberté d’implémenter
des détails sans devoir tenir compte du fait que d’autres programmeurs écriront
du code qui dépend de ces décisions internes. La seule contrainte pour le pro-
grammeur est d’offrir une interface publique appropriée par laquelle le bout de
code intergira avec le monde extérieur.
Design Patterns
La conception orienté objet facilite la réutilisabilité du code, sa robustesse
et son adaptabilité. Concevoir des bons programmes exige plus que la simple
compréhension de la méthodologie orienté objet. Elle requiert un usage efficace
des techniques de conception orienté objet. Les chercheurs en informatique, les
programmeurs les plus expérimentés ont développés une variété de concepts
organisationnels et de méthodologies pour la conception des logiciels orientés
objets de qualité, concis et réutilisables. Un ”design pattern” décrit la solu-
tion d’un problème logiciel typique. Un pattern fournit un modèle général (un
template) de solution qui peut être appliqué à différentes situations. Il décrit
les principaux éléments d’une solution d’une manière abstraite qui peut être
spécialisée pour chaque problème particulier éligible à l’usage du pattern. Dans
ce cours nous discuterons des design pattern algorithmiques suivants :
— La récursivité
— ”Amortization”
— Diviser pour régner
— ”Prune and search”
— La force brute
— Programmation dynamique
— La méthode gloutonne (”greddy method”)
De même nous aborderons les ”design pattern” logiciels suivants :
— Iterator
— Adapter
— Position
52CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
— Composition
— Template Method
— Locator
— Factory method
Conception (Design)
Pour le développement des logiciels orienté-objet, la conception est proba-
blement la phase la plus importante. En effet, c’est dans la phase de conception
que nous décidons de la façon d’organiser notre programme en classes, nous
décidons de la manière dont les classes vont interagir les unes avec les autres,
quelles données chacune d’elle va stocké et quelles actions elle réalisera. Il est un
fait que le défi le plus important que rencontrent les nouveaux programmeurs
est le fait de décider des classes qu’ils vont définir pour leur permettre de réaliser
leur travail. Il n’existe pas des règles générales en la matière. Toutefois, il existe
quelques règles empiriques qui peuvent être utilisées au moment de décider des
classes à utiliser et des relations qui existerons entre elles :
— Responsabilités : Organiser le travail autour de différents acteurs. Cha-
cun des acteurs aura une responsabilité différente. Essayer de décrire les
responsabilités en utilisant les verbes d’action. Ces acteurs seront les
classes du programme.
— Indépendance : S’assurer du fait que le travail d’une classe soit aussi
indépendant que possible du travail des autres classes. Répartir les res-
ponsabilités entre les classes de telle façon que chaque classe soit au-
tonome pour certains aspects du programme. Donner le contrôle des
données à la classe qui a compétence sur les actions qui nécessitent l’accès
à ces données.
— Comportement : Définir soigneusement et précisement le comporte-
ment de chaque classe et s’assurer ainsi que chaque action effectuée par
la classe est bien comprise par les classes qui interagissent avec elle. Ces
comportement vont définir les méthodes de cette classe. L’ensemble des
comportement d’une classe représente l’interface de communication de
cette classe avec le monde extérieur. C’est à travers cette interface que
les autres classes interagissent avec les objets de notre classe.
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 53
Concevoir les classes ainsi que leurs méthodes et données membres est le point
clé de la conception orienté objet. Un bon programmeur s’assurera de développer
des bonnes aptitudes en cette matière. Un outil commun pour développer une
conception initiale de haut niveau pour un projet est l’utilisation des cartes
CRC (Class-Responsability-Collaborator).
Pseudo-code
Comme étape intermédiaire entre la conception et l’implémmentation, les
programmeurs sont souvent appelés à décrire les algorithmes pour les besoins
de la compréhension des humains. De telles descriptions sont appelés des pseudo-
codes. Le pseudo-code n’est pas un programme, mais il est bien plus structuré
que la simple prose. C’est une mixture du langage naturel et des structures des
langages de programmation de haut-niveau.
Documentation
Python permet d’incorporé directement la documentation dans le code source
grâce au mécanisme de docstring. Formellent chaque litteral qui apparait
comme première instruction dans le corps d’un module , d’une classe ou d’une
fonction sera considéré comme un docstring. Par convention ces littéraux sont
délimités par (”””). Ci-dessous un exemple :
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 55
Une docstring est stockée comme une donnée membre du module, d’une fonc-
tion, ou d’une classe (ou elle est déclarée). Elle sert comme documentation et
peut être retrouvée de plusieurs manières. Exemple help(x) produit la docu-
mentation associée à l’objet x. Un outil externe nommé pydoc est distribué avec
Python et peut être utilisé pour généré la documentation formelle. Un tutorial
pour produire des docstring intéressants est disponible sur :
http ://www.python.org/dev/peps/pep-0257/.
Tests et Débogage
Les tests représentent le processus de vérification expérimentale de l’exacti-
tude d’un programme. Le debogage consiste quant à lui au processus de suivi de
l’exécution d’un programme en vue d’éliminer les erreurs qui y subsisteraient.
Les tests et le débogage sont deux activités très consomatrices de temps dans le
processus de développement d’un programme.
Tests
Un bon plan de tests est essentiel dans le développement d’un programme.
Nous savons très bien que la vérification de l’exactitude d’un programme à
l’aide de toutes les entrées possibles n’est pas réalisable. Nous devons donc nous
contenter d’exécuter le programme avec les entrées représentatives. Nous devons
au minimum nous assurer que chaque méthode d’une classe est testée au moins
une fois. Mieux encore un plan de test qui s’assure que chaque bout de code
du programme est testé au moins une fois est plus approprié. En général, les
programmes ne donnent pas les bons résultats pour les entrées spéciales. Des
tels cas doivent toujours être identifiés et testés. Par exemple lorsqu’il faut tester
une méthode qui trie une séquence d’entiers, nous devons prendre en compte les
entrées suivantes :
56CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
if __name__ == ’__main__’:
\# perform tests
Ces tests seront exécutés lorsque Python est directement invoqué sur ce module
et non lorsque le module est importé dans un autre module ou fonction. Le
module unittest de Python offre des mécanismes plus automatisés de tests. Ce
cadre permet de grouper des test des cas individuels dans des grandes suites de
tests et offre de l’aide pour l’exécution de ces suites et l’analyse des résultats
des tests. Dans le processus de la maintenance d’un logiciel, la regression test
est utilisée. En fait tous les anciens tests sont ré-exécutés pour s’assurer que les
changements apportés aux logiciels n’ont pas introduit des nouvelles erreurs.
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 57
Débogage
La plus simple technique de débogage est l’utilisation de l’instruction print.
Elle permet de suivre les valeurs de variables au cours de l’exécution du pro-
gramme. Le problème avec cette façon de faire est qu’après les tests, il faut
retirer les print ou les transformer en commentaires.
Une meilleure approche est d’utiliser un débogueur, qui est un environne-
ment spécialisé pour contrôler et suivre l’exécution d’un programme. La fonc-
tionalité de base qu’offre un débogueur est la mise en place dans le programme
des ”breakpoints” dans le code. Lorsque le programme est exécuté dans un
débogueur, il s’arrete à chaque ”breakpoint” permettant l’examen des valeurs
des variables à cet instant.
La distribution standard de Python inclu le module pdb qui offre les fonc-
tionalités du débogueur dans l’interpréteur. La plupart des IDEs dont IDLE
offrent un débogueur sous environnement graphique.
La variable self
En Python, la variable self joue un rôle clé. Dans le contexte de la classe
CreditCard, il peut y avoir plusieurs instances de CreditCard. Chaque instance
maintient sa balance, sa limite de crédit, et ainsi de suite. Du point de vue de la
syntaxe self identifie l’instance sur laquelle la méthode est invoquée. Supposons
par exemple, que l’utilisateur de la classe CreditCard a une variable my card
qui identifie une instance de la classe CreditCard. Lorsque l’utilisateur fait l’ap-
pel my card.get balance(), la variable self, dans la définition de la méthode
58CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
get balance, se réfère à la carte de crédit connue comme étant my card pour
l’appelant. L’expression, self. balance fait référence à la variable balance
stockée comme membre de cette carte de crédit particulière. Ci-dessous le code :
class CreditCard:
"""A consumer credit card."""
def __init__(self, customer, bank, acnt, limit):
"""Create a new credit card instance.
The initial balance is zero.
customer the name of the customer (e.g. Etina Jeanne)
bank the name of the bank (e.g. ’EquityBCDC’)
acnt the account identifier (e.g. ’5391 0375 9387 5309’)
limit credit limit (measured in dollars)
"""
self._customer = customer
self._bank =bank
self._account = acnt
self._limit = limit
self._balance = 0
def get_customer(self):
"""Return the name of the customer."""
return self._customer
def get_bank(self):
"""Return the bank’s name."""
return self._bank
def get_account(self):
"""Return the card identifying number (typically stored as a string)."""
return self._account
def get_limit(self):
"""Return current limit."""
return self._limit
def get_balance(self):
"""Return current balance."""
return self._balance
return False
else:
self._balance += price
return True
if __name__ == ’__main__’:
wallet = []
wallet.append(CreditCard(’Etina Jeanne’,’EquityBCDC’,’5391 0375 9387 5309,2500))
wallet.append(CreditCard(’Etina Jeanne’,’RawBank’, ’3485 0399 3387 5309, 3500))
wallet.append(CreditCard(’Etina Jeanne’,’TMB’, ’5391 0375 9387 5309, 2500))
wallet.append(CreditCard(’Etina Jeanne’,’FBNBank’, ’5391 0375 9387 5309, 5000))
for c in range(3):
print(’Customer = ’, wallet[c].get_customer())
print(’Bank = ’, wallet[c].get_bank())
print(’Account = ’, wallet[c].get_account())
print(’Limit = ’, wallet[c].get_limit())
print(’Balance = ’, wallet[c].get_balance())
while wallet[c].get_balance() > 100:
wallet[c].make_payment(100)
print(’New balance =’, wallet[c].get_balance())
print
Observons maintenant la différence entre les signatures des méthodes au mo-
ment de leur définition et au moment de leur utilisation par un appelant. Par
exemple, du point de vue d’un utilisateur la méthode get. balance est appelée
sans arguments. Au moment de la définition de la méthode dans la classe self
est un argument explicite. La méthode charge est définie dans la classe avec
comme arguments self et price, par contre au moment de l’appel, la méthode
ne reçoit qu’un seul argument my card.charge(200). L’interpréteur lie auto-
matiquement l’instance sur laquelle la méthode est appelée à l’argument self.
Le constructeur
Un utilisateur peut créer une instance de la classe CreditCard en utilisant
la syntaxe :
cc = CreditCard(’Bolumbu Jean’,’RawBank’,’5391 0375 9387 5309’,1000)
60CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Dans la dernière partie du code nous avons mis en oeuvre la classe Cre-
ditCard en insérant trois cartes de crédit dans une liste nommée wallet.
Nous avons utilisé quelques boucles pour charger les cartes de crédit, effectuer
quelques paiements et imprimer les résultats de nos opérations dans la console.
Les tests sont le body de l’instruction : if name == ’ main ’. Cette
façons de faire permet d’inclure les tests dans le fichier qui contient le code de
la classe.
Encapsulation
Les classes internes de Python offrent une sématique pour plusieurs opérateurs.
Par exemple a+b invoque l’addition pour les types numériques, et la concaténation
pour les types séquences. Au moment de la définition d’une nouvelle classe, nous
devons envisager si la syntaxe a+b sera définie si a ou b est une instance de la
classe.
Par défaut, l’opérateur + n’est pas défini pour une nouvelle classe. Toutefois,
il appartient à l’auteur de la classe de fournir une définition de + en utilisant
la technique de surcharge des opérateurs. Ceci est fait par l’implémentation
d’une méthode spécialement nommée. L’opérateur + est surchargée par l’implé-
mentation de la méthode add qui reçoit l’opérande de droite comme ar-
gument. Ainsi l’opération a+b est convertie en un appel sur l’objet a de la
méthode add () à laquelle on passe b comme argument. Soit a. add (b).
Des méthodes similaires existent pour les autres opérateurs. Le tableau ... donne
une liste de ces fonctions.
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 61
def __len__(self):
"""Return the dimension of the vector."""
return len(self._coords)
def __str__(self):
"""Produce string representation of vector."""
return ’<’ +str(self.coords)[1:-1]+’>’ # adapt list representation
64CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Itérateur
L’itération est un concept important dans la conception des structures des
données. Un iterator pour une collection supporte la méthode next qui
retourne le prochain élément de la collection et déclenche une erreur StopI-
teration exception lorsqu’il n’y a plus d’éléments. Dans les faits il est rare
que l’on implémente directement une classe iterator. L’approche généralement
préférée est l’usage de la syntaxe d’un générateur qui produit un iterator.
Python aide également en fournissant une implémentation automatique d’un
iterator pour toute classe qui implémente len et getitem . Le code ci-
dessous montre l’implémentation d’un itérateur de bas-niveau qui fonctionne sur
toute collection qui supporte len et getitem . Cette classe peut être ins-
tantiée par l’instruction SequenceIterator(data). Elle conserve une référence
interne de la séquence data, aussi bien que l’indice actuel dans la séquence.
Chaquefois que next est appelé, l’indice est incrémenté jusqu’à ce que l’on
atteigne la fin de la séquence.
class SequenceIterator:
"""An iterator for any Python’s s\’equence type"""
def __init__(self, sequence):
"""Create an iterator for the given sequence."""
self._sequence # keep a reference to the underlying data
self._k = -1 # sera incr\’ement\’e \‘a 0 au premier appel
# de next
def __next__(self):
"""Return the next element, or raise StopIteration error."""
self._k +=1 # advance to next index
if self._k < len(self._seq):
return(self._seq[self._k]) # return the data element
else:
raise StopIteration() # there are no more elements
def __iter__(self):
""" By convention, an iterator must return itself as
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 65
an iterator"""
return self
2.12.4 Héritage
Une façon naturelle d’organiser structurellement les composants d’un logiciel
est la hiérachisation des classes. Les classes sont organisées des plus spécifiques
aux plus génerales ou de la plus générale aux plus spécifiques. Dans le cas des
batiments on peut avoir la hiérarchie reprise dans la figure 2.12.
Classes abstraites
Lorsque l’on définit un groupe de classes comme faisant partie d’une hiéarchie
des classes basées sur l’héritage afin d’éviter la répétition du code, une des
techniques est de concevoir une classe de base contenant toutes les fonctionalités
pouvant être héritées. Un exemple est la hiérarchie des progressions numériques
décrite dans la section précédente. La classe Progression sert de classe de base
pour trois sous-classes : ArithmeticProgression, GeometricProgression,
et FibonacciProgression.
Quoiqu’il soit possible de créer une instance de la classe de base Progres-
sion, il y a peu d’intérêt à faire cela dans la mesure où cette instance est en fait
une instance d’une progression arithmétrique de raison 1. Le vrai objectif de
la classe Progression est de centralisé les implémentations des comportements
dont les autres progressions ont besoin, ce qui réduit le code relegué dans les
sous-classes.
En terminologie orientée objet, nous disons qu’une classe est une classe abs-
traite de base si sa seule utilité est de servir comme une classe de base dans
la hiéarchie d’héritage. Plus formellement, une classe abstraite de base est une
classe qui ne peut pas être instanciée, alors qu’une classe concrète est une classe
qui peut être instantiée. De part ces définitions, notre classe Progression est
techniquement une classe concrète même si nous l’avons conçue essentiellement
comme une classe abstraite de base. En Java et en C++, une classe abstraite
de base sert comme type permettant la définition des méthodes abstraites. Cela
permet la mise en oeuvre du polymorphisme.
C’est par ce mécanisme qu’une variable peut avoir comme type une classe
abstraite de base même si elle se réfère à une instance d’une sous-classe concrète.
Comme il n’y a pas de déclaration de type en python, ce type de polymorphisme
peut être mis en oeuvre sans qu’on ait absolument besoin d’une classe abstraite
de base. Pour cette raison il n’est pas très habituel de déclaré des classes abs-
traites de base en Python même si le language de programmation dispose des
mécanismes nécessaires pour cela.
La raison pour laquelle nous insistons sur la définition des classes abstraites
de base en Python est le fait que dans l’étude des structures des données abs-
traites le module collection de Python offre des nombreuses classes abstraites
68CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
de base qui offrent des services au moment de la définition des structures des
données qui partagent une interface commune avec quelques unes des struc-
tures des données internes de Python. Ceci s’appuie sur le pattern orienté objet
connu sous l’appellation de Template method pattern. Le pattern ”Tem-
plate method pattern” consiste dans le fait qu’une classe abstraite de base offre
des comportements concrèts qui s’appuient sur l’appel d’autres comportements
abstraits.
Comme exemple concrèt la classe abstraite de base collections.Sequence
définit des comportements communs aux classes list, str, et tuple comme des
séquences qui permettent l’accès d’un élément par le biais d’un indice entier. La
classe collections.Sequence offre des implémentations concrètes des méthodes
count, index, et contains qui peuvent être héritées par n’importe quelle
classe qui offre des implémentations concrètes de len et getitem . Pour
des fins d’illustration, nous donnons un exemple d’implémentation de la classe
abstraite de base Sequence dans le bout de code ci-dessous :
class Sequence(metaclass=ABCMeta):
"""Our own version of collections.s\’equence abstract
base class."""
@abstractmethod
def __len__ (self):
"""Return the length of the sequence."""
@abstractmethod
def __getitem__ (self, j):
"""Return the element at index j of the sequence."""
Figure 2.15 – Une vue conceptuelle de trois espaces des noms : (a) Espace
de noms de la classe CreditCard ; (b) Espace des noms de la classe Preda-
toryCreditCard ; (c) Espace des noms d’un instance (un objet) de la classe
PredatoryCreditCard
nous avons : init , get customer, get bank, get balance, get limit, charge
et make payment.
Classes imbriquées
Il est possible de définir une classe à l’intérieur d’une autre classe. Il s’agit
d’une construction interéssante à exploiter de temps en temps. Exemple :
une séquence de chaines de caractères devant servir de noms pour les variables
d’instances.
palette = warmtones
Cela aurait fait de palette un alias de warmtones comme on peut le voir sur la
figure 3.17.
Malheureusement cette façon de faire ne résout pas notre problème. Chaque
modification que nous effectuons sur la liste palette est automatiquement répercutée
à la liste warmtones. Nous pouvons par contre créer une nouvelle instance de la
classe couleurs par l’instruction suivante :
palette = list(warmtones)
72CHAPITRE 2. RAPPELS SUR LA PROGRAMMATION ET LE LANGAGE PYTHON
Figure 2.16 – Deux alias pour une même liste de couleurs. palette n’est pas
une copie de warmtones.
Cette deuxième technique fait appel au constructeur de la classe list et lui passe
la liste warmtones comme argument. Une nouvelle instance de la classe liste est
alors crée et affactée à la variable palette comme on peut le voir sur la figure
3.18. Ceci est ce que l’on appelle une copie superficielle. Une nouvelle liste a
été crée. Cette liste a été initialisée avec les mêmes couleurs que la première
liste. Compte tenu de la manière dont Python gère les listes, la nouvelle liste
est une série de références vers les mêmes éléments que ceux de la primière
liste. La situation est certes meilleure que celle avec les alias dans la mesure
ou l’on peut ajouter ou retirer des nouvelles couleurs à palette sans modifier
warmtones, mais si nous modifions une couleur de palette, nous changeons alors
le contenu de warmtones. Malgré le fait que palette et warmtones soient des
2.12. PROGRAMMATION ORIENTÉE OBJET EN PYTHON 73
palette = copy.deepcopy(warmtones)
Introduction à
l’algorithmique
3.1 Introduction
De manière informelle un algorithme se définit comme étant une procédure de
calcul qui prend en entrée une valeur, ou un ensemble de valeurs, et qui donne
en sortie une valeur ou un ensemble de valeurs. Un algorithme est donc une
séquence d’étapes de calcul qui transforme une entrée en une sortie.
Pour introduire les choses nous allons considérer le problème du tri. Il pour-
rait être posé de la manière suivante :
75
76 CHAPITRE 3. INTRODUCTION À L’ALGORITHMIQUE
Un algorithme est dit correct si, pour chaque instance en entrée, il se termine
en produisant la bonne sortie. Un algorithme correct résout le problème donné.
Un algorithme incorrect risque de ne pas se terminer pour certaines instances
en entrée, voire de se terminer sur une réponse autre que celle désirée. Dans
certains cas, un algorithme incorrect peut s’avérer utile.
Quels sont les types de problèmes susceptibles d’ être résolus par des
algorithmes
Le tri n’est pas l’unique problème pour lequel ont été développé des al-
gorithmes. Les applications concrètes des algorithmes sont innombrables et
couvrent tous les dommaines des problèmes solubles par ordinateur. Entre autres :
Le projet du génome humain ; l’identification des 100 000 gènes de l’ADN ;
la détermination des séquences de 3 milliards de paires de bases chimiques
qui constituent l’ADN ; le stockage de ces informations, etc. ;
les algorithmes de routage très utilisés sur Internet ;
le commerce électronique ;
L’industrie et le commerce ;
Cette énumération est loin d’être exhaustive, mais témoigne de deux caractéristiques
que l’on retrouve dans bon nombre de problèmes algorithmiques intéressants :
1. Il existe beaucoup de solutions candidates, mais la plupart d’entre elles
ne résolvent pas le problème. Trouver une solution qui convienne, ou une
qui est la meilleure, voilà qui n’est pas toujours évident ;
2. Ils ont des applications concrètes. Exemple dans le cas de la recherche
du chemin le plus court on peut donner l’exemple de la recherche d’un
itinéraire sur le web, ou celle de l’utilisation d’un GPS pour trouver son
chemin. En tout cas, le temps machine comme la mémoire des ordinateurs
restent des ressources à gerer avec parcimonie et les algorithmes perfor-
mants en termes de durée et d’encombrement restent un outil essentiel
dans cette gestion.
Si les ordinateurs étaient infinement rapides et leurs mémoires gratuites, on ne
se poserait pas la question de la nécessité des algorithmes. Toutefois il est clair
qu’en toutes circonstances les algorithmes sont importants ne serait ce que pour
montrer que la solution ne boucle pas indéfinement et qu’elle se termine avec la
bonne réponse.
f (n) = c (3.1)
pour b > 1.
Cette fonction est définie par :
x = logb n ⇔ bx = n (3.4)
f (n) = n (3.12)
Cette fonction apparait dans les algorithmes chaquefois que nous devons réaliser
une simple opération pour chacun de n éléments d’un ensemble. Par exemple
comparer un nombre x à chaque élément d’un tableau de n éléments exigera n
comparaisons.
f (n) = n2 (3.14)
La raison principale pour laquelle la fonction quadratique apparait dans l’ana-
lyse des algorithmes est due au fait qu’il y a plusieurs algorithmes qui exploitent
des boucles imbriquées. Soit n opérations pour la boucle interne, n . n opérations
pour la boucle externe, ce qui fait n2 opérations en tout.
1 + 2 + 3 + ... + (n − 2) + (n − 1) + n (3.15)
n(n + 1)
1 + 2 + 3 + 4 + ... + (n − 2) + (n − 1) + n = (3.16)
2
Cette fonction est une fonction quadratique car pour n grand assez on peut
négliger 1/2 par rapport à n/2
f (n) = n3 (3.17)
Cette fonction apparait moins souvent que les fonctions constante, linéaire et
quadratique mentionnées précédement. Toutefois, elle apparait de temps en
temps.
Toutes les fonctions que nous avons étudié précédement peuvent être considérées
comme faisant partie de la classe des fonctions polynomiales. Une fonction po-
lynomiale est une fonction de la forme
f (n) = n2 + 5n + 2 (3.19)
f (n) = n3 + 1 (3.20)
f (n) = 1 (3.21)
f (n) = n (3.22)
2
f (n) = n (3.23)
(3.24)
Un polynome f (n) de degré d dont les coefficients sont a0 ...ad peut être écrit
sous la forme ci-dessous :
Xd
f (n) = ai ni (3.31)
i=0
3.3. EFFICACITÉ D’UN ALGORITHME 81
En plus, même une application qui à première vue n’emploie pas d’algorithmes
s’appuie, souvent indirectement, sur une foule d’algorithmes. En effet, l’appli-
cation tourne sur du matériel performant dont la conception est basée sur des
algorithmes. L’application possède une interface graphique utilisateur dont la
conception repose sur de puissants algorithmes. L’application exploite le réseau
et le routage s’appuie fondamentalement sur des algorithmes. Les algorithmes
sont donc au coeur de la plupart des technologies employées dans les ordina-
teurs modernes. Posséder une base solide en algorthmique ou pas fait souvent
la différence entre les programmeurs.
Pour être capable d’identifier les bons algorithmes et les bonnes structures
des données, nous devons disposer de bonnes méthodes d’analyse.
Toutes autres choses restant égales, le temps d’exécution sera plus petit pour
un processeur plus rapide ou si l’implémentation est faite dans un programme
compilé (de façon optimal) plûtot qu’exécuter sur un interpréteur, ou une ma-
chine virtuelle.
Le pseudo-code
Le pseudo-code appelé également Langage de Description d’Algorithmes
(LDA) est une façon de décrire un algorithme sans référence à un langage de
programmation particulier. L’écriture du pseudo-code permet souvent de bien
prendre toute la mesure de la difficulté de la mise en oeuvre de l’algorithme et de
développer une démarche structurée dans la conception de celui-ci. Le pseudo-
code est destiné aux humains, même s’il existe des langages de spécifications qui
au départ du pseudo-code et des diagrammes de toutes sortes peuvent générer
du code.
Le tri par insertion est un algorithme de tri qui pourrait être expliqué en
partant de la manière dont les gens tiennent les cartes à jouer. Au départ la main
gauche du joueur est vide et ses cartes sont posées sur la table. Il prend alors
les cartes de la table une par une, pour les placer dans la main gauche. Pour
savoir ou placer une carte, le joueur la compare avec chacune des cartes déjà
présentes dans sa main gauche en examinant les cartes de droite vers la gauche.
Ci-dessous un exemple de pseudo-code pour l’algorithme du tri par insertion.
1 Pour j=2 à A.longueur
2 clé = A[j]
3 // Insère A[j] dans la séquence triée A[1, ..j-1]
4 i=j−1
5 tant que i > 0 et A[i] >clé
6 A[i + 1] = A[i]
7 i=i−1
8 A[i + 1] =clé
Un organigramme
Un organigramme, également appelé algorigramme, logigramme ou ordi-
nogramme est une représentation graphique normalisée des opérations et des
décisions effectuées par un ordinateur. La norme ISO 5807 décrit en détails les
différents symboles à utiliser pour représenter un programme informatique de
manière normalisée.
3.5. CONCEPTION ET ANALYSE DES ALGORITHMES 85
Figure 3.2 – Mécanisme du tri par insertion. Les cartes sont prises de la table
puis comparées à celles déjà dans la main gauche en partant de la droite vers la
gauche. On s’arrête lorsqu’on a trouvé la position de la carte en fonction de la
relation d’ordre spécifiée par le tri.
def insertionSort(liste):
for index in range(len(liste)): # on parcourt la liste
item = liste[index] # un element de la liste
j=index
while j>0 and liste[j-1]>item:
liste[j] = liste[j-1]
j=j-1
liste[j] = item # on insere item en place
# Nous allons maintenant tester la fonction
if __name__ == ’__main__’:
myliste = [10, 5, 9, 2, 7, 1]
insertionSort(myliste)
print(myliste)
# On obtient comme resultat l’impression de la liste triee.
[1, 2, 5, 7, 9, 10]
Cette méthodologie associe à chaque algorithme étudier une fonction f(n), qui
caractérise son temps d’exécution en fonction de la taille n de l’entrée. Cette
fonction s’obtient par un fitting approprié des données expérimentales.
Ci-dessous le code Python utilisé pour produire les résultats du tableau (voir
exercice) :
Figure 3.5 – Le temp d’exécution de l’algorithme est situé entre une borne
inférieure et une borne supérieure
Une analyse basée sur le cas moyen exige que nous puissions évaluer les temps
d’exécution d’une distribution d’entrées, ce qui implique des calculs probabilis-
tiques compliqués. Bien souvent, on base l’analyse sur l’étude du plus mauvais
cas (tous les autres cas sont meilleurs que celui là). L’analyse basée sur le plus
mauvais cas exige que l’on puisse identifier l’entrée correspondant au plus mau-
vais cas et cela est souvent simple à faire. Cette approche mène le plus souvent
au meilleur algorithme.
Sur le plan théorique, nous trions une suite, mais l’entrée se présente sous la
forme de n éléments. Le tri par insertion s’inspire de la manière dont la plupart
des gens tiennent des cartes à jouer. Au début la main gauche du joueur est
vide et ses cartes sont posées sur la table (voir figure 3.4). Il prend alors des
cartes sur la table une par une, pour les placer dans la main gauche. Pour savoir
oú placer une carte dans son jeu, le joueur la compare avec chacune des cartes
déjà présente dans sa main gauche, en examinant les cartes de la droite vers la
gauche. Ainsi à tout moment les cartes qu’il tient à la main gauche sont triées.
En pseudo-code l’algorithme que nous venons de décrire peut se résumé comme
ci-dessous :
3.5. CONCEPTION ET ANALYSE DES ALGORITHMES 91
Même pour des entrées ayant la même taille, le temps d’exécution d’un al-
gorithme peut dépendre de l’entrée particulière ayant cette taille. Dans le cas
du tri par insertion, le cas le plus favorable est celui ou le tableau en entrée est
déjà trié. Dans ce cas pour j = 2,3, ....n, on trouve que A[j] ≤ clé en ligne 5
quand i prend sa valeur initiale de j-1. Donc tj = 1 pour j = 2,3, ...n. Le temps
d’exécution associé à ce cas optimal est donc :
T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5 (n − 1) + c8 (n − 1) (3.33)
= (c1 + c2 + c4 + c5 + c8 )n − (c2 + c4 + c5 + c8 ) (3.34)
Si le tableau est trié dans l’ordre décroissant alors c’est le cas le plus défavorable.
On doit comparer chaque élément A[j] avec chaque élément du sous tableau trié
A[1..j − 1], et donc tj = j pour j = 2,3,...,n. Si l’on se souvient du fait que :
n
X n(n + 1)
j = −1 (3.35)
j=2
2
3.5. CONCEPTION ET ANALYSE DES ALGORITHMES 93
n
X n(n − 1)
(j − 1) = (3.36)
j=2
2
Alors le temps d’exécution pour le tri par insertion dans le cas le plus défavorable
est donné par :
n(n + 1) n(n − 1)
T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5 ( − 1) + c6 ( )+
2 2
n(n − 1)
c7 ( ) + c8 (n − 1)
2
c5 c6 c7
= ( + + )n2 + (c1 + c2 + c4 +
2 2 2
c5 c6 c7
− − + c8 )n − (c2 + c4 + c5 + c8 )
2 2 2
Ordre de grandeur
Nous avons utilisé des hypothèses simplificatrices pour faciliter notre analyse
de la procédure tri-insertion. D’abord nous avons ignoré le coût réel de chaque
instruction en employant des constantes ci pour représenter ces coûts. Ensuite,
94 CHAPITRE 3. INTRODUCTION À L’ALGORITHMIQUE
nous avons observé que même ces constantes nous donnent plus de détails que
nécessaires : le temps d’exécution du cas le plus défavorable est an2 + bn + c,
a, b, c étant des constantes qui dépendent des coûts ci des instructions. Nous
avons non seulement ignoré les coûts réels des instructions, mais aussi les coûts
abstraits ci .
Dans le cas du tri par insertion par exemple, le temps d’exécution est une
fonction quadratique de la taille n de l’entrée pour le cas le plus défavorable.
Notation Θ
Notation O
Propriété :
Notation Ω
Notation o
Notation w
La Récursivité
4.1 Introduction
Une façon de réaliser une répétition dans un ordinateur est de recourir à
une boucle. En Python nous avons les boucles while et for. Une autre manière
totalement différente de réaliser une répétition est de recourir à la récursivité.
La récursivité est un processus par lequel une fonction s’appelle elle même
au cours de son exécution. Ce processus se manifiste également lorsque pour
construire une structure de données on commence par la décomposer, une ou
plusieurs fois, en structures identiques à la structure de départ, mais plus fa-
ciles à manipuler. Ensuite on résout le problème pour les petites structures
avant d’obtenir la solution du problème pour la structure de départ en combi-
nant les solutions obtenues pour les petites structures. Ainsi en informatique la
récursivité nous offre une manière élégante d’effectuer des tâches répétitives. La
récursivité est une technique importante pour l’étude des structures des données.
Dans ce chapitre, nous allons illustré la recursivité par quelques exemples :
— La fonction factorielle
— La recherche binaire
— Le Système de fichiers
Nous étudierons ensuite comment effectuer une analyse formelle du temps d’exécution
d’un algorithme récursif avant d’examiner certains pièges potentiels lors de la
mise en oeuvre des processus récursifs.
99
100 CHAPITRE 4. LA RÉCURSIVITÉ
0 1 2 3 4 5 6 7 8 9 10 11 12 13 Indice
2 4 5 7 8 9 12 14 17 19 22 25 27 28 Valeur
4.2. QUELQUES EXEMPLES ILLUSTRANT LA RÉCURSIVITÉ 101
"""
if low > high:
return false
else:
mid = (low+high)//2
if target == data[mid]:
return True
elif target < data[mid]:
# recur on the portion left of the midle
return binary_search(data,target, low, mid-1)
else:
# recur on the portion right of the middle
return binary_search(data, target, mid+1, high)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37
↑ ↑ ↑
l m h
2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37
↑ ↑ ↑
l m h
2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37
↑ ↑ ↑
l m h
2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37
↑
l=
m=
h
La première ligne de ce tableau nous donne les indices des différents éléments
du tableau. l=low ; m=mid ; h=high. Les flêches vers le haut ainsi que l, m, et
h nous permettent de suivre la recherche de l’élément 22 au fur et à mesure des
appels récursifs de la fonction.
4.2. QUELQUES EXEMPLES ILLUSTRANT LA RÉCURSIVITÉ 103
Figure 4.2 – Une vue du système de fichier avec l’espace occupé par chaque
répertoire ou fichier. Dans le bloc représentant un élément est mentionné l’espace
disque qu’il occupe. Au-dessus de chaque bloc est mentionné l’espace occupé par
ce bloc et tous les répertoires et fichiers qu’il contient.
Pour obtenir une implémentation de notre algorithme en python nous allos nous
appuyer sur le module os de Python. Ce module est une grande librairie, mais
nous allons utiliser seulement les fonctions suivantes :
— os.path.getsize(path). Retourne l’espace occupé par le fichier ou le
répertoire identifé par la chaine de caractère path en byte (example
/usr/rt/courses).
— os.path.isdir(path). Retourne True si l’entrée désignée par path est un
répertoire ; et False dans le cas contraire.
— os.listdir(path). Retourne la liste de chaines de caractères qui sont les
4.3. ANALYSE D’UN ALGORITHME RÉCURSIF 105
import os
def disk_usage(path):
""" Return the number of bytes used by
the file or the folder and descandants
"""
total = os.path.getsize(path)
# account for direct usage
if os.path.isdir(path):
# if this is a directory
for filename in os.listdir(path):
# then for each child
childpath = os.path.join(path, filename)
total += disk_usage(childpath)
# appel r\’ecursif
print(’{0:7}.format(total), path)
return total
Ce qui implique que le temps d’exécution de binary search() est O(log n).
4.4. QUELQUES AUTRES EXEMPLES DE RÉCURSIVITÉ 107
def bad_fibonacci(n):
""" Return nth Fibonacci number """
if n<=1:
return n
else:
return bad_fibonacci(n-2)+bad_fibonacci(n-1)
c0 = 1
c1 = 1
c2 = 1 + c0 + c1 =1+1+1=3
c3 = 1 + c1 + c2 =1+1+3=5
c4 = 1 + c2 + c3 =1+3+5=9
c5 = 1 + c3 + c4 = 1 + 5 + 9 = 15
c6 = 1 + c4 + c5 = 1 + 9 + 15 = 25
c7 = 1 + c6 + c7 = 1 + 15 + 25 = 41
c8 = 1 + c8 + c9 = 1 + 25 + 41 = 67
def good_fibonacci(n):
4.4. QUELQUES AUTRES EXEMPLES DE RÉCURSIVITÉ 109
Pour analyser la somme binaire, nous considérons par souci de simplicité le cas
ou n est une puissance de deux. La figure 4.3 montre la trace de récursivité
d’une exécution de la somme de 0 à 8. Nous avons étiqueté chaque cas avec les
valeurs des paramètres start :stop pour cet appel de la fonction. La taille de
la plage est divisée en deux à chaque appel récursif et donc la profondeur de
la récursivité est 1 + [log2 n] par conséquent, la somme binaire utilise O(log n).
Toutefois, comme il y a (2n − 1) appels de la fonction chacun nécessitant un
temps constant, le temps d’exécution de binary sum est O(n).
110 CHAPITRE 4. LA RÉCURSIVITÉ
5.1 Introduction
Dans ce chapitre, nous explorons les différentes classes de séquences de Py-
thon, à savoir : les classes list, tuple et str. Il existe des points communs im-
portants entre ces classes : chacune prend en charge l’indexation pour accéder à
un élément individuel de la séquence, en utilisant une syntaxe telle que seq[k].
Chacune utilise un concept de bas niveau connu sous l’appellation de tableau
pour représenter la séquence. Cependant, il existe des différences significatives
entre les abstractions que représentent ces classes d’une part, et la manière dont
les instances de ces classes sont représentées en interne par Python d’autre part.
Parce que ces classes sont utilisées largement dans les programmes Py-
thon, et parce qu’elles deviendront des blocs de construction sur lesquelles nous
développerons des structures de données plus complexes, il est impératif que
nous ayons une compréhension claire à la fois du comportement public et du
fonctionnement interne de ces classes (voir figure 5.1).
111
112 CHAPITRE 5. LES SÉQUENCES BASÉES SUR LES TABLEAUX
Figure 5.3 – Une chaı̂ne de caractères Python inscrite dans la mémoire sous
forme d’un tableau de caractères. Nous avons supposé que chaque caractère
unicode de la chaı̂ne nécessite deux octets de mémoire. Les nombres sous les
entrées sont les indices des éléments du tableau.
que ces lits sont numérotés de 0 à 199, nous pourrions envisager d’utiliser une
structure basée sur un tableau pour maintenir les noms des patients actuelle-
ment affectés à ces lits. Par exemple, en Python nous pourrions utiliser une liste
de noms, tels que :
Pour représenter une telle liste avec un tableau, Python doit respecter l’exi-
gence selon laquelle chaque cellule du tableau utilise le même nombre d’octets.
Pourtant les éléments sont des chaı̂nes de caractères, et les chaı̂nes de caractères
ont naturellement des longueurs différentes. Python pourrait tenter de réserver
suffisamment d’espace pour chaque cellule pour contenir la chaı̂ne de longueur
maximale (pas seulement les chaı̂nes actuellement stockées, mais de n’importe
quelle chaı̂ne que nous pourrions vouloir stocker), mais ce serait du gaspillage.
Au lieu de cela, Python représente une liste ou une instance de tuple utilisant
comme stockage interne le mécanisme d’un tableau de références d’objets. Au
niveau le plus bas, ce qui est stocké est une séquence consécutive d’adresses
mémoire dans lesquelles les éléments de la séquence résident. Un diagramme de
haut niveau d’une telle liste est illustré à la figure 5.5.
Figure 5.5 – Un tableau stockant des références vers des chaines de caractères.
Bien que la taille relative des éléments individuels puisse varier, le nombre
de bits utilisés pour stocker l’adresse mémoire de chaque élément est fixe (par
116 CHAPITRE 5. LES SÉQUENCES BASÉES SUR LES TABLEAUX
exemple, 64 bits par adresse). De cette façon, Python peut prendre en charge
en un temps constant l’accès à une liste ou à un élément d’un tuple en fonction
de son indice. Dans la figure 5.5, nous caractérisons une liste de chaı̂nes qui
sont les noms des patients dans un hôpital. Il est plus probable qu’un système
d’information médicale gère des informations plus complètes sur chaque patient,
peut-être représentées comme une instance d’une classe Patient. Du point de
vue de la mise en oeuvre de la liste, le même principe s’applique : la liste
conservera simplement une séquence de références à ces objets. Notez également
qu’une référence à l’objet N one peut être utilisée comme élément de la liste pour
représenter un lit vide à l’hôpital.
Le fait que les listes et les tuples soient des structures référentielles est impor-
tant pour la sémantique de ces classes. Une seule instance de liste peut inclure
plusieurs références au même objet, et il est possible pour un seul objet d’être
un élément de deux ou plusieurs listes, car ces listes stockent simplement des
références à cet objet. Par exemple, lorsque vous calculez une tranche d’une
liste, le résultat est une nouvelle instance liste, mais cette nouvelle liste a des
références aux mêmes éléments qui sont dans la liste originale, comme le montre
la figure 5.6.
Lorsque les éléments de la liste sont des objets non modifiables, comme pour
les instances entières dans la figure 5.5, le fait que les deux listes partagent des
éléments n’est pas si significatif, car aucune des listes ne peut modifier l’objet
partagé. Si, par exemple, la commande temp[2] = 15 a été exécutée à partir
de cette configuration, cela ne modifie pas l’objet entier existant ; cela change
la référence dans la cellule 2 de la liste temporaire pour référencer un objet
différent. La configuration résultante est illustrée à la figure 5.7.
La même sémantique est démontrée lors de la création d’une nouvelle liste
en tant que copie d’une liste existante, avec une syntaxe telle que backup =
5.2. LES TABLEAUX DE BAS-NIVEAU 117
list(primes). Cela produit une nouvelle liste qui est une copie superficielle, en
ce qu’elle fait référence aux mêmes éléments que dans la première liste. Avec
des éléments non modifiables, ce point est sans objet. Si le contenu de la liste
était de type modifiable, une copie profonde, c’est-à-dire une nouvelle liste avec
de nouveaux éléments, peut être produite en utilisant la fonction deepcopy du
module de copie.
Comme exemple plus frappant, c’est une pratique courante en Python d’ini-
tialiser un tableau d’entiers en utilisant la syntaxe counters = [0] * 8. Cette
syntaxe produit une liste de huit éléments, les huit éléments ayant chacun la
valeur zéro. Techniquement, toutes les huit cellules de la liste font référence au
même objet, comme le montre la figure 5.8. à première vue, le niveau extrême
d’aliasing dans cette configuration peut sembler alarmant. Cependant, nous nous
appuyons sur le fait que l’entier référencé est non modifiable. Même une com-
mande telle que counters[2] += 1 ne change pas techniquement la valeur de
l’instance entière existante. Ceci calcule un nouvel entier, avec la valeur 0+1, et
définit la cellule 2 pour référencer la valeur nouvellement calculée. La configu-
ration résultante est illustrée par la figure 5.9.
118 CHAPITRE 5. LES SÉQUENCES BASÉES SUR LES TABLEAUX
la figure 5.12.
Figure 5.12 – Des entiers triés comme éléments d’un tableau compact de Py-
thon.
de telles structures peuvent être créés au niveau inférieur par le module nommé
ctypes.
Figure 5.13 – Un tableau de 12 bytes stockés dans les cellules mémoires allant
de 2146 à 2157.
Parce que le système peut dédier des emplacements de mémoire voisins pour
stocker d’autres données, la capacité d’un tableau ne peut pas être augmentée
de manière triviale en l’étendant aux cellules mémoire voisines. Dans le contexte
de la représentation d’un tuple Python ou d’une instance str, cette contrainte
n’est pas un problème. Les instances de ces classes sont non modifiables, donc
la bonne taille d’un tableau peut être fixée lorsque l’objet est instancié.
La classe list de Python présente une abstraction plus intéressante. Bien
qu’une liste ait une longueur particulière lorsqu’elle est construite, la classe nous
permet d’ajouter des éléments à la liste, sans limite apparente pour la capacité
globale de la liste. Pour fournir cette abstraction, Python s’appuie sur un tour
de passe-passe algorithmique connu sous l’appellation de tableau dynamique.
La première clé pour fournir la sémantique d’un tableau dynamique est
qu’une instance de list maintient un tableau sous-jacent qui a souvent une
capacité supérieure à la longueur actuelle de la liste. Par exemple, alors qu’un
utilisateur peut avoir créé une liste avec cinq éléments, le système peut avoir
réservé un tableau sous-jacent capable de stocker huit objets références (plutôt
que cinq).
Cette capacité supplémentaire permet d’ajouter facilement un nouvel élément
à la liste en utilisant la prochaine cellule disponible du tableau. Si un utilisa-
teur continue d’ajouter des éléments à une liste, toute capacité réservée finira
par s’épuiser. Dans ce cas, la classe demande un nouveau tableau plus grand
au système, et initialise le nouveau tableau afin que son préfixe corresponde à
celui du tableau plus petit existant. A ce moment-là, l’ancien tableau n’est plus
nécessaire, il est donc récupéré par le système.
122 CHAPITRE 5. LES SÉQUENCES BASÉES SUR LES TABLEAUX
Nous donnons des preuves empiriques que la classe list de Python est basée
sur une telle stratégie. Le code source de notre expérience est affiché dans le
fragment de code suivant :
Nous nous appuyons sur une fonction nommé getsizeof qui est disponible
à partir du module sys. Cette fonction rapporte le nombre d’octets utilisés
pour stocker un objet en Python. Pour une liste, il rapporte le nombre d’oc-
tets consacrés au tableau et aux autres variables d’instance de la liste, mais pas
l’espace consacré aux éléments référencés par la liste.
5.3. LES TABLEAUX DYNAMIQUES ET AMORTISSEMENT 123
Dès que le premier élément est inséré dans la liste, nous détectons un chan-
gement dans le taille sous-jacente de la structure. En particulier, on voit le
nombre d’octets sauter de 72 à 104, soit une augmentation d’exactement 32
octets. Notre expérience s’est déroulée sur une machine 64 bits, ce qui signifie
que chaque adresse mémoire est un nombre 64 bits (c’est-à-dire 8 octets). Nous
supposons que l’augmentation de 32 octets reflète l’allocation de un tableau
sous-jacent capable de stocker quatre références d’objets. Cette hypothèse est
cohérente avec le fait que nous ne voyons aucun changement sous-jacent dans
la mémoire utilisée après avoir inséré le deuxième, le troisième ou le quatrième
élément dans la liste.
Une fois que le cinquième élément a été ajouté à la liste, nous voyons le
saut d’utilisation de la mémoire de 104 octets à 136 octets. Si nous supposons
l’utilisation de base d’origine de 72 octets pour la liste, le total de 136 suggère 64
= 8 x 8 octets supplémentaires qui fournissent de la capacité de stocker jusqu’à
huit références d’objets. Encore une fois, cela est cohérent avec l’expérience, car
l’utilisation de la mémoire n’augmente plus jusqu’à la neuvième insertion. A ce
moment, les 200 octets peuvent être considérés comme les 72 d’origine plus un
tableau supplémentaire de 128 octets pour stocker 16 références d’objets. La 17e
insertion pousse l’utilisation globale de la mémoire à 272 = 72+200 = 72+25×8,
soit suffisament de mémoire pour stocker jusqu’à 25 références d’éléments. Parce
qu’une liste est une structure référentielle, le résultat de getsizeof() pour une
instance de list n’inclut que la taille pour représenter sa structure principale ; il
ne tient pas compte de la mémoire utilisée par les objets qui sont des éléments
de la liste.
Si nous devions continuer une telle expérience pour d’autres itérations, nous
pourrions essayer de discerner le modèle de la taille d’un tableau que Python crée
à chaque fois que la capacité du tableau précédent est épuisée. Avant d’explorer
la séquence précise des capacités utilisées par Python, nous continuons dans
124 CHAPITRE 5. LES SÉQUENCES BASÉES SUR LES TABLEAUX
cette section à décrire une approche générale pour implémenter des tableaux
dynamiques et pour effectuer une analyse asymptotique de leurs performances.
def __len__(self):
"""Return number of elements stored in the array."""
return self._n
def __getitem__(self, k)
"""Return the item at position k"""
if not 0 <= k < self._n:
raise IndexError(’invalid index’)
return self._A[k] # retreive from array
def _make_array(self,c):
"""Return new array with capacity c."""
return (c*ctypes.py_object)()
Bien que cohérent avec l’interface de la classe list de Python, nous ne four-
nissons que quelques fonctionnalités limitées sous la forme de quelques méthodes
d’ajout, et les accesseurs len. La prise en charge de la création de tableaux de
bas niveau est fournie par un module nommé ctypes. Parce que nous n’utili-
serons généralement pas une structure de niveau aussi bas dans le reste de ces
notes de cours, nous omettons une explication détaillée du module ctypes. Ici
nous nous sommes contentés d’encapsuler la commande nécessaire pour déclarer
le tableau brut dans un utilitaire privé, la méthode make array(self,c).
126 CHAPITRE 5. LES SÉQUENCES BASÉES SUR LES TABLEAUX
from time import time # import time function from time module
def compute_average(n):
"""Perform n appends to an empty list and return average time elapsed"""
5.4. L’EFFICACITÉ DES SÉQUENCES EN PYTHON 127
data = []
start = time()
for k in range(n):
data.append(None)
end = time()
return (end-start)/n
Nous constatons un coût moyen plus élevé pour les ensembles de données plus
petits, peut-être en partie en raison des frais généraux de la plage de boucle.
Il existe également un écart naturel dans la mesure du coût amorti de cette
manière, en raison de l’impact de l’événement de redimensionnement final par
rapport à n. Pris dans l’ensemble, il semble évident que le temps amorti pour
chaque opération d’ajout est indépendant du nombre n d’ajouts.
129
130CHAPITRE 6. LES PILES, LES FILES D’ATTENTE ETLES FILES D’ATTENTE PRIORITAIRES
Par convention, nous supposons qu’une pile nouvellement créée est vide, et qu’il
n’y a pas à priori de limite à la capacité de la pile. Les éléments ajoutés à la
pile peuvent avoir un type arbitraire.
Exemple 6.3
Le tableau suivant montre une série d’opérations sur une pile et leur effets
sur la pile.
Figure 6.2 – Implémentation d’une pile avec une liste Python. L’élément du
top de la pile est situé le plus à droite, ce qui correspond à la fin de la liste.
pass
class ArrayStack:
""" LIFO stack implementation using
a Python list as underlying storage"""
def __init__(self):
""" Create an empty stack"""
self._data = []
def __len__(self):
"""Return the number of
elements in the stack"""
return len(self._data)
def is_empty(self):
"""Return True if the stack is empty"""
return len(self._data) == 0
def top(self):
"""Return (but not remove) the
element at the top of the stack"""
Raise Empty Exception if the stack is empty"""
if self.is_empty():
raise Empty(’Stack is empty’)
return self._data[-1]
def pop(self):
"""Remove and return the element
from the top of the stack (i.e. LIFO).
Raise Empty exception if the stack is empty."""
if self.is_empty():
raise Empty(’Stack is empty’)
return self._data.pop()
134CHAPITRE 6. LES PILES, LES FILES D’ATTENTE ETLES FILES D’ATTENTE PRIORITAIRES
Dans le tableau ci-dessus, nous utilisons l’exposant (*) pour indiquer que le
résultat s’appuie sur la technique d’ammortissement.
def reverse_file(filename):
"""Overwrite given file with its contents
line-by-line reversed"""
S = ArrayStack()
original = open(filename)
for line in original:
S.push(line.rstrip(’\n’)
original.close()
output.write(S.pop() + ’\n’)
output.close()
S.push(c)
elif c in righty:
if S.is_empty():
return False
if righty.index(c) != lefty.index(S.pop()):
return False
return S.is_empty()
Nous supposons que l’entrée est une séquence de caractères, telle que [(5+x)-
(y+z)] . Nous effectuons un balayage de gauche à droite de la séquence originale,
en utilisant une pile S pour faciliter la correspondance entre les symboles d’ou-
verture et de fermeture de regroupement. Chaque fois que nous rencontrons un
symbole d’ouverture, nous poussons ce symbole dans S, et chaque fois que nous
rencontrons un symbole de fermeture, nous faisons expulser un symbole de la
pile S (en supposant que S n’est pas vide). Nous vérifieons ensuite que ces deux
les symboles forment une paire valide. Si nous atteignons la fin de l’expression
et que la pile est vide, alors l’expression d’origine était bien formée avec les
paires de délimiteurs correctement balancés. Sinon, il doit y avoir un délimiteur
d’ouverture sur la pile sans symbole de fermeture correspondant. Si la taille
de l’expression originale est n, l’algorithme fera au plus n appels à push() et
n appels à pop(). Ces appels s’exécutent en un temps total de O(n), même
en considérant le caractère amorti du temps O(1) lié à ces méthodes. Etant
donné que notre sélection de délimiteurs possibles, ({[, a une taille constante,
des tests auxiliaires tels que c dans lefty et righty.index(c) s’exécutent chacun en
un temps O(1). En combinant ces opérations, l’algorithme de correspondance
des délimiteurs s’exécute en un temps O(n) pour une séquence de longueur n .
Figure 6.3 – Illustration des tags HTML. (a) Un document HTML ; (b) Le
texte formater.
if k == -1:
return False
tag = raw[j+1:k]
if not tag.startswith(’/’):
S.push(tag)
else:
if S.is_empty():
return False
if tag[1:] != S.pop():
return False
j=raw.find(’<’, k+1)
return S.is_empty()
Figure 6.4 – Exemples de files d’attente. Le premier entré est le premier sorti.
(a) Des personnes attendent dans une file d’attente pour acheter des billets. (b)
Des appels téléphoniques sont acheminées vers un centre de service client dans
une file d’attente.
Par convention, on suppose qu’une file d’attente nouvellement créée est vide, et
qu’il n’y a pas a priori de limite à la capacité de la file d’attente. Les éléments
ajoutés à la file d’attente peuvent être d’un type arbitraire. Le tableau suivant
montre une série d’opérations dans une file d’attente et leurs effets sur la file
initialement vide. La file d’attente nommée ici Q contient des entiers.
140CHAPITRE 6. LES PILES, LES FILES D’ATTENTE ETLES FILES D’ATTENTE PRIORITAIRES
Figure 6.6 – File d’attente dont les éléments sont stockés dans un tableau
circulaire.
La mise en oeuvre de cette vue circulaire n’est pas difficile. Quand on retire
un élément de la file d’attente et que l’on veut Â≪ faire avancer Â≫ l’indice du
front, on utilise l’arithmétique f = (f + 1)%N . Rappellons que l’opérateur %
en Python désigne l’opérateur modulo, qui est calculé en prenant le reste après
une division intière. Par exemple, 14%3 = 2. A titre d’exemple concret, si nous
142CHAPITRE 6. LES PILES, LES FILES D’ATTENTE ETLES FILES D’ATTENTE PRIORITAIRES
avons une liste de longueur 10, et un indice de front 7, on peut avancer le front
en calculant formellement (7 + 1)%10, qui est simplement 8, car 8 divisé par 10
est 0 avec un reste de 8. De même, l’avancement de l’indice 8 entraı̂ne l’indice 9.
Mais lorsque nous avançons à partir de l’indice 9 (le dernier élément du tableau),
nous calculons (9 + 1)%10, ce qui donne l’indice 0 (comme 10 divisé par 10 égal
à un avec un reste de zéro).
class ArrayQueue:
""" FIFO queue implementation using a Python
list as underlying storage"""
DEFAULT_CAPACITY = 10
def __init__(self):
"""Create an empty queue"""
self._data = [None] * ArrayQueue.DEFAULT_CAPACITY
self._size = 0
self._front = 0
def __len__(self):
"""Return the number of elements in the queue"""
return self._size
def is_empty(self):
6.3. FILES D’ATTENTE À DEUX EXTRÉMITÉS 143
def first(self):
"""Return (but not remove) the
element at the front of the queue.
Raise Empty exception if the queue is empty.
"""
if self.is_empty():
raise Empty(’Queue is Empty’)
return self._data[self._front]
def dequeue(self):
"""Remove and return the first element
of the queue (i.e. FIFO).
Raise Empty exception if the queue is empty
if self.is_empty():
raise Empty(’Queue is empty’)
answer = self._data[self._front]
self._data[self._front] = None
self._front = (self._front + 1) % len(self._data)
pile et l’ADT file d’attente. La généralité supplémentaire peut être utile dans
certaines applications. Par exemple, lorsque nous utilisons une file d’attente
pour maintenir la liste d’attente d’un restaurant. Occasionnellement, la première
personne d’une file d’attente peut être supprimée de la file d’attente uniquement
pour découvrir qu’une table n’est pas disponible ; généralement, le restaurant
réinsérera la personne en première position dans la file d’attente. Ce peut-être
aussi qu’un client en fin de file d’attente s’impatiente et menace de quitter le
restaurant. Nous aurons besoin d’une structure de données encore plus générale
si nous voulons modéliser également des clients quittant la file d’attente depuis
d’autres positions.
Pour fournir une abstraction symétrique, l’ADT deque est défini de telle
sorte que deque prenne en charge les méthodes suivantes :
7.1 Introduction
Dans le chapitre 5, nous avons examiné la classe list de Python, qui utilise
un tableau pour stocké ses éléments. Dans le chapitre 6, nous avons montré
l’utilisation de cette classe dans l’implémentation d’une pile classique, d’une
file d’attente et de l’ADT file d’attente. Cette classe list de Python est haute-
ment optimisée et est souvent un excellent choix pour le stockage. Cela dit, elle
présente quelques inconvénients majeurs :
— La taille d’un tableau dynamique peut être plus importante que ce qui
est requis pour stocker le nombre réel d’éléments.
— Les limites amorties pour les opérations peuvent être inacceptables dans
des systèmes temps réel.
— Les insertions et les suppressions aux positions intérieures d’un tableau
sont coûteuses.
Dans ce chapitre, nous introduisons une structure de données appelée liste
chaı̂née, qui fournit une alternative à une séquence basée sur un tableau (telle
qu’une liste Python). Les séquences basées sur des tableaux, comme les listes
chaı̂nées conservent les éléments dans un certain ordre, mais en utilisant des
types d’organisations très différents.
Un tableau fournit la représentation la plus centralisée. Il dispose d’un es-
pace mémoire capable d’accueillir des références à de nombreux éléments. Une
liste chaı̂née, en revanche, repose sur une représentation plus distribuée dans la-
quelle un objet léger, appelé noeud, est alloué à chaque élément. Chaque noeud
conserve une référence à son élément et une ou plusieurs références aux noeuds
voisins afin de représenter collectivement l’ordre linéaire de la séquence.
Nous mettrons en évidence les avantages et les inconvénients des séquences
basées sur des tableaux et de celles basées sur les listes chaı̂nées lorsque nous les
comparerons. Les éléments d’une liste chaı̂née ne peuvent pas être efficacement
accessibles par un indice numérique k, et nous ne pouvons pas dire simplement
en examinant un noeud s’il s’agit du deuxième, du cinquième ou du vingtième
147
148 CHAPITRE 7. LES LISTES CHAINÉES
noeud de la liste. Cependant, les listes chaı̂nées évitent les trois inconvénients
majeurs associés aux séquences basées sur les tableaux à savoir :
— La taille d’un tableau dynamique peut être beaucoup plus importante
que le nombre d’éléments à stocker ;
— Les temps d’exécution des opérations dans un tableau par la méthode de
l’amortissement peuvent se révéler inacceptables dans certains cas ;
— L’insertion et la suppression des éléments à l’intérieur d’un tableau sont
très couteuses (temps d’exécution inacceptable dans certains cas).
Figure 7.1 – Exemple d’une instance noeud faisant partie d’une liste chaı̂née
simple. Le noeud fait référence à un objet arbitraire de la liste et au noeud sui-
vant. Ici le noeud fait référence à l’objet ”MSP” de la liste. Il dispose également
d’une reférence vers le noeud qui vient juste après lui. Le noeud suivant fait
référence à lui même et au prochain élément de la liste, ou à N one s’il n’y a
plus d’éléments dans la liste.
Le premier et le dernier noeud d’une liste chaı̂née sont appelés tête et queue
de la liste, respectivement. En commençant par la tête, et en passant d’un noeud
à un autre en suivant la prochaine référence de chaque noeud, nous pouvons
atteindre la fin de la liste. Ce processus est communément appelé le parcourt
de la liste chaı̂née.
Nous pouvons identifier la queue comme le noeud ayant N one comme noeud
suivant. Parce que la prochaine référence d’un noeud peut être considéré comme
un lien ou un pointeur vers un autre noeud, le processus de traversée d’une liste
est également connu sous le nom de saut de lien ou de saut de pointeur. La
représentation en mémoire d’une liste chaı̂née repose sur la collaboration de
plusieurs objets.
Chaque noeud est représenté comme un objet unique, cette instance stocke
une référence à son élément et une référence au noeud suivant (ou à N one).
Un autre objet représente la liste chaı̂née dans son ensemble. Au minimum,
7.2. LES LISTES CHAINÉES SIMPLES 149
Figure 7.2 – Exemple d’une liste chaı̂née simple dont les éléments sont des
chaines de caractères correspondants aux codes d’aéroports. L’intance de la
liste maintient un élément appelé tête de liste (head) qui correspond au premier
élément de la liste et dans certaines applications un élément appelé queue de
liste qui correspond au dernier noeud de la liste.
l’instance de liste chaı̂née doit conserver une référence à la tête de liste. Sans
référence explicite à la tête, il n’y aurait aucun moyen de localiser ce noeud (et
indirectement, tout autre).
Il n’y a pas de nécessité absolue de stocker une référence directe à la fin de
la liste, car cela pourrait autrement être localisé en commençant par la tête et
en parcourant le reste de la liste. Pourtant, stocker une référence explicite au
noeud de queue est une commodité courante.
De la même manière, il est courant qu’une instance de liste chaı̂née garde
un décompte du nombre total de noeuds qui composent la liste (généralement
décrit comme la taille de la liste), pour éviter d’avoir à parcourir la liste pour
compter les noeuds.
Pour le reste de ce chapitre, nous continuons à illustrer les noeuds en tant
qu’objets, et la référence Â≪ suivante Â≫ de chaque noeud en tant que pointeur.
Cependant, par souci de simplicité, nous illustrons l’élément d’un noeud intégré
directement dans la structure du noeud, même bien que l’élément soit, en fait, un
objet indépendant. Par exemple, la figure 7.3 est une illustration plus compacte
de la liste chaı̂née de la figure 7.2.
tuel d’éléments. Lors de l’utilisation d’une liste chaı̂née simple, nous pouvons
facilement insérer un élément en tête de liste, comme indiqué dans la figure 7.4,
et décrit avec un pseudo-code ci-dessous :
Algorithm add_first(L,e):
newest = Node(e) # creer un nouveau noeud
newest.next = L.head # l’ancien head devient
# le next du nouveau noeud
L.head = newest # le nouveau noeud devient le head
L.size = L.size + 1 # incrementer le nombre
# d’elements de la liste
L’idée principale est que nous créons un nouveau noeud. Nous définissons
son élément et nous définissons son prochain comme étant l’actuelle tête de liste.
Enfin, nous faisons pointé la tête de la liste sur le nouveau noeud.
Figure 7.4 – Insertion d’un élément en tête d’une liste chaı̂née simple. (a)
Avant insertion ; (b) Après création d’un nouveau noeud ; (c) Après affectation
de la référence de la tête de liste au nouveau noeud.
Algorithm add_last(L,e):
newest = Node(e) # creer un nouveau noeud
newest.next = None # le next du nouveau noeud est None
L.tail.next = newest # ancienne queue pointe vers nouveau noeud
L.tail = newest # le nouveau noeud devient le tail
L.size = L.size + 1 # incrementer le nombre
# d’elements de la liste
Figure 7.5 – Insertion d’un élément en queue d’une liste chaı̂née simple. (a)
Avant l’insertion ; (b) Après création d’un nouveau noeud ; (c) Après affectation
de la reférence de la queue de la liste au nouveau noeud.
Algorithm remove_first(L):
if L.head is None then
Indicate an error: the list is empty.
L.head = L.head.next # head pointe vers le nouveau noeud
L.size = L.size - 1 # d\’ecrementer le nombre
# d’elements de la liste
Figure 7.6 – Suppresion d’un élément en tête d’une liste chaı̂née simple. (a)
Avant la suppression ; (b) Après avoir déplacé la tête de liste vers le noeud
suivant celui à supprimer ; (c) Configuration finale.
avant la queue en suivant les prochains liens de la queue. Le seul moyen d’accéder
à ce noeud est de commencer à partir de la tête de la liste et de parcourir tout
le chemin à travers la liste. Mais une telle séquence d’opérations de saut de lien
pourrait prendre beaucoup de temps. Si nous voulons soutenir efficacement une
telle opération, nous devrons liée doublement notre liste (comme cela est fait
dans la section 7.3).
classe _Node:
""" Lightweight, nonpublic class
for storing a singly linked node."""
7.2. LES LISTES CHAINÉES SIMPLES 153
def top(self):
""" Return (but not remove) the element
at the top of the stack
Raise Empty exception if the stack is empty."""
if self.is_empty():
raise Empty(’Stack is empty’)
return self._head._element
154 CHAPITRE 7. LES LISTES CHAINÉES
def pop(self):
"""Remove and return the element
at the top of the stack (LIFO)"""
Raise Empty exception if the stack is empty."""
if self.is_empty():
raise Empty(’Stack is empty’)
answer = self._head._element
self._head = self._head._next
self._size -= 1
return answer
class LinkedQueue:
"""FIFO queue implementation using
a singly linked list for storage """
classe _Node:
""" Lightweight, nonpublic class
for storing a singly linked node."""
__slots__ = ’_element’, ’_next’
def __init__(self, element, next):
self._element = element
self._next = next
def __init__(self):
"""Create an empty queue"""
self._head = None
self._tail = None
self._size = 0
def __len__(self):
"""Return the number of
elements in the queue"""
return self._size
def is_empty():
"""Return True if the
queue is empty """
return self._size == 0
def first(self):
"""Return (but not remove)
156 CHAPITRE 7. LES LISTES CHAINÉES
def dequeue(self):
’’’’’’Remove and return the first element of the queue (i.e. FIFO)
Raise Empty Exception if the queue is empty.’’’’’’
if self.is_empty():
raise Empty(’Queue is empty’)
answer = self._head._next
self._size -=1
if self.is_empty():
self._tail = None
return answer
def enqueue(self,e):
’’’’’’Add an element to the back of the queue’’’’’’
newest = self._Node(e,None)
if self.is_empty():
self._head = newest
else:
self:_tail._next = newest
self._tail = newest
self._size +=1
Une liste chaı̂née circulaire fournit un modèle plus général qu’une liste chaı̂née
simple. Il s’agit d’une liste dans laquelle les données sont chainées de manière
circulaire.
Figure 7.8 – Exemple de liste chaı̂née circulaire avec un noeud courant (current
node). Ce noeud désigne la référence au noeud actuellement sélectioné.
Dans une telle liste les données se suivent de manière cyclique, c’est-à-dire
qu’elles n’ont pas de notion particulière de début et de fin. La figure 7.8 fournit
une illustration plus symétrique de la liste chaı̂née circulaire présentée par la
figure 7.7.
Une vue circulaire similaire à la figure 7.8 pourrait être utilisée, par exemple,
pour décrire l’ordre des arrêts de train dans la boucle de Chicago, ou l’ordre
dans lequel les joueurs se relaient pendant un match. Même si une liste chaı̂née
circulaire n’a ni début ni fin, nous devons toutefois maintenir une référence à
un noeud particulier afin d’utiliser la liste. Nous utilisons l’identifiant courant
158 CHAPITRE 7. LES LISTES CHAINÉES
class CircularQueue:
"""Queue implementation using
circularly linked list for storage"""
class _Node:
"""Lightweight, nonpublic class
for storing a singly linked node"""
(omitted here; identical to that
of LinkedStack._Node)
def __init__(self):
""" Create an empty queue"""
self._tail = None
self._size = 0
def __len__(self):
"""Return the number of
elements in the queue"""
return self._size
def is_empty(self):
"""Return True if the
queue is empty."""
return self._size == 0
def first(self):
"""Return (but not remove)
the element at the front of
the queue. Raise Empty
exception if the queue is empty."""
7.3. LES LISTES CHAINÉES CIRCULAIRES 159
if self.is_empty():
raise Empty(’Queue is empty’)
head = self._tail._next
return head._element
def dequeue(self):
"""Remove and return the first
element of the queue (FIFO).
Raise Empty exception if the
queue is empty."""
if self.is_empty():
raise Empty(’Queue is empty’)
oldhead = self._tail._next
if self._size == 1:
self._tail = None
else:
self._tail._next = oldhead._next
self._size -= 1
return oldhead._element
def rotate(self):
"""Rotate front element to
the back of the queue."""
if self._size >0:
self._tail = self._tail._next
Les deux seules variables d’instance sont tail, qui est une référence au noeud
de queue (ou à N one lorsque la liste est vide) et la taille (size), qui est la
valeur actuelle du nombre d’éléments dans la file d’attente. Lorsqu’une opération
implique l’avant de la file d’attente, nous reconnaissons self. tail. next comme
la tête de la file d’attente. Lorsque la méthode enqueue est appelèe, un nouveau
noeud est placé juste après la queue de la liste, mais avant la tête actuelle. Ainsi
le nouveau noeud devient la queue de la file d’attente.
En plus des opérations de file d’attente traditionnelles, la classe Circular-
Queue prend en charge une méthode rotate qui met en oeuvre plus effica-
160 CHAPITRE 7. LES LISTES CHAINÉES
Afin d’éviter certains cas particuliers lorsque l’on opère près de la tête ou
de la queue d’une liste doublement chaı̂née, il est utile d’ajouter deux noeuds
spéciaux dans la liste : un noeud d’en-tête au début de la liste et un noeud de
queue à la fin de la liste. Ces noeuds Â≪ fictifs Â≫ sont appelés sentinelles (ou
gardes), et ils ne stockent pas d’éléments de la séquence. Une liste doublement
chaı̂née avec de telles sentinelles est illustrée à la figure 7.9.
Lors de l’utilisation de noeuds sentinelles, une liste vide est initialisée de
sorte que l’en-tête (header) ait comme next le trailer et la queue (trailer) ait
comme prev le header.
7.4. LISTE DOUBLEMENT CHAÎNÉE 161
Figure 7.9 – Une liste doublement chaı̂née contenant la séquence {(JFK, PVD,
SFO)} et utilisant une sentinelle d’en tête (header) et une sentinelle de queue
(trailer).
Figure 7.10 – Ajout d’un élément dans une liste doublement chaı̂née ayant
une sentinelle en tête (header) et une sentinelle en queue (trailer). (a) Avant
l’opération ; (b) Après création du nouveau noeud ; (c) Après avoir lié le nouveau
noeud à ses voisins.
Nous verrons que les listes doublement chaı̂nées peuvent supporter des in-
sertions et suppressions générales ayant un temps d’exécution O(1)dans le pire
des cas. Avec les séquences basées sur des tableaux, un indice entier était un
moyen pratique pour décrire une position dans une séquence. Cependant, un in-
dice n’est pas pratique pour les listes chaı̂nées car il n’y a pas de moyen efficace
de trouver le j ième élément ; il semblerait qu’il faille parcourir une partie de la
liste.
Lorsque l’on travaille avec une liste chaı̂née, la façon la plus directe de décrire
l’emplacement d’une opération consiste à identifier un noeud pertinent de la
liste. Cependant, nous préférons encapsuler le fonctionnement interne de notre
structure de données en évitant que des utilisateurs aient directement accès aux
noeuds de notre liste. Dans la suite de ce chapitre, nous développerons deux
classes publiques qui héritent de notre classe DoublyLinkedBase pour offrir
des abstractions plus cohérentes.
Notre classe DoublyLinkedBase de bas niveau repose sur l’utilisation
d’une classe N ode non publique similaire à celle que nous avons utilisé pour
l’implémentation d’une liste chaı̂née simple. Toutefois, la version de la classe
N ode utilisée pour l’implémentation d’une classe doublement chaı̂née inclut un
attribut prev, en plus des attributs next et element, comme indiqué dans
le fragment de code ci-dessous :
class _Node:
"""Ligthweight, nonpublic class
7.4. LISTE DOUBLEMENT CHAÎNÉE 163
class _Node:
"""Ligthweight, nonpublic class
for storing q doubly linked node"""
__slots__ = ’element’, ’\_prev’, ’\_next’
def __init__(self):
164 CHAPITRE 7. LES LISTES CHAINÉES
def __len__(self):
"""Return the number of elements in the list"""
return self._size
def is_empty(self):
"""Return True if list is empty."""
return self._size == 0
Ensuite les champs de ce nouveau noeud sont liés à ceux des noeuds voisins
spécifiés. Pour plus de commodité plus tard, la méthode renvoie une référence
au noeud nouvellement créé.
L’implémentation de la méthode delete node est basée sur l’algorithme
illustré à la figure 7.11. Les voisins du noeud à supprimer sont liés directe-
ment l’un à l’autre, contournant ainsi le noeud à supprimé de la liste. A titre
de formalité, nous réinitialisons intentionnellement les champs prev, next et
element du noeud supprimé à N one (après enregistrement de l’élément à re-
tourner). Bien que le noeud supprimé soit ignoré par le reste de la liste, définir
ses champs sur N one est avantageux car il peut aider le ramasse-miettes de
Python, car les liens inutiles sont éliminés. Nous nous appuierons également sur
cette configuration pour reconnaı̂tre un noeud comme ”deprecated” lorsqu’il ne
fait plus partie de la liste.
class LinkedDeque(_DoublyLinkedBase):
"""Double-ended queue implementation
based on a doubly linked list """
def first(self):
"""Return (but not remove) the element
at the front of the deque."""
if self.is_empty():
raise Empty("Deque is empty")
return self._header._next._element
def last(self):
"""Return (but not remove) the
element at the back of the deque."""
if self.is_empty():
raise Empty("Deque is empty")
return self._trailer._prev._element
def delete_first(self):
"""Remove and return the element from the
front of the deque.
Raise Empty exception if deque is empty."""
if self.is_empty():
raise Empty("Deque is empty")
return self._delete._node(self._header._next)
def delete_last(self):
"""Remove and return the element from the
back of the deque.
Raise Empty exception if deque is empty."""
if self.is_empty():
raise Empty("Deque is empty")
return self._delete_node(self._trailer._prev)
Figure 7.12 – Nous souhaitons pouvoir identifier la position d’un élément dans
une séquence sans utiliser un indice.
Autre exemple, un document texte peut être considéré comme une longue
séquence de personnages. Un traitement de texte utilise l’abstraction d’un cur-
seur pour déterminer une position dans le document sans utilisation explicite
d’un indice entier. Cette abstraction devrait permettre des opérations telle que
supprimer le caractère au niveau du curseur, ou insérer un nouveau caractère
168 CHAPITRE 7. LES LISTES CHAINÉES
juste après le curseur. De plus, nous devons être en mesure de faire référence à
une position inhérente dans un document, comme le début d’une section parti-
culière, sans s’appuyer sur un indice (ou même un numéro de section) qui peut
changer au fur et à mesure de l’évolution du document.
L’un des grands avantages d’une liste chaı̂née est qu’il est possible d’effectuer
des insertions et des suppressions en un temps O(1) à des positions arbitraires
de la liste, dès que nous recevons une référence à un noeud pertinent de la liste.
Il est donc très tentant de développer un ADT dans lequel une référence de
noeud sert de mécanisme pour déterminer une position. En fait, notre classe
DoubleLinkedBase a des méthodes insert between et delete node qui
acceptent les références de noeuds comme paramètres. Cependant, une telle
utilisation directe des noeuds violerait les principes d’abstraction et d’encap-
sulation qui sont de rigueur dans la conception orientée objet. Il y a plusieurs
raisons de préférer que nous encapsulions les noeuds d’une liste chaı̂née, tant
pour notre propre protection que pour la protection des utilisateurs de notre
abstraction :
— Ce sera plus simple pour les utilisateurs de notre structure de données
s’ils ne sont pas dérangés par détails inutiles de notre implémentation,
tels que la manipulation de bas niveau de noeuds, ou notre dépendance
à l’utilisation de noeuds sentinelles. Notez que pour utiliser la méthode
insert between de notre classe DoubleLinkedBase pour ajouter un
noeud au début d’une séquence, l’en-tête sentinelle doit être passée en
argument.
— Nous pouvons fournir une structure de données plus robuste si nous ne
permettons pas aux utilisateurs d’accéder directement aux noeuds. De
cette façon, nous nous assurons que les utilisateurs ne peuvent pas inva-
lider la cohérence d’une liste en gérant mal l’enchaı̂nement des noeuds.
Un problème plus subtil se pose si un utilisateur est autorisé à appeler
les méthodes de gestion de noeud de notre classe DoubleLinkedBase,
insert between et delete node en passant comme argument un noeud
qui n’appartient pas à la liste.
— En encapsulant mieux les détails internes de notre implémentation, nous
avons une plus grande flexibilité pour reconcevoir la structure des données
et améliorer ses performances. En fait, avec une abstraction bien conçue,
nous pouvons donner l’impression de ne pas utiliser le concept de position
numérique, même si nous utilisons une séquence basée sur un tableau.
Pour ces raisons, au lieu de s’appuyer directement sur les noeuds, nous intro-
duisons une abstraction de position pour désigner l’emplacement d’un élément
dans une liste, et puis une ADT liste positionnelle complète qui peut encapsuler
une liste doublement chaı̂née (ou même une séquence basée sur un tableau).
7.5. LA LISTE POSITIONNELLE ADT 169
Pour fournir une abstraction générale d’une séquence d’éléments avec la ca-
pacité d’identifier l’emplacement d’un élément, nous définissons un ADT posi-
tional list ainsi qu’un ADT position plus simple pour déterminer un emplace-
ment dans une liste. Une position agit comme un marqueur ou un jeton dans une
liste de positions. Une position p n’est pas affectée par des changements ailleurs
dans une liste ; la seule façon dont une position devient invalide est qu’une com-
mande explicite soit émise pour la supprimer. Une instance de position est un
objet simple, prenant en charge uniquement la méthode suivante :
Dans le cadre de l’ADT position list, les positions servent d’arguments à cer-
taines méthodes et comme valeurs de retour pour d’autres méthodes. Ci-dessous,
les méthodes d’accès supportées par une ADT liste positionelle L :
Pour les méthodes de l’ADT qui acceptent une position p comme argument, une
erreur se produit si p n’est pas une position valide pour la liste L.
Proposition :
Chaque méthode de l’ADT liste positionnelle s’exécute dans le pire des cas
en un temps O(1) lorsque l’ADT est mis en oeuvre avec une liste doublement
chaı̂née. Nous nous appuyons sur la classe DoubleLinkedBase pour notre
implémentation. La principale responsabilité de notre nouvelle classe est de four-
nir une interface publique conforme à l’ADT liste positionnelle. Nous donnons
notre définition de la classe PositionalList dans le fragment de code ci-dessous.
La définition de la classe de Position est imbriquée dans notre classe Positio-
nalList.
class PositionalList(_DoublyLinkedBase):
"""A sequential container of elements
allowing positional access"""
#------------------------nested Position class------
class Position:
"""An abstraction representing the
7.5. LA LISTE POSITIONNELLE ADT 171
def element(self):
"""Return the element stored
at this position"""
return self._node._element
#----------------accessors---------------------------
172 CHAPITRE 7. LES LISTES CHAINÉES
def first(self):
"""Return the first Position in the
list (or None if list is empty)."""
return self._make_position(self._header._next)
def last(self):
"""Return the last Position in the list
(or None if list is empty)."""
node = self._validate(p)
return self._make_position(node._prev)
original = self._validate(p)
old_value = original._element
original._elment = e
return old_value
Les arbres
8.1 Introduction
Les arbres représentent un important développement dans l’organisation des
données, dans la mesure ou ils nous permettent d’implémenter des algorithmes
plus rapides que ceux basés sur les structures de données linéaires comme les
listes chainées.
Pour de nombreux experts, l’innovation vient du fait que l’arbre est une struc-
ture de données non linéaire. Par non linéaire on entend le fait que la relation
entre les éléments d’un arbre est plus riche que le simple fait de se retrouver
avant ou après un élément.
La relation entre les éléments d’un arbre est hiérarchique avec quelques objets
situés au dessus et quelques autres en dessous d’un objet pris au hasard dans
la structure. Pour décrire la relation entre les éléments d’un arbre, on trouve
les mots tels que : “parent”, “enfant”, “ancêtre”, “descendant”. La figure 8.1
montre un arbre tel qu’on le trouve dans la nature. L’arbre informatique quant
à lui occupe une position inversée par rapport à l’arbre naturel. La figure 8.2
en présente un.
Notons que les éléments sont disposés sur l’abre informatique de façon inverse
par rapport à l’arbre botanique.
177
178 CHAPITRE 8. LES ARBRES
Figure 8.1 – Exemple d’un arbre tel qu’on en trouve dans la nature.
Définition formelle
De façon plus formelle, un arbre T est un ensemble de noeuds stockant des
éléments de telle sorte que les noeuds ont entre eux une relation “parent-enfant”
qui satisfait aux propriétés suivantes :
— Si T n’est pas vide, il a un noeud spécial, appelé racine qui n’a pas de
parent ;
— Chaque noeud de T différent de la racine a un parent unique ; chaque
noeud ayant un parent est appelé enfant de celui-ci.
Selon notre définition, un arbre peut être vide, ceci revient à dire qu’il n’a pas
de noeuds. Cette convention nous permet de définir un arbre en nous appuyant
sur la récursivité : un arbre est donc soit vide, soit constitué d’un noeud appelé
racine et d’un ensemble d’arbres (vides ou pas) dont les racines sont les enfants
de la racine de l’arbre de départ.
Exemple
Dans presque tous les systèmes d’exploitation, les fichiers sont organisés hiérar-
chiquement dans des répertoires qui sont présentés à l’utilisateur sous forme d’un
arbre. Les noeuds internes de l’arbre sont associés aux répertoires (directories)
et les noeuds externes aux fichiers. Voir schéma. Un noeud u est un ancêtre d’un
noeud v si u = v ou si u est un ancêtre du parent de v. Dans le schéma présenté
ci-dessus cs252/ est un ancêtre de papers/ et pr3 est un descendant de cs016/.
Le sous-arbre dont la racine est v est un arbre formé de tous les descandants de
v y compris v lui même.
8.4. BORDS ET CHEMINS DANS UN ARBRE 179
Figure 8.2 – L’abre informatique occupe une position inversée par rapport à
l’arbre naturel. La racine se situe au-dessus et les feuilles en-dessous. Ici nous
présentons le système de fichier du système d’exploitation Unix sous forme d’un
arbre.
Les composantes d’un document structuré, tel qu’un livre, sont hiérarchi-
quement organisés en un arbre dont les noeuds internes sont les parties du livre,
les chapitres, les sections, et dont les neouds externes sont des paragraphes, les
180 CHAPITRE 8. LES ARBRES
tables et les figures. La racine de cet arbre est le livre lui même. Nous pourions
continuer le développement de l’arbre dans la mesure ou les paragraphes sont
constituées des phrases, les phrases des mots et les mots de caractères, mais il
faut bien s’arreté quelque part. Un tel arbre est un arbre ordoné, parce-qu’il
existe une relation d’ordre bien définie entre les enfants de chaque noeud.
Si T est un arbre ordoné, alors la collection itérable des enfants de v est or-
donnée. Si v est un noeud externe alors la collection de ses enfants est vide.
En plus des méthodes ci-dessus l’objet position devrait supporter les méthodes
suivantes qui sont des questions :
Toute méthode qui reçoit comme argument une position, doit générer une er-
reur si la position est invalide. Jusqu’à ce niveau nous n’avons pas implémenter
de méthode de mise à jour de l’arbre. Dans la pratique on décrit les méthodes
de mise à jour d’un arbre en même temps que les méthodes spécifiques de l’ap-
plication en cours de développement.
Une façon naturelle de représenter un arbre est d’utilisé une structure chainée
ou nous représentons chaque noeud u de T par un objet position comprenant
les champs suivants :
— une réfrérence à l’élément stocké en u ;
— un lien vers le parent de u ;
— et une collection permettant de stocker les liens vers les enfants de u.
— une référence vers la racine ;
— le nombre de noeuds de T.
Les Graphes
9.1 Introduction
La connection par paires des objets joue un role important dans bon nombre
de problèmes informatiques. Nombreuses sont les questions qui viennent à l’es-
prit du fait de cette relation entre paires d’objets. Est-il possible de connecter
un objet à un autre en suivant cette relation ? Combien d’objets pourrait-on
connecter à un un objet donné ?
Pour modéliser des telles situations, nous utilisons des objets mathématiques
abstraits appelés graphes. La théorie des graphes est une discipline majeure
des mathématiques qui a été étudiée intensivement depuis plusieurs centaines
d’années. De nombreuses propriétés importantes des graphes ont été découvertes,
de nombreux algorithmes importants ont été développés en se servant des graphes
et plusieurs problèmes compliqués sont encore activement étudiés.
Dans ce cours nous allons étudier quelques algorithmes importants sur les
graphes qui sont utilisés dans le développement et l’optimisation des réseaux.
Pour illustrer la diversité des applications dans lesquelles la théorie des graphes
est utilisée, nous allons commencer par donner quelques exemples de ces appli-
cations.
Les cartes
183
184 CHAPITRE 9. LES GRAPHES
Le contenu du web
Lorsque nous naviguons sur le web, nous rencontrons les pages qui contiennent
des liens vers d’autres pages et nous passons d’une page à une autre en clickant
sur ces liens. Le web dans son entierté est un graphe dont les sommets sont les
pages web et les connections entre les pages les liens entre elles. Le traitement
des algorithmes sur les graphes sont une composante essentielle de la recherche
des informations sur le web.
La planification
Un processus industriel requiert une variété de corps de métiers pour être
exécuté, sous un ensemble de contraintes qui indiquent par exemple que cer-
taines tâches ne peuvent être commencées sans que d’autres ne soient achevées.
Comment organisons nous les tâches pour que nous puissions à la fois satisfaire
toutes les contraintes et en même temps réaliser le processus en prenant le moins
de temps possible ?
hautes tensions vers les basses et moyennes tensions avant que l’électricité ne soit
enfin transportée et distribuée aux consomateurs industriels et domestiques à
travers un réseau de distribution. Comment interconnecter les différents postes
du réseau pour en minimiser le coût ? Comment organiser ces éléments pour
minimiser les pertes ohmiques et bien d’autres problèmes obligent les électriciens
à recourir aux graphes.
Les logiciels
Un compilateur par exemple construit des graphes pour représenter les liens
entre différents modules d’un gros logiciel. Les objets en jeu sont ici les différentes
classes ou modules constituant le système et les connections sont associées aux
échanges de messages entre les classes ou modules. Une analyse du graphe de
relations entre les éléments du logiciel est nécessaire pour déterminer comment
allouer les ressources le plus efficacement possible aux éléments du logiciel.
Lorsque vous utilisez un réseau social, vous établissez des connections avec
des amis, des connaissances. Ici les objets en jeu sont des personnes et les connec-
tions entre ces personnes correspondent au fait qu’elles sont amies ou ”follo-
wers”. Une bonne compréhension des relations entre les amis passe par l’usage
des graphes et interesse les politiques, les entreprises et les agences d’intelligence.
Le tableau ci-dessous résume la situation de ces quelques exemples men-
tionnés ci-dessus.
9.2 Généralités
Un graphe est un ensemble de sommets dont certaines paires de sommets ou
toutes sont reliés par des arêtes. Nous représentons graphiquement les graphes
par des cercles pour les sommets et des lignes qui les relient pour illustrés les
arêtes. Une telle représentation matérialise intuitivement un graphe, mais elle
peut également désorienter en fournissant des informations fausses sur le graphe.
Par exemple, les sommets sont numérotés de 1 à n, ce qui pourrait faire penser
à un certain ordre alors qu’il n’y en réalité aucun ordre entre les sommets d’un
graphe. Le seul fait est que deux sommets peuvent être reliés par une ou plusieurs
arêtes.
Un graphe pour lequel l’arête reliant les sommets u et w peut être notée
(u, w) ou (w, u), est un graphe non orienté. Dans le cas d’un graphe orienté (u, w)
sont (w, u) deux arêtes différentes. La figure 9.1 donne deux représentations
d’un même graphe. Lorsqu’un sommet est relié à lui même par une arête, on
parle d’une boucle. Deux arêtes qui interconnectent les mêmes sommets sont
dits parallèles. La figure 9.2 illustre une boucle et des arêtes parallèles dans
un graphe. Pour les mathématiciens un graphe contenant des arêtes parallèles
est un multigraphe, alors qu’un graphe sans arêtes parallèles ni boucles est un
graphe simple.
Dans le présent cours nous ne parlerons que de graphes simples et n’aborde-
rons les multigraphes qu’en le spécifiant expressément. Lorsque deux sommets
(on parle parfois de noeuds) d’un graphe sont interconnectés par une arête, on
dit qu’ils sont adjacents l’un et l’autre et l’arête est dite incidente aux deux
sommets. Le dégré d’un sommet est le nombre d’arêtes qui lui sont incidents.
Un sous-graphe est un sous-ensemble d’un graphe ayant lui même les pro-
priétés d’un graphe. Beaucoup de tâches de calcul reviennent à identifier un
sous-graphe ayant des propriétés particulières. Un chemin dans un graphe est
une séquence de sommets connectés par des arêtes. Un chemin simple est un
chemin dans lequel aucun sommet ne se répète. Un cycle est un chemin avec au
moins une arête pour laquelle le premier et le dernier sommets sont identiques.
Un cycle simple est un cycle sans répétition d’arête ou de sommet à l’exception
de la répétition du premier et du dernier sommets.
La longueur d’un chemin ou d’un cycle est le nombre de ses arêtes. Nous
disons qu’un sommet est connecté à un autre s’il existe un chemin qui contient
les deux sommets. Pour représenter le chemin allant de u à x, on utilise la
notation u − v − w − x. u − v − w − x − u est un cycle qui commence et se termine
au sommet u.
Un graphe est dit connecté s’il existe un chemin partant d’un sommet quel-
conque vers tout autre sommet. Un graphe qui n’est pas connecté consiste en
un ensemble de composants non connectés. Un graphe acyclique est un graphe
sans cycles. Plusieurs des algorithmes que nous allons analyser et implémenter
se contentent de trouvé un graphe acyclique satisfaisant certaines propriétés.
Un arbre est un graphe acyclique connecté. Un ensemble d’arbres disjoints
est appelé une forêt. L’arbre couvrant d’un graphe connecté est un sous-graphe
contenant tous les noeuds du graphe et un arbre unique. La forêt couvrante
9.2. GÉNÉRALITÉS 187
Figure 9.3 – Deux graphes avec le même nombre de sommets. Celui de 200
arrêtes n’est pas dense et cellui de 1000 arrêtes est dense.
d’un graphe est l’union des arbres couvrants de ses composantes connectées.
Un graphe G de S sommets est un arbre si seulement si il satisfait l’une des
conditions suivantes :
— G a S − 1 arêtes et pas de cycles.
— G a S − 1 arêtes et est connecté.
— G est connecté, mais la suppression de n’importe quelle arête le déconnecte.
— Exactement un simple chemin connecte n’importe quelle paire de som-
mets de G.
La densité d’un graphe est la proportion de paires de sommets connectés
par des arêtes. Un graphe peu dense a peu d’arêtes connectant les sommets.
Un graphe dense a peu d’arêtes manquants. Généralement on dit d’un graphe
qu’il est peu dense si le nombres de ses arêtes A vaut son nombre de sommets
multiplié par un facteur constant petit. Dans le cas contraire, le graphe est dit
dense. La figure 9.3 illustre un graphe dense et un graphe peu dense.
Un graphe bipartite est un graphe dont les sommets peuvent êtres répartis
en deux ensembles tels que chaque arête connecte un sommet d’un des ensembles
vers un sommet de l’autre ensemble.
Figure 9.4 – Un graphe non orienté avec ses représentation en liste d’adjacence
et matrice d’adjacence.
u dans G. Cette représentation est préférée à celle par matrice d’adjacence, car
elle fournit un moyen compact de représenter les graphes peu denses (ceux pour
lesquels |A| est largement inférieur à |S|. La figure 9.4 montre un graphe non
orienté représenté par liste d’adjacence, puis par matrice d’adjacences.
1 si (i, j) ∈ A
aij =
0 si non
soit minimal. Puisque T est acyclique et connecte tous les sommets, il doit
former un arbre, que l’on appelle arbre couvrant car il couvre le graphe G. Si
en plus son poids est minimal, alors il est un arbre couvrant de poids minimal.
Règle bleu
La règle bleu est basée sur la notion de coupe. Une coupe est un ensemble
d’arêtes qui relie deux groupes de sommets du graphe. Une coupe disponible est
une coupe dont aucune arête n’est colorié en bleu. La règle bleu stipule : prendre
une coupe disponnible et colorié en bleu l’arête de poids minimum. Cette règle
192 CHAPITRE 9. LES GRAPHES
exprime le fait que l’arête de poids minimum d’une coupe fera partie de l’arbre
couvrant de poids minimum.
Règle rouge
La règle rouge est basée sur la notion de cycle disponible. Un cycle dispo-
nible est un cycle dans lequel aucune arête n’est coloriée en rouge. La règle
rouge stipule : prendre un cycle disponible et colorié en rouge l’arête de poids
maximum. Cette règle exprime en fait la propriété selon laquelle l’arête de poids
maximum d’un cycle disponnible ne fera jamais partie d’un arbre couvrant de
poids minimum.
Conclusions et Perspectives
Nous n’avons pas abordé toutes les structures de données possibles, la chose
étant tout simplement impossible dans le cadre de ce cours. Nous nous sommes
contentés de présenter quelques unes en l’occurence les plus populaires d’entre
elles. Nous nous efforcerons d’inclure dans ces notes d’autres structures des
données telles que les dictionnaires, les tables de hâchage. Toutefois, une compré-
hension approfondie de celles qui sont présentées permet déjà de résoudre bon
nombre de probèmes avec élégance et efficacité. Nous sollicitons l’indulgence du
lecteur dans la mesure où ces notes de cours sont encore incomplètes et n’ont pas
atteint le niveau de finition souhaité. Les illustrations sont encore peu abouties
et quelques fautes d’orthographe subsistent. Nous avons toutefois livré ces notes
aux étudiants pour leur assurer un support clair dans la mesure ou le cours est
difficile à suivre sans support. Nous pensons délivrer une version plus aboutie
de ces notes de cours dès que possible.
197