Vous êtes sur la page 1sur 92

ALGORITHME ET STRUCTURE DE DONNEES

CHAPITRE 1 : Introduction à la programmation

I. Notion de programme
Rappel :

Un ordinateur est une machine électronique programmable servant au traitement de


l’information codée sous forme binaire, c’est-à-dire sous forme de tout ou rien (soit le courant passe,
soit il ne passe pas).

Contrairement à la vision des films de science-fiction, un ordinateur est une machine


totalement dénuée d'intelligence. Il n'est capable de traiter qu’un nombre limité d'instructions. Donc
il ne faut en aucun cas être intimidé par les ordinateurs : ils sont infiniment plus bêtes que vous. Ce
n'est que lorsqu'on réalise vraiment la stupidité des ordinateurs qu'on commence à progresser, car il
faut s'abaisser à son niveau : il faut tout lui dire, car il fait tout au pied de la lettre, sans réfléchir.
Pourtant, contrairement aux autres machines qui sont dédiées à un nombre limité de tâches,
l'ordinateur est potentiellement capable d’effectuer une infinité de tâches concernant le traitement
rationnel de l’information. On dit que c'est une machine universelle.
Alors comment une machine stupide peut traiter autant de problèmes différents ? C'est que,
grâce aux actions de base qu'elle sait réaliser, il est possible en les assemblant de façon pertinente, de
résoudre la plupart des problèmes concernant le traitement de l'information. Il suffit de lui indiquer
l'ordre dans lequel il faut qu'il effectue ces actions basiques et avec quelles données. Ces ordres
élémentaires sont appelés instructions et sont rassemblées au sein d'un programme.
Comme l'ordinateur a l'avantage d'exécuter très rapidement et sans erreurs les ordres
qu'on lui donne (les instructions), il exécute beaucoup de traitements complexes plus vite et plus
sûrement qu'un homme.
Pour donner des ordres à l'ordinateur, il est nécessaire de pouvoir communiquer avec lui. Cette
communication passe par un langage de programmation, dans lequel est écrit le programme.

Un programme est un assemblage et un enchaînement d’instructions élémentaires écrit dans un


langage de programmation, et exécuté par un ordinateur afin de traiter les données d’un
problème et renvoyer un ou plusieurs résultats.

Un algorithme représente l'enchaînement des actions (instructions) nécessaires pour faire


exécuter une tâche à un ordinateur(résoudre un problème)
Un algorithme s'écrit le plus souvent en pseudo-langage de programmation (appelé langage
algorithmique)

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

problème algorithme programme

II. Notion de variables et déclarations


1. Présentation
Les programmes ont pour but de traiter différentes données afin de produire des résultats. Les
résultats peuvent eux-mêmes être des données pour d'autres programmes.

donnée(s) programme résultat(s)

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é

 Attention à ne pas confondre la variable et son contenu


Une variable est un contenant, c'est à dire une sorte de boîte,
alors que le contenu d'une variable est une valeur numérique, alphanumérique ou
booléenne, ou de tout autre type

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. Déclaration des variables

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.

Donc la déclaration d'une variable indique deux choses:


- son identificateur (son nom)
- son type (sa taille)

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

En algorithmique, on distingue 5 types principaux :

- 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

-domaine de valeur incluse dans le du type

3. Les opérateurs de l’algorithmique

Exemple opérations possibles symbole ou mot clé correspondant

3
Type

réel -15.69 , 0.36 addition +


