Vous êtes sur la page 1sur 180

Algorithmique et structures de données

1
● Année Académique:
2014/2015
● Parcours
INFOTEL
● Enseignant
Douwé Hallam Vincent

1
Informations générales

● Organisation du cours
CM: 12h
TD: 8h
TP: 10h
● Organisation des évaluations
1 CC de 2h
1 TPE: une semaine
Examen final 3h
Rattrapage 3h 2
Objectifs du cours

● Approfondir les connaissances sur


l'algorithmique
● Présenter les éléments de la
programmation avancée

3
Plan du cours

● Rappels des notions de base de


l'algorithmique
● Les pointeurs et la programmation
dynamique
● Les types de données abstraits
● La récursivité

4
Bibliographie

● Kolyang. Elements d'algorithmique,


Editions Ka'arang, 2004
● T. H. Cormen, C. E. Leiserson, R. L. Rivest.
Introduction to algorithms second editions,
MIT Press, 2000
● A. V. Aho, J. E. Hopcroft, J. D. Ullman.
Data Structures and Algorithms, Addison-
Wesley
5
● C. Delannoy, Programmation en C
RAPPELS SUR
L'ALGORITHMIQUE

6
Algorithmique - Définition

L'algorithmique est une branche de


l'informatique pratique qui étudie les
algorithmes. Elle se concentre sur la
conception, la complexité et l'efficacité des
algorithmes.
La conception se concentre sur la description
des données d'un problème et la description
des méthodes de résolution.
La complexité s'intéresse au temps de calcul
et à l'occupation mémoire
7
Algorithmique - objectifs

L'un des objectifs de l'algorithmique est


l'étude des méthodes de résolution des
problèmes complexes en utilisant un nombre
limité de structures.
La programmation est la réalisation sur
machine des algorithmes.
Les programmes sont une suite
d'instructions. Ces instructions utilisent un
nombre limité de structures.
Nous présentons ici ces différentes
8
structures.
Les variables

Les variables servent à stocker les valeurs.


Il s'agit en réalité d'une valeur à laquelle on a
attribué un nom.
X=5
X= 3*Y+5

9
Opérations sur les variables

Il existe deux opérations fondamentales :


1. La Déclaration : Chaque variable a un type de
données. La déclaration est donc une
opération qui introduit une nouvelle variable
dans l'environnement d'un programme en
spécifiant son type.
int resultat ;
int[5] resultats ;
10
Opérations sur les variables

2. L'affectation : Il s'agit d'un opération qui


permet de modifier la valeur contenu dans une
variable. Pour que cette opération soit valable,
il faut que le type de l'expression corresponde
au type de données que la variable peut
contenir.
int x ;
x= 2 ;
11
La séquence

La séquence est une opération qui permet


d'exécuter un ensemble d'instructions les unes
après les autres. En C (et beaucoup d'autres
langages de programmation) elle est
matérialisée par ;. Il s'agit d'une opération qui
permet de donner une ordre dans l'exécution
des instructions.
int superficie ;
superficie =2*rayon ;
12
La sélection

La sélection est une opération qui permet de


faire un choix.
On distingue généralement :
• L'alternative : on fait un choix parmi deux

options
if(x>2)
printf('x est supérieur à 2') ;
else
printf('x est inférieur à 2') ;
13
La sélection

• Le choix multiple. On fait un choix parmi


plusieurs options. Il s'agit d'imbriquer
plusieurs alternatives
if (balance==0)
printf('le compte est vide')
else if (x<0)
printf('le compte est déficitaire') ;
else
printf('le compte est créditaire') ;
14
Les itérations

Les itérations permettent d'exécuter une ou


plusieurs instructions :
• Un nombre connu de fois :

for (int index=0 ; index<5 ; index++)


printf('Bonjour') ;
• Tant qu'une condition est vérifiée

while (x>0)
x-- ;
15
Les itérations

• Jusqu'à ce qu'une condition soit vérifiée


do
x-- ;
while(x<0) ;

16
L'utilisation des sous-programmes

Les sous programmes sont des bouts de code


auxquels l'on a attribué un nom. Ils permettent
d'éviter d'écrire des codes similaires à plusieurs
endroits dans le code du programme.
Ils participent à rendre un code modulaire.
On distingue deux opérations avec les sous-
programmes.

17
La déclaration de sous-programme

Un sous-programme est constitué d'une entête


(prototype) et d'un corps.
●Le prototype d'une fonction est constitué du

nom de la fonction, de l'ensemble des


arguments et du type du résultat. Il définit la
forme que peut prendre le sous-programme.
int carre(int entree) ;

18
Déclaration de sous-programme
• Le corps du sous-programme est constitué des
déclarations locales(variables qui n'ont de
signification qu'à l'intérieur du sous-
programme) et du code correspondant au
sous-programme.
int carre (int entree){
int resultat ;
resultat=entree*entree ;
return resultat ;
}
19
Appel de sous-programme

Une fois le sous-programme déclaré, on peut


l'appeler dans n'importe quelle portion du
programme en utilisant son nom et en
fournissant les différents arguments ; L'appel
d'un sous-programme peut produire un résultat
ou changer simplement l'environnement.
carre(4) ;

20
Paramètres des sous-programmes
• Les variables utilisées comme arguments
lors de la déclaration d'un sous-programme
sont appelées paramètres formels.
• Les variables utilisées lors de l'appel du
sous-programme sont appelées paramètres
effectifs.
• Les variables déclarées à l'intérieur d'un
sous-programme sont appelées des
variables locales. 21
Passage des paramètres
Il existe trois façons de passer les paramètres
actuels aux sous-programmes :
• Passage par valeur. Le paramètre effectif

est évalué et son résultat utilisé dans le


corps du sous-programme.
• Passage par adresse. L'adresse du
paramètre effectif est passé au sous-
programme.
• Passage par nom. Le paramètre effectif est

substitué littéralement22 et aucune évaluation


n'est faite.
Les types de données

Les types de données permettent de décrire la


forme des valeurs que peuvent prendre des
variables. Ils correspondent aux types en
mathématique.
Dans les langages de programmation, il existe
plusieurs types de données

23
Les types de base

Nous distinguons généralement :


• Le type des caractères (char). Les éléments
de ce type sont les caractères.
• Le type des entiers (int, long, short) qui
correspondent aux entiers naturels
• Le type des nombres à virgule flottante (float,

double, long double)


• Le type pour les valeur de vérité vrai et faux

24
Les types composés

Les types de bases ne suffisent pas en général


