Structure de données : Organisation d'une collection de données en vue de leur exploitation efficace (accès, modification, ...). Elle regroupe des données à gérer et un ensemble d'opérations qu'on peut leur appliquer.
Interface (ou Type abstrait de données) : Vue « logique » de la structure de données. Elle spécifie la nature des données ainsi que l'ensemble des opérations permises sur la structure.
Implémentation : Vue « physique » de la structure de données. Il s'agit de la programmation effective des opérations définies dans l'interface, en utilisant des types de données existants.
Remarque importante : L'interface est la partie visible pour qui veut utiliser ce type de données. Elle précise comment utiliser la structure de données sans se préoccuper de la façon dont les choses ont été programmées (son implémentation).
Rationnel
¶On aimerait définir une structure de données appelée Rationnel
correspondant à l'ensemble des nombres rationnels (noté $\mathbb Q$). Voici les opérations que l'on souhaite effectuer sur les rationnels :
On spécifie l'ensemble des opérations souhaitées en proposant l'interface suivante :
creerRationnel(n, d)
: crée un élément de type Rationnel
à partir de deux entiers n
(numérateur) et d
(dénominateur). Précondition : $d\neq 0$. numerateur(r)
: accès au numérateur du rationnel r
(renvoie un entier)denominateur(r)
: accès au dénominateur du rationnel r
(renvoie un entier non nul)ajouter(r1, r2)
: renvoie un nouveau rationnel correspondant à la somme des rationnels r1
et r2
soustraire(r1, r2)
: renvoie un nouveau rationnel correspondant à la différence des rationnels r1
et r2
(r1
-r2
)multiplier(r1, r2)
: renvoie un nouveau rationnel correspondant au produit des rationnels r1
et r2
egal(r1, r2)
: renvoie Vrai si les deux rationnels r1
et r2
sont égaux, Faux sinon.On ajoute à cela une opération permettant d'afficher un rationnel sous la forme d'une chaîne de caractères :
afficher(r)
: affiche le rationnel r
sous la forme d'une chaîne de caractères 'n/d'
où n
et d
sont respectivement le numérateur et le dénominateur de r
.L'interface apporte toutes les informations nécessaires pour utiliser le type de données. Ainsi, le programmeur qui l'utilise n'a pas à se soucier de la façon dont les données sont représentées ni de la manière dont les opérations sont programmées. L'interface (uniquement) lui permet d'écrire toutes les instructions qu'il souhaite et obtenir des résultats corrects. Par exemple, il sait qu'il peut écrire le programme suivant pour manipuler le type Rationnel
(écrit ici en Python mais on pourrait le faire dans un autre langage)
r1 = creerRationnel(1, 2)
den = denominateur(r1) # den vaut donc 2
r2 = creerRationnel(1, 3*den) # r2 représente le rationnel 1/6
r = ajouter(r1, r2) # r est le résultat de 1/2 + 1/6
egal(r, creerRationnel(2, 3)) # doit renvoyer True puisque 1/2 + 1/6 = 2/3
A faire : Activités 1 et 2 (partie A)
Pour implémenter une structure de données, c'est-à-dire programmer les opérations sur la structure, il est nécessaire d'utiliser les structures de données déjà définies dans le langage utilisé. En Python, on peut avantageusement utiliser les types int
, float
, boolean
ainsi que les tuples (avec le type tuple
), les tableaux (avec le type list
), les dictionnaires (avec le type dict
), les ensembles (avec le type set
).
Nous allons proposer deux implémentations : l'une avec un tuple (on aurait pu utiliser un tableau de manière identique) et l'autre avec un dictionnaire.
On peut par exemple implémenter (= programmer concrètement) le type abstrait Rationnel
en utilisant des couples (le type tuple
de Python). Voici ce que pourrait alors être l'implémentation de certaines des opérations du type Rationnel
.
Remarque : on n'a écrit ci-dessous que les opérations permettant d'illustrer le propos.
# IMPLEMENTATION AVEC UN COUPLE
def creerRationnel(n, d):
"""Entier x Entier --> Rationnel"""
return (n, d)
def denominateur(r):
"""Rationnel --> Entier"""
return r[1]
def ajouter(r1, r2):
"""Rationnel x Rationnel --> Rationnel"""
# calculs du numérateur et du dénominateurs en procédant à une réduction au même dénominateur
num = r1[0] * r2[1] + r2[0] * r1[1]
den = r1[1] * r2[1]
# simplification du rationnel en divisant le numérateur et le dénominateur par leur pgcd
d = pgcd(num, den)
return (num // d, den // d)
def egal(r1, r2):
"""Rationnel x Rationnel --> Booléen"""
return r1 == r2
def afficher(r):
"""Rationnel --> str"""
print(str(r[0]) + "/" + str(r[1]))
# Une fonction ne faisant pas partie de l'interface de la structure de données \
# mais utilisée dans la fonction ajouter
def pgcd(a, b):
"""Renvoie le pgcd des entiers positifs a et b"""
return a if b == 0 else pgcd (b,a) if b > a else pgcd (a-b, b)
On peut alors vérifier, même si on le savait déjà grâce à l'interface, que les instructions précédentes donnent des résultats corrects (on a pris le soin d'afficher certains résultats pour illustrer).
r1 = creerRationnel(1, 2)
afficher(r1) # pour vérifier
den = denominateur(r1)
print(den) # pour vérifier
r2 = creerRationnel(1, 3*den)
afficher(r2) # pour vérifier
r = ajouter(r1, r2)
afficher(r) # pour vérifier
egal(r, creerRationnel(2, 3))
Imaginons que le programmeur qui a implémenté le type abstrait Rationnel
ait fait des choix différents :
# IMPLEMENTATION AVEC UN DICTIONNAIRE ET PAS DE SIMPLIFICATIONS INTERMEDIAIRES
def creerRationnel(n, d):
"""Entier x Entier --> Rationnel"""
return {"num": n, "den": d}
def denominateur(r):
"""Rationnel --> Entier"""
return r["den"]
def ajouter(r1, r2):
"""Rationnel x Rationnel --> Rationnel"""
# calculs du numérateur et du dénominateurs en procédant à une réduction au même dénominateur
num = r1["num"] * r2["den"] + r2["num"] * r1["den"]
den = r1["den"] * r2["den"]
# pas de simplification !
return {"num": num, "den": den}
def egal(r1, r2):
"""Rationnel x Rationnel --> Booléen"""
# test d'égalité à modifier
return r1["num"] * r2["den"] == r2["num"] * r1["den"] # produit en croix
def afficher(r):
"""Rationnel --> str"""
print(str(r["num"]) + "/" + str(r["den"]))
On peut vérifier que l'on peut écrire exactement les mêmes instructions que précédemment et obtenir exactement les mêmes résultats, alors même que l'implémentation est totalement différente.
r1 = creerRationnel(1, 2)
afficher(r1) # pour vérifier
den = denominateur(r1)
print(den) # pour vérifier
r2 = creerRationnel(1, 3*den)
afficher(r2) # pour vérifier
r = ajouter(r1, r2)
afficher(r) # pour vérifier
egal(r, creerRationnel(2, 3))
Moralité : Le programmeur qui utilise une structure de données fait abstraction à la fois :
- de la manière dont les données sont représentées (ex. : l'écriture des instructions et les résultats sont les mêmes, que les données soient représentées par des couples ou des dictionnaires, ou autre chose...)
- de la manière dont les opérations sont programmées (ex. : le test d'égalité ne suit pas la même logique selon les deux implémentations mais le résultat est le même).
A faire : Activités 2 (partie B), 3 et 4
On parle de structure de données abstraites ou de type abstrait de données car au niveau de l'interface les données, leurs liens et les opérations permises sont caractérisées mais on ne sait pas (et on ne veut pas savoir) comment c'est fait concrètement (implémentation).
Pourquoi avoir recours à cette notion de type abstrait ?
Tout simplement pour définir des types de données non primitifs, c'est-à-dire non disponibles dans les langages de programmation courants. Les types primitifs sont par exemple les entiers, les flottants, les booléens.
Les structures de données abstraites que nous étudierons cette année peuvent être classées selon la nature de l'organisation de la collection de données :
Ces structures de données sont parfois implémentées nativement dans les langages de programmation comme type de données mais ce n'est pas toujours le cas. En Python, les listes (type
list
), les dictionnaires (typedict
) et les ensembles (typeset
) le sont mais pas les autres.
Les opérations usuelles d'une SDA sont :
Chaque opération doit être bien spécifiée (entrée, sorties, précondition) : c'est ce que l'on fait dans l'interface.
Pour réaliser un traitement algorithmique, les structures de données doivent être choisies :
sur le plan « logique » ⇒ choix d'une SDA
sur le plan « logique » ⇒ choix d'une implémentation de la SDA retenue
Nous reviendrons sur ces choix au fur et à mesure des structures étudiées au cours de l'année.
random
présente la spécification des opérations disponibles (son interface) et nous suffit pour l'utiliser. Il est néanmoins possible de voir l'implémentation en cliquant en haut de la page sur le lien vers le code source.Références :
Germain BECKER, Lycée Mounier, ANGERS
Ressource éducative libre distribuée sous Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International