soustraction –
multiplication * (et pas x pour ne pas confondre avec la lettre
division /
exposant ^
pourcentage %
comparaisons <, ≤,>, ≥, =, ≠

entier -10, 3, 689 addition +


soustraction –
multiplication * (et pas x pour ne pas confondre avec la lettre
division DIV cf. ci-après 
modulo MOD cf. ci-après 
exposant ^
pourcentage %

caractère 'B' 'h' '£' '?' '\n' comparaisons <, ≤,>, ≥, =, ≠ cf. ci après 

chaine "Bonjour" "93000" concaténation & cf. ci-après 


"toto@caramail.com" longueur longueur(chaîne)
extraction

booléen VRAI, FAUX comparaison <, ≤,>, ≥, =, ≠


négation NON
conjonction ET
disjonction OU

 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

Les instructions élémentaires les plus courantes sont :


- l'affectation: le fait de donner une nouvelle valeur à une variable - l'affichage sur l'écran
- la saisie à travers le clavier

D'autre instructions permettent de lire et d'écrire sur d'autres périphériques:

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

L’instruction de saisie permet de communiquer des données au programme. Cette instruction


assigne une valeur entrée au clavier dans une variable. Tant que l'utilisateur n'entre rien au clavier,
le déroulement du programme est stoppé.

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

Afficher expression1, [expression2]

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.

Il existe deux grands types de structures de contrôle :


- les structures conditionnelles vont permettre de n'exécuter certaines instructions que
sous certaines conditions
- les structures répétitives, encore appelées boucles, vont permettre de répéter des
instructions un certain nombre de fois, sous certaines conditions
I. Les structures conditionnelles

A. Présentation

Les structures conditionnelles permettent d'exécuter des instructions différentes en fonction de


certaines conditions. Une condition (encore appelée expression conditionnelle ou logique) est évaluée,
c'est à dire qu'elle est jugée vrai ou fausse. Si elle est vraie, un traitement (une ou plusieurs
instructions) est réalisé ; si la condition est fausse, une autre instruction va être exécutée, et ensuite le
programme va continuer normalement.

Il existe 2 types principaux de structures conditionnelles


- les structures alternatives (Si…Alors…Sinon)
- les structures conditionnelles au sens strict (Si…Alors)

La syntaxe générale de cette structure est la suivante :

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.

La syntaxe d'une structure conditionnelle est la suivante:

Si <condition> Alors
<traitement>
Finsi

III. Les structures répétitives ou boucles


Les structures répétitives aussi appelées boucles, permettent de répéter un
traitement ( c'est à dire une instruction simple ou composée) autant de fois qu'il est
nécessaire: soit un nombre déterminé de fois, soit tant qu'une condition est vraie.
Il existe trois grands types principaux de structures répétitives :
- la structure Tant que…Faire, qui permet d'effectuer une instruction tant qu'une
condition est satisfaite
- la structure Pour qui permet de répéter une instruction un certain nombre de fois - la
structure Répéter…Jusqu'à, qui comme son nom l'indique, permet de répéter une instruction
jusqu'à ce qu'une condition soit satisfaite.

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.

A. La boucle Tant que … Faire

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

<traitement> // instruction simple ou bloc d'instructions

Fin Tant que

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

La boucle Pour est en fait une simplification de la boucle Tant Que.

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.

IV. Les différentes structures de contrôle

A. Un extension de la structure Si:

La structure Selon…Faire (de choix)

La structure Selon permet de choisir le traitement à effectuer en fonction de la valeur ou de


l'intervalle de valeur d'une variable ou d'une expression. Cette structure permet de remplacer
avantageusement une succession de structures Si…Alors.

La syntaxe de cette structure est

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

Exemple: Les différents cas sont des valeurs littérales.

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.

Selon mois Faire


1 : Afficher "Janvier"
2 : Afficher "Février"
3 : Afficher "Mars"
4 : Afficher "Avril"

11 : Afficher "Novembre"
12 : Afficher "Décembre"
Sinon Afficher "Un numéro de mois doit être compris entre 1 et 12"
Finselon

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

Jusqu'à condition d'arrêt

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.

Différences entre la boucle Répéter et Tantque

Tant que…Faire Répéter… Jusqu'à


Condition vérifiée avant le traitement : Condition vérifiée après le traitement: le
le traitement peut ne pas être exécuté traitement est forcément exécuté une fois

condition de continuation : condition d'arrêt :le traitement est répété si la


le traitement est répété si la condition est vraie condition est fausse

10
Vrai traitement
test
Faux

traitement test
Faux
Vrai
su ite su ite

CHAPITRE 3 : LES TABLEAUX

I. Introduction aux tableaux à 1 dimension

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

B. Caractéristiques d'un tableau à une dimension


Un tableau à une dimension (ou vecteur) peut être vu comme une liste d'éléments.
On le représente souvent comme une suite de cases contenant chacune une valeur.

ex: Tableau salaire

1ière case 2ième case 3ième case 4ième case … 19ième case 20ième case

10000 8500 12300 13000 6500 9800

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.

les variables d'un tableau

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.

Différence entre variables classiques et variables indicées :


- les variables classiques sont déclarées individuellement et ont un nom distinct ;
- les variables indicées (constituant le tableau) sont implicitement déclarées lors de la
déclaration du tableau. Pour bien montrer que ces variables n’ont pas de signification propre, mais
sont juste « une des variables du tableau », elles ne possèdent pas de nom propre, mais juste un
numéro appelé indice (de 1 à n si le tableau possède n cases). Chaque case (variable) est donc
totalement identifiée par son indice et le nom du tableau.

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

II. Déclaration et manipulation des tableaux à 1 dimension

A. Déclaration
1. Tableau variable
La syntaxe de la déclaration d'une variable tableau est la suivante :

identificateur: tableau [valeur_indice_minimum .. valeur_indice_maximum] de type_des_éléments

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…

L'indice d'un élément peut être:


- directement une valeur ex: salaire [10]
- une variable ex: salaire [i]
- une expression entière ex: salaire [k+1] avec k de type entier

Quelque soit sa forme, la valeur de l'indice doit


être : - entière
- comprise entre les valeurs minimales et maximales déterminées à la déclaration du
tableau. Par exemple, avec le tableau tab [1..20], il est impossible d'écrire tab[0] et tab[21]. Ces
expressions font référence à des éléments qui n'existent pas.

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:

toto: tableau[1..10] d'entiers

Chapitre 4 : Les sous-programmes

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.

Les sous-programmes sont utilisés pour deux raisons essentielles :


- quand un même traitement doit être réalisé plusieurs fois dans un programme (ou qu'il est
utilisé dans plusieurs programmes): on écrit un sous-programme pour ce traitement et on
l'appelle à chaque endroit où l'on en a besoin. On évite ainsi de réécrire plusieurs fois le code
du traitement.
- pour organiser le code, améliorer la conception et la lisibilité des gros programmes. En effet,
le découpage en sous-programmes permet de traiter séparément les difficultés.

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.

Il existe deux sortes de sous-programmes : les procédures et les fonctions


Nous allons étudier d'abord les procédures simples, puis les fonctions. Nous reviendrons ensuite sur les
procédures pour étudier tous les types de passage de paramètres.

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.

Procédure ligneEtoile( ) EN-TETE


Var
i : entier
Début
Pour i de 1 jusqu'à 10 Faire CORPS
Afficher '*'
FinPour Afficher '\n'
FinProc

Cette procédure permet d'afficher une ligne de 10 étoiles puis passe à la ligne.

APPEL d'une procédure

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

Lorsque le processeur rencontre l'appel d'une procédure, il arrête momentanément l'exécution du


programme appelant pour aller exécuter les instructions de la procédure. Quand il a terminé l'exécution
de la procédure, le processeur reprend l'exécution du programme appelant là où il s'était arrêté.

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.

Procédure ligneEtoile(donnée nombre : entier ) //sous-programme


Var
cpt : entier paramètre formel
Début
Pour cpt de 1 jusqu'à nombre Faire
Afficher '*'
FinPour
Afficher '\n'
FinProc

Programme RectangleEtoile //programme appelant


Var
nlignes, netoiles: entier //nombre de lignes et nombre d'étoiles par
ligne i : entier
Début
Afficher "Ce programme dessine un rectangle d'étoiles."
Afficher "Combien voulez-vous d'étoiles par ligne?"
Saisir netoiles
Afficher "Combien voulez-vous de lignes?"
Saisir nlignes
Pour i de 1 jusqu'à nlignes Faire
ligneEtoile(netoiles)
Fin FinPour paramètre effectif

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.

L'appel des fonctions est différent de l'appel des procédures :


L'appel d'une fonction doit obligatoirement se trouver à l'intérieur d'une instruction (affichage,
affectation,…) qui utilise sa valeur.

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

Début //variables locales  instruction obligatoire

//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

Fonction factorielle(n: entier) : entier type de la valeur retournée


/*Cette fonction retourne la factorielle
du nombre n passé en paramètre*/
Var commentaires de spécifications
i : entier
fact : entier
Début
fact  1
Si n ≠ 0 Alors
Pour i de 1jusqu'à n faire
fact  fact * n
FinPour
FinSi
Retourne fact instruction de retour
FinFonction

Fonction saisie_nb_positif( ) : entier


/*Cette fonction permet de faire saisir à l'utilisateur un nombre positif qui est alors retourné*/
Var

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.

LES PROCEDURES (suite et fin)


Comment faire pour qu'un sous-programme communique plusieurs résultats au programme appelant?
C'est impossible avec une fonction qui ne donne qu'un seul résultat, mais c'est possible grâce aux
procédures ayant des paramètres de statut résultat.

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.

Résumé des différents statuts pour les paramètres

• paramètre donnée (appelant  sous-programme) la valeur du paramètre effectif est utilisée


dans la procédure et elle reste inchangée.
• paramètre résultat (sous-programme appelant) la valeur initiale du paramètre effectif est
ignorée par la procédure. La valeur final du paramètre formel résultat est copiée dans le paramètre
effectif correspondant (qui doit obligatoirement être une variable de même type)
• paramètre donnée-résultat ( appelant  sous-programme)
La valeur initiale paramètre effectif est utilisée par la procédure qui modifie cette valeur. Au retour
d'appel, la nouvelle valeur est recopiée dans le paramètre effectif correspondant (qui est
obligatoirement une variable)

Résumé des différences entre fonctions et procédures.

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.

Les procédures peuvent communiquer de 0 à


Les fonctions ne peuvent communiquer qu'un plusieurs résultats au programme appelant à
seul résultat au programme appelant à travers travers des paramètres résultats ou
une valeur de retour (et non à travers un donnéesrésultats. La valeur de ces résultat est
paramètre) affectée aux paramètres effectifs
correspondant (qui doivent obligatoirement
être des variables du programme appelant).

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

Chapitre 5 : les types structurés et les enregistrements

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)

Préalable: déclaration d'un type structuré


Jusqu'à présent, nous n'avons utilisé que des types primitifs (caractères, entiers, réels, chaînes) et des tableaux de
types primitifs. Mais nous pouvons créer nos propres types puis déclarer des variables ou des tableaux d'éléments
de ce type.

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

Manipulation d'un enregistrement


La manipulation d'un enregistrement se fait au travers de ses champs. Comme pour les
tableaux, il n'est pas possible de manipuler un enregistrement globalement, sauf pour affecter un
enregistrement à un autre de même type. Par exemple, pour afficher un enregistrement il faut
afficher tous ses champs uns par uns.

A. Accès aux champs d'un enregistrement

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

nom_enregistrement . nom_champ représente la valeur


mémorisée dans le champ de l'enregistrement

B. Passage d'un enregistrement en paramètre d'un sous-programme

Il est possible de passer tout un enregistrement en paramètre d'une fonction ou d'une


procédure (on n'est pas obligé de passer tous les champs uns à uns, ce qui permet de diminuer le
nombre de paramètres à passer), exactement comme pour les tableaux.

Exemple1 :
Voilà une fonction qui renvoie la différence d'age entre deux personnes

Fonction différence (p1, p2 : tpersonne)


Début
Si pers1.age > pers2.age
Alors Retourne ( pers1.age – pers2.age )
Sinon Retourne ( pers2.age – pers1.age )
FinSi
FinFonct

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.

Procédure majpv (E/S x: produit)


Début
Aff "produit: ", x.lib
Aff "prix de vente hors taxe actuel: ", x.pvht
Aff "Entrez le nouveau prix de vente: "
Saisir x.pvht
Aff "le nouveau prix de vente est: ", x.pvht
FinProc

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 pourrait éventuellement sur-dimensionner


tableau lors de sa déclaration afin d’avoir un
marge de manœuvre, mais cette solution n’es
satisfaisante .

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.

Un maillon sera un objet composé de deux attributs :

 Le premier sera celui de la donnée.


 Le second sera un pointeur vers le maillon suivant.

On verra que l’on peut même avoir éventuellement un troisième a


pointeur vers le maillon précédent si le chaînage est double.

Voici donc l'allure d'une liste chaînée :

22
On dénombre plusieurs avantages à l'utilisation d'une telle structure :

 Il s'agit d'une structure linéaire à accès séquentiel, chaque élément permettant


l'accès au suivant.
 La recherche d’un élément se fait par balayage depuis le premier, il s'agit donc
d'une recherche séquentielle.
 Cette structure est modulable, on peut facilement insérer ou supprimer des
éléments.

Listes simplement chaînées


Il s'agit du cas le plus fréquent de liste chaînée, à partir d'un maillon on ne peut accéder
qu'à son successeur.

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.

Une liste simplement chaînée non circulaire ressemble donc à cela :

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

Pour manipuler les listes, il convient d'implémenter un certain nombre de méthodes de


mise à jour ou de recherche. L'énumération suivante n’est pas exhaustive et varie selon
les besoins :

 Construction d’une liste vide.


 Insertion d’un élément au début de la liste.
 Insertion d’un élément à la fin de la liste.
 Insertion d’un élément à n’importe quelle position de la liste.
 Opérateur d'indexation.
 Suppression d’un élément (par valeur ou par position).
 Recherche d’un élément.
 Modification d’un élément.

Exercice corrigé : complexité des méthodes de la classe liste

Fonctionnement de la méthode d'insertion

Intéressons nous ici au fonctionnement algorithmique de la méthode d'insertion .

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 :

Fonctionnement de la méthode de suppression

Procédons de même pour la méthode de suppression.

Supposons que l'on soit dans ce cas de figure :

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 :

Il ne reste plus alors qu'à détruire le maillon voulu :

Notre opération de suppression est ainsi terminée :

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 :

 Le premier sera celui de la donnée.


 Le second sera un pointeur vers le maillon précédent.
 Le troisième sera un pointeur vers le maillon suivant.
Voici donc l'allure d'une liste doublement chaînée non circulaire :

Pour un chaînage circulaire, le premier maillon pointe vers le dernier et réciproquement :

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.

Illustrons maintenant avec quelques images le fonctionnement d'une pile afin


que le lecteur en saisisse bien son principe.

Considérons par exemple une pile constituée d'un seul élément :

L'insertion d'un élément se fait donc nécessairement à la fin de la pile :

29
Une autre insertion d'élément :

Encore une insertion :

La suppression d’un élément se fait également obligatoirement à la fin de la pile :

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 :

 Construction d’une pile vide.


 Empilage d’un élément.
 Dépilage d’un élément.
 Récupération de l’élément au sommet de la pile sans le dépiler.
Il est temps pour le lecteur d'évaluer la complexité de chacune de ces méthodes.

Exercice corrigé : complexité des méthodes de la classe pile

Pour finir cette partie, citons quelques applications importantes des piles :

 Gestion de certains registres des processeurs (voir à ce sujet le cours


d'architecture des ordinateurs).
 Mémorisation de l’historique dans un navigateur web.
 Logiciels de calculs fonctionnant en notation polonaise inversée

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.

Considérons par exemple une file constituée d'un seul élément :

31
L'insertion d'un élément se fait donc nécessairement à la fin de la file :

Une autre insertion d'élément :

Encore une insertion :

La suppression d’un élément se fait elle obligatoirement au début 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.

Exercice corrigé : complexité des méthodes de la classe file

Chapitre 7 : Les Arbres


1. Notions générales sur les arbres

La structure d'arbre est très utilisée en informatique. Sur le fond on peut


considérer un arbre comme une généralisation d'une liste car les listes
peuvent être représentées par des arbres. La complexité des
algorithmes d’insertion de suppression ou de recherche est généralement plus
faible que dans le cas des listes (cas particulier des arbres équilibrés). Les

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 :

fig-1 fig-2 fig-3

Ci dessus 3 représentations graphiques de la même structure d'arbre, dans la


figure fig-1 tous les sommets ont une disposition équivalente, dans la figure
fig-2 et dans la figure fig-3 le sommet "rouge" se distingue des autres.
Lorsqu'un sommet est distingué par rapport aux autres, on le
dénomme racine et la même structure d'arbre s'appelle une arborescence,
par abus de langage dans tout le reste du document nous utiliserons le
vocable arbre pour une arborescence.

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.

1.1 Vocabulaire employé sur les arbres

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 :

Hauteur, profondeur ou niveau d'un noeud

Nous conviendrons de définir la hauteur(ou profondeur ou niveau ) d'un


noeud X comme égale au nombre de noeuds à partir de la racine pour
aller jusqu'au noeud X. En reprenant l'arbre précédant et en notant h la
fonction hauteur d'un noeud :

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.

Par définition la hauteur de la racine est égal à 1.

35
h(racine) =1 (pour tout arbre non vide)

(Certains auteurs adoptent une autre convention pour calculer la hauteur


d'un noeud: la racine a pour hauteur 0 et donc n'est pa comptée dans le
nombre de noeuds, ce qui donne une hauteur inférieure d'une unité à notre
définition).

Chemin d'un noeud

On appelle chemin du noeud X la suite des noeuds par lesquels il faut passer
pour aller de la racine vers le noeud X :

Chemin du noeud 10 = (1,5,8,10)


Chemin du noeud 9 = (1,5,8,9)
.....
Chemin du noeud 7 = (1,4,7)
Chemin du noeud 5 = (1,5)
Chemin du noeud 1 = (1)

Remarquons que la hauteur h d'un noeud X est égale au nombre de noeuds


dans le chemin :

h( X ) = NbrNoeud( Chemin( X ) ).

Noeuds frères, parents, enfants, ancêtres

Le vocabulaire de lien entre noeuds de niveaux différents et reliés entres eux


est emprunté à la généalogie :

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

5 est le parent de 8 et l'ancêtre de 9 et 10.

On parle aussi d'ascendant, de descendant ou de fils pour évoquer des


relations entres les noeuds d'un même arbre reliés entre eux.

Nous pouvons définir récursivement la hauteur h d'un noeud X à partir de


celle de son parent :

h (racine) = 1;
h ( X ) = 1+ h ( parent ( X ) )

Reprenons l'arbre précédent en exemple :

Calculons récursivement la hauteur du noeud 9, notée h(9) :

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

Degré d'un noeud

Par définition le degré d'un noeud est égal au nombre de ses


descendants (enfants). Soient les deux exemples ci-dessous extraits de
l'arbre précédent :

37
Le noeud 1 est de degré 4, car il a 4 enfants

Le noeud 5 n'ayant qu'un enfant son degré est 1.


Le noeud 8 est de degré 2 car il a 2 enfants.

Remarquons que lorsqu'un arbre a tous ses noeuds de degré 1, on le


nomme arbre dégénéré et que c'est en fait une liste.

Hauteur ou profondeur d'un arbre

Par définition c'est le nombre de noeuds du chemin le plus long dans


l'arbre. La hauteur h d'un arbre correspond donc au nombre de niveau
maximum :

h (Arbre) = max { h ( X ) /  X, X noeud de Arbre }


si Arbre =  alors h( Arbre ) = 0

La hauteur de l'arbre ci-dessous :

38
Degré d'un arbre

Le degré d'un arbre est égal au plus grand des degrés de ses noeuds :

d°(Arbre) = max { d° ( X ) /  X, X noeud de Arbre }

Soit à répertorier dans l'arbre ci-dessous le degré de chacun des 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

La valeur maximale est 4 , donc cet arbre est de degré 4.

Taille d'un arbre

On appelle taille d'un arbre le nombre total de noeuds de cet arbre :

taille(< r , fg , fd >) = 1 + taille( fg ) + taille( fd )

Cet arbre a pour taille 10 (car il a 10 noeuds)

Arbre lexicographique

Rangement de mots par ordre lexical (alphabétique)

39
Soient les mots BON, BONJOUR, BORD, BOND, BOREALE, BIEN, il est
possible de les ranger ainsi dans une structure d'arbre :

Cet arbre se dénomme un arbre lexicographique.

Arbre d'héritage (exemple sur les graphiques)

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

L'arbre abstrait de l'expression a*b + c-(d+e) est un arbre binaire :

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.

Exemple, soit l'arbre binaire A :

A =

Les sous-arbres gauche et droit de l'arbre A :

filsG( A ) = < * , a , b >


filsD( A ) = < - , c , < + , d , e > >

42
2.2 Exemples et implémentation d'arbre binaire étiqueté

Nous proposons de représenter un arbre binaire étiqueté selon deux


spécifications différentes classiques :

1°) Une implantation fondée sur une structure de tableau en allocation de


mémoire statique, nécessitant de connaître au préalable le nombre maximal
de noeuds de l'arbre (ou encore sa taille).

2°) Une implantation fondée sur une structure d'allocation de mémoire