pour décrire toutes les variables d'un
programme. On a ainsi besoin des types
composés

25
Le type énuméré

Il permet de créer des sous-ensemble des


entiers.

enum MOIS={Janvier, Fevrier,Mars, Avril,


Mai, Juin, Juillet, Aout, Septembre, Octobre,
Novembre, Decembre}

26
Les tableaux

Les tableaux sont des collections d'éléments de


même type désignés par un identificateur unique.
Chaque élément est repéré par un indice précisant
sa position au sein de l'ensemble.
int[5] resultat;
On peut aussi des tableaux à plusieurs dimensions.
int[5][5] matrice;

27
Les enregistrements

Un enregistrement permet de définir une structure


de données dont les composantes peuvent être de
types différents
struct matiere {
char nom[10];
int coefficient;
}
Les enregistrements sont créés en C en utilisant le
mot clé struct 28
Les enregistrements

L'opérateur . Est utilisé pour accèder aux différentes


composantes d'un enregistrement.
matiere algorithmique;
algorithmique.nom='Algorithmique';
algorithmique.coefficient=3;

29
LES POINTEURS ET
L'ALLOCATION DYNAMIQUE DE
LA MÉMOIRE

30
Introduction

«... Les pointeurs étaient mis dans le même sac que


l'instruction goto comme une excellente technique de
formuler des programmes incompréhensibles. Ceci est
certainement vrai si les pointeurs sont employés
négligemment, et on peut facilement créer des
pointeurs qui pointent 'n'importe où'. Avec une
certaine discipline, les pointeurs peuvent aussi être
utilisés pour programmer de façon claire et simple.
C'est précisément cet aspect que nous voulons faire
ressortir dans la suite. ... » 
Kernighan & Ritchie dans leur livre 'Programming in C'
31
Introduction

Les pointeurs sont tellement utilisé en C, parce


que d'une part il s'agit souvent du seul moyen
pour exprimer un calcul et d'autre part ils
fournissent un moyen d'avoir des codes plus
compacts et plus efficaces.
Malheureusement les pointeurs (lorsque mal
implémentés) sont à l'origine de nombreux bugs
dans les programmes et du fameux message
«segmentation fault (Erreur de segmentation)»
32
Adressage et variables

La mémoire d'un ordinateur est constituée d'un


ensemble contiguë de cellules appelées mots.
Les mots peuvent être manipulés
individuellement ou en groupe.
Une variable est un nom donné à un mot ou à
un groupe de mot (dépendant du type de la
variable).
Chaque mot à une adresse unique sur une
longueur définie. 33
Les pointeurs

Les pointeurs sont des variables qui


contiennent les adresses d'autres variables.
L'opérateur unaire & permet de connaître
l'adresse d'une variable. Ainsi :
int x ;
&x donne l'adresse de la variable x.
L'opérateur & s'applique uniquement aux
valeurs en mémoire. Il ne peut donc être
appliqué à une expression
34
ou une constante.
Les pointeurs

L'opérateur unaire * est un opérateur


d'indirection ou de contenu.
Quand il est appliqué à un pointeur, il produit en
résultat la valeur pointé par le pointeur.
Cet opérateur permet également de déclarer
une variable comme étant de type pointeur.

35
Les pointeurs

La déclaration des pointeurs se fait de la


manière suivante:
type *identificateur;
Exemple:
int *var;
On dit que var est une variable(pointeur) qui
pointe sur les valeurs de type int ou encore var
est une variable qui peut contenir l'adresse
d'une variable de type 36int.
Les pointeurs

Exemple:
int x=1; /* x est de type int */
int y=2; /* y est de type int */
int *ip; /* ip est de type int * */
ip=&x; /* ip pointe sur x */
y=*ip ; /* y contient la valeur pointée par ip */
*ip=0 ; /* la valeur pointée par ip vaut 0 */
x=?? y= ??
37
Les pointeurs

Les pointeurs peuvent être utilisés directement


sans être déréferencés.
Ainsi on a le droit de faire ip=iq si ip et iq sont
deux pointeurs de même type.
Il existe une valeur qui peut être prise par
n'importe quel type de pointeur. Il s'agit du
pointeur nul (valeur 0) pour indiquer qu'un
pointeur pointe nulle part.
38
Conversion des pointeurs

Il n'existe aucune conversion implicite d'un type


de pointeur dans un autre.
Il est toujours possible de faire appel à
l'opérateur de cast.

39
Le pointeur générique

Le type void * joue un rôle particulier. Il définit


un type de pointeur générique. Ce type de
pointeur est compatible avec tous les autres
types de pointeurs. Un pointeur de ce type peut
pointer vers n'importe quel type. Il s'agit d'un
type à part entière (et non pas d'un pointeur
vers quelque chose de type void, ce qui n'existe
pas)
40
Le pointeur générique

Ce type de pointeur joue un rôle central dans le


prototypage des fonctions pour désigner:
●Le type retourné de fonctions renvoyant des

pointeurs sur des zones de données structurées


librement par les applications.
●Le type de pointeurs transmis en paramètres

(si tout type de pointeur est acceptable)

41
Le pointeur générique

Une variable de type void * ne peut pas


intervenir dans des opérations arithmétiques;
Notamment si p et q sont de type void *, on ne
peut pas parler de p+i ou p-q

42
Priorité des opérateurs

Les opérateurs & et * ont la même priorité que


les autres opérateurs unaires. Ils sont aussi
associatifs à gauche.

43
Priorité des opérateurs.

Si ip pointe sur la valeur x, alors *ip peut être


utilisé n'importe où x peut être utilisé.
Exemple:
Si on a: int x=10 ;int *ip=&x;
*ip=*ip+10; est équivalent à x=x+10;
int y=*ip +1; équivalent à int y=x+1;
*ip+=1; est équivalent à x+=1;
++*ip; est équivalent à ++x;
(*ip)++ est équivalent à x++;
44
Pointeurs et les fonctions
En C, par défaut les paramètres sont passés
par valeur aux fonctions.
Que si la fonction doit changer l'un de ses
arguments.
Exemple: Échanger le contenu de deux
variables.
void swap(int a, int b){
int c=a;
a=b;
b=c;
} 45
Ce code ne marche pas
Pointeurs et les fonctions

La solution consiste donc à passer les


paramètres par adresses.
Exemple:
int n;
scanf("%d",&n);
Ainsi, si on a:
int x=10, y=20;
On doit écrire
swap(&x,&y); /* ERREUR car &x n'est pas de type
int */ 46
Pointeurs et les fonctions

