Vous êtes sur la page 1sur 210

Avant-propos

Cet ouvrage, comme tous ceux de la collection Compétences attendues, s’adresse aux élèves qui souhaitent
travailler en autonomie.
Il est découpé en chapitres qui suivent le programme de la spécialité NSI de terminale. Chaque chapitre est
structuré en trois grandes parties :
• le cours développé, précis avec des encadrés qui soulignent le vocabulaire à connaître par cœur et les
concepts logiques à maîtriser ;
• les exercices classés par compétences attendues du programme avec devant chaque énoncé la démarche
scientifique exigée ;
• les corrigés détaillés de tous les exercices avec les remarques et conseils précieux, tirés de l’expérience des
auteurs, professeurs de lycée.
Au début du livre une introduction présente les grandes compétences associées à l’enseignement en Numérique
et Sciences Informatiques. Un index permettra aux lecteurs de travailler différemment en choisissant la
compétence liée à la démarche scientifique qu’il veut maîtriser.
Cet ouvrage a été conçu dans l’esprit des nouveaux programmes avec la volonté de répondre aux grands enjeux
de notre époque imposés par les nouvelles technologies.
Nous espérons qu’il vous donnera entière satisfaction.
Sommaire
Introduction
Les compétences en NSI
Partie 1
Structures de données
Chapitre 1
Structures de données linéaires et dictionnaires
Chapitre 2
Structures de données hiérarchiques
Chapitre 3
Structures de données relationnelles
Chapitre 4
Initiation à la programmation orientée objet
Partie 2
Bases de données
Chapitre 5
Les bases de données : du modèle conceptuel à l’implémentation dans le système de gestion des bases
de données relationnelles
Chapitre 6
Le langage SQL
Partie 3
Architecture matérielle, systèmes d’exploitation et réseaux
Chapitre 7
Architectures matérielles et systèmes d’exploitation
Chapitre 8
Réseaux et sécurité
Partie 4
Langages et programmation
Chapitre 9
Calculabilité, décidabilité, modularité et récursivité
Chapitre 10
Paradigmes de programmation et validation d’un programme
Partie 5
Algorithmique
Chapitre 11
Algorithmes sur les arbres binaires et sur les graphes
Chapitre 12
Méthodes d’optimisation et recherche textuelle
Table des matières détaillée
Introduction
Les compétences en NSI
L’enseignement de spécialité́ de numérique et sciences informa ques en terminale voie générale a pour objec f
de poser les fondements de l’informatique afin de se préparer à une poursuite d’études dans l’enseignement
supérieur.
Cet enseignement permet de développer les compétences suivantes :
• analyser et modéliser un problème en termes de flux et de traitement d’informations ;
• décomposer un problème en sous-problèmes, reconnaître des situa ons déjà̀ analysées et réu liser des
solutions ;
• concevoir des solutions algorithmiques ;
• traduire un algorithme dans un langage de programmation, en spécifier les interfaces et les interactions,
comprendre et réutiliser des codes sources existants, développer des processus de mise au point et de
validation de programmes ;
• mobiliser les concepts et les technologies utiles pour assurer les fonctions d’acquisition, de mémorisation, de
traitement et de diffusion des informations ;
• développer des capacités d’abstraction et de généralisation.

1 Les compétences
A Analyser et modéliser un problème en termes de flux et de traitement d’informations
Il s’agit d’appréhender les informations utiles à la résolution d’un problème ainsi que leur origine et le moyen de
les traiter. Puis de mettre en œuvre les moyens afin de produire une solution au problème. Au préalable, il faut
formaliser le problème, c’est-à-dire à expliciter dans un format technique (algorithme, langage, schéma…). Selon
les situations, le choix du format ou l’outil utilisé sera à la charge de l’élève.

Conseils du professeur
Il faut toujours, dans un premier temps, s’approprier le contexte, lire la totalité du sujet, parcourir les annexes disponibles, pour avoir une vision
globale de la problématique avant de débuter toute phase de formalisation. Ensuite, il ne faut pas hésiter à prendre un papier brouillon et d’y
mettre quelques idées avant de résoudre un problème sur ordinateur. Ceci permet d’organiser vos idées et vous fera gagner du temps par la suite.

B Décomposer un problème en sous-problèmes, reconnaître des situa ons déjà̀ analysées et réu liser des
solutions
Un problème à résoudre peut être assez vaste au premier abord et donc un peu déroutant. Une astuce consiste
à décomposer le problème en plusieurs sous-problèmes que l’on peut résoudre plus facilement. On retombera
souvent sur des problèmes déjà résolus par le passé ou tout au moins sur des situations de départ similaires à
certaines vues par le passé.

Conseils du professeur
Avant de débuter une conception ou une réalisation, nous devons faire un état des lieux de l’existant : qu’avons-nous à notre disposition ? Quelles
sont les technologies disponibles dans ce contexte ? Sur quel existant puis-je me baser ?

C Concevoir des solutions algorithmiques


Un algorithme est une méthode précise constituée d’une suite d’instructions logiques (d’étapes) et permettant
de résoudre un problème. Il s’agit finalement d’une liste d’étapes à suivre pour parvenir à une solution. C’est un
peu comme une recette de cuisine ou un protocole opératoire en laboratoire. La conception va permettre de
produire une solution qui corresponde parfaitement à la problématique, de manière optimisée. Il est nécessaire de
connaitre des méthodes

Remarque
Il existe bien souvent plusieurs algorithmes qui mènent à la même solution.

Conseils du professeur
Il est nécessaire de bien connaitre les méthodes disponibles dans chaque domaine : méthodes d’analyse pour le développement, normes pour les
infrastructures, conventions, réglementation…
Il est impossible de se dispenser de cette phase de conception.
D Traduire un algorithme dans un langage de programmation, en spécifier les interfaces et les interactions,
comprendre et réutiliser des codes sources existants, développer des processus de mise au point
et de validation de programmes
Une fois l’algorithme écrit, il doit être traduit en une suite d’instructions comprises par une machine, (comme
un ordinateur, ou une calculatrice). La machine sera alors à même d’effectuer les calculs nécessaires qui mènent à
la solution du problème étudié. Cette étape de traduction s’intitule la programmation de l’algorithme.
Une fois le programme écrit, il est essentiel de vérifier sa validité, c’est-à-dire que le programme est correct et
qu’il produit bien le résultat attendu.

Conseils du professeur
Nous avons dit qu’il existe bien souvent plusieurs algorithmes qui mènent à la même solution. Il en va de même pour leur programmation : il
existe bien souvent plusieurs façons de programmer un même algorithme. Aussi, lorsque vous écrirez de programmes, il faudra bien garder en
tête qu’il n’existe pas toujours de solution unique à un même problème.

E Mobiliser les concepts et les technologies utiles pour assurer les fonctions d’acquisition, de mémorisation,
de traitement et de diffusion des informations
Dans le monde actuel, la récolte, le stockage et la diffusion de l’information est devenue une activité
primordiale. Aussi, les entreprises utilisent elles-mêmes des systèmes d’information (appelés SI) qui sont
justement des ensembles de ressources qui permet de collecter, stocker, traiter et distribuer de l’information, en
général grâce à des ordinateurs.
Le programme de Numérique et Sciences Informatiques ne requiert pas la connaissance d’un système
d’information en particulier mais plutôt la connaissance de quelques technologies permettant l’acquisition, la
mémorisation, le traitement et la diffusion de données. Il faut savoir y faire appel à bon escient.

F Développer des capacités d’abstraction et de généralisation


L’écriture d’algorithmes et de programmes informatiques vous permettra de développer un forte capacité
d’abstraction qui sera répercutée sur votre capacité à résoudre un problème aussi bien en mathématiques, qu’en
physique, chimie, SVT etc. Vous devrez en effet bien souvent partir d’un problème très spécifique et développer
une résolution plus abstraite en modélisant les variables par des lettres, les phénomènes par une formule
mathématique.

Conseils du professeur
Cette démarche d’abstraction vous sera bien utile en cours de physique chimie où vous devrez bien souvent tâcher de parvenir à une expression
littérale plutôt que d’effectuer votre raisonnement sur des valeurs numériques.

G Compétences transversales
Au cours de ce livre, diverses activités sont proposées, celles-ci vous permettront de :
• Faire preuve d’autonomie, d’ini a ve et de créa vité́ ;
• Présenter un problème ou sa solution, développer une argumentation dans le cadre d’un débat ;
• Coopérer au sein d’une équipe dans le cadre d’un projet ;
• Rechercher de l’information, partager des ressources ;
• Faire un usage responsable et critique de l’informatique.

2 Récapitulatif des exercices illustrant les compétences


Compétences Exercices concernés

▶ Analyser et modéliser un problème en terme de flux et de traitement d’informations Chap. 1 : 1.4, 1.5
Chap. 3 : 3.1, 3.2
Chap. 4 : 4.1
Chap. 5 : 5.1, 5.2, 5.3
Chap. 6 : 6.5, 6.6, 6.7
Chap. 7 : 7.1, 7.2, 7.3
Chap. 8 : 8.2, 8.4, 8.5,
8.6
Chap. 9 : 9.1, 9.2
Chap. 11 : 11.3, 11.4
Chap. 12 : 12.1, 12.2

▶ Décomposer un problème en sous-problèmes, reconnaitre des situations déjà analysées et réutiliser des solutions. Chap. 1 : 1.3, 1.5
Chap. 2 : 2.1, 2.3
Chap. 3 : 3.4, 3.6
Chap. 5 : 5.3, 5.4
Chap. 8 : 8.1, 8.4, 8.5
Chap. 9 : 9.3
Chap. 10 : 10.2
Chap. 12 : 12.2

▶ Concevoir des solutions algorithmiques Chap. 4 : 4.1


Chap. 6 : 6.1, 6.2, 6.3,
6.4
Chap. 7 : 7.4
Chap. 9 : 9.3, 9.4
Chap. 12 : 12.1, 12.3,
12.4

▶ Traduire un algorithme dans un langage de programmation, spécifier les interfaces et les interactions, développer des processus de mise Chap. 1 : 1.6
au point et de validation de programmes Chap. 4 : 4.1, 4.2
Chap. 5 : 5.7
Chap. 6 : 6.1, 6.2, 6.3,
6.4
Chap. 10 : 10.1, 10.3,
10.4, 10.5
Chap. 12 : 12.5

▶ Mobiliser les concepts et les technologies utiles pour assurer les fonctions d’acquisition, de mémorisation, de traitement et de diffusion Chap. 2 : 2.2, 2.5
des informations Chap. 3 : 3.3
Chap. 5 : 5.1, 5.2, 5.5
Chap. 7 : 7.1, 7.2, 7.3
Chap. 8 : 8.1, 8.2
Chap. 9 : 9.4
Chap. 10 : 10.5
Chap. 11 : 11.1, 11.2

▶ Développer des capacités d’abstraction et de généralisation Chap. 1 : 1.1, 1.2


Chap. 2 : 2.4
Chap. 3 : 3.5
Chap. 4 : 4.2
Chap. 5 : 5.1, 5.5
Chap. 8 : 8.3, 8.7
Chap. 9 : 9.1, 9.2
Chap. 10 : 10.1, 10.3
Chap. 12 : 12.4
Partie 1
Structures de données
Chapitre 1
Structures de données linéaires et dictionnaires
Cours

1 Généralités
Les structures de données abstraites
Les algorithmes opèrent sur des données qui peuvent être de différentes natures. La première version d’un
algorithme est autant que possible indépendante d’une implémentation particulière, c’est-à-dire que la
représentation des données n’est pas fixée.
À ce premier niveau les données sont considérées de manière abstraite, on se donne une notation pour les décrire
ainsi que l’ensemble des opérations que l’on peut appliquer et les propriétés de ces opérations. On parle alors de
type abstrait de données.
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.

Rappels
Les types de données primitifs sont par exemple les entiers, les flottants, les booléens.

Nous étudierons 4 types de structures de données abstraites :


• Les structures linéaires : les listes, les piles et les files
• Les structures à accès par clé : les dictionnaires
• Les structures hiérarchiques : les arbres (cf. chapitre 2)
• Les structures relationnelles : les graphes (cf. chapitre 3)

Remarques
Ces structures de données sont parfois « prévues » nativement dans les langages de programmation comme type de données mais ce n’est pas toujours
le cas !

Une structure de données possède un ensemble de routines (procédures ou fonctions) permettant d’ajouter,
d’effacer, d’accéder aux données. Cet ensemble de routines est appelé interface.
Les opérations élémentaires
L’interface est généralement constituée des 4 routines élémentaires dites CRUD :
• Create : ajout d’une donnée.
• Read : lecture d’une donnée.
• Update : modification d’une donnée.
• Delete : suppression d’une donnée.
Derrière les opérations de lecture, de modification ou de suppression d’une donnée, se cache une autre routine
tout aussi importante : la recherche d’une donnée.

Précision
Il faut d’abord trouver la donnée dans la structure avant de pouvoir la lire, la modifier ou la supprimer.

2 Les listes
Définition
Une liste est une structure de données permettant de regrouper des données et dont l’accès est séquentiel. Elle
correspond à une suite finie d’éléments repérés par leur rang (index). Les éléments sont ordonnés et leur place a
une grande importance.
Une liste est évolutive, on peut ajouter ou supprimer n’importe quel élément.
Voici les opérations qui peuvent être effectuées sur une liste créée :
• INSERER (L, x, i) qui repère la donnée de rang i-1 puis décale de 1 rang à droite tous les éléments situés derrière
lui et insère x au rang i.
• SUPPRIMER (L, i) qui repère la donnée de rang i (en commençant par le premier élément) puis décale de 1 rang à
gauche tous les éléments situés derrière lui.

Attention
Le type liste de Python ne correspond pas à la structure de données liste. En Python, le type « liste » représente un tableau dynamique.

Type Abstrait Liste


Type abstrait : Liste
Données : éléments de type T
Opérations
CREER_LISTE_VIDE() qui retourne un objet de type Liste
La liste existe et elle est vide.
INSERER(L, e, i)
L’élément e est inséré à la position i dans la liste L.
SUPPRIMER(L, i)
L’élément situé à la position i est supprimé de la liste L.
RECHERCHER(L, e) qui retourne un objet de type Entier
L’élément e est cherché dans la liste L et on retourne son index (sa position).
LIRE(L, i) qui retourne un objet de type T
L’élément situé à la position i dans la liste L est retourné.
MODIFIER(L, i, e)
L’élément situé à la position i dans la liste L est écrasé par le nouvel élément e.
LONGUEUR(L) qui retourne un objet de type Entier
Le nombre d’éléments présents dans la liste L est retourné.
Conditions
LIRE(L, i) est défini si et seulement si
MODIFIER(L, i, e) est défini si et seulement si
SUPPRIMER(L, i) est défini si et seulement si
INSERER(L, e, i) est défini si et seulement si
Exemple d’application de ce type abstrait
Prenons la suite d’instructions suivantes :
L = CREER_LISTE_VIDE()
INSERER(L, ‘A’, 1)
INSERER(L, ‘O’, 2)
INSERER(L, ‘B’, 1)
INSERER(L, ‘V’, 3)
INSERER(L, ‘R’, 2)
On voit assez aisément qu’à la fin, la liste L contient 5 éléments de type « caractère » et L = (‘B’, ‘R’, ‘A’, ‘V’, ‘O’).
Représentation d’une liste avec un tableau
Il existe plusieurs façons de concevoir une liste. La plus simple consiste à utiliser un tableau (de taille fixe) dont
chaque élément est identifié par son indice. Tous les langages de programmation permettent de réaliser des
tableaux. Pour le langage Python, le plus simple est d’utiliser des listes.
On peut par exemple réaliser une liste capable de contenir n éléments avec un tableau pouvant contenir
éléments :
• La première case du tableau (d’indice 0) contient le nombre d’éléments présents dans la liste.
• Les cases suivantes du tableau (d’indices 1 à n), contiennent les éléments de la liste ou sont vides.

Remarque
Si ( ) la liste est vide. À chaque fois qu’on insère un élément dans la liste, on augmente d’une unité.
Lorsque ( ), la liste est pleine. De la même façon, lorsqu’on supprime un élément, on diminue d’une unité.

Représentation d’une liste de taille maximale 5 éléments

Remarques
Il existe plusieurs façons de remplir le tableau après avoir créé un tableau vide de 6 éléments et initialiser la première case à 0.
1re façon (la plus intuitive) :
• On insère 8 en position 1
• On insère 3 en position 2
• On insère 5 en position 3
On peut également faire :
• On insère 3 en position 1
• On insère 5 en position 2
• On insère 8 en position 1
Il y a donc une certaine souplesse, mais ces différentes façons ne sont pas équivalentes en accès mémoire (à cause des recopies de cases pour les
décaler).

Pseudo-code
Fonction INSERER(L, x, i) :
Si (L[0] == n) OU (i-1>L[0]) :
Retourner Faux
Sinon :
Pour k allant de (L[0]+1) à (i+1) par pas de (-1)
L[k] = L[k-1]
L[i] = x
L[0] = L[0] + 1
Retourner Vrai

Fonction SUPPRIMER(L, i) :
Si (L[0] != 0) ET (i<=L[0]) :
Pour k allant de i à (L[0]-1) par pas de 1
L[k] = L[k+1]
L[0] = L[0] - 1
Retourner Vrai
Sinon :
Retourner Faux
Implémentation en Python
On peut par exemple proposer le code suivant :
Regardons en détail le contenu de la liste Ma_liste depuis sa création à la ligne 24.

Remarque
Bien que l’élément 5 apparaisse encore dans le tableau en position 3, il n’est plus dans la liste.

Parmi les listes, il existe 2 structures particulières dont les accès mémoire sont optimisés : il s’agit des piles et des
files.

3 Les piles
Définition
Il s’agit d’une structure de données qui donne accès en priorité aux dernières données ajoutées. Ainsi, la dernière
information ajoutée sera la première à en sortir. Autrement dit, on ne peut accéder qu’à l’objet situé au sommet
de la pile. On décrit souvent ce comportement par l’expression « dernier entré, premier sorti » ou encore LIFO :
Last In, First Out.

Analogie
Le rangement des assiettes convient à cette description. En effet l’ordre dans lequel les assiettes sont dépilées est l’inverse de celui dans lequel elles
ont été empilées, puisque seule l’assiette supérieure est accessible.

Les 2 opérations élémentaires que l’on a besoin avec cette structure sont :
• EMPILER (P, x) qui correspond à l’insertion de la donnée x au sommet de la pile P si la pile n’est pas pleine.
• DEPILER (P) qui retire la dernière donnée de la pile P et la retourne si la pile n’est pas vide.

On peut également définir d’autres opérations comme :


• PILE_VIDE(P) qui indique si la pile P est vide ou non.
• PILE_PLEINE(P) qui indique si la pile P est pleine ou non.
Type Abstrait Pile
Type abstrait : Pile
Données : éléments de type T
Opérations
CREER_PILE_VIDE() qui retourne un objet de type Pile
La pile existe et elle est vide.
EMPILER(P, e)
L’élément e est inséré au sommet de la pile P.
DEPILER(P) qui retourne un objet de type T
L’élément situé au sommet de la pile P est enlevé de la pile et est retourné.
EST_VIDE(P) qui retourne un objet de type Booléen
Retourne Vrai si la pile P est vide et retourne Faux sinon.
EST_PLEINE(P) qui retourne un objet de type Booléen
Retourne Vrai si la pile P est pleine et retourne Faux sinon.
Conditions
EMPILER(P, e) est défini si et seulement si EST_PLEINE(P) = Faux
DEPILER(P) est défini si et seulement si EST_VIDE(P) = Faux
Exemple d’application de ce type abstrait
Prenons la suite d’instructions suivantes :
P = CREER_PILE_VIDE()
EMPILER(P, 3)
EMPILER(P, 2)
N = DEPILER(P)
EMPILER(P, 5)
EMPILER(P, 7)
EMPILER(P, 9)
On voit assez aisément qu’à la fin, la pile P contient 4 éléments de type « entier » et P = (3, 5, 7, 9) et N = 2.
Représentation d’une pile avec un tableau
On peut par exemple réaliser une pile capable de contenir n éléments avec un tableau pouvant contenir
éléments :
• La première case du tableau (d’indice 0) contient l’indice de la prochaine case vide (c’est l’indice qui
correspondra au prochain élément à insérer dans la pile).
• Les cases suivantes du tableau (d’indices 1 à n), contiennent les éléments de la pile ou sont vides. La dernière
case non vide du tableau est le sommet de la pile.

Remarque
Si ( ) la pile est vide. À chaque fois qu’on insère un élément, on augmente d’une unité.
Lorsque ( ), la pile est pleine.

Exemple avec n = 5
Pseudo-code
Fonction EMPILER(P, x) :
Si (P[0] == n+1) :
Retourner Faux
Sinon :
i = P[0]
P[i] = x
P[0] = i + 1
Retourner Vrai

Fonction DEPILER(P) :
Si P[0] != 1 :
P[0] = P[0] - 1
i = P[0]
Retourner P[i]
Implémentation en Python
On peut par exemple proposer le code suivant :

Regardons en détail le contenu de la pile Ma_pile depuis sa création à la ligne 21.

Remarque
Bien que l’élément 5 apparaisse encore dans le tableau, il n’est plus dans la pile. Le sommet étant l’élément 3.

4 Les files
Définition
Une file est une structure de données dans laquelle on accède aux éléments suivant la règle du premier arrivé
premier sorti. Autrement dit, on ne peut accéder qu’à l’objet situé au début de la file. On décrit souvent ce
comportement par l’expression « premier entré, premier sorti » ou encore FIFO : First In, First Out. La file
comporte une tête et une queue.

Analogie
La file de clients qui attend à un guichet ou à une caisse convient à cette description. En effet le client qui passe en premier est celui qui est arrivé le
premier.

Les 2 opérations élémentaires que l’on a besoin avec cette structure sont :
• ENFILER(F, x) qui correspond à l’insertion de la donnée x à la queue de la file F si la file n’est pas pleine.
• DEFILER(F) qui retire la donnée de tête de la file F et la retourne si la file n’est pas vide.

Type Abstrait File


Type abstrait : File
Données : éléments de type T
Opérations
CREER_FILE_VIDE() qui retourne un objet de type File
La file existe et elle est vide.
ENFILER(F, e)
L’élément e est inséré en queue de la file F.
DEFILER(F) qui retourne un objet de type T
L’élément situé en tête de la file F est enlevé de la file et est retourné.
EST_VIDE(F) qui retourne un objet de type Booléen
Retourne Vrai si la file F est vide et retourne Faux sinon.
EST_PLEINE(F) qui retourne un objet de type Booléen
Retourne Vrai si la file F est pleine et retourne Faux sinon.
Conditions
ENFILER(F, e) est défini si et seulement si EST_PLEINE(F) = Faux
DEFILER(F) est défini si et seulement si EST_VIDE(F) = Faux
Exemple d’application de ce type abstrait
Prenons la suite d’instructions suivantes :
F = CREER_FILE_VIDE()
ENFILER(F, 21)
ENFILER(F, 22)
ENFILER(F, 23)
N = DEFILER(F)
ENFILER(F, 24)
ENFILER(F, 25)
N = DEFILER (F)
On voit assez aisément qu’à la fin, la file F contient 3 éléments de type « entier » et F = (23, 24, 25) et N = 22.
Représentation d’une file avec un tableau
On peut par exemple réaliser une file capable de contenir n éléments avec un tableau pouvant contenir
éléments :
• La première case du tableau (d’indice 0) contient l’indice de la tête de la file.
• La deuxième case du tableau (d’indice 1) contient l’indice de la queue de la file, c’est-à-dire la prochaine case
disponible pour la queue.
• La troisième case du tableau (d’indice 2) contient le nombre d’éléments présents dans la file.
• Les cases suivantes du tableau (d’indices 3 à n + 2), contiennent les éléments de la file ou sont vides.

Remarque
Si ( ) la file est vide et si ( ) la file est pleine.
À chaque fois qu’on enfile un élément, on augmente la taille d’une unité ainsi que la queue.
À chaque fois qu’on défile un élément, on diminue la taille d’une unité et on augmente la tête d’une unité.
Dès que les indices de tête ou de queue dépassent la longueur du tableau, ils repartent au début du tableau (on appelle cela un tableau avec une
gestion circulaire).

Représentation pour n = 5

Après enfilage de 8, 3 et 5

Après un défilage
Pseudo-code
Fonction ENFILER(F, x) :
Queue = F[1]
Taille = F[2]
Si Taille == n :
Retourner Faux
Sinon :
F[Queue] = x
Si (Queue == (n + 3)) :
Queue = 3
Sinon
Queue += 1
Taille += 1
F[2] = Taille
F[1] = Queue
Retourner Vrai

Fonction DEFILER(F) :
Tete = F[0]
Taille = F[2]
Si (Taille != 0) :
X = F[Tete]
Si (Tete == (n + 3)) :
Tete = 3
Sinon :
Tete += 1
Taille -= 1
F[2] = Taille
F[0] = Tete
Retourner X
Implémentation en Python
On peut par exemple proposer le code suivant :

Regardons en détail le contenu de la pile Ma_file depuis sa création à la ligne 38.


Remarque
Tout comme pour les piles, bien que l’élément 8 apparaisse encore dans le tableau, il n’est plus dans la file. L’indice de la tête étant bien 4.

5 Les dictionnaires
Définition
Un dictionnaire est une structure de données qui permet d’associer une valeur à une clé. Cette clé peut être un
mot ou un entier. L’ensemble clé-valeur est appelé entrée. On peut définir par exemple une structure dictionnaire
pour un contact téléphonique en posant 2 clés : nom et numéro de téléphone. On pourrait ainsi avoir :
Nom : Toto
Numéro de téléphone : 06 12 34 56 78
On peut rappeler que les dictionnaires ne comportent pas d’ordre. Contrairement aux listes, on ne peut pas
retrouver un élément via sa position, mais uniquement via sa clé. Il s’agit d’une structure de données très
commune et très pratique !

Précision
Cette structure de données est appelée dictionnaire car dans un dictionnaire, on associe un mot (la clé) à sa définition (la valeur).

Il faut noter que Python possède nativement cette structure de données, ce qui n’est pas forcément vrai dans
d’autres langages de programmation. Il faudra alors l’implémenter si on souhaite l’utiliser.
Type Abstrait Dictionnaire
Type abstrait : Dictionnaire
Données : éléments de type T
Opérations
CREER_DICO_VIDE() qui retourne un objet de type Dictionnaire
Le dictionnaire existe et il est vide.
INSERER(D, cle, valeur)
L’entrée cle-valeur est insérée dans le dictionnaire D.
SUPPRIMER(D, cle)
L’entrée dont la clé est cle est supprimée du dictionnaire.
LIRE(D, cle) qui retourne un objet de type T
La valeur correspondante à l’entrée dont la clé est cle est retournée.
RECHERCHER(D, cle) qui retourne un objet de type Booléen
Retourne Vrai s’il existe une entrée dont la clé est cle dans le dictionnaire D et retourne Faux sinon.
Conditions
LIRE(D, cle) est défini si et seulement si RECHERCHER(D, cle) = Vrai
SUPPRIMER(D, cle) est défini si et seulement si RECHERCHER(D, cle) = Vrai
Implémentation
L’implémentation des dictionnaires en Python a déjà été vue en classe de première. L’exemple ci-après montre un
dictionnaire constitué de 3 entrées.
6 Applications
Les applications à base de piles et de files sont nombreuses. On pourra par exemple citer la gestion sous forme de
file pour l’impression. En effet, si nous souhaitons imprimer plusieurs documents, celui qui aura été lancé en
premier sera le premier imprimé. Dans un navigateur web, une pile peut servir à mémoriser les pages visitées.
L’adresse de chaque nouvelle page visitée est empilée et en cliquant sur un bouton « Afficher la page
précédente », l’utilisateur dépile l’adresse de la page précédente.
Exercices

Compétences attendues
Choisir un type de données en fonction d’un problème à résoudre.

Exercice 1.1
▶ Développer des capacités d’abstraction
Quelle structure de données choisir pour chacune de ces tâches ?
1. Représenter un répertoire téléphonique.
2. Stocker l’historique des actions effectuées dans un logiciel et disposer d’une commande Annuler (ou Undo).
3. Envoyer des fichiers au serveur d’impression.
Compétences attendues
Savoir raisonner à l’aide du type abstrait Liste.

Exercice 1.2
▶ Développer des capacités d’abstraction et de généralisation
On donne la séquence d’instructions suivante :
L1 = CREER_LISTE_VIDE()
L2 = CREER_LISTE_VIDE()
INSERER(L1, 1, 1)
INSERER(L1, 2, 2)
INSERER(L1, 3, 3)
INSERER(L1, 4, 4)
INSERER(L2, LIRE(L1, 1) , 1)
INSERER(L2, LIRE(L1, 2) , 1)
INSERER(L2, LIRE(L1, 3) , 1)
INSERER(L2, LIRE(L1, 4) , 1)
1. Illustrer le résultat de chaque étape de cette séquence
2. Quelle est l’opération effectuée ?
Compétences attendues
Savoir raisonner à l’aide du type abstrait File.

Exercice 1.3
▶ Reconnaître des situations déjà analysées et réutiliser des solutions
On donne la séquence d’instructions suivante :
F = CREER_FILE_VIDE()
ENFILER(F, 4)
ENFILER(F, 1)
ENFILER(F, 3)
N = DEFILER(F)
ENFILER(F, 8)
N = DEFILER(F)
Illustrer le résultat de chaque étape de cette séquence.
Compétences attendues
Savoir raisonner à l’aide des types abstrait File et Pile.

Exercice 1.4
▶ Analyser et modéliser un problème
On suppose que l’on a déjà une file F1 qui contient les éléments suivants saisis dans l’ordre alphabétique F1 = (‘A’,
‘B’, ‘C’, ‘D’, ‘E’).
1. Quel est l’élément issu d’un défilage de F1 ?
2. Proposer une séquence d’instructions (à l’aide de 2 piles P1 et P2) permettant la saisie d’affilée (sans sortie
intermédiaire) des 5 éléments ‘A’, ‘B’, ‘C’, ‘D’ et ‘E’ et de sortir ces éléments comme s’ils sortaient d’une file.
3. Que faudrait-il faire pour avoir exactement le même fonctionnement qu’avec une file, c’est-à-dire avec sortie
éventuelle d’élément.

Exercice 1.5
▶ Analyser et modéliser un problème
▶Décomposer un problème en sous-problème
La Notation Polonaise Inversée (NPI) permet d’écrire des opérations arithmétiques, sans utiliser de parenthèses.
Ici, nous nous limiterons à des nombres entiers naturels et aux opérations +, -, * et / sur eux. Dans cette
notation, les opérateurs sont écrits après les opérandes (nombres entiers naturels). Par exemple l’expression
classique :

Donne en NPI :

On écrit et on exécute les opérations dans le sens des priorités vues en cours de mathématiques. Dans cette
notation, on réalise :
• L’addition entre 3 et 2 ( )
• La multiplication entre le précédent résultat et 13 ( )
• On a ainsi le résultat.
1. Donner la File correspondante à la saisie NPI de l’exemple. Faire de même avec la Pile.
2. Quelle est la structure adaptée à la résolution de l’expression ?
Note : On remarquera qu’on doit toujours avoir 2 opérandes pour un opérateur. Il faut stocker le résultat
intermédiaire dans la structure pour effectuer la suite des calculs.
3. En utilisant les opérations du type abstrait Pile, proposer une fonction permettant d’afficher le résultat d’une
expression en NPI.
Note : On pourra considérer qu’on a déjà la fonction INVERSER_PILE(P) qui retourner une Pile qui est dans l’ordre
inverse de celle donnée en argument. On supposera également que la syntaxe en NPI est correcte.
Compétences attendues
Savoir Implémenter une structure à l’aide d’un type donné en Python.

Exercice 1.6
▶ Traduire un algorithme dans un langage de programmation
Étudier les fonctions append() et pop() du type liste de Python à l’aide de la documentation Python.
1. Proposer une implémentation des opérations classiques de la pile à l’aide des fonctions pop() et append() du
type liste de Python.
Les opérations classiques sont : CREER_PILE, EMPILER, DEPILER, EST_VIDE.
2. Proposer de la même manière une implémentation des opérations classiques de la file en Python.
Exercice-bilan
Exercice-bilan 1.1
60 min ● ... points
Dans un logiciel de calcul formel ou, plus généralement dans un éditeur de texte (par exemple utilisé pour écrire
des programmes), il y a une gestion dynamique du parenthésage. Par exemple, les deux expressions suivantes sont
erronées :

(deux parenthèses fermantes pour une ouvrante)

(deux parenthèses ouvrantes pour une fermante)


L’objectif de cet exercice est de proposer une solution informatique pour programmer une fonction qui reçoit
comme argument une chaîne de caractères constituée uniquement de parenthèses ouvrantes et fermantes (pas
d’autres caractères), Cette fonction analyse le parenthésage et renvoie à l’utilisateur un message adapté. On se
propose de plus d’indiquer, pour chaque parenthèse ouvrante, la position de la parenthèse fermante
correspondante. Ainsi, pour le mot ‘(())()’, on donnera les couples d’indices (1,2), (0,3) et (4,5).
L’idée consiste à parcourir le mot de la gauche vers la droite et à utiliser une pile pour indiquer les indices de
toutes les parenthèses ouvertes, et non encore fermées, vues jusqu’à présent.
1. En utilisant les opérations du type abstrait Pile, proposer une fonction booléenne permettant de retourner Vrai
si la chaîne donnée en argument est bien parenthésée et Faux sinon.
2. Implémenter cette fonction en Python. On pourra utiliser l’implémentation de la pile vue dans l’exercice 1.4.
Corrigé des exercices
Exercice 1.2
1. Pour réaliser un répertoire téléphonique, la structure la plus adaptée est sans doute le dictionnaire.

Précisions
On aura par exemple a minima, comme clés, le nom et le numéro de téléphone.

2. La commande Annuler annule la dernière action faite, on se trouve dans la logique du dernier entré, premier
sorti. On choisira donc une Pile.
3. Les fichiers à imprimer sont dans la logique du premier envoyé, premier imprimé. C’est le concept de la file.

Exercice 1.2
1.
L1 = CREER_LISTE_VIDE() L1 = ()
L2 = CREER_LISTE_VIDE() L2 = ()
INSERER(L1, 1, 1) L1 = (1)
INSERER(L1, 2, 2) L1 = (1, 2)
INSERER(L1, 3, 3) L1 = (1, 2, 3)
INSERER(L1, 4, 4) L1 = (1, 2, 3, 4)
INSERER(L2, LIRE(L1, 1) , 1) L2 = (1)
INSERER(L2, LIRE(L1, 2) , 1) L2 = (2, 1)
INSERER(L2, LIRE(L1, 3) , 1) L2 = (3, 2, 1)
INSERER(L2, LIRE(L1, 4) , 1) L2 = (4, 3, 2, 1)
2. Dans la liste L2, on a les mêmes éléments de la liste L1 mais ordonnés dans l’autre sens.

Exercice 1.3
F = CREER_FILE_VIDE() F = ()
ENFILER(F, 4) F = (4)
ENFILER(F, 1) F = (4, 1)
ENFILER(F, 3) F = (4, 1, 3)
N = DEFILER(F) F = (1, 3) et N=4
ENFILER(F, 8) F = (1, 3, 8)
N = DEFILER(F) F = (3, 8) et N=1

Exercice 1.4
1. L’action du défilage d’un élément de F1 renvoie l’élément A.
2. Il faut d’abord visualiser la situation par un schéma avant de proposer des instructions. Le voici :

Il faut d’abord empiler tous les éléments dans la pile P1 et une fois tous empilés, les dépiler un à un pour les
empiler dans la pile P2. Les instructions donnent alors :
P1 = CREER_PILE_VIDE()
P2 = CREER_PILE_VIDE()
EMPILER(P1,’A’)
EMPILER(P1,’B’)
EMPILER(P1,’C’)
EMPILER(P1,’D’)
EMPILER(P1,’E’)
Tant que EST_VIDE(P1) == Faux
N=DEPILER(P1)
EMPILER(P2,N)
3. Pour établir un fonctionnement similaire à une file avec 2 piles, il faut penser au dépilage intermédiaire. C’est-à-
dire que l’enfilage (avec 2 piles) se fera toujours sur la pile P1 et le défilage (avec 2 piles) se fera toujours sur la
pile P2 mais à une seule condition, c’est que pour chacune des fonctions, il faut que l’autre pile soit vide (c’est-à-
dire dépiler sur l’autre pile)

Remarque
Le fonctionnement de ces piles permet en fait la même gestion qu’une file. Cependant cela nécessite beaucoup d’instructions en mémoire.

Exercice 1.5
1. Après saisie, la pile peut être représentée par

De même la file :

2. La File donne l’apparence que nos éléments peuvent sortir dans le bon ordre (par la gauche), cependant on ne
pourra pas stocker dans la file, au bon endroit, le résultat intermédiaire (vu que l’enfilage se fait par la droite !).
Ce n’est donc pas la structure adaptée.
Dans le cas de la Pile, on s’aperçoit qu’il faut inverser les éléments pour qu’ils puissent sortir dans le bon ordre.
La saisie correspond à un empilage successif dans P1, pour inverser, il faut dépiler toute la pile P1 dans P2.

Précisions
Il faudra prendre les 2 premiers éléments de la pile (opérandes) et le troisième (opérateur) pour empiler le résultat intermédiaire et répéter jusqu’au
résultat final.

3. En utilisant les opérations du type abstrait Pile et la fonction INVERSER(), on peut proposer la fonction suivante :
Fonction RESULTAT(P) :
N = INVERSER(P)
Resultat_trouve = faux
Resultat = 0
Tant que Resultat_trouve == Faux
A=DEPILER(N)
Si EST_VIDE(N)== Vrai :
Resultat_trouve = Vrai
Resultat = A
Sinon :
B= DEPILER(N)
Op = DEPILER(N)
Si Op == ‘+’ :
EMPILER(N, A+B)
Sinon :
Si Op == ‘-‘ :
EMPILER(N, A-B)
Sinon :
Si Op == ‘*’ :
EMPILER(N, A*B)
Sinon :
EMPILER(N, A/B)
Retourner Resultat

Exercice 1.6
1. On peut proposer le code Python suivant définissant les 4 opérations de bases sur les piles.

2. On peut proposer le code Python suivant définissant les 4 opérations de bases sur les files : la seule différence se
trouve sur la fonction défiler.
Corrigé de l’exercice-bilan
Exercice-bilan 1.1
1.
Fonction VERIFIER_PARENTHESES(chaine) :
P = CREER_PILE_VIDE()
Pour i allant de 0 à Longueur(chaine)-1 par pas de 1 :
Si chaine[i] == ‘(‘ :
EMPILER(P, i)
Sinon :
Si EST_VIDE(P) :
Retourner Faux
Sinon :
J = DEPILER(P)
Afficher le couple(j, i)
Retourner EST_VIDE(P)
2.
Chapitre 2
Structures de données hiérarchiques
Cours

1 Les arbres
Il n’existe pas seulement une façon linéaire de structurer les données comme les listes, les piles ou les files. Nous
pouvons également structurer de façon hiérarchique.
Un arbre est une structure de données constituée de nœuds, qui peuvent avoir des enfants (qui sont d’autres
nœuds). Cette structure est hiérarchisée et le sommet de l’arbre est appelé racine. Un nœud qui ne possède pas
d’enfant est appelé feuille. Les nœuds autre que la racine et les feuilles sont appelés nœuds internes. Une branche
est une suite finie de nœuds consécutifs de la racine vers une feuille. Un arbre a donc autant de branches que de
feuilles.

Dans l’exemple ci-dessus, l’arbre possède 11 nœuds dont :


• 1 racine (nœud A)
• 7 feuilles et donc 7 branches
• 3 nœuds internes
Un arbre peut être caractérisé par :
• Son arité : le nombre maximal d’enfant qu’un nœud peut avoir.
• Sa taille : le nombre de nœuds qui le compose.
• Sa hauteur : la profondeur à laquelle il faut descendre pour trouver la feuille la plus éloignée de la racine
(hiérarchie la plus basse).
Si nous reprenons l’exemple de l’arbre précédent, on peut dire que :
• Son arité est de 3. En effet, le nœud D a 3 enfants, c’est le maximum dans cet arbre.
• Sa taille est de 11 puisqu’il possède 11 nœuds.
• Sa hauteur est de 3. Nous avons 2 feuilles lointaines (J et K) qui sont au même niveau. Pour y parvenir, nous
avons parcouru depuis la racine 3 niveaux.

Analogie
On peut utiliser du vocabulaire emprunté au domaine de la généalogie. La racine est un ancêtre de tous les nœuds., chaque nœud est un descendant de
la racine. Les nœuds ayant le même parent sont des frères.

Dans la suite de ce cours, nous nous intéresserons qu’aux arbres d’arité 2 que l’on appelle arbres binaires.

2 Les arbres binaires


Les arbres binaires sont constitués de nœuds pouvant donner 0, 1 ou 2 enfants. On définit généralement à partir
du nœud racine 2 sous-arbres disjoints :
• Le sous-arbre gauche de l’arbre binaire (SAG)
• Le sous-arbre droit de l’arbre binaire (SAD)
Parmi les arbres binaires, il existe des cas particuliers :
• Arbre dégénéré (ou filiforme) : c’est un arbre dont les nœuds ne possèdent qu’un ou aucun enfant.

• Arbre localement complet : c’est un arbre (binaire) dont chacun des nœuds possède soit 2 enfants, soit aucun.

• Arbre complet : c’est un arbre qui est localement complet et dont toutes les feuilles sont au niveau hiérarchique
le plus bas.

3 Mesures sur les arbres binaires


Sur ces arbres, nous pouvons établir quelques opérations qui vont nous permettre de prendre des mesures et de
pouvoir déterminer la complexité des différents algorithmes qui leurs seront appliqués.
Ces opérations sont les suivantes :
• La taille d’un arbre B correspond au nombre de ses nœuds, elle est définie par :

• La hauteur d’un nœud d’un arbre B est définie par :

• La hauteur d’un arbre B est définie par :


Remarque
La hauteur est aussi appelée profondeur ou encore niveau. Cela revient également à dire que la hauteur d’un arbre correspond au nombre d’arêtes
entre la racine et la feuille la plus éloignée.

• La longueur de cheminement d’un arbre B est définie par :

• La longueur de cheminement externe d’un arbre B possédant NF feuilles est définie par :

• La longueur de cheminement interne d’un arbre B possédant NF feuilles est définie par :

On a alors la relation :
• La profondeur moyenne d’un arbre B est définie par :

• La profondeur moyenne externe d’un arbre B possédant NF feuilles est définie par :

• La profondeur moyenne interne d’un arbre B possédant NF feuilles est définie par :

Remarque
Des mesures comme la longueur de cheminement et la profondeur moyenne seront très utiles pour déterminer la complexité des algorithmes
appliqués aux arbres binaires.

Essayons de mettre en pratique ces opérations sur l’arbre binaire suivant a :

Commençons d’abord par donner les caractéristiques de a :


• Le nœud A est la racine de a
• Le sous arbre gauche de a : SAG(a) est l’arbre de racine B
• Le sous arbre droit de a : SAD(a) est l’arbre de racine C
· a possède 6 feuilles : J, K, L, M, N et O. On notera NF = 6.
· a possède 6 branches : ABEJ, ACFK, ACGL, ACGM, ABDHN et ABEIO
Évaluons maintenant les mesures :
• Taille :
• Hauteur des nœuds :





• Hauteur de a : H(a) = Max(Hauteur des nœuds ) = 4
• Longueur de cheminement de a :

• Longueur de cheminement externe de a :

• Longueur de cheminement interne de a :

• Profondeur moyenne de a :
• Profondeur moyenne externe de a :

• Profondeur moyenne interne de a :

Une expression arithmétique peut être présentée sous la forme d’un arbre binaire en exprimant une opération
dans les nœuds internes et les nombres dans les feuilles. Par exemple, l’expression arithmétique : 3 + ((5 − 1) ´ 4
) peut se représenter par l’arbre suivant :

Astuce
On parle alors d’arbre étiqueté. Chaque nœud possède une étiquette. Il est d’usage de mettre cette étiquette en lieu et place de l’identifiant du nœud.

4 Arbre binaire de recherche


Un arbre binaire de recherche est un arbre pour lequel l’étiquette d’un nœud est appelée clé. L’arbre binaire de
recherche satisfait aux 2 critères suivants :
• Les clés de tous les nœuds du sous-arbre gauche d’un nœud X sont inférieures ou égales à la clé de X
• Les clés de tous les nœuds du sous-arbre droit d’un nœud X sont strictement supérieures à la clé de X.

Remarque
Il en résulte que l’ensemble des clés est totalement ordonné.

Prenons par exemple l’arbre binaire de recherche suivant :


Les nœuds sont insérés par comparaisons successives depuis la racine de l’arbre.
Il se lit comme si, sur chaque lien direct en provenance d’un nœud, il y avait le symbole :
• « £ » pour les liens situés à gauche
• « > » pour les liens situés à droite

Astuce
Il peut également se lire de bas en haut.

5 Type abstrait Arbre


De façon analogue aux structures abstraites séquentielles, on peut proposer une structure abstraite Arbre qui
pourra être implémentée informatiquement de différentes façons.
Un arbre peut être défini de façon récursive sur des éléments de type N :
• Soit un arbre est vide
• Soit il est composé d’un élément de type N, d’un sous-arbre gauche (SAG) et d’un sous-arbre droit (SAD)

Précision
Il s’agit d’une structure récursive car les sous-arbres sont aussi des arbres.

Nous définirons ici les routines de base. Le parcours des arbres sera vu dans le chapitre 11.
Type abstrait : Arbre
Données : éléments de type N
Opérations
CREER_ARBRE_VIDE() qui retourne un objet de type Arbre
La racine et les SAG et SAD ne sont pas définis.
CREER_ARBRE(e, Ag, Ad) qui retourne un objet de type Arbre
La racine de cet arbre est définie par l’élément e, le SAG est défini par l’arbre Ag et le SAD est défini par Ad.
CREER_ARBRE_FEUILLE(e) qui retourne un objet de type Arbre
La racine de cet arbre est définie par l’élément e, le SAG et le SAD sont 2 arbres vides.
RACINE(A) qui retourne un objet de type N
Renvoie la racine de l’arbre A.
SAG(A) qui retourne un objet de type Arbre
Renvoie le sous-arbre gauche de l’arbre A.
SAD(A) qui retourne un objet de type Arbre
Renvoie le sous-arbre droit de l’arbre A.
EST_VIDE(A) qui retourne un objet de type booléen
Retourne Vrai si l’arbre A est vide et retourne Faux sinon
Conditions
RACINE(A) est défini si et seulement si EST_VIDE(A) = Faux
Exemple d’application de ce type abstrait
Prenons la suite d’instructions suivantes :
A = CREER_ARBRE_FEUILLE(5)
B = CREER_ARBRE_FEUILLE(4)
C = CREER_ARBRE(3, B, A)
D = CREER_ARBRE_FEUILLE(2)
E = CREER_ARBRE(1, D, C)
on peut assez facilement représenter l’arbre correspondant :

Avec en détail chaque arbre :

6 Représentation sous forme d’un tableau


On peut représenter un arbre binaire sous la forme d’un tableau ou chaque nœud est caractérisé par une
étiquette, le nœud racine de son sous-arbre gauche et le nœud racine de son sous-arbre droit.
Par exemple, si nous reprenons l’arbre suivant :

On peut le représenter par le tableau suivant :


Nœud Étiquette Nœud du SAG Nœud du SAD

A + B C

B 3

C * D E

D – F G

E 4

F 5
G 1
Exercices

Compétences attendues
Savoir décrire et caractériser un arbre ou un nœud.

Exercice 2.1
▶ Reconnaître des situations déjà analysées
On donne l’arbre suivant :

1. Déterminer pour cet arbre, sa racine, sa taille, sa hauteur, ses nœuds internes et ses feuilles.
2. Pour le nœud 4, déterminer son père, ses frères, sa hauteur, sa profondeur.

Exercice 2.2
▶ Mobiliser les concepts
On donne ci-dessous le tableau caractérisant un arbre :
Nœud Étiquette Nœud du SAG Nœud du SAD

1 * 2 3

2 + 4 5

3 - 6 7

4 3

5 / 8 9

6 8

7 * 10 11

8 4

9 2

10 2

11 3

1. Représenter l’arbre correspondant.


2. Quelle est la hauteur de cet arbre ?
3. Cet arbre est-il binaire, complet, dégénéré ?
4. Quel est le résultat de cette suite d’opérations mathématiques ?
Compétences attendues
Savoir évaluer quelques mesures sur les arbres binaires.

Exercice 2.3
▶ Reconnaître des situations déjà analysées
On donne l’arbre A suivant :

1. Calculer toutes les longueurs de cheminement.


2. En déduire toutes les profondeurs moyennes. On pourra essayer de les représenter sur l’arbre.
Compétences attendues
Identifier des situations nécessitant une structure de données arborescentes.

Exercice 2.4
▶ Développer des capacités d’abstraction et de généralisation
On donne ci-dessous une liste aléatoire de 14 nombres entiers :
25 60 35 10 5 20 65 45 70 40 50 55 30 15

Construire (dans l’ordre de la liste) l’arbre binaire de recherche associé.


Compétences attendues
Savoir raisonner avec un type abstrait.

Exercice 2.5
▶ Mobiliser les concepts
On donne la suite d’instructions suivantes :
A = CREER_ARBRE(2, CREER_ARBRE_FEUILLE(4), CREER_ARBRE_FEUILLE(3))
B = CREER_ARBRE(5, CREER_ARBRE_VIDE(), CREER_ARBRE_FEUILLE(6))
C = CREER_ARBRE(1, A, B)
1. Représenter la situation sous la forme d’un arbre.
2. Donner l’arbre correspondant à l’instruction :
T = SAD(C)
3. Quelle est la valeur retournée par l’instruction suivante :
r = RACINE(B)
Corrigé des exercices
Exercice 2.1
1. Cet arbre est caractérisé par :
• Sa racine : 1
• Sa taille : 24
• Sa hauteur : 6
• Ses nœuds internes : 1, 2, 3, 10, 4, 13, 5, 17, 19
• Ses feuilles : 6, 7, 8, 9, 11, 12, 14, 15, 16, 18, 20, 21, 22, 23, 24
2. Le nœud 4 de cet arbre est caractérisé par :
• Son père : 3
• Ses frères : 10, 11, 12, 13
• Sa hauteur : 3

Exercice 2.2
1. Voici l’arbre correspondant au tableau :

2. La hauteur de cet arbre est 3.


3. Cet arbre est binaire car chaque nœud a 0, 1 ou 2 enfants. Cependant il n’est ni complet (il est localement
complet), ni dégénéré.
4. Avec un peu de réflexion, on peut écrire l’expression mathématique traduite avec cet arbre.

Le résultat de cette opération vaut 10.


Conseils
On parcourt l’arbre de la gauche vers la droite. On évalue en premier les opérations concernant les feuilles les plus basses.

Exercice 2.3
1. Pour pouvoir calculer les longueurs de cheminements, nous avons besoin de connaitre la taille de l’arbre, le
nombre de feuilles, la hauteur de chaque nœud ainsi que la hauteur de l’arbre.
• Taille :
• NF(A) = 7
• Hauteur des nœuds :

• Hauteur de : H( = Max(Hauteur des nœuds ) = 4


• Longueur de cheminement de :
• Longueur de cheminement externe de :

• Longueur de cheminement interne de :

2. • Profondeur moyenne de :

• Profondeur moyenne externe de :

• Profondeur moyenne interne de :

Exercice 2.4
L’arbre binaire de recherche correspondant à la liste est le suivant :

Précision
• Le premier élément de la liste (25) est forcément la racine.
• Ensuite on compare le 2e élément 60 avec la racine 25. Dans notre cas, 60 est strictement supérieur à 25 donc le nœud de clé 60 est un fils à droite de
25.
• 35 > 25, il appartient au SAD de 25, il faut maintenant le comparer à 60. 35 est inférieur ou égal à 60, c’est un donc un fils à gauche de 60.
• 10 ≤ 25, c’est donc un fils à gauche de 25.
• 5 ≤ 25, il appartient donc au SAG de 25. De plus 5 ≤ 10, c’est donc un fils à gauche de 10.
• … ainsi de suite pour tous les éléments de la liste.

Exercice 2.5
1. Les instructions permettent de construire l’arbre suivant :

2. La donnée T correspond à l’arbre suivant :


3. L’instruction RACINE(B) renvoie la racine de l’arbre B, donc la valeur de r = 5.
Chapitre 3
Structures de données relationnelles
Cours

1 Introduction
Au milieu du xxe siècle, le physicien hongrois Eugène Wigner parle de « la déraisonnable efficacité des
mathématiques dans les sciences de la nature ». La modélisation mathématique facilite la compréhension d’un
problème car elle détermine un seul vocabulaire formel pour différentes situations, et elle permet de trouver une
méthode de résolution automatique via un programme informatique.
La modélisation mathématique peut atteindre un niveau d’abstraction permettant le développement d’une
théorie précise sur le modèle, indépendante de la réalité, tout en gardant de nombreuses applications réelles.
Le modèle mathématique dont nous allons parler dans ce chapitre est le graphe.
Outre les moyens de transport, nous croisons les graphes dans de nombreux contextes :
• Les réseaux de télécommunications (internet, téléphonie…)
• Les circuits électriques
• Les hiérarchies des fichiers informatiques
• Les bases de données relationnelles
• Le codage
• Les multiples relations entre personnes d’un même groupe
• La séquence ARN (biologie)
• La représentation des molécules (chimie)
• Et bien d’autres applications : organiser l’ordonnancement de tâches, les services de secours…

Extrait du plan de métro parisien

2 Les graphes
Un graphe est une structure de données constituée d’objets, appelés sommets, et de relations entre ces sommets.
Il existe 2 types de graphes :
• Les graphes orientés : les relations sont appelées arcs.

• Les graphes non orientés : les relations sont appelées arêtes.


3 Terminologie
Terminologie des graphes non orientés
• On note x – y l’arête (x, y) dans un graphe non-orienté où x et y sont les 2 extrémités.
• Deux arêtes d’un graphe sont dites adjacentes si elles possèdent au moins une extrémité commune.
• Deux sommets d’un graphe non-orienté sont dits adjacents s’il existe une arête les joignant.
• Dans un graphe non-orienté, on appelle degré d’un sommet x le nombre d'arêtes dont x est une extrémité.
• Dans un graphe non-orienté, on appelle chaîne toute suite de sommets consécutifs reliés par des arêtes.
• Une chaîne est dite élémentaire si elle ne comporte pas plusieurs fois le même sommet.
• Une chaîne dont le sommet de début est le même que le sommet de fin est appelé cycle.

Exemple de cycle
• Un graphe non-orienté est dit connexe lorsqu’il existe une chaîne pour toute paire de sommets.

Graphe connexe

Graphe non connexe

Remarque
Intuitivement, un graphe connexe comporte un seul « morceau ».

• Un graphe non-orienté non connexe se décompose en composantes connexes. Dans l’exemple du graphe non
connexe vu précédemment, les 2 composantes connexes sont : {1, 2, 5} et {3, 4}.

• Un graphe non-orienté est dit complet si chacun de ses sommets est relié directement à tous les autres.

Graphe complet
Terminologie des graphes orientés
• On note x ® y l’arc (x, y) dans un graphe orienté où x est son extrémité initiale et y son extrémité finale. y est le
successeur de x et x est le prédécesseur de y.
• Deux arcs d’un graphe sont adjacents s’ils possèdent au moins une extrémité commune.
• Deux sommets d’un graphe orienté sont dits adjacents s’il existe un arc les joignant.
• Dans un graphe orienté, on appelle degré d’un sommet x le nombre d’arcs dont x est une extrémité .
• Dans un graphe orienté, on appelle chemin toute suite de sommets consécutifs reliés par des arcs.
• Un chemin est dit élémentaire s’il ne comporte pas plusieurs fois le même sommet.
• Un chemin dont le sommet de début est le même que le sommet de fin est appelé circuit.

Exemple de circuit
• Un graphe orienté est dit fortement connexe lorsque pour toute paire de sommets distincts (u, v), il existe un
chemin de u vers v et un chemin de v vers u.

Graphe fortement connexe

Graphe non fortement connexe

Remarque
Le sommet 5 n’est pas accessible depuis les sommets 1, 2, 3 ou 4.

• Un graphe orienté non fortement connexe se décompose en composantes fortement connexes. Dans l’exemple
du graphe non fortement connexe vu précédemment, les composantes fortement connexes sont représentées
en pointillés rouges et verts.

Décomposition d’un graphe non fortement connexe


en composantes fortement connexes

4 Graphes simples et multigraphes


Un graphe est simple si au plus une relation relie deux sommets et s’il n’y a pas de boucle sur un sommet. On peut
imaginer des graphes avec une relation qui relie un sommet à lui-même (une boucle), ou plusieurs relations reliant
les deux mêmes sommets. On appelle ces graphes des multigraphes.

Exemple de multigraphe non orienté


5 Graphes étiquetés et pondérés
On appelle graphe étiqueté tout graphe où chaque relation est affectée d’un symbole (par exemple une lettre, un
mot…).

Exemple de multigraphe orienté étiqueté

Remarque
On peut utiliser ce type de graphe pour déterminer des codes d’accès. On peut donner par exemple les codes de 4 lettres empt ou encore eoru.

Dans le cas où le symbole est un nombre positif, le graphe est appelé graphe pondéré. Ce type de graphe peut
représenter par exemple une carte routière où les pondérations (étiquettes) peuvent représenter les distances en
km, ou encore le temps en heures et minutes ou enfin le prix des péages en euros.

Exemple de graphe orienté pondéré


Dans le cas d’un graphe pondéré, on appelle poids le nombre positif de l’étiquette de la relation. De ce fait, le
poids d’une chaîne (respectivement d’un chemin) est la somme des poids des arêtes (des arcs) qui la (le)
composent.

6 Les matrices associées à un graphe


On appelle matrice d’adjacence d’un graphe non étiqueté à n sommets notés S1, S2, …, Sn la matrice carrée (le
tableau de n lignes et n colonnes), constituée des coefficients aij (à l’intersection de la i ème ligne et de la j ème
colonne) tels que :

Prenons le cas du graphe orienté suivant :

Sa matrice d’adjacence (après avoir ordonné les sommets dans l’ordre alphabétique) est la suivante :

Cette matrice est obtenue en remplissant ligne par ligne un tableau où chaque ligne correspond au sommet de
départ et chaque colonne correspond au sommet d’arrivée.

La case verte correspond au coefficient a34 et indique qu’il existe une arête entre C et D et orientée dans le sens de
C vers D.
Si nous prenons le cas du multigraphe non-orienté suivant :

Sa matrice d’adjacence (après avoir ordonné les sommets dans l’ordre A, B, C et D) est la suivante :

Remarque
Dans le cas d’un graphe non-orienté, la matrice d’adjacence est toujours symétrique.

7 Les listes d’adjacence


Les listes des successeurs et prédécesseurs associées à un graphe orienté
Nous avons vu précédemment que nous pouvions représenter un graphe sous la forme d’une matrice d’adjacence.
Cependant nous pouvons également représenter un graphe orienté en donnant pour chacun des sommets la liste
des sommets que l’on peut atteindre directement par un arc, cela constitue les listes des successeurs. On peut
également donner la liste des sommets d’où il est accessible directement, cela constitue les listes des
prédécesseurs.
Reprenons le graphe orienté suivant :

Les listes des successeurs et des prédécesseurs sont les suivantes :


Sommet Listes des successeurs Listes des prédécesseurs

A (B, D, F) Ø

B (D, E) (A, F)

C (D) Ø

D (E) (A, B, C)

E Ø (B, D)

F (B) (A)

Les listes des voisins associées à un graphe non-orienté


Dans le cas d’un graphe non-orienté, il n’est pas réellement possible de parler de successeurs ou de prédécesseurs,
on parle généralement de voisins.
Reprenons le graphe non-orienté suivant :

Les listes des voisins sont les suivantes :


Sommet Listes des voisins

1 (2, 3)
2 (1, 3, 6)

3 (1, 2, 4, 6)

4 (3, 5)

5 (4, 6)

6 (2, 3, 5)

8 Le type abstrait Graphe orienté


Dans un graphe orienté, ajouter ou retirer un sommet peut affecter l’ensemble des arcs et le fait d’ajouter ou de
supprimer un arc peut affecter l’ensemble des sommets. On a donc défini les conventions suivantes :
• Pour ajouter un arc, il faut que ses extrémités existent
• Si nous supprimons un sommet, on supprime également les arcs incidents
Type abstrait : Graphe
Données : éléments de type S (sommet)
Opérations
CREER_GRAPHE_VIDE() qui retourne un objet de type Graphe
Le graphe orienté existe et il est vide.
AJOUTER_SOMMET(G, s)
Le sommet s est ajouté au graphe G.
AJOUTER_ARC(G, sd, sa)
L’arc est créé et orienté entre les sommets de départ sd et d’arrivée sa.
SUPPRIMER_SOMMET(G, s)
Le sommet s est supprimé du graphe
SUPPRIMER_ARC(G, sd, sa)
L’arc orienté du sommet de départ sd vers le sommet d’arrivée sa est supprimé.
SOMMET_EXISTE(G, s) qui retourne un objet de type Booléen
Retourne Vrai si le sommet s est présent dans le graphe G et retourne Faux sinon.
ARC_EXISTE(G, sd, sa) qui retourne un objet de type Booléen
Retourne Vrai si l’arc orienté de sd vers sa est présent dans le graphe G et retourne Faux sinon.
Conditions
AJOUTER_SOMMET(G, s) est défini si et seulement si SOMMET_EXISTE(G, s)= Faux
SUPPRIMER_SOMMET(G, s) est défini si et seulement si SOMMET_EXISTE(G, s)= Vrai
AJOUTER_ARC(G, sd, sa) est défini si et seulement si SOMMET_EXISTE(G, sd)= Vrai ET SOMMET_EXISTE(G, sa)= Vrai
ET ARC_EXISTE(G, sd, sa)= Faux
SUPPRIMER_ARC(G, sd, sa) est défini si et seulement si ARC_EXISTE(G, sd, sa)= Vrai
Exemple d’application de ce type abstrait
Prenons la suite d’instructions suivantes :
G = CREER_GRAPHE_VIDE()
AJOUTER_SOMMET(G, 1)
AJOUTER_SOMMET(G, 2)
AJOUTER_SOMMET(G, 3)
AJOUTER_SOMMET(G, 4)
AJOUTER_SOMMET(G, 5)
AJOUTER_SOMMET(G, 6)
AJOUTER_ARC(G, 1, 2)
AJOUTER_ARC(G, 2, 4)
AJOUTER_ARC(G, 5, 2)
AJOUTER_ARC(G, 4, 5)
AJOUTER_ARC(G, 1, 3)
AJOUTER_ARC(G, 3, 5)
AJOUTER_ARC(G, 4, 6)
On voit assez aisément qu’à la fin, le graphe G contient 6 sommets (éléments) de type « entier » et nous pouvons
représenter ce graphe par :
Exercices

Compétences attendues
Savoir représenter une situation sous la forme d’un graphe.

Exercice 3.1
▶ Analyser et modéliser un problème
On souhaite organiser un tournoi de football avec 4 équipes (numérotées de 1 à 4). Chaque équipe rencontre une
seule fois toutes les autres.
1. Représenter la situation sous la forme d’un graphe.
2. Combien d’arêtes possède-t-il ? En déduire le nombre de matchs au total pour ce tournoi ?
3. Ce graphe est-il connexe ?
4. Ce graphe est-il complet ?
Compétences attendues
Savoir analyser une situation mise sous la forme d’un graphe.

Exercice 3.2
▶ Analyser et modéliser un problème
Un club de tennis doit sélectionner deux joueurs parmi quatre pour représenter le club à un tournoi national. Les
quatre joueurs sont notés A, B, C et D. Pour réaliser la sélection le club organise des matchs : chaque joueur
rencontre les trois autres.
Règle :
• Tout match gagné donne un point
• Tout match perdu enlève un point.
Les joueurs sélectionnés sont les joueurs ayant obtenu le plus grand nombre de points. On donne le résultat sous
la forme d’un graphe orienté.

Le sens de l’arc A ® B indique que le A a battu B


1. Donner le nombre de points de chaque joueur.
2. En déduire les joueurs sélectionnés.

Exercice 3.3
▶ Mobiliser les concepts
Pour accéder à sa messagerie, Antoine a choisi un code qui doit être reconnu par le graphe étiqueté suivant les
sommets 1-2-3-4. Une succession des lettres constitue un code possible si ces lettres se succèdent sur un chemin
du graphe orienté ci-dessus en partant du sommet 1 et en sortant au sommet 4.
1. Parmi les trois codes suivants, quel est (sont) le(s) code(s) reconnu(s) par le graphe.
• SUCCES
• SCENES
• SUSPENS
2. Quelle est la taille du plus petit code possible ? Ce code est-il unique ?
3. Y a-t-il une taille maximale ?
Compétences attendues
Représenter un graphe sous la forme d’une matrice d’adjacence ou sous la forme de listes d’adjacences.

Exercice 3.4
▶ Reconnaître des situations déjà analysées
On donne le graphe suivant :

1. Donner une représentation de ce graphe au moyen d’une liste d’adjacence.


2. Donner une représentation de ce graphe au moyen d’une matrice d’adjacence.
Compétences attendues
Représenter un graphe à partir d’une matrice d’adjacence ou à partir des listes d’adjacences.

Exercice 3.5
▶ Développer des capacités d’abstraction et de généralisation

1. Donner le graphe associé à la matrice d’adjacence A ci-dessous :

Note : On notera les sommets A, B, C, D, E et F.


2. Donner le multigraphe associé à la matrice d’adjacence B ci-dessous :

Note : On notera les sommets A, B, C et D

Exercice 3.6
▶ Reconnaître et réutiliser des solutions

1. Donner le graphe associé à la liste des prédécesseurs donnée ci-après :


Sommet Listes des prédécesseurs

A (E, F)

B Ø

C (A, F)

D (D, F)

E Ø

F (B, C)

2. Donner le graphe associé à la liste des voisins donnée ci-après :


Sommet Listes des voisins

A (C, D, E)

B (C, D, E)

C (A, B, D)

D (A, B, C, E)

E (A, D, B)
Exercice-bilan
Exercice-bilan 3.1
20 min ● … points
On considère un groupe de dix personnes présentes sur un réseau social, le tableau suivant indique les paires de
personnes qui ont une relation d’amitié dans ce réseau social.
i Amis de i

1 3, 6, 7

2 6, 8

3 1, 6, 7

4 5, 10

5 4, 10

6 1, 2, 3, 7

7 1, 3, 6

8 2

10 4, 5

1. Représenter cette situation par un graphe dans lequel une arête montre le lien d’amitié.
2. Ce graphe est-il connexe ? Si non, donner ses composantes connexes.
3. L’adage « les amis de nos amis sont nos amis » est-il vérifié ? Si non, que faudrait-il faire pour qu’il le soit ?
Corrigé des exercices

Exercice 3.1
1. Il y a 4 sommets. On obtient le graphe non-orienté suivant :

2. Il y a 6 arêtes, ce qui correspond au nombre total de matchs pour ce tournoi.


3. Le graphe non-orienté est connexe car il existe une chaîne reliant 2 sommets distincts.

Astuce
Le graphe est constitué d’un seul morceau, il est donc connexe.

4. Chaque sommet est relié à tous les autres, donc le graphe est complet.

Exercice 3.2
1. A a gagné ses 3 matchs, il a donc 3 points.
B a gagné 2 matchs et en a perdu 1, il se retrouve donc avec 1 point.
C a perdu ses 3 matchs, il a donc -3 points.
D a gagné 1 match et en a perdu 2, il a ainsi -1 point.
2. Les 2 joueurs sélectionnés sont donc A et B.

Exercice 3.3
1. • Pour le mot SUCCES, il est possible d’écrire les 2 premières lettres, on est alors situé sur le sommet 1 et depuis
ce sommet, il n’est pas possible d’avoir la lettre C. Conclusion : code non reconnu
• Pour le mot SCENES, il est possible d’écrire les 4 premières lettres, on est alors situé sur le sommet 3 et depuis
ce sommet, il n’est pas possible d’avoir la lettre E. Conclusion : code non reconnu
• Pour le mot SUSPENS, il est possible d’écrire toutes les lettres depuis le premier sommet jusqu’au dernier.
Conclusion : code reconnu
2. Le plus court chemin est 1-2-3-4, il y a donc 3 lettres possibles pour le code le plus court et il est unique, il s’agit
du code SES.
3. Nous ne pouvons pas savoir au maximum le nombre de fois où je peux boucler sur les sommets 2 et 3, ni sur le
nombre d’aller-retour qu’il est possible de faire entre les sommets 1 et 2.

Exercice 3.4
1. Les listes d’adjacences pour les graphes orientés sont constituées des listes des successeurs et des
prédécesseurs.
Sommet Listes des successeurs Listes des prédécesseurs

1 (1, 2, 3, 4) (1)

2 (2) (1, 2)

3 (5, 6) (1, 4, 5, 6)
4 (3, 4) (1, 4)

5 (3, 5) (3, 5, 6)

6 (3, 5, 6) (3, 6)

Astuce
Pour ne pas oublier de sommets, on les prend dans l’ordre des numéros.

2. La matrice d’adjacence est la matrice A ci-après :

Exercice 3.5
1. On remarque que la matrice d’adjacence n’est pas symétrique, donc le graphe est forcément orienté. On obtient
le graphe suivant :

2. On remarque que la matrice d’adjacence est symétrique, donc le multigraphe est forcément non-orienté. On
obtient le multigraphe suivant :

Exercice 3.6
1. On remarque que l’on est en présence d’une liste des prédécesseurs, donc le graphe est forcément orienté. On
obtient le graphe suivant :

2. On remarque que l’on est en présence d’une liste des voisins, donc le graphe est forcément non-orienté. On
obtient le graphe suivant :
Corrigé de l’exercice-bilan
Exercice-bilan 3.1
1. La situation peut être représentée par le graphe non-orienté suivant :

2. Ce graphe n’est pas connexe car il y a 3 morceaux. On peut donc le décomposer en 3 composantes connexes :
• Composante n° 1 : 9
• Composante n° 2 : 4-5-10
• Composante n° 3 : 1-2-3-6-7-8

Précisions
Une composante peut être constituée d’un sommet unique.

3. Étudions chaque composante connexe :


• Composante n° 1 : 9 n’a pas d’amis donc l’adage est vérifié.
• Composante n° 2 : la composante est complète, donc l’adage est vérifié.
• Composante n° 3 : 8 est amis qu’avec 2. Or 2 est amis avec 6, mais 6 n’est pas amis avec 8, donc l’adage n’est
pas vérifié. Pour qu’il le soit il faudrait obtenir une composante complète.
Chapitre 4
Initiation à la programmation orientée objet
Cours

1 Introduction
Jusqu’ici, tous nos programmes ont suivi un raisonnement classique ou procédural. En effet, chaque programme a
été décomposé en fonctions élémentaires réalisant des tâches relativement simples. Cette manière de concevoir
les programmes consiste en quelque sorte à traiter indépendamment les données sans tenir compte des relations
qui les lient.
Cependant lorsque plusieurs programmeurs travaillent simultanément sur un projet, il est nécessaire de
programmer autrement afin d’éviter les conflits entre les fonctions. On a défini une approche où la programmation
relève d’une conception définie comme des « messages » échangés par des entités de base appelées objets. C’est
ce qu’on appelle la Programmation Orientée Objet.

Histoire
La programmation orientée objet a fait ses débuts dans les années 1960 avec les réalisations dans le langage Lisp. Cependant elle a été formellement
définie avec les langages Simula (vers 1970) puis SmallTalk. Puis elle s’est développée dans les langages anciens comme le Fortran, le Cobol et est même
incontournable dans des langages plus récents comme Java.

En réalité, nous avons déjà travaillé avec des objets, sans trop savoir qu’on les utilisait. Par exemple, lorsque l’on
utilise des chaînes de caractères ou encore des listes en Python, on travaille avec des objets. En effet, ces objets
sont manipulés par l’intermédiaire de méthodes. Pour les listes Python, on a par exemple len(), append(), sort() et
bien d’autres encore.

2 Conception orientée objet


En 1995, l’ingénieur américain Grady Booch propose 5 étapes dans l’établissement d’une conception orientée
objet.

Citation de G. Booch
« The function of good software is to make the complex appear to be simple. ». Ce qui pourrait être traduit de la manière suivante : « La fonction d’un
bon logiciel est de faire apparaître de manière simple ce qui est complexe. ».

Cette démarche se révèle être utile pour un débutant :


• Identifier les objets et leurs attributs : On cherche à identifier les objets du monde réel que l’on voudra réaliser.
Conseil
Il faut identifier les propriétés caractéristiques de l’objet (par l’expérience, l’intuition…). On peut s’aider d’une description textuelle (en langage
naturel) du problème.

• Identifier les opérations : On cherche ensuite à identifier les actions que l’objet subit de la part de son
environnement et qu’il provoque sur son environnement.
Conseil
Les verbes utilisés dans la description informelle (textuelle) fournissent de bons indices pour l’identification des opérations.

• Établir la visibilité : L’objet étant maintenant identifié par ses caractéristiques et ses opérations, on définira ses
relations avec les autres objets.
Conseil
On établira quels objets le voient et quels objets sont vus par lui.

• Établir l’interface : Dès que la visibilité est acquise, on définit l’interface précise de l’objet avec le monde
extérieur.
Conseil
Cette interface définit exactement quelles fonctionnalités sont accessibles et sous quelles formes.

• Implémenter les objets : La dernière étape consiste à implanter les objets en écrivant le code.
Conseil
Cette étape peut donner lieu à la création de nouvelles classes correspondant par exemple à des nécessités d’implantation. Le code en général
correspond aux spécifications concrètes effectuées avec les Types Abstraits de Données.

3 Les classes
Dans la programmation orientée objet, les différents objets utilisés peuvent être construits indépendamment les
uns des autres (par exemple par des programmeurs différents) sans qu’il n’y ait de risque d’interférence.
Ce résultat est obtenu grâce au concept d’encapsulation : la fonctionnalité interne de l‘objet et les variables qu’il
utilise pour effectuer son travail, sont en quelque sorte enfermées dans l’objet. Les autres objets et le monde
extérieur ne peuvent y accéder qu’à travers des procédures bien définies, c’est que l’on appelle l’interface de
l’objet. Un système complexe a un grand nombre d’objets. Pour réduire cette complexité, on regroupe ces objets
en classes.
Une classe est une description d’un ensemble d’objets ayant une structure de données commune (attributs) et
pouvant réaliser des actions (méthodes). On considère en fait une classe comme un nouveau type de données.
On appelle instance de la classe l’objet (du type de la classe) qui la représente.
On peut représenter graphiquement une classe de la manière ci-dessous :

4 Création de classes
L’encapsulation désigne le principe de regrouper des données brutes dans un objet avec un ensemble de
méthodes permettant de les lire ou de les manipuler. Cette encapsulation se réalise par l’intermédiaire des classes.
Leur but étant de :
• Simplifier la vie du programmeur qui les utilise.
• Masquer leur complexité.
• Permettre de les modifier indépendamment du reste du programme.
Une donnée peut être déclarée en accès :
• Public : les autres objets peuvent accéder à la valeur de cette donnée et/ou la modifier.
• Privé : les autres objets n’ont pas le droit d’accéder directement à la valeur de cette donnée (ni de la modifier).
En revanche, ils peuvent le faire indirectement par des méthodes de l’objet concerné (si celles-ci existent en
accès public).
Chaque classe doit définir une ou plusieurs méthodes particulières appelées des constructeurs. Un constructeur
est une méthode invoquée lors de la création d’un objet. Cette méthode effectue les opérations nécessaires à
l’initialisation d’un objet. Cette méthode n’a aucune valeur de retour (c’est l’objet créé qui est renvoyé).
Prenons l’exemple d’une carte à jouer (correspondant à l’élément de base d’un jeu de 32 ou 52 cartes) que l’on
souhaite créer informatiquement.
Dans le monde réel, une carte (d’un jeu de 32 ou 52 cartes) est définie par :
• Sa couleur : Carreau, Cœur, Pique ou Trèfle
• Sa valeur : de 2 à 10 (carte numérotée) et de 11 à 13 pour les figures et 14 pour l’as
• Sa figure : Aucune, Valet, Dame ou Roi.
On va donc créer une classe Carte dont les attributs sont ceux vus juste au-dessus.
Conseil
Par convention, une classe s’écrit toujours avec une majuscule.

On va créer également des méthodes permettant d’interagir avec la carte pour attribuer une valeur ou récupérer
la figure d’une carte par exemple. On peut représenter cette classe par la figure ci-après.
Les attributs privés signifient qu’ils sont accessibles uniquement via les méthodes publiques. En principe ils sont
initialisés dans la méthode de construction. Les méthodes publiques seront accessibles pour « manipuler » notre
objet tandis que les méthodes privées ne sont utilisables qu’en interne (à l’intérieur de la classe).
L’implémentation d’une classe varie selon les langages de programmation. Les notions de « privé » « publique »
sont plus lisibles dans les langages comme le C++ ou le Java.

Exemple d’implémentation en JAVA


En Python, c’est implicite mais on essayera de respecter ce « classement » avec une syntaxe particulière et
adaptée :
• Les attributs privés seront nommés en préfixant par deux underscores.
• Les méthodes privées sont également préfixées par deux underscores.
Implémentation
On déclare une classe en Python à l’aide du mot-clé class :

Bonne pratique
Lignes 2 à 5, on a documenté notre classe. De cette manière, on pourra accéder à ces informations à n’importe quel moment avec la commande
__doc__. Ce commentaire entre les caractères """ est appelé docstring.

Le constructeur
Pour déterminer et initialiser les attributs d’un objet que l’on crée, on utilise la méthode particulière appelée
constructeur. En Python, son nom est imposé : __init__.

Bonne pratique
• On retrouve nos 3 attributs privés qui sont précédés du double underscore.
• La variable self, dans les méthodes d’un objet, désigne l’objet auquel s’appliquera la méthode. Elle représente l’objet dans la méthode en attendant
qu’il soit créé.

Nous pouvons maintenant instancier notre classe. Pour créer un objet (une instance) de notre classe Carte, on
utilise la syntaxe :

Nous avons créé en mémoire un objet informatique représentant notre carte réelle :
Précision
Dans cet exemple, la méthode __init__ est appelée implicitement. Et self fait référence à l’objet ma_carte.

Si l’on veut se rassurer, on peut exécuter les instructions suivantes pour bien voir que notre objet est bien créé en
mémoire. Et que l’on peut accéder à la documentation de la classe et du constructeur.

La console nous affiche :

Dans l’état actuel, on ne peut pas accéder, ni modifier ses attributs vus qu’ils sont privés. On va le faire au travers
des méthodes publiques.
Les méthodes dédiées
Pour utiliser ou modifier les attributs, on utilisera de préférence des méthodes dédiées dont le rôle est de faire
l’interface entre l’utilisateur de l’objet et la représentation interne de l’objet (ses attributs). Il existe 2 familles :
• Les accesseurs (ou getters) : pour obtenir la valeur d’un attribut.
• Les mutateurs (ou setters) : pour modifier la valeur d’un attribut.
Dans notre cas, on souhaite accéder de manière publique à tous les accesseurs. En règle générale, on nomme ces
méthodes en commençant par Get suivi du nom de notre attribut.

Pour les mutateurs, on souhaite rendre publique la modification de la valeur et de la couleur. Par contre on
souhaite garder la main sur la façon d’attribuer une figure à notre carte pour des questions de cohérence. On ne
veut pas laisser la liberté à l’utilisateur de créer une carte incohérente entre sa valeur et sa figure, par exemple, un
roi de valeur 4 ! donc le mutateur correspondant à l’attribut figure sera en accès privé. Son nom sera donc préfixé
par les 2 underscore.

Précision
De cette manière la figure sera toujours cohérente avec la valeur de la carte.

Dans notre cas, la classe Carte se limite à ses attributs et ses accesseurs et mutateur. On pourra par exemple
enregistrer le fichier Python correspondant (carte.py) et l’utiliser comme un module dans un autre fichier Python
comme on peut le voir ci-dessous :
Dans la console, nous avons :

Précision
Nous voyons bien le rôle de l’accesseur public et du mutateur public (faisant lui-même appel au mutateur privé). De cette façon, on n’a même pas
besoin de savoir comment la classe Carte est conçue, mais nous pouvons l’utiliser sans difficulté.

5 Notion d’agrégation de classes


La conception d’une classe a pour but généralement de pouvoir créer des objets qui suivent tous le même modèle
de fabrication. Continuons avec notre carte, elle va nous servir pour créer un jeu de carte complet (de 32 ou 52
cartes). On dit que l’objet « jeu de carte » est un objet agrégé (constitué) de 32 ou 52 cartes (objets de type Carte).
Comme c’est un objet, on va donc pouvoir créer une classe JeuDeCarte que l’on peut synthétiser dans le schéma ci-
dessous :

Précisions
• Ligne 19, on crée une instance de la classe Carte, que l’on a ajouté à notre liste de cartes qui constitue notre paquet.
• Cette classe est malgré tout assez sommaire, on peut ajouter des méthodes comme par exemple le fait de distribuer une carte ou encore sécuriser la
création d’un jeu en contraignant la création à 32 et 52 cartes.

On peut ainsi manipuler notre jeu de cartes (ici de 32 cartes).

Précision
Nous avons bien créé un nouvel objet JeuDeCarte à partir de plusieurs objets Carte.
Extrait de la console
Exercices

Compétences attendues
Savoir formaliser un objet réel en classe.

Exercice 4.1
▶Analyser et modéliser un problème
▶Concevoir des solutions algorithmiques
▶Traduire un algorithme en langage de programmation
On souhaite caractériser informatiquement la notion de point telle qu’elle existe en 2 dimensions : aussi bien en
coordonnées cartésiennes qu’en coordonnées polaires .

1. Remplir le modèle de la classe Point ci-dessous en listant les attributs privés et les actions permettant seulement
d’y accéder (accesseurs) :

2. Implémenter cette classe en Python. On utilisera le module math. On rappelle que dans l’intervalle , on a :

3. À l’aide de cette classe, on donnera les coordonnées polaires des 4 points suivants :




Compétences attendues
Savoir Implémenter une Pile et une File sous forme de classe.

Exercice 4.2
▶ Développer des capacités d’abstraction et de généralisation
▶Traduire un algorithme dans un langage de programmation
1. À partir de la structure abstraite Pile vue dans le chapitre 1. Proposer une implémentation sous forme de classe.
2. On proposera de la même manière une implémentation de la file.

Précisions
On utilisera le type liste pour typer notre attribut privé Pile (respectivement File). On précise également ici que nous n’avons ici ni accesseurs ni
mutateurs mais simplement des méthodes publiques pour manipuler notre Pile (respectivement File).
Exercice-bilan
Exercice-bilan 4.1
60 min ● … points
Le domino est un jeu très ancien constitué de 28 pièces toute différentes. Sur chacune de ces pièces, il y a deux
cotés qui sont constitués de 0 (blanc) à 6 points noirs. Lorsque les 2 cotés possèdent le même nombre de points,
on l’appelle domino double.

1. Proposer un classe Domino permettant de représenter une pièce. Les objets seront initialisés avec les valeurs des
deux côtés (gauche et droite). On définira des méthodes pour tester si le domino est double ou blanc. On
implémentera également une méthode pour compter le nombre de points sur un domino.
On ajoutera également une méthode qui affiche les valeurs des deux faces de manière horizontale pour un
domino classique et de manière verticale pour un domino double comme le montre la figure ci-dessous.

2. Proposer une classe JeuDeDomino permettant de manipuler le jeu de domino complet. On créera une méthode
pour mélanger le jeu et pour distribuer selon 2 joueurs ou plus.
3. En utilisant cette classe, on affichera le jeu de 2 joueurs ainsi que le jeu restant (la pioche). Pour chaque joueur,
on affichera le nombre de points dans son jeu.
Corrigé des exercices
Exercice 4.1
1.

2. Le code Python peut être le suivant :

Précision
On a créé 2 méthodes privées pour le calcul des coordonnées polaires à partir des coordonnées cartésiennes.

3. Il suffit d’instancier 4 objets de la classe Point :

Dans la console, nous obtenons nos réponses :

Exercice 4.2
1.
2.
Corrigé de l’exercice-bilan
Exercice-bilan 4.1
1.

2.

3.
Partie 2
Bases de données
Chapitre 5
Les bases de données : du modèle conceptuel
à l’implémentation dans le système de gestion des bases
de données relationnelles
Cours

1 Introduction aux bases de données


Définitions
Une base de données (BDD) représente un ensemble ordonné de données dont l’organisation est régie par un
modèle de données.
Le Système de Gestion de Bases de Données (SGBD) peut être vu comme le logiciel qui prend en charge la
structuration, le stockage, la mise à jour et la maintenance des données. C’est en fait l’interface entre la base de
données et les multiples utilisateurs (ou leurs programmes).
Historique
Quasiment toutes les bases de données que nous utilisons aujourd’hui sont basées sur les travaux d’Edgar F. Codd
(1970). C’est le point essentiel dans l’histoire des bases de données.

2 Conception des bases de données relationnelles


La conception des bases de données est la tâche la plus ardue du processus de développement du système
d’information. Les méthodes de conception préconisent une démarche en étapes et font appel à des modèles pour
représenter les objets qui composent les systèmes d’information, les relations existantes entre ces objets ainsi que
les règles sous-jacentes.
La modélisation se réalise en trois étapes principales qui correspondent à trois niveaux d’abstraction différents :
• Niveau conceptuel : représentation de la base de données indépendamment de toute considération
informatique
• Niveau logique : adaptation du schéma conceptuel en tableaux à deux dimensions
• Niveau physique : implémentation informatique sur un système de gestion de bases de données relationnelles

3 Le modèle Entité-Association
Ce modèle permet de distinguer les entités qui constituent la base de données et les associations entre ces entités.
Entités, attributs et identifiants
On appelle entité un objet pouvant être identifié distinctement. Chaque entité est donc unique et est décrite par
un ensemble de propriétés appelées attributs. Un ou plusieurs attributs permettent d’identifier de manière unique
l’entité, on parle alors d’identifiant (ou de clé).
On peut représenter une entité par le schéma suivant :

Un attribut est désigné par :


• Un nom
• Une valeur de type défini (entier, chaîne de caractères, date…)
Prenons l’exemple d’une entité « Auteur ». Un auteur est bien identifié de manière unique par son numéro de
sécurité sociale (Num_securite_sociale) et est caractérisé par un nom (Nom_auteur), un prénom (Prenom_auteur)
et une date de naissance (Date_naissance).

Une entité peut avoir une ou plusieurs occurrences (et parfois même aucune). Pour illustrer cela, voici un exemple
de 3 occurrences de l’entité « Auteur ».
Num_securite_sociale Nom_auteur Prenom_auteur Date_naissance

182086926825812 DUPOND Louis 23/08/1982

274037511510792 MARTIN Lucie 11/03/1974

163111382632757 LEFRANC Pierre 05/11/1963

Remarques
Ces occurrences sont appelées tuples ou lignes.
On devrait plutôt parler d’entités-types, les entités étant en fait des instances d’entités-types. Par soucis de simplicité, on gardera les termes d’entités
dans la suite.

Associations
Une association définit un lien sémantique entre des entités. Elle permet de traduire une partie des règles de
gestion qui n’ont pas été satisfaites par la simple définition des entités.
Une association est a minima caractérisée par :
• Un nom : généralement on utilise un verbe définissant le lien entre les entités
• Deux cardinalités : elles sont présentes sur les 2 extrémités du lien. Chaque cardinalité est un couple de valeurs
(mini, maxi) qui traduisent 2 règles de gestion (une par sens)

Remarque
Une association peut aussi avoir des attributs.

On peut représenter une association par le schéma suivant :


Dans l’exemple suivant, on définit une association « Être originaire de » entre les entités « Auteur » et « Pays ». Ce
lien possède 2 cardinalités (1,1) et (0,n) qui traduisent les 2 règles de gestion suivantes :
• Un auteur est originaire au minimum et au maximum d’un seul pays (sens « Auteur » vers « Pays »)
• Dans un pays, il peut y avoir au minimum aucun auteur originaire de ce pays et au maximum plusieurs auteurs.
(sens « Pays » vers « Auteur »)

Remarque
Les cardinalités les plus répandues sont les suivantes : 0,N ; 1,N ; 0,1 ; 1,1.

À partir des entités et associations, il est possible d’élaborer le schéma conceptuel des données pour une gestion
de livres comme le montre l’exemple ci-dessous :

Remarque
Une situation à modéliser peut avoir plusieurs schémas différents, chaque modèle présentant des avantages et des inconvénients.

4 Le modèle relationnel
Dans le modèle relationnel, les entités et les associations du schéma conceptuel sont transformées en tableaux à
deux dimensions appelés relations.
Transformation d’une entité en relation
Toute entité devient relation comme le montre la figure suivante :

Remarques
• L’identifiant de l’entité est la clé primaire (Primary Key) de la relation. Il est représenté sur la figure ci-dessus par une clé et la dénomination CP.
• L’ensemble des valeurs possibles d’un attribut définit un domaine.

On trouve également la notation textuelle : NomRelation(identifiant, attribut1, attribut2…). On peut par


exemple noter la relation « Auteur » par :
Auteur(Num_securite_sociale, Nom_auteur, Prenom_auteur, Date_naissance)

Remarques
• Le nom de la relation est en gras.
• La clé primaire est soulignée

Transformation d’une association en relation


Il existe 2 cas :
• Associations possédant au moins une cardinalité (0,1) ou (1,1)
• Associations sans cardinalités (0,1) ou (1,1)
Associations possédant au moins une cardinalité (0,1) ou (1,1)
Prenons l’exemple de l’association « Auteur/Pays » :

Remarque
• On a ajouté un attribut Nom_pays dans la relation Auteur, cet attribut correspond à la clé primaire de la relation Pays. On dit que c’est une clé
étrangère (Foreign Key). Dans le schéma ci-dessus, elle est également représentée par une clé avec le préfixe FK.
• On lie ces deux attributs entre eux.

Dans l’écriture textuelle, on ajoute la clé étrangère avec le symbole « # » ou en soulignant l’attribut concerné par
un trait en pointillé.
Par exemple la relation « Auteur » devient :
Auteur(Num_securite_sociale, #Nom_pays, Nom_auteur, Prenom_auteur, Date_naissance)
Ou encore
Auteur(Num_securite_sociale, Nom_pays, Nom_auteur, Prenom_auteur, Date_naissance)
Associations sans cardinalité (0,1) ou (1,1)
Prenons l’exemple de l’association Auteur/Livre :
Remarque
On a transformé l’association en relation par l’ajout de deux attributs correspondant respectivement aux clés primaires de chacune des entités. Dans ce
cas, il y a donc deux clés étrangères. Et ce couple de clés étrangères forme la clé primaire. Sur notre schéma, on a les 2 dénominations associées (CP et
FK).

Nous avons donc le modèle relationnel complet :

5 Les contraintes d’intégrité


Le modèle relationnel impose une règle minimale qui est l’unicité des clés, comme nous allons le voir ci-dessous.
Cependant il existe plusieurs points pour respecter l’intégrité d’une base de données.
Unicité de clé
Par définition, une relation est un ensemble de tuples. Un ensemble n’ayant pas d’élément en double, il ne peut
pas exister deux fois le même tuple dans une relation. Toute relation doit donc posséder une clé unique (clé
primaire).
Contraintes référentielles
Correctement construite, une base de données fait appel à des données situées dans différentes relations. Pour
que les données restent utilisables et cohérentes, il ne faut pas que l’on puisse détruire des données qui
dépendent les unes des autres. C’est le rôle de l’intégrité référentielle de protéger ces relations. Autrement dit,
l’intégrité référentielle vérifie qu’une valeur de clé étrangère existe bien en tant que valeur de clé primaire dans
une autre table. Dans notre exemple : Des éditeurs sont reliés à un livre. L’intégrité référentielle empêchera la
suppression d’un éditeur si des livres y sont rattachés dans la base de données. En procédant ainsi, les éditeurs
seront toujours reliés à leur livre et l’utilisateur ne pourra pas supprimer (involontairement) des données
essentielles.
Valeurs nulles
Lors de l’insertion de tuples dans une relation, il arrive fréquemment qu’un attribut soit inconnu ou non applicable.
Par exemple, la population ou la superficie d’un pays à une certaine date peuvent être inconnues. On est alors
amené à introduire dans la relation une valeur conventionnelle, appelée valeur nulle.

Attention
Tout attribut dans une relation ne peut prendre une valeur nulle. En effet, l’existence d’une clé unique impose la connaissance de la clé afin de pouvoir
vérifier que cette valeur de clé n’existe pas déjà. La clé ne peut donc pas être nulle !

Contraintes de domaines
En théorie, une relation est construite à partir d’un ensemble de domaines. En pratique, les domaines gérés par les
systèmes sont souvent limités aux types de base (entiers, réels, chaînes de caractères), parfois monnaie et date.
Afin de spécialiser un type de données pour composer un domaine plus fin (par exemple, les années de parution
d’un livre qui peuvent être des entiers compris entre 1500 et 2100. Cette notion de contrainte de domaine est
souvent ajoutée aux règles d’intégrité.

6 Le modèle physique : SGBD relationnel


Un Système de Gestion de Bases de Données doit répondre aux objectifs suivants :
• Indépendance physique : La façon dont les données sont définies doit être indépendante des structures de
stockage utilisées.
• Indépendance logique : Un même ensemble de données peut être vu différemment par des utilisateurs
différents. Toutes ces visions personnelles des données doivent être intégrées dans une vision globale.
• Accès aux données : L’accès aux données se fait par l’intermédiaire d’un Langage de Manipulation de Données
(LMD). Il est crucial que ce langage permette d’obtenir des réponses aux requêtes en un temps « raisonnable ».
Le LMD doit donc être optimisé, minimiser le nombre d’accès disques, et tout cela de façon totalement
transparente pour l’utilisateur.
• Administration centralisée des données (intégration) : Toutes les données doivent être centralisées dans un
réservoir unique commun à toutes les applications. En effet, des visions différentes des données (entre autres) se
résolvent plus facilement si les données sont administrées de façon centralisée.
• Non-redondance des données : Afin d’éviter les problèmes lors des mises à jour, chaque donnée ne doit être
présente qu’une seule fois dans la base.
• Cohérence des données : Les données sont soumises à un certain nombre de contraintes d’intégrité qui
définissent un état cohérent de la base. Elles doivent pouvoir être exprimées simplement et vérifiées
automatiquement à chaque insertion, modification ou suppression des données. Les contraintes d’intégrité sont
décrites dans le Langage de Description de Données (LDD).
• Partage des données : Il s’agit de permettre à plusieurs utilisateurs d’accéder aux mêmes données au même
moment de manière transparente. Si ce problème est simple à résoudre quand il s’agit uniquement
d’interrogations, cela ne l’est plus quand il s’agit de modifications dans un contexte multiutilisateur, car il faut :
permettre à deux utilisateurs (ou plus) de modifier la même donnée « en même temps » et assurer un résultat
d’interrogation cohérent pour un utilisateur consultant une table pendant qu’un autre la modifie.
• Sécurité des données : Les données doivent pouvoir être protégées contre les accès non autorisés. Pour cela, il
faut pouvoir associer à chaque utilisateur des droits d’accès aux données.
• Résistance aux pannes : Que se passe-t-il si une panne survient au milieu d’une modification, si certains fichiers
contenant les données deviennent illisibles ? Il faut pouvoir récupérer une base dans un état « sain ». Ainsi, après
une panne intervenant au milieu d’une modification deux solutions sont possibles : soit récupérer les données
dans l’état dans lequel elles étaient avant la modification, soit terminer l’opération interrompue.
Parmi les SGBD les plus connus, on peut citer : MySQL, PostgreSQL, SQLite, Oracle Database, Microsoft SQL Server,
Microsoft Access.
Dans la suite de ce chapitre on se basera sur l’utilisation du SGBD MySQL (système libre) et on s’intéressera
essentiellement à la partie de création de la structure de la base de données et au chapitre suivant à la partie de
manipulation des données. Le langage utilisé est le langage SQL dans sa version SQL2.
Conventions d’écriture
On veillera à ne jamais utiliser d’espaces ou d’accents dans les noms de bases de données, de relations et
d’attributs. On évitera également d’utiliser des mots réservés. Par « mots réservés », on entend un mot-clé SQL,
donc un mot qui sert à définir quelque chose dans le langage SQL. On pourra trouver une liste exhaustive de ces
mots réservés dans la documentation officielle du langage SQL.
Une convention largement répandue veut que les commandes et mots-clés SQL soient écrits complètement en
majuscules. Nous respecterons cette convention et nous vous encourageons à le faire également. Il est plus facile
de relire une commande de 5 lignes lorsque l’on peut différencier au premier coup d’œil les commandes SQL des
noms des relations et de attributs.

Remarques
Dans MySQL, on choisira le moteur de stockage InnoDB qui permet la bonne gestion des bases en respectant les contraintes d’intégrité (et notamment
les clés étrangères).

Création d’une base de données : CREATE DATABASE


Avant de créer une base de données, il faut définir l’encodage utilisé. Le plus souvent on utilise l’UTF-8. Voici donc
la commande complète à taper pour créer la base correspondant à notre exemple. On appellera notre base
LivresAuteurs :
CREATE DATABASE LivresAuteurs CHARACTER SET ‘utf8’
Suppression d’une base de données : DROP DATABASE
La décision de supprimer une base de données doit être très réfléchie. Il faut être très prudent, car on efface avec
cette action tous les fichiers créés par MySQL qui servent à stocker les informations de la base concernée. Si l’on
souhaite supprimer la base LivresAuteurs, on exécute la commande suivante :
DROP DATABASE LivresAuteurs
Utilisation d’une base de données : USE
Une fois que la base de données est créée, pour pouvoir agir dessus, il faut la sélectionner. La commande est très
simple :
USE LivresAuteurs
À partir de cette commande, toutes les actions effectuées le seront sur la base de données LivresAuteurs (création
et modification de tables, par exemple).
Création d’une relation : CREATE TABLE
La commande CREATE TABLE permet de créer la relation (table) en définissant le nom, le type des attributs. Elle
permet également de définir les contraintes d’intégrité. Commençons par créer la relation Pays :
CREATE TABLE Pays(
Nom_pays VARCHAR(255),
Population INTEGER,
Superficie INTEGER,
PRIMARY KEY (Nom_pays)
)

Remarques
On a bien défini le type de chaque attribut ainsi que la clé primaire. L’attribut Nom_pays ne peut pas prendre de valeur nulle étant donné que c’est la
clé primaire, cette condition est bien prise en considération avec PRIMARY KEY.
Pour chacune des relations que l’on crée avec MySQL, on ajoutera à la fin de la commande CREATE, le type avec le complément ci-après TYPE = InnoDB
comme le montre l’exemple suivant :
CREATE TABLE Pays(
Nom_pays VARCHAR(255),
….
) TYPE = InnoDB

Passons maintenant à la création de la relation Auteur :


CREATE TABLE Auteur(
Num_securite_sociale INT,
Nom_pays VARCHAR(255),
Nom_auteur VARCHAR(255) NOT NULL,
Prenom_auteur VARCHAR (255),
Date_naissance DATE,
PRIMARY KEY (Num_securite_sociale),
UNIQUE (Nom_auteur, Prenom_auteur)
CHECK (Date_naissance BETWEEN 1900-01-01 AND 2100-12-31),
FOREIGN KEY (Nom_Pays) REFERENCES Pays(Nom_pays)
ON DELETE CASCADE
ON UPDATE CASCADE
)

Remarques
On a ajouté ici une contrainte de domaine pour la date de naissance qui doit être compris entre le 1er janvier 1900 et le 31 décembre 2100. Nous avons
également ajouté une contrainte pour spécifier l’unicité du couple (nom, prénom) de l’auteur. Enfin nous avons ajouté une contrainte référentielle sur
la clé étrangère.
Nous avons spécifié une contrainte de suppression et de mise à jour sur la clé étrangère avec respectivement ON DELETE CASCADE et ON UPDATE
CASCADE. Cela veut simplement dire que si nous supprimons des occurrences dans la relation Pays, les lignes en référence dans la relation Auteur
seront également supprimées. Quand on modifie la clé d’un pays, la modification sera répercutée sur les lignes de la relation Auteur. L’utilisation des
actions ON UPDATE et ON DELETE simplifie considérablement la gestion de la base de données.

Nous pouvons ainsi créer les autres relations de la même manière.


CREATE TABLE Editeur (
Siret INTEGER,
Nom_editeur VARCHAR(255),
PRIMARY KEY (Siret)
)
CREATE TABLE Livre (
Num_isbn INTEGER,
Siret INTEGER,
Titre VARCHAR(255),
Annee YEAR,
PRIMARY KEY (Num_isbn),
CHECK Annee BETWEEN 1900 AND 2100 NOT NULL,
FOREIGN KEY (Siret) REFERENCES Editeur(Siret)
ON DELETE CASCADE
ON UPDATE CASCADE
)
CREATE TABLE Ecrire (
Num_isbn INTEGER,
Num_securite_sociale INTEGER,
Nb_chapitres INTEGER,
PRIMARY KEY (Num_isbn, Num_securite_sociale),
FOREIGN KEY (Num_isbn) REFERENCES Livre(Num_isbn)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (Num_securite_sociale) REFERENCES Auteur(Num_securite_sociale)
ON DELETE CASCADE
ON UPDATE CASCADE
)
Les types de MySQL sont nombreux. Pour plus de précisions les concernant, on se reportera à la documentation de
MySQL. On peut cependant dire qu’ils se répartissent en 3 grandes familles :
• Numériques (NUMERIC) :
– Les entiers : Les types de données qui acceptent des nombres entiers comme valeurs sont désignés par le
mot-clé INT (ou INTEGER), et ses déclinaisons TINYINT, SMALLINT, MEDIUMINT et BIGINT. La différence
entre ces types est le nombre d’octets (donc la place en mémoire) réservés à la valeur du champ.
– Les nombres décimaux : le type le plus utilisé étant FLOAT. Il existe cependant 4 autres mots-clés permettent
de stocker des nombres décimaux : DECIMAL, NUMERIC, REAL et DOUBLE.
• Alphanumériques (STRING) :
– Chaines courtes (inférieures à 255 caractères) : Pour stocker un texte relativement court, vous pouvez utiliser
les types CHAR et VARCHAR. Ces deux types s’utilisent avec un paramètre qui précise la taille que peut
prendre votre texte (entre 1 et 255 caractères).
– Chaines plus longues : Il suffit alors d’utiliser le type TEXT, ou un de ses
dérivés TINYTEXT, MEDIUMTEXT ou LONGTEXT. La différence entre ceux-ci est la place qu’ils permettent
d’occuper en mémoire.
• Temporels (DATE and TIME) :
– MySQL dispose de cinq types qui permettent, lorsqu’ils sont bien utilisés, de faire énormément de choses. Les
cinq types sont DATE, DATETIME, TIME, TIMESTAMP et YEAR.
Les contraintes d’intégrité sont elles aussi nombreuses. Il est bien évidemment indispensable de les inclure dans le
schéma pour assurer, dans la mesure du possible, l’intégrité de la base. Nous avons vu dans les exemples
précédents les principales règles :
• NOT NULL : pour spécifier qu’un attribut ne peut pas avoir de valeur nulle.
• PRIMARY KEY : pour définir le ou les attributs comme clé primaire.
• FOREIGN KEY…. REFERENCE : pour lier la clé étrangère à la clé primaire de la relation concernée.
• UNIQUE : pour spécifier qu’un (ou plusieurs) attribut(s) doi(ven)t être unique(s) dans la relation.
• CHECK : pour vérifier une contrainte de domaine.

Remarques
On ne surcharge pas les attributs faisant parti d’une clé (primaire ou étrangère) avec la propriété NOT NULL. C’est déjà pris en compte avec PRIMARY
KEY et FOREIGN KEY.

Modification d’une relation : ALTER TABLE


La création d’une structure n’est souvent qu’une première étape dans la vie d’une base de données. On est
toujours amené à un moment donné à modifier ce schéma en ajoutant, en modifiant ou en supprimant des
attributs. La requête permettant une modification d’une relation se construit à partir des mots clés ALTER TABLE.
Par exemple, pour ajouter un attribut email dans notre relation Auteur, on écrira par exemple la requête suivante :
ALTER TABLE Auteur ADD email VARCHAR(10)
D’après la commande précédente, l’attribut email ne peut prendre que 10 caractères. Si on souhaite agrandir sa
taille et par la même occasion définir cet attribut obligatoire, on peut par exemple modifier cet attribut par :
ALTER TABLE Auteur MODIFY email VARCHAR(255) NOT NULL
Si nous voulons changer le nom de cet attribut et le modifier en mail, on effectuera la requête suivante :
ALTER TABLE Auteur CHANGE email mail
Si l’on souhaite par exemple supprimer l’attribut mail que l’on vient de modifier, on effectue la commande
suivante :
ALTER TABLE Auteur DROP mail

Attention
La modification d’une table peut poser des problèmes si elle est incompatible avec les données déjà présentes. Par exemple modifier un attribut pour le
mettre à NOT NULL implique que cet attribut possède déjà des valeurs pour toutes les occurrences.

Suppression d’une relation : DROP TABLE


Pour supprimer définitivement une relation d’une base de données, on utilise les mots clés DROP TABLE. Si nous
voulons supprimer la relation Pays de notre exemple, on effectue la commande suivante :
DROP TABLE Pays
S’il y a une contrainte référentielle avec d’autres tables, il est recommandé de les supprimer avant de supprimer la
table. C’est le cas par exemple s’il y a des clés étrangères.

Attention
Il faut utiliser cette commande avec beaucoup d’attention car une fois la relation supprimée, les données sont définitivement perdues. Alors avant
d’effectuer ce genre d’action, il est bien entendu préférable de réaliser une sauvegarde pour éviter les mauvaises surprises.
Exercices

Compétences attendues
Savoir analyser un modèle Entités/Associations.

Exercice 5.1
▶Analyser et modéliser un problème
▶Mobiliser les concepts
▶Développer des capacités d’abstraction et de généralisation
On souhaite gérer des réservations dans une compagnie d’hôtels. On considère donc le modèle
Entités/Associations suivant :

À l’aide de ce modèle, répondre aux questions suivantes :


1. Peut-on avoir des clients homonymes ?
2. Un client peut-il réserver plusieurs chambres à une date donnée ?
3. Est-il possible de réserver une chambre sur plusieurs jours ?
4. Peut-on savoir si une chambre est libre à une date donnée ?
5. Peut-on réserver plusieurs fois une chambre à une date donnée ?

Exercice 5.2
▶ Analyser un problème
▶Mobiliser les concepts
On donne ci-dessous le modèle Entités/Associations représentant des visites dans un centre médical

En utilisant ce modèle, répondre aux questions suivantes :


1. Un patient peut-il effectuer plusieurs visites ?
2. Un médecin peut-il recevoir plusieurs patients dans la même consultation ?
3. Peut-on prescrire plusieurs médicaments dans une même consultation ?
4. Deux médecins différents peuvent-ils prescrire le même médicament ?
Compétences attendues
Savoir passer du modèle Entités/Associations au modèle relationnel.

Exercice 5.3
▶ Décomposer un problème en sous-problèmes
Donner le schéma relationnel de la base de données « compagnie d’Hotels » décrite par le modèle
Entités/Association dans l’exercice 5.1.

Exercice 5.4
▶ Reconnaître des situations déjà analysées
Donner le schéma relationnel de la base de données « visites médicales » décrite par le modèle Entités /
Association dans l’exercice 5.2.
Compétences attendues
Identifier le type des données ainsi que les contraintes d’intégrité dans un modèle relationnel.

Exercice 5.5
▶ Mobiliser les concepts
▶Développer des capacités d’abstraction et de généralisation
À partir du modèle relationnel construit dans l’exercice 5.3. Remplir le tableau ci-dessous :
Relation Attribut Type Unicité Domaine éventuel Valeur nulle permise Clé

Chambre Nom_hotel

Chambre Prix

Réservation Date_resa

Client Numero

Pour la colonne Type, on choisira parmi : Entier, Réel, Texte, Date.


Pour les colonnes unicité et Valeur nulle permise, on répondra par Oui ou Non.
Pour la colonne Clé, on mettra CP pour clé primaire et CE pour clé étrangère ou on laissera vide.
Pour la colonne Domaine éventuel, on précisera le domaine possible.
Compétences attendues
Identifier les anomalies parmi les occurrences d’une relation donnée.

Exercice 5.6
▶ Analyser un problème
On donne ci-dessous les occurrences de la relation Consultation issue du modèle relationnel construit dans
l’exercice 5.4. Citer les anomalies constatées :
Numero Matricule Numero_SS Date_consult
1 123 21/11/2018

2 123 182086926825812

2 526 ‘Aspirine’ 13/03/2019

Compétences attendues
Convertir une relation du modèle relationnel au modèle physique.

Exercice 5.7
▶ Traduire un algorithme dans un langage de programmation
Supposons que la base de données de l’exercice 5.4 existe dans le SGBD MySQL. On a déjà écrit les requêtes
suivantes pour la création des relations Patient, Medecin et Medicament.
CREATE TABLE Patient (
Numero_SS INTEGER,
Nom_patient VARCHAR(255),
PRIMARY KEY (Numero_SS)
)
CREATE TABLE Medecin (
Matricule INTEGER,
Nom_medecin VARCHAR(255),
PRIMARY KEY (Matricule)
)
CREATE TABLE Medicament (
Code INTEGER,
Libelle VARCHAR(255),
PRIMARY KEY (Code)
)
Donner l’écriture avec le langage SQL permettant la création de des relations Consultation et Prescrire.
Exercice-bilan
Exercice-bilan 5.1
60 min ● ... points
Pendant la phase de conception, toutes les données recueillies et spécifiées sont inscrites dans ce que l’on appelle
un dictionnaire de données. On dispose du dictionnaire de données suivant :
Code de la donnée Description Type Taille maxi

Code_ven Identifiant du vendeur Entier

Nom_ven Nom du vendeur Texte 20 caractères

Ville_ven Ville où travaille le vendeur Texte 15 caractères

Code_cli Identifiant du client Entier

Nom_cli Nom du client Texte 20 caractères

Rue_cli Rue où habite le client Texte 20 caractères

Cp_cli Code postal du client Entier

Ville_cli Ville où habite le client Texte 15 caractères

Dnaiss_cli Date de naissance du client Date

Email_cli Adresse mail du client Texte 255 caractères

Num_fact Identifiant de la facture Entier

Date_fact Date de facturation Date

Num_prod Identifiant du produit Entier

Des_prod Désignation du produit Texte 30 caractères

Prix_prod Prix du produit Réel

Quantite Quantité commandée Entier

1. À partir de l’analyse du dictionnaire de données précédent :


a. Identifier les différentes entités en jeu ainsi que leurs identifiants.
b. Les 3 associations mises en jeu par ces entités seront nommées « Établir », « Recevoir » et « Ajouter ». Pour
chacune d’entre elle, spécifier leur cardinalité et éventuellement leurs attributs.
2. Compléter les données manquantes au modèle conceptuel Entités/Associations suivant :

3. En déduire le modèle relationnel.


4. Donner la commande pour créer la relation Facture dans le SGBD MySQL en considérant que la date de
facturation ne peut pas être nulle.
Corrigé des exercices
Exercice 5.1
1. Le nom du client n’est pas l’attribut identifiant l’entité Client. L’identifiant est ici l’attribut numero. Il peut donc y
avoir des clients homonymes, ces clients auront des numéros différents.
2. La cardinalité maximale de l’entité Client est n donc un client peut réserver plusieurs fois. Cependant d’après la
cardinalité de l’entité Reservation, une chambre ne peut correspondre qu’à une et une seule réservation. On en
conclue qu’un client peut réserver plusieurs chambres à une date donnée s’il effectue plusieurs réservations.
3. Oui, un client peut réserver une chambre sur plusieurs jours à conditions qu’il effectue plusieurs réservations.
4. Oui, pour savoir si une chambre est disponible à une date donnée, il faudra lister les réservations s’assurer et
rechercher qu’il n’y a aucune réservation à la date donnée.
5. Oui dans ce schéma rien ne garantit que la même chambre puisse être louée qu’une seule fois.

Exercice 5.2
1. Oui : car la cardinalité maximale n (coté entité Patient) existant sur l’association Assister exprime le fait qu’un
patient peut participer à plusieurs consultations.
2. Non : car une consultation est donnée par un et un seul médecin, cela est spécifié par la cardinalité (1,1) coté
entité Consultation sur l’association Donner. De même seul un patient assiste à une consultation. Cela est
également spécifié par la cardinalité (1,1) coté Consultation sur l’association Assister.
3. Oui : Au minimum, aucun médicament n’est prescrit par le médecin (cardinalité minimale 0) et au maximum
plusieurs médicaments peuvent être prescrits par le médecin (cardinalité maximale n).
4. Oui : rien n’empêche que deux médecins prescrivent le même médicament pour deux consultations différentes.
Ceci est bien exprimé par la cardinalité (0,n) coté entité Medicament sur l’association Prescrire. Cela signifie
qu’un médicament peut participer plusieurs fois à cette association comme elle peut ne pas participer du tout
(c’est le cas où un médicament n’est jamais prescrit).

Exercice 5.3
Il faut commencer pour convertir toutes les entités en relations et ensuite compléter ces relations en fonction des
associations.

Conseils
• On convertit les 4 entités en 4 relations en changeant les identifiants en clé primaires.
• Ici nous avons que des associations possédant au moins une cardinalité (0,1) ou (1,1), il suffit donc d’ajouter les clés étrangères dans les
relations existantes.
• Il faut lier ces contraintes de clé étrangères aux clés primaires dont elles sont issues.

Exercice 5.4
La méthode est la même que celle utilisée dans l’exercice précédent. La seule différence est qu’il existe dans ce
schéma une association sans cardinalité (0,1) ou (1,1), il faudra donc créer une relation propre à cette association.

Attention
Il faut bien veiller à définir comme clé primaire de la relation Prescrire les deux clés étrangères Numero et Code.

Exercice 5.5
Relation Attribut Type Unicité Domaine éventuel Valeur nulle permise Clé

Chambre Nom_hotel Texte Non Non CE

Chambre Prix Réel Non R+ Oui

Réservation Date_resa Date Non Non

Client Numero Entier Oui Non CP

Attention
• Nom_hotel est une clé étrangère dans la relation Chambre, il ne peut pas être nul, par contre il peut ne pas être unique.
• Prix doit vraisemblablement être positif.
• Date_resa n’est pas unique mais doit être présent.
• Numero est bien la clé primaire de la relation Client, cet attribut est donc unique et non nul.

Exercice 5.6
On dénombre 3 anomalies :
• Il ne peut pas y avoir dans les valeurs de l’attribut Numero des valeurs identiques puisque cet attribut est la clé
primaire de la relation Consultation.
• Le Numero_SS ne peut pas être absent dans la première occurrence. En effet il fait le lien avec le patient.
• Le Numero_SS doit être de type entier, donc ‘Aspirine’ ne convient pas comme valeur.

Remarques
• Date_consult peut être absente, nous n’avons aucune contrainte sur cet attribut.
• Nous pouvons avoir le même matricule sur des occurrences différentes. En effet un médecin peut effectuer plusieurs consultations. La seule
contrainte, c’est d’avoir un Numero différent.

Exercice 5.7
Après avoir analyser les contraintes d’intégrité, on peut écrire les requêtes suivantes :
CREATE TABLE Consultation (
Numero INTEGER,
Matricule INTEGER,
Numero_SS INTEGER,
Date_consult DATE NOT NULL,
PRIMARY KEY (Numero),
FOREIGN KEY (Matricule) REFERENCES Medecin(Matricule)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (Numero_SS) REFERENCES Patient(Numero_SS)
ON DELETE CASCADE
ON UPDATE CASCADE
)
CREATE TABLE Prescrire (
Numero INTEGER,
Code INTEGER,
Nb_prises INTEGER,
PRIMARY KEY (Numero,Code),
FOREIGN KEY (Numero) REFERENCES Consultation(Numero)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (Code) REFERENCES Medicament(Code)
ON DELETE CASCADE
ON UPDATE CASCADE
)
Corrigé de l’exercice-bilan
Exercice-bilan 5.1
1. a. On peut identifier 4 entités :
• Vendeur et l’identifiant est Code_ven
• Client et l’identifiant est Code_cli
• Facture et l’identifiant est Num_fact
• Produit et l’identifiant est Num_prod
b.

Précisions
• Un vendeur peut ne pas être associé à une facture ou bien il peut être associé à plusieurs factures, d’où la cardinalité coté Vendeur de (0,n).
• Une facture ne peut être associée qu’à un seul vendeur.

Remarques
• Un client peut ne pas être associé à une facture ou bien il peut être associé à plusieurs factures, d’où la cardinalité coté Client de (0,n).
• Une facture ne peut être associée qu’à un seul client.

Remarques
• Un produit peut ne pas être associé à une facture ou bien il peut être associé à plusieurs factures, d’où la cardinalité coté Produit de (0,n).
• Une facture peut être associée au minimum à un produits. Et aussi à plusieurs.

2.

3.
4.
CREATE TABLE Facture (
Num_fact INTEGER,
Code_ven INTEGER,
Code_cli INTEGER,
Date_fact DATE NOT NULL,
PRIMARY KEY (Num_fact),
FOREIGN KEY (Code_ven) REFERENCES Vendeur(Code_ven),
FOREIGN KEY (Code_cli) REFERENCES Cleint(Code_cli)
)
Chapitre 6
Le langage SQL
Cours

1 Généralités
Ce chapitre présente le langage SQL dans sa partie d’interrogation et de manipulation des données (insertion, mise
à jour, destruction), donc dans la partie LMD de la plupart des SGBDR. Il existe bien évidemment des manipulations
bien plus avancées qui dépassent le cadre du programme de NSI.
Pour la suite de ce chapitre, nous prendrons en exemple la petite base de données suivante constituée de 4
relations :
NomStation Capacité Lieu Région Tarif

Tanger 350 Maroc Afrique 1 200


La Bourboule 250 Auvergne Europe 700
Victoria 200 Seychelles Océan Indien 1 500
Courchevel 400 Alpes Europe 2 200

Relation Stations
NomStation Libellé Prix

La Bourboule Pêche 50
La Bourboule Randonnée 0
Tanger Plongée 120
Tanger Excursion 60
Victoria Plongée 130
Courchevel Ski 120

Relation Activités
Id Nom Prénom Ville Région Solde

1 Bauer Elmut Berlin Europe 9 825


2 Smith John Londres Europe 12 436
3 Jonhson Britney New York Amérique 6 721

Relation Clients
IdClient Station Arrivée NbPlaces

1 Courchevel 17/02/2019 2
3 Tanger 17/11/2018 5
2 Courchevel 28/01/2018 4
3 La Bourboule 20/07/2016 3
3 Victoria 13/09/2015 6
2 La Bourboule 13/08/2019 3
3 Courchevel 27/02/2017 5
1 Victoria 05/09/2018 3

Relation Séjours

2 Requêtes d’interrogation : SELECT


L’utilisation la plus courante consiste à lire des données issues de la base de données. Cela s’effectue grâce à la
commande SELECT, qui retourne des enregistrements dans un tableau de résultats.
Sélections simples
Prenons l’exemple où l’on souhaite extraire de notre base de données le nom et le lieu de toutes les stations se
trouvant en Europe. La requête s’effectue de la manière suivante :
SELECT nomStation, Lieu
FROM Stations
WHERE Région = ‘Europe’
Cette requête est constituée de 3 clauses :
• SELECT : on trouve ici les attributs (colonnes) que l’on souhaite extraire (afficher).
• FROM : dans cette clause, il y a toutes les tables dans lesquelles on trouve les attributs utiles à la requête.
• WHERE : Il s’agit ici d’indiquer les conditions que doivent satisfaire les tuples (les lignes) de la base pour faire
partie du résultat.
Le résultat obtenu est le suivant :
nomStation lieu

La Bourboule Auvergne
Courchevel Alpes

Remarque
• Le résultat d’une requête est une relation (table) dont les attributs sont ceux sélectionnés dans la clause SELECT.
• Pour sélectionner toutes les colonnes, on utilise le caractères « * ». Par exemple SELECT * FROM Stations.

Dans la clause WHERE, on spécifie une condition booléenne. On utilise alors les mots clés standards de la logique
booléenne, c’est-à-dire AND, OR et NOT. On peut également utiliser les opérateurs de comparaison : <, <= (pour
, >, >=
(pour , = et <> (pour .
La recherche « floue »
Il est parfois utile de rechercher les enregistrements dans la base de données dont la valeur d’un attribut
commence par telle ou telle lettre. Pour cela on utilise l’opérateur LIKE dans la clause WHERE. Par exemple la
requête suivante recherchera dans la base des données les clients dont le nom commence par « B ».
SELECT Nom
FROM Clients
WHERE Nom LIKE ‘B%’
Nom

Bauer

Les modèles de recherche sont multiples :


• LIKE ‘%a’ : Recherche toutes les valeurs de l’attribut qui se terminent par le caractère « a ».
• LIKE ‘a%’: : Recherche toutes les valeurs de l’attribut qui commencent par le caractère « a ».
• LIKE ‘%a%’ : Recherche toutes les valeurs de l’attribut qui contiennent le caractère « a ».
• LIKE ‘pa%on’ : Recherche toutes les valeurs de l’attribut qui commencent par la chaîne « pa » et qui se terminent
par « on ».

Remarque
• Le caractère « % » peut être remplacé par un nombre incalculable de caractères. Si on souhaite prendre en compte le nombre de caractères dans
notre recherche, on utilise le caractère « _ ». Par exemple LIKE ‘J___’ recherche toutes les valeurs de l’attribut qui ont 4 caractères et qui commencent
par « J ».
• Dans certains SGBD, le caractère « % » est remplacé par le caractère « * ».

Éviter les doublons


Bien que la spécification des clés permette d’éviter les doublons dans les relations stockées dans la base de
données, il peut ne pas en être de même pour le résultat de la requête. Par exemple la requête suivante donnera
autant de lignes dans la relation résultat que de lignes dans la relation Activités.
SELECT Libellé
FROM Activités
Libellé

Pêche
Randonnée
Plongée
Excursion
Plongée
Ski

Pour éviter d’obtenir deux lignes identiques, on utilise le mot-clé DISTINCT.


SELECT DISTINCT Libellé
FROM Activités
Libellé

Pêche
Randonnée
Plongée
Excursion
Ski

Trier le résultat
Il est possible de trier le résultat d’une requête avec la clause ORDER BY suivie de la liste des attributs suivie des
mots clé ASC pour un tri par ordre ascendant ou DESC pour un tri par ordre descendant. Si ces mots clés ne sont
pas présents, par défaut c’est le mot-clé ASC qui est utilisé.
SELECT *
FROM Stations
ORDER BY Région ASC, Lieu DESC
NomStation Capacité Lieu Région Tarif

Tanger 350 Maroc Afrique 1 200


La Bourboule 250 Auvergne Europe 700
Courchevel 400 Alpes Europe 2 200
Victoria 200 Seychelles Océan Indien 1 500

Remarque
L’ordre ascendant correspond au tri par ordre alphabétique lorsque l’attribut est de type chaîne de caractères.

Usages avancés
Dans le langage SQL, on peut interagir de manière dynamique avec la base de données comme par exemple :
• Appliquer des fonctions aux valeurs de chaque ligne : il s’agit principalement d’opérations mathématiques pour
les attributs numériques (+, *, …) ou de manipulations de chaînes de caractères (concaténation, minuscules,
majuscules…).
• Renommer les attributs de la relation résultat : souvent on utilise cette fonctionnalité pour donner plus de clarté
dans la lecture de la relation résultats.
Par exemple la requête suivante donnera une relation plus « lisible »
SELECT CONCAT(Prénom, ‘ ‘, UPPER(Nom)) AS Individu, Ville
FROM Clients
Individu Ville

Elmut BAUER Berlin


John SMITH Londres
Britney JONHSON New York

Remarque
• La fonction CONCAT permet de concaténer plusieurs attributs et la fonction UPPER permet de transformer l’attribut initialement en minuscules en
majuscules.
• Le mot-clé AS permet de renommer l’attribut. On utilise un alias permettant de faciliter la lecture du résultat.

Requêtes sur plusieurs tables (jointures)


On appelle jointure l’opération consistant à rapprocher selon une condition les tuples de deux relations d’une
base de données afin de former une troisième relation qui contient l’ensemble de tous les tuples obtenus en
concaténant un tuple de la première relation et un tuple de la seconde vérifiant la condition de rapprochement.
Essayons par exemple d’interroger la base de données pour donner le nom des clients avec le nom des stations où
ils ont séjourné. L’information concernant le nom du client est dans la relation Clients tandis que le lien
client/séjour se trouve dans la relation Séjours. On joint ainsi les lignes de nos 2 relations.
SELECT Nom, Station FROM Clients
INNER JOIN Séjours ON Id = IdClient
Nom Station

Bauer Courchevel
Bauer Victoria
Smith Courchevel
Smith La Bourboule
Jonhson Courchevel
Jonhson La Bourboule
Jonhson Tanger
Jonhson Victoria

Il arrive souvent qu’un même nom d’attribut soit utilisé dans plusieurs relations. Dans ce cas il faut préfixer le nom
de l’attribut par le nom de la relation. Par exemple essayons d’effectuer une recherche des stations avec leurs lieux
et régions ainsi que leurs activités et leurs tarifs.
SELECT Stations.NomStation, Lieu, Région, Libellé, Prix FROM Stations
INNER JOIN Activités ON Stations.NomStation = Activités.NomStation
NomStation Lieu Région Libellé Prix

Tanger Maroc Afrique Excursion 60


Tanger Maroc Afrique Plongée 120
La Bourboule Auvergne Europe Pêche 50
La Bourboule Auvergne Europe Randonnée 0
Victoria Seychelles Océan Indien Plongée 130
Courchevel Alpes Europe Ski 120

Dans un souci d’alléger l’écriture des requêtes, on utilise souvent des alias raccourcis pour remplacer le nom des
relations. On peut ainsi récrire la requête précédente en utilisant les alias :
SELECT s.NomStation, Lieu, Région, Libellé, Prix FROM Stations AS s
INNER JOIN Activités AS a ON s.NomStation = a.NomStation

Remarque
Le mot-clé INNER est facultatif.

On peut également faire une jointure avec plus de 2 relations. Essayons d’interroger la base de données pour
donner le nom des clients avec le nom des stations ainsi que les régions où ils ont séjourné.
SELECT c.Nom, se.Station, s.Région
FROM ((Clients AS c
INNER JOIN Séjours AS se ON c.Id = se.IdClient)
INNER JOIN Stations AS s ON se.Station = s.NomStation)
Nom Station Région

Bauer Courchevel Europe


Bauer Victoria Océan Indien
Smith Courchevel Europe
Smith La Bourboule Europe
Jonhson Courchevel Europe
Jonhson La Bourboule Europe
Jonhson Tanger Afrique
Jonhson Victoria Océan Indien

Cette jointure correspond à l’intersection de la théorie des ensembles.


Les fonctions d’agrégation
Les fonctions d’agrégation dans le langage SQL permettent d’effectuer des opérations statistiques sur une colonne
(en général de type numérique). Les principales fonctions sont les suivantes :
• COUNT() pour compter le nombre d’enregistrements sur une table ou une colonne distincte.
• MAX() pour récupérer la valeur maximum d’une colonne sur un ensemble de ligne.
• MIN() pour récupérer la valeur minimum de la même manière que MAX().
• SUM() pour calculer la somme d’un attribut sur un ensemble d’enregistrements.
• AVG() pour calculer la moyenne d’un attribut sur un ensemble d’enregistrements.
Essayons de savoir combien il y a de stations en Europe dans la relation Stations.
SELECT COUNT(NomStation) AS Nombre FROM Stations
WHERE Région = ‘Europe’
Nombre

Essayons maintenant de savoir le tarif minimum, maximum et moyen des stations.


SELECT MIN(Tarif) AS Mini, MAX(Tarif) AS Maxi, AVG(Tarif) as Moy FROM Stations
Mini Maxi Moy

700 2 200 1 400

Pour connaître le nombre total de places que M. Smith a réservé pour l’ensemble des séjours, on peut exécuter la
requête suivante :
SELECT SUM(NbPlaces) AS Total FROM Séjours
INNER JOIN Clients ON Id = IdClient AND Nom = ‘Smith’
Total

3 Requêtes de mises à jour


Insertion de données : INSERT INTO
L’insertion des données s’effectue à l’aide de la commande INSERT INTO. Cela permet d’ajouter une ou plusieurs
lignes dans la relation voulue de notre base de données. Par exemple nous souhaitons ajouter un client dans la
relation Clients.
INSERT INTO Clients (Id, Nom, Prénom, Ville, Région, Solde)
VALUES (4,’Yuan’, ‘Tchang’, ‘Pékin’, ‘Chine’, 8256)
Si nous souhaitons ajouter plusieurs clients à la fois, on sépare les valeurs par une virgule. On peut également pour
plus de clarté aller à la ligne après le mot-clé VALUES :
INSERT INTO Clients (Id, Nom, Prénom, Ville, Région, Solde)
VALUES
(5,’De Oliveira’, ‘Manuel’, ‘Porto’, ‘Europe’, 7253),
(6,’Sako’, ‘Mamadou’, ‘Abidjan’, ‘Afrique’, 2561)

Remarque
Il faut bien faire attention lors de cette manipulation car il n’est pas possible d’ajouter des doublons de lignes notamment à cause de la clé primaire de
la relation qui est unique.

Suppression de données : DELETE FROM


La commande DELETE permet de supprimer des lignes dans une relation. En utilisant cette commande associée à la
clause WHERE il est possible de sélectionner les lignes qui seront supprimées.
Conseil
Avant d’essayer de supprimer des lignes, il est recommandé d’effectuer une sauvegarde de la base de données, ou tout du moins de la table
concernée par la suppression. Ainsi, s’il y a une mauvaise manipulation il est toujours possible de restaurer les données.

Essayons de supprimer dans notre relation Clients les trois individus que l’on ajouté dans le paragraphe précédent.
DELETE FROM Clients
WHERE Id >= 4

Remarque
Si la clause WHERE n’est pas présente, cela supprimera toutes les lignes. La relation sera alors vide !

Modification de données : UPDATE


La commande UPDATE permet d’effectuer des modifications sur des lignes existantes. Très souvent cette
commande est utilisée avec la clause WHERE pour spécifier sur quelles lignes doivent porter la ou les
modifications. Essayons de modifier la valeur de l’attribut Solde de l’individu prénommé John dans la relation
Clients :
UPDATE Clients SET Solde = 11728
WHERE Prénom = ‘John’
Nous pouvons aussi mettre à jour plusieurs attributs et appliquer des formules dans la même requête. Par
exemple, essayons de modifier en majuscule les libellés et tous les prix des activités ayant subi une augmentation
de 10%.
UPDATE Activités SET Libellé=UPPER(Libellé), Prix = Prix * 1.1
Exercices
Pour tous les exercices qui suivent, on utilisera la base de données de ce chapitre.
Compétences attendues
Exprimer dans le langage SQL des requêtes d’interrogation.

Exercice 6.1
▶ Concevoir des solutions algorithmiques
▶Traduire un algorithme dans un langage de programmation
Donner l’expression SQL des requêtes suivantes ainsi que le résultat obtenu.
1. Noms des stations ayant strictement plus de 200 places.
2. Noms des clients dont le nom commence par ‘J’ ou dont le solde est supérieur à 10 000.
3. Noms des stations qui proposent de la plongée.

Exercice 6.2
▶ Concevoir des solutions algorithmiques
▶Traduire un algorithme dans un langage de programmation
Donner l’expression SQL des requêtes suivantes ainsi que le résultat obtenu.
1. Noms des clients qui sont allés à La Bourboule.
2. Noms des stations visitées par des européens.

Exercice 6.3
▶ Concevoir des solutions algorithmiques
▶Traduire un algorithme dans un langage de programmation
Donner l’expression SQL des requêtes suivantes ainsi que le résultat obtenu.
1. Combien de séjours ont eu lieu à Victoria ? On stockera le résultat dans une colonne nommée ‘Total’.
2. Donner le prix moyen d’une activité à Tanger. On stockera le résultat dans une colonne nommée ‘Prix Moyen
Activités Tanger’.

Exercice 6.4
▶ Concevoir des solutions algorithmiques
▶Traduire un algorithme dans un langage de programmation
1. Donner l’expression SQL de la requête permettant d’afficher la liste des stations suivie du lieu (en majuscule)
entre parenthèses et du tarif HT et TTC comme le montre l’exemple suivant :
Stations Tarif HT Tarif TTC

Tanger (MAROC) 1 200 1 440


La Bourboule (AUVERGNE) 700 840
Courchevel (ALPES) 2 200 2 640
Victoria (SEYCHELLES) 1 500 1 800

On supposera que le Prix saisi dans la base est le tarif HT et que le taux de TVA est de 20 %.
2. Les données correspondantes au tarif TTC des stations sont-elles stockées dans la base de données ?
Compétences attendues
Exprimer dans le langage SQL des requêtes d’insertion.

Exercice 6.5
▶ Analyser et modéliser un problème

1. Donner l’expression SQL des requêtes permettant d’ajouter la cliente venant de Toronto (Canada) suivante :
Mme Karibou Juliette avec un solde de 7 213 €. Cette cliente a séjourné (3 places) à La Bourboule le 10/07/2019.
2. Peut-on, dans l’état, ajouter à cette base que Mme Karibou a fait de la randonnée ?
Compétences attendues
Exprimer dans le langage SQL des requêtes de mise à jour de valeurs.

Exercice 6.6
▶ Analyser et modéliser un problème

1. Donner l’expression SQL de la requête permettant de mettre à jour la capacité de la station Courchevel à 450
places ainsi que le nouveau tarif de 2 300 €.
2. Peut-on changer ici le nom de l’attribut ‘Prix’ en ‘Prix HT’ de la relation Activités par une requête de type
UPDATE ?
Compétences attendues
Exprimer dans le langage SQL des requêtes de suppression.

Exercice 6.7
▶ Analyser et modéliser un problème

1. Donner l’expression SQL de la requête permettant de supprimer tout ce qui concerne Mme Karibou (données
insérées dans le cadre de l’exercice 6.5).
On supposera que la structure est bien correcte, à savoir que l’attribut IdClient de la relation Séjours est bien une
clé étrangère liée en référence à l’attribut Id de la relation Clients et que l’on a bien spécifié le ON DELETE
CASCADE à la création de la clé étrangère de la relation (voir chap. 5.).
2. Que faire si la clé étrangère n’a pas été définie dans la relation Séjours ?
Corrigé des exercices
Exercice 6.1
1.
SELECT NomStation
FROM Stations
WHERE Capacité >200
La requête SQL retourne le résultat suivant :
NomStation

Tanger
La Bourboule
Courchevel

2.
SELECT Nom
FROM Clients
WHERE Nom LIKE ‘J%’ OR Solde >= 10 000
La requête SQL retourne le résultat suivant :
Nom

Smith
Jonhson

À noter
La caractère « % » peut dans certain SGBD être remplacé par le caractère « * ».

3.
SELECT NomStation
FROM Activités
WHERE Libellé = ‘Plongée’
La requête SQL retourne le résultat suivant :
NomStation

Tanger
Victoria

À noter
Lorsque les valeurs sont de type chaîne de caractères, on les place entre les symboles « ‘ » pour pouvoir effectuer les comparaisons.

Exercice 6.2
1.
SELECT Nom
FROM Clients
INNER JOIN Séjours ON ((Clients.Id = Séjours.IdClient) AND (Séjours.Station = ‘La Bourboule’))
La requête SQL retourne le résultat suivant :
Nom

Smith
Jonhson
À noter
Il s’agit bien ici d’une jointure de 2 relations qui doit respecter 2 conditions. Pour simplifier l’écriture, on peut également utiliser des alias pour les noms
des relations. Ce qui donnerait par exemple la requête suivante :
SELECT Nom
FROM Clients AS c
INNER JOIN Séjours AS s ON ((c.Id = s.IdClient) AND (s.Station = ‘La Bourboule’))

2. Intuitivement, on a envie de faire la jointure suivante :


SELECT Station
FROM Séjours AS s
INNER JOIN Clients AS c ON ((c.Id = s.IdClient) AND (c.Région = ‘Europe’))
Le résultat de cette requête donne :
Station

Courchevel
Victoria
Courchevel
La Bourboule

On s’aperçoit qu’il y a des doublons, ce qui est logique puisque Courchevel a été visité par M. Bauer mais aussi
par M. Smith.
Il nous faut donc éliminer ce doublon en utilisant le mot-clé DISTINCT, ce qui donne la requête suivante :
SELECT DISTINCT Station
FROM Séjours AS s
INNER JOIN Clients AS c ON ((c.Id = s.IdClient) AND (c.Région = ‘Europe’))

Exercice 6.3
1. Cela revient à compter toutes les lignes où Station = ‘Victoria’.
SELECT COUNT(*) AS Total FROM Séjours
WHERE Station = ‘Victoria’
Le résultat obtenu est :
Total

2.
SELECT AVG(Prix) AS ‘Prix Moyen Activités Tanger’ FROM Activités
WHERE Activités.NomStation = ‘Tanger’
Le résultat obtenu est :
Prix Moyen Activités Tanger

90

Exercice 6.4
1.
SELECT CONCAT(NomStation,' (',UPPER(Lieu),')') AS Stations, Tarif AS ‘Tarif HT’, Tarif*1.2 AS ‘Tarif TTC’
FROM Stations
2. Les données calculées à partir de données présentes dans une requête SELECT ne sont pas stockées dans la base
de données.

À noter
Pour pouvoir stocker ces valeurs, il faudrait au préalable modifier la structure de la base de données comme par exemple ajouter un attribut dans la
relation Stations.

Exercice 6.5
1. La mise à jour de la base de données se décompose en l’ajout d’une occurrence dans la relation Clients et d’une
occurrence dans la relation Séjours.
INSERT INTO Clients (Id, Nom, Prénom, Ville, Région, Solde)
VALUES (4,’Karibou’, ‘Juliette’, ‘Toronto’, ‘Amérique’, 7213)
INSERT INTO Séjours (IdClient, Station, Arrivée, NbPlaces)
VALUES (4,’La Bourboule’, ‘2019-07-10’, 3)
2. En l’état, il n’y a aucune relation (association) entre la relation Séjours et la relation Activités. On ne peut donc
pas dire que Mme Karibou a fait de la randonnée.

À noter
Il faudrait au préalable modifier la structure de la base de données comme par exemple ajouter une clé primaire ‘Idactivité’ dans la relation Activités et
la clé étrangère associée dans la relation Séjours.

Exercice 6.6
1.
UPDATE Stations SET Capacité = 450, Tarif = 2300
WHERE NomStation = ‘Courchevel’
2. Avec une requête de type UPDATE, on ne peut mettre à jour que des valeurs. On se trouve ici dans la partie LMD
du langage SQL.

À noter
Si l’on souhaite changer le nom d’un attribut, Il faut modifier la structure de la base de données au niveau LDD du langage SQL avec le mot clé ALTER
TABLE (voir chap. 5) comme le montre la requête suivante : ALTER TABLE Stations CHANGE Prix ‘Prix HT’

Exercice 6.7
1.
DELETE FROM Clients
WHERE Id = 4

À noter
Le critère du WHERE est bien l’attribut Id car c’est lui qui l’identifiant (clé primaire). En effectuant cette requête, toutes les relations référencées seront
modifiées.

2. S’il n’y a aucune référence à d’autres relations, il faut en pratique supprimer toutes les occurrences concernées
dans la relation Séjours avec la requête suivante :
DELETE FROM Séjours
WHERE IdClient = 4
Et enfin supprimer les occurrences dans la relation Clients avec la requête du 1.

À noter
Afin d’éviter le plus possible les anomalies (lignes orphelines dans une relation), il est primordial d’avoir bien réfléchi à la structure de la base de
données.
Il est aussi impératif de bien savoir le risque que l’on prend lorsque l’on supprime des données. Il est souvent recommandé de faire une sauvegarde
avant toute suppression.
Partie 3
Architecture matérielle, systèmes d’exploitation et réseaux
Chapitre 7
Architectures matérielles et systèmes d’exploitation
Cours

1 Un peu d’histoire
Du premier processeur, ouvrant la voie à l’informatique, aux récents microprocesseurs et aux systèmes
d’exploitation mobiles, l’histoire a été rapide. Nous la retraçons ici en quelques dates clés :

2 Microprocesseur et mémoire
L’architecture de base
Le fonctionnement d’un outil numérique, quelle que soit sa nature (ordinateur, tablette, téléphone, assistant GPS,
appareil photo…) est basé sur deux éléments fondamentaux :
• le processeur
Aussi appelé microprocesseur (car sa taille miniaturisée lui permet d’être intégré à n’importe quel élément
numérique actuel), c’est lui qui est en charge d’effectuer les calculs élémentaires nécessaires à tout
fonctionnement.
Le processeur porte aussi le nom de CPU (Central Processing Unit).
• la mémoire
Elle est chargée de stocker (de manière plus ou moins statique selon sa nature), les données nécessaires à toute
opération.
En utilisant ces deux éléments, un principe de base permet l’activité numérique : tout programme est une suite
d’opérations simples qui ont toutes la même forme :
❶ une instruction élémentaire à effectuer est chargée de la mémoire sur le processeur

❷ les opérandes (données sur lesquelles va être fait le calcul) sont chargées de la mémoire sur le processeur

❸ le calcul de l’opération élémentaire est effectué

❹ le résultat de l’opération est stocké en mémoire

Les microprocesseurs
Au niveau technique, le microprocesseur est un circuit électronique intégré qui effectue des opérations. Sa taille
est de plus en plus réduite.
Les opérations qu’est capable d’effectuer un microprocesseur sont son jeu d’instructions.
La vitesse d’un microprocesseur est définie par son horloge : l’horloge fournit le rythme des tâches élémentaires
effectuées, en Hz (nombre de pulsations par seconde).

Concept
La rapidité à effectuer des instructions par un microprocesseur s’exprime en MIPS (Millions d’Instructions Par Seconde).

Historiquement, deux familles de microprocesseurs sont disponibles sur le marché, basées sur des
fonctionnements opposés :
• les processeurs RISC (Reduced Instruction Set Computer) proposent un nombre restreint d’instructions, qu’il est
possible d’effectuer efficacement et très rapidement
• les processeurs CISC (Complex Instruction Set Computer) disposent d’instructions plus nombreuses et plus
élaborées, mais sont donc moins rapides pour effectuer ces instructions.
Le choix du processeur selon le besoin a donc une importance, mais notons que les dernières évolutions en termes
de rapidité permettent de créer des RISC très puissants dont l’utilisation peut être comparée à celle des CISC,
rendant la spécificité de chaque famille moins évidente.
Les mémoires
La mémoire est le support, principalement magnétique (disques durs) ou électronique (RAM, Clés USB, disques
SSD, ROM) qui contient les programmes et les données.

Concept
La mémoire peut être de deux natures :
• permanente : les données sont conservées lorsque la machine est éteinte
• volatile : les données ne sont conservées que pour la durée de leur utilisation

Dans un ordinateur, plusieurs grandes familles de mémoire sont utilisées :


• La mémoire vive contient les programmes et données nécessaires au microprocesseur. C’est une mémoire
volatile, mais accessible très rapidement.
• Le stockage est constitué des dispositifs qui permettent de conserver de manière permanente toutes les données
(système d’exploitation, applications, données…).
• Le cache est une mémoire très rapide, dans laquelle sont stockées des données auxquelles le microprocesseur a
besoin d’accéder souvent, permettant ainsi un gain de temps.
• Le registre est une mémoire, de taille réduite, mais directement intégrée dans le microprocesseur, pour un gain
de temps d’accès très important. Pour les microprocesseurs actuels, la taille du registre est un facteur important
de choix et de prix.

3 Les systèmes sur puces : les SoCs


Le principe
Rendu possible par la miniaturisation permanente des composants électroniques, l’idée de base est d’intégrer dans
une seule puce plusieurs éléments, de natures et de rôles différents, pour créer un système autonome capable
d’effectuer une tâche spécifique.
Ces dispositifs portent le nom de systèmes sur puce, ou System On a Chip, d’où l’appellation actuelle de SoCs.

Concept
Le principe élémentaire d’un système sur puce est d’effectuer une tâche définie, de manière complète, robuste et rapide. Ce sont ces objectifs qui
définissent les éléments à intégrer dans le SoC.

Les éléments qui peuvent être intégrés dans un SoC sont extrêmement nombreux : des microprocesseurs, de la
mémoire, des dispositifs de communication sans fil, des dispositifs d’entrée/sortie, des capteurs…
Exemples
Nous prenons ici quelques exemples de natures différentes pour illustrer l’apport des SoCs dans les appareils
numériques actuels.
❶ Certains appareils de photo numérique intègrent des SoCs très complets de traitements d’image, offrant des

fonctions qui devaient auparavant être réalisées par des applications logicielles.
❷ Des fabricants ont mis sur le marché des ordinateurs miniaturisés complets, dits nano-ordinateurs, basés sur

l’utilisation de SoCs très complets, voire sur un seul SoC qui regroupe toutes les fonctionnalités d’un ordinateur
habituel.
❸ Dans les smartphones, un seul SoC peut être en charge de toutes les communications.

❹ Les microprocesseurs actuels, dits processeurs multi-cœurs, sont équipés de plusieurs unités de travail

indépendantes, appelées cœurs : ils sont capables d’effectuer plusieurs opérations simultanément, c’est-à-dire
qu’il est devenu possible d’effectuer un nombre plus élevé d’instructions que l’horloge ne donne de pulsations.
Des avantages nombreux
Les avantages des systèmes sur puce sont nombreux :
• La vitesse de traitement et donc l’efficacité sont accrues. En effet, la proximité des composants sur le circuit
électronique miniaturisé réduit les distances, l’utilisation d’éléments moins génériques améliore l’efficacité.
• Le regroupement des éléments ne nécessite plus d’alimentations multiples, la consommation énergétique est
réduite de manière significative, entraînant une baisse du coût d’énergie et une amélioration de la gestion de
l’énergie (principalement pour l’informatique mobile et les smartphones, pour lesquels la gestion des
performances de la batterie est importante).
• Même si les coûts d’ingénierie sont plus élevés sur la phase de conception, les coûts de matières premières et de
fabrication sont eux aussi réduits par rapport à une architecture classique.
• Les possibilités de miniaturisation des ordinateurs et de tous les autres équipements numériques sont encore
accrues, offrant ainsi un fort potentiel d’innovation.
• L’adaptation au besoin est affinée : la spécificité de chaque système permet une efficacité optimale.

4 Les systèmes d’exploitation


Le fonctionnement général
Le système d’exploitation est un ensemble de programmes qui va permettre d’utiliser les éléments physiques d’un
ordinateur pour exécuter les applications nécessaires à l’utilisateur.
L’élément fondamental du système d’exploitation est le noyau, c’est lui qui permet et gère l’accès aux ressources
matérielles.
Ses principales fonctions sont :
• le dialogue avec les périphériques (microprocesseur, mémoire, disques, carte graphique, carte réseau, clavier,
souris…)
• l’exécution par le microprocesseur des programmes souhaités par les utilisateurs et l’ordonnancement de ces
tâches
• la gestion des accès aux ressources, pour permettre d’une part à tous les utilisateurs de travailler simultanément,
et d’autre part de ne permettre l’utilisation d’une ressource qu’aux utilisateurs autorisés.
Au-dessus du noyau, de très nombreux programmes sont en charge de toutes les fonctions qui sont offertes aux
programmes utilisateurs pour permettre une utilisation complète et optimale de la machine physique
(gestionnaire de fichiers, lecture de sons, gestion de l’énergie, gestion des communications réseau, gestion des
performances…)
Les systèmes d’exploitation actuels proposent aussi de nombreux outils de niveau supérieur, qui apportent du
confort de travail à l’utilisateur, jusqu’à lui éviter l’installation de programmes à part entière (navigateur Internet,
outils de traitement d’image, logiciel de messagerie, traitement de texte, outils de diagnostic…).
Les différents éléments
Le schéma suivant illustre l’architecture globale d’un système d’exploitation :

Au niveau utilisateur, nous trouvons les applications, exécutées via l’interface graphique ou directement en mode
commandes. Les applications peuvent utiliser des bibliothèques de fonctions.
Ces applications s’appuient sur le noyau, élément central du système d’exploitation, qui génère des appels
système pour accéder à une ressource.
Selon la nature de la ressource nécessaire, des gestionnaires spécifiques sont sollicités : le gestionnaire de
processus pour l’exécution d’un programme par le microprocesseur, le gestionnaire de mémoire pour l’accès à
une donnée en mémoire, le système de fichiers pour la gestion des périphériques de stockage de masse (disque
dur, DVD…), les protocoles réseaux pour les outils de gestion des différents réseaux disponibles.
Chaque ressource physique est gérée par un pilote, seule entité logicielle capable du dialogue avec le
périphérique.
Pour de nombreux périphériques, un gestionnaire spécifique n’est pas nécessaire : le noyau peut solliciter
directement le pilote concerné.

5 Les processus
Objectifs
Pour permettre le fonctionnement d’un ordinateur, de nombreuses tâches ou applications doivent être exécutées
simultanément, par le système d’exploitation et les différents utilisateurs.
Notons aussi qu’une même application (programme) doit pouvoir s’exécuter plusieurs fois simultanément (par
plusieurs utilisateurs par exemple), ou que plusieurs applications doivent pouvoir accéder à un même périphérique
sans conflit.
Pour permettre cela, le système d’exploitation génère de nombreux processus, puis gère leur exécution.

Concept
Un processus est un programme en cours d’exécution.
Les notions de programme et de processus sont différentes : le même programme exécuté plusieurs fois (dans le temps ou par plusieurs utilisateurs
simultanément) générera plusieurs processus.

Chaque processus possède en mémoire les instructions à exécuter et ses données.


L’ordonnancement
Le système d’exploitation doit permettre à toutes les applications et tous les utilisateurs de travailler en même
temps, c’est-à-dire donner l’impression à chacun qu’il est seul à utiliser l’ordinateur et ses ressources physiques.
Cette gestion complexe des processus est réalisée par une partie spécifique du noyau : l’ordonnanceur.

Concept
Comme une ressource (le processeur ou un périphérique) ne peut pas être partagée, c’est son temps d’utilisation qui va l’être : le temps d’utilisation
d’une ressource est partagé en intervalles très courts, pendant lesquels l’ordonnanceur l’alloue à un seul utilisateur.

L’ordonnanceur permet :
• de minimiser le temps de traitement du processus d’un utilisateur
• de garantir l’équité entre les différents utilisateurs
• d’optimiser l’utilisation de la ressource
• d’éviter les blocages.
Plusieurs algorithmes d’ordonnancement sont possibles, parmi les plus répandus, nous pouvons citer :
• Le tourniquet : la ressource est affectée à chaque processus à tour de rôle. Pour l’exécution simultanée des
processus, c’est la rapidité de ce tour de rôle qui va donner l’impression à chaque utilisateur que son processus
est seul à utiliser le processeur. Cette méthode ancienne a les avantages de sa simplicité, de sa rapidité de
gestion et de sa robustesse.

• La mise en place d’un système de priorités : l’ordre d’affectation de la ressource sera alors fonction de la priorité
de la tâche. Cette méthode est très équitable, mais la définition du niveau de priorité d’une tâche doit être
objective.
• La gestion du premier entré, premier sorti (FIFO : First In, First Out). L’exemple le plus évident de cet algorithme
est la file d’impression des documents sur une imprimante.
• L’algorithme du « plus court d’abord » : très efficace pour satisfaire au mieux les utilisateurs, mais il n’est pas
toujours simple d’évaluer le temps d’exécution d’une tâche avant son début.
Parallèlement à l’évolution des performances des microprocesseurs, l’ordonnancement est aussi un moyen
d’amélioration de la rapidité de traitement : d'autres algorithmes, de plus en plus complexes, ont été proposés
récemment.
Les interblocages
Nous avons dit précédemment que des processus peuvent avoir besoin de la même ressource.
Dans de nombreuses situations, deux processus (ou davantage) peuvent souhaiter accéder à la même donnée sur
le disque dur :
• Les deux processus ont uniquement besoin de lire la donnée : celle-ci est alors partagée, sans problème
complexe.
• Les deux processus ont besoin de la donnée de manière exclusive, pour la modifier par exemple.
• Les deux processus ont besoin de communiquer entre eux : l’un doit attendre un résultat de l’autre.
Sur le schéma suivant, les processus P1 et P2 ont tous les deux besoin de la même donnée D pour la modifier,
c’est-à-dire de manière exclusive. Le premier à y accéder est P1, D lui est allouée par le système d’exploitation.
Lorsque P2 souhaite accéder à D, la ressource n’est pas disponible : P2 est alors bloqué jusqu’à la fin de l’utilisation
de D par P1.
Prenons maintenant un autre exemple : deux processus P1 et P2 ont tous les deux besoin de deux données,
nommées sur le schéma suivant D1 et D2. Voici une situation qui peut se produire :

Chaque processus bloque une donnée et est en attente de l’autre, rien ne pourra évoluer sans une intervention
extérieure : cette situation porte le nom d’interblocage.
Face à cette problématique, deux solutions sont envisageables :
• essayer d’éviter un interblocage avant qu'il survienne
• détecter qu’un interblocage est apparu, et le supprimer.
La plupart des systèmes d’exploitation ont choisi de ne pas essayer d’éviter les interblocages, mais de les détecter
s’ils surviennent et de les solutionner.
Nous n’irons pas plus en avant ici sur ces techniques, qui ne relèvent pas du programme de cette matière.
Exercices

Compétences attendues
Appliquer l’algorithme d’ordonnancement du plus court d’abord.

Exercice 7.1
▶ Analyser et modéliser un problème
▶Mobiliser les concepts
Les 3 processus suivants doivent être exécutés simultanément sur un ordinateur à un seul microprocesseur.

L’ordonnanceur du système d’exploitation utilise la technique du « plus court d’abord ».


Schématiser l’ordre de traitement des instructions des 3 processus.
Compétences attendues
Appliquer l’algorithme d’ordonnancement en tourniquet.

Exercice 7.2
▶ Analyser et modéliser un problème
▶Mobiliser les concepts
Schématiser l’ordre de traitement des instructions des 3 processus de l’exercice 7.1 pour un ordonnancement en
tourniquet.
Compétences attendues
Appliquer l’algorithme d’ordonnancement premier entré, premier sorti.

Exercice 7.3
▶ Analyser et modéliser un problème
▶Mobiliser les concepts
Schématiser l’ordonnancement des tâches d’impression soumises par des ordinateurs d’un réseau local sur une
imprimante connectée et partagée sur ce réseau.
Exercice 7.4
▶ Concevoir des solutions algorithmiques
Écrire en Python un programme correspondant à l’ordonnancement effectué dans l’exercice 7.2.
Exercices-bilan
Exercice-bilan 7.1
2 h ● ... points
Trois commerciaux (Audrey, Enzo et Louis) d’une société de vente à distance travaillent en réseau sur le même
serveur, sur lequel ils stockent des fichiers qu’ils partagent : fichier_produits et fichier_clients.
1. Schématiser ce contexte.
2. À certaines heures de travail, les 3 commerciaux effectuent des accès nombreux aux 2 fichiers.
Voici la liste de leurs accès aux fichiers entre 9 h et 9 h 30 :
Heure de début Durée nécessaire Utilisateur Fichier Tâche effectuée

09:01:00 00:01:00 Louis fichier_produits Impression

09:02:00 00:01:00 Louis fichier_clients Impression

09:05:00 00:04:00 Audrey fichier_clients Lecture

09:07:00 00:02:00 Enzo fichier_clients Modification

09:12:00 00:09:00 Audrey fichier_produits Modification

09:18:00 00:02:00 Enzo fichier_produits Modification

Schématiser la chronologie des accès qui sont faits sur cette période.
3. Compléter le schéma du 2. avec les accès suivants :
Heure de début Durée nécessaire Utilisateur Fichier Tâche effectuée

09:24:00 00:10:00 Louis fichier_produits Mise à jour

09:28:00 00:10:00 Audrey fichier_clients Mise à jour

09:32:00 00:06:00 Audrey fichier_produits Mise à jour

09:36:00 00:06:00 Louis fichier_clients Mise à jour

4. Quel est le problème qui survient sur cette période ?


Heure de début Durée nécessaire Utilisateur Fichier Tâche effectuée

09:44:00 00:05:00 Louis fichier_produits Mise à jour

09:46:00 00:05:00 Audrey fichier_clients Mise à jour

09:49:00 00:04:00 Louis fichier_produits et fichier_clients Mise à jour

09:51:00 00:04:00 Audrey fichier_clients et fichier_produits Mise à jour


Corrigé des exercices
Exercice 7.1
Nous ordonnançons les 3 processus du traitement le moins long au traitement le plus long.

Remarque
Nous n’avons pas d’information sur la durée de traitement des instructions, nous pouvons uniquement nous baser sur le nombre d’instructions pour
estimer une durée et définir l’ordre du plus court d’abord.

Conseils
L’utilisation de la couleur dans ce schéma est une solution simple pour mettre en évidence l’ordonnancement sans lourdeur.

Exercice 7.2
Nous organisons l’ordonnancement en exécutant une instruction de chaque processus à tour de rôle :

Exercice 7.3
L’ordonnancement d’une file d’impression est organisé en FIFO : premier entré, premier sorti :

Exercice 7.4
Pour l’algorithme de l’ordonnancement en tourniquet des 3 processus, nous créons une structure de données
adaptée : les processus sont stockés dans une liste (liste_processus) et chaque élément de cette liste
(liste_processus[i]) est la liste des instructions du processus.
Nous écrivons le programme d’ordonnancement en Python :
Corrigé des exercices-bilan

Exercice-bilan 7.1
1. Nous mettons en évidence les accès aux 2 fichiers qui peuvent être nécessaires aux 3 utilisateurs :

2. Nous créons une chronologie à partir de 09:00:00, minute par minute. Nous choisissons de représenter de
couleur verte un accès non exclusif à un fichier, et de couleur rose un accès exclusif.
Heure fichier_produits fichier_clients Commentaire

09:01:00 Louis

09:02:00 Louis

09:03:00

09:04:00

09:05:00 Audrey

09:06:00 Audrey
La lecture ne nécessite pas d’accès exclusif, Enzo peut modifier le fichier, Audrey continuera d’avoir la version qu’elle a lue à
09:05:00
09:07:00 Audrey Enzo

09:08:00 Audrey Enzo

09:09:00

09:10:00

09:11:00

09:12:00 Audrey

09:13:00 Audrey

09:14:00 Audrey

09:15:00 Audrey

09:16:00 Audrey
La mise à jour nécessite un accès exclusif, Audrey bloque le ficher pendant 9 minutes, Enzo ne pourra faire sa modification
09:17:00 Audrey
que lorsqu’Audrey aura terminé la sienne, à 09:21:00
09:18:00 Audrey

09:19:00 Audrey

09:20:00 Audrey

09:21:00 Enzo

09:22:00 Enzo

3. Nous complétons la chronologie avec les accès concurrents d’Audrey et Louis aux 2 fichiers.
Heure fichier_produits fichier_clients Commentaire

09:24:00 Louis
09:25:00 Louis
Louis bloque de manière exclusive fichier_produits pendant 10 minutes
09:26:00 Louis

09:27:00 Louis

09:28:00 Louis Audrey

09:29:00 Louis Audrey

09:30:00 Louis Audrey


Audrey bloque de manière exclusive fichier_clients pendant 10 minutes
09:31:00 Louis Audrey

09:32:00 Louis Audrey

09:33:00 Louis Audrey

09:34:00 Audrey Audrey

09:35:00 Audrey Audrey


Audrey peut commencer sa modification
de fichier_produits
09:36:00 Audrey Audrey

09:37:00 Audrey Audrey

09:38:00 Audrey Louis

09:39:00 Audrey Louis

09:40:00 Louis
Louis peut commencer sa modification
de fichier_clients
09:41:00 Louis

09:42:00 Louis

09:43:00 Louis

4. Nous complétons la chronologie avec les nouveaux accès d’Audrey et Louis aux 2 fichiers :
Heure fichier_produits fichier_clients Commentaire

09:44:00 Louis
Louis bloque de manière exclusive fichier_produits pendant 5 minutes
09:45:00 Louis

09:46:00 Louis Audrey

09:47:00 Louis Audrey Audrey bloque de manière exclusive fichier_clients pendant 5 minutes

09:48:00 Louis Audrey

09:49:00 Louis Audrey


À partir de 09:49:00, Louis continue de bloquer fichier_produits de manière exclusive et attend que
fichier_clients soit disponible
09:50:00 Louis Audrey

09:51:00 Louis Audrey

09:52:00 Louis Audrey


À partir de 09:51:00, Audrey continue de bloquer fichier_clients de manière exclusive et attend que
fichier_produits soit disponible
09:53:00 Louis Audrey

09:54:00 Louis Audrey

… Louis Audrey

Nous nous trouvons dans une situation d’interblocage : Audrey et Louis ont besoin des 2 fichiers, ils ont chacun
un accès exclusif à un fichier, et attendent que l’autre libère son fichier.
Chapitre 8
Réseaux et sécurité
Cours

1 Un peu d’histoire
Internet, en tant que réseau, n’a pas été inventé, il s’est constitué progressivement au fur et à mesure de
l’apparition des technologies. Il en a été de même pour les services qu’il propose, innombrables et d’une très
grande diversité. Aujourd’hui, la sécurisation est un enjeu majeur dans le monde Internet (données,
communications…).

2 L’infrastructure d’Internet
Le maillage
Internet est né de l’interconnexion de très nombreux réseaux locaux, de norme, de taille et d’organisation très
différentes.

Les machines
Les éléments d’infrastructure sont nombreux et de natures très hétérogènes :
personnels, fixes ou portables, serveurs : leurs caractéristiques sont diverses (performances, système
ordinateurs
d’exploitation, applications…)

terminaux les téléphones, tablettes, consoles de jeux, sont aujourd’hui intégrés à l’infrastructure Internet
mobiles

la plupart des périphériques mis sur le marché sont connectables à Internet (imprimantes, copieurs, scanners,
périphériques
appareils photos…)

objets en pleine expansion, le monde des objets connectés est de nature entièrement intégré à Internet
connectés
commutateurs au cœur des réseaux locaux, ce sont eux qui sont en charge de l’interconnexion des équipements terminaux

ponts cette famille d’équipements gère l’interconnexion des infrastructures de natures ou normes différentes

en charge du trafic à travers le maillage Internet, ils permettent à l’information de transiter d’un émetteur à
routeurs
un destinataire, par un chemin satisfaisant à des contraintes structurelles, organisationnelles ou temporelles

3 Le protocole TCP/IP
Un protocole universel
Nous avons dit en introduction qu’Internet est constitué de l’interconnexion de très nombreux réseaux, de norme,
de taille et d’organisation très différentes. De manière évidente, les hôtes constituant chacun de ces réseaux sont
aussi de natures très différentes : les ordinateurs (serveurs, clients, mobiles…), des périphériques, des téléphones,
ainsi que tous les éléments d’infrastructure (commutateurs, routeurs, points d’accès Wifi…).
Devant l’hétérogénéité des équipements, la création d’un protocole de communication universel s’est imposée : le
protocole de communication IP (Internet Protocol) a donc été présenté, permettant l’interconnexion de systèmes
hétérogènes, indépendamment des supports de transmission, des normes d’infrastructure réseau, des systèmes
d’exploitation ou des applications utilisées.
IP est aujourd’hui devenu le protocole de communication universel.

Concept
Un protocole est un ensemble de caractéristiques sur lesquelles vont s’appuyer deux entités différentes pour rendre possible une action commune.

La pile TCP/IP
Le protocole IP est l’un des membres d’une famille de protocoles que l’on nomme couramment la pile TCP/IP. Dans
la plupart des cas, il est associé au protocole TCP, d’où l’appellation courante de TCP/IP, mais il peut communiquer
avec d’autres protocoles.
SSH, FTP, SMTP, DNS, SIP, NTP…

TCP, UDP

IP

La version d’IP la plus répandue est IPv4, mais une nouvelle version (IPv6) a été normalisée.
Le datagramme IP
La première tâche du protocole IP consiste à scinder les données provenant des applications en paquets de taille
constante, puis de les mettre dans un format défini, appelé datagramme IP, pour pouvoir les émettre sur un
réseau.
Le protocole IP propose un service :
• non fiable
IP véhicule les datagrammes IP entre un émetteur et un destinataire à travers le maillage d’un réseau, sans
aucune garantie de remise au destinataire.
La gestion des erreurs est simplifiée : en cas de constat d’erreur dans les données reçues, une demande de
réémission du datagramme erroné est transmise à son émetteur.
• sans connexion
IP émet les datagrammes IP en mode non connecté : chaque datagramme IP émis fera l’objet d’un routage
indépendant.
IP ne disposant pas de connaissance sur l’état des lignes de transmission (principalement leur débit ou une
coupure de ligne), l’ordre de réception peut différer de celui d’émission.
Les adresses IP
Pour permettre le transport des datagrammes, chacun des éléments d’une infrastructure (hôtes, serveurs,
périphériques, objets connectés, commutateurs administrables, routeurs…) doit posséder une adresse unique sur
le réseau : son adresse IP.
L’adresse IP est utilisée :
• pour identifier chaque élément dans l’infrastructure
• pour réaliser le routage des datagrammes IP dans celle-ci
L’adresse IP d’un ordinateur est une suite de 32 bits (soit 4 octets), habituellement représentée en notation
décimale pointée, de la forme x1.x2.x3.x4. Ces quatre octets regroupent :
❶ l’identifiant du réseau auquel appartient l’ordinateur : rID
❷ l’identifiant de l’ordinateur à l’intérieur du réseau : oID

Les adresse IP sont divisées en cinq classes, notées classe A à classe E, définies par le premier octet de l’adresse.
Les 3 principales (classes A, B et C) sont décrites dans le schéma ci-dessous.
La classe A regroupe un petit nombre de très grands réseaux (réseaux nationaux, gouvernementaux, armées,
grands opérateurs de télécommunications…). Notons qu’il n’y a plus aujourd’hui d’adresse de classe A disponible
pour des réseaux qui en auraient la nécessité.
Les adresses de réseaux de classe B sont plus nombreuses. Elles permettent d’identifier des réseaux de taille
relativement importante (jusqu’à 65 534 éléments adressables). Leur nombre est cependant restreint (16 384
réseaux possibles) et comme pour les adresses de classe A, les adresses de classe B sont actuellement totalement
attribuées.
Les adresses de classe C sont destinées aux réseaux locaux, qui ne comptent qu’un nombre peu élevé
d’ordinateurs (254 au maximum).
1 octet 1 octet 1 octet 1 octet

Classe A 0 rID oID

27 réseaux 224-2 ordinateurs


(126) (16 777 216)

De 0. 0. 0. 1

A 127. 255. 255. 254

Classe B 10 rID oID

214 réseaux 216-2 ordinateurs


(16 384) (65 534)

De 128. 0. 0. 1

A 191. 255. 255. 254

Classe C 110 rID oID

221 réseaux
28-2 ordinateurs (254)
(2 097 152)

De 192. 0. 0. 1

A 223. 255. 255. 254

Au niveau mondial, les adresses IP sont réparties par l’IANA (Internet Assigned Numbers Authority) entre les
différents registres régionaux d’adresses Internet ou RIR (Regional Internet Registries), représentant chacun une
zone géographique du monde.
Certaines adresses (par exemple 192.168.0.0 à 192.168.255.0) sont des adresses réservées destinées aux réseaux
locaux.

4 Le routage
Mode non connecté et mode connecté
En mode non connecté, les données envoyées par la machine source sont découpées en paquets avant leur envoi.
Ces paquets (les datagrammes) sont alors acheminés dans le réseau indépendamment les uns des autres.
Dans le cas d’une transmission de données en mode non connecté, aucun contrôle sur le flux d’information n’est
effectué. En effet, les données sont émises sans évaluation préalable du trafic ou de la qualité du transfert. C’est le
cas pour les transferts de données sur Internet, basés sur des services de niveau réseau sans connexion et non
fiable.

Concept
Mode non connecté :
Certaines applications ne requièrent pas d’établir une connexion avant le début d’un échange : elles fonctionnent en mode non connecté : l’émetteur
envoie les données sur le support de transmission et c’est ce dernier qui est en charge de les remettre au destinataire.
Remarquons que l’émetteur ne dispose, lorsqu’il soumet le message au réseau, d’aucune information concernant :
• l’état du destinataire, qui peut par exemple ne pas être disponible à cet instant
• le temps nécessaire jusqu’à la réception du datagramme.
Exemples : le mail (un utilisateur qui envoie un message ne vérifie pas la validité de l’adresse du destinataire), l’achat à distance.

La réalisation d’une communication en mode connecté nécessite une phase d’établissement d’une connexion
préalablement à l’envoi des données : un circuit virtuel est mis en place. Tous les paquets à véhiculer de la source
au destinataire transiteront de manière identique par ce chemin.
De même, l’acquittement de chacun des paquets reçus par les deux extrémités est transmis via le circuit virtuel
établi, ce qui permet d’offrir un service fiable sans procédé technique supplémentaire.

Concept
Mode connecté :
Certaines applications requièrent d’établir la connexion avant le début de l’échange : elles fonctionnent en mode connecté.
Un tel échange est caractérisé par trois phases bien distinctes :
❶ la connexion
❷ l’échange, de durée variable
❸ la déconnexion qui termine le dialogue.
Exemples : la prise de main à distance (effectuer des tâches sur un poste de travail à partir d’un autre poste en réseau), la communication téléphonique

En mode sans connexion, le routage est primordial pour l’acheminement de chaque datagramme.
En mode connecté, il est indispensable pour mettre en place le circuit virtuel.
Principe de routage
Nous allons voir dans cette partie ce que signifie plus précisément le terme de routage puis quelques exemples
d’algorithmes disponibles.
Un algorithme de routage a pour rôle d’acheminer un datagramme à travers le réseau. Une telle fonction ne peut
donc pas être centralisée, mais doit être présente dans chaque nœud du maillage. Elle doit, pour chaque paquet
parvenant au nœud sur l’un de ses ports, choisir sur quel port de sortie l’orienter.
De manière évidente, un algorithme de routage doit être :
• déterministe : face à une situation donnée, une solution unique doit être fournie : aucun choix n’est laissé à
l’utilisateur ou au hasard
• rapide : en toutes circonstances, il doit être en mesure de définir rapidement une route
• équitable entre les utilisateurs, dont le nombre peut être très important
• robuste : il doit fonctionner en toutes circonstances, même dégradées
• optimisé : il doit proposer le meilleur chemin possible (en temps, en distance, en encombrement…)
Les algorithmes de routages peuvent être divisés en deux familles principales :
• Les algorithmes non adaptatifs utilisent un ensemble de routes statiques mises en place par une étude
préliminaire. Ils ne tiennent pas compte de l’état des lignes de transmission au moment de l’envoi d’un
datagramme.
• Les algorithmes adaptatifs précèdent tout envoi de données d’une étude du contexte. Ces algorithmes se basent
sur l’observation directe du maillage du réseau ou du trafic sur les lignes à un instant donné. On parle ici de
routage dynamique. Les techniques mises en œuvre sont plus complexes mais sont justifiées par les
performances obtenues.
Types d’algorithmes
Nous présentons dans cette partie quelques-uns des algorithmes de routage disponibles. Certains d’entre eux sont
implémentés dans des infrastructures de réseaux (routage par inondation, routage à vecteur de distance, routage
hiérarchique) alors que d’autres nécessitent des adaptations pour être utilisables.
• Routage par inondation
Le routage par inondation (Flooding) est la technique utilisée en mode diffusion. Lorsqu’un datagramme est reçu
par un routeur sur l’un de ses ports, il est réémis sur tous les autres ports.
De manière évidente, cette méthode engendre un trafic très important sur la totalité des lignes de transmission.
Elle ne convient donc pas à des réseaux de taille élevée ou possédant un grand nombre de nœuds.
• Routage du plus court chemin
Un réseau maillé peut être représenté par un graphe G = (X, E) dont l’ensemble des sommets X regroupe les
routeurs et l’ensemble des arêtes E contient les lignes de transmission. Un chemin entre deux routeurs
correspond alors à une chaîne de G, c’est-à-dire une suite alternée de sommets et d’arêtes.
Il est possible d’associer un coût (carête) à chaque arête : le réseau peut ainsi être assimilé à un graphe valué.
La recherche du plus court chemin consiste alors à trouver la chaîne dont la somme des coûts des arêtes est
minimale (ce coût minimal pourra correspondre, en fonction du critère important dans une situation précise,
au nombre de routeurs traversés, à la distance géographique ou au trafic sur un chemin…).

Le protocole OSPF (exercice-bilan 8.1) est basé sur le coût de chaque lien, utilisé pour calculer le coût global du
chemin (par l’algorithme de Dijkstra).
• Routage à vecteur de distance
Créé initialement pour les réseaux locaux Netware de Novell, puis utilisé par Internet, le routage à vecteur de
distance est l’un des premiers algorithmes dynamiques.
Chaque élément actif possède en mémoire une table de routage qui lui est propre. Cette structure lui indique,
pour chacune des destinations connues, le port de sortie à utiliser, ainsi qu’un port par défaut pour les
destinations inconnues.
Des communications inter-routeurs permettent de mettre à jour régulièrement la table de routage de chaque
routeur à partir des connaissances de ses voisins.
Pour le routage dans Internet, cette technique a atteint ses limites car les tables des routeurs peuvent contenir
de très nombreuses entrées, et donc entraîner des pertes de temps trop importantes pour parcourir ces tables
lors de la recherche d’un destinataire.
• Routage hiérarchique
Le routage hiérarchique est basé sur la technique de routage à vecteur de distance, mais une réflexion sur la
structure des tables de routage a été menée, dans le but de limiter le nombre d’entrées à consulter lors de la
recherche d’un destinataire.
La solution consiste à diviser le réseau en plusieurs zones géographiques appelées régions.
Chaque routeur va alors posséder dans sa table trois types de données :
– les ports de sortie à emprunter pour accéder à chaque destinataire situé dans sa région,
– les ports de sortie permettant d’accéder à chacune des autres régions du réseau,
– un port de sortie à utiliser par défaut pour une adresse de destinataire inconnu.
Une telle technique peut être améliorée en mettant en place plusieurs découpages hiérarchiques successifs,
c’est-à-dire en divisant chaque zone en plusieurs sous-zones et ceci répété plusieurs fois.
Le routage à vecteur de distance sur lequel est basé l’algorithme de routage RIP utilisé sur Internet intègre cette
notion de hiérarchie.
• Routage dans les réseaux sans fil
Dans un réseau sans fil, une difficulté supplémentaire apparaît pour effectuer le routage : les ordinateurs mobiles
se déplacent géographiquement à travers le réseau et sont associés pour une durée limitée à un point d’accès
sans fil (on appelle cellule la zone géographique correspondant au point d’accès sans fil).
Le routage adapté à ces changements de cellule nécessite un algorithme particulier. Nous n’en présentons ici
qu’un fonctionnement simplifié :
❶ Initialement, chaque mobile est associé à un point d’accès de rattachement (sa station de base de

rattachement).
❷ Lorsqu’un mobile arrive dans une nouvelle cellule, il demande sa connexion à la station de base

correspondante. Cette phase de connexion nécessite un échange avec sa cellule de rattachement.


❸ La station de base de rattachement enregistre la localisation du mobile dans une table de routage. De même,

celle de la nouvelle cellule enregistre l’adresse du mobile dans une table (possédant autant d’entrées qu’il y a de
mobiles dans la cellule à cet instant).
❹ La communication peut se réaliser : les données destinées au mobile sont envoyées à sa station de base de

rattachement, qui les transmet à son tour à la station de base de la cellule où se trouve le mobile à cet instant.
Celle-ci, connaissant l’adresse du mobile grâce à sa table, transmet les données au mobile.

5 Le routage IP
Une fois les datagrammes créés, ils sont transmis sur le réseau par la machine émettrice : le protocole IP est alors
en charge de les acheminer à travers le maillage du réseau (très complexe lorsque ce réseau est étendu comme
Internet).
Le protocole IP dispose d’une méthode spécifique de routage : le protocole RIP (Routing Information Protocol).
RIP est un algorithme de routage par sauts successifs (Next-Hop Routing) : cette méthode spécifie qu’un routeur ne
connaît pas le chemin que va emprunter un datagramme, mais seulement le routeur suivant à qui ce datagramme
va être transmis.
Le principe consiste à intégrer à chaque routeur une table de routage proposant le routeur suivant pour chaque
destinataire (quelle que soit sa nature : machine, réseau, adresse inconnue). Notons que la présence d’une adresse
de machine comme destinataire est rare : sauf besoin particulier, les destinataires sont généralement des adresses
de réseau.
La structure d’une table de routage RIP est simple, comptant 4 champs pour définir une route :
• La destination du datagramme est une adresse IP (d’un hôte, d’un réseau ou d’un routeur de sortie par défaut).
• Le routeur de saut suivant (passerelle) qui permettra au datagramme d’accéder à un autre réseau (cette adresse
est le routeur lui-même si le destinataire est situé sur un réseau directement accessible via une de ses
interfaces).
• L’adresse de l’interface du routeur à utiliser pour pouvoir accéder au routeur de saut suivant.
• La valeur du vecteur de distance, qui correspond au nombre de sauts à effectuer avant d’atteindre le réseau de
la machine destinataire du datagramme. On appelle aussi cette valeur la métrique de la route.
Adresse de destination Passerelle Interface Vecteur de distance

Adresse 1 Routeur suivant 1 Interface 1 2

Adresse 2 Routeur suivant 2 Interface 1 1

… … … 1

Autres Routeur suivant 3 Interface 5 1

L’algorithme utilisé par RIP est relativement simple : il consiste à rechercher dans la table de routage la meilleure
route vers le destinataire voulu :
❶ Lorsque ce destinataire est connu du routeur, le datagramme lui est transmis directement (si le routeur est

directement connecté au réseau de destination) ou au routeur suivant à utiliser.


❷ Si le réseau du destinataire est connu du routeur, le datagramme est transmis au routeur suivant à utiliser.

❸ Si l’adresse est inconnue, le datagramme est transmis au routeur par défaut (pour la route par défaut, le vecteur

de distance est toujours de 1).


Le programme en Python suivant correspond au traitement RIP d’un datagramme reçu par un routeur, pour le
réémettre sur la route vers son destinataire :

Dans cet exemple, la structure de données de la table de routage est uniquement donnée à titre d’exemple (dans
une utilisation réelle, cette table complète est stockée dans la mémoire du routeur). De même, dans cet objectif
d’exemple simple, à la ligne 26 du programme, nous ne détaillons pas la fonction qui extrait l’adresse réseau du
destinataire à partir de l’adresse du destinataire entrée (qui nécessiterait de traiter l’adresse entrée en fonction de
sa classe et d’un éventuel masque de sous-réseau, pour en extraire l’identifiant du réseau rID, puis de le compléter
par des octets 0 pour l’identifiant de l’ordinateur oID).
Les routeurs s’échangent les informations contenues dans leurs tables au moyen de messages particuliers appelés
messages RIP : à intervalles de temps réguliers (généralement 30 secondes), chaque routeur émet un message RIP
à destination de ses voisins directs.
Un message RIP contient la liste des réseaux connus du routeur émetteur :
Routeur : toutes les adresses connues par le routeur (machines ou réseaux) Vecteur de distance : combien de sauts compte le chemin que connaît le routeur

adresse 1 1

adresse 2 3

… 1

6 TCP et la gestion des erreurs de transmission


Le protocole TCP (Transmission Control Protocol) est un protocole complémentaire au protocole IP dans la pile
TCP/IP.
TCP est associé à IP pour améliorer la qualité de service en mettant en place une transmission fiable en mode
connecté.
TCP agit à plusieurs niveaux :
• ouverture et fermeture de la connexion
• découpage des données reçues des applications en paquets appropriés à la constitution des datagrammes IP (au
maximum 65 536 octets) et réassemblage à l’arrivée si nécessaire
• contrôle de la qualité du service pour conserver un service fiable en mode connecté
• gestion des problèmes de transmission et reprise en cas d’interruption.

Concept
Une fois découpées en paquets, les données sont transmises sur le réseau par le protocole IP, dans des datagrammes IP. Ces datagrammes sont traités
individuellement. À la réception, ces paquets sont réassemblés puis mis à la disposition des applications.

Remarque
Si un émetteur envoie plusieurs trames à un même destinataire, toutes n’emprunteront pas forcément le même chemin.

Le support physique n’est pas parfait et des problèmes (perte, corruption…) peuvent survenir au cours de la
transmission d’un datagramme entre deux machines. Des méthodes doivent être mises en place pour rendre ces
problèmes d’erreurs transparents aux applications.
Deux types de problèmes peuvent se poser au cours d’un échange :
• La détection, à la réception, d’une erreur de transmission dans le datagramme, doit entraîner un traitement
spécifique : correction de l’erreur si une méthode correctrice est disponible ou envoi d’une demande de
retransmission dans le cas contraire.
• La perte d’un datagramme doit être détectée et gérée de façon à reconstituer le message émis initialement.
Une méthode de détection d’erreurs doit permettre de constater qu’une erreur est apparue dans le datagramme.
Elle ne fournit aucun détail sur le nombre d’erreurs, leur localisation, leurs conséquences sur les données… Son
seul but est de signaler que le datagramme reçu est différent de celui envoyé, et donc de demander à l’émetteur
une retransmission du datagramme endommagé.
Une méthode de correction d’erreurs est beaucoup plus complexe qu’une méthode simplement détectrice. Elle
doit transmettre en plus des données tout ce qui est nécessaire à les reconstituer en cas de constat d’erreur à
l’arrivée. La complexité de ce type de méthode et la perte de temps importante générée en font des outils peu
utilisés dans les réseaux actuels.
La méthode la plus utilisée pour gérer les pertes de datagramme dans les échanges entre deux machines est basée
sur l’utilisation d’accusés de réception (acquittements).
Le principe de base consiste à faire envoyer un datagramme spécifique par le récepteur, accusant réception du
datagramme qu’il vient de recevoir :
• lorsque l’émetteur reçoit l’accusé de réception du datagramme qu’il a envoyé, il peut émettre la datagramme
suivant
• si l’émetteur ne reçoit pas d’accusé de réception dans un délai de temps défini, il renvoie ce datagramme.
Concrètement, l’acquittement du datagramme reçu est réalisé par le champ Numéro d’acquittement dans un
datagramme de réponse retourné à l’émetteur.

Pour améliorer cette méthode, TCP propose un mécanisme de fenêtre : un nombre défini de datagrammes sont
envoyés en continu par l’émetteur sans attendre un accusé de réception. Ce nombre de datagrammes envoyés est
ce que l’on appelle la taille de la fenêtre TCP.
Notons qu’en mode non connecté, c’est le protocole UDP (User Data Protocol) qui est associé à IP pour améliorer
la qualité de service.

7 La sécurisation des communications


Les besoins
Pour mettre en place une communication sécurisée, plusieurs techniques sont complémentaires :
• L’authentification des extrémités : cette phase, prérequis indispensable à toute communication, est basée sur un
système de clés de sécurité (symétrique ou asymétrique).
• La confidentialité : assurée par un algorithme de chiffrement, qui a pour objectif que le contenu de la
communication ne soit pas lisible par un tiers. De même que pour l’authentification, le chiffrement est basé sur
un système de clés (symétriques ou asymétriques).
• L’intégrité des données : les deux extrémités de la communication ont la garantie que les données ne sont pas
modifiées entre eux.
• La gestion d’autorisations : il est possible d’appliquer des droits aux utilisateurs, selon la stratégie de sécurité
définie pour chaque nature de communication.
Principes de chiffrement
Le principe de base de la sécurisation d’une communication consiste à modifier la donnée qui doit être transmise
(le chiffrement), de façon à ce que toute personne qui l’intercepterait ne pourrait pas en comprendre le sens. Seul
le destinataire va pouvoir retrouver la donnée initiale (le déchiffrement) et la lire.
Les méthodes de chiffrement sont basées sur l’utilisation de clés (des chaînes de caractères numériques) qui vont,
par l’application d’algorithmes spécialisés, permettre de chiffrer ou déchiffrer des messages.

Concept
Le chiffrement d’un message consiste à le modifier pour le rendre illisible par une personne qui n’y est pas autorisée.
Le chiffrement (puis le déchiffrement) est effectué par l’application au message initial d’une fonction mathématique, basée sur une donnée convenue
entre les deux extrémités appelée clé de chiffrement.

Dans ce domaine, deux principales techniques sont disponibles : le chiffrement symétrique ou asymétrique. Les
protocoles actuels utilisent selon leur nature l’une ou l’autre, ou la combinaison des deux méthodes (HTTPS, le
protocole de base du Web par exemple).
Chiffrement symétrique
Cette technique consiste à définir une clé de chiffrement, dite clé publique, commune à l’ensemble des
interlocuteurs. Cette clé va servir à chiffrer les données lors de leur envoi, et de les déchiffrer à leur réception.
Une phase initiale de définition de la clé publique est nécessaire. Cette clé est ensuite communiquée à tous les
ordinateurs susceptibles d’échanger des données.
Chaque ordinateur peut à son tour être émetteur ou récepteur : selon le contexte, il utilise la clé publique pour
chiffrer (en émission) ou déchiffrer (en réception).

Chiffrement asymétrique
Le principe de chiffrement asymétrique (appelé aussi cryptographie asymétrique ou cryptographie à clé publique)
est basé sur l’utilisation de 2 clés :
• Une clé privée est définie sur le poste client et stockée sur celui-ci de manière sécurisée.
• Une clé publique est diffusée par le client à tous les postes distants.

Le transport sécurisé des données est ensuite assuré par leur chiffrement : la clé publique sert à chiffrer et la clé
privée est utilisée pour déchiffrer.
Ce dispositif nécessite qu’un ordinateur possède les clés publiques de tous les postes susceptibles de lui envoyer
un message.

Notons que le chiffrement asymétrique est aussi employé pour permettre l’authentification de l’expéditeur d’un
message : pour certifier qu’un message provient bien de lui, l’émetteur va utiliser sa clé privée pour chiffrer un
message (l’inverse du principe général). Le récepteur le déchiffre grâce à sa clé publique : s’il peut le faire, c’est
que ce message a bien été chiffré par l’émetteur, car c’est le seul à détenir la clé privée.

Concept
Le concept de signature numérique est basé sur cette technique d’authentification par chiffrement asymétrique.

HTTPS
HTTP est le protocole de base du Web : il est en charge des requêtes d’affichage des pages Web. HTTP est un
protocole non sécurisé : il a donc été nécessaire de lui ajouter des outils assurant la sécurité des transmissions des
pages.
Les protocoles SSL (Secure Socket Layer), puis TLS (Transport Laye Security) apportent une couche supplémentaire
permettant la sécurisation des échanges de façon transparente pour HTTP.

Concept
L’association des protocoles HTTP et TLS porte généralement le nom de HTTPS.

Basés sur un chiffrement asymétrique, SSL et TLS apportent à HTTP la sécurisation nécessaire entre un serveur
Web et le navigateur Internet d’un ordinateur client :
• l’échange sécurisé des clés
• l’authentification du client et du serveur
• la confidentialité des transmissions par le mécanisme de chiffrement.
Nous étudierons HTTPS de manière approfondie dans l’exercice-bilan 8.2.
Autres exemples
SSH
SSH est le protocole de référence de prise de commande à distance sécurisé (Secure SHell).
Le principe de base est la possibilité de se connecter, à partir d’un client SSH, à une autre machine (sur laquelle est
installé un serveur SSH) pour exécuter des commandes sur celle-ci.
Le client SSH peut prendre la forme d’une application en ligne de commandes (shell) ou d’une application
graphique.
Techniquement, SSH reprend les fonctionnalités communes à toutes les communications sécurisées :
authentification (basée sur une méthode de chiffrement asymétrique), confidentialité (plusieurs algorithmes de
chiffrement possibles), intégrité des données, autorisations (possibilité de donner des droits différents selon les
utilisateurs).
L’infrastructure sans fil Wifi
Le principe d’une infrastructure Wifi est de permettre une communication sans utiliser de support physique
matériel, par onde radio.
L’infrastructure est centralisée, autour d’un point d’accès Wifi. Chaque station (ordinateur portable, smartphone,
console de jeux, imprimante…) met en place une connexion avec le point d’accès : l’ensemble des éléments ainsi
connectés constitue une cellule Wifi.
L’emploi d’un protocole de chiffrement permet de sécuriser les transmissions de données entre le point d’accès et
les stations. Le protocole de chiffrement utilisé actuellement est WPA2 (Wifi Protected Access), basé sur
l’utilisation d’une clé de chiffrement publique, connue de tous les éléments autorisés et utilisée pour chiffrer
(émission) et déchiffrer (réception) les trames. Le chiffrement est réalisé par un algorithme spécifique appelé AES.
Exercices

Compétences attendues
Analyser un datagramme IP.

Exercice 8.1
▶ Décomposer un problème en sous-problèmes
▶Mobiliser les concepts
Nous avons utilisé un analyseur de trames pour capturer les datagrammes échangés sur un réseau local. Nous
étudions le datagramme suivant :
Frame 167: 74 bytes on wire (592 bits), 74 bytes captured (592 bits) on interface \Device\NPF_{CFFD652A-
7927-4235-8C66-9F399453EEB5}, id 0
Interface id: 0 (\Device\NPF_{CFFD652A-7927-4235-8C66-9F399453EEB5})
Interface name: \Device\NPF_{CFFD652A-7927-4235-8C66-9F399453EEB5}
Interface description: Wi-Fi
Encapsulation type: Ethernet (1)
Arrival Time: Jan 14, 2020 09:35:03.487495000 Paris, Madrid
[Time shift for this packet: 0.000000000 seconds]
Epoch Time: 1578990903.487495000 seconds
[Time delta from previous captured frame: 0.004513000 seconds]
[Time delta from previous displayed frame: 0.004513000 seconds]
[Time since reference or first frame: 15.057563000 seconds]
Frame Number: 167
Frame Length: 74 bytes (592 bits)
Capture Length: 74 bytes (592 bits)
[Frame is marked: False]
[Frame is ignored: False]
[Protocols in frame: eth:ethertype:ip:icmp:data]
[Coloring Rule Name: ICMP]
[Coloring Rule String: icmp || icmpv6]
Ethernet II, Src: Azurewav_68:77:ed (f0:03:8c:68:77:ed), Dst: Ubiquiti_3c:45:af (24:a4:3c:3c:45:af)
Internet Protocol Version 4, Src: 172.1.0.102, Dst: 192.168.1.1
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
Total Length: 60
Identification: 0x47ef (18415)
Flags: 0x0000
...0 0000 0000 0000 = Fragment offset: 0
Time to live: 128
Protocol: ICMP (1)
Header checksum: 0x84c1 [validation disabled]
[Header checksum status: Unverified]
Source: 172.1.0.102
Destination: 192.168.1.1
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0x4d19 [correct]
[Checksum Status: Good]
Identifier (BE): 1 (0x0001)
Identifier (LE): 256 (0x0100)
Sequence number (BE): 66 (0x0042)
Sequence number (LE): 16896 (0x4200)
[No response seen]
Data (32 bytes)

1. Quelle est la nature du réseau utilisé ?


2. Extraire l’adresse IP de l’émetteur et celle du destinataire de ce datagramme.
3. Quelle est l’application qui a généré ce datagramme ?
Compétences attendues
Connaître le fonctionnement du protocole de routage RIP.

Exercice 8.2
▶ Analyser et modéliser le problème
▶Mobiliser les concepts
Un routeur a la table de routage suivante :
Adresse de destination Passerelle Interface Vecteur de distance

192.8.13.20 192.168.1.254 192.168.1.3 3

192.168.1.0 192.168.1.254 192.168.1.3 1

180.18.0.0 180.18.1.254 180.18.1.1 1

180.19.0.0 180.19.1.254 180.18.1.1 2

180.19.3.0 180.19.1.254 180.18.1.1 2

Défaut 192.168.1.254 192.168.1.3 1

Donner le message RIP émis par ce routeur.

Exercice 8.3
▶ Développer des capacités d’abstraction et de généralisation
Soit le réseau suivant :

Donner la table de routage RIP du routeur R1.

Exercice 8.4
▶ Analyser et modéliser un problème
▶Décomposer un problème en sous-problème s
Soit le réseau suivant :

1. Expliquer comment, lorsqu’il reçoit un datagramme sur l’une de ses interfaces, le routeur R2 retransmet ce
datagramme en fonction de son destinataire.
2. Donner la table de routage RIP du routeur R2.
3. Donner la table de routage RIP du routeur R4.
4. Donner la table de routage RIP du routeur R6.
Exercice 8.5
▶ Analyser et modéliser un problème
▶Reconnaître des situations déjà analysées
La société Import3000 est spécialisée dans l’import de produits numériques et dans leur revente sur le marché
français. Son réseau informatique est structuré en 3 parties :
• le réseau administratif abritant tous les postes de travail et les serveurs de fichiers et de gestion (réseau
192.168.1.0)
• le réseau commercial (réseau 142.7.0.0)
• la zone démilitarisée (DMZ) hébergeant les serveurs Web accessibles par Internet (réseau 19.0.0.0)
L’organisation de ce réseau est la suivante :

1. Donner la ligne de la table de routage d’un hôte du réseau administratif nécessaire pour qu’il puisse joindre tout
hôte du réseau commercial.
2. Donner la ligne de la table de routage de cet hôte du réseau administratif nécessaire pour qu’il puisse joindre le
serveur Web.
3. Donner la ligne de la table de routage d’un hôte du réseau commercial nécessaire pour qu’il puisse joindre le
serveur Web.
Compétences attendues
Analyser une situation réelle de routage RIP.

Exercice 8.6
▶ Analyser et modéliser un problème
Sur un serveur Linux, la commande qui permet d’afficher la table de routage est route.
Dans cet exercice, nous exécutons route sur un serveur en activité, le résultat est le suivant :
Destination Passerelle Genmask Indic Metric Ref Use Iface
192.169.1.36 0.0.0.0 255.255.255.255 UH 0 0 0 eth0
192.169.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
195.1.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth1
70.0.0.0 0.0.0.0 255.0.0.0 U 3 0 0 eth1
127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
default 192.169.1.254 0.0.0.0 UG 1 0 0 eth0
À combien de réseaux ce routeur est-il relié ?
Compétences attendues
Connaître le fonctionnement des accusés de réception.

Exercice 8.7
▶ Développer des capacités d’abstraction et de généralisation
Sur la base du diagramme au paragraphe 6. du cours, schématiser les échanges de trames et la gestion de la
fenêtre pour un envoi ayant les caractéristiques suivantes :
• le message à envoyer est constitué de 8 datagrammes
• la taille de la fenêtre est 3
• le datagramme ④ se perd en cours de transmission
• l’acquittement ⑥ se perd en cours de transmission
Exercices-bilan
Exercice-bilan 8.1
30 min ● ... points
Le protocole OSPF est un autre protocole de routage, dit de « routage à état de liens ».
Faire toutes les recherches nécessaires pour établir un comparatif entre RIP et OSPF.
Exercice-bilan 8.2
60 min ● ... points
HTTP est le protocole de base du Web : c’est lui qui transmet les requêtes de pages Web et assure le transport de
ces pages Web entre le serveur et le client, pour que le navigateur de celui-ci puisse les afficher.
Les transferts générés par HTTP ne sont pas sécurisés : il a donc été nécessaire de lui ajouter des outils assurant la
sécurité des transmissions des requêtes et pages.
La sécurité a été ajoutée à HTTP par les protocoles SSL, puis TLS, donnant un complexe qui a pris le nom d’HTTPS.
HTTPS intègre la sécurité aux différents niveaux d’un échange, par l’utilisation des techniques de chiffrement :
• l’échange sécurisé de clés,
• l’authentification du client et du serveur,
• la confidentialité des transmissions (requêtes et pages) par un mécanisme de chiffrement.
1. Sachant que HTTPS assure la confidentialité des données par un chiffrement symétrique, représenter par un
schéma la transmission sécurisée de la requête d’une page Web d’un client à destination d’un serveur Web.
2. Ajouter à ce schéma la transmission de la page Web du serveur vers le client.
3. Nous avons dit que la clé publique utilisée par le client et le serveur pour cette transmission des pages est
générée au départ de l’échange par le serveur, puis transmise au client.
Quelle est la problématique qui se pose à ce niveau ?
4. Proposer une solution pour sécuriser cette transmission de la clé publique de chiffrement symétrique.
5. Compléter le schéma de 2. en intégrant la diffusion de la clé publique symétrique.
Corrigé des exercices
Exercice 8.1
1. Dans la première partie, l’analyseur décrit les niveaux physique et liaison de données de la transmission : nous
en extrayons les lignes intéressantes :
Interface id: 0 (\Device\NPF_{CFFD652A-7927-4235-8C66-9F399453EEB5})
Interface name: \Device\NPF_{CFFD652A-7927-4235-8C66-9F399453EEB5}
Interface description: Wi-Fi
Encapsulation type: Ethernet (1)

La 2e ligne nous indique l’interface qui est utilisée, et la 3e ligne nous précise que c’est une interface Wifi.
La 4e ligne indique que c’est un datagramme Ethernet, norme utilisée pour les réseaux locaux.
2. Dans la partie concernant le protocole réseau, nous extrayons les lignes suivantes :
Internet Protocol Version 4, Src: 172.1.0.102, Dst: 192.168.1.1
0100 .... = Version: 4
Source: 172.1.0.102
Destination: 192.168.1.1

En étudiant ces caractéristiques, nous lisons à la 2e ligne que le protocole de communication est IPv4. Les 3e et 4e
lignes nous indiquent que l’adresse de l’émetteur (Source) est 172.1.0.102, et que celle du destinataire
(Destination) est 192.168.1.1.
3. Au niveau supérieur, correspondant aux couches applicatives, nous trouvons les caractéristiques suivantes :
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Ce datagramme est une requête ping (basée sur le protocole ICMP d’administration de réseau) lancée par
l’émetteur pour savoir s’il peut joindre le destinataire.

Exercice 8.2
Un message RIP est émis par chaque routeur pour transmettre à ses voisins la liste des destinataires (hôtes ou
réseaux) pour lesquels il possède une route dans sa table.
La 1re ligne définit la route vers l’hôte d’adresse IP 192.8.13.20 (adresse de classe C, avec 192.168.1 pour
l’identifiant du réseau et 20 pour l’identifiant de l’hôte dans ce réseau).
La 2e ligne définit la route vers le réseau 192.168.1.0 (réseau de classe C).
La 3e ligne définit la route vers le réseau 180.18.0.0 (réseau de classe B).
La 4e ligne définit la route vers le réseau 180.19.0.0 (réseau de classe B).
La 5e ligne définit la route vers l’hôte d’adresse IP 180.19.3.0 (adresse de classe B, avec 180.19 pour l’identifiant du
réseau et 3.0 pour l’identifiant de l’hôte dans ce réseau). Cet hôte fait partie du réseau 180.19.0.0 pour lequel la
route est déjà connue (4e ligne) : cette ligne ne sera pas reprise dans le message RIP car la route vers cet hôte est
déjà comprise dans une autre ligne du message RIP.
Le message RIP émis par le routeur est :
Destinataire Vecteur de distance

192.8.13.20 3

192.168.1.0 1

180.18.0.0 1

180.19.0.0 2

Exercice 8.3
La table de routage liste les routes d’accès à chaque réseau, chacune de ces routes étant définie par 4 champs.
Un routeur n’a pas obligatoirement de route vers chaque réseau, mais une route par défaut permet de
transmettre les datagrammes destinés à des réseaux pour lesquels il ne connaît pas de route.
En reprenant les réseaux présents dans le schéma, une table de routage de R1 peut être :
Adresse de destination Passerelle Interface Vecteur de distance

192.168.0.0 192.168.0.1 192.168.0.1 1

18.13.0.0 192.168.0.254 192.168.0.1 1

19.20.0.0 192.168.0.254 192.168.0.1 1

défaut 192.168.0.254 192.168.0.1 1

Exercice 8.4
1. Lorsque le routeur R2 reçoit un datagramme sur l’une de ses interfaces, il l’analyse pour en extraire le
destinataire. R2 recherche ensuite dans sa table de routage s’il connaît une route vers ce destinataire (le
destinataire lui-même ou le réseau qui le contient), puis retransmet le datagramme au routeur de saut suivant
défini dans cette route, par son interface elle aussi définie dans la route.
Listons les possibilités de routage au niveau de R2, à partir des réseaux que nous trouvons sur le schéma :
• Les trames destinées au réseau 192.168.0.0 seront retransmises à la passerelle 192.168.0.100, c’est-à-dire sa
propre interface 192.168.0.100, et ce réseau sera accessible directement sur cette interface (vecteur de saut à
1).
• Les trames destinées au réseau 32.0.0.0 seront retransmises à la passerelle 32.2.2.1, c’est-à-dire sa propre
interface 32.2.2.1, et ce réseau sera accessible directement sur cette interface (vecteur de saut à 1).
• Les trames destinées au réseau 16.0.0.0 seront retransmises à la passerelle 16.1.1.1, ici aussi sa propre
interface 16.1.1.1 pour un réseau accessible directement sur cette interface.
• Les trames destinées au réseau 17.0.0.0 seront retransmises à la passerelle 16.1.1.100, par son interface
16.1.1.1, et ce réseau sera accessible par 1 saut de routeur.
• De même, les trames destinées au réseau 18.0.0.0 et au réseau 19.0.0.0 seront retransmises à la passerelle
16.1.1.100, par son interface 16.1.1.1, et ces réseaux seront accessibles chacun par 1 saut de routeur.
• Les trames dont le destinataire n’est pas listé dans la table (destinataire défaut) seront retransmises à la
passerelle 16.1.1.100, par son interface 16.1.1.1. (pour la route par défaut, le vecteur de distance est toujours
spécifié à 1).

Erreurs à éviter
Le vecteur de distance (la métrique) est le nombre de routeurs à traverser pour atteindre le réseau destinataire, mais bien noter les deux métriques
spécifiques :
• pour un réseau directement accessible par une interface du routeur, sa valeur est toujours 1
• pour la route par défaut, sa valeur est toujours 1.

2. Nous devons proposer une route vers chaque réseau, et ajouter une route par défaut qui sera utilisée pour
émettre tous les datagrammes dont le destinataire n’est pas listé dans la table.
Dans un premier temps, nous proposons une table exhaustive, c’est-à-dire qui liste la totalité des destinataires
du schéma, nous la simplifierons ensuite.
Une table de routage de R2 peut donc être :
Adresse de destination Passerelle Interface Vecteur de distance

192.168.0.0 192.168.0.100 192.168.0.100 1

32.0.0.0 32.2.2.1 32.2.2.1 1

16.0.0.0 16.1.1.1 16.1.1.1 1

17.0.0.0 16.1.1.100 16.1.1.1 1

18.0.0.0 16.1.1.100 16.1.1.1 1

19.0.0.0 16.1.1.100 16.1.1.1 1


défault 16.1.1.100 16.1.1.1 1

Dans un second temps, nous simplifierons la table en regroupant en une seule ligne les routes qui utilisent la
même passerelle.
La table de routage ci-dessus devient :
Adresse de destination Passerelle Interface Vecteur de distance

192.168.0.0 192.168.0.100 192.168.0.100 1

32.0.0.0 32.2.2.1 32.2.2.1 1

16.0.0.0 16.1.1.1 16.1.1.1 1

défault 16.1.1.100 16.1.1.1 1

Conseils
Pour simplifier la table routage et diminuer le nombre de routes qu’elle contient, il est intéressant de supprimer des routes qui sont communes :
• plusieurs routes peuvent être regroupées par une seule ligne lorsqu’elles sont accessibles par le même routeur
• si certains réseaux connus sont accessibles par l’adresse défaut, il n’est pas nécessaire de les lister.

3. D’après le schéma du réseau, une table de routage exhaustive de R4 peut être :


Adresse de destination Passerelle Interface Vecteur de distance

192.168.0.0 16.1.1.1 16.1.1.100 1

32.0.0.0 16.1.1.1 16.1.1.100 1

16.0.0.0 16.1.1.100 16.1.1.100 1

17.0.0.0 17.1.1.1 17.1.1.1 1

18.0.0.0 18.1.1.1 18.1.1.1 1

19.0.0.0 19.1.1.1 19.1.1.1 1

défault 19.1.1.100 19.1.1.1 1

Cette table présente une seule route commune : pour les réseaux 192.168.0.0 et 32.0.0.0). Il n’existe pas de
manière de regrouper en un seul réseau ces deux réseaux (de classe différente, non consécutifs), donc cette
simplification n’est pas possible, et la table ne peut pas être réduite.
4. Après étude du schéma, une table de routage de R6 peut être :
Adresse de destination Passerelle Interface Vecteur de distance

192.168.0.0 18.1.1.1 18.1.1.100 2

32.0.0.0 18.1.1.1 18.1.1.100 2

16.0.0.0 18.1.1.1 18.1.1.100 1

17.0.0.0 18.1.1.1 18.1.1.100 1

18.0.0.0 18.1.1.100 18.1.1.100 1

19.0.0.0 18.1.1.1 18.1.1.100 1

défault 18.1.1.1 18.1.1.100 1

Nous essayons ensuite de simplifier si possible cette table. À l’exception de l’accès au réseau 18.0.0.0,
directement accessible sur l’une des interfaces du routeur R6, toutes les autres routes sont définies en utilisant le
routeur R4 comme routeur de saut suivant (tous les datagrammes sont retransmis à la passerelle 18.1.1.1) : ces
routes peuvent être regroupées en une seule, et la table de routage de R6 peut devenir :
Adresse de destination Passerelle Interface Vecteur de distance

18.0.0.0 18.1.1.100 18.1.1.100 1

défault 18.1.1.1 18.1.1.100 1


Remarque
Notons que nous avons perdu le vecteur de distance associé à chaque route : si un nouveau message RIP informe R6 d’une nouvelle route vers l’un des
réseaux, R6 le stockera obligatoirement, ne sachant pas si cette nouvelle route est plus courte que celle qu’il connaissait auparavant.
La table de routage grandira donc à nouveau très rapidement pour redevenir telle que nous l’avons créée ci-dessus.

Exercice 8.5
1. Nous extrayons de l’architecture la partie concernant l’interconnexion des deux réseaux (administratif et
commercial) :

Nous choisissons arbitrairement l’hôte 192.168.1.101.


Dans sa table de routage, nous ajoutons l’entrée correspondant au réseau commercial : pour joindre ce réseau
142.7.0.0, l’hôte 192.168.1.101 émet ses datagrammes à destination du routeur R_Import_3000_1 d’adresse
192.168.1.254 via son interface 192.168.1.101.
La ligne à ajouter est donc :
Adresse de destination Passerelle Interface Vecteur de distance

142.7.0.0 192.168.1.254 192.168.1.101 1

2. Dans la table de routage de notre hôte 192.168.1.101, nous devons ajouter l’entrée qui permet d’accéder à la
DMZ. La seule route possible est de passer ici aussi par le routeur de saut suivant R_Import_3000_1 d’adresse
192.168.1.254.
La ligne à ajouter est donc :
Adresse de destination Passerelle Interface Vecteur de distance

19.0.0.0 192.168.1.254 192.168.1.101 2

3. Si nous considérons par exemple l’hôte 142.7.1.1 du réseau commercial, nous devons ajouter dans sa table de
routage la route d’accès à la DMZ, en passant par le routeur R_Import_3000_1dmz d’adresse 142.7.1.254.
La ligne à ajouter est donc :
Adresse de destination Passerelle Interface Vecteur de distance

19.0.0.0 142.7.1.254 142.7.1.1 1

Exercice 8.6
Trois réseaux sont reliés à ce routeur : le réseau 192.169.1.0, le réseau 195.1.1.0. et le réseau 70.0.0.0.
Le destinataire 192.169.1.36 correspond à l’interface locale, le destinataire 127.0.0.0 est l’adresse locale de
rebouclage et la route par défaut définit la passerelle de sortie pour les datagrammes dont le destinataire n’est pas
listé.

Exercice 8.7
Pour proposer un diagramme lisible, nous devons choisir un formalisme. Sur la base du schéma du cours, nous
proposons le formalisme suivant :
• l’envoi d’un datagramme est symbolisé par une flèche rouge
• l’envoi d’un acquittement est symbolisé par une flèche verte
• la gestion de la fenêtre est symbolisée par des cases :
– case de fond rouge : datagramme émis
– case de fond vert : acquittement reçu et fenêtre décalée
– case de fond rouge avec numérotation en vert : acquittement reçu mais un acquittement antérieur est
manquant : la fenêtre ne peut pas être décalée.
La transmission du message constitué de 8 datagrammes, avec la perte du datagramme ④ et de l’acquittement ⑥
peut alors être schématisée ainsi :
Corrigé des exercices-bilan
Exercice-bilan 8.1
OSPF (Open Shortest Path First) est un protocole de routage dit « à état de liens ».
À intervalle régulier, chaque routeur envoie des messages hello à ses voisins pour connaître les liens qui lui sont
offerts, ce qui lui permet de créer une carte de routage autour de lui, et de calculer le meilleur chemin pour
atteindre un destinataire.
De même, à intervalle régulier, chaque routeur communique à tout le réseau des messages LSA (Link-State
Advertisements) qui indiquent les réseaux auxquels il est connecté.
La métrique utilisée par OSPF n’est pas basée sur le nombre de routeurs à traverser comme RIP, mais sur le coût
global de la route, calculé à partir d’un coût défini au niveau de chaque routeur pour chaque liaison (basé sur le
débit de la liaison par exemple).
Nous résumons dans le tableau suivant les principales caractéristiques de comparaison :
RIP OSPF

Fonctionnement Vecteur de distance État de liens

Métrique Nombre de sauts Coût global

Limite taille
15 sauts Illimité
du réseau

Messages émis Message RIP : toute la table Messages LSA : seulement si changements

• Peu consommateur de bande passante réseau


• Peu de stockage au niveau de chaque routeur
• Pas de taille limite de réseau
Avantages • Facile à configurer
• Envoi des LSA dès qu’une modification apparaît
• Pas de calcul au niveau du routeur
• Pas de boucle possible

• Mémoire importante nécessaire dans chaque routeur pour


• Nombreux messages RIP, même si pas de changement : utilisation importante stocker tous les liens
Inconvénients
de la bande passante • Nécessité d’un processeur puissant dans le routeur pour les
calculs de coût

Exercice-bilan 8.2
1. HTTPS assure la confidentialité des données par un chiffrement symétrique : le serveur et le client disposent tous
les deux d’une clé (dite publique) qui leur sert à chiffrer un message lors d’un envoi, ou déchiffrer un message
reçu.
Le schéma suivant représente les phases réalisées de l’émission d’une requête Web par un ordinateur client à sa
réception par le serveur Web.
Conseils
Prendre le temps d’analyser la situation : quel est le point de départ ? qu’est-ce qui termine la procédure ?
Découper en phases élémentaires le cheminement du message, puis organiser la représentation graphique de ce cheminement.

2. Une fois la requête traitée par le serveur, celui-ci peut envoyer la page Web au client :
3. Par définition, la clé publique utilisée par le client et le serveur pour cette transmission des pages est générée au
départ de l’échange par le serveur, puis transmise au client.
Une problématique apparaît alors : n’importe quel ordinateur du réseau peut lire la clé publique lors de cette
transmission de la clé par le serveur vers le client. Tous les ordinateurs posséderont alors la clé publique et
pourront déchiffrer les futurs échanges chiffrés entre le client et le serveur : la sécurisation de ces échanges n’est
pas assurée.
4. L’idée consiste à proposer un transfert sécurisé de cette clé publique de chiffrement symétrique du serveur vers
le client, pour que cette clé reste connue d’eux seuls.
Comment mettre en place ce transfert sécurisé ? Nous avons décrit dans le cours une autre méthode qui permet
de transmettre un message sécurisé : le chiffrement asymétrique.
La méthode du chiffrement asymétrique utilise deux clés :
• une clé publique connue des deux extrémités, servant à chiffrer un message avant son émission sur le réseau,
• une clé privée restant stockée sur l’émetteur, servant à déchiffrer un message chiffré reçu.
Nous pouvons utiliser cette méthode asymétrique pour transférer, de façon sécurisée, la clé symétrique qui
servira ensuite aux échanges requêtes/pages Web.

Remarque
Notons que le serveur et le client devront conserver la clé publique de manière sécurisée, pour éviter tout vol de cette clé et donc toute possibilité de
lecture de leurs échanges par un tiers.

5. Dans le schéma global suivant, nous appelons PRIVÉE 1 et PUBLIQUE 1 les clés utilisées pour le chiffrement
asymétrique initial, puis PUBLIQUE 2 la clé symétrique qui servira de base pour les échanges Web.
Conseils
Reprendre les phases élémentaires pour constituer le schéma global.
Veiller à une organisation graphique claire, structurée sur la page.

Erreurs à éviter
Bien noter que la clé publique est utilisée pour chiffrer et la clé privée pour déchiffrer, et non pas le contraire.
Partie 4
Langages et programmation
Chapitre 9
Calculabilité, décidabilité, modularité et récursivité
Cours

1 Introduction
En classe de première, ou dans des projets personnels, vous avez sans doute déjà utilisé le langage de
programmation C. Le langage est un langage compilé, cela veut dire qu’une fois le code source saisi, le
compilateur, qui est en fait un logiciel comme un autre, transforme ce code source en langage binaire
compréhensible par la machine. C’est une notion fondamentale qui est « cachée » derrière ce compilateur. Cela
nous montre en fait que certains programmes utilisent d’autres programmes comme données, et ce n’est pas
seulement réservé aux compilateurs !
Il existe bien évidemment d’autres exemples montrant cette notion fondamentale :
• Un système d’exploitation est lui aussi un programme qui fait « tourner » d’autres programmes.
• Beaucoup d’éditeurs utilisés dans la programmation pour un langage donné possèdent des « détecteurs
d’erreur » dans le code source. Il existe même des programmes qui prouvent mathématiquement qu’un autre
programme fait bien ce pour quoi il a été conçu.

Anecdote
Le 4 juin 1996, le vol inaugural du lanceur européen Ariane 5 (vol 501) s’est soldé par un échec causé par un dysfonctionnement informatique. La fusée
a explosé en vol seulement 36,7 secondes après son décollage. Depuis cette date, la France a développé de nombreux programmes permettant de
valider d’autres programmes.

Le fait qu’un programme arrive, dans un cas particulier, dans une boucle infinie et donc ne s’arrête jamais est un
vrai « cauchemar » pour tout développeur. En effet, dans ce cas, le programme (ou le logiciel) est incapable de
fournir le résultat attendu par l’utilisateur. Alors on rêve tous d’un programme capable de tester si un autre
programme se termine. Cependant, depuis les travaux des mathématiciens Alonso Church et Alan Turing, on sait
qu’un tel programme ne peut pas exister !

2 D’Alonso Church à Alan Turing


On peut intuitivement caractériser de nombreuses fonctions mathématiques par une suite de manipulations
définies à partir d’une combinaison de symboles. Ainsi, toute multiplication de nombres entiers peut être réduite à
des additions successives. De même toute division se ramène à des additions et des soustractions. Quant aux
additions et soustractions, elles se convertissent aisément en manipulations de symboles.

Le saviez-vous ?
Le mot calcul vient du latin « calculus » qui signifie « caillou ». On peut visualiser rapidement l’addition et la soustraction par la manipulation de ces
« fameux » cailloux.

En revanche, nous avons l’impression que d’autres fonctions définies formellement semblent
difficiles, voire impossibles à « caillouter ». Autrement dit, à décrire uniquement en termes de
déplacements de cailloux. Au début des années 1930, l’américain Alonso Church identifie une
classe de fonctions mathématiques qui semblait avoir les mêmes propriétés intuitives des
fonctions calculables. Ces travaux sont rassemblés dans « La thèse de Church ».
Parallèlement, le britannique Alan Turing cherche lui aussi à caractériser ces fonctions et
définit un ensemble de fonctions programmables sur des machines imaginaires qu’il avait Alonso Church
conçues. En 1937, il montre que l’ensemble des fonctions calculables au sens de Church était
équivalent à l’ensemble des fonctions programmables sur ses machines. En son hommage, ces
machines portent son nom.
La machine de Turing
Il s’agit bien d’une machine imaginaire inventée par Alan Turing en 1936 pour expliquer la
notion de « procédure mécanique » : l’équivalent d’un algorithme. Cette machine est la plus
élémentaire possible destinée à mettre en œuvre ces mécanismes de calcul, numériques ou Alan Turing
symboliques, comme le font notamment les ordinateurs. Ne perdons pas de vue que lorsqu’Alan Turing décrit sa
machine dans un article en 1936, les ordinateurs n’existent pas encore !
Cette machine comporte :
• Un ruban infini divisé en cases, dans lesquelles la machine peut écrire des symboles.
• Une tête de lecture et d’écriture
• Une table de transition : Chacune des lignes de cette table est associée à un état et spécifie les actions à
effectuer quand la machine est dans cet état, en fonction du symbole présent sous la tête de lecture. Ces actions
peuvent être l’écriture d’un symbole (ici un 0 ou un 1) et le déplacement du ruban d’une case à droite ou à
gauche. La ligne spécifie également le nouvel état après l’exécution des actions. La machine s’arrête quand un
état marqué comme final est atteint.
Pour que sa machine fonctionne comme une machine à calculer en binaire, Turing envisage le cas particulier où les
symboles utilisés sont 0 et 1.
L’entrée du programme est une liste de symboles binaires, écrits sur le ruban. Une fois le calcul effectué, c’est sur
ce même ruban que sera écrit le résultat du calcul, la sortie du programme. À chaque instant, le ruban mémorise
l’état du calcul. Voilà la forme la plus simple de mémoire mécanique !

Le saviez-vous ?
En 2012, pour célébrer le centenaire de la naissance d’Alan Turing, huit étudiants en master de l’École Normale Supérieure de Lyon ont fabriqué en
légos la première machine réelle (purement mécanique) du modèle de Turing. Pour en savoir plus, rendez-vous sur le site internet du projet :
http://rubens.ens-lyon.fr/fr/

La thèse de Church-Turing
On rassemble sous le nom de Thèse de Church-Turing le fait que tout traitement réalisable mécaniquement peut
être accompli par une machine de Turing. Tout programme d’ordinateur, peu importe le langage de
programmation, peut donc être traduit en une machine de Turing.
La thèse peut être reformulée en disant que les machines de Turing formalisent correctement la notion de
méthode effective de calcul. On considère généralement qu’une méthode effective doit satisfaire aux obligations
suivantes :
• L’algorithme consiste en un ensemble fini d’instructions simples et précises qui sont décrites avec un nombre
limité de symboles.
• L’algorithme doit toujours produire le résultat en un nombre fini d’étapes.
• L’algorithme peut en principe être suivi par un humain avec seulement du papier et un crayon.
• L’exécution de l’algorithme ne requiert pas d’intelligence de l’humain sauf celle qui est nécessaire pour
comprendre et exécuter les instructions.

3 Décidabilité
Une propriété mathématique est dite décidable s’il existe un algorithme, une procédure mécanique qui détermine
en un nombre fini d’étapes, si elle est Vraie ou Fausse. S’il n’existe pas de tels algorithmes, le problème est dit
indécidable.
Les propriétés qui s’expriment en langage mathématique, sont-elles toutes décidables ? Telle est la question posée
depuis la fin du xixe siècle par quelques mathématiciens, dont le très fameux David Hilbert en 1928, et identifiée
sous le nom de « problème de la décision ». Un des problèmes les plus connus, proposé en 1936 par Alan Turing
est le problème de l’arrêt.
Le problème de l’arrêt (Halting problem)
Le problème de l’arrêt est un problème classique en décidabilité. Il fait partie des problèmes qui n’ont pas de
solution : il est impossible d’écrire un algorithme qui résout ce problème. La plupart des autres problèmes
indécidables reviennent à ce problème.
Nous supposons l’existence d’un programme, appelé « Arrêt », ayant deux entrées : la première représente un
programme P et la seconde représente une donnée X du programme P (X est une donnée utilisée par le
programme P). Le programme « Arrêt » détermine sans jamais se tromper si l’exécution du programme P s’arrête
ou pas. Il donnera ainsi en sortie la valeur 0 si le programme P ne s’arrête pas et la valeur 1 si le programme P
s’arrête.

Nous pouvons alors construire un autre programme que l’on appelle Q constitué de 3 programmes : Le premier
programme est le programme « Duplication », le deuxième est le programme « Arrêt », et le troisième est le
programme « Négation ».

L’idée est d’exécuter le programme Q avec comme entrée le programme Q lui-même. Cela revient donc à la
situation suivante :

Il faut alors distinguer 2 cas :


• En sortie du programme « Arrêt », on a la sortie 1 si « Q s’arrête ». Ce qui nous amène en sortie du programme Q
une situation où l’on se trouve dans une boucle infinie, ce qui veut dire que « Q ne s’arrête pas ». Ce qui est
contradictoire.
• En sortie du programme « Arrêt », on a la sortie 0 si « Q ne s’arrête pas ». Ce qui nous amène en sortie du
programme Q le résultat 1, ce qui signifie que : « Q s’arrête ». Ce qui est là aussi contradictoire.
Conclusion : nous aboutissons dans les deux cas à une contradiction, ce qui montre l’impossibilité de l’existence du
programme « Arrêt ». Il n’existe donc pas de programme qui détermine d’une façon générale si un programme
donné se termine sur une entrée donnée.
Les notions de décidabilité et de calculabilité sont très étroitement liées, mais il est difficile, dans le cadre du
programme de NSI d’aller plus loin.

4 Récursivité
La récursivité est un concept qui est très proche de la notion mathématique de la récurrence. On dit qu’un sous-
programme (procédure ou fonction) est récursif s’il s’appelle lui-même.
Pour résoudre un problème ou effectuer un calcul, on se ramène à la résolution d’un problème similaire mais de
complexité moindre. On recommence ainsi jusqu’à obtenir un problème élémentaire que l’on sait résoudre. Pour
cela, la fonction en question va s’appeler elle-même avec un paramètre plus « petit ». Cet appel en induira un
autre, puis un autre, etc. D’appel en appel, la taille du paramètre va ainsi diminuer. On s’arrêtera quand cette taille
sera celle d’un problème immédiatement résolvable.
Les différents problèmes intermédiaires, ceux qui permettent de passer du problème initial au problème
élémentaire, sont stockés successivement en mémoire dans une pile. Dans notre cas, on utilisera ainsi en premier
le résultat du problème élémentaire, puis par dépilages successifs, on arrivera à celui du problème initial.
La fonction mathématique factorielle
En mathématiques, la fonction factorielle est une fonction définie sur qui à associe On montre
alors facilement la relation de récurrence Si l’on sait calculer on connaîtra donc la valeur de Or,
toujours d’après la formule de récurrence, On est donc ramené au calcul de Et ainsi de suite
jusqu’à dont on connaît la valeur : 1.
On peut donc proposer la fonction algorithmique suivante :
Fonction Factorielle(n) :
Si n==1 OU n==0 :
Retourner 1
Sinon
Retourner n*Factorielle(n-1)
Si on essaye de représenter l’exécution de cette fonction à l’aide de la pile sur l’exemple de Factorielle(4), on
obtient :

Important
Il est indispensable de prévoir une condition d’arrêt à la récursion sinon la fonction va s’appeler une infinité de fois. Dans la pratique, la pile qui stocke
les appels récursifs est de taille finie, une fois qu’elle est pleine le programme ne répondra plus.

Récursivité ou itérativité ?
Lorsque l’on programme des fonctions qui ne s’appellent pas, on dit que l’on programme de façon itérative. Il est
toujours possible de transformer une fonction récursive en fonction itérative et vice et versa. La méthode itérative
nous est plus familière et est plus rapide une fois le code implémenté dans un langage de programmation.
Reprenons l’exemple de la fonction factorielle, avec la méthode itérative, cela donne :
Fonction FactorielleIt(n) :
Res=1
POUR i allant de 2 à n :
Res = Res*i
Retourner Res
La méthode récursive est plus élégante et lisible et elle évite d’utiliser de nombreuses structures itératives. Elle est
également très utile pour concevoir des algorithmes sur des structures de données complexes comme les listes, les
arbres et les graphes. Le plus gros inconvénient de la récursivité est qu’une fois cette technique implémentée dans
un langage de programmation, elle est très « gourmande » en ressource mémoire. Du fait que l’on empile tous les
appels récursifs, des débordements de capacité peuvent se produire lorsque cette pile est pleine.
Récursivité croisée et récursivité multiple
Dans cette méthode récursive, il arrive qu’une fonction appelle une autre fonction qui appelle elle-même la
première, ce cas-là est appelé récursivité croisée. Prenons par exemple les deux fonctions ci-dessous permettant
de tester si un nombre est pair ou impair.
Fonction Pair(n) :
Si n==0 :
Retourner Vrai
Sinon :
Retourner Impair(n-1)
Fonction Impair(n) :
Si n==0 :
Retourner Faux
Sinon :
Retourner Pair(n-1)
Conseil
Ce n’est bien évidemment pas la méthode la plus simple, mais elle fonctionne ! On aurait pu par exemple tester le reste de la division euclidienne
de n par 2.

Il existe également un autre cas particulier où la fonction s’appelle plusieurs fois, on parle alors de récursivité
multiple. C’est le cas par exemple dans le cas du calcul des coefficients binomiaux. On peut donner un rappel
mathématique de ces coefficients binomiaux qui sont caractérisés par la définition suivante pour toute valeur
entière de et telles que :

Alors on peut donner la fonction algorithmique suivante :


Fonction CoeffBinomial(n, k) :
Si k==0 OU k == n :
Retourner 1
Sinon :
Retourner CoeffBinomial(n-1, k-1) + CoeffBinomial(n-1, k)

Remarque
Ces coefficients servent à la construction du célèbre triangle de Pascal où chaque élément de ce tableau est le coefficient binomial, comme le montre la
figure suivante jusqu’au rang 4.

n\k 0 1 2 3 4

0 1

1 1 1

2 1 2 1

3 1 3 3 1

4 1 4 6 4 1

5 Modularité
Lorsque l’on apprend à programmer, les codes sources sont relativement simples et on écrit ces derniers très
souvent de manière linéaire. Si l’on veut réutiliser du code existant, on utilise la méthode magique du « copier-
coller ». Sur des projets avec un nombre de lignes beaucoup plus conséquent, cela pose pas mal de problèmes,
notamment la difficulté de maintenance. Nous avons déjà essayé de poser les fondements de l’utilité des
fonctions, voire de la programmation à l’aide de classes qui sont des mécanismes de la modularité.
La modularité est un concept général qui peut se résumer selon 4 phases :
• Découper un ensemble en composants indépendants afin de rendre les gros projets réalisables.
• Donner de la structure à l’ensemble dans le but de rendre les gros projets compréhensibles.
• Spécifier les liens entre les différents composants pour rendre les gros projets maintenables.
• Identifier des sous-composants indépendants avec l’objectif de rendre des bouts de projets réutilisables.

Dans le domaine informatique


On pourra aisément remplacer le terme projet par programme.
On retrouve également ce concept dans le monde électronique : à partir des transistors, on forme des opérateurs
logiques, qu’on assemble en modules (additionneurs, multiplicateurs), que l’on assemble ensuite pour réaliser des
unités arithmétiques, etc. Cette composition hiérarchique est primordiale dans les circuits électroniques.
Quoi qu’il en soit, on est tous amené à :
• Construire des fonctions élémentaires.
• Combiner ces fonctions.
• Utiliser une structure pyramidale : on assemble des fonctions élémentaires pour constituer des fonctions plus
complexes.
Voici par exemple, un résumé sommaire de l’usage de la modularité pour un programme informatique selon le
nombre de lignes de code qui le constitue :
• 100 lignes : Cela représente un script codé dans un seul fichier faisant éventuellement appel à des modules
existants.
• 1 000 lignes : Quelques fichiers représentant une petite application. La modularité facilite l’écriture et la
maintenance.
• 10 000 lignes : Une application sérieuse. La modularité permet de mieux organiser le code et le développement
en parallèle de plusieurs parties et la réutilisation de plusieurs fonctions.
• 100 000 et plus : Cela représente un gros projet, probablement développé par plusieurs personnes devant
évoluer dans le temps avec une architecture robuste et probablement le développement de modules ou librairies
spécialisées.
Utilisation d’une bibliothèque
Dans Python, une bibliothèque est un ensemble de modules permettant d’ajouter des possibilités étendues dans
une thématique donnée. Les bibliothèques les plus communes sont installées avec l’IDE Python, cependant, dans
des cas particuliers, il faut les installer séparément. Parmi les plus communes, et vous en avez sans doute déjà
utilisé certaines, on peut citer :
• Tkinter : affichage graphique
• Math : calcul mathématique
• PIL : Traitement d’images
• Random : traitement de données aléatoires
• Time : gestion du temps
Nous allons prendre des exemples dans le domaine du traitement de l’image, et nous intéresser à la bibliothèque
PIL. La première chose à faire, est de se renseigner sur toutes les possibilités proposées par cette bibliothèque et
pour cela, il n’y a pas 36 solutions, il faut lire la documentation de cette dernière. Ce document est souvent en
anglais et possède un nombre de pages important, cependant c’est la seule solution pour pouvoir utiliser
correctement les fonctionnalités proposées.
Conseil
Il faut bien sûr, au préalable, vérifier, que la documentation que vous lisez corresponde à la version de votre bibliothèque.

Dans notre cas, il s’agit de la version 7.1.0 dont vous pouvez retrouver la documentation sur le site internet
suivant :
https://readthedocs.org/projects/pillow/downloads/pdf/latest/
L’idée ici n’est pas de faire une étude détaillée de cette bibliothèque mais d’en illustrer quelques exemples.
Imaginons que nous souhaitions traiter l’image suivante pour la transformer en 256 tonalités de gris. Il s’agit d’une
image bien connue dans le monde du traitement de l’image : Léna.
Cette image est présente sous la forme d’un fichier JPEG, on souhaite la transformer en niveaux de gris, avec la
même taille et l’enregistrer sous la forme d’un fichier PNG.
Essayons de décomposer nos contraintes pour chercher dans la documentation les informations souhaitées :
• Le format JPEG est-il accepté en lecture ? Quelle fonction est à utiliser ?
• Peut-on récupérer la taille d’une image ?
• Existe-t-il une fonctionnalité de conversion en niveaux de gris ? Si oui, quelle méthode utilise-t-elle ?
• Le format PNG est-il accepté en écriture ? Quelle fonction est à utiliser ?
Après lecture de la documentation, nous avons-nous réponses :
• Dans la rubrique « Image file formats », on nous dit que cette bibliothèque prend en considération de nombreux
formats d’images, y compris, le JPEG et le PNG, nous voilà déjà rassurés.
• Dans la rubrique « Reading and writing images », on nous informe que pour la lecture et l’écriture d’une image,
on doit utiliser le module Image. En particulier une fonction d’ouverture et une méthode d’enregistrement nous
sont utiles, prenant comme paramètre obligatoire le nom du fichier image, respectivement à ouvrir et à
enregistrer :
– open(nomfic1) qui retourne un objet de type Image.
– save(nomfic2) qui enregistre le fichier nomfic2.
Une fois l’instance de la classe Image créée, nous pouvons accéder aux attributs publics de cette classe dont
l’attribut qui nous intéresse size qui est un tuple constitué de 2 éléments : la largeur et la hauteur données en
pixels.
• Dans la rubrique « The Image class », on s’aperçoit qu’il existe une fonction de conversion en niveaux de gris qui
s’applique sur un objet de type Image existant et qui doit prendre comme paramètre ‘L’ :
• convert(L) qui retourne un objet de type Image.
• La méthode utilisée par défaut est la méthode CCIR 601 (utilisée dans le monde de la télévision), à savoir, qu’à
partir des composantes RGB d’un pixel, on obtient la tonalité en niveaux de gris d’un pixel selon la décomposition
suivant :

• Pour être rigoureux, dès que nous n’aurons plus besoin de ces différents objets (instances de la classe Image), il
faudra libérer la mémoire en utilisant la méthode close().
Il nous reste plus qu’à implémenter tout cela dans notre éditeur préféré, en n’oubliant pas de commenter notre
code :

Et voici Léna en 256 tonalités de gris dans le format PNG :

Utilisation d’une API


API signifie « Application Programming Interface ». En d’autres termes, c’est un moyen mis en place par une
application pour que d’autres applications puissent interagir simplement avec elle. Nous utilisons alors des
fonctions et des méthodes publiques. Tout comme les bibliothèques, il existe de très nombreuses API. Nous allons
nous intéresser plus particulièrement à l’API Openrouteservice. Il s’agit d’un service de routage basé sur les
données ouvertes OpenStreetMap.
Prenons une problématique : « Nous cherchons un appartement situé à Lyon ou dans sa proche banlieue avec le
critère primordial qu’il soit situé à moins de 30 minutes à pied (on pourra faire des isochrones sur une plage de 5
minutes) de mon lieu de travail situé 108, rue Saint-Georges dans le 5e arrondissement de Lyon.
Précision
Une isochrone est une ligne située à une distance ou un temps donné d’un point d’origine.

Dans un premier temps, on peut se familiariser avec les fonctionnalités disponibles sur le site internet proposant
l’API (le menu à gauche permet de saisir et d’exploiter différentes options) :
http://maps.openrouteservice.org/reach

Pour poursuivre, il faut créer un compte sur le site www.openrouteservice.org afin d’obtenir une clé permettant
d’utiliser l’API.
Maintenant il faut se familiariser directement avec les paramètres de l’API via l’interface (il faut repérer les
paramètres qui nous sont utiles dans le formulaire situé à droite, on s’aperçoit ainsi que c’est déjà beaucoup moins
intuitif !):
https://openrouteservice.org/dev/#/api-docs/v2/isochrones/{profile}/post

Précision
On notera qu’il faut non plus une adresse pour fixer notre point d’intérêt mais des coordonnées (longitude, latitude). On a plutôt l’habitude du couple
(latitude, longitude) ! On pourra observer, en bas du formulaire, après avoir cliqué sur le bouton CALL ACTION, le résultat escompté qui n’a pas tout à
fait le même aspect graphique.

Après avoir analysé tout cela, et notamment la rubrique « Show Example code », et sélectionné la catégorie
Python, on s’aperçoit qu’on peut utiliser cette API via l’envoi d’un formulaire HTML contenant les paramètres fixés
dans un dictionnaire qui représente une structure de données JSON (que l’on connaît bien). Il nous faudra donc
utiliser le module json. Une fois ce formulaire envoyé, on obtient en retour une réponse également sous la forme
de données JSON. Il faut donc être capable d’envoyer une requête HTML. On va utiliser le module requests. Si on
souhaite avoir un retour visuel avec en fond de carte, les données d’OpenStreetMap, il faut utiliser une
bibliothèque permettant de le faire. Nous prendrons la bibliothèque folium. Voilà ce que pourrait donner le
programme utilisant cette API :

Et voici le résultat obtenu lorsque nous ouvrons le fichier « map_appart.html » :


Précision
L’utilisation d’une API nécessite une lecture encore plus approfondie de la documentation technique qu’une bibliothèque. Les contraintes sont
imposées par le propriétaire de l’API, à nous de faire avec !

Création de module
Nous avons en fait déjà créé nos propres modules lorsque nous avons implémenté les structures de données
particulières (pile, file, arbre, graphe) en programmation orientée objet et que nous compléterons dans les
chapitres suivants. Il ne faut surtout pas négliger la saisie de commentaire pour bien documenter son code.
Conseil
Il faut documenter au fur et à mesure de la saisie du code.
Exercices

Compétences attendues
Savoir utiliser une machine de Turing.

Exercice 9.1
▶ Analyser et modéliser un problème
▶Développer des capacités d’abstraction et de généralisation
Considérons une machine de Turing se promenant sur un ruban constitué d’une suite de cases pouvant être
vierges ("V") ou contenir une information binaire ("0" ou "1"). Elle possède la faculté de se déplacer d’une case
vers la gauche ou vers la droite, de lire le caractère inscrit sur le ruban et d’écrire un caractère sur le ruban
(remplaçant le précédent). De plus, elle possède un certain nombre d’états qui vont déterminer son
comportement. Ces états sont précisés dans la table des transitions ci-dessous :
État Caractère lu Écrire Déplacement Nouvel état

V V droite E0

E0 1 0 droite E1

0 1 droite E1

V Arrêt

E1 1 0 droite E1

0 1 droite E1

Au commencement, la tête de lecture écriture est positionnée dans l’état E0 sur une des cases vierges située à
gauche du nombre binaire.
1. Faire fonctionner cette machine de Turing sur le ruban suivant :
… V V 0 1 0 0 1 1 V V …

2. Quelle est l’opération effectuée par cette machine de Turing ?

Exercice 9.2
▶ Analyser et modéliser un problème
▶Développer des capacités d’abstraction et de généralisation
Prenons maintenant une nouvelle machine de Turing avec 9 états et dont la table de transition est la suivante :
État Caractère lu Écrire Déplacement Nouvel état

V Arrêt

E0 0 V droite E1

1 V droite E5

V V droite E2
E1
0-1 inchangé droite E1

V 0 gauche E3
E2
0-1 inchangé droite E2

V V gauche E4
E3
0-1 inchangé gauche E3

V 0 droite E0
E4
0-1 inchangé gauche E4

V V droite E6
E5
0-1 inchangé droite E5

V 1 gauche E7
E6
0-1 inchangé droite E6

V V gauche E8
E7
0-1 inchangé gauche E7

V 1 droite E0
E8
0-1 inchangé gauche E8

Au départ, la tête de lecture écriture est positionnée dans l’état E0 sur le symbole binaire de gauche du nombre
inscrit sur le ruban qui, comme dans l’exercice précédent, contient une suite continue de symboles binaires, les
autres cases étant vierges.
1. Faire fonctionner cette machine de Turing sur le ruban suivant :
… V V 0 1 1 0 V V …

2. Quelle est l’opération effectuée par cette machine de Turing ?


3. Que faudrait-il changer dans l’algorithme pour que le nombre inscrit sur le ruban soit recopié avec une inversion
des symboles binaires (c’est-à-dire que le ruban V01101VVV. . . devienne V01101V10010VV. . .) ?
Compétences attendues
Savoir écrire un programme récursif.

Exercice 9.3
▶ Décomposer un problème en sous-problèmes
▶Concevoir des solutions algorithmiques
La méthode du paysan russe est un très vieil algorithme de multiplication de deux nombres entiers déjà décrit,
sous une forme légèrement différente, sur un papyrus égyptien rédigé autour de 1650 avant J.-C. Il s’agissait de la
principale méthode de calcul en Europe avant l’introduction des chiffres arabes.
Les premiers ordinateurs l’ont utilisé avant que la multiplication ne soit directement intégrée dans le processeur
sous forme de circuit électronique. Sous une forme moderne, il peut être décrit ainsi :
Fonction Multiplication(x, y) :
p=0
TANT QUE x>0 :
Si x est impair :
p=p+y
x = x //2
y=y+y
Retourner p
1. Appliquer cette fonction pour effectuer la multiplication de 105 par 253. Détailler les étapes en remplissant le
tableau suivant :
x y p

105 253 …
… … …

2. On admet que cet algorithme repose sur les relations suivantes :


=

Proposer alors la fonction équivalente selon la méthode récursive.


Compétences attendues
Savoir utiliser une bibliothèque.

Exercice 9.4
▶ Mobiliser les concepts
▶Concevoir des solutions algorithmiques
L’objectif de cet exercice est de réaliser la fractale de Von Koch à l’aide du module Python turtle. Il faudra donc lire
la documentation attentivement.
Une fractale est une sorte de courbe mathématique un peu complexe extrêmement riche en détail, et qui possède
une propriété intéressante visuellement : lorsque l’on regarde des détails de petite taille, on retrouve des formes
correspondant aux détails de plus grande taille (auto-similarité). Cela nous rappelle étrangement la récursivité ! La
première courbe à tracer a été imaginée en 1904 par le mathématicien suédois Niels Fabian Helge von Koch.
Le principe est simple : on divise un segment initial en trois morceaux, et on construit un triangle équilatéral sans
base au-dessus du morceau central. On réitère le processus fois, est appelé l’ordre. Dans la figure suivante on
voit les ordres 0, 1, 2 et 3 de cette fractale.

Et si l’on trace trois fois cette figure, on obtient successivement un triangle, une étoile, puis un flocon de plus en
plus complexe :

1. Proposer une fonction récursive Python permettant de dessiner la fractale de Von Koch en lui donnant comme
paramètres l’ordre et la longueur du segment initial.
2. Proposer une fonction permettant de faire le flocon complet à partir de la fonction réalisée dans la question
précédente.
Corrigé des exercices
Exercice 9.1
1. Après déroulement du programme, on obtient :

2. Après analyse, on s’aperçoit, qu’en entrée, on avait le mot « 010011 » et on obtient en sortie le mot « 101100 ».
On a donc réalisé l’opération d’inversion.

Précision
Cela représente en réalité l’opération logique NON.

Exercice 9.2
1. Après déroulement du programme, on obtient :

2. La fonction réalisée par cette machine est la duplication ou la recopie.


3. Pour réaliser l’inversion, il faut modifier la table des transitions avec :
État Caractère lu Écrire Déplacement Nouvel état

E2 V 1 Gauche E3

E6 V 0 Gauche E7

Exercice 9.3
1. Lorsque l’on applique la fonction Multiplication(105,253), on obtient les résultats intermédiaires suivants :
x y p

105 253 0

52 506 253

26 1 012 253

13 2 024 253

6 4 048 2 277

3 8 096 2 277

1 16 192 10 373

0 32 384 26 565

Le produit de 105 par 253 est donc : 26 565


2.
Fonction MultiplicationRec(x, y) :
Si x <= 0 :
Retourner 0
Si (x%2 == 0) :
Retourner MultiplicationRec(x // 2, y+y)
Sinon :
Retourner MultiplicationRec(x // 2, y+y) + y

Exercice 9.4
1. La fonction permettant le dessin de la fractale d’ordre n est :

2. Afin de dessiner le flocon entier, il faut bien réfléchir à la façon dont on dessine notre triangle à l’ordre 1, et
obtient par exemple :

Par exemple, on obtient pour l’ordre 4 le flocon de Von Koch suivant :


Chapitre 10
Paradigmes de programmation et validation
d’un programme
Cours

1 Introduction
Un paradigme est un point de vue particulier sur la réalité, un angle d’attaque privilégié sur une classe de
problèmes, un état d’esprit. Il est évident et clair qu’un paradigme, à lui seul, ne permet qu’une vision limitée de la
« réalité ». Il existe des milliers de langages de programmation pour commander des machines (ordinateurs,
téléphones, robots…). Ils sont souvent classés par catégories, correspondant à des principes de fonctionnement
différents : ces catégories sont appelées des paradigmes de programmation.
Chaque paradigme de programmation privilégie un ensemble particulier de stratégies d’analyse et de descriptions.
Chacun impose une approche, un point de vue particulier sur tout problème. Certains types de problèmes se
traitent plus facilement selon un certain paradigme. La thèse de Turing-Church formulée en 1936 met en évidence
l’équivalence de l’ensemble de ces paradigmes. Le choix du langage de programmation doit donc se faire avant
tout selon la nature du problème à appréhender.
Le but de cette partie n’est pas de détailler chaque famille mais d’en examiner les principales.

2 Les principaux paradigmes


Paradigme impératif
Les concepts et opérations supportées par ce paradigme sont ceux qui décrivent l’architecture et le
fonctionnement des ordinateurs traditionnels. Les langages correspondant à ce paradigme sont fondés sur une
exécution étape par étape, on parle d’exécution « séquentielle ». Le programmeur décrit l’ordre selon lequel sont
exécutées les instructions. Il a aussi la possibilité d’accéder directement à la mémoire contenant les données, ce
qui permet d’écrire des programmes dont l’exécution est très rapide. C’est le paradigme le plus courant et
historiquement le plus ancien, puisqu’il a été inventé conjointement à la création des premiers ordinateurs.

Exemple de langages de programmation


C, Pascal, Python, COBOL, Fortran…

Prenons l’exemple de la recherche du minimum d’une liste en Python :

Ce code est assez explicite pour nous car nous en avons l’habitude. En effet on commence par évaluer le nombre
d’éléments de notre liste (que l’on met dans la variable longueur) à l’aide de la fonction len(). Ensuite on teste si la
liste est vide : si c’est le cas, on affiche dans la console le message « La liste est vide ». Sinon, on affecte à la
variable mini le premier élément de la liste et on initialise le compteur i à 1. Ensuite, on teste pour tous les
éléments de la liste si cet élément est plus petit que mini. Si c’est le cas, alors la variable mini prend pour nouvelle
valeur cet élément. Une fois la boucle terminée, le programme affiche la valeur de la variable mini qui est bien le
minimum de la liste.
La console nous affiche bien évidemment le résultat :

Paradigme fonctionnel
Ce paradigme prend son origine dans le langage mathématique traitant des fonctions. Une fonction, dans son
incarnation informatique, accepte des données et produit des données. Une fonction peut être soit « primitive »
soit formée par une composition de fonctions. Il n’y a pas de séparation entre données et programmes : une
fonction est un objet de « première classe » sur lequel d’autres fonctions peuvent opérer. Ce paradigme ne
contient pas la notion de variable : un programme écrit dans un langage fonctionnel pur ne produit jamais d’effets
secondaires.
Ce paradigme propose un point de vue « transformationnel ». Tout comportement doit être perçu comme un
enchaînement de transformations sur un état initial et produisant un état final. Malgré sa généralité, l’approche
fonctionnelle n’est pas toujours la plus appropriée. Dans un contexte de bases de données, par exemple, le
concept organisateur serait relationnel plutôt que fonctionnel.

Exemple de langages de programmation


Lisp, LOGO, OCaml, Haskell, Python…

Prenons le même exemple de la recherche du minimum d’une liste avec ce paradigme dans le langage OCaml :

Outre le côté un peu déroutant du langage, on a défini une fonction récursive minimum qui prend comme
paramètre une liste lst. Selon le contenu de lst, il se dégage 3 situations :
• Aucun élément dans la liste. Dans ce cas, la fonction lève une exception pour signaler que la liste est vide.
• Un seul élément dans la liste, la fonction renvoie alors cet élément qui est bien le minimum.
• Au moins 2 éléments : dans ce cas, la liste est décomposée en un premier élément e et du reste de la liste r. Dans
ce cas, la fonction minimum est appelée récursivement sur la liste r afin d’évaluer le minimum de cette liste.
Cette valeur est alors notée min. Il suffit alors de comparer la valeur de e avec min pour trouver le minimum de la
liste et donc de renvoyer soit e soit min en fonction des cas.
La console nous affiche là aussi le résultat :

En Python, on peut également utiliser le paradigme fonctionnel :

Ici la fonction min est définie nativement en python mais nous aurions pu la recréer à partir du code donné dans le
paradigme impératif.
Paradigme logique
Ce paradigme est fondé sur la logique formelle définie au début du xxe siècle pour traiter des problèmes posés par
les fondements des mathématiques. Le langage emblématique de ce paradigme est Prolog. Le programmeur
définit un ensemble de faits élémentaires et de règles de logique leur associant des conséquences. Il permet
d’écrire des programmes très concis et élégants car proches de la description mathématique de l’algorithme à
l’origine du programme. L’usage de ces langages reste toutefois confidentiel.

Exemple de langages de programmation


Prolog, OZ, CLIPS, PyPy…

Reprenons notre recherche du minimum d’une liste avec ce paradigme dans le langage Prolog :

Là aussi, de prime abord, cela nous paraît déroutant (il y a en effet une syntaxe bien particulière). En fait on a défini
2 règles logiques qui portent le même nom :
• Dans le cas où la liste contient un seul élément X, le minimum est donc X.
• Si la liste contient au moins 2 éléments, on décompose cette liste en son premier élément E et le reste de cette
liste appelée Fin. Cette règle calcule, de manière récursive, le minimum de la fin de la liste qui est stocké dans
MinFin. Finalement, le minimum de la liste initiale, noté Mini est le minimum du premier élément E et de MinFin.
Pour exécuter cela sur notre liste, il faut au préalable compiler notre fichier correspondant à nos règles, puis on
saisit à la console et l’on obtient notre résultat :
Paradigme Orienté Objet
Programmer avec un langage orienté-objet amène le programmeur à identifier les « acteurs » qui composent un
problème, puis à déterminer ce qu’est et ce que doit savoir chaque acteur. En regroupant les aspects communs
puis en les spécialisant, le programmeur établit une hiérarchie de classes. Puis il cherche à décrire quels échanges
de messages entre les acteurs produiront les comportements voulus. Ce mode de programmation est un levier
intellectuel très puissant. Il offre, via l’objet, un excellent outil de modularité. En utilisant une hiérarchie de classe,
il peut décrire un comportement au niveau de généralité le plus approprié, avec l’économie de programme que
cela procure. Nous avons déjà vu cela dans le chapitre 4 sur l’initiation à la programmation orientée objet. Les
programmes orientés-objets sont éminemment réutilisables et modifiables. En fait les environnements de
programmation orientée-objet offrent un vaste choix de classes prédéfinies. Programmer dans de tels
environnements revient à trouver ce dont on a besoin ou à spécialiser ce qui s’en rapproche le plus.

Exemple de langages de programmation


SmallTalk, C++, Java, Python…

Nous pouvons en Python utiliser les classes pour répondre à notre exemple :

En effet, lors de la création de notre liste, on crée en fait un objet qui est une instance de la classe liste. Et cette
classe possède de nombreuses méthodes publiques dont la méthode sort() qui ordonne (de manière croissante) les
éléments de la liste. Le minimum de cette liste est donc forcément le premier élément.
Et les autres…
Il existe d’autres catégories de langages que les quatre paradigmes cités précédemment. Citons par exemple les
langages événementiels : par opposition à la programmation séquentielle (impérative), dans ce cas, l’exécution est
fondée sur la gestion d’événements (comme un clic de souris). La librairie swing en Java ou encore Node.js en
JavaScript sont des langages événementiels. Il existe aussi des langages spécialisés, adaptés à des utilisations
particulières : les langages pour pages Web (PHP, JavaScript), pour les bases de données (SQL) ou encore pour la
pédagogie (LOGO, Scratch) et même des langages de programmation très exotiques.
En conclusion
Ainsi, on peut constater que certains programmes sont plus concis que d’autres, et que les syntaxes diffèrent
radicalement entre les différents langages. Dans tous les cas, on note que la connaissance de chaque langage serait
cruciale pour comprendre en détail le fonctionnement de ces différents programmes, pourtant issus du même
algorithme.

3 La validation d’un programme


Pourquoi faire des tests de validation ?
Comme le dit le célèbre proverbe latin « Errare humanum est » qui signifie l’erreur est humaine. Et même après de
nombreuses années d’expérience en programmation, on continue de faire des erreurs. Il faut savoir que chaque
bug a un coût qui peut être a minima la somme des coûts suivants :
• Un coût de désagrément : causé à l’utilisateur du programme (perte de temps, perte de données).
• Un coût de développement : reprise du code, compréhension du bug, correction.
• Un coût « d’image de marque » : perte de crédibilité vis-à-vis de l’utilisateur final.
Alors on comprendra aisément qu’en détectant une erreur de programmation au stade du développement, on
réalise de nombreuses économies et on préserve son image.
Afin de vérifier la validité du code d’un programme, il existe principalement deux moments particuliers :
• À l’écriture : en commentant proprement son code (et en réalisant une documentation digne de ce nom sur des
projets de grande envergure).
• À l’exécution du code : ce moment est malheureusement trop souvent ignoré !
C’est ce deuxième point que nous allons essayer de développer. L’idée est de placer dans le code des points de
contrôle à tous les endroits critiques au moment où on tape le code. Par endroits critiques, on entend par
exemple :
• La gestion des listes ou des tableaux (débordement).
• Les instructions conditionnelles non exhaustives.
• Les paramètres d’entrée des fonctions.
• Les codes d’erreur retournés par d’autres fonctions issues d’autres modules ou librairies.
Si on pense à bien poser ces points de contrôle au moment où on implémente le code, on construit une sorte de
test permanent. Ces points de contrôle sont de deux sortes : les assertions et la gestion des erreurs.
Les assertions
Une assertion permet de vérifier une condition considérée comme vraie. Si cette condition est vraie, alors
l’assertion sera muette. Si elle est fausse, alors une erreur sera produite. Une assertion s’introduit via le mot-clé
assert et elle est suivie de la condition à vérifier et éventuellement d’un message d’erreur. Voici un exemple
d’utilisation d’assertion en Python :

La console nous indique l’erreur :

Le mécanisme d’assertion est là pour empêcher des erreurs qui ne devraient pas se produire, en arrêtant le
programme de manière prématurée. Si une telle erreur survient, c’est que le programme doit être modifié pour
qu’elle n’arrive plus. Dans cet exemple, on se rend immédiatement compte qu’un appel ne respectant pas les
préconditions a été fait, et qu’il faut donc le changer. Notre première réaction face aux assertions est de se dire
« ça ne sert à rien de tester cette condition, c’est évident qu’elle est satisfaite ». Mais justement, c’est parce
qu’elle est fondamentalement évidente qu’elle est une parfaite candidate à une assertion. Si un jour, l’assertion
signale le non-respect de la condition (évidente), vous le saurez tout de suite. En revanche si vous n’avez pas placé
d’assertion, vous n’imaginerez pas une seconde que votre condition puisse ne pas être satisfaite et vous irez donc
chercher l’erreur ailleurs pendant un certain temps !
Si vous utilisez les assertions mais ne gérez pas complètement le cas d’erreur, vous n’aurez fait que la moitié du
travail !

Remarque
Ce mode de programmation qui utilise les assertions est souvent appelé programmation défensive. En pratique, on va utiliser ce type de
programmation pour du code sur lequel on a le contrôle total. Par exemple, lorsqu’on écrit un module, on peut programmer défensivement pour les
fonctions qui ne sont destinées qu’à être appelées au sein de ce dernier.

La gestion active des erreurs


Une assertion « met le doigt » sur quelque chose qui ne devrait pas arriver : un problème de logique, une
incohérence de structure ou de programmation. A contrario, une erreur provient d’un problème dont l’issue était
prévisible, généralement dans les données. Lorsqu’on écrit des fonctions destinées à être utilisées par d’autres
personnes, on va plutôt faire une gestion active des erreurs, notamment avec l’instruction if - else et en prévoyant
des valeurs de retour spéciales. Reprenons l’exemple du calcul de la racine carré dans l’ensemble des réels vu plus
haut qui ne « gère » pas les erreurs de type. À l’aide d’une gestion active des erreurs, on peut proposer le code
suivant :
La console nous indique l’erreur :

Précision
Tout ce qui provient de l’intérieur du code peut être vérifié par des assertions (dans notre code Ligne 2). Tout ce qui provient de l’extérieur peut être
vérifié par une gestion active des erreurs (dans notre code, ligne 8). En effet, on utilise la fonction isinstance() externe à notre code.

Les exceptions
Lorsqu’une instruction ou une expression est syntaxiquement correcte, elle peut provoquer une erreur lorsqu’on
essaye de l’exécuter. Les erreurs détectées à l’exécution sont appelées exceptions et ne sont pas fatales.
Néanmoins, la plupart des exceptions ne sont pas gérées par un programme et entraînent des messages d’erreur
comme le montre l’exemple ci-dessous :

La console affiche les 2 premiers résultats et nous indique l’erreur :

La dernière ligne du message d’erreur indique ce qui s’est passé. Les exceptions peuvent être de plusieurs types, le
type est écrit dans le message. Dans notre exemple, le type est ZeroDivisionError (erreur de division par zéro). Le
type est suivi d’un petit message descriptif sur la cause de l’erreur. Les exceptions ne sont pas un simple
mécanisme de débogage. Elles servent d’abord et avant tout à gérer les cas exceptionnels, et on peut donc les
détecter, et réagir quand elles surviennent, à l’aide de l’instruction try/except en Python.

La console affiche alors correctement les inverses :

Précision
• Quand on « attrape » une exception, le programme ne « plante » pas. À la place de l’exception, c’est le bloc except (au nom de l’exception) qui est
appelé.
• Bien entendu, si une exception qui ne porte pas ce nom est levée, le programme plantera.

Une instruction try peut avoir plusieurs clauses d’exception, de façon à définir des gestionnaires d’exception
différents pour des exceptions différentes.

La console affiche les résultats sans plantage :

On peut également provoquer soi-même des exceptions avec l’instruction raise. Prenons par exemple le cas où
l’on souhaite traiter que des nombres positifs.
Remarque
Dans ce cas-ci lever une exception n’est pas forcément la meilleure solution, on aurait pu utiliser un assert. L’important est d’être capable de savoir,
grâce à l’interpréteur de commandes, quelles exceptions peuvent être levées par Python dans une situation donnée.

4 Les tests
Le programme ou le logiciel « zéro défaut » n’existe pas. La présence de fautes logicielles systématiques
introduites à la conception d’un dispositif programmé doit donc être considérée avec beaucoup d’attention, en
particulier lorsque les conséquences de ces fautes peuvent influer sur la sécurité d’un dispositif.
Construction des jeux de test
On peut considérer 3 niveaux de tests :
• Les tests unitaires : le but est de tester chaque « morceau » de code : les fonctions, les méthodes… Ils
permettent de s’assurer que chaque brique du programme fonctionne correctement et indépendamment des
autres briques. On s’attardera principalement sur ce niveau dans cette partie.
• Les tests d’intégration : le but est de commencer à tester de petites interactions entre les différentes parties du
programme. On peut réaliser ces tests avec les mêmes outils utilisés pour les tests unitaires. Cependant, on
supposera que toutes les briques prises individuellement sont valides.
• Les tests complets : le but est de tester le programme dans son ensemble. Si les 2 premiers niveaux sont négligés,
les tests complets ne servent à rien !
Le module unittest de Python
Le module unittest de la bibliothèque standard de Python inclut le mécanisme des tests unitaires. Pour chaque
fonctionnalité de notre code, on réalise un test qui vérifie que la fonctionnalité fait bien ce qu’on lui demande. Par
exemple, que si une certaine fonction est appelée avec certains paramètres, elle retourne telle valeur. Pour
l’instant, nous allons nous intéresser à la forme la plus simple du test unitaire. Par exemple, nous voulons tester
nos 2 fonctions suivantes EstPair(x) et EstImpair(x) :

Pour réaliser ces tests (afin de vérifier les résultats pour 0, 1 et 2), on utilise une classe qui possède des propriétés
spécifiques, on dit qu’elle hérite de la classe unittest.TestCase, on ajoute donc à notre code la suite :

Précision
Pour que les méthodes de cette classe soient bien considérées comme des tests, il faut que leur nom soit préfixé avec le mot-clé test. Les tests sont
exécutés par ordre alphabétique.

Si l’on exécute ce code, la console affiche :

Le retour affiché se décompose en trois parties :


• D’abord, la première ligne contient un caractère par test exécuté. Les principaux caractères sont : un point (".") si
le test s’est validé, la lettre F si le test n’a pas obtenu le bon résultat et la lettre E si le test a rencontré une erreur
(si une exception a été levée pendant l’exécution de la méthode).
• On trouve ensuite une ligne qui récapitule le nombre de tests exécutés.
• La dernière ligne résume le nombre de réussites ou échecs ou erreurs. Si tout va bien, cette dernière ligne devrait
être simplement "OK".
Si nous avions fait par exemple une erreur de frappe à la ligne 3 et mis le code suivant : return x%2 == 1, on aurait
obtenu le bilan suivant suite à l’exécution des tests :

Remarque
On parle d’échec (failure) et non pas d’erreur (error). Cela signifie que notre assertion ne s’est pas vérifiée (regardez le traceback) mais que notre test
s’est correctement exécuté.

Les méthodes d’assertion sont nombreuses, pour en avoir une liste complète, il faut consulter la documentation
officielle du module unittest, cependant on peut donner les plus courantes :
Méthodes Explications

Vérifier l’égalité entre a et b


assertEqual(a, b)
(a == b)

Vérifier que x est Vrai


assertTrue(x)
(x is True)

Vérifier que x est Faux


assertFalse(x)
(x is False)

Vérifier que x ne vaut pas « rien »


assertIsNotNone(x)
(x is not None)

Vérifier que a est présent dans b


assertIn(a, b)
(a in b)

Vérifier que a n’est pas présent dans b


assertNotIn(a, b)
(a not in b)

Vérifier que a est bien de type b


assertIsInstance(a, b)
(isinstance(a, b))

Vérifier que a n’est pas de type b


assertNotIsInstance(a, b)
(Not isinstance(a, b))

En résumé, ce module fournit un riche ensemble d’outils pour construire et lancer des tests. Les exemples que
nous avons vus montrent les fonctionnalités les plus utilisées pour couvrir les besoins courants en matière de tests.
Cependant on peut bien évidemment aller plus loin avec notamment la fonctionnalité de ce module en ligne de
commande afin de réaliser les tests sur tout un projet, on appelle cela la découverte automatique des tests. Pour
cela il faudra ici encore se baser sur la documentation officielle.
Exercices

Compétences attendues
Savoir distinguer les paradigmes impératif, fonctionnel et objet.

Exercice 10.1
▶ Traduire un algorithme dans un langage de programmation
▶Développer des capacités d’abstraction et de généralisation
En s’inspirant de l’exemple du cours, proposer un programme Python donnant le maximum d’une liste.
1. Dans le paradigme impératif.
2. Dans le paradigme objet.

Exercice 10.2
▶ Développer des capacités d’abstraction et de généralisation
▶Reconnaître des situations déjà analysées
Toujours en s’inspirant de l’exemple du cours.
Proposer un programme OCaml donnant le maximum d’une liste dans le paradigme fonctionnel.
Conseil
On pourra utiliser l’IDE OCaml-Top pour découvrir ce nouveau langage de programmation.

Compétences attendues
Savoir répondre aux causes typiques de bugs.

Exercice 10.3
▶ Développer des processus de mise au point et de validation
Prenons le cas de la recherche du minimum d’une liste avec la fonction Python suivante :

1. Quelles sont les 2 conditions implicites au bon fonctionnement de cette fonction ?


2. À l’aide des commentaires et des assertions, proposer un code plus explicite.

Exercice 10.4
▶ Développer des processus de mise au point et de validation

1. Que se passe-t-il après exécution de l’instruction suivante si le fichier « test.txt » n’existe pas ?
f = open("test.txt","r")
2. En utilisant les exceptions, proposez une fonction « propre » en Python permettant l’ouverture en lecture d’un
fichier texte donné en paramètre ne plantant pas sauvagement lorsque le fichier est introuvable. Cette fonction
que l’on appellera OuvertureFichierLecture(nomfic) retournera un tuple de 2 éléments :
• f : l’objet en mémoire désignant le dit fichier si l’ouverture a réussi et None sinon.
• erreur : une chaîne indiquant la description de l’erreur si erreur il y a et une chaîne vide si pas d’erreur.
Conseil
La lecture de la documentation officielle de Python concernant la fonction open est une base pour répondre à la problématique donnée.

Compétences attendues
Savoir construire des jeux de tests.

Exercice 10.5
▶ Développer des processus de mise au point et de validation
▶Mobiliser les concepts
Dans un projet de validation de la création d’un mot de passe qui doit respecter les règles suivantes : ce mot de
passe doit être constitué de 8 caractères au minimum et doit comprendre au minimum :
• 3 chiffres
• 1 majuscule
• 2 caractères spéciaux parmi les 5 suivants : #, @, |, ~, §
Nous avons déjà créé les fonctions unitaires suivantes :

1. Proposer à l’aide du module unittest un jeu de test permettant de tester et valider ces fonctions.
Conseil
On pourra par exemple tester :
• La fonction TypeParametre() sur les 4 types principaux de Python : int, float, str et bool.
• La fonction Presence3ChiffresMin() avec les chaînes suivantes : "1234", "123", "12a", "" et "a234"
• La fonction PresenceMaj() avec les chaînes suivantes : "a", "A" , "1" et "@"
• La fonction NbCaractSpeciaux() avec les chaînes suivantes : "12-", "#a" et "#@|~§"
• La fonction Presence8CaractMin() avec les chaînes suivantes : "12345678" et "#a"

2. Le jeu de test a dû faire apparaître 2 erreurs dans la fonction PresenceMaj. Proposer une correction de cette
fonction.
Corrigé des exercices
Exercice 10.1
1.

2.

Précision
On peut réaliser cela de deux manières, ici nous avons utilisé la méthode sort() qui ordonne les éléments de la liste dans l’ordre croissant, pour la
recherche du maximum, il suffit donc de prendre le dernier élément. On aurait pu également utilisé la méthode sort(reverse = True) qui ordonne les
éléments de manière décroissante et nous aurions donc pris le premier élément.

Exercice 10.2

Précision
On pourra reprendre les explications détaillées dans la partie cours sur la fonction récursive minimum. Cette fonction repose sur le même principe.

Exercice 10.3
1. Comme son nom l’indique, elle s’adresse à un paramètre de type liste constituée d'entiers ou de réels et il faut
que cette dernière possède au moins un élément puisqu’on affecte à la variable mini le premier élément de cette
liste. La liste ne doit donc pas être vide.
2.

Exercice 10.4
1. Cette instruction fait planter « sauvagement » le programme avec des messages déroutants en anglais (bien que
vous ne devriez pas trop être surpris), c’est un plantage de type FileNotFoundError.
2. Pour palier à cela, on peut écrire une fonction plus propre qui ne fera pas planter le programme. La voici :

Exercice 10.5
1. On peut proposer les tests suivants :

Ces tests font apparaître 2 erreurs prévisibles mais non gérées sur les caractères numériques et spéciaux dans la
fonction PresenceMaj().
2. On peut proposer cette solution :

Précision
On a simplement listé les majuscules autorisées et affiné en conséquence la condition de comptage à la ligne 20.
Partie 5
Algorithmique
Chapitre 11
Algorithmes sur les arbres binaires et sur les graphes
Cours

1 Algorithmes sur les arbres binaires


Nous avons déjà parlé des structures de données arborescentes. Ces dernières permettent de représenter
différents problèmes, notamment pour faciliter la recherche dans les informations qui forment naturellement une
hiérarchie. Reprenons simplement la définition d’un arbre binaire. Chaque nœud peut avoir 0,1 ou 2 enfants.
À chaque nœud d’un arbre binaire, on associe :
• Une clé (valeur associée au nœud)
• Un sous-arbre gauche (SAG)
• Un sous-arbre droit (SAD)
Considérons l’arbre binaire a suivant :

Si on considère le nœud racine du sous-arbre droit de clé A, on a :


• Sa clé : F
• Son SAG : l’arbre (feuille) de racine dont la clé est G
• Son SAD : l’arbre dont la racine est de clé H

Remarques
Un sous-arbre est un arbre même s’il n’est constitué que d’un seul nœud ou s’il ne contient aucun nœud (arbre vide). Un arbre vide est souvent noté
NIL.

On utilisera les classes suivantes :

Précisions
• Excepté la classe File ou l’attribut file est privé, tous les attributs sont publics.
• Pour la classe Arbre, l’attribut Racine correspond à un objet de type Nœud. Les attributs Sag et Sad sont des objets de type Arbre.
• La création d’un arbre crée simplement un nœud racine dont la clé est définie et dont les sous-arbres gauche et droit sont vides.

Par exemple pour construire l’arbre a donné précédemment, on peut donner les instructions suivantes :
a = Arbre(‘A’)
a.Sag = Arbre(‘B’)
a.Sag.Sag = Arbre(‘C’)
a.Sag.Sag.Sag = Arbre(‘D’)
a.Sag.Sag.Sad = Arbre(‘E’)
a.Sad = Arbre(‘F’)
a.Sad.Sag = Arbre(‘G’)
a.Sad.Sad = Arbre(‘H’)
a.Sad.Sad.Sad = Arbre(‘I’)
Nous avons déjà vu les formules de calcul de la taille et de la hauteur d’un arbre binaire dans la partie sur les
structures de données arborescentes.
Calcul de la taille d’un arbre binaire
La taille d’un arbre binaire B correspond au nombre de ses nœuds.
Elle est définie par :

On peut voir aisément que la taille se réalise de manière récursive. On peut donc donner l’algorithme suivant avec
a un objet de type Arbre :
Fonction Taille (a) :
Si a est vide :
Retourner 0
Sinon :
Retourner 1 + Taille(a.Sag) + Taille(a.Sad)
Dans le cas de notre arbre a, la taille est de 9.
Hauteur d’un arbre binaire
La hauteur d’un arbre correspond au nombre d’arêtes entre la racine et la feuille la plus éloignée (voir chapitre sur
les structures de données arborescentes). Autrement dit cela correspond au nombre de nœuds rencontrés lors de
ce parcours décrémenté de 1. Le nombre de nœuds se calcule de manière récursive. On peut donc donner
l’algorithme suivant avec a un objet de type Arbre :
Fonction NbNoeudsEntreRetF (a) :
Si a est vide :
Retourner 0
Sinon :
Retourner 1 + Maxi(a.Sag , a.Sad)

Remarque
La fonction Maxi(param1, param2) renvoie la plus grande valeur des 2 valeurs passées en paramètres. Par exemple Maxi (3,8) renvoie 8.

Le calcul de la hauteur de l’arbre a se ramène donc au pseudo-code suivant :


Fonction Hauteur(a) :
Retourner NbNoeudsEntreRetF(a) - 1
Dans le cas de notre arbre a, la hauteur est de 3.

2 Parcours des arbres binaires


Un parcours d’arbre est une façon d’ordonner les nœuds d’un arbre afin de les parcourir. On peut le voir comme
une fonction qui à un arbre associe une liste de ses nœuds. On distingue essentiellement deux types de parcours :
• le parcours en largeur d’abord (de la gauche vers la droite)
• les parcours en profondeur d’abord (du haut vers le bas). Parmi les parcours en profondeur, on distingue à
nouveau le parcours préfixe, le parcours infixe et le parcours suffixe.
Parcours en largeur d’abord
Le parcours en largeur consiste à parcourir l’arbre niveau par niveau. Les nœuds de niveau 0 sont d’abord
parcourus puis les nœuds de niveau 1 et ainsi de suite. Dans chaque niveau, les nœuds sont parcourus de la gauche
vers la droite. Le parcours en largeur de l’arbre ci-dessus parcours les nœuds dans l’ordre [A, B, F, C, G, H, D, E, I].
Le parcours en largeur se programme à l’aide d’une file (Fifo) que nous avons déjà vu. On peut proposer
l’algorithme suivant permettant de retourner une liste des clés des nœuds parcourus en largeur de l’arbre a :
Fonction ParcoursLargeur (a) :
Lst_resultat = ListeVide
Si a n’est pas vide :
F = File()
F.Enfiler(a)
TANT QUE F.EstVide() == False :
Arbre_courant = F.Defiler()
Ajouter Arbre_courant.Racine.Cle à Lst_resultat
Si Arbre_courant.Sag n’est pas vide
F.Enfiler(Arbre_courant.Sag)
Si Arbre_courant.Sad n’est pas vide
F.Enfiler(Arbre_courant.Sad)
RETOURNER Lst_resultat
Parcours en profondeur d’abord : Préfixe (Racine – SAG – SAD)
Dans le parcours préfixe, la racine est traitée avant les appels récursifs sur les sous-arbres gauche et droit (faits
dans cet ordre). Le parcours préfixe de l’arbre a ci-dessus donne les nœuds dans l’ordre [A, B, C, D, E, F, G, H, I]. On
peut donner l’algorithme suivant sur un objet a de type Arbre. Cet algorithme remplit au fur et à mesure du
parcours une liste lst (initialement vide) avec les clés des nœuds parcourus.
Fonction ParcoursPrefixe (lst, a) :
Si a est non vide :
Ajouter a.Racine.Cle à la fin de la liste lst
ParcoursPrefixe (lst, a.Sag)
ParcoursPrefixe (lst, a.Sad)
Parcours en profondeur d’abord : Infixe (SAG – Racine – SAD)
Dans le parcours infixe, le traitement de la racine est fait entre les appels sur les sous-arbres gauche et droit. Le
parcours infixe de l’arbre a donne les nœuds dans l’ordre [D, C, E, B, A, G, F, H, I]. On peut donner l’algorithme
suivant :
Fonction ParcoursInfixe (lst, a) :
Si a est non vide :
ParcoursInfixe (lst, a.Sag)
Ajouter a.Racine.Cle à la fin de la liste lst
ParcoursInfixe (lst, a.Sad)
Parcours en profondeur d’abord : Postfixe (SAG – SAD – Racine)
Dans le parcours en postfixe, la racine est traitée après les appels récursifs sur les sous-arbres gauche et droit (faits
dans cet ordre). Le parcours suffixe de l’arbre ci-dessus parcourt les nœuds dans l’ordre [D, E, C, B, G, I, H, F, A]. On
peut donner l’algorithme suivant :
Fonction ParcoursPostfixe (lst, a) :
Si a est non vide :
ParcoursPostfixe (lst, a.Sag)
ParcoursPostfixe (lst, a.Sad)
Ajouter a.Racine.Cle à la fin de la liste lst

3 Recherche d’une clé dans un arbre binaire de recherche


Nous avons déjà vu qu’un arbre binaire de recherche est un arbre dans lequel chaque clé d’un nœud est
supérieure ou égale à la clé de son fils gauche et strictement inférieur à la clé de son fils droit. Un arbre binaire de
recherche est intéressant puisqu’il est toujours possible de connaître dans quelle branche de l’arbre se trouve un
élément et de proche en proche le localiser dans l’arbre.
Considérons l’arbre binaire b de recherche suivant :
Pour rechercher une clé donnée dans L’arbre binaire de recherche, nous la comparons d’abord avec racine, si la clé
est présente à la racine, nous retournons Vrai. Si la clé est supérieure à la clé de la racine, on recommence pour le
sous-arbre droit du nœud racine. Sinon, par le sous-arbre gauche. Si la clé n’a pas été trouvée, on retourne Faux.
On peut proposer l’algorithme suivant portant sur un objet b de type Arbre :
Fonction RechercheCle (b, cle) :
Si b est vide :
Retourner Faux
Sinon :
Si b.Racine.Cle == cle :
Renvoyer Vrai
Sinon :
Si cle < b.Racine.Cle :
Renvoyer RechercheCle (b.Sag, cle)
Sinon :
Renvoyer RechercheCle (b.Sad, cle)
Appliquons la recherche dans l’arbre b :
RechercheCle(b, 13) renvoie Vrai tandis que RechercheCle(b, 16) renvoie Faux
Cet algorithme de recherche d’une clé dans un arbre binaire de recherche ressemble beaucoup à la recherche
dichotomique vue en première. C’est principalement pour cette raison qu’en général, la complexité en temps dans
le pire des cas de l’algorithme de recherche d’une clé dans un arbre binaire de recherche est : .

4 Insérer un nœud dans un arbre binaire de recherche


Un nouveau nœud de clé définie est toujours inséré dans une feuille. On commence à chercher une valeur de clé,
par comparaison, à partir de la racine jusqu’à ce qu’on atteigne une feuille. Une fois qu’une feuille est trouvée, le
nouveau nœud est ajouté en tant qu’enfant de la feuille. On propose l’algorithme suivant portant sur un objet b de
type Arbre.
Fonction InsererNoeud (b, n) :
Tant que b n’est pas vide :
Arbre_courant = b
Si n.Cle arbre_courant.Racine.Cle :
b = b.Sag
Sinon :
b = b.Sad
Si n.Cle arbre_courant.Racine.Cle:
Arbre_courant.Sag = Arbre(n.Cle)
Sinon :
Arbre_courant.Sad = Arbre(n.Cle)
Si nous appliquons InsererNoeud(b,Nœud(16)), on obtient l’arbre suivant :
5 Algorithmes de parcours sur les graphes
L’idée du « parcours » est de « visiter » tous les sommets d’un graphe en partant d’un sommet quelconque. Ces
algorithmes de parcours d’un graphe sont à la base de nombreux algorithmes très utilisés : routage des paquets de
données dans un réseau, découverte du chemin le plus court pour aller d’une ville à une autre…
Prenons comme exemple, le graphe g suivant :

Il existe 2 méthodes pour parcourir un graphe :


• Le parcours en largeur d’abord
• Le parcours en profondeur d’abord
Le parcours en largeur d’abord
Nous allons travailler sur un graphe constitué d’un ensemble de sommets et d’un ensemble des arêtes de ce
graphe. À chaque sommet de ce graphe nous associons une couleur : blanc ou noir.
• si la couleur est blanche alors le sommet n’a pas été parcouru
• si la couleur est noire alors le sommet a déjà été parcouru
On définira les classes suivantes :

Précisions
• Excepté la classe File ou l’attribut file est privé, tous les attributs sont publics.
• Pour la classe Graphe, l’attribut ListeS correspond à la liste des clés de tous ses sommets. Dans l’exemple du graphe g ci-dessus, on a donc :
g.ListeS = [‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’]
• L’attribut ListAdj correspond à un dictionnaire dont les clés sont les sommets du graphe et dont les valeurs correspondent à la liste des clés des
sommets voisins. Dans l’exemple du graphe g ci-dessus, on a donc :
g.ListeAdj = {‘A’: [‘C’, ‘D’, ‘E’, ‘F’], ‘B’: [‘C’], ‘C’: [‘A’, ‘B’], ‘D’: [‘A’, ‘E’], ‘E’: [‘A’, ‘D’], ‘F’: [‘A’]}

Dans un parcours en largeur, tous les sommets à une profondeur i doivent avoir été visités (couleur noire) avant
que le premier sommet à la profondeur ne soit visité (couleur blanche). Un tel parcours nécessite l’utilisation
d’une file d’attente pour se souvenir des branches qui restent à visiter. Voici l’algorithme qui retourne la liste des
clés des sommets parcourus en largeur :
Fonction ParcoursLargeur (g, cle_sommet_depart) :
Lst_resultat = ListeVide
F = File()
S = g.GetSommet(cle_sommet_depart)
F.Enfiler(S)
TANT QUE F.EstVide() == False :
S = F.Defiler()
Si S.Couleur == « Blanc » :
Ajouter S.Cle à Lst_resultat
S.Couleur = « Noir »
POUR cle_voisine DANS ListeAdj(S.Cle) :
S = g.GetSommet(cle_voisine)
Si S.Couleur == « Blanc » :
Ajouter S.Cle à Lst_resultat
F.Enfiler(S)
S.Couleur = « Noir »
RETOURNER Lst_resultat
Dans notre graphe g, ParcoursLargeur (g, ‘A’) donne : [‘A’, ‘C’, ‘D’, ‘E’, ‘F’, ‘B’]. Si nous prenons comme départ le
sommet de clé ‘D’, le parcours en largeur donne : [‘D’, ‘A’, ‘E’, ‘C’, ‘F’, ‘B’]
On trouve plusieurs applications :
• Dans les réseaux Peer-to-peer : le parcours en largeur est utilisé pour rechercher tous les nœuds voisins.
• Dans les sites de réseaux sociaux : on trouve des individus à une distance k d’une personne en utilisant le
parcours en largeur jusqu’au niveau k.
• Robots d’indexation de sites dans les moteurs de recherche : L’idée est de commencer à partir de la page source
et de suivre tous les liens à partir de la source et de continuer à faire la même chose.
Le parcours en profondeur d’abord
Le parcours en profondeur d’un graphe est similaire au parcours en profondeur d’un arbre. À la différence des
arbres, les graphes peuvent contenir des cycles, ce qui nous permet de revenir au même nœud. Pour éviter de
traiter un sommet plusieurs fois, nous utilisons la couleur du sommet (comme pour le parcours en largeur) :
• Blanc : le sommet n’a pas été parcouru
• Noir : le sommet a déjà été parcouru
Avant le parcours en profondeur, tous les sommets doivent être de couleur Blanche. On peut donner l’algorithme
récursif suivant portant sur un objet g de type graphe et qui remplit au fur et à mesure du parcours une liste lst
(initialement vide) avec les clés des nœuds parcourus.
Fonction ParcoursProfondeur (lst, g, cle_sommet_depart) :
S = g.GetSommet(cle_sommet_depart)
S.Couleur = « Noir »
Ajouter S .Cle à lst
POUR cle_voisine DANS ListeAdj(S.Cle) :
Som = g.GetSommet(cle_voisine)
Si Som.Couleur == « Blanc » :
ParcoursProfondeur (lst, g, Som.Cle)
Dans le cas de notre graphe g, on obtient la liste de parcours suivante au départ du sommet de clé A : [‘A’, ‘C’, ‘B’,
‘D’, ‘E’, ‘F’].

Déroulement
Dans le cas du parcours en profondeur, on va chercher à aller « le plus loin possible » dans le graphe : A -> C -> B, quand on tombe sur « un cul-de-sac »
(dans notre exemple, B est un « cul-de-sac », car une fois en B, on peut uniquement aller en C, or, C a déjà été noircit (parcouru…), on revient « en
arrière » (dans notre exemple, on repart de A pour aller explorer une autre branche : D -> E. Nouveau « cul-de-sac », on revient en arrière, on repart de
A et on finit par aller en F.

Tout comme le parcours en largeur, le parcours en profondeur admet plusieurs possibilités, cela dépend en fait de
l’ordre de la liste d’adjacence.
6 Autres algorithmes sur les graphes
Pour différentes raisons, il peut être intéressant de détecter la présence d’un ou plusieurs cycles dans un graphe
(par exemple pour savoir s’il est possible d’effectuer un parcours qui revient à son point de départ sans être obligé
de faire demi-tour).
On rappelle 2 notions vues dans le chapitre des structures de données relationnelles :
• Une chaîne est une suite d’arêtes consécutives dans un graphe. On utilise le terme de chaîne dans les graphes
non orientés et de chemin dans les graphes orientés.
• Un cycle est une chaîne qui commence et se termine au même sommet.
Repérage d’un cycle dans un graphe
Pour détecter la présence d’au moins un cycle dans un graphe, on utilise une pile. On utilisera donc en plus des
classes précédentes, la classe Pile.

On peut donner l’algorithme suivant portant sur un objet g de type graphe et qui renvoie Vrai si au moins un cycle
a été détecté et Faux sinon. Il faut bien évidemment que tous les sommets soient initialement « colorés » en Blanc.
On peut prendre comme sommet de départ n’importe lequel des sommets.
Fonction PresenceCycle (g, cle_sommet_depart) :
p = Pile()
s = g.GetSommet(cle_sommet_depart)
p.empiler(s)
TANT QUE p. EstVide() == False :
Som = p.depiler()
POUR cle_voisine DANS ListeAdj(Som.Cle) :
Sv = g.GetSommet(cle_voisine)
Si Sv.Couleur == « Blanc » :
p.empiler(Sv)
Si Som.Couleur == « Noir » :
Retourner Vrai
Sinon :
Som.Couleur = « Noir »
Retourner Faux
Dans le cas de notre graphe g, PresenceCycle (g, ‘A’) renvoie Vrai. En effet, on voit clairement qu’il y a un cycle, la
chaîne ‘A’ - ‘D’ - ‘E’ - ‘A’.
Recherche d’un chemin dans un graphe
Intéressons-nous maintenant à un algorithme permettant de trouver une chaîne entre 2 sommets (sommet de
départ et sommet d’arrivée). Les algorithmes de ce type ont une grande importance et sont très souvent utilisés,
notamment pour les choix de parcours (en plus courte distance, le plus rapide, le moins cher, etc.
Dans la plupart des cas, les algorithmes de recherche de chaîne (ou de chemin), travaillent sur des graphes
pondérés (par exemple pour rechercher la route entre un point de départ et un point d’arrivée dans un logiciel de
cartographie). Ces algorithmes recherchent aussi souvent les chemins les plus courts. Parmi les algorithmes les plus
connus, on peut citer l’algorithme de Dijkstra ou encore l’algorithme de Bellman-Ford.
Fonction RechercheChaine (g, cle_depart, cle_arrivee, lst_chaîne) :
Lst_chaîne = Lst_chaîne + [cle_depart]
Si cle_depart == cle_arrivee :
Retourner lst_chaîne
S = g.GetSommet(cle_depart)
POUR cle_voisine DANS ListeAdj(S.Cle) :
Sv = g.GetSommet(cle_voisine)
Si Sv.Cle n’est pas présente dans lst_chaîne :
Newchaine = RechercheChaine(g,Sv.Cle, cle_arrivee, lst_chaîne)
Si Newchaine n’est pas vide :
Retourner Newchaine
Retourner liste vide
Dans le cas de notre graphe g, print(RechercheChaine(g, ‘B’, ‘F’, [])) affiche [‘B’, ‘C’, ‘A’, ‘F’].
Exercices

Compétences attendues
Savoir parcourir un arbre binaire.

Exercice 11.1
▶ Mobiliser les concepts
À partir des classes Nœud, Arbre et File vues dans la partie Cours, on donne ci-dessous la représentation
informatique de l’arbre a. On rappelle que la création d’un arbre crée simplement un nœud racine dont la clé est
définie en paramètre et dont les sous-arbres gauche et droit sont vides.
a = Arbre(37)
a.Sag = Arbre(41)
a.Sag.Sag = Arbre(13)
a.Sag.Sag.Sad = Arbre(3)
a.Sag.Sag.Sad.Sag = Arbre(5)
a.Sag.Sag.Sad.Sad = Arbre(23)
a.Sad = Arbre(2)
a.Sad.Sag = Arbre(7)
a.Sad.Sad = Arbre(11)
a.Sad.Sad.Sag = Arbre(19)
1. Représenter graphiquement cet arbre a.
2. Donner le résultat du parcours de cet arbre en largeur d’abord.
3. Donner les résultats du parcours en préfixe, en infixe et en postfixe.
Compétences attendues
Savoir rechercher et insérer une clé dans un arbre binaire de recherche.

Exercice 11.2
▶ Mobiliser les concepts
Toujours à partir des mêmes classes, on donne ci-dessous la représentation informatique de l’arbre binaire de
recherche abr.
lst_cle = [3,10,1,6,14,4,7,4,13]
abr = Arbre(8)
for cle in lst_cle:
nd = Noeud(cle)
if not abr.RechercheCle(abr,cle):
abr.InsererNoeud(nd)
1. Représenter graphiquement cet arbre binaire de recherche abr.
2. Donner le résultat du parcours de cet arbre en infixe. Que remarque-t-on ?
Compétences attendues
Savoir parcourir un graphe.
Exercice 11.3
▶ Analyser et modéliser un problème
▶Mobiliser les concepts
À partir des classes Sommet, Graphe, File et Pile vues dans la partie Cours, on donne ci-dessous la représentation
informatique du graphe g.
g = Graphe()
lst_cle = [‘A’,’B’,’C’,’D’,’E’,’F’,’G’,’H’, ‘I’]
for c in lst_cle:
s = Sommet(c)
g.AjouterSommet(s)
g.AjouterArete(‘A’, ‘B’)
g.AjouterArete(‘A’, ‘F’)
g.AjouterArete(‘B’, ‘A’)
g.AjouterArete(‘B’, ‘C’)
g.AjouterArete(‘B’, ‘D’)
g.AjouterArete(‘B’, ‘G’)
g.AjouterArete(‘C’, ‘B’)
g.AjouterArete(‘C’, ‘E’)
g.AjouterArete(‘D’, ‘B’)
g.AjouterArete(‘D’, ‘I’)
g.AjouterArete(‘E’, ‘C’)
g.AjouterArete(‘E’, ‘I’)
g.AjouterArete(‘F’, ‘A’)
g.AjouterArete(‘F’, ‘G’)
g.AjouterArete(‘F’, ‘H’)
g.AjouterArete(‘G’, ‘B’)
g.AjouterArete(‘G’, ‘F’)
g.AjouterArete(‘G’, ‘I’)
g.AjouterArete(‘H’, ‘F’)
g.AjouterArete(‘H’, ‘I’)
g.AjouterArete(‘I’, ‘D’)
g.AjouterArete(‘I’, ‘E’)
g.AjouterArete(‘I’, ‘G’)
g.AjouterArete(‘I’, ‘H’)
1. Représenter graphiquement ce graphe.
2. Donner la liste d’adjacence correspondante g.ListAdj
3. Donner le résultat du parcours de ce graphe en largeur d’abord à partir du sommet de clé B.
4. Donner le résultat du parcours de ce graphe en profondeur d’abord à partir du sommet C
Compétences attendues
Savoir détecter un cycle dans graphe. Savoir rechercher un chemin dans un graphe.

Exercice 11.4
▶ Analyser et modéliser un problème
Reprenons le graphe défini dans l’exercice précédent.
1. Ce graphe possède-t-il au moins un cycle ? Si on applique l’algorithme vu en cours au départ du sommet de clé A,
quel est le sommet qui en permet l’arrêt ?
2. Donner une chaîne de « trajet » entre les sommets de clé E et H en appliquant l’algorithme de recherche vu dans
la partie cours. Est-ce la plus courte ?
Exercices-bilan
Exercice-bilan 11.1
60 min ● ... points
En utilisant l’implémentation des classes vues dans la partie structures de données. Proposer une implémentation
en Python des algorithmes (vus en cours) relatifs aux arbres sous forme de méthodes de la classe Arbre. On pourra
ainsi vérifier les résultats des exercices 11.1 et 11.2
On pourra utiliser le début d’implémentation suivant :

Exercice-bilan 11.2
60 min ● ... points
En utilisant l’implémentation des classes vues dans la partie structures de données. Proposer une implémentation
en Python des algorithmes (vus en cours) relatifs aux graphes sous forme de méthodes de la classe Graphe. On
pourra l’utiliser pour vérifier les résultats des exercices 11.3 et 11.4
On pourra utiliser le début d’implémentation suivant :
Corrigé des exercices
Exercice 11.1
1. La représentation graphique de l’arbre a est la suivante :

2. Le parcours de cet arbre en largeur d’abord donne : [37, 41, 2, 13, 7, 11, 3, 19, 5, 23]
3. Le parcours de cet arbre :
• En préfixe donne : [37, 41, 13, 3, 5, 23, 2, 7, 11, 19]
• En infixe donne : [13, 5, 3, 23, 41, 37, 7, 2, 19, 11]
• En postfixe donne : [5, 23, 3, 13, 41, 7, 19, 11, 2, 37]

Exercice 11.2
1. La représentation graphique de l’arbre binaire de recherche abr est la suivante :

Remarque
Vu que l’on teste la présence d’une clé dans l’arbre, le nœud de clé 4 ne peut être ajouté une deuxième fois.

2. Le parcours de cet arbre en infixe donne : [1, 3, 4, 6, 7, 8, 10, 13, 14]. On s’aperçoit que les clés sont classées par
ordre croissant.

Exercice 11.3
1. La représentation graphique du graphe g est la suivante :

2. La liste d’adjacence correspond au dictionnaire suivant :


{‘A’: [‘B’, ‘F’], ‘B’: [‘A’, ‘C’, ‘D’, ‘G’], ‘C’: [‘B’, ‘E’], ‘D’: [‘B’, ‘I’], ‘E’: [‘C’, ‘I’], ‘F’: [‘A’, ‘G’, ‘H’], ‘G’: [‘B’, ‘F’, ‘I’], ‘H’: [‘F’,
‘I’], ‘I’: [‘D’, ‘E’, ‘G’, ‘H’]}
3. Le parcours en largeur d’abord depuis le sommet B donne :
[‘B’, ‘A’, ‘C’, ‘D’, ‘G’, ‘F’, ‘E’, ‘I’, ‘H’]
4. Le parcours en profondeur d’abord depuis le sommet C donne :
[‘C’, ‘B’, ‘A’, ‘F’, ‘G’, ‘I’, ‘D’, ‘E’, ‘H’]

Exercice 11.4
1. On voit très clairement que ce graphe possède plusieurs cycles. Si l’on applique l’algorithme de recherche de
cycle à partir du sommet de clé A, il renvoie Vrai au moment où il dépile le sommet de clé E déjà noirci.
2. Étant donné qu’il parcourt en profondeur à partir du sommet de clé E et dans l’ordre de la liste d’adjacence,
l’algorithme trouve le chemin suivant [‘E’, ‘C’, ‘B’, ‘A’, ‘F’, ‘G’, ‘I’, ‘H’]. On s’aperçoit que ce n’est évidemment pas
le plus court qui est [‘E’, ‘I’, ‘H’].
Corrigé des exercices-bilan
Exercice-bilan 11.1
Calcul de la taille et de la hauteur d’un arbre

Parcours d’un arbre en largeur d’abord

Parcours d’un arbre en profondeur (préfixe, infixe et postfixe)

Recherche d’une clé dans un arbre binaire de recherche

Insertion d’un nœud dans un arbre binaire de recherche

Vérification de l’exercice 11.1

La console affiche :

Vérification de l’exercice 11.2


La console affiche :

Exercice-bilan 11.2
Parcours d’un graphe en largeur d’abord

Parcours d’un graphe en profondeur d’abord

Détection d’un cycle dans un graphe à partir d’un sommet de clé définie

Recherche d’une chaîne dans un graphe

Vérification de l’exercice 11.3

La console affiche :

Vérification de l’exercice 11.4

La console affiche :
Chapitre 12
Méthodes d’optimisation et recherche textuelle
Cours

1 Méthode « Diviser pour régner »


Nous avons déjà parlé de la récursivité lors du chapitre 9 et nous pouvons remarquer que la plupart de ces
algorithmes étaient basés sur la simple exploitation d’une formule de récurrence. Mais beaucoup d’autres
problèmes, plus complexes, se résolvent aussi naturellement de façon récursive, avec des algorithmes s’appelant
eux-mêmes une ou plusieurs fois sur des données de tailles inférieures. Et ce jusqu’à obtenir des problèmes de
taille élémentaire, résolvables immédiatement. Il faudra enfin combiner les résultats issus de chacun de ces
appels.
Cette méthode dite « diviser pour régner » se décompose ainsi en trois phases :
• Diviser : on divise les données initiales en plusieurs sous-parties.
• Régner : on résout récursivement chacun des sous-problèmes associés (ou on les résout directement si leur taille
est assez petite).
• Combiner : on combine les différents résultats obtenus pour obtenir une solution au problème initial.
Cela va prendre tout son sens sur les exemples à venir, qui vont nous permettre de nous familiariser avec ce
principe général. Pour chacun d’eux on présentera en détail les trois phases : diviser, régner et combiner.
Calcul du minimum d’une liste
En adoptant la méthode « diviser pour régner », le but est de « découper » la liste en 2 sous-listes (de taille 2 fois
plus petite si le nombre d’éléments est pair) et de calculer récursivement le minimum de la première sous-liste et
celui de la seconde, puis de les comparer. Le plus petit des deux sera le minimum de toute la liste. La condition
d’arrêt à la récursivité sera l’obtention d’une liste à un seul élément, son minimum étant bien sûr la valeur de cet
élément.
Voici donc les trois étapes de la résolution de ce problème via la méthode « diviser pour régner » :
• Diviser la liste en deux sous-listes en la « coupant » en 2.
• Calculer récursivement le minimum de chacune de ces sous-listes. Arrêter la récursion lorsque les listes n’ont
plus qu’un seul élément.
• Retourner le plus petit des deux minimums précédents.
Prenons comme exemple la liste suivante : L = [23, 12, 4, 56, 35, 57, 3, 11, 6]. On peut représenter les étapes de la
méthode « diviser pour régner » par l’arbre suivant :

On peut donner la fonction suivante qui calcule par la méthode « diviser pour régner » le minimum d’une liste L.
On lui donne également en paramètres les indices du premier et du dernier élément.
Fonction Minimum(L, d, f) :
Si d == f :
Retourner L[d]
Sinon :
m = (d+f) // 2
x = Minimum(L, d, m)
y = Minimum(L,m+1, f)
Si x < y
Retourner x
Sinon
Retourner y
Tri par fusion
Nous avons déjà vu en classe de première deux méthodes de tri qui ont un coût quadratique :
• Le tri par insertion
• Le tri par sélection
Nous allons maintenant voir un autre tri, le tri par fusion. Il est surprenant par deux aspects, qui sont très liés :
• Il n’est pas du tout naturel au départ.
• Il est beaucoup plus efficace (rapide) que les 2 méthodes de tri citées précédemment.
L’idée du tri par fusion repose sur la méthode « diviser pour régner » : on découpe le problème (une liste en 2
sous-listes), on traite chaque sous-problème séparément, puis on fusionne (rassemble) les résultats de manière
intelligente.

L’idée qui permet d’avoir une fusion efficace (en bleu sur le schéma ci-dessus) repose sur le fait que les deux listes
sont triées. Il suffit en fait de les parcourir dans l’ordre : on sait que les plus petits éléments des deux listes sont au
début, et le plus petit élément de la liste globale est forcément soit le plus petit élément de la première liste, soit le
plus petit élément de la deuxième (c’est le plus petit des deux). Une fois qu’on l’a déterminé, on le retire de la
demi-liste dans laquelle il se trouve, et on recommence à regarder les éléments (restants) du début. Une fois qu’on
a « vidé » les deux demi-listes, on a bien effectué la fusion. Le schéma ci-dessous reprend la technique pour l’étape
finale de fusion.

Pour réaliser la fusion de 2 listes L1 et L2, on peut proposer la fonction suivante :


Fonction Fusion(L1, L2) :
Si L1 est vide :
Retourner L2
Si L2 est vide :
Retourner L1
Si L1[0] < L2[0] :
Retourner [L1[0] + Fusion(L1[1 :], L2)]
Sinon :
Retourner [L2[0] + Fusion(L1, L2[1:])]
La fonction permettant le tri par fusion d’une liste L est donnée par le pseudo-code suivant :
Fonction TriFusion(L) :
nb = le nombre d’éléments de L
Si nb <=1 :
Retourner L
L1 = L[x] pour tout

L2 = L[x] pour tout

Retourner Fusion(TriFusion(L1), TriFusion(L2))

Précisions
On a utilisé une copie (partielle) pour « scinder » les deux demi-listes en dehors de la liste L avant de les trier et de les fusionner. La procédure fusion,
elle aussi, crée une nouvelle liste, qu’elle renvoie. On a donc alloué de la mémoire supplémentaire, ce n’est pas un tri en place. Il est possible de faire
mieux : on peut, en manipulant des indices au lieu des listes complètes, trier les demi-listes dans la liste initiale, ce qui la modifie mais permet de ne pas
allouer de mémoire supplémentaire.

Complexité du tri par fusion


L’étude de la complexité du tri par fusion est assez simple. On commence avec une liste (ou un tableau) de n
élément (pour des questions de simplicité, on prendra une valeur de n puissance de 2). On le découpe, ce qui fait
deux tableaux de éléments. On les découpe, ce qui fait 4 tableaux de éléments. On les découpe, ce qui fait
8 tableaux…
La phase de découpage s’arrête lorsque nous arrivons à des tableaux de taille 1. Et combien de fois faut-il diviser N
par 2 pour obtenir 1 ? C’est la fonction logarithme base 2. En effet, si on a un tableau de taille 1, on renvoie le
tableau en une seule opération , et si on double la taille du tableau il faut faire une découpe de plus
. On a donc phases de « découpe » successives.

Rappel mathématique

Quel est le travail effectué à chaque étape ? C’est le travail de fusion : après le tri, il faut fusionner les demi-listes.
Notre algorithme de fusion est linéaire : on parcourt les deux demi-listes une seule fois, donc la fusion de deux
tableaux de taille est en O(N).
On a donc étapes à opérations chacune. Au total, cela nous fait donc opérations. La complexité du
tri fusion est donc en .
On est passé, en changeant d’algorithme, d’une complexité de à une complexité de C’est bien gentil,
mais est-ce si génial que ça ?
La réponse est évidemment oui, surtout pour des grandes valeurs de n. On peut s’en convaincre rapidement à
l’aide du comparatif suivant donnant les temps d’exécution sur une même machine pour un tri par sélection et
pour un tri par fusion codé dans le même langage de programmation :
Nombre d’éléments dans la liste (n) Tri par sélection Tri par fusion

100 0.006s 0.006s

1 000 0.069s 0.010s

10 000 2.162s 0.165s

20 000 7.526s 0.326s

40 000 28.682s 0.541s


Précisions
• Avec une complexité en , si l’on double la taille de la liste d’entrée, on va traiter 4 fois plus lentement (2²). Avec une complexité en , on va
traiter 2 fois plus lentement ( ).
• En extrapolant ces données, si nous évaluions le tri par fusion sur 10 240 000 éléments, on obtiendrait un temps de traitement de 2 minutes 30
secondes tandis que pour le tri en sélection on obtiendrait un temps d’exécution de presque 22 jours !

2 Programmation dynamique
Nous venons de voir que la méthode « diviser pour régner » divise un problème en sous-problèmes indépendants
(qui ne se chevauchent pas), résout chaque sous-problème, et combine les solutions des sous-problèmes pour
former une solution au problème initial. Il y a cependant des cas où les sous-problèmes ne sont pas indépendants !
On peut alors retrouver un même sous-problème dans des appels récursifs différents, et donc être amené à le
résoudre plusieurs fois, ce qui ressemble fort à du gaspillage de « ressources ».
Prenons en exemple l’étude de la suite de Fibonacci qui est définie par :

La fonction algorithmique récursive permettant de calculer le terme d’indice n de cette suite qui nous vient
rapidement à l’idée est la suivante :
Fonction Fibo(n) :
Si n == 0 ou n == 1 :
Retourner n
Sinon :
Retourner Fibo(n-1) + Fibo(n-2)
Étudions le cas pour et essayons de représenter les appels successifs de cette fonction sous la forme d’une
arbre.

On peut s’apercevoir que plusieurs appels à cette fonction sont réalisés avec la même valeur de paramètre :
• 3 appels avec le paramètre 2
• 2 appels avec le paramètre 3
Méthode de Bellman (1950)
Pour éviter ces appels récursifs redondants et coûteux, l’américain Richard Bellman a eu une idée relativement
simple : on va mémoriser les résultats des sous-problèmes afin de ne pas les recalculer plusieurs fois. Cette
technique n’est valable que si les sous-problèmes sont dépendants, c’est-à-dire que ces sous-problèmes ont des
sous-sous-problèmes communs. La mémorisation des solutions des sous-problèmes est une sorte de mémoire
cache implémentée sous forme de tableau ou de liste. Cette technique porte le nom de programmation
dynamique.
Dans la pratique, la programmation dynamique peut prendre 2 formes :
• Une forme récursive appelée « Top Down » (du haut vers le bas) :
– On appelle directement la formule de récurrence.
– Lors d’un appel récursif, avant d’effectuer le calcul, on vérifie dans la liste de la mémoire cache si ce calcul n’a
pas déjà été fait.

Remarque
Cette forme est aussi appelée technique de mémoïsation.
• Une forme itérative appelée « Bottom Up » (du bas vers le haut) :
– On résout en premier les problèmes du plus petit niveau, puis ceux du niveau supérieur et au fur et à mesure,
on mémorise ces résultats dans la liste de mémoire cache.
– On continue jusqu’au niveau qui nous intéresse.
Proposons alors une la première forme (Top Down) de cette programmation dynamique pour le calcul du terme de
rang n pour la suite de Fibonnacci. Il nous faut donc mémoriser les termes d’indice 0, 1, …, n. Nous allons donc
stocker ces résultats intermédiaires dans une liste de (n+1) éléments initialisés à 0.
Fonction FiboTD(n) :
Stock = Liste de (n+1) éléments initialisés à 0
Retourner FiboTDRec (n, Stock)

Précision
C’est bien cette fonction qui déclare notre liste de mémoire cache et qui réalise le premier appel à la fonction récursive qui effectue le calcul.

Fonction FiboTDRec(n, Stock) :


Si n == 0 ou n == 1 :
Stock[n]=n
Retourner n
Sinon :
Si Stock[n] > 0 :
Retourner Stock[n]
Sinon :
Stock[n] = FiboTDRec (n-1, Stock) + FiboTDRec (n-2, Stock)
Retourner Stock[n]

Précision
On a simplement ajouté, par rapport à la premiere version récursive pure, la fonctionnalité majeure de l’efficacité afin d’utiliser les résultats déjà
calculés.

On peut maintenant proposer la deuxième forme Bottom Up de la programmation dynamique dans le cas du calcul
du terme de rang n pour la suite de Fibonacci.
Fonction FiboBU(n) :
Stock = Liste de (n+1) éléments initialisés à 0
Stock[1] = 1
P0UR les indices i allant de 2 à n :
Stock[i] = Stock[i-1] + Stock[i-2]
Retourner Stock[n]

Précision
Ici aussi, on utilise la formule de récurrence qui définit notre suite, mais sous forme itérative.

Problème du rendu de monnaie


Ce problème doit vous être connu puisque vous l’avez déjà rencontré en classe de première sur la notion des
algorithmes gloutons. Reprenons la question de ce problème : quel est le nombre minimal de pièces à utiliser pour
rendre une somme donnée ?
On part d’une liste de pièces ou billets possible dans un système monétaire connu :

Précision
Par exemple, dans notre système monétaire,
.
Pour résoudre ce problème de façon récursive, sans parler encore de programmation dynamique, il faut
commencer par établir une formule de récurrence. Pour une somme à rendre X, on va noter Nb[X] ce nombre
minimal de pièces. À partir de X, on peut choisir de rendre d’abord ou ou ou … . Les sommes inférieures à X
obtenables à partir de X sont donc . On peut alors présenter cette formule de récurrence :

On peut donc proposer la fonction suivante permettant de rendre la somme X avec les pièces ou billets présents
dans la liste L :
Fonction RM(L, X) :
Si X == 0 :
Retourner 0
Sinon :
Mini = X+1
POUR tous les indices de la liste L :
Si L[i] <= X :
Nb = 1 + RM(L, X-L[i])
Si Nb < Mini :
Mini = Nb
Retourner Mini

Précision
La technique pour le calcul du minimum est des plus classiques. On initialise d’abord une variable arbitrairement « trop grande » destinée à contenir in
fine ce minimum : dans notre cas (X+1). On parcourt ensuite un à un les éléments de notre liste sur lequel on effectue la minimisation en mettant à jour
si nécessaire. Nous avons logiquement restreint l’ensemble des pièces à celles plus petites que la somme à rendre.

Cet algorithme fait bien partie de la méthode « diviser pour régner » puisque nous avons divisé le problème initial
puis appliqué un traitement récursif. Nous sommes de plus dans une situation où les sous-problèmes ne sont pas
indépendants. Pour s’en assurer, on peut représenter les appels à cette fonction récursive sous la forme d’un
arbre. On a pris X = 5. Les valeurs inscrites sur les liaisons correspondent aux valeurs des pièces ou billets inclues
dans la liste .

Remarques
• Tous les cas sont traités. Les algorithmes qui traitent tous les cas sont appelés « brute force ».
• La profondeur minimale de la feuille 0 est de 1, il y a une seule possibilité avec le rendu d’une pièce de 5.
• Dans certains cas, il arrive qu’une feuille ne soit pas nulle, la valeur retournée par la fonction est dans notre cas (X+1) et cette solution ne sera pas
retenue.
• On remarque également de nombreux appels redondants, ce qui nous oriente vers une résolution en approche dynamique.

Pour proposer une résolution par la méthode dynamique, il nous faut mémoriser les résultats pour les sommes 0,
1, …, X. Nous allons donc stocker ces résultats intermédiaires dans une liste de (X+1) éléments initialisés à 0.
Fonction RM_TD(L, X) :
Stock = Liste de (X+1) éléments initialisés à 0
Retourner RM_TDRec (L, X, Stock)

Fonction RM_TDRec(L, X, Stock) :


Si X == 0 :
Retourner 0
Sinon :
Si Stock[X] > 0 :
Retourner Stock[X]
Sinon :
Mini = X+1
POUR tous les éléments d’indice i de L :
Si L[i] <= X :
Nb = 1 + RM_TDRec(L, X-L[i], Stock)
Si Nb < Mini :
Mini = Nb
Stock[X] = Mini
Retourner Mini

Précision
Il s’agit ici de la forme Top Down : forme récursive dont on peut donner l’arbre correspondant.

À l’issue du programme, nous obtenons une liste de mémoire cache, qui nous donne, pour toutes les valeurs
inférieures ou égales à X, le nombre minimum de pièces. Dans notre exemple, on a Stock qui correspond au
tableau suivant :
i 0 1 2 3 4 5

Stock[i] 0 1 1 2 2 1

Pour être complet, il nous reste à donner le nombre d’exemplaires de chaque pièce ou billet dont il faut se servir
pour rendre X. Il nous faut alors modifier les fonctions ci-dessus en faisant intervenir un nouveau tableau qu’on
appellera Garde dont le contenu est égal au numéro de la première pièce utilisée. On peut aisément, à partir des
arbres vus précédemment voir par exemple que :
i 0 1 2 3 4 5

Garde[i] 0 1 2 1 2 3

Précision
Cela correspond au minimum de pièces. Par exemple :
Pour X = 5, le minimum de pièce est 1 et sa valeur est 5, ce qui correspond à la 3e pièce dans notre système. Pour X = 3, le minimum de pièce est 2 et on
passe par le chemin de la pièce de valeur 2, c’est-à-dire la 2e pièce.

Pour obtenir la liste des pièces et billets à utiliser, il suffit de faire des soustractions successives. Par exemple, pour
X = 5, puisque Garde[5] = 3, on doit d’abord rendre la 3e pièce, celle-ci vaut 5, il ne reste rien à rendre puisque X-
5=0. Si nous prenons X = 3, puisque Garde[3] = 1, on doit d’abord rendre la 1re pièce, celle-ci vaut 1, il nous reste
donc à rendre X-1 = 2, puisque Garde[4] = 2, on rend ensuite la 2e pièce (de valeur 2) et il ne reste plus rien à
rendre.
D’un point de vue algorithmique, on va retourner une liste P constituée des valeurs des pièces à retourner. Les
fonctions modifiées deviennent alors les suivantes :
Fonction RM_TD(L, X) :
Stock = Liste de (X+1) éléments initialisés à 0
Garde = Liste de (X+1) éléments initialisés à 0
Retourner RM_TDRec (L, X, Stock, Garde)
Fonction RM_TDRec(L, X, Stock, Garde) :
Si X == 0 :
Retourner 0, []
Sinon :
Si Stock[X] > 0 :
Retourner Stock[X], Garde[X]
Sinon :
Mini = X+1
POUR tous les éléments d’indice i de L :
Si L[i] <= X :
Nb = 1 + RM_TDRec(L, X-L[i], Stock, Garde)[0]
Si Nb < Mini :
Mini = Nb
Stock[X] = Mini
Garde[X] = i+1
x, P = X, []
TANT QUE x>0 :
Ajouter à P l’élément L[Garde[x]-1]
Soustraire à x la valeur L[Garde[x]-1]
Retourner Mini, P
Si nous souhaitons rendre de manière optimale une valeur 9 avec la liste des pièces et billets donnée, on obtient,
en ayant appelé notre programme le résultat suivant : (3, [2, 2, 5])

Précision
Il nous faut bien 3 pièces : 2 pièces de 2 et 1 billet de 5.

3 Recherche textuelle
Les algorithmes qui permettent de trouver une sous-chaîne de caractères dans une chaîne de caractères plus
grande sont des « grands classiques » de l’algorithmique. On parle aussi de recherche d’un motif (sous-chaîne)
dans un texte.
Un des secteurs qui utilise le plus cette recherche textuelle est le domaine de la bioinformatique notamment dans
l’analyse des informations génétiques. Cette information génétique présente dans nos cellules est portée par les
molécules d’ADN et permet la fabrication des protéines qui gère la quasi-totalité de nos fonctions biologiques. La
molécule d’ADN est constituée d’un grand nombre de nucléotides (environ 3,3 milliards de paires de nucléotides)
qui vont permettre la création d’acides aminés, composants des protéines. On compte 4 sortes de nucléotides
(appelés aussi bases) symbolisés par les lettres A, C, G et T respectivement nommés Adénine, Cytosine, Guanine et
Thymine. Un nucléotide est une structure chimique composée d’une base azotée, d’un phosphate et d’un sucre.
Notre ADN est composé de deux brins, plus exactement de deux chaînes complémentaires de nucléotides. Les
deux chaînes sont complémentaires car il existe une complémentarité chimique qui fait que la Guanine fait
toujours face à la Cytosine et l’Adénine toujours face à la Thymine à l’aide de liaisons moléculaires appelées
« liaisons hydrogènes » (3 pour le couple G-C et 2 pour le couple A-T). On peut représenter ces deux brins de
manière schématique :
L’information génétique est donc très souvent représentée par de très longues chaînes de caractères, composées
des caractères A, T, G et C.
Exemple : ATAACAGGAGTAAATAACGGCTGGAGTA…
Il est souvent nécessaire de détecter la présence de certains enchaînements de bases azotées (dans la plupart des
cas un triplet de bases azotées). On peut par exemple se poser la question suivante : trouve-t-on le motif CGGCTG
dans le brin d’ADN ci-dessus et si oui, en quelle position ?
Pour répondre à cela, la première approche consiste à placer notre motif (CGGCTG) au niveau des premiers
caractères de la chaîne. Si le premier élément du motif et le premier caractère de la chaîne ne correspondent pas,
on décale le motif d’un caractère vers la droite. S’ils correspondent, on fait le test entre les 2e caractères du motif
et de la chaîne. On répète le procédé jusqu’au moment ou les 6 caractères du motif sont trouvés (ou pas).

Dans le cas de cet algorithme dit naïf, il peut y avoir de très nombreuses comparaisons, ce qui peut entraîner un
temps d’exécution très long sur des chaînes très longues. On va donc s’intéresser à un algorithme plus optimisé qui
porte le nom de ses inventeurs américains.
Algorithme de Boyer-Moore
Cet algorithme, qui a été développé en 1977 par Robert S. Boyer et J Strother Moore, repose sur les
caractéristiques suivantes :
• Le motif est pré-traité : l’algorithme connaît alors tous les caractères du motif.
• Le décalage est judicieusement choisi : il peut être de plusieurs caractères.
• Les caractères sont comparés de droite à gauche (plutôt que de gauche à droite)
Reprenons le même exemple :

À l’étape 1, on compare G et A, pas de correspondance, de plus, grâce au prétraitement du motif, on sait que A
n’est pas un caractère du motif, on va décaler de la longueur du motif, c’est-à-dire de 6 caractères.
À l’étape 2, même scénario, on redécale le motif de 6 caractères
À l’étape 3, G et C ne correspondent pas, par contre, toujours grâce au prétraitement du motif, on sait que C est
présent à 2 reprises, on décale alors le motif jusqu’à l’occurrence de C la plus à droite. Le décalage est de 2
caractères
À l’étape 4, on recompare à partir de la droite, correspondance des 2 caractères de droite, mais pas des 2
caractères précédents, on décale jusqu’à la dernière occurrence de C, c’est-à-dire de 3 caractères.
À l’étape 5, on détecte la présence du motif.

Remarque
Plus le motif est long et plus cet algorithme est efficace.

Le prétraitement du motif consiste à garder dans une liste en mémoire la position de la dernière occurrence de
chaque caractère distinct du motif (sauf le dernier : car c’est déjà lui qu’on utilise pour comparer). Pour cela on
utilise l’alphabet ASCII qui contient 256 caractères. On peut donc proposer la fonction de prétraitement suivante :
Fonction PreTraitement(motif) :
Stock = Liste de (256) éléments initialisés à -1
POUR i allant de 0 à m-1 :
Stock[ord(motif[i])] = i
Retourner Stock

Remarque
Ord(c) est une fonction qui renvoie le nombre entier correspondant au caractère c dans la table ASCII.

Pour le motif de notre exemple, on a :


c C G T Autre caractère c dans la table ASCII

ord(c) 67 71 84 ord(c)

Stock[ord(c)] 3 2 4 -1

Alors l’algorithme de Boyer-Moore donné ci-dessous renvoie la liste des positions du motif éventuellement
trouvé :
Fonction BoyerMoore(motif, texte) :
Lst_rs = []
d_oc = PreTraitement(motif)
m = nombre de caractères dans la chaîne motif
n = nombre de caractères dans la chaîne texte
i=m-1
j=i
TANT QUE (i < n) :
Si motif[j] == texte[i] :
Si j == 0 :
Ajouter i à la Lst_res
i = i + 2*m-1
j=m–1
Sinon :
i = i -1
j=j–1
Sinon :
i = i + m – minimum(j, d_oc[ord(texte[i])])
j=m-1
Retourner Lst_res
Exercices
Compétences attendues
Comprendre la méthode « diviser pour régner ».

Exercice 12.1
▶ Analyser et modéliser un problème
▶Concevoir des solutions algorithmiques
En s’inspirant de la méthodologie donnée pour la recherche du minimum d’une liste, le but de cet exercice est de
rechercher le maximum. On pourra prendre comme liste d’étude la liste L = [25, 11, 3, 7, 5, 51, 32, 1, 23].
1. Représenter graphiquement l’arbre correspondant à la méthode « diviser pour régner » pour la recherche du
maximum d’une liste.
2. Proposer le pseudocode correspondant à cette fonction.
3. Implémenter cette fonction en Python.
Compétences attendues
Écrire un algorithme en utilisant la méthode « diviser pour régner ».

Exercice 12.2
▶ Analyser et modéliser un problème
▶Décomposer un problème en sous-problème s
On donne l’algorithme suivant :
Fonction TriFusion(A, i, k) :
j = (i+k) // 2
TriFusion(A, i, j)
TriFusion(A, j+1, k)
Fusionner(A, i, j, k)
1. Représenter graphiquement à l’aide d’un arbre l’application de cet algorithme sur la liste A = [9, 8, 12, 3, 5, 14,
6].
2. Quel est son rôle ?

Exercice 12.3
▶ Concevoir des solutions algorithmiques
On cherche à calculer la somme des éléments d’une liste B constituées de n éléments entiers.
1. Proposer un algorithme naïf.
2. Proposer un autre algorithme par la méthode « diviser pour régner ».
3. Comparer la complexité en temps de ces 2 solutions, et conclure.
Compétences attendues
Savoir utiliser la programmation dynamique.
Exercice 12.4
▶ Concevoir des solutions algorithmiques
▶Développer des capacités d’abstraction et de généralisation
1. Proposer une solution Bottom Up du problème de rendu de monnaie.
2. Implémenter cette solution en Python.
Compétences attendues
Étudier l’algorithme de Boyer Moore.

Exercice 12.5
▶ Traduire un algorithme dans un langage de programmation

1. Proposer une implémentation en Python d’un algorithme naïf permettant la recherche d’un motif de longueur m
dans une chaîne de caractères de longueur n. on pourra prendre pour les tests :
• le motif :
« CGGCTG »
• la chaîne :
« ATAACAGGAGTAAATAACGGCTGGAGTAATAACAGGAGTAAATAACGGCTGGAGTAATAACAGGAGTAAATAACGGCTGGAGTATAACAGG
2. Implémenter L’algorithme de Boyer-Moore donné dans le cours en Python. On pourra prendre pour les tests les
mêmes motifs et texte que la question précédente.
3. Comparer, avec Python, les temps d’exécution pour la recherche du motif donné avec les deux solutions
précédentes. Pour que ce test soit intéressant, il faut constituer une chaîne de texte d’1 million de caractères pris
au hasard dans l’ensemble (A, T, C, G). On fera la même chose pour constituer un motif de 1 000 caractères.
Aide : Pour mesurer le temps d’exécution d’un programme Python, on peut utiliser la fonction time() du module
time comme le montre l’exemple ci-dessous. Cette fonction renvoie le temps en seconde qu’il s’est écoulé depuis
le 1er janvier 1970.
import time
top =time.time()
…..
print (« temps d’exécutions : » , time.time() – top)
Corrigé des exercices
Exercice 12.1
1. La recherche du maximum peut se représenter avec la méthode « diviser pour régner » par l’arbre suivant :

2. On peut donner la fonction suivante qui calcule par la méthode « diviser pour régner » le maximum d’une liste L.
On lui donne également en paramètres les indices du premier et du dernier élément.
Fonction Maximum(L, d, f) :
Si d == f :
Retourner L[d]
Sinon :
m = (d+f) // 2
x = Maximum(L, d, m)
y = Maximum(L,m+1, f)
Si x < y
Retourner y
Sinon
Retourner x
3. On peut proposer le code Python suivant :

Exercice 12.2
1. La représentation graphique de l’arbre binaire est la suivante :
2. Cet algorithme utilise la méthode « diviser pour régner » afin de trier une liste. C’est la méthode du « tri
fusion ».

Précision
La fonction prend comme paramètre la liste à trier, ainsi que les indices du premier et du dernier élément.

Exercice 12.3
1. L’algorithme naïf que l’on peut proposer sur une liste à n éléments est le suivant :
Fonction SommeNaive(L) :
Somme = L[0]
POUR i allant de 1 à n :
Somme = Somme + L[i]
Retourner Somme
2. L’algorithme par la méthode « diviser pour régner » que l’on peut proposer est :
Fonction SommeDR(L, i, j) :
Si i == j :
Retourner L[i]
m= (i+j) // 2
gauche = SommeDR (B,i,m)
droite= SommeDR (B,m+1,j)
Retourner (gauche+droite)
3. Le premier algorithme est en tandis que le deuxième est en , ce qui veut dire que dans ce cas
l’algorithme qui met le plus de temps est le deuxième.

Précision
La méthode « diviser pour régner » n’est pas toujours la plus efficace.

Exercice 12.4
1. Pour une somme de X, notre liste de mémoire cache sera comme dans la forme Top Down constituée de X+1
éléments. La différence est qu’avec une approche Bottom Up, on va remplir cette fois notre liste de façon
itérative en partant de la plus petite valeur possible à rendre, donc de 0 jusqu’à X. Le calcul des différents
éléments de Stock provenant lui toujours de la formule de récurrence. Comme dans la solution du cours, la
solution à notre problème initial sera Stock[X].
Voici la fonction adoptant cette approche Bottom Up :
Fonction RM_BU(L, X) :
Stock = Liste de (X+1) éléments initialisés à 0
Garde = Liste de (X+1) éléments initialisés à 0
POUR i allant de 1 à X :
Mini = X+1
POUR chaque élément x de L :
Si (L[i] <= x) ET (1+Stock[x-L[i]] < Mini):
Mini = 1 + Stock[x-L[i]]
Piece = i
Stock[x] = Mini
Garde[x] = Piece
x, P = X, []
TANT QUE x>0 :
Ajouter à P l’élément L[Garde[x]-1]
Soustraire à x la valeur L[Garde[x]-1]
Retourner Stock[X], P
2. En Python, nous pouvons proposer le code suivant :

Exercice 12.4
1. On peut proposer le code naïf suivant :

2. L’implémentation en Python de l’algorithme de Boyer-Moore donne :

3. Afin de réaliser un test de temps d’exécution, il nous faut au préalable, construire notre chaîne de texte et notre
motif. On utilise la méthode choice() du module random.

Précision
On pourra même éventuellement vérifier que l’algorithme va encore « plus vite » si le motif est plus long ou s’il n’est constitué que de 3 types de
caractères (au lieu des 4 initiaux).

Le code du test (que l’on pourra exécuter plusieurs fois) est le suivant :
Table des matières

Introduction
Les compétences en NSI
1 Les compétences
2 Récapitulatif des exercices illustrant les compétences

Partie 1
Structures de données

Chapitre 1
Structures de données linéaires et dictionnaires
Cours
1 Généralités
2 Les listes
3 Les piles
4 Les files
5 Les dictionnaires
6 Applications

Exercices
Exercice-bilan
Corrigé des exercices
Corrigé de l’exercice-bilan

Chapitre 2
Structures de données hiérarchiques
Cours
1 Les arbres
2 Les arbres binaires
3 Mesures sur les arbres binaires
4 Arbre binaire de recherche
5 Type abstrait Arbre
6 Représentation sous forme d’un tableau
Exercices
Corrigé des exercices

Chapitre 3
Structures de données relationnelles
Cours
1 Introduction
2 Les graphes
3 Terminologie
4 Graphes simples et multigraphes
5 Graphes étiquetés et pondérés
6 Les matrices associées à un graphe
7 Les listes d’adjacence
8 Le type abstrait Graphe orienté
Exercices
Exercice-bilan
Corrigé des exercices
Corrigé de l’exercice-bilan

Chapitre 4
Initiation à la programmation orientée objet
Cours
1 Introduction
2 Conception orientée objet
3 Les classes
4 Création de classes
5 Notion d’agrégation de classes
Exercices
Exercice-bilan
Corrigé des exercices
Corrigé de l’exercice-bilan

Partie 2
Bases de données

Chapitre 5
Les bases de données : du modèle conceptuel à l’implémentation dans le système de gestion des bases
de données relationnelles
Cours
1 Introduction aux bases de données
2 Conception des bases de données relationnelles
3 Le modèle Entité-Association
4 Le modèle relationnel
5 Les contraintes d’intégrité
6 Le modèle physique : SGBD relationnel
Exercices
Exercice-bilan
Corrigé des exercices
Corrigé de l’exercice-bilan

Chapitre 6
Le langage SQL
Cours
1 Généralités
2 Requêtes d’interrogation : SELECT
3 Requêtes de mises à jour
Exercices
Corrigé des exercices

Partie 3
Architecture matérielle, systèmes d’exploitation et réseaux

Chapitre 7
Architectures matérielles et systèmes d’exploitation
Cours
1 Un peu d’histoire
2 Microprocesseur et mémoire
3 Les systèmes sur puces : les SoCs
4 Les systèmes d’exploitation
5 Les processus
Exercices
Exercices-bilan
Corrigé des exercices
Corrigé des exercices-bilan

Chapitre 8
Réseaux et sécurité
Cours
1 Un peu d’histoire
2 L’infrastructure d’Internet
3 Le protocole TCP/IP
4 Le routage
5 Le routage IP
6 TCP et la gestion des erreurs de transmission
7 La sécurisation des communications
Exercices
Exercices-bilan
Corrigé des exercices
Corrigé des exercices-bilan

Partie 4
Langages et programmation

Chapitre 9
Calculabilité, décidabilité, modularité et récursivité
Cours
1 Introduction
2 D’Alonso Church à Alan Turing
3 Décidabilité
4 Récursivité
5 Modularité
Exercices
Corrigé des exercices

Chapitre 10
Paradigmes de programmation et validation d’un programme
Cours
1 Introduction
2 Les principaux paradigmes
3 La validation d’un programme
4 Les tests

Exercices
Corrigé des exercices

Partie 5
Algorithmique

Chapitre 11
Algorithmes sur les arbres binaires et sur les graphes
Cours
1 Algorithmes sur les arbres binaires
2 Parcours des arbres binaires
3 Recherche d’une clé dans un arbre binaire de recherche
4 Insérer un nœud dans un arbre binaire de recherche
5 Algorithmes de parcours sur les graphes
6 Autres algorithmes sur les graphes
Exercices
Exercices-bilan
Corrigé des exercices
Corrigé des exercices-bilan

Chapitre 12
Méthodes d’optimisation et recherche textuelle
Cours
1 Méthode « Diviser pour régner »
2 Programmation dynamique
3 Recherche textuelle

Exercices
Corrigé des exercices

Vous aimerez peut-être aussi