dynamique implémentée soit par des pointeurs (variables dynamiques) soit
par des références (objets) .

2.2.1 - Implantation dans un tableau statique

Spécification concrète

Un noeud est une structure statique contenant 3 éléments :

o l'information du noeud
o le fils gauche
o le fils droit

Pour un arbre binaire de taille = n, chaque noeud de l'arbre binaire est


stocké dans une cellule d'un tableau de dimension 1 à n cellules. Donc
chaque noeud est repéré dans le tableau par un indice (celui de la cellule le
contenant).

Le champ fils gauche du noeud sera l'indice de la cellule contenant le


descendant gauche, et le champ fils droit vaudra l'indice de la cellule
contenant le descendant droit.

43
Exemple

Soit l'arbre binaire ci-après :

Selon l'implantation choisie, par hypothèse de départ, la racine <a, vers b,


vers c> est contenue dans la cellule d'indice 2 du tableau, les autres noeuds
sont supposés être rangés dans les cellules 1, 3,4,5 :

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

Spécification d'implantation en Pascal

Nous proposons d'utiliser les déclarations suivantes :

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 :

Lorsque Tree.ptrRac = 0 on dit que l'arbre est vide.


L'accès à la racine de l'arbre s'effectue ainsi : Tree.table[ptrRac]
L'accès à l'info de la racine de l'arbre s'effectue
ainsi : Tree.table[ptrRac].info
L'accès au fils gauche de la racine de l'arbre s'effectue ainsi :
var
ptr:0..taille ;
ptr := Tree.table[ptrRac].filsG;
Tree.table[ptr] ....