On doit donc changer la signature de swap. Les


valeurs des paramètres doivent être passées
indirectement.
void swap(int *x, int *y){
int c;
c=*x;
*x=*y;
*y=c ;
}
47
Pointeurs et les fonctions

Les arguments passés par adresse permettent


aux fonctions de modifier leur valeur. Ce mode
de passage est très important quand la fonction
abstrait un traitement (au lieu de calculer une
valeur).

48
Pointeurs et tableaux

En C, il y a une forte relation entre les


pointeurs et les tableaux. Toutes les opérations
faites à travers les tableaux peuvent être faites
à travers les tableaux. Les versions faites au
travers des pointeurs est en général plus
rapide.
En fait le nom d'un tableau contient l'adresse
du premier élément du tableau.
49
Pointeurs et tableaux

Exemple:
int vecteur[10]; déclare 10 emplacements qui
peuvent contenir des valeurs de type int.
La notation vecteur[i] fait référence au i ème
élément du tableau.
int *pa=&vecteur[0];
pa contient l'adresse du premier élément du
tableau
pa+1 l'adresse du deuxième élément.
50
Pointeurs et tableaux

Il y a une différence entre le nom d'un vecteur et


le pointeur.
Le pointeur est une variable, ainsi on peut écrire
pa=a ou pa++ si pa est un pointeur;
Si a est le nom d'un tableau, alors les
instructions a=e et a++ sont illégales.

51
Pointeurs et tableaux

Lorsque le nom d'un tableau est passé à une


fonction, ce que l'on transmet est en réalité
l'adresse du premier élément du tableau.
À l'intérieur de la fonction, cette variable est une
copie locale.

52
Exemple

Écrire une fonction qui calcule le nombre de


caractères d'une chaîne de caractères.
int strlen(char *s){
int n ;
for(n=0; *s!='\0';s++)
n++;
return n;
}
53
Opérations arithmétiques

Si p est un pointeur alors p++ incrémente p pour


pointer sur le prochain élément et p+=i ajoute i à
l'adresse de p pour pointer sur ieme élément qui
se trouve après p.
En général, un pointeur peut être initialisé
comme n'importe quelle variable mais en
général les seules valeurs significatives sont 0
et une expression qui fait référence à des
adresses d'autres variables.
54
Opérations arithmétiques

Les pointeurs et les entiers ne sont pas


interchangeables. 0 est la seule exception.
La constance symbolique NULL définie dans
stdio.h est souvent utilisé à la place de 0.

55
Opérations arithmétiques

Si p et q sont des pointeurs qui appartiennent


au même tableau, alors les opérations
==,<,>,<=,=> et != peuvent être utilisé.
Ainsi p<q est vrai si p pointe sur un élément qui
se trouve avant l'élément pointé par q dans le
tableau.

56
Opérations

Les seules opérations valides sur les pointeurs


sont donc l'affectation des pointeurs de même
type, ajouter ou soustraire un entier à un
pointeur, soustraire ou comparer deux pointeurs
qui appartiennent au même tableau, affectation
ou comparaison avec 0.

57
Pointeurs et chaînes de caractères

En C, il n'existe pas de type pour représenter


les chaînes de caractères. Elles sont
représentées comme des pointeurs sur des
caractères.

58
Les chaînes constantes

Une chaîne constante est mise entre les


doubles quotes.
"je suis une chaine" ; est représenté intérieurement
comme un tableau de caractère terminé avec le
caractère nul '\0'. Le nombre d'emplacement requis
est donc égal au nombre de caractère +1.

59
Les pointeurs et chaînes

On peut affecter la valeur d'une chaîne


constante à un pointeur de type char *.
Ainsi
char *pmessage="Bonjour"
Affecte à pmessage un pointeur sur le tableau des
caractères. Il ne s'agit pas d'une opération de copie de
caractères mais juste de manipulation des pointeurs.

60
Les pointeurs et chaînes

Il existe une différence fondamentale entre le


nom d'un tableau de caractère et un pointeur
sur les caractères.
Exemple:
char m[20]="Hello boy";
char * pm="Good morning";
m désigne ici l'emplacement utilisé pour stocker la
chaînes. Les caractères peuvent changer
individuellement mais on ne peut pas changer m.
61
Les pointeurs et chaînes

Par contre pm pointe sur un emplacement


mémoire qui contient la chaîne constante. La
valeur du pointeur peut changer pour pointer
quelque part d'autre mais la modification de la
chaîne constante est indéfinie

62
Opérations sur les chaînes

La bibliothèque string.h définie un ensemble de


fonctions sur les chaînes de caractères:
●strcpy copie une chaine dans une autre

●strcat copie une chaine à la suite d'une autre

●strcmp compare deux chaînes

●strchr fournit un pointeur sur la première


occurrence d'un caractère
●strlen renvoie la longueur d'une chaine

●memcpy opération de recopie mémoire

63
Pointeurs de fonctions

En C, contrairement à d'autres langages, les


fonctions ne sont pas des variables. Il est
cependant possible de définir des pointeurs sur
les fonctions qui peuvent être affectés, placés
dans des tableaux, passés en argument à des
fonctions ou retournés par les fonctions.

64
Pointeurs de fonctions

La définition d'une fonction crée par défaut un


pointeur sur une fonction.
D'une manière générales, la définition d'une
fonction fonc de prototype T fonc(T1,T2,..,Tn)
définit un pointeur constant (non modifiable) de
type incomplet (utilisable uniquement comme
paramètre dans un prototype sans nommage
des paramètres)
T (*)(T1,T2,..,Tn) 65
Pointeurs de fonctions

La définition d'un pointeur ptr_fonc vers une


fonction telle que fonc est réalisée par :
T (*ptr_fonc)(T1,T2,..,Tn).
On peut en déduire la définition d'un nom de
type pour ces pointeurs :
typedef T(* pointeur_fonction) (T1,T2,..Tn)
qui autorise une définition telle que:
pointeur_fonction ptr_fonc;
66
Exemple

●Définition d'un pointeur sur une fonction ayant


2 paramètre de type int et renvoyant un int:
typedef int (* pointeur)(int, int);
●Définition d'une fonction renvoyant un entier et

ayant en paramètres un pointeur sur une


fonction renvoyant un int et ayant deux int en
paramètres et deux int supplémentaires en
paramètres:
int calc(int (*)(int, int),int,int)
67
Questions

Expliquer la déclaration suivante:


