Cours Algorithme Et Structure de Données 2023
Cours Algorithme Et Structure de Données 2023
Cours Algorithme Et Structure de Données 2023
I. Notion de programme
Rappel :
Un algorithme n'est donc exécutable directement par aucune machine. Mais il a l'avantage d'être traduit
facilement dans tous les langages de programmation. L'algorithmique, l'art d'écrire des
algorithmes, permet de se focaliser sur la procédure de résolution du problème sans avoir à se
soucier des spécificités d'un langage particulier.
1
Pour résoudre un problème, il est vivement conseillé de réfléchir d'abord à l'algorithme avant de
programmer proprement dit, c'est à dire d'écrire le programme en langage de programmation.
réflexion codage
Les données d'un programme doivent être récupérées en mémoire centrale, à partir du clavier
ou d'un fichier par exemple, pour pouvoir être traitées par le processeur qui exécute le programme.
Ainsi, toutes les données d'un programme sont mémorisées en mémoire centrale, dans des sortes de
cases que l'on appelle variables.
Une variable peut être représentée par une case mémoire, qui contient la valeur d'une
donnée.
Chaque variable possède un nom unique appelé identificateur par lequel on peut accéder
à son contenu.
Par exemple, on peut avoir en mémoire une variable prix et une variables quantité qui
contiennent les valeurs 10.2 et 5
10.2 5
prix quantité
Deux variables peuvent avoir la même valeur, mais une variable ne peut pas avoir plusieurs
valeurs en même temps.
En revanche, la valeur d'une variable peut varier au cours du programme.
L'ancienne valeur est tout simplement écrasée et remplacée par la nouvelle.
Les variables dont la valeur ne change pas au cours de l'exécution du programme sont appelées
variables constantes ou plus simplement constantes.
2
Pour qu'un programme puisse utiliser une variable, il faut au préalable que cette variable ait été
déclarée, c'est-à-dire que le programme lui ait réservé une place en mémoire et ait attribué
l'identificateur à cette place.
Mais toutes les variables n'ont pas besoin de la même place en mémoire. Un grand nombre
prend plus de place qu'un caractère. Selon le type de l'objet, il faudra lui réserver plus ou moins de
place: c'est pourquoi il faut déclarer le type des variables et pas seulement leur nom. Par ailleurs, selon
le type des variables, les opérations possibles seront différentes.
Un identificateur peut être composé de lettres et de chiffres mais il ne peut pas commencer par un
chiffre et ne peut comporter d'espaces.
L'identificateur des variables doit être suffisamment signifiant pour qu'on reconnaisse leur fonction
aisément. Par exemple pour des variables représentant un prix et une quantité, évitez a et b mais
utilisez plutôt prix et quant
- les caractères (lettres, chiffres, ponctuation, code des opérations, espace, retour chariot,… et plus
généralement toutes les touches que l'on peut trouver sur une machine à écrire)
- les chaînes de caractère (suites de caractères)
- les entiers (les nombres sans virgule)
- les réels (les nombres à virgule et sans virgule)
- les booléens (qui n'ont que deux valeurs possibles : soit VRAI, soit FAUX)
Les opérations possibles sur les variables dépendent de leur type (voir page suivante)
Synthèse:
identificateur type
variable
- commence par - détermine le domaine de
une lettre valeur de la variable
- pas d'espace
valeur
3
Type
caractère 'B' 'h' '£' '?' '\n' comparaisons <, ≤,>, ≥, =, ≠ cf. ci après
Pour les entiers, la division est notée Div. Elle est nommée division entière et diffère un peu de
division que l'on trouve sur les calculettes. Elle ne donne que le chiffre avant la virgule du résultat (elle
renvoie un entier).
Les entiers supportent une opération supplémentaire appelée modulo, notée mod et qui renvoie le
reste de la division entière.
Exemple:
7 / 2 donne 3.5
7 Div 2 donne 3
7 Mod 2 donne 1
Les caractères sont comparés selon l’ordre du code ASCII. C’est ainsi qu’on peut comparer tous les
caractères entre eux. Par exemple la lettre Z (majuscule), de code ASCII 90 est inférieure à la lettre a
(minuscule) de code ASCII 97. L’ordre ASCII des lettres de la même casse suit l’ordre alphabétique,
de sorte que A<B<C<D<…
L’opérateur & sert à concaténer des chaînes de caractère, ce qui signifie transformer plusieurs chaînes
en une seule en les ajoutant les unes à la suite des autres. Ex : « Bonjour » & « à tous » donne «
Bonjour à tous »
4
III. Les instructions élémentaires
1. Présentation générale
L'exécution d'un programme est constituée :
- d'échanges d'informations en mémoire
- de calculs
Une instruction est un ordre élémentaire que peut exécuter directement l'ordinateur. Une instruction
revient à déplacer une information d'un endroit à un autre de la mémoire.
Les informations (données) manipulées par les instructions peuvent prendre plusieurs formes:
- des variables proprement dites
- des variables constantes
- des valeurs littérales (écrites telles qu'elles dans le programme: ex "bonjour", 45, VRAI)
- des messages (libellés) envoyés à l'utilisateur (quelles données sont à entrer, quels résultats sont
affichés…), qui sont des valeurs littérales particulières
- des expressions complexes (combinaisons de variables, constantes et valeurs littérales avec des
opérateurs) ex : 2 * r * 3.14
B. L’affectation
1. Présentation détaillée
L’affectation consiste tout simplement à placer une valeur dans une variable (ce qui revient à
changer le contenu de cette variable)
La nouvelle valeur est évaluée à partir d'une expression, qui peut être
- soit une autre variable ou constante,
- soit une valeur littérale
- soit une combinaison de variables, de valeurs littérales et d'opérateurs
D. l'affichage
5
La plupart des programmes nécessitent de communiquer à l’utilisateur un certain nombre de
résultats par l’intermédiaire d’un périphérique. Pour cela, ils utilisent des instructions d'affichage.
L'instruction d'affichage permet de fournir des résultats sous forme directement compréhensible
pour l'utilisateur à travers l'écran.
Syntaxe
Exemples
Afficher toto
Cette instruction permet d'afficher la valeur de la variable toto à l'écran
Si toto est une chaîne qui vaut "tutu", cette instruction affichera tutu à l'écran
Afficher "Bonjour!"
Celle-ci permet d'afficher la chaîne littérale Bonjour! à l'écran
Afficher a, b
Quand on veut afficher deux objets à la suite, on les sépare d'une virgule Si a vaut 5 et
b vaut 10, on obtient alors à l'écran:
5 10
Chapitre 2:
6
Les structures de contrôle :
notions fondamentales
Introduction
En programmation procédurale comme en algorithmique (qui respecte les contraintes fondamentales
de la programmation!), l'ordre des instructions est primordial.
Le processeur exécute les instructions dans l'ordre dans lequel elles apparaissent dans le programme.
On dit que l'exécution est séquentielle.
Une fois que le programme a fini une instruction, il passe à la suivante. Tant qu'une instruction n'est
pas terminée, il attend avant de continuer. Par exemple, une instruction de saisie va attendre que
l'utilisateur rentre une valeur au clavier avant de continuer.
Parfois, il est nécessaire que le processeur n'exécute pas toutes les instructions, ou encore qu'il
recommence plusieurs fois les mêmes instructions. Pour cela, il faudra casser la séquence. C'est le rôle
des structures de contrôle.
A. Présentation
Si <condition>
Alors <traitement1>
Sinon <traitement2>
Finsi
7
II. La structure Si…Alors (conditionelle)
Cette structure est utilisée si on veut exécuter une instruction seulement si une condition est vraie et ne
rien faire si la condition est fausse. Elle évite d’écrire Sinon rien.
Si <condition> Alors
<traitement>
Finsi
Seule la boucle Tant que est fondamentale. Avec cette boucle, on peut réaliser toutes
les autres boucles alors que l'inverse n'est pas vrai. La boucle Pour est très utilisée aussi car
elle permet de simplifier la boucle Tant que lorsque le nombre de tour de boucle est connu
d’avance. La boucle Répéter, très peu utilisée, sera étudiée au chapitre suivant.
La boucle Tant que … Faire permet de répéter un traitement tant qu'une expression
conditionnelle est vraie. Si d'emblée, la condition n'est pas vraie, le traitement ne sera
pas exécuté. On voit donc que la boucle Tant que à un point commun avec la structure
conditionnelle où si la condition n'est pas vraie, le traitement n'est pas exécuté.
Syntaxe :
Tant que <condition d'exécution> Faire
B. La boucle Pour
La boucle Pour permet de répéter une instruction un nombre connu de fois. Elle a le
formalisme suivant :
Pour < compteur> de <valeur initiale> jqà <valeur finale> [pas de <incrément>] Faire
8
<traitement>
FinPour
Elle permet de faire la même chose que la boucle Tant que mais de façon plus rapide, du
moins lorsque le nombre de répétition est connu.
La variable compteur est de type entier. Elle est initialisée à la valeur initiale. Le compteur
augmente (implicitement) de l'incrément à chaque répétition du traitement. Lorsque la
variable compteur vaut la valeur finale, le traitement est exécuté une dernière fois puis le
programme sort de la boucle.
Par défaut, l’incrément est de 1
Grâce à une telle structure, le traitement va être répétée 20 fois. On pourrait faire la même
chose avec une boucle tant que, mais il faudrait initialiser la variable compteur et
l'incrémenter explicitement.
X := 1
ant que x <= 20 Faire
<traitement> x :=
x+1
FinTantQue
Application
Affichons la table de multiplication du 7. Pour cela on va utiliser une variable a qui varie de 1
à 10 et multiplier cette variable par 7 à chaque incrémentation. Cette variable va aussi servir
de compteur pour la structure Pour.
9
Selon expression Faire
valeur 1 de l'expression : traitement 1
valeur 2 de l'expression : traitement 2 valeur
3 de l'expression : traitement 3 …
[Sinon traitement par défaut] Finselon
Voilà l'algorithme qui affiche le mois en toute lettre selon son numéro. Le
numéro du mois en mémorisé dans la variable mois.
B. La boucle Répéter…Jusqu'à
Cette boucle sert à répéter une instruction jusqu'à ce qu'une condition (expression booléenne) soit
vraie. Son formalisme est le suivant :
Répéter
traitement // une instruction simple ou un bloc d'instructions
Le traitement est exécuté, puis la condition est vérifiée. Si elle n'est pas vraie, on retourne au début
de la boucle et le traitement est répété. Si la condition est vraie, on sort de la boucle et le
programme continue séquentiellement. A chaque fois que le traitement est exécuté, la condition
d'arrêt est de nouveau vérifiée à la fin.
10
Vrai traitement
test
Faux
traitement test
Faux
Vrai
su ite su ite
A. Exemple introductif
Saisir la liste des 12 notes sur 30
16 23 8 19 28 20 18 14 10 9 15 24
Voici la liste de ces notes sur 20
10.67 15.33 5.33 12.67 18.67 13.33 12 9.33 6.67 6 10 16
Dans l'état actuel de vos connaissances, pour écrire l'algorithme qui donne la sortie d'écran suivante,
vous êtes obligé de déclarer 12 variables différentes. On ne peut pas utiliser une seule variable qui
prend successivement chaque valeur. Passe encore avec 12 notes, mais si l'on voulait réaliser ce
traitement avec 30 ou 40 notes, cela deviendrait fastidieux.
En outre, le même traitement est effectué 12 fois sur des variables différentes. Comme les variables ont
des noms différents, on ne peut pas utiliser de boucle, ce qui allonge considérablement le code et le
rend très répétitif.
Pour résoudre ces problèmes, il est tentant d'utiliser un nom commun pour toutes les variables et de
les repérer par un numéro. Ainsi, on pourrait déclarer toutes les variables d'un seul coup et utiliser une
boucle pour effectuer le traitement en faisant varier le numéro des variables. Cela est possible grâce à
l'utilisation d'un tableau.
Programme conv_note
Var
Note : tableau[1..12] de
réels i: entier
11
Début
Afficher "Saisir la liste des 12 notes sur 30"
Pour i de 1 jqà 12 Faire
Saisir note[i]
FinPour
Afficher "Voici la liste de ces notes sur
20" Pour i de 1 jqà 12 Faire Afficher
note[i]*2/3
FinPour Fin
1ière case 2ième case 3ième case 4ième case … 19ième case 20ième case
Un tableau possède un nom (ici salaire) et un nombre d'éléments (de cases) qui représente sa taille (ici
20).
Tous les éléments d'un tableau ont le même type (c'est normal car ils représentent des valeurs
logiquement équivalentes).
Pour désigner un élément, on indique le nom du tableau suivi son indice (son numéro) entre crochets:
salaire [3] représente le 3ième élément du tableau salaire et vaut 12300
Remarque :
Dans un programme, chaque élément d'un tableau est repéré par un indice. Dans la vie courante, nous
utilisons souvent d'autres façons de repérer une valeur. Par exemple, au lieu de parler de salaire [1], salaire[2],
salaire [3], nous préférons parler des salaires de Mr Dupond, de Mme Giraud et de Mr Fournier. Le tableau ne
permet pas de repérer ses valeurs autrement que par un numéro d'indice. Donc si cet indice n'a pas de
signification, un tableau ne permet pas de savoir à quoi correspondent les différentes valeurs.
La notion de « case contenant une valeur » doit faire penser à celle de variable. Et, en effet, les cases
du tableau, encore appelées éléments du tableau, sont des variables, qualifiées d'indicées.
12
Le tableau lui-même constitue aussi une variable. C’est une variable complexe (par opposition aux variables
simples) car il est constitué d'autres variables (les éléments du tableau).
A. Déclaration
1. Tableau variable
La syntaxe de la déclaration d'une variable tableau est la suivante :
B. Manipulation
Les tableaux peuvent se manipuler de deux manières :
soit à travers leurs éléments le nom du tableau est alors suivi d'un indice entre crochets c'est la
méthode que nous utiliserons
soit dans sa globalité le nom du tableau n'est pas suivi d'indice nous n'étudierons pas cette
possibilité de manipulation car elle fait appel à des concepts mathématiques qui n'ont pas été vus.
Les éléments d'un tableau sont des variables indicées qui s'utilisent exactement comme n'importe
quelles autres variables classiques. Autrement dit, elles peuvent faire l'objet d'une affectation, elles
peuvent figurer dans une expression arithmétique, dans une comparaison, elles peuvent être affichées
et saisies…
C. Synthèse
Un tableau est une structure de donnée permettant de mémoriser des valeurs de même type et
logiquement équivalentes.
Un vecteur est un tableau à une dimension (un seul indice permet d'identifier toutes les valeurs)
Chaque élément d'un tableau est repéré par un indice indiquant sa position.
La taille d'un tableau est le nombre de ses éléments.
La taille d'un tableau est fixe.
13
Les indices doivent obligatoirement être entiers et varier entre une valeur minimale ( en général 1) et
une valeur maximale constante.
Chaque élément du tableau est une variable indicée, identifiée par le nom du tableau suivi de
l'indice entre crochets. Les variables indicées s'utilisent comme des variables classiques.
Déclaration
ex: un tableau nommé toto de 10 éléments entiers se déclare ainsi:
INTRODUCTION
Lorsqu'un programme est long, il est irréaliste d'écrire son code d'un seul tenant. En fait, on décompose
le programme en plusieurs parties plus petites, on donne un nom à chacune de ces parties, et on les
assemble pour former le programme final. C'est le principe de la programmation modulaire, qui
repose sur l'écriture de sous-programmes.
Un sous-programme est, comme son nom l'indique, un petit programme réalisant un traitement
particulier qui s'exécute à l'intérieur d'un autre programme.
Certains sous-programmes ont déjà été écrits et peuvent être utilisés directement dans n'importe quel
programme. Ce sont des sous-programmes standards ou prédéfinis. C'est le cas par exemple des sous
programmes permettant de faire des calculs mathématiques (racine carrée, exposant, …). La nature et
le nombre de programmes standards dépendent des langages.
Mais les sous-programmes prédéfinis ne suffisent pas pour découper un gros programme : le
programmeur est amené à écrire le code de ses propres sous-programmes.
LES PROCEDURES
14
Une procédure est un ensemble d'instructions regroupées sous un nom, qui réalise un traitement
particulier dans un programme lorsqu'on l'appelle.
Comme un programme, une procédure possède un nom, des variables, des instructions, un début et une
fin. Mais contrairement à un programme, un sous-programme ne peut pas s'exécuter indépendamment
d'un autre programme.
Cette procédure permet d'afficher une ligne de 10 étoiles puis passe à la ligne.
Pour déclencher l'exécution d'une procédure dans un programme, il suffit de l'appeler, c'est-à-dire
d'indiquer son nom suivi de parenthèses.
Programme RectangleEtoile
Var
nlignes:
entier cpt :
entier
Début
Afficher "Ce programme dessine un rectangle d'étoiles. Combien voulez-vous de lignes?"
Saisir nlignes
Pour cpt de 1 jusqu'à nlignes Faire
ligneEtoile ( ) Var
FinPour i : entier
Fin Début
Pour i de 1 jusqu'à 10 Faire
Afficher '*'
FinPour
appel de l a Afficher '\n'
procédure FinProc
Une procédure peut être appelée soit par un programme, soit par un autre sous-programme (qui lui
même a été appelé). Les appels de sous-programmes peuvent s'imbriquer autant qu'on le désire.
15
Notions de variables locales et de paramètres
Les variables déclarées dans une procédure ne sont pas utilisables dans le programme appelant et
inversement, les variables déclarées dans le programme appelant ne sont pas utilisables dans les
procédures. Chaque programme et sous-programme a son propre espace de variables, inaccessible par
les autres. On dit que les variables sont LOCALES. (on verra qu'il existe des variables globales, mais
elles sont très peu utilisées).
Dans notre exemple, on ne pourrait pas saisir ou afficher cpt dans le programme principal, car cpt est
une variable de la procédure, utilisable seulement dans celle-ci. Le programme principal n'a pas le
droit de l'utiliser.
Une question qui se pose alors est de savoir comment procédures et programmes vont pouvoir
communiquer des données. Par exemple, on pourrait vouloir que le programme principal communique
à le procédure combien d'étoiles afficher par ligne. Cela est possible grâce aux paramètres.
Un paramètre est une variable particulière qui sert à la communication entre programme
appelant et sous-programme.
Exemple :
Dans notre exemple, nous allons mettre le nombre d'étoiles par lignes en paramètre.
Pour cela, nous indiquons entre parenthèses la déclaration du paramètre (qui est une variable de la
procédure !), précédé du mot clé donnée pour indiquer que le paramètre constitue une donnée du
traitement réalisé par la procédure. La valeur de cette donnée est communiquée à l'appel, par le
programme appelant.
16
Les paramètres réels et formels
Il est primordial de bien distinguer les paramètres qui se trouvent dans l'en-tête d'une procédure, lors
de sa définition et les paramètres (ou arguments) qui se trouvent placés entre parenthèses lors de
l'appel.
Les paramètres placés dans la définition d'une procédure sont les paramètres formels. Ils servent
à décrire le traitement à réaliser par la procédure indépendamment des valeurs traitées. Les
paramètres formels sont des variables locales à la procédure, et à ce titre ils sont déclarés dans
l'entête de la procédure.
Les paramètres placés dans l'appel d'une procédure sont les paramètres réels ou effectifs.
Lorsqu'ils sont de type donnée, ils contiennent effectivement les valeurs sur lesquelles sera
effectué le traitement de la procédure. Lors de l'appel, leur valeur est recopiée dans les
paramètres formels correspondants. Un paramètre effectif en donnée peut être soit une
variable du programme appelant, soit une valeur littérale, soit le résultat d'une expression.
LES FONCTIONS
Les fonctions sont des sous-programmes qui retournent un et un seul résultat au programme appelant.
De ce fait, les fonctions sont appelées pour récupérer une valeur, alors que les procédures ne renvoient
aucune valeur au programme appelant.
Le résultat d'une fonction doit obligatoirement être retourné au programme appelant par l'instruction
Retourne.
Syntaxe générale
Fonction nom(déclaration des paramètres) : type de la valeur retournée
Var
//traitement
Retourne valeur à retourner
FinFonction
correspond à la valeur d'une variable, d'une
expression ou d'une valeur littérale.
17
Exemple :
déclarationduparamètreformel
nb_saisi :entie
r
Début
Afficher "Veuillez entrer un nombre positif"
Saisir nb_saisi
Tantque nb_saisi < 0 Faire
Afficher "Erreur. Saisissez un nombre supérieur à 0 s'il vous
plait !" Saisir nb_saisi
FinTantque
Retourne nb_saisi
FinFonction
Les paramètres d'une fonction sont toujours de type donnée. La valeur des paramètres effectifs
à l'appel est recopiée dans les paramètres formels qui servent à réaliser le traitement de la
fonction.
18
Une procédure peut renvoyer plusieurs résultats au programme appelant à travers des paramètres de
statut résultat. Elle peut aussi modifier la valeur d'un paramètre : ce paramètre doit alors avoir le statut
de donnéerésultat.
Rappel et mise en garde: une fonction ne communique pas son résultat à travers un paramètre, mais à
travers une simple valeur de retour. Ne pas confondre avec les paramètres résultats utilisés dans les
procédures ! Le mode de communication est totalement différent
Il existe donc trois statuts pour les paramètres des procédures: donnée, résultat et donnée-résultat
Nous avons déjà étudié le fonctionnement des paramètres données dans la première partie de ce cours;
nous allons voir ci-dessous le fonctionnement des deux autres.
Les paramètres résultats
Les paramètres résultats sont communiqués au programme appelant au moment du retour d'appel,
c'est-àdire à la fin de la procédure. La valeur du paramètre formel résultat est alors copiée dans la
variable passée en paramètre dans l'appel (le paramètre effectif). La valeur initiale du paramètre
effectif (à l'appel) n'a aucune importance ; elle est même souvent indéterminée. La procédure se charge
de lui affecter une valeur.
19
Les fonctions ne peuvent avoir que des Les procédures peuvent avoir des paramètres
paramètres données. résultats, données ou données-résultats.
Une s'appelle à l'intérieur d'une instruction. L'appel d'une procédure représente une
L'instruction utilise la valeur retournée par la instruction en elle-même. On ne peut pas
fonction. appeler une procédure au milieu d'une
instruction
Introduction
Contrairement aux tableaux qui sont des structures de données dont tous les éléments sont de même type, les
enregistrements sont des structures de données dont les éléments peuvent être de type différent et qui se
rapportent à la même entité (au sens de Merise)
Les éléments qui composent un enregistrement sont appelés champs.
Avant de déclarer une variable enregistrement, il faut avoir au préalable définit son type, c'est à dire le nom et le
type des champs qui le compose. Le type d'un enregistrement est appelé type structuré. (Les enregistrements
sont parfois appelé structures, en analogie avec le langage C)
Pour créer des enregistrements, il faut déclarer un nouveau type, basé sur d'autres types existants, qu'on appelle
type structuré Après avoir défini un type structuré, on peut l'utiliser comme un type normal en déclarant une ou
plusieurs variables de ce type. Les variables de type structuré sont appelées enregistrements.
La déclaration des types structurés se fait dans une section spéciale des algorithmes appelée Type, qui précède la
section des variables (et succède à la section des constantes).
20
Déclaration d'un enregistrement à partir d'un type structuré
Une fois qu'on a défini un type structuré, on peut déclarer des variables enregistrements exactement de
la même façon que l'on déclare des variables d'un type primitif.
1
Alors que les éléments d'un tableau sont accessibles au travers de leur indice, les champs d'un
enregistrement sont accessibles à travers leur nom, grâce à l'opérateur '.'
Exemple1 :
Voilà une fonction qui renvoie la différence d'age entre deux personnes
Exemple 2 :
Voilà une procédure qui permet de modifier le prix de vente hors taxes d'un produit passé en
paramètre. Cette procédure commence par afficher le libellé et l'ancien prix de vente hors taxes
du produit puis saisit le nouveau prix de vente entré par l'utilisateur.
21
Chapitre 6 Les Listes Chaînées
On commencera par le concept général de liste chaînée, que l'on particularisera ensuite
aux structures de pile et file.
Listes chaînées
Cette partie sera consacrée à l'étude des listes chaînées, structures de données où l'on
passe d'un élément à un autre grâce à un pointeur.
Principe
En C et C++ on a présenté une structure de données nommée tableau permettant de
stocker des valeurs de même type au sein d’une seule variable. Le principal défaut d'un
tableau est qu'une fois déclaré on ne peut pas modifier simplement sa taille . Le
problème de l’insertion de nouvelles valeurs est donc difficile à gérer avec une telle
structure. De même pour la suppression.
On va maintenant introduire une structure plus souple, celle de liste chaînée. Il s’agit
d’une succession de maillons, liés entre eux par des pointeurs.
22
On dénombre plusieurs avantages à l'utilisation d'une telle structure :
Représentation générale
On doit distinguer deux cas selon que le dernier maillon pointe vers le premier ou non. Si
c'est le cas on qualifie alors le chaînage de circulaire.
Pour une liste simplement chaînée circulaire l'attribut pointeur du dernier maillon
contient donc l'adresse du premier :
23
Les principales méthodes de la classe liste
On suppose que l'on dispose d'une liste chaînée et que l'on souhaite insérer un élément à
une certaine position.
Il faut naturellement commencer par construire un nouveau maillon avec cet élément :
24
On est donc confronté à cette situation :
Il faut ensuite faire pointer le maillon nouvellement construit vers le "bon" maillon de la
liste :
Il ne reste plus alors qu'à faire pointer le "bon" maillon de la liste vers le maillon
nouvellement construit :
25
Notre opération d'insertion est ainsi terminée :
Il est possible que l'on souhaite conserver la valeur de l'élément à retirer, commençons
donc par la mémoriser :
26
Il faut ensuite relier les maillons d'avant et d'après celui que l'on veut supprimer :
27
Listes doublement chaînées
Avec ce type de chaînage, on peut à partir d'un maillon accéder à la fois à son
prédécesseur et à son successeur.
Pour réaliser ce chaînage, les maillons devront être des objets composés de trois
attributs :
28
Les méthodes de traitement des listes doublement chaînées sont bien sûr du même type
que celles des listes simplement chaînées. Leur écriture est juste plus complexe dans la
mesure où l’on a deux pointeurs à gérer par maillon. Nous laissons le lecteur réfléchir lui-
même à la question.
Piles
Nous allons ici nous intéresser aux piles, i.e. aux listes chaînées soumises au
principe L.I.F.O..
Principe
Une pile est une liste simplement chaînée bien particulière où les opérations d’insertion
et de suppression d’élément ne se font qu’ à la fin. Cette structure de donnée obéit ainsi à
la règle L.I.F.O., Last In First Out. Cela signifie concrètement que l’on ne peut supprimer
que le dernier élément inséré.
Puisque c'est un cas particulier de liste chaînée, une pile sera constituée elle aussi d'une
succession de maillons, qui seront comme précédemment des objets possédant deux
attributs, l'un pour la donnée, l'autre pour pointer vers un autre maillon.
29
Une autre insertion d'élément :
30
Une autre suppression d'élément :
Puisque la classe pile suit le principe L.I.F.O. elle contient juste les méthodes suivantes :
Pour finir cette partie, citons quelques applications importantes des piles :
Files
Cette dernière partie sera consacrée aux listes chaînées régies par le principe F.I.F.O.,
que l'on appelle des files.
Principe
Une file est une liste simplement chaînée bien particulière où l'opération d’insertion
d’un élément ne se fait qu’à la fin, et celle de suppression qu’au début. Cette structure
de donnée obéit ainsi à la règle F.I.F.O., First In First Out. Cela signifie concrètement
que l’on ne peut supprimer que le premier élément inséré.
Puisque c'est un cas particulier de liste chaînée, une file sera constituée elle aussi d'une
succession de maillons, qui seront comme précédemment des objets possédant deux
attributs, l'un pour la donnée, l'autre pour pointer vers un autre maillon.
Illustrons maintenant avec quelques images le fonctionnement d'une file afin que le
lecteur en saisisse bien son principe.
31
L'insertion d'un élément se fait donc nécessairement à la fin de la file :
32
Une autre suppression d'élément :
Puisque la classe file suit le principe F.I.F.O. elle contient juste les méthodes suivantes :
Construction d’une file vide.
Enfilage d’un élément.
Défilage d’un élément.
Récupération du premier élément de la file sans le défiler.
Un dernier effort est maintenant demandé au lecteur, celui d'évaluer la complexité de ces
méthodes.
33
mathématiciens voient les arbres eux-mêmes comme des cas particuliers de
graphes non orientés connexes et acycliques, donc contenant des sommets et
des arcs :
Enfin certains arbres particuliers nommés arbres binaires sont les plus
utilisés en informatique et les plus simples à étudier. En outre il est toujours
possible de "binariser" un arbre non binaire, ce qui nous permettra dans ce
chapitre de n'étudier que les structures d'arbres binaires.
Etiquette
Un arbre dont tous les noeuds sont nommés est dit étiqueté. L'étiquette (ou
nom du sommet) représente la "valeur" du noeud ou bien l'information
associée au noeud. Ci-dessous un arbre étiqueté dans les entiers entre 1 et
10 :
34
Racine, noeud, branche, feuille
Nous rappellons la terminologie de base sur les arbres sur le schéma ci-
dessous :
Pour atteindre le noeud étiqueté 9 , il faut parcourir le lien 1--5, puis 5--8,
puis enfin 8--9 soient 4 noeuds donc 9 est de profondeur ou de hauteur égale
à 4, soit h(9) = 4.
Pour atteindre le noeud étiqueté 7 , il faut parcourir le lien 1--4, et enfin 4--7,
donc 7 est de profondeur ou de hauteur égale à 3, soit h(7) = 3.
35
h(racine) =1 (pour tout arbre non vide)
On appelle chemin du noeud X la suite des noeuds par lesquels il faut passer
pour aller de la racine vers le noeud X :
h( X ) = NbrNoeud( Chemin( X ) ).
36
9 est l'enfant de 8 10 est l'enfant de 8
8 est le parent de 9 8 est le parent de 10 9 et 10 sont des frères
h (racine) = 1;
h ( X ) = 1+ h ( parent ( X ) )
h(9) = 1+h(8)
h(8) = 1+h(5)
h(5) = 1+h(1)
h(1) = 1 => h(5)=2 => h(8)=3 => h(9)=4
37
Le noeud 1 est de degré 4, car il a 4 enfants
38
Degré d'un arbre
Le degré d'un arbre est égal au plus grand des degrés de ses noeuds :
d°(1) = 4 d°(2) = 0
d°(3) = 0 d°(4) = 2
d°(5) = 1 d°(6) = 0
d°(7) = 0 d°(8) = 2
d°(9) = 0 d°(10) = 0
Arbre lexicographique
39
Soient les mots BON, BONJOUR, BORD, BOND, BOREALE, BIEN, il est
possible de les ranger ainsi dans une structure d'arbre :
40
Arbre de recherche
Voici à titre d'exemple que nous étudierons plus loin en détail, un arbre dont
les noeuds sont de degré 2 au plus et qui est tel que pour chaque noeud la
valeur de son enfant de gauche lui est inférieure ou égale, la valeur de son
enfant de droite lui est strictement supérieure.
Ci-dessous un tel arbre ayant comme racine 30 et stockant des entiers selon
cette répartition :
41
2. Les arbres binaires
Un arbre binaire est un arbre de degré 2 (dont les noeuds sont de degré 2 au
plus).
Vocabulaire :
Les descendants (enfants) d'un noeud sont lus de gauche à droite et sont
appelés respectivement fils gauche (descendant gauche) et fils
droit(descendant droit) de ce noeud.
A =
42
2.2 Exemples et implémentation d'arbre binaire étiqueté
Spécification concrète
o l'information du noeud
o le fils gauche
o le fils droit
43
Exemple
racine = table[2]
table[1] = < d , 0 , 0 >
table[2] = < a , 4 , 5 >
table[3] = < e , 0 , 0 >
table[4] = < b , 0 , 0 >
table[5] = < c , 1 , 3 >
Explications :
table[2] = < a , 4 , 5 > signifie que le fils gauche de ce noeud est dans
table[4] et son fils droit dans table[5]
table[5] = < c , 1 , 3 > signifie que le fils gauche de ce noeud est dans
table[1] et son fils droit dans table[3]
table[1] = < d , 0 , 0 > signifie que ce noeud est une feuille
...etc
44
const
taille = n; // n valeur effective 10, 1000, 10000 etc...
type
Noeud = record
info : T0;
filsG , filsD : 0..taille ;
end;
Tableau = Array[1..taille] of Noeud ;
ArbrBin = record
ptrRac : 0..taille;
table : Tableau ;
end;
Var
Tree : ArbrBin ;
Explications :
45
2.2.2 - Implantation avec des variables dynamiques
Spécification concrète
o l'information du noeud
o une référence vers le fils gauche
o une référence vers le fils droit
Exemple
46
Nous proposons d'utiliser les déclarations de variables dynamiques suivantes
:
type
ArbrBin = ^Noeud ;
Noeud = record
info : T0;
filsG , filsD : ArbrBin ;
end;
Var
Tree : ArbrBin ;
Explications :
interface
// dans cette classe tous les champ sont publics afin de simplifier l'écriture
TreeBin = class
public
Info : string;
filsG , filsD : TreeBin;
constructor CreerTreeBin(s:string);overload;
47
constructor CreerTreeBin(s:string; fg , fd : TreeBin);overload;
destructor Liberer;
end;
implementation
......
end.
48
Nous en avons déjà vu un plus haut :
Prenons par exemple le noeud (25) son sous-arbre droit est bien composé de
noeuds dont les clefs sont supérieures à 25 : (29,26,28). Le sous-arbre
gauche du noeud (25) est bien composé de noeuds dont les clefs sont
inférieures à 25 : (18,9).
Nous remarquons dans les deux cas que nous avons affaire à une liste
chaînée donc le nombre d'opérations pour la suppression ou la recherche est
au pire de l'ordre de O(n). Il faudra utiliser une catégorie spéciale d'arbres
binaires qui restent équilibrés (leurs feuilles sont sur 2 niveaux au plus) pour
assurer une recherche au pire en O(log(n)).
49
Arbre parfait :
c'est un arbre binaire dont tous les noeuds de chaque niveau sont présents sauf éventuellement
au dernier niveau où il peut manquer des noeuds (noeuds terminaux = feuilles), dans ce cas
l'arbre parfait est un arbre binaire incomplet et les feuilles du dernier niveau doivent être
regroupées à partir de la gauche de l'arbre.
(parfait complet : le dernier niveau est complet car il contient tous les
enfants )
(non parfait : le dernier niveau est incomplet car il manque 1 enfant, les
50
feuilles
ne sont pas regroupées à gauche)
Les noeuds de l'arbre sont dans les cellules du tableau, il n'y a pas d'autre
information dans une cellule du tableau, l'accès à la topologie arborescente
est simulée à travers un calcul d'indice permettant de parcourir les cellules du
tableau selon un certain 'ordre' de numérotation correspondant en fait à
un parcours hiérarchique de l'arbre. En effet ce sont les numéros de ce
parcours qui servent d'indice aux cellules du tableau :
51
Lorsque l'indice i d'une cellule (d'un noeud) est fixé :
52
Exemple de rangement d'un tel arbre dans un tableau (pour une vision
pédagogique on a figuré l'indice de numérotation hiérarchique de chaque
noeud dans le rectangle associé au noeud) :
Le nombre qui figure dans la cellule (nombre qui vaut l'index de la cellule =
le numéro hiérarchique du noeud) n'est mis là qu'à titre pédagogique afin de
bien comprendre le mécanisme.
On voit par exemple, que par calcul on a bien le fils gauche du noeud
d'indice 2 est dans la cellule d'index 2*2 = 4 et son fils droit se trouve dans la
cellule d'index 2*2+1 = 5 ...
53
arbre parfait parcours hiérarchique
rangement de l'arbre
dans un tableau
numérotation hiérarchique
C'est un arbre étiqueté dont les valeurs des noeuds appartiennent à un ensemble muni d'une
relation d'ordre total (les nombres entiers, réels etc... en sont des exemples) tel que pour un
noeud donné tous ses fils ont une valeur supérieure ou égale à celle de leur père.
Nous remarquons que la racine d'un tel arbre est toujours l'élément de
l'ensemble possédant la valeur minimum (le plus petit élément de
54
l'ensemble), car la valeur de ce noeud par construction est inférieure à celle
de ses fils et par transitivité de la relation d'ordre à celles de ses descendants
c'est le minimum. Si donc nous arrivons à ranger une liste d'éléments dans un
tel arbre le minimum de cette liste est atteignable immédiatement comme
racine de l'arbre.
Voici réellement ce qui est stocké dans le tableau :(entre parenthèses l'index
de la cellule contenant le noeud)
Le tas :
L'intérêt d'utiliser un arbre parfait complet ou incomplet réside dans le fait que le tableau est
toujours compacté, les cellules vides s'il y en a se situent à la fin du tableau.
Le fait d'être partiellement ordonné sur les valeurs permet d'avoir immédiatement un
extremum à la racine.
55
2.5 Parcours d'un arbre binaire
Objectif : les arbres sont des structures de données. Les informations sont
contenues dans les noeuds de l'arbre, afin de construire des algorithmes
effectuant des opérations sur ces informations (ajout, suppression,
modification,...) il nous faut pouvoir examiner tous les noeuds d'un arbre.
Nous devons avoir à notre disposition un moyen de parcourir ou traverser
chaque noeud de l'arbre et d'appliquer un traitement à la donnée rattachée à
chaque noeud.
Parcours :
Largeur ( Arbre )
si Arbre alors
ajouter racine de l'Arbre dans Fifo;
tantque Fifo faire
prendre premier de Fifo;
traiter premier de Fifo;
ajouter filsG de premier de Fifo dans Fifo;
ajouter filsD de premier de Fifo dans Fifo;
ftant
Fsi
56
Un autre algorithme général de parcours d'un arbre est employé très souvent,
il s'agit du parcours dit "en profondeur".
Parcours en profondeur :
57
Chaque noeud a bien été examiné selon les principes du parcours en
profondeur :
parcourir ( Arbre )
si Arbre alors
Traiter-1 (info(Arbre.Racine)) ;
parcourir ( Arbre.filsG ) ;
Traiter-2 (info(Arbre.Racine)) ;
parcourir ( Arbre.filsD ) ;
Traiter-3 (info(Arbre.Racine)) ;
Fsi
58
traitement de l'information située aux noeuds. Chacun de ces 3 parcours
définissent un ordre implicite (préfixé, infixé, postfixé) sur l'affichage et le
traitement des données contenues dans l'arbre.
parcourir ( Arbre )
si Arbre alors
Traiter-1 (info(Arbre.Racine)) ;
parcourir ( Arbre.filsG ) ;
parcourir ( Arbre.filsD ) ;
Fsi
parcourir ( Arbre )
si Arbre alors
parcourir ( Arbre.filsG ) ;
parcourir ( Arbre.filsD ) ;
Traiter-3 (info(Arbre.Racine)) ;
Fsi
parcourir ( Arbre )
si Arbre alors
parcourir ( Arbre.filsG) ;
Traiter-2 (info(Arbre.Racine)) ;
parcourir ( Arbre.filsD ) ;
Fsi
Le lecteur trouvera ailleurs des exemples de parcours selon l'un des 3 ordres
infixé, préfixé, postfixé, nous proposons un exemple didactique de parcours
général avec les 3 traitements.
59
Nous allons voir comment utiliser une telle structure arborescente afin de
restituer du texte algorithmique linéaire.
60
Traitement des attributs pour produire le texte :
61
parcourir ( Arbre )
si Arbre alors
Traiter-1 (Attribut
n°1) ;
parcourir
( Arbre.filsG ) ;
Traiter-2 (Attribut
n°2) ;
parcourir
( Arbre.filsD ) ;
Traiter-3 (Attribut
n°3) ;
Fsi
si A=0 alors
si B=0 alors
si C=0 alors
ecrire(R est sol)
sinon
ecrire(pas de sol)
Fsi
sinon
X1=-C/B;
ecrire(X1);
Fsi
sinon
Equation2
Fsi
62
feuille et le parcours complet associé :
si B=0 alors
si C=0 alors
ecrire(R est sol)
sinon
ecrire(pas de sol)
Fsi
sinon
Soit l'arbre suivant possédant 2 attributs par noeuds (un symbole de type
caractère)
63
On propose le traitement en profondeur de l'arbre comme suit :
L'attribut de gauche est écrit en descendant, l'attribut de droite est écrit
en remontant, il n'y a pas d'attribut ni de traitement lors de l'examen du fils
droit en venant du fils gauche.
Réponse : abcdfghjkiemnoqrsuv
si Arbre alors
ajouter racine de l'Arbre dans Fifo;
tantque Fifo faire
prendre premier de Fifo;
traiter premier de Fifo;
ajouter filsG de premier de Fifo dans Fifo;
ajouter filsD de premier de Fifo dans Fifo;
ftant
Fsi
Soyons tout de même un peu plus précis à la fois dans la terminologie et dans
la définition.
Définition
64
D’un ensemble EE dont les éléments sont des parties à un ou deux éléments
de VV, et sont appelés les arêtes du graphe.
Pour définir un graphe non orienté, il faut donc commencer par décrire l'ensemble de
ses sommets :
V={A,B,C,D,E}V={A,B,C,D,E}
On peut alors donner l'ensemble de ses arêtes :
E={{A,B},{B},{B,C},{B,D},{C,D},{E,C}}E={{A,B},{B},{B,C},{B,D},{C,D},{E,C}}
Définition
La représentation sagittale d’un graphe non orienté est sa représentation sous forme
de schéma, les sommets étant modélisés par des disques et les arêtes par des lignes.
E={{A,B},{B},{B,C},{B,D},{C,D},{E,C}}E={{A,B},{B},{B,C},{B,D},{C,D},{E,C}}
Sa représentation sagittale va donc comporter cinq disques (un pour chacun des
sommets) et six lignes (une pour chacune des arêtes) :
65
A noter qu'un même graphe peut bien sûr avoir plusieurs représentations sagittales.
En voici par exemple une autre du même graphe :
Définitions
66
Les sommets DD et CC sont adjacents car le graphe GG comporte l'arête {C,D}
{C,D}.
L'ordre du graphe GG est égal à 55.
Le graphe GG n'est pas un graphe simple car il comporte une boucle, l'arête {B}
{B}.
Graphes orientés
Comme le lecteur s'y attend, ce qui va différer avec les graphes orientés est que l'on va
imposer une direction sur les liaisons entre sommets.
Définition
Pour définir un graphe orienté, il faut donc commencer par décrire l'ensemble de
ses sommets :
V={A,B,C,D,E}V={A,B,C,D,E}
On peut alors donner l'ensemble de ses arcs :
E={(A,A),(A,B),(B,D),(C,B),(C,D),(D,C),(E,A)}E={(A,A),(A,B),(B,D),(C,B),(C,D),
(D,C),(E,A)}
67
Comme dans le cas des graphes non orientés, une représentation graphique permettra
une meilleure appréhension.
Définition
E={(A,A),(A,B),(B,D),(C,B),(C,D),(D,C),(E,A)}E={(A,A),(A,B),(B,D),(C,B),(C,D),
(D,C),(E,A)}
Sa représentation sagittale va donc comporter cinq disques (un pour chacun des
sommets) et sept flèches (une pour chacun des arcs) :
On définit les notions de boucle, de graphe simple et d’ordre comme dans le cas des
graphes non orientés. Par contre la notion de sommets adjacents n'a plus lieu d'être, il
faut la substituer par un concept prenant en compte l'orientation.
Définition
68
Example 1.6. Illustration des termes techniques
Reprenons le graphe GG des exemples précédents :
Définitions complémentaires
Nous allons terminer cette première partie par quelques définitions
générales concernant les deux types de graphes, orientés ou non.
Graphes complets
Dans un graphe complet tous les sommets sont reliés entre eux.
Définition
Un graphe non orienté est complet s’il est simple et si deux sommets quelconques sont
reliés par une arête.
Un graphe orienté est complet s’il est simple et si pour toute paire de sommets {x,y}
{x,y} il existe au moins un des deux arcs (x,y)(x,y) ou (y,x)(y,x).
69
Example 1.7. Des graphes complets
Voici le graphe complet non orienté d'ordre 55 :
Propriété
Démonstrations
Chacun des nn sommets est adjacent aux n−1n-1 autres. Ce qui donne a
priori n(n−1)n(n-1) arêtes. Mais dans ce calcul chaque arête est comptabilisée deux fois,
une pour chacune de ses extrémités. On a donc bien finalement n(n−1)2n(n-1)2 arêtes.
Q.E.D.
On peut également faire une preuve combinatoire de ce résultat. Une arête est en fait une
combinaison de deux éléments d'un ensemble à nn éléments (l'ensemble des sommets
du graphe). Le graphe étant complet il possèdera autant d'arêtes que le nombre de ces
combinaisons, i.e. (n2)=n(n−1)2(n2)=n(n-1)2. Q.E.D.
Multigraphes et sous-graphes
70
D’après les définitions des deux premières sous-parties, entre deux sommets d’un graphe
on a au plus une arête dans le cas non orienté, et au plus un arc de même sens dans le
cas orienté. Cela provient du fait que dans un ensemble tous les éléments sont différents,
donc en particulier dans celui des arcs ou arêtes il n'y a pas de doublons.
Si l'on autorise le fait d’avoir plusieurs arêtes ou plusieurs arcs de même sens entre deux
sommets, on ne parle plus de graphes mais de multigraphes.
Un multigraphe orienté :
Les définitions suivantes concernent des graphes construits comme sous-parties d'un
graphe existant.
Définitions
71
Example 1.9. Des sous-graphes
Considérons le graphe GG suivant :
72
Graphes planaires
Définition
On n’étudiera pas de façon exhaustive les graphes planaires dans ce cours. C’est une
question assez difficile. On se contentera donc principalement d'exemples.
73
corrigé : l'énigme des trois maisons
Trois maisons doivent être chacune reliées à trois usines d’eau, de gaz et d’électricité. Peut-on disposer les ca
chevauchent pas ?
Il est facile de voir que les maisons, usines et canalisations peuvent être modélisées par ce graphe :
La question peut alors se reformuler comme suit : le graphe précédent est-il planaire ?
74
Chapitre 9 : Généralités sur la complexité
Nous allons dans cette partie introduire la notion de complexité algorithmique, sorte de
quantification de la performance d'un algorithme.
Si nous devons par exemple trier une liste de nombres, est-il préférable d'utiliser un tri fusion ou
un tri à bulles ?
Ce type de question est primordial, car pour des données volumineuses la différence entre les
durées d'exécution de deux algorithmes ayant la même finalité peut être de l'ordre de plusieurs
jours.
Pour faire cela nous chercherons à estimer la quantité de ressources utilisée lors de l'exécution
d'un algorithme.
Les règles que nous utiliserons pour comparer et évaluer les algorithmes devront respecter
certaines contraintes très naturelles. On requerra principalement qu'elles ne soient pas tributaires
des qualités d'une machine ou d'un choix de technologie.
En particulier, cela signifiera que ces règles seront indépendantes des facteurs suivants :
Complexité en temps
75
Pour rendre ce calcul réalisable, on émettra l'hypothèse que toutes les opérations élémentaires sont
à égalité de coût. En pratique ce n'est pas tout à fait exact mais cette approximation est cependant
raisonnable.
On pourra donc estimer que le temps d'exécution de l'algorithme est proportionnel au nombre
d’opérations élémentaires.
Complexité en espace
La complexité en espace est quand à elle la taille de la mémoire nécessaire pour stocker les
différentes structures de données utilisées lors de l'exécution de l'algorithme.
Par exemple, pour un algorithme de tri cette taille sera le nombre de valeurs à trier.
On supposera de plus que nos algorithmes n'ont qu'une donnée, dont la taille est nécessairement
un entier naturel. La complexité en temps d’un algorithme sera donc une fonction
de Nℕ dans R+ℝ+. Nous la noterons en général TT (pour Time).
Souvent la complexité dépendra aussi de la donnée en elle même et pas seulement de sa taille. En
particulier la façon dont sont réparties les différentes valeurs qui la constituent.
Imaginons par exemple que l'on effectue une recherche séquentielle d’un élément dans une liste
non triée. Le principe de l'algorithme est simple, on parcourt un par un les éléments jusqu'à
trouver, ou pas, celui recherché. Ce parcours peut s’arrêter dès le début si le premier élément est
"le bon". Mais on peut également être amené à parcourir la liste en entier si l’élément cherché est
en dernière position, ou même n'y figure pas. Le nombre d'opération élémentaires effectuées
dépend donc non seulement de la taille de la liste, mais également de la répartition de ses
valeurs.
Cette remarque nous conduit à préciser un peu notre définition de la complexité en temps. En
toute rigueur, on devra en effet distinguer trois formes de complexité en temps :
la complexité dans le meilleur des cas : c'est la situation la plus favorable, qui
correspond par exemple à la recherche d'un élément situé à la première postion d'une liste,
ou encore au tri d'une liste déjà triée.
la complexité dans le pire des cas : c'est la situation la plus défavorable, qui correspond
par exemple à la recherche d'un élément dans une liste alors qu'il n'y figure pas, ou encore
au tri par ordre croissant d'une liste triée par ordre décroissant.
la complexité en moyenne : on suppose là que les données sont réparties selon une
certaine loi de probabilités.
76
On calculera le plus souvent la complexité dans le pire des cas, car elle est la plus pertinente. Il
vaut mieux en effet toujours envisager le pire.
Dernière chose importante à prendre en considération, si la donnée en elle même est un nombre
entier, la façon de le représenter influera beaucoup sur l’appréciation de la complexité.
Démonstration
Cette égalité est triviale puisque l'on a nn termes dans cette somme, chacun d'eux étant égal à cc.
Démonstration
77
que n+1∑k=1k=(n+1)×(n+2)2∑k=1n+1k=(n+1)×(n+2)2. Ce qui est bien l'égalité au
rang n+1n+1.
On conclut alors en appliquant le principe de récurrence.
L'auteur ne peut s'empécher une petite digression en proposant une preuve sans mots du résultat précédent :
Pour d'autres démonstrations du même genre, consulter l'excellent livre de Roger B. Nelsen, "Proofs Withou
Démonstration
Cette égalité se démontre également par récurrence, en utilisant les mêmes arguments que lors de
la preuve précédente. Nous laissons le lecteur y réfléchir.
78
Définition
Il existe ainsi une infinité de fonctions logarithmes différentes, autant que de réels strictement
positifs différents de 11. En voici les plus courantes.
Example 1.1. Fonctions logarithmes usuelles
Si a=ea=e, il s'agit du logarithme népérien, noté également lnln.
Si a=2a=2, il s'agit du logarithme binaire.
Si a=10a=10, il s'agit du logarithme décimal.
Règles opératoires
Même si ce n'est bien sûr pas l'objet de ce cours, présentons quelques éléments de preuve de ces
formules. Cela permettra au lecteur de bien se familiariser avec cette fonction.
Démonstration
79
Figure 1.1. Le logarithme binaire et sa fonction réciproque
Avant de commencer, rappelons notre hypothèse de base : toutes les opérations élémentaires
sont à égalité de côut. Cela permet donc d'affirmer que le temps d'exécution est proportionnel au
nombre de ces opérations élémentaires.
Les algorithmes étudiés seront présentés en Python. Comme remarqué précédemment, ce choix
n'influe bien sûr pas sur leur complexité.
80
La fonction suivante convertit un nombre de secondes en heures, minutes, secondes :
def conversion(n):
h = n // 3600
m = (n - 3600*h) // 60
s = n % 60
return h,m,s
Cet algorithme ne comporte pas de structures de contrôle.
Chaque alternative possède une affectation, ainsi le maximum des coûts des différentes
alternatives est de un.
On a donc T(n)=3T(n)=3.
Le cas des structures itératives
Il y a deux possibilités lors du traitement d'une structure itérative.
Si chaque itération ne possède pas le même nombre d'opérations , il faudra alors distinguer ces
itérations, c'est-à-dire évaluer la complexité de chacune d'elle puis en faire la somme.
def sommeEntiers(n):
somme = 0
81
for i in range(n+1):
somme += i
return somme
Ici chaque itération a le même nombre d’opérations, à savoir cinq : deux affections ( i et somme),
deux additions (i et somme) et une comparaison.
Ainsi T(n)=5n+1T(n)=5n+1.
Une autre méthode pour calculer cette somme est d'utiliser une formule explicite.
Example 1.5. Calcul de la somme des nn premiers entiers à l'aide d'une formule explicite
Cette fonction utilise l'une des formules présentées dans la sous-partie 1.4 :
def sommeEntiersBis(n):
return n*(n+1)//2
Cet algorithme ne comporte pas de structures de contrôle, il est juste constitué de trois opérations
arithmétiques.
On a donc T(n)=3T(n)=3.
La fonction suivante recherche l'élément x dans la liste l. Si x appartient à l elle retourne l'indice
de la première occurence de xdans l, sinon elle retourne -1.
Son fonctionnement est simple, les éléments de la liste sont parcourus un par un grâce à une
structure for :
def recherche(l,x):
for i in range(len(l)):
if l[i]==x:
return i
return -1
Ici la complexité sera fonction de la longueur de la liste, que nous noterons nn.
Dans le pire des cas l'élément recherché n'appartient pas à la liste, et il a fallu la parcourir en
entier pour arriver à cette conclusion, c'est-à-dire effectuer nn itérations.
De plus, chaque itération comporte le même nombre d'opérations élémentaires, à savoir une
affectation, une addition et deux comparaisons.
On a donc T(n)=4nT(n)=4n.
L'exemple qui suit contient une imbrication de boucles, il demande donc un peu plus de vigilance.
82
Il consiste dans un premier temps à mettre à la première place le plus petit élément de la liste, puis
à la seconde place le deuxième plus petit élément, etc.
1. Rechercher dans la liste la plus petite valeur et la permuter avec le premier élément de la
liste.
2. Rechercher ensuite la plus petite valeur à partir de la deuxième case et la permuter avec le
second élément de la liste.
3. Et ainsi de suite jusqu’à avoir parcouru toute la liste.
En voici son implémentation en Python :
def triSelection(l):
for i in range(len(l)-1):
indMini=i
for j in range(i+1,len(l)):
if l[j]<l[indMini]:
indMini=j
l[i],l[indMini]=l[indMini],l[i]
Ici aussi la complexité sera fonction de la longueur nn de la liste.
Le pire des cas correspond à une liste triée par ordre décroissant.
Chaque itération de la boucle principale, la plus externe, ne possède pas le même nombre
d'opérations. Il y a toujours les six mêmes (les opérations concernant la variable i, l'initialisation
de indMini et l'échange des valeurs), plus les opérations dues à la boucle la plus interne, qui elles
sont en nombre variable.
La boucle interne a elle par contre le même nombre d'opérations par itération, à savoir cinq.
Le nombre d'itérations de la boucle interne varie d'une itération à l'autre de la boucle externe :
6+5×(n−1)+6+5×(n−2)+...
+6+5×1=n−1∑i=1(6+5×i)=6×(n−1)+5×n−1∑i=1i=6×(n−1)+5×(n−1)×n2=52n2+72n−66+5×(n−
1)+6+5×(n−2)+...+6+5×1=∑i=1n−1(6+5×i)=6×(n−1)+5×∑i=1n−1i=6×(n−1)+5×(n−1)×
n2=52n2+72n−6
Lors de ce calcul, on a utilisé la valeur d'une somme de termes constants et celle de la somme des
premiers entiers (voir sous-partie 1.4).
83
Conclusion, la complexité dans le pire des cas du tri par sélection
est T(n)=2n2+3n−5T(n)=2n2+3n-5.
Notations asymptotiques
Dans cette sous-partie de généralités, on supposera que toutes les fonctions considérées sont
définies sur Nℕ et à valeurs dans R+ℝ+.
Dans notre contexte ce n'est bien sûr pas une contrainte, car les fonctions exprimant une
complexité sont nécessairement positives.
Notion de grand O
Moralement, cela signifie qu'à partir d'un certain rang la fonction ff est majorée par une constante
fois la fonction gg. Il s'agit donc d'une situation de domination de la fonction ff par la
fonction gg.
Figure 1.2. Interprétation graphique de la notion de grand O
84
A partir du rang n0n0, la courbe de ff est au dessous de celle de cc fois gg.
Example 1.8. Quelques relations grand O
Si T(n)=4T(n)=4 alors T(n)=O(1)T(n)=Ο(1). Pour le prouver, prendre par
exemple c=5c=5 et n0=0n0=0.
Si T(n)=3n+2T(n)=3n+2 alors T(n)=O(n)T(n)=Ο(n). Pour le prouver, prendre par
exemple c=4c=4 et n0=2n0=2.
Si T(n)=2n+3T(n)=2n+3 alors T(n)=O(n2)T(n)=Ο(n2). Pour le prouver, prendre par
exemple c=3c=3 et n0=1n0=1.
Cette fois-ci, à partir d'un certain rang la fonction ff est minorée par une constante fois la
fonction gg. Il s'agit donc d'une situation de domination de la fonction gg par la fonction ff.
Figure 1.3. Interprétation graphique de la notion de grand Oméga
85
A partir du rang n0n0, la courbe de ff est au dessus de celle de cc fois gg.
Example 1.9. Quelques relations grand Oméga
Si T(n)=4T(n)=4 alors T(n)=Ω(1)T(n)=Ω(1).
Si T(n)=4n+2T(n)=4n+2 alors T(n)=Ω(n)T(n)=Ω(n).
Si T(n)=4n2+1T(n)=4n2+1 alors T(n)=Ω(n)T(n)=Ω(n).
Borne asymptotique
Cette situation combine les deux précédentes, à partir d'un certain rang la
fonction ff est encadrée par des multiples de la fonction gg. Cela signifie que les
fonctions ff et gg sont du même ordre de grandeur.
f(n)=Θ(g(n))⇔(f(n)=O(g(n))etf(n)=Ω(g(n)))f(n)=Θ(g(n))⇔(f(n)=Ο(g(n))etf(n)=Ω(g(n)))
Figure 1.4. Interprétation graphique de la notion de grand Théta
86
A partir du rang n0n0, la courbe de ff est entre celle de c1c1 fois gg et celle de c2c2 fois gg.
Example 1.10. Quelques relations grand Théta
Si T(n)=4T(n)=4 alors T(n)=Θ(1)T(n)=Θ(1).
Si T(n)=4n+2T(n)=4n+2 alors T(n)=Θ(n)T(n)=Θ(n).
Si T(n)=4n2+1T(n)=4n2+1 alors T(n)=Θ(n2)T(n)=Θ(n2).
Notion d'équivalence
Equivalence
Il faut bien comprendre que cette notion est plus forte que celle de grand Théta, car non
seulement les fonctions sont du même ordre de grandeur mais leur quotient tend vers 11.
Example 1.11. Quelques équivalences
Si f(n)=4n+2f(n)=4n+2 et g(n)=4n−666g(n)=4n-666, alors f(n)∼g(n)f(n)∼g(n).
Si f(n)=4n2−3n+5f(n)=4n2-3n+5 et g(n)=4n2g(n)=4n2, alors f(n)∼g(n)f(n)∼g(n).
Cette première propriété stipule que toutes les fonctions logarithmes sont du même ordre de
grandeur.
87
Soient a,ba,b éléments de Rℝ tels que a>1a>1 et b>1b>1.
Alors, loga(n)=Θ(logb(n))loga(n)=Θ(logb(n)).
Démonstration
D'après la propriété de proportionnalité entre les fonctions logarithmes, voir sous-partie 1.5, on
a loga(n)=logb(n)logb(a)loga(n)=logb(n)logb(a). Avec les notations de la définition de grand
Théta, il suffit alors de poser n0=1n0=1 et c1=c2=1logb(a)c1=c2=1logb(a) pour prouver le
résultat.
La formule suivante, due au mathématicien écossais James Stirling (1692-1770), montre la très
forte vitesse de croissance vers plus l'infini de la fonction factorielle.
On a
n!∼√ 2πn (ne)nn!∼2πn(ne)n
Finissons cette sous-partie avec un résultat bien connu des bacheliers scientifiques.
Croissances comparées
Considérons les
fonctions f1(n)=1f1(n)=1, f2(n)=log(n)f2(n)=log(n), f3(n)=nf3(n)=n, f4(n)=n×log(n)f4(n)=n
×log(n), f5(n)=n2f5(n)=n2, f6(n)=n3f6(n)=n3, f7(n)=2nf7(n)=2n et f3(n)=n!f3(n)=n!.
Elles sont classées de telle sorte que ∀i∈{1,...,7},fi(n)=O(fi+1(n))∀i∈{1,...,7},
fi(n)=O(fi+1(n)).
Autre formulation, chacune de ces fonctions est un grand O de la fonction suivante.
Pour bien fixer les idées sur le comportement de ces fonctions, voici le tracé de leurs courbes.
88
Du "bas vers le haut" on a les fonctions log(x)log(x), xx, x×log(x)x×log(x), x2x2, x3x3, 2x2x.
Classes de complexité
Il est temps maintenant de revenir à notre sujet de départ.
Les complexités algorithmiques que nous allons calculer vont dorénavant être exprimées comme
des grand O ou grand Théta de fonctions de références. Cela va nous permettre de les classer.
Des algorithmes appartenant à une même classe seront alors considérés comme de complexité
équivalente. Cela signifiera que l'on considèrera qu'ils ont la même efficacité.
OΟ Type de complexité
O(1)Ο(1) constant
O(log(n))Ο(log(n)) logarithmique
O(n)Ο(n) linéaire
O(n×log(n))Ο(n×log(n)
quasi-linéaire
)
O(n2)Ο(n2) quadratique
O(n3)Ο(n3) cubique
89
OΟ Type de complexité
O(2n)Ο(2n) exponentiel
O(n!)Ο(n!) factoriel
Classes de complexité
Voici la réinterprétation en terme de classes de complexité de certains calculs déjà effectués :
Le calcul de la somme des nn premiers entiers à l’aide d’une formule explicite est de
complexité constante.
Ce même calcul réalisé de façon itérative est de complexité linéaire.
Le tri par sélection est de complexité quadratique.
Exercice 1
1. Créez une liste avec les n premiers entiers dans l’ordre décroissant.
2. Calculez la moyenne d’une liste.
3. Retournez la liste des carrés d’une autre liste passée en paramètre.
4. Créez une liste contenant des mots. Retournez le mot le plus grand suivant l’ordre
alphanumérique.
5. Retirez le premier élément d’une liste.
6. Retirez le dernier élément d’une liste.
7. Ecrire une fonction qui concatène deux listes.
Exercice 2
Gestion d’une pile FIFO :
- l’ajout d’un élément se fait au sommet de la pile,
- la suppression d’un élément se fait également au sommet de la pile.
Écrire une fonction qui prend en argument une liste triée ll et un entier eltelt et qui renvoie la liste triée obtenue
par insertion à sa place de eltelt dans ll. On fera attention à ce que la liste ll peut être vide.
90
Exercice 6 - Nombre d'occurrences
1. Écrire une fonction maxi(L)maxi(L) prenant en argument une liste d'entiers naturels LL et renvoyant
le maximum des entiers de cette liste (on n'utilisera pas de fonction spécifique déterminant ce
maximum). Quelle est le nombre d'opérations élémentaires effectué par cette fonction en fonction de la
longueur nn de la liste?
2. Écrire une fonction nboc(L)nboc(L) prenant en argument une liste d'entiers naturels LL et retournant
une liste TT de longueur M=maxi(L)+1M=maxi(L)+1 où, pour tout i∈{0,…,M}i∈{0,
…,M}, T[i]T[i] est le nombre d'occurences de ii dans la liste LL.
3. Quel est, en fonction de nn et MM, le nombre d'opérations élémentaires effectué par votre
fonction nboc(L)nboc(L)?
4. On veut que ce nombre ne dépende pas de LL. Modifier votre fonction si ce n'est pas le cas.
def rech_dicho(L,g,d,x)
if x>L(d) Alors
return d+1
else
a=g
b=d
while a!=b Faire
c=(a+b)//2
if x<=L[c]:
b=c
else:
a=c+1
return a
Exercice1
Définir une structure struct noeud_s permettant de coder un nœud d'un arbre binaire contenant une
valeur entière. Ajouter des typedef pour définir les nouveaux types noeud_t et arbre_t (ces types
devraient permettre de représenter une feuille, c'est à dire un arbre vide).
Exercice2
Écrire une fonction cree_arbre() qui prend en argument une valeur entière ainsi que deux arbres et
renvoie un arbre dont la racine contient cette valeur et les deux sous-arbres sont ceux donnés en
paramètre.
Exercice3
Écrire une fonction affiche_arbre2() permettant d'afficher les valeurs des nœuds d'un arbre binaire de
manière à lire la structure de l'arbre. Un nœuds sera affiché ainsi : {g,v,d} où g est le sous-arbre
91
gauche, v la valeur du nœuds et d le sous-arbre droit. Par exemple, l'arbre de la figure 1 sera affiché
par : {{{_,1,_},3,_},4,{{_,6,_},6,{{_,7,_},9,_}}}. Les '_' indiquent les sous-arbres vides.
Exercice4
Écrire une fonction compare() qui compare deux arbres binaires (la fonction renvoie une valeur nulle
si et seulement si les deux arbres binaires ont la même structure d'arbre et qu'ils portent les mêmes
valeurs aux nœuds se correspondant).
Exercice5
Écrire une fonction trouve_noeud() qui renvoie l'adresse d'un nœud de l'ABR donné en paramètre
contenant une certaine valeur (ou NULL si cette valeur ne figure pas dans l'arbre).
Exercice6
Écrire une fonction vérifie () qui renvoie un entier non nul si et seulement si l'arbre binaire passé en
paramètre est un arbre binaire de recherche. Remarque : on pourra écrire une fonction auxiliaire
(récursive) qui vérifie qu'un arbre binaire (non vide) satisfait les propriétés d'ABR et en même temps
détermine les valeurs minimales et maximales contenues dans cette arbre binaire (et les renvoie via
des pointeurs en argument...).
Exercice7
Écrire une fonction supprime() qui supprime une valeur de l'arbre (on supprimera la première
rencontrée) tout en conservant les propriétés d'ABR. L'algorithme est le suivant (une fois trouvé le
nœud contenant la valeur en question) :
si le nœud à enlever ne possède aucun fils, on l'enlève,
si le nœud à enlever n'a qu'un fils, on le remplace par ce fils,
si le noeud à enlever a deux fils, on le remplace par le sommet de plus petite valeur dans le sous-arbre
droit, puis on supprime ce sommet.
92