L'insertion ou la suppression d'un noeud dans l'arbre ainsi représenté


s'effectue directement dans une cellule du tableau. Il faudra donc posséder
une structure (de liste, de pile ou de file par exemple) permettant de
connaître les cellules libres ou de ranger une cellule nouvellement libérée.
Une telle structure se dénomme "espace libre".

L'insertion se fera dans la première cellule libre, l'espace libre diminuant


d'une unité.
La suppression rajoutera une nouvelle cellule dans l'espace libre qui
augmentera d'une unité.

45
2.2.2 - Implantation avec des variables dynamiques

Spécification concrète

Le noeud reste une structure statique contenant 3 éléments dont 2 sont


dynamiques :

o l'information du noeud
o une référence vers le fils gauche
o une référence vers le fils droit

Exemple

Soit l'arbre binaire ci-après :

Selon l'implantation choisie, par hypothèse de départ, la référence vers la


racine pointe vers la structure statique (le noeud) < a, ref vers b, ref vers c >

ref racine < a, ref vers b, ref vers c >


ref vers b < b, null, null >
ref vers c  < a, ref vers d, ref vers e >
ref vers d  < d, null, null >
ref vers e  < e, null, null >

Spécification d'implantation en Pascal

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 :

Lorsque Tree = nil on dit que l'arbre est vide.


L'accès à la racine de l'arbre s'effectue ainsi : Tree
L'accès à l'info de la racine de l'arbre s'effectue ainsi : Tree^.info
L'accès au fils gauche de la racine de l'arbre s'effectue ainsi : Tree^.filsG
L'accès au fils gauche de la racine de l'arbre s'effectue ainsi : Tree^.filsD

Nous noterons une simplification notable des écritures dans cette


implantation par rappoprt à l'implantation dans un tableau statique. Ceci
provient du fait que la structure d'arbre est définie récursivement et que
la notion de variable dynamique permet une définition récursive donc plus
proche de la structure.