●void *(*start_routine)(void *)
●void (*signal (int sig, void (*fcn)(int)))(int)

68
Exercice

●Écrire une fonction qui ne renvoie aucune


valeur et qui déterminer la valeur maximale et la
valeur minimale d'un tableau d'entiers (à un
indice) de taille quelconque. Il faudra donc
prévoir 4 arguments : le tableau, sa dimension,
le maximum et le minimum.
●Modifier l'algorithme du tri par bulles de telle

sorte qu'il prenne en paramètre une fonction de


comparaison de deux entiers.
69
Exercice.

On considère le problème d'échange de deux


variables.
void swap(int *x, int *y){
int c;
c=*x;
*x=*y;
*y=c ;
}
Que faire si l'on veut échanger
70
deux floats?
Exercice

Modifier la fonction swap de telle sorte qu'elle


puisse échanger n'importe quel type de valeurs
(les deux valeurs étant de même type).
Hint: Utiliser la fonction memcpy définie dans
string.h

71
LA GESTION DYNAMIQUE DE
LA MÉMOIRE

72
Allocation dynamique de la mémoire

En C, il existe trois types de données :


●Les données statiques

●Les données automatiques

●Les données dynamiques

73
Les données statiques

Il s'agit des données qui occupent un


emplacement parfaitement défini lors de la
compilation.
On retrouve dans cette catégorie les
constantes, les variables (non pointeurs)
globales.

74
Les données automatiques

Ce sont des données qui n'ont pas de taille


définie à priori. Elles sont créées et détruites au
fur et à mesure de l'exécution du programme.
Elles sont souvent gérées sous forme de pile
qui croît et décroît suivant les besoins du
programme.
On a dans cette catégorie les variables locales
aux fonctions.
75
Les données dynamiques

Elles n'ont pas de taille définie à priori. Leur


création et destruction dépend des demandes
explicites faites lors de l'exécution du
programme. Leur gestion est généralement faite
dans une structure appelée tas (heap).

76
Inconvénients des données statiques
Les données statiques présentent certains défauts:
●Mauvaise utilisation de l'ensemble de la mémoire.

On réserve souvent beaucoup plus d'espace qu'il


en faut.
●La gestion statique ne se prête pas aisément à la

mise en œuvre de certaines structures de données


comme les listes chaînées, les arbres binaires,
objets dont ni la structure ni l'ampleur ne sont
généralement connues lors de la compilation.
77
Allocation dynamique de la mémoire

Les données dynamiques vont permettre de


pallier aux défauts des données statiques en
donnant la possibilité au programmeur d'allouer
et libérer de la mémoire dans le tas au fur et à
mesure de ses besoins.

78
Les opérations de base
Le langage C offre quatre fonctions définies
dans la bibliothèque stdlib.h pour la gestion
dynamique de la mémoire. Il s'agit des
opérations:
●malloc permet d'allouer un emplacement de

taille définie
●calloc alloue des espaces en les initialisant à 0

●realloc realloue un emplacement préalablement

alloué
●free libère un emplacement alloué.
79
La fonction malloc

Le prototype de la fonction malloc est la


suivante:
void * malloc(size_t taille);
size_t est un type qui dépend de
l'implémentation (taille maximale autorisée).
malloc renvoie un pointeur sur la zone allouée.
taille est le nombre octets.

80
La fonction malloc

On considère le programme suivant :


#include <stdlib.h>
char * adr;
adr=malloc(50);
for(int i=0 ; i<50;i++)
*(adr+i)='x' ;

81
La fonction malloc

En général, l'argument de la fonction malloc


n'est pas un entier fixé (comme dans l'exemple
précédent). On utilise en général l'opérateur
sizeof qui permet de renvoyer la taille d'une
donnée.
Exemple:
long * adr;
adr=malloc(100*sizeof(long)); /* alloue 100
emplacements de type long */
82
La fonction free

L'un de l'intérêt essentiels de la gestion


dynamique est de pouvoir récupérer des
emplacements donc on n'a plus besoin. Le rôle
de la fonction free est de libérer un
emplacement préalablement alloué.
free prend en entrée un pointeur renvoyé par
malloc.
83
La fonction free

#include <stdlib.h>
#include <stdio.h>

main(){
char *adr1;
adr1=malloc(100);
*adr1='y';
free(adr1) ;
}
84
La fonction calloc

La fonction
void * calloc(size_t nb_blocs, size_t taille);
Alloue l'emplacement nécessaire à nb_blocs
consécutifs, ayant chacun une taille de taille
octets.
Contrairement à ce qui se passe avec malloc
où le contenu de l'espace mémoire alloué était
imprévisible, calloc remet à zéro chacun des
octets de la zone ainsi 85allouée.
La fonction realloc

La fonction
void * realloc(void *pointeur, size_t taille);
permet de modifier la taille d'une zone
préalablement allouée (par malloc, calloc ou
realloc).
Le pointeur doit être l'adresse de début de la
zone dont on veut modifier la taille.
taille représente la nouvelle taille souhaitée.
86
Les fonctions d'allocation

La valeur renvoyée les fonctions malloc, realloc


et calloc est un pointeur sur la zone allouée si
cette opération réussie. Sinon elles renvoient
des pointeurs nuls.

87
LES STRUCTURES DE DONNÉES
ABSTRAITS

88
Définition
Les algorithmes et les systèmes réactifs
manipulent les données. Ils les lisent,
manipulent et livrent des résultats à l'extérieur
Ces tâches peuvent se laissent spécifier sans
se soucier de l'organisation interne de ces
données.
Ce principe se nomme abstraction des données.
Les données sont alors des objets avec une
interface c'est à dire une liste d'opérations qui
peuvent s'exécuter sur ces objets.
89
Définition

Les types de données abstraits constituent le


concept approprié pour décrire ces
abstractions et les interfaces correspondants.
L'interface est donnée par la signature de
l'ADT et des axiomes. La représentation
interne dans une machine d'un ADT est
appelée structure de données.
La spécification de l'ADT est désignée
d'interface. 90
Définition

Le principe selon lequel un objet peut se définir


à travers une interface est désigné
d'encapsulation (information hiding).

91
Les fondements mathématiques

La tâche essentielle des informaticiens est la


conception des algorithmes. La question
centrale à laquelle ils doivent répondre est de
savoir si un algorithme A résout effectivement
le problème B.
Les types de données en mathématiques
correspondent à la notion d'algèbre et non à
la notion d'ensembles mathématiques.
92
Exemple – type booléen

Le type booléen est le type des valeurs de


vérité VRAI ou FAUX. On peut le définir de la
manière suivante :
boolean
type boolean
opns : TRUE {} → boolean
FALSE {} → boolean
93
Exemple – type entier naturel

On peut définir le type des entiers en utilisant le


nombre 0 et l'opération qui permet de trouver le
successeur d'un nombre (vu que l'ensemble des
entiers naturels est totalement ordonné) :
nat
type nat
opns : 0 {} → nat
succ nat → nat
94
Caractéristiques

Les deux exemples précédent ont les


caractéristiques communes :
● Ils possèdent des données, qui sont
distribuées sur une ou plusieurs structures de
données.
● Il existe des opérations pour accéder ou
manipuler les données.

95
Définition mathématique

Étant donné une signature Σ, un ensemble X


d'opérations et un ensemble T de termes corrects
sur Σ et X, et un ensemble d'axiomes A qui
respectent l'application des opérations de Σ alors
Σ=(T,X,A) est une structure algébrique.
Les spécifications algébriques sont basées sur les
ADT et constituent le fondement du Génie
Logiciel.
96
Définition mathématique

La spécification d'un ADT est constituée de


deux parties:
●La spécification fonctionnelle (signature). Ici, on

doit donner un nom significatif au type à définir


et donner le prototype des différentes
opérations sur la structure de données.
●La sémantique. Ici on doit donner le sens des

différentes opérations.
97
La sémantique

Pour donner une sémantique abstraite nous


utilisons le langage de la logique équationnelle.
Nous caractérisons chaque opération par des
axiomes. Les opérations sont considérées
comme des fonctions totales ou partielles dont
la restriction s'exprime par des préconditions.

98
La sémantique

Une façon pour construire les axiomes nous


conduit à partager les opérations du types en
constructeurs et opérateurs.
Un constructeur est indispensable à la
représentation des valeurs du type.
Chaque axiome est construit par application
d'un opérateur sur un constructeur si la
précondition est satisfaite.
99
Les listes
Les listes sont des structures de données
flexibles qui peuvent s'agrandir ou se rétrécir
en fonction de la demande. On peut accéder,
supprimer ou insérer n'importe quel élément
dans la liste.
Plusieurs listes peuvent être concaténées ou
bien une liste peut être subdivisée en sous-
liste.
Mathématiquement, une liste est une
séquence d'un ou de 100plusieurs éléments de
même type.
Propriétés des listes

Une propriété importante d'une liste est que les


éléments de la liste sont linéairement ordonnées
en fonction de leur position dans la liste. La
notion de précédence est ainsi bien définie.
Dépendant de l'application considérée, on peut
avoir les opérations suivantes sur les listes.

101
Opérations sur les listes

● INSERT(x,p,L) permet d'insérer l 'élément x à


la position p dans la liste L
● LOCATE(x,L) permet de localiser x dans la

liste L. Si x apparaît plusieurs fois, alors


renvoyer l'indice de la première occurrence.
● RETRIEVE (p, L) renvoie l'élément qui se
trouve à la position p
● DELETE(p, L) supprime l'élément qui est à la

position p.
102
Opérations sur les listes
● NEXT(p,L) et PREVIOUS(p,L) renvoient
respectivement les positions avant et après p
dans la liste L.
● MAKENULL(L) transforme L en une liste vide

● FIRST(L) renvoie la première position de la

liste
● PRINTLIST(L) affiche les éléments de L dans

l'ordre d'apparence.
● END(L) renvoie la position après le dernier

élément de la liste 103


Les constructeurs

Nous avons les constructeurs suivants:


●CreateList: crée une liste vide

createList:{} → List
●Cons: permet d'ajouter un élément à une liste

cons: A x List → List

104
Caractéristiques

List
type List T
opns : createList: {} → List
cons: T x List → List
insert: T x Int x List → List
locate: T x List → List
retrieve: int x List → List
delete: int x List → List
next: int x List → List
previous: int x List → T
makeNull: List → T
first: List → T
printList: List → {}
105
Problème

On désire écrire une fonction qui permet de


supprimer les doublons d'une liste. Nous
devons donc écrire une fonction qui n'utilise
que les opérations défini sur la liste sans tenir
compte es détails d'implémentations

106
Solution

void purge (List* L){


int p, q;
p = FIRST(L);
while (p != END(L)){
q = NEXT(p, L);
while (q!= END(L) )
if (same(RETRIEVE(p, L), RETRIEVE(q, L)))
DELETE(q, L)
else
q = NEXT(q, L);
}
p = NEXT(p, L)
} 107
Implémentation en utilisant les tableaux

const int maxlength = 100