2.2.3 - Implantation avec une classe

Nous livrons ci-dessous une écriture de la signature et l'implementation


minimale d'une classe d'arbre binaire nommée TreeBin en Delphi
(l'implementation complète est à faire lors des exercices sur les classes) :

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.

2.3 Arbres binaires de recherche

 Nous avons étudié au chap4.6 des algorithmes de recherche en table,


en particulier la recherche dichotomique dans une table triée dont la
recherche s'effectue en O(log(n)) comparaisons.

 Toutefois lorsque le nombre des éléments varie (ajout ou suppression)


ces ajouts ou suppressions peuvent nécessiter des temps en O(n).

 En utilisant une liste chaînée qui approche bien la structure


dynamique (plus gourmande en mémoire qu'un tableau) on aura en
moyenne des temps de suppression ou de recherche au pire de l'ordre
de O(n). L'ajout en fin de liste ou en début de liste demandant un
temps constant noté O(1).

Les arbres binaires de recherche sont un bon compromis pour un


temps équilibré entre ajout, suppression et recherche.

Un arbre binaire de recherche satisfait aux critères suivants :

 L'ensemble des étiquettes est totalement ordonné.


 Une étiquette est dénommée clef.
 Les clefs de tous les noeuds du sous-arbre gauche d'un noeud X,
sont inférieures ou égales à la clef de X.
 Les clefs de tous les noeuds du sous-arbre droit d'un noeud X,
sont supérieures à la clef de X.

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

On appelle arbre binaire dégénéré un arbre binaire dont le degré = 1, ci-


dessous 2 arbres binaires de recherche dégénérés :

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

2.4 Arbres binaires partiellement ordonnés (tas)

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.

1 - Un arbre parfait complet :

(parfait complet : le dernier niveau est complet car il contient tous les
enfants )

2 - Un autre arbre parfait incomplet :

(parfait incomplet : le dernier niveau est incomplet car il manque 3


enfants, mais ils
sont manquant à la droite du niveau, les feuilles sont regroupées à gauche)

3.a - Un arbre non parfait :

(non parfait : le dernier niveau est incomplet car il manque 1 enfant, les

50
feuilles
ne sont pas regroupées à gauche)

3.b - Un autre arbre non parfait :

(non parfait : les feuilles sont bien regroupées à gauche, mais


il manque 1 enfant à l'avant dernier niveau )

Un arbre binaire parfait se représente classiquement dans un tableau :

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 :

Si t est ce tableau, nous avons donc les règles d'accès suivantes :

 t[1] est la racine :

51
Lorsque l'indice i d'une cellule (d'un noeud) est fixé :

 t[i div 2] est le père de t[i] pour i > 1 :

 t[2 * i] et t[2 * i + 1] sont les deux fils, s'ils existent, de t[i] :

 si p est le nombre de noeuds de l'arbre et si 2 * i = p, t[i] n'a qu'un fils,


t[p].

si i est supérieur à p div 2, t[i] est une feuille.

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) :

Cet arbre sera stocké dans un tableau en disposant séquentiellement et de


façon contigüe les noeuds selon la numérotation hiérarchique (l'index de la
cellule = le numéro hiérarchique du noeud).

Dans cette disposition le passage d'un noeud de numéro k (indice dans le


tableau) vers son fils gauche s'effectue par calcul d'indice, le fils gauche se
trouvera dans la cellule d'index 2*k du tableau, son fils droit se trouvant dans
la cellule d'index 2*k + 1 du tableau. Ci-dessous l'arbre précédent est stocké
dans un tableau : le noeud d'indice hiérarchique 1 (la racine) dans la cellule
d'index 1, le noeud d'indice hiérarchique 2 dans la cellule d'index 2, etc...

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

Exemple d'un arbre parfait étiqueté avec des caractères :

53
arbre parfait parcours hiérarchique

rangement de l'arbre
dans un tableau
numérotation hiérarchique

Soit le noeud 'b' de numéro hiérarchique 2


(donc rangé dans la cellule de rang 2 du
tableau), son fils gauche est 'd', son fils
droit est 'e'.

Arbre partiellement ordonné :

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.

Exemple de deux arbres partiellement ordonnés sur l'ensemble


{20,27,29,30,32,38,45,45,50,51,67,85} d'entiers naturels :

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.

En reprenant l'exemple précédent sur 3 niveaux : (entre parenthèses le


numéro hiérarchique du noeud)

Voici réellement ce qui est stocké dans le tableau :(entre parenthèses l'index
de la cellule contenant le noeud)

Le tas :

On appelle tas un tableau représentant un arbre parfait partiellement ordonné.

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 :

L'opération qui consiste à retrouver systématiquement tous


les noeuds d'un arbre et d'y appliquer un même traitement se
dénomme parcours de l'arbre.

Parcours en largeur ou hiérarchique :

Un algorithme classique consiste à explorer chaque noeud


d'un niveau donné de gauche à droite, puis de passer au
niveau suivant. On dénomme cette stratégie le parcours en
largeur de l'arbre.

Exemple (déjà cité ci-haut) :

Algorithme de parcours en largeur (hiérarchique)

Cet algorithme nécessite l'utilisation d'un file du type Fifo


dans laquelle l'on stocke les noeuds.

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 :

La stratégie consiste à descendre le plus profondément


soit jusqu'aux feuilles d'un noeud de l'arbre, puis lorsque
toutes les feuilles du noeud ont été visitées, l'algorithme
"remonte" au noeud plus haut dont les feuilles n'ont pas
encore été visitées.

Notons que ce parcours peut s'effectuer systématiquement en commençant


par le fils gauche, puis en examinant le fils droit ou bien l'inverse.

Parcours en profondeur par la gauche :

Traditionnellement c'est l'exploration fils gauche, puis


ensuite fils droit qui est retenue on dit alors que l'on traverse
l'arbre en "profondeur par la gauche".

Schémas montrant le principe du parcours exhaustif en "profondeur par la


gauche" :

Soit l'arbre binaire suivant:

Appliquons lui la méthode de parcours proposée :

57
Chaque noeud a bien été examiné selon les principes du parcours en
profondeur :

En fait pour ne pas surcharger les schémas arborescents, nous omettons de


dessiner à la fin de chaque noeud de type feuille les deux noeuds enfants
vides qui permettent de reconnaître que le parent est une feuille :

Lorsque la compréhension nécessitera leur dessin nous conseillons au lecteur


de faire figurer explicitement dans son schéma arborescent les noeuds vides
au bout de chaque feuille.

Algorithme général récursif de 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

Les différents traitements Traiter-1 ,Traiter-2 et Traiter-3 consistent à traiter


l'information située dans le noeud actuellement traversé soit lorsque l'on
descend vers le fils gauche ( Traiter-1 ), soit en allant examiner le fils droit
( Traiter-2 ), soit lors de la remonté après examen des 2 fils ( Traiter-3 ).

En fait on n'utilise que trois variantes de cet algorithme, celles qui


constituent des parcours ordonnés de l'arbre en fonction de l'application du

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.

Algorithme de parcours en pré-ordre : (ordre préfixé)

parcourir ( Arbre )
si Arbre  alors
Traiter-1 (info(Arbre.Racine)) ;
parcourir ( Arbre.filsG ) ;
parcourir ( Arbre.filsD ) ;
Fsi

Algorithme de parcours en post-ordre : (ordre postfixé)

parcourir ( Arbre )
si Arbre  alors
parcourir ( Arbre.filsG ) ;
parcourir ( Arbre.filsD ) ;
Traiter-3 (info(Arbre.Racine)) ;
Fsi

Algorithme de parcours en ordre symétrique : (ordre infixé)

parcourir ( Arbre )
si Arbre  alors
parcourir ( Arbre.filsG) ;
Traiter-2 (info(Arbre.Racine)) ;
parcourir ( Arbre.filsD ) ;
Fsi

Illustration pratique d'un parcours général en profondeur

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.

L'assistant d'algorithmes propose des exemples d'arbres de programmation


d'algorithmes simples, en particulier celui de l'équation du second degré.

59
Nous allons voir comment utiliser une telle structure arborescente afin de
restituer du texte algorithmique linéaire.

Voici ce que nous donne l'assistant :

Nous pouvons établir un modèle d'arbre (binaire ici) où les informations au


noeud sont au nombre de 3 (nous les nommerons attribut n°1, attribut
n°2 et attribut n°3). Chaque attribut est une chaîne de caractères, vide s'il y
a lieu.

Nous noterons ainsi un attribut contenant une chaîne vide :

Ci-dessous une représentation de l'arbre de programmation précédent :

60
Traitement des attributs pour produire le texte :

Traiter-1 (Arbre.Racine.Attribut n°1) consiste à écrire le contenu de


l'Attribut n°1 :

si Attribut n°1 non vide alors


ecrire( Attribut n°1 )
Fsi

Traiter-2 (Arbre.Racine.Attribut n°2) consiste à écrire le contenu de


l'Attribut n°2 :

si Attribut n°2 non vide alors


ecrire( Attribut n°2 )
Fsi

Traiter-3 (Arbre.Racine.Attribut n°3) consiste à écrire le contenu de


l'Attribut n°3 :

si Attribut n°3 non vide alors


ecrire( Attribut n°3 )
Fsi

Parcours en profondeur de l'arbre de programmation de l'équation du second


degré :

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

Texte produit après parcours :


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

Rappellons que le symbole représente la chaîne vide il est uniquement


mis dans le texe dans le but de permettre le suivi du parcours de l'arbre.

Pour bien comprendre le parcours aux feuilles de l'arbre précédent, nous


avons fait figurer ci-dessous sur un exemple, les noeuds vides de chaque

62
feuille et le parcours complet associé :

Le parcours partiel ci-haut produit le texte algorithmique suivant (le


symbole est encore écrit pour la compréhension de la traversée) :

si B=0 alors
si C=0 alors
ecrire(R est sol)
sinon


ecrire(pas de sol)
Fsi
sinon

Exercice proposé au lecteur

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.

écrire la chaîne de caractère obtenue par le parcours ainsi défini.

Réponse : abcdfghjkiemnoqrsuv

Terminons cette revue des descriptions algorithmiques des différents


parcours classiques d'arbre binaire avec le parcours en largeur (Cet
algorithme nécessite l'utilisation d'un file du type Fifo dans laquelle l'on
stocke les nœuds).

Algorithme de parcours en largeur


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

Chapitre 8 : Les Graphes


Graphes non orientés
La définition d'un graphe non orienté correspond tout à fait à l'idée que l'on s'en fait
intuitivement : un ensemble de points, dont certains sont reliés par des lignes
parcourables dans les deux sens.

Soyons tout de même un peu plus précis à la fois dans la terminologie et dans
la définition.

Définition

Un graphe non orienté G=(V,E)G=(V,E) est la donnée :


 D’un ensemble VV dont les éléments sont appelés les sommets du graphe.

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.

Donnons de suite un exemple afin de fixer les idées.

Example 1.1. Un graphe non orienté

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}}

Cette définition mathématique d'un graphe est rigoureuse et autosuffisante. Pour


autant, on la complètera souvent par une représentation graphique afin de la rendre
plus parlante et interprétable.

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.

Complétons maintenant le premier exemple.

Example 1.2. Des représentations sagittales


Reprenons le graphe GG de l'exemple précédent. Il est défini par :
V={A,B,C,D,E}V={A,B,C,D,E}
et

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 :

Introduisons un peu de vocabulaire complémentaire.

Définitions

Deux sommets reliés par une arête sont dits adjacents.


L’ordre d’un graphe est le nombre de ses sommets.
Une boucle est une arête ne possédante qu'un seul élément.
Un graphe ne comportant pas de boucles est un graphe simple.

Nous pouvons à présent terminer notre exemple récurrent.

Example 1.3. Illustration des termes techniques


Reprenons le graphe GG des exemples précédents :

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

Un graphe orienté G=(V,E)G=(V,E) est la donnée :


 D’un ensemble VV dont les éléments sont appelés les sommets du graphe.
 D’un ensemble EE dont les éléments sont des couples d'éléments de VV, et sont
appelés les arcs du graphe.

Comme souvent, un petit exemple ne sera pas superflu.

Example 1.4. Un graphe orienté

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

La représentation sagittale d’un graphe orienté est sa représentation sous forme


de schéma, les sommets étant modélisés par desdisques et les arcs par des flèches.

Poursuivons notre exemple.

Example 1.5. Une représentation sagittale


Reprenons le graphe GG de l'exemple précédent. Rappelons qu'il est défini par :
V={A,B,C,D,E}V={A,B,C,D,E}
et

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

En présence d'un arc de la forme (x,y)(x,y), on dit que yy est un successeur de xx et


que xx est un prédécesseur de yy.
On dit également que xx est l’origine de l’arc (x,y)(x,y) et que yy est son extrémité.

Revenons sur notre exemple et terminons le.

68
Example 1.6. Illustration des termes techniques
Reprenons le graphe GG des exemples précédents :

 Le graphe GG possède l'arc (B,D)(B,D) donc BB est un prédécesseur


de DD et DD un successeur de BB.
 Le sommet EE est l'origine de l'arc (E,A)(E,A) et AA son extrémité.
 L'ordre du graphe GG est égal à 55.
 Le graphe GG n'est pas un graphe simple car il comporte une boucle, l'arc (A,A)
(A,A).

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 :

Voici un graphe complet orienté d'ordre 44 :

Vu sa structure bien particulière, il est facile de dénombrer le nombre d'arêtes d'un


graphe complet non orienté.

Propriété

Un graphe non orienté complet d'ordre nn possède n(n−1)2n(n-1)2 arêtes.

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.

Example 1.8. Des multigraphes

Un multigraphe non orienté :

Un multigraphe orienté :

Les définitions suivantes concernent des graphes construits comme sous-parties d'un
graphe existant.

Définitions

Soit G=(V,E)G=(V,E) un graphe orienté ou non.


Un graphe G′=(V′,E′)G′=(V′,E′) est un sous-graphe de GG si V′⊆VV′⊆V et E′⊆EE′⊆E.
Un sous-graphe recouvrant de GG est un sous-graphe de la forme G′=(V,E′)G′=(V,E
′), i.e. un sous-graphe de GG possédant tous les sommets de GG.
Un sous-graphe induit de GG est un sous graphe G′=(V′,E′)G′=(V′,E′) dont les arêtes
ou arcs sont toutes celles de GG ayant leurs extrémités dans V′V′.

Illustrons tout cela par un exemple.

71
Example 1.9. Des sous-graphes
Considérons le graphe GG suivant :

Voici un sous-graphe de GG, constitué donc de certains sommets et arcs de GG :

Le sous-graphe suivant de GG ne comporte que trois sommets B,C,DB,C,D. Il


est induit, car il contient tous les arcs de GG ayant B,CB,C ou DD pour extrémités :

Le sous-graphe ci-dessous de G est recouvrant car il possède tous les sommets


de GG :

72
Graphes planaires

Concluons cette partie par la notion de graphe planaire.

Définition

Soit GG un graphe orienté ou non.


On dira que GG est planaire s’il admet une représentation sagittale où ses arêtes (ou
arcs) ne se coupent pas.

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.

Example 1.10. Un graphe planaire


Considérons le graphe GG suivant :

Ce graphe est planaire car il admet aussi cette représentation sagittale :

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 ?

Ci-dessous figure l'illustration originale de l'auteur du problème :

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 ?

Nous laissons le lecteur y réfléchir de façon intuitive, sans considérations mathématiques.

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.

But d'un calcul de complexité


L'objectif premier d'un calcul de complexité algorithmique est de pouvoir comparer l’efficacité
d’algorithmes résolvant le même problème. Dans une situation donnée, cela permet donc d'établir
lequel des algorithmes disponibles est le plus optimal.

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 :

 du langage de programmation utilisé pour l'implémentation.


 du processeur de l'ordinateur sur lequel sera exécuté le code.
 de l'éventuel compilateur employé.
Nous allons donc effectuer des calculs sur l’algorithme en lui même, dans sa version "papier".
Les résultats de ces calculs fourniront une estimation du temps d’exécution de l’algorithme, et de
la taille mémoire occupée lors de son fonctionnement.

Les deux types de complexité


On distinguera deux sortes de complexité, selon que l'on s'intéresse au temps d'exécution ou à
l'espace mémoire occupé.

Complexité en temps

Réaliser un calcul de complexité en temps revient à décompter le nombre d’opérations


élémentaires (affectation, calcul arithmétique ou logique, comparaison…) effectuées par
l’algorithme.

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.

De quoi est fonction la complexité ?


La complexité d'un algorithme va naturellement être fonction de la taille des données passées en
paramètres. Cette dépendance est logique, plus ces données seront volumineuses, plus il faudra
d'opérations élémentaires pour les traiter.

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

Par exemple, si n=4096n=4096 on peut considérer que la taille de nn est :


 la valeur de nn en elle même, façon la plus naturelle de voir les choses, i.e. 40964096
 le nombre de chiffres que comporte l'écriture en binaire de nn, i.e. 1313
 le nombre de chiffres que comporte l'écriture en décimal de nn, i.e. 44
Vu la finalité informatique de nos algorithmes, nous choisirons souvent dans ces cas là le nombre
de chiffres dans l'écriture binaire de l'entier nn.
Quelques calculs de sommes usuelles
Nous allons dans cette sous-partie énoncer et démontrer quelques égalités bien connues relatives à
certaines sommes. Elles nous seront fort utiles lors de nos calculs de complexités.

Somme de n termes constants

Soit nn élément de N∗ℕ∗ et cc élément de Rℝ.


On a
n∑k=1c=n×c∑k=1nc=n×c

Démonstration

Cette égalité est triviale puisque l'on a nn termes dans cette somme, chacun d'eux étant égal à cc.

Somme des n premiers entiers

Soit nn élément de N∗ℕ∗.


On a
n∑k=1k=n×(n+1)2∑k=1nk=n×(n+1)2

Démonstration

Effectuons la preuve de ce résultat par récurrence :

 Initialisation : pour n=1n=1, on a 1∑k=1k=1∑k=11k=1 qui est bien égal


à 1×(1+1)2=11×(1+1)2=1.
 Hérédité : supposons que pour un n≥1n≥1 l'égalité soit vérifiée. Calculer une somme avec
un indice allant de 11 à n+1n+1 revient à calculer cette même somme avec un indice
allant de 11 à nn, puis à rajouter le terme d'indice n+1n+1. On a
ainsi n+1∑k=1k=n∑k=1k+n+1∑k=1n+1k=∑k=1nk+n+1, et donc d'après l'hypothèse de
récurrence n+1∑k=1k=n×(n+1)2+n+1∑k=1n+1k=n×(n+1)2+n+1. Une simple mise au
même dénominateur et une petite factorisation prouvent ensuite

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

Somme des n premiers carrés d'entiers

Soit nn élément de N∗ℕ∗.


On a
n∑k=1k2=n×(n+1)×(2n+1)6∑k=1nk2=n×(n+1)×(2n+1)6

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.

Somme des n premiers termes d'une suite géométrique

Soit nn élément de N∗ℕ∗ et qq élément de Rℝ.


Si q≠1q≠1, on a
n∑k=0qk=1−qn+11−q∑k=0nqk=1−qn+11−q

A noter que si q=1q=1, on retrouve la somme de termes constants.


Démonstration

Là aussi une simple récurrence prouve ce résultat.

Rappels sur la fonction logarithme


La fonction logarithme jouant un rôle important dans la suite de ce cours, nous allons consacrer
cette sous-partie à en rappeler sa définition et ses propriétés classiques.

78
Définition

Soit aa élement de R∗+ℝ+* tel que a≠1a≠1.


Le logarithme de base aa, noté logaloga, est l'unique fonction définie sur R∗+ℝ+* vérifiant les
deux propriétés suivantes :
1. loga(a)=1loga(a)=1.
2. ∀x,y>0, loga(xy)=loga(x)+loga(y)∀x,y>0, loga(xy)=loga(x)+loga(y).

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.

Présentons maintenant les principales formules de calculs de la fonction logarithme.

Règles opératoires

Soit aa élement de R∗+ℝ+* tel que a≠1a≠1.


1. loga(1)=0loga(1)=0.
2. ∀x>0,loga(1x)=−loga(x)∀x>0,loga(1x)=-loga(x).
3. ∀x,y>0,loga(xy)=loga(x)−loga(y)∀x,y>0,loga(xy)=loga(x)-loga(y).
4. ∀x>0,∀n∈N,loga(xn)=n×loga(x)∀x>0,∀n∈ℕ,loga(xn)=n×loga(x).

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

1. Si l'on applique l'égalité définissant le logarithme avec x=1x=1 et y=1y=1, on


obtient loga(1)=2×loga(1)loga(1)=2×loga(1). Ce qui prouve bien sûr
que loga(1)=0loga(1)=0.
2. Appliquons cette même égalité avec cette fois xx et 1x1x. Il vient loga(1)=loga(x)
+loga(1x)loga(1)=loga(x)+loga(1x), ce qui prouve le résultat.
3. Utilisons maintenant l'égalité avec xx et 1y1y. On a alors loga(xy)=loga(x)
+loga(1y)loga(xy)=loga(x)+loga(1y). Il ne reste alors plus qu'à utiliser la seconde
formule pour conclure.
4. Cette dernière égalité se prouve sans soucis par récurrence en utilisant la définition du
logarithme.

Présentons la représentation graphique d'une fonction logarithme et de sa fonction réciproque. Nul


doute que le lecteur connaît déjà l'allure de ces courbes.

79
Figure 1.1. Le logarithme binaire et sa fonction réciproque

En rouge la courbe du logarithme binaire et en vert celle de sa fonction


réciproque, l'exponentielle binaire. Ces deux courbes sont symétriques par rapport à la droite
tracée en bleu d'équation y=xy=x.

Premiers calculs de complexité : algorithmes itératifs


Nous allons dans cette partie effectuer nos premiers calculs de complexité. Nous ne traiteront ici
que le cas des algorithmes itératifs, ceux récursifs seront étudiés dans le second chapitre de ce
cours.

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

Algorithmes sans structures de contrôle


Pour mémoire, une structure de contrôle est une structure itérative ou une structure
conditionnelle. Si un algorithme n'en comporte pas, pour évaluer sa complexité il suffit juste
de dénombrer le nombre d’opérations successives qu'il possède.

Example 1.2. Une fonction de conversion

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.

On peut dénombrer cinq opérations arithmétiques et trois affectations. On a donc T(n)=8T(n)=8.


Le cas des structures conditionnelles
En présence d'une structure conditionnelle, il faut commencer par dénombrer le nombre de
conditions du test.

On décompte ensuite le nombre d’opérations élémentaires de chacune des alternatives, et l'on


prend le maximum de ce décompte afin d'obtenir la complexité dans le pire des cas.

Example 1.3. Un calcul de puissance


La fonction suivante calcule (−1)n(-1)n sans effectuer de produit mais en utilisant un test avec
une alternative :
def puissanceMoinsUn(n):
if n%2==0:
res = 1
else:
res = -1
return res
Le test de la conditionnelle comporte une opération arithmétique et une comparaison.

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 comporte le même nombre d'opérations élémentaires , pour évaluer la


complexité il suffit de multiplier le nombre d'itérations par le nombre d'opérations de chacune
d'elles.

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.

Example 1.4. Calcul itératif de la somme des nn premiers entiers


Cette fonction utilise une structure for pour calculer la somme des nn premiers entiers :

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.

On a d'autre part une affectation, lors de l'initialisation de la variable somme.

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.

Premiers exemples un peu plus élaborés


On va dans cette sous-partie se pencher sur des situations un peu plus délicates avec le calcul de la
complexité des algorithmes derecherche séquentielle et du tri par sélection.

Example 1.6. Recherche séquentielle d'un élément dans une liste

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.

Example 1.7. Tri par sélection

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.

Sa description est la suivante :

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 :

 à la première itération de la boucle externe la variable i vaut 00 et la boucle interne


effectue donc n−1n-1 itérations.
 à la seconde itération de la boucle externe la variable i vaut 11 et la boucle interne
effectue donc n−2n-2 itérations.
 etc.
La complexité de cet algorithme sera alors égale à la somme du nombre d'opérations de chaque
itération de la boucle externe. A savoir

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.

Comportement asymptotique des fonctions de référence


Le but de cette partie va être de comparer les complexités calculées avec des fonctions de
référence (puissance, logarithme, exponentielle, etc.). Il faudra préalablement introduire quelques
notations classiques des études de fonctions.

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.

Les définitions suivantes permettent de comparer le comportement à l'infini de deux fonctions


définies sur Nℕ. Plus précisément, il s'agit de critères pour affirmer qu'une fonction
en domine une autre, ou au contraire est du même ordre de grandeur, voir même équivalente.

Notion de grand O

Borne supérieure asymptotique

On dit qu’une fonction ff est un grand O d’une fonction gg si et seulement si


∃c>0, ∃n0>0 tel que ∀n>n0, f(n)<c×g(n)∃c>0, ∃n0>0 tel que ∀n>n0, f(n)<c×g(n)
On note alors f(n)=O(g(n))f(n)=Ο(g(n)).

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.

Notion de grand Oméga


Borne inférieure asymptotique

On dit qu’une fonction ff est un grand Oméga d’une fonction gg si et seulement si


∃c>0, ∃n0>0 tel que ∀n>n0, c×g(n)<f(n)∃c>0, ∃n0>0 tel que ∀n>n0, c×g(n)<f(n)
On note alors f(n)=Ω(g(n))f(n)=Ω(g(n)).

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

Notion de grand Théta

Borne asymptotique

On dit qu’une fonction ff est un grand Théta d’une fonction gg si et seulement si


∃c1>0, ∃c2>0, ∃n0>0 tel que ∀n>n0, c1×g(n)<f(n)<c2×g(n)∃c1>0, ∃c2>0, ∃n0>0 tel
que ∀n>n0, c1×g(n)<f(n)<c2×g(n)
On note alors f(n)=Θ(g(n))f(n)=Θ(g(n)).

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.

Il est facile de voir que

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

On dit qu’une fonction ff est équivalente à une fonction gg si et seulement si


limn→∞f(n)g(n)=1limn→∞f(n)g(n)=1
On note alors f(n)∼g(n)f(n)∼g(n).

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

Croissance des fonctions de référence


Dans cette sous-partie nous allons énoncer quelques résultats permettant de comparer entre elles
les fonctions de référence, à savoir les fonctions logarithme, exponentielle et puissance.

Cette première propriété stipule que toutes les fonctions logarithmes sont du même ordre de
grandeur.

Comparaison des fonctions logarithmes

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.

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

Figure 1.5. Représentation graphique des fonctions de référence

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

Le tableau suivant récapitule les complexités de référence :

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.

Annexes : Les exercices


Annexe 1 : Exercices sur les manipulations de listes chaînées

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.

Ecrire les fonctions suivantes :

 - InitialiserPile () qui initialise une liste vide


 - PileVide() qui retourne vrai si la liste est vide
 - Empiler() qui permet d’ajouter un élément en tête de la liste
 - Depiler() qui enlève un élément en tête de la liste

Exercice 3 - Insertion dans une liste triée

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

Exercice 4 - Recherche du premier élément d'une liste


Écrire une fonction prenant en argument une liste Liste et une variable x, et qui retourne le plus petit indice k de
la liste tel que Liste[k] soit égal à x. Si la liste ne contient pas x, alors la fonction doit retourner -1

Exercice 5 - Fusion de deux listes


Écrire une fonction fusion qui prend en argument deux listes triées L1 et L2 et qui renvoie une seule liste triée
contenant les éléments de L1 et L2.

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.

Exercice 7 - Recherche par dichotomie dans une liste triée

On propose l'algorithme suivant :

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

1. On prend L=[2,4,5,7,7,8,10] Que renvoient les instructions suivantes?


 rech_dicho(L,1,5,6)
 rech_dicho(L,0,5,1)
2. Que fait la fonction rech_dicho?
3. Quelle est la complexité de cette fonction, mesurée en nombre de comparaisons.

Annexe 2 : Exercices sur les arbres

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

Vous aimerez peut-être aussi