struct LIST{
T elements[maxlength];
int last;
}
int position;
int END(LIST L ){
return (L.last + 1) ;
} 108
Implémentation en utilisant les tableaux
void INSERT ( x:T; p: position; LIST* L){
if (L.last > = maxlength)
printf("list is full");
else
if ((p > L.last + 1) or (p < 1))
printf("position does not exist");
else{
for(int q = L.last; q>=p;q--) {
L.elements[q + 1 ]= L.elements[q];
L.last = L.last + 1;
L.elements[p] = x
}
} 109
Implémentation en utilisant les tableaux

void DELETE (int p, LIST* L ){


if ((p > L.last) or (p < 1))
printf("position does not exist");
else{
L.last = L.last - 1;
for (int q = p; q< L.last; q++)
L.elements[q] = L.elements[q + 1]
}
} 110
Implémentation en utilisant les tableaux

int LOCATE (T x, LIST L){


for(int q = 0; q< L.last;q++)
if (L.elements[q] == x)
return q;
return (L.last + 1)
}

111
Implémentation des listes en utilisant les
pointeurs

struct celltype{
T element;
celltype* next;
}
typedef celltype* LIST;
typedef LIST* position;
position END ( LIST L){
return null ;
}

112
Les listes linéaires

Les listes linéaires sont des séquences d’éléments.


La liste vide est représentée par []. On peut
prolonger les listes par des éléments individuels à
travers l’opérateur : 1 :[2,3] = [1,2,3] = 1 :2 :[3] =
1 :2 :3 :[].
L’opérateur est associatif à la droite. 1 :2 :[3]=1 :
(2:[3]). Les listes peuvent contenir d’autres listes.
[[1],[1,2]].
113
Les listes linéaires
Plusieurs opérations peuvent être invoquées sur
les listes linéaires :
●createList qui crée une liste vide à partir de rien

●cons qui permet d'insérer un élément au début de

la liste (l'opérateur:)
●length qui donne le nombre d'éléments dans la

liste
●head qui renvoie l'élément qui se trouve en tête

de la liste.
●tail qui donne le reste de la liste sans l'élément de

tête 114
●++ qui permet de fusionner deux listes en une.
Les listes linéaires

D’une manière générale, l’on a la déclaration


suivante:
Constructeurs
CreateList: {} → Liste t - opérateur ([])
Const : t → Liste t → Liste t – opérateur ( :)
Autres opérateurs
length : [t] → int
head : [t] → t
tail : [t] → [t]
(++) : [t] → [t] → [t] 115
Les listes linéaires

On a les axiomes suivants:


length [] = 0
length(x :xs) = 1+length(xs)
head(x:xs) = x
tail(x:xs) = xs
[]++ys = ys
(x :xs)++ys = x :(xs++ys)

116
Les vecteurs

Un vecteur est une liste de longueur finie. Le choix


de l’élément ième et la construction d’un nouveau
vecteur qui se distingue uniquement par l’élément
ième d’un autre vecteur constituent les opérations
élémentaires.
Les vecteurs sont les types de données standard de
tous les langages de programmation impératifs.

117
Les vecteurs
Les opérations suivantes peuvent être
invoquées sur les vecteurs :
●makeArray qui transforme une liste en un
vecteur
●length qui donne le nombre d'éléments du
vecteur
●lookup prend un indice et renvoie l'élément qui

est stocké à cet indice


●update permet de modifier la valeur stockée à

un indice donné.
118
Les vecteurs

Les opérations suivantes sont disponibles:


mkarray :[a] → array a
length : array a → integer
lookup :array a → integer → a
update : array a → integer → a → array a

119
Les vecteurs

On a les axiomes suivants:


length (mkarray xs ) = #xs
length (update A k x) = length A
lookup(mkarray xs ) k = xs ! k -- ! indique
la sélection
lookup (update A j x ) k = if j =k then x
else lookup A k

120
Les piles

La pile est une structure de données dans laquelle


on peut ajouter, lire ou supprimer des éléments.
Tous ses éléments constituent une structure
algébrique ou algèbre abstraite Pile(T) avec T le
type des éléments dont est constituée la pile.
Avec les piles, on insère et on insère à une extremité
donnée.
Les piles constituent une structure LIFO.
121
Les piles

Les opérations suivantes peuvent être définies


sur les piles :
●createStack permet de créer une pile vide à

partir du néant
●push permet d'insérer un élément au sommet

de la pile
●pop permet d'enlever l'élément stocké au
sommet de la pile
●top lit la valeur stockée au sommet de la pile

●empty renvoie si oui ou non est pile est vide


122
Les piles

On a les opérations suivantes:


Σ Pile(T) : CreateStack : {∅} → Pile(T)
push : Pile(T)x T → Pile (T)
pop : Pile(T) → Pile (T)
top : Pile(T) → T
empty : Pile(T) → Boolean

123
Les piles

On a les axiomes suivantes:


P1 empty(Createstack) = True;
P2 empty(push(p,t)) = false
P3 pop(push(p,t)) = p
P4 top(push(p,t)) = t

124
Les files d'attentes

Quand une donnée, une opération, un événement


etc. ne peut pas être traité tout de suite, l’on le
met dans une file. Les registres des ordinateurs ne
sont autre chose que des files. Il existe plusieurs
sortes de l’organisation des files. Les plus
importantes sont FIFO où l’élément qui attend le
plus longtemps est traité le premier et LIFO où
l’élément arrivé en dernier est traité le premier.
125
Les files d'attentes

Les opérations suivantes peuvent être définie


sur les files d'attente :
●createQueue crée une file vide à partir du
néant
●enqueue ajoute un élément en fin de la file

●dequeue supprime l'élément en début de file

●front renvoie l'élément au début de la file

●empty renvoie si oui ou non la file est vide

126
Les files d'attente

On a les opérations suivantes:


Σ File(T) : CreateQueue : {} → Queue(T)
enqueue : Queue(T)x T → Queue (T)
dequeue : Queue(T) → Queue (T)
front : Queue(T) → T
empty : Queue(T) → Boolean

127
Les files d'attente

On a les axiomes suivants:


P1 empty (CreateQueue) = True;
P2 empty (Enqueue(s, a)) = False
P3 front (Enqueue (CreateQueue, a)) = a
P4 front (Enqueue(s,a)) = front s
P5 dequeue (Enqueue (CreateQueue, a)) =
CreateQueue
P6 dequeue(Enqueue(s,a))=Enqueue (dequeue(s),
a) 128
Les arbres binaires

Un arbre est binaire si ses arcs ne comportent qu’au


maximum deux fils. Quand le nombre des
descendants est limité, l’implémentation devient
facile.
Pour la formulation des algorithmes déterministes,
nous distinguons entre les deux fils. Pour le faire,
nous le marquons par les identificateurs de left et de
right.
129
Les arbres binaires

Les opérations suivantes sont définies sur


l'ADT des arbres binaires :
●emptyTree crée un arbre binaire vide

●bin crée un arbre binaire en combinant deux

sous arbres et un élément qui sera la racine


●left renvoie le sous-arbre gauche d'un arbre

●right renvoie le sous-arbre droit d'un arbre

●value renvoie la valeur de la racine de l'arbre

●empty renvoie si oui ou non l'arbre est vide


130
Les arbres binaires

On a les operations suivantes:


Σ BinBaum(T) : EmptyTree : {} → BinTree T
Bin : BinTree T x T x BinTree T → BinTree T
left : BinTree T → BinTree T
right : BinTree T → BinTree T
value : BinTree T → T
empty : BinTree T → Boolean

131
Les arbres binaires

On a les axiomes suivants:


P1 left (Bin (b1, v, b2) ) = b1
P2 right (Bin (b1, v, b2)) = b2
P3 value (Bin (b1, v, b2)) = v
P4 empty(EmptyTree()) = true
P5 empty (Bin (b1, v, b2)) = False

132
LA RÉCURSIVITÉ

133
Introduction

L'un des principes fondamentaux en


informatique est la récursivité. Cette technique
permet une représentation plus compacte et
plus simples des algorithmes.

134
Définition

La récursivité est le principe de définition d'un


problème, d'une fonction ou d'une méthode en
fonction de lui-même.
Les définitions récursives sont en général plus
courtes et plus faciles que d'autres
présentations, puisqu'elles soulignent les
propriétés caractéristique d'une fonction.

135
Définition
Une fonction récursive f est définie pour partie
en référence à elle-même. Sa définition est une
structure conditionnelle comprenant :
●Un test d'arrêt qui détecte un sous-ensemble

de valeurs vi du domaine de définition de la


fonction et les fait correspondre à des
définitions non récursives de la fonction
●Des fonctions qui provoquent la convergence

des valeurs de la variable de récursion vers


des valeurs satisfaisant
136
l'un des tests d'arrêt de
la fonction.
Définition
L'importance de la récursivité réside dans la
possibilité de décrire dans une proposition finie
un ensemble d'objets.
De la même manière un ensemble infini de
calcul peut se laisser décrire par un programme
récursif.
Dans la programmation impérative, un outil
suffisant et nécessaire pour présenter les
programmes récursifs est la fonction (sous-
programme)
137
Quand utiliser la récursion ?

●Le problème de départ doit se laisser


partitionner en des exemplaires plus simples
du même problème.
●Quand tous les sous-problèmes sont résolus,

les solutions doivent se laisser fusionner de


telle manière que la solution du problème initial
soit établit.
138
Quand utiliser la récursion ?

●Un grand problème doit se laisser partitionner


en des sous-problèmes moins complexes et
plus faciles tels qu'ils peuvent se laisser
résoudre sans autre partition.

139
Exemples

Additionner deux entiers.


Add(X, 0) = X
Add(X,Y+1) = 1+Add(X,Y)

Multiplier deux entiers


Mult(X,0) = 0
Mult(X,Y+1) = X+Mult(X,Y)

140
La puissance de la récursivité

La puissance la récursivité se trouve dans la


possibilité de définir un ensemble d'objets
infinis grâce à un nombre fini d'expressions.
On peut aussi définir un nombre infini de
traitements en utilisant un nombre fini
d'instructions.

141
Usage

La récursivité est particulièrement adaptée


quand le problème à résoudre, la fonction à
calculer ou bien la structure de données à
déclarer peut se décrire en termes récursives.

142
Dangers

Comme avec les structures itératives (boucles),


la récursivité peut introduire la possibilité de la
non terminaison. Il est donc essentiel
d'examiner la terminaison des calculs récursifs.
Une condition fondamentale est qu'un appel
récursif soit soumis à une condition qui peut
devenir fausse à un moment donné.

143
Dangers

Dans la pratique, il n'est suffisant de montrer


qu'un algorithme récursif se termine, c'est à dire
le nombre d'appels récursifs est fini, mais il faut
aussi démontrer que ce nombre n'est pas très
grand. Ceci parce qu'un appel récursif implique
un surcoût.

144
Schéma général

Un appel récursif se présente sous l'une des


formes suivantes :
● P = if B P[S,P]

● P = P[S, if B P]

145
Quand ne pas utiliser la récurrence?

Les solutions récursives ne sont pas appropriés


pour des problèmes qui respectent les formes
suivantes :
● P = if B S ; P

● P = S ; if B P

Dans ces schéma, on fait appel à P une seule


fois : soit au début soit à la fin.

146
Exemple

La fonction factorielle
On a :
f(0)=1
f(n+1)=(n+1)*f(n)
Si on ajoute deux variables I et F pour contenir
n et f(n) correspondant, on a :
P= if I<n {I=I+1 ; F=I*F ; P}

147
Exemple

On a la fonction suivante :

int factorielle (int n){


if (n>0)
return n*factorielle(n-1) ;
else
return 1 ;
}
148
Exemple

La fonction itérative correspondante est :

int factorielle (int n){


int i=0 ;
int f=1 ;
while (i<n)
f*=++i ;
return f ;
} 149
Les algorithmes récursifs

La récursivité peut se présenter d'au moins


deux manières :
●La récursivité directe : P appelle P

●La récursivité indirecte : P appelle Q et Q

appelle P
Ainsi la récurrence peut ne pas être apparente
quand on a un algorithme.
150
Les algorithmes récursifs

Étudions les instructions de la forme : while B


A.
Désignons par V l'ensemble des variables, le
schéma peut se réécrire :
V=Vo ;
while p(V)
V=f(V)
Avec P un prédicat et f une fonction.
151
Les algorithmes récursifs

Si on désigne par vi la valeur de V après i


passage dans la boucle.
On a donc les conditions suivantes :
v i=f  v i−1 , ∀ i0
v i≠v j , ∀ i, j avec i≠ j
p v n =false
p v i =true , ∀ i , in
152
Les suites composées

La forme while B A est aussi appropriée pour le


calcul des suites à termes composés.
On veut calculer :
Si on a s 0 =t 0t 1..t i
On peut définir les relations suivantes :
t i=f t i−1 
s i=s i−1t i
s 0 =t 0
153
La fonction pgcd

La fonction pgcd peut être définie en utilisant les


formules suivantes :
●pgcd (n,n)=n

●pgcd (m,n) = pgcd(n,m)

●pgcd(m,n) = pgcd(m,n-m) pour n>m

154
La fonction d'Ackermann

Un exemple de fonction récursive est la fonction


d'Ackermann. Elle est définie de la manière
suivante :
●A(0,n) = n+1

●A(m+1,0) = A(m,0)

●A(m+1,n+1) = A(m, A(m+1,n))

155
Les structures de données récursives
La définition d'un type est la spécification de
l'ensemble des valeurs d'un domaine.
On a des types statiques et des types
dynamiques.
Les types de données récursifs sont des types
qui se définissent en termes d'eux même.
Exemples :
le type des expressions
L'arbre généalogique.
156
Les structures de données récursives

La propriété fondamentale des structures de


données récursives (dynamiques) est que leurs
tailles varient en fonction de la demande. Ainsi,
on n'alloue pas une quantité fixée de mémoire à
ces structures de données.
La technique que l'on utilise pour réaliser cette
allocation dynamique est l'utilisation des
pointeurs.
157
Les pointeurs

En C, on utilise l'opérateur * pour déclarer un


pointeur.
Ainsi int * p définit une variable p qui contient
l'adresse d'une variable qui contient des
données de type entier.
Avec les pointeurs, on peut aussi allouer
dynamiquement de la mémoire en utilisant
l'instruction malloc de la bibliothèque stdlib.h.
158
Les listes linéaires - revisitées

Une liste linéaire est une structure de données


donc les données sont reliées entre elles. Dans
la configuration la plus simple, un élément de la
liste linéaire à une référence sur son
successeur dans la liste.

159
Les listes linéaires
Avec les listes linéaires, on a les
constructeurs :
●createList qui crée une liste vide

●cons qui permet d'ajouter un élément à une

liste.
On a aussi les opérations suivantes :
●head qui donne l'élément de tête

●tail qui renvoie le reste la liste sans l'élément

de tête
●length qui donne le nombre d'éléments de la

liste 160
Les listes linéaires

On a l'implémentation suivante (pour une liste


linéaires de noms) :
struct node{
char* nom;
struct node* next;
};

161
Implémentations

struct node* createList(void){


struct node* result=0 ;
return result
}

162
Implémentations

struct node* cons(struct node * data, char* nom){


if (data==NULL){
data=(struct node*) malloc(sizeof(struct node)) ;
data->nom=nom ;
data->next=NULL ;
}else{
data->next=cons(data->next, nom) ;
}
return data ;
} 163
Implémentations

char* head (struct node* data){


if (data==NULL)
return NULL ;
return data->nom ;
}

struct node* tail(struct node* data){


if (data==NULL)
return NULL ;
return data->next ;
} 164
Implémentations

int length(struct node* data){


if (data==NULL)
return 0 ;
return 1+length(data->next) ;
}

int empty(struct node* data){


return (data==NULL) ;
}
165
Les structures arborescentes

La récursion peut être utilisée pour structurer la


séquence et l'itération.
Les séquences et les itérations sont si
communes que l'on les considère comme des
structures fondamentales.
Mais il existe des structures qui ne peuvent pas
se laisser représenter en termes de séquence ni
d'itération.
Les structures arborescentes sont un exemple.
166
Définition

Un arbre binaire est une structure de données


constituée :
●une racine

●deux sous arbres (gauche et droit).

Un arbre ordonné est un arbre donc les


branches sont ordonnées. Un nœud y qui se
trouve directement sous un nœud x est
s’appelle successeur (descendant) de x. Si x
se trouve au niveau i alors y sera au niveau
i+1. x est appelé prédécesseur
167 (ancestor) de y.
Définition

La racine d’un arbre se trouve par définition au


niveau 0. Le plus grand niveau d’un arbre
s’appelle hauteur. Quand un élément n’a plus
de successeur,
l’on le désigne de feuille (leaf). Un élément qui
n’est pas terminal s’appelle nœud interne. Le
nombre de successeurs directs d’un nœud
s’appelle grade. Le plus grand grade parmi
tous les nœuds est le grade
168
de l’arbre.
Définition
Un arbre binaire est dit complet si tous les
nœuds internes ont deux fils.
Un arbre binaire de recherche est un arbre tel
que tous les éléments qui sont à la partie
gauche de l'arbre sont inférieurs au nœud
interne et ceux de droite sont supérieurs.
Un arbre AVL (nommé selon les
mathématiciens russes Adelson-Velski et
Landis qui les ont inventés en 1962) est un
arbre binaire tel que la différence des hauteurs
des deux sous-arbres 169ne dépasse pas 1.
Implémentation

Un arbre binaire peut être défini de la manière


suivante :
struct binaryTree{
char* root;
struct binaryTree* left ;
struct binaryTree* right;
}
170
Le backtracking

La but d'un algorithme est d'être général. Il doit


résoudre une classe de problème.
Il existe des algorithmes qui pour résoudre le
problème ne suivent pas de règles bien fixées.
Au contraire, ils procèdent par des essais et
erreurs (trial and errors). Il s'agit d'une classe
d'algorithmes appelés algorithmes exploratifs.
Ils explorent une ensemble fini d'états à la
recherche de la solution.
171
Le backtracking

En général, le problème à résoudre est


subdivisé en sous tâches. Ces sous tâches se
définissent en général plus facilement de
manière récursive. Lors de l'exploration, si l'on
se rend compte que ne va pas atteindre la
solution, on recommence l'exploration à partir
d'un autre état (backtracking).
172
Le problème des huit reines

Nous allons présenter le problème huit reines


pour essayer d'illustrer cette approche.
Le problème consiste à placer huit reines sur un
échiquier de telle sorte qu'aucune reine ne soit
en danger.
Ce problème a été traité par Gauss dans les
années 1850.

173
Le problème des huit reines

On a la procédure générique suivante :


Try(i: INTEGER){
Initialiser les positions pour la ième reine.
Repeter
Faire un choix
Si sécurise mettreReine ;
Si i < 8 alors Try(i+1);
Si non success alors enleverReine ;
Jusqu'à succes ou plus174 de positions.
Diviser pour régner

Une des techniques les plus importantes et les


plus répandues de conception d'algorithmes
est la stratégie appelée "diviser pour régner".
Le principe en est de diviser un problème
complexe de taille N en sous-problèmes plus
petits et plus simples de manière que la
solution de chaque sous-problème facilite la
construction de la solution du problème entier.
Nous allons utiliser les Tours d'Hanoï pour
l'illustrer 175
Les Tours d'Hanoï
On donne trois pieus (piquets ou pylônes) et un
certain nombre N de disques de diamètres
différents avec un trou central qui permet
d'enfiler les disques sur les pieus.
Initialement, les disques sont tous empilés sur
le pieu P1 par diamètres décroissants en
partant du bas. Le but du problème est de
passer en un minimum de déplacements ces N
disques du pieu P1 sur le pieu P2 en s'aidant
du pieu P3 qui peut 176servir à des stockages
intermédiaires de disques.
Les Tours d'Hanoï

Les règles suivantes doivent être respectées:


●on ne peut déplacer qu'un seul disque à la

fois,
●un disque peut être déplacé d'un des trois

pieus sur l'un des deux autres,


●un disque ne peut être placé que sur le sol ou

sur un disque de diamètre supérieur.


On demande d'afficher tous les mouvements
de disques qui doivent177être effectués.
Les Tours d'Hanoï

1 2 3
178
Les Tours d'Hanoï

Pour déplacer les N disques du pieu 1 au pieu


3 (nous dirons plus simplement "de 1 à 3") en
respectant les règles, on peut décomposer la
tâche en trois parties:
• déplacer les (N-1) premiers disques de 1 à 3
en respectant les contraintes,
●déplacer le N-ième disque de 1 à 2,

• déplacer les (N-1 ) disques de 3 à 2 en


respectant les contraintes.
179
Exemple – la suite de Fibonacci

La suite de Fibonacci est définit de la manière


suivante :
F(0)=1
F(1)=1
F(n+2)=F(n+1)+F(n)

180

Vous aimerez peut-être aussi