SUPPORT DE COURS DE
STRUCTURE DE
DONNEES AVANCEES
M. YOSSA NGUEKAM Christian
TABLE DES MATIERES
CHAPITRE 1 : LES FONCTIONS ET PROCEDURES ................................ 3
1.1. Introduction ................................................................................................ 3
1.2. Notion de sous-programme ....................................................................... 3
1.3. Procédure .................................................................................................... 4
1.4. Fonction ...................................................................................................... 5
1.4.1. Passage paramètres par valeur................................................................. 6
1.4.2. Passage paramètres par variable.............................................................. 8
CHAPITRE 2 : LA NOTION DE RECURSIVITE ....................................... 10
2.1. Introduction .............................................................................................. 10
2.2. Comment écrire une fonction récursive ................................................ 10
2.3. Types de récursivité ................................................................................. 12
2.4. Récursivité terminale et récursivité non terminale .............................. 13
CHAPITRE 3 : LES TECHNIQUES DE RECHERCHE ............................. 15
3.1. Introduction .............................................................................................. 15
3.2. Recherche séquentielle ............................................................................ 15
3.2.1. Recherche séquentielle usuelle................................................................ 15
3.2.2. Recherche séquentielle dans une séquence triée ................................... 18
3.3. Recherche dichotomique ......................................................................... 20
3.3.1. Principe de la recherche dichotomique.................................................. 20
3.3.2. Version récursive ..................................................................................... 20
3.3.3. Version itérative ....................................................................................... 21
3.4. Recherche par interpolation ................................................................... 22
3.4.1. Principe ..................................................................................................... 22
3.4.2. Version itérative ....................................................................................... 23
CHAPITRE 4 : LES TECHNIQUES DE TRI................................................ 24
4.1. Introduction .............................................................................................. 24
4.2. La procédure échanger ............................................................................ 24
4.3. Tri par minimum successif (ou tri par sélection).................................. 25
4.3.1. Principe ..................................................................................................... 25
4.3.2. Foncion indiceDuMinimum .................................................................... 25
4.3.3. Algorithme ................................................................................................ 25
4.4. Tri par échange ........................................................................................ 26
4.5. Tri à Bulle (Bubble Sort) ......................................................................... 27
4.6. Tri par insertion ....................................................................................... 28
4.7. Tri par fusion............................................................................................ 29
4.8. Tri rapide .................................................................................................. 30
CHAPITRE 5 : LA NOTION DE COMPLEXITE ........................................ 33
5.1. Introduction .............................................................................................. 33
5.2. Notions mathématiques ........................................................................... 33
5.3. Classes de complexité............................................................................... 34
5.4. Calcul de la complexité ............................................................................ 34
5.4.1. Cas d'une instruction simple : écriture, lecture, affectation ............... 34
5.4.2. Cas d'un traitement conditionnel ........................................................... 35
5.4.3. Cas d'un traitement itératif : Boucle Tant Que .................................... 35
5.4.4. Cas d'un traitement itératif : Boucle Pour ............................................ 35
5.5. Exemples de calcul de la complexité ...................................................... 35
CHAPITRE 6 : LES TRAVAUX PRATIQUES DANS UN LANGAGE
PROFESSIONNEL (C OU C++) ..................................................................... 36
CHAPITRE 1 : LES FONCTIONS ET PROCEDURES
1.1. Introduction
Dès qu’on commence à écrire des programmes importants, il devient difficile d’avoir une vision
globale sur son fonctionnement et de traquer les erreurs. En général, les problèmes qui se pose
en pratique sont suffisamment complexes et nécessitent leur décomposition en sous problèmes
plus faciles à résoudre séparément.. D’où vient l’idée de décomposition d’un programme
complexe en sous-programmes (fonctions et procédures). De même lorsqu’une partie de code
doit être exécutée plusieurs fois, à des endroits différents, ou réutilisée ultérieurement on pourra
ne l’écrire qu’une fois et lui donner un nom en faisant une Fonction ou une Procédure, pour
éviter les redondances de codes dans le programme. Par exemple pour calculer la somme
suivante : Sn=1 !+2 !+3 !+4 !+5 !+6 !+......+n ! , on doit calculer le factoriel n fois (pour le 1 ;2
;3 ;4 ;.... ;n). Au lieu d’écrire le code de calcul du factoriel n fois dans le programme, on définir
une fonction ou procédure qui calcul celui-ci et on fait appel au besoin.
1.2. Notion de sous-programme
Un sous-programme (procédure ou fonction) est un programme à l’intérieur d’un autre
programme. Il possède la même structure que le programme principal. Il peut être appelé par
le programme principal ou par un autre sous-programme pour réaliser un certain traitement et
retourner des résultats. Le schéma illustre le mécanisme d’appel et retour des sous-
programmes.
Un sous-algorithme est déclaré de manière générale c.-à-d qu’il peut être appelé plusieurs fois
avec différentes valeurs grâce à des arguments. Ces derniers, bien qu’ils soient facultatifs, sont
dits paramètres et sont clairement déclarés, au besoin, dans l’entête du sous-algorithme.
Un paramètre est une valeur du bloc principal dont le sous-algorithme a besoin pour exécuter
avec des données réelles l’enchaînement d’actions qu’il est chargé d’effectuer. On distingue
deux types de paramètres :
• Les paramètres formels sont la définition du nombre et du type de valeurs que devra
recevoir le sous algorithme pour se mettre en route avec succès. On déclare les
paramètres formels pendant la déclaration du sous-algorithme.
• Les paramètres effectifs sont des valeurs réelles (constantes ou variables) reçues par
le sous-algorithme au cours de l’exécution du bloc principal. On les définit
indépendamment à chaque appel du sous algorithme dans l’algorithme principal.
L’exécution d’un sous-algorithme (procédure ou fonction) se fait par une instruction d’appel
(voir sections suivantes). L’application de cette instruction génère un saut vers le sous-
algorithme appelé. La terminaison de ce sous-algorithme redémarre la suite d’instruction
interrompue par l’appel.
Il existe des procédures et des fonctions prédéfinies simples comme printf(x), scanf(x), sqrt(x),
ou cos(x). Si on a besoin d’un traitement que le langage n’offre pas dans sa bibliothèque, on
peut définir nos propres procédures et fonctions.
III. Types de sous-algorithme
Un sous-algorithme peut se présenter sous forme de fonction ou de procédure.
Une fonction est un sous-algorithme qui, à partir de donnée(s), calcul et rend à l’algorithme Un
et Un seul résultat alors qu’en général, une procédure affiche le(s) résultat(s) demandé(s).
1.3. Procédure
Une procédure est un bloc d’instructions nommé et déclaré dans l’entête de l’algorithme et
appelé dans son corps à chaque fois que le programmeur en a besoin.
Déclaration d’une procédure :
L’appel d’une procédure peut être effectué en spécifiant, au moment souhaité, son nom et
éventuellement ses paramètres ; cela déclenche l’exécution des instructions de la procédure.
Exemple : Voici un algorithme utilisant une procédure qui calcule une somme de 100 nombres.
1.4. Fonction
Une fonction est un bloc d’instructions qui retourne obligatoirement une et une seule valeur
résultat à l’algorithme appelant. Une fonction n’affiche jamais la réponse à l’écran car elle la
renvoie simplement à l’algorithme appelant.
Déclaration d’une fonction :
Etant donné qu’une fonction a pour but principal de renvoyer une valeur, il est donc nécessaire
de préciser le type de la fonction qui est en réalité le type de cette valeur.
Un appel de fonction est une expression d’affectation de manière à ce que le résultat soit
récupéré dans une variable globale : Nom_variable-globale Nom_Fonction (paramètres) ;
Exemple : L’algorithme précédent, qui calcule une somme de N nombres, peut utiliser une
fonction au lieu d’une procédure.
Note : De même qu’une procédure, une fonction peut appeler d’autres sous-algorithmes à
condition qu’ils soient définis avant elle ou qu’ils soient déclarés dans son entête.
IV. Mode de passages de paramètres
Un sous-algorithme avec paramètres est très utile parce qu’il permet de répéter une série
d’opérations complexes pour des valeurs qu’on ne connaît pas à l’avance. Il existe deux types
de passage de paramètres : par valeur et par variable (dite aussi par référence ou encore par
adresse).
1.4.1. Passage paramètres par valeur
C’est le mode de transmission par défaut, il y a copie de la valeur, des paramètres effectifs dans
les variables locales issues des paramètres formels de la procédure ou de la fonction appelée.
Dans ce mode, le contenu des paramètres effectifs ne peut pas être modifié par les instructions
de la fonction ou de la procédure ; car nous ne travaillons pas directement avec la variable,
mais sur une copie. À la fin de l’exécution du sous-algorithme la variable conservera sa valeur
initiale. Les paramètres dans ce cas sont utilisés comme données.
Syntaxe :
Procedure nom_procédure (param1 :type1 ; param2, param3 :type2) ;
Fonction <nom_fonction> (param1 :type1 ; param2 :type2) : Type_fonction ;
Exemple : Soit l’algorithme suivant.
Exécutons cet algorithme pour la valeur (-6)
Avant l’appel de procédure : la seule variable déclarée est la variable globale (M)
M Ecran
-6
Après l’appel de procédure : la variable-paramètre "nombre" est déclarée et reçoit en copie la
valeur de M.
M Nombre Ecran
-6 -6
-6 6
-6 6 6
Au retour à l’algorithme (au niveau de l’appel) il ne reste que la variable globale avec sa valeur
initiale
M Ecran
-6
-6 -6
1.4.2. Passage paramètres par variable
Ici, il s’agit non plus d’utiliser simplement la valeur de la variable, mais également son
emplacement dans la mémoire (d’où l’expression « par adresse »). En fait, le paramètre formel
se substitue au paramètre effectif durant le temps d’exécution du sous-programme et à la sortie
il lui transmet sa nouvelle valeur.
Un tel passage de paramètre se fait par l’utilisation du mot-clé Var.
Syntaxe :
Procedure nom_procédure (Var param1 :type1, param2, param3 :type2) ;
Fonction <nom_fonction> (Var param1 : type1, param2 :type2) : Type_fonction ;
Note : Les paramètres passés par valeur et par adresse peuvent cohabiter à l’intérieur d’un
même sousalgorithme. Il suffit de partager les deux types de passage par un (;).
Syntaxe :
Procedure nom_procédure (Var param1 :type1 ; param2, param3 :type2) ;
Dans ce cas param1 est passé par référence alors que les deux autres ont par valeur
Fonction <nom_fonction> (param1 : type1 ; Var param2 :type2) : Type_fonction ;
Dans ce cas param1 est passé par valeur alors que le deuxième est passé par référence.
Exemple : Soit l’algorithme précédent modifié dans le type de passage de paramètre
Exécutons cet algorithme toujours pour la valeur (-6)
Avant l’appel de procédure : la seule variable déclarée est la variable globale (M)
M Ecran
-6
Après l’appel de procédure : la variable-paramètre nombre se substitue à la variable M
(M) nombre Ecran
-6
6
6 6
Au retour à l’algorithme il ne reste que la variable globale avec sa nouvelle valeur.
M Ecran
6
6 6
V. Exemples
Exemple 1 :
Un algorithme qui calcule et affiche la valeur absolue d’une valeur en utilisant une fonction
Lors de l’exécution de la fonction abs, la variable a et le paramètre unEntier sont associés par
un passage de paramètre en entrée : La valeur de a est copiée dans unEntier.
CHAPITRE 2 : LA NOTION DE RECURSIVITE
2.1. Introduction
Pour mieux appréhender la notion de récursivité, passons par une notion mathématique connue
de tous : la récurrence.
De nombreuses définitions mathématiques sont récursives :
Selon Peano :
• 0 est un entier naturel.
• Tout entier n a un successeur unique Sn (= n + 1) ;
• Tout entier sauf 0 est le successeur d’un unique entier ;
• Pour tout énoncé P(n) si P(0) est vrai et si pour tout n, P(n) implique P(Sn) alors
l’énoncé 8n : P(n) est vrai.
La récursivité est un moyen simple et élégant de résoudre certain problème. On appelle
récursive toute fonction ou procédure qui s’appelle elle-même.
La récursion est un principe puissant permettant de définir une entité à l’aide d’une partie de
celle-ci. Chaque appel successif travaille sur un ensemble d’entrées toujours plus affinées, en
se rapprochant de plus en plus de la solution d’un problème.
L’exécution d’un appel récursif passe par deux phases : la descente et la remontée.
• Dans la phase de descente, chaque appel récursif fait à son tour un appel récursif. Cette
phase se termine lorsque l’un des appels atteint une condition terminale (retour de
valeur).
• Ensuite, la phase de remontée se poursuit jusqu’à ce que l’appel initial soit terminée.
C’est ce qu’on appelle pile d’exécution.
2.2. Comment écrire une fonction récursive
Il suffit de la faire s’appeler elle-même
Fonction f(n : entier) : entier
Début
f ← f(n-1)
Fin
La fonction f est récursive : elle s’appelle elle-même mais f n’a-t-elle pas un problème ?
Une obbligation : s’arrêter
La fonction f telle qu’elle est écrite ne s’arrête pas :
• Appel : f(2)
• Appel : f(1)
• Appel : f(0)
• Appel : f(-1)
• Appel : f(-2)
• Etc...
Comment y parvenir
Première étape : la condition terminale
• Obligatoirement au début de toute fonction récursive
• Une condition : le cas particulier
• Pour ce cas, pas d’autre appel à la fonction : la chaîne d’appels s’arrête
Exemple :
Fonction f(n : entier) : entier
Début
Si n = 0 alors
afficher("Bonjour")
Sinon
f f(n-1)
Fin Si
Fin
Ici quand n vaut 0, on s’arrête
Problème : arrive-t-on à n = 0 ?
Terminaison de la fonction
Il faut que la fonction s’arrête. La condition terminale ne sert à rien si elle ne devient jamais
vraie
Exemple avec la fonction précédente :
• f(-2) provoque une pile d’appels infinie
• Probablement d’autres tests à faire (si n<0, envoyer une exception par exemple)
Une bonne solution
Fonction f(n : entier) : entier
Début
Si n < 0 alors
afficher("erreur")
Sinon Si n = 0 alors
afficher("Bonjour")
Sinon
f f(n-1)
Fin Si
Fin
2.3. Types de récursivité
On distingue 3 types de récursivité :
La récursivité simple : Ici, la fonction contient un seul appel récursif dans son corps.
Exemple : la fonction factorielle
Voici la trace (ou pile) d’exécution pour la valeur 4
La récursivité multiple : Ici, la fonction contient plus d’un appel récursif dans son
corps. Exemple : La fonction combinaison en se servant de la relation de Pascal
La récursivité mutuelle : Ici, il s’agit de plusieurs fonctions dépendantes les unes des
autres. Exemple : La parité d’un entier
2.4. Récursivité terminale et récursivité non terminale
Une fonction récursive est dite récursive terminale si tous ses appels sont récursif terminaux.
Un appel récursif est terminal s’il s’agit de la dernière instruction exécutée dans le corps d’une
fonction et que sa valeur de retour ne fait pas partie d’une expression. Les fonctions récursives
terminales sont caractérisées par le fait qu’elles n’ont rien à faire pendant la remontée.
Elimination de la récursivité
Dérécursiver, c’est transformer un algorithme récursif en un algorithme équivalent ne
contenant pas d’appels récursifs. La récursivité terminale simple peut être remplacée par une
solution itérative.
Dans un algorithme récursif non terminal, l’appel récursif est suivi d’un traitement, c’est-à-
dire qu’il y a d’autres instructions après l’appel récursif. Il va falloir donc sauvegarder, sur une
pile, le contexte de l’appel récursif, typiquement les paramètres de l’appel engendrant l’appel
récursif. Elle peut être remplacée par une solution itérative utilisant une pile.
CHAPITRE 3 : LES TECHNIQUES DE RECHERCHE
3.1. Introduction
Les méthodes présentées ici supposent que le (multi-)ensemble est dans une structure tabulaire
(tableau ou vecteur). En général, le critère de recherche porte sur la valeur d'une clé. Par
exemple, chercher le numéro de téléphone (tel) d'une personne dans un ensemble de personnes
(une personne = nom + adresse + tel). Le but de ce chapitre est d’apprendre à rechercher un
élément dans une structure tabulaire. En procédant uniquement par comparaison entre
éléments:
• La méthode séquentielle parcourt l'un après l'autre les éléments de la séquence en les
comparant à l'élément recherche.
• La méthode dichotomique, utilisable uniquement lorsque les éléments sont rangés en
ordre (croissant ou décroissant), s'appuie sur cette propriété d'ordre pour éliminer, à
chaque comparaison, la moitié des éléments de l'ensemble restant à traiter.
• La méthode par interpolation tient compte de la valeur de l'élément recherché pour
estimer à quel endroit faire une recherche.
• Les méthodes auto-adaptatives modifient la position de l'élément recherché afin de
tenter d'améliorer les recherches futures.
3.2. Recherche séquentielle
3.2.1. Recherche séquentielle usuelle
La recherche séquentielle (sequential search en anglais) parcourt, case après case, les A.taille
éléments d'une Sequence A et s'arrête au premier élément rencontré qui est égal à l'Element x.
Dans ce cas l'information renvoyée est la position (c.-à-d. l'indice) contenant l'élément
recherché.
Version récursive
La récursion se fait sur la taille (= nombre d'éléments) du sous-tableau :
• Si la taille est 0 : l'élément cherché n'y est pas
• Sinon on compare l'élément cherché avec le premier élément :
- S'il y a égalité : on renvoie cette position
- Sinon on recommence sur le sous-tableau privé de son premier élément
Fonction rechSeqRec ( DR A : Sequence ; x : Element ) : Entier Début
| Retourner rechSeqGRec ( A , 1 , x )
Fin
Fonction rechSeqGRec ( DR A : Sequence ; k : Entier ; x : Element ) :
Entier Début
| Si ( k > A.taille ) Alors
| | Retourner ( - 1 )
| Sinon
| | Si ieme ( A , k ) = x Alors
| | | Retourner ( k )
| | Sinon
| | | Retourner rechSeqGRec ( A , k + 1 , x )
| | FinSi
| FinSi
Fin
Version itérative
La vision itérative repose sur un parcours (de la gauche vers la droite) des cases du tableau :
• Au départ on se positionne sur la première case du tableau
• A chaque itération (tour de boucle) on examine le contenu d'une case :
- S'il est égal à l'élément cherché : on termine en renvoyant la position
- Sinon on passe à la case suivante
• Si on traverse tout le tableau : l'élément cherché n'était pas présent
Fonction rechSeq ( DR A : Sequence ; x : Element ) : Entier
Variable k : Entier
Variable trouve : Booléen
Début
| trouve <- Faux
| k <- 1
| TantQue ( Non trouve Et k <= A.taille ) Faire
| | Si ( ieme ( A , k ) = x ) Alors
| | | trouve <- Vrai
| | Sinon
| | | k <- k + 1
| | FinSi
| FinTantQue
| Retourner ( trouve ? k : - 1 )
Fin
Version itérative avec sentinelle
Dans l'algorithme rechSeq, le test k <= n est effectué à chaque itération alors qu'il est utile
uniquement lorsque l'élément cherché n'est pas dans le tableau. Pour ne pas avoir à faire ce test,
il suffit d'être sûr que l'élément est présent dans la partie du tableau où l'on effectue la recherche.
On va donc rajouter la valeur de l'élément à chercher après avoir fait le test avec l'élément <
sentinelle > puis on effectue la recherche dans le tableau modifié.
Ainsi on finira toujours par trouver l'élément recherché, et il suffira de tester s'il est trouvé dans
la case modifiée. Cette astuce est dite < recherche avec sentinelle >. Il faut juste penser à
restaurer le contenu de la case sentinelle.
Fonction rechSeqS ( DR A : Sequence ; x : Element ) : Entier Variable k : Entier
Variable n : Entier
Variable aux : Element
Début
| n <- A.taille
| aux <- ieme ( A , n )
| Si ( aux = x ) Alors
| | k <- n
| Sinon
| | fixerIeme ( A , n , x )
| | k <- 1
| | TantQue ( ieme ( A , k ) <> x ) Faire
| | | k <- k + 1
| | FinTantQue
| | Si ( k = n ) Alors
| | | k <- - 1
| | FinSi
| | fixerIeme ( A , n , aux )
| FinSi
| Retourner ( k )
Fin
Version récursive avec sentinelle
L'utilisation d'une sentinelle pour la version récursive présente le même avantage en gagnant
un test à chaque appel récursif.
Fonction rechSeqRecS ( DR A : Sequence ; x : Element ) : Entier Variable k : Entier
Variable n : Entier
Variable aux : Element
Début
| n <- A.taille
| aux <- ieme ( A , n )
| Si ( aux = x ) Alors
| | k <- n
| Sinon
| | fixerIeme ( A , n , x )
| | k <- rechSeqGRecS ( A , 1 , x )
| | Si ( k = n ) Alors
| | | k <- - 1
| | FinSi
| | fixerIeme ( A , n , aux )
| FinSi
| Retourner ( k )
Fin
Fonction rechSeqGRecS ( DR A : Sequence ; k ; : Entier ; x : Element ) : Entier
Début
| Si ( ieme ( A , k ) = x ) Alors
| | Retourner ( k )
| Sinon
| | Retourner rechSeqGRecS ( A , k + 1 ,x )
| FinSi
Fin
3.2.2. Recherche séquentielle dans une séquence triée
Principe
On suppose que les éléments du tableau sont triés en ordre croissant.L'ordre permet de
déterminer exactement l'intervalle auquel doit appartenir l'élément cherché ce qui accélère la
recherche. Tout d'abord un simple test avec le dernier élément du tableau permet de savoir si
l'élément cherché est dans l'intervalle ; et dans ce cas, le tableau contient un élément z supérieur
ou égal à l'élément cherché. Cet élément z sert de sentinelle et l'on peut donc arrêter la recherche
dès que l'on rencontre un tel z (recherche avec échec si z est strictement supérieur à l'élément
cherché).
Version itérative
Fonction rechLin ( DR A : Sequence ; x : Element ) : Entier Variable k : Entier
Début
| Si ( ieme ( A , A.taille ) < x ) Alors
| | Retourner ( - 1 )
| Sinon
| | k <- 1
| | TantQue ieme ( A , k ) < x Faire
| | | k <- k + 1
| | FinTantQue
| | Si ( ieme ( A , k ) = x ) Alors
| | | Retourner ( k )
| | Sinon
| | | Retourner ( - 1 )
| | FinSi
| FinSi
Fin
Version récursive
Fonction rechLinRec ( DR A : Sequence ; x : Element ) : Entier Début
| Si ( ieme ( A , A.taille ) < x ) Alors
| | Retourner ( - 1 )
| Sinon
| | Retourner ( rechLinGRec ( A , 1 , n , x ) )
| FinSi
Fin
Fonction rechLinGRec ( DR A : Sequence ; k ; : Entier ; x : Element ) : Entier
Début
| Si ( ieme ( A , k ) < x ) Alors
| | Retourner rechLinGRec ( A , k + 1 , x )
| Sinon
| | Si ( ieme ( A , k ) = x ) Alors
| | | Retourner ( k )
| | Sinon
| | | Retourner ( - 1 )
| | FinSi
| FinSi
Fin
3.3. Recherche dichotomique
3.3.1. Principe de la recherche dichotomique
La recherche par dichotomie (binary search en anglais) compare l'élément cherché x avec
l'élément en position m situé au milieu du sous-tableau :
• si A[m] = x : on a trouvé l'élément en position m
• si A[m] > x : il est impossible que x se trouve après la position m dans le tableau
(puisque les éléments sont en ordre croissant). Il reste à traiter uniquement la moitié
inférieure du tableau
• de même si A[m] < x : il reste à traiter uniquement la moitié supérieure du tableau
• on continue ainsi la recherche, et après chaque comparaison, la taille du sous-tableau
restant à traiter diminue de moitié. Si la recherche aboutit sur un tableau de taille nulle,
x n'est pas présent et la recherche s'arrête.
Remarque
Si le tableau contient plusieurs fois l'élément cherché, la fonction renverra l'un des indices ou
il apparaît, mais on ne précise pas laquelle.
3.3.2. Version récursive
La recherche dichotomique récursive suit directement la description
Fonction rechDichoRec ( DR A : Sequence ; x : Element ) : Entier
Début
| Retourner rechDichoAuxRec ( A , 1 , A.taille ,
x ) Fin
Fonction rechDichoAuxRec ( DR A : Sequence ; g , h : Entier ; x : Element ) :
Entier Début
| Si ( h < g ) Alors
| | Retourner ( - 1 )
| Sinon
| | m <- DivEnt ( g + h , 2 )
| | Si ( ieme ( A , m ) = x ) Alors
| | | Retourner ( m )
| | Sinon
| | | Si ( ieme ( A , m ) < x ) Alors
| | | | Retourner rechDichoAuxRec ( A , m + 1 , h
,x) | | | Sinon
| | | | Retourner rechDichoAuxRec ( A , g , m - 1
,x) | | | FinSi
| | FinSi
| FinSi
Fin
3.3.3. Version itérative
La version itérative transcrit la version récursive en modifiant l'une des deux extrémités du
sous-tableau.
Fonction rechDicho ( A : Sequence ; x : Element ) : Entier
Variable g , h , m : Entier
Variable trouve : Booléen
Début
| trouve <- Faux
| g <- 1
| h <- A.taille
| TantQue ( g <= h Et Non trouve ) Faire
| | m <- DivEnt ( g + h , 2 )
| | Si ( ieme ( A , k ) = x ) Alors
| | | trouve <- Vrai
| | Sinon
| | | Si ( ieme ( A , k ) < x )
| | | | g <- m + 1
| | | Sinon
| | | | h <- m - 1
| | | FinSi
| | FinSi
| FinTantQue
| Si trouve Alors
| | Retourner ( m )
| Sinon
| | Retourner ( - 1 )
| FinSi
Fin
3.4. Recherche par interpolation
3.4.1. Principe
La recherche par interpolation tient compte de la valeur de l'élément recherché pour estimer
à quel endroit faire une recherche. C'est le principe de la recherche dans un dictionnaire : pour
rechercher le mot < ananas >, on ouvrira le dictionnaire vers le début, plutôt qu'au milieu.
Plus précisément, supposons que les éléments de la séquence sont des nombres, rangés en ordre
croissant de l'indice ideb à l'indice ifin, et A[k] le k-ème élément de la séquence.
Sous l'hypothèse que les nombres sont en progression régulière, on admettra qu'il paraît
raisonnable d'aller chercher le nombre x autour de la position p telle que :
La valeur de l'indice p est obtenue en prenant la partie entière de la solution pour p dans
l'équation ci-dessus. Cette valeur est valide, c.-à-d. comprise entre les bornes ideb à l'indice
ifin, uniquement lorsque x est compris entre les éléments A[ideb] et A[ifin] de la séquence.
Lorsque l'indice p est valide, on compare x et A[p] :
• si x = A[p] : on a terminé
• si x < A[p] : on recommence la recherche sur A[ideb..p-1]
• si x > A[p] : on recommence la recherche sur A[p+1..ifin]
Lorsque l'indice p n'est pas valide, l'élément x n'est pas dans la séquence.
3.4.2. Version itérative
Fonction rechInterpol ( DR A : Sequence ; x : Element ) : Entier
Variable g , h , p : Entier
Variable trouve : Booléen
Variable numer , denom : Entier
Début
| trouve <- Faux
| g <- 1
| h <- A.taille
| TantQue ( g <= h Et Non trouve ) Faire
| | Si ( x < ieme ( A , g ) Ou ieme ( A , h ) < x ) Alors
| | | Sortir
| | FinSi
| | numer <- x - ieme ( A , g )
| | denom <- ieme ( A , h ) - ieme ( A , g ) + 1
| | p <- g + DivEnt ( ( h - g + 1 ) * numer , denom )
| | Si ( ieme ( A , p ) = x ) Alors
| | | trouve <- Vrai
| | Sinon
| | | Si ( ieme ( A , p ) < x )
| | | | g <- p + 1
| | | Sinon
| | | | h <- p - 1
| | |
FinSi | |
FinSi
| FinTantQue
| Si trouve Alors
| | Retourner ( p )
| Sinon
| | Retourner ( - 1 )
| FinSi
Fin
CHAPITRE 4 : LES TECHNIQUES DE TRI
4.1. Introduction
Le traitement des données pose le problème de leur classement (tri). C'est pour cela que
différentes méthodes de tris ont été développées. Un algorithme de tri sert à donner un ensemble
d'éléments. Il facilite l'accès à l'information et la recherche d'un élément dans une quantité très
grande d'information. Vu l'importance de l'opération de tri, plusieurs algorithmes performants
ont été proposés. Les différents algorithmes présentent des performances différentes, en termes
de temps d'exécution et en termes d'espace mémoire requis pour leur exécution. Les tableaux
permettent de stocker plusieurs éléments de même type au sein d’une seule entité. Lorsque le
type de ces éléments possède un ordre total, on peut donc les ranger en ordre croissant ou
décroissant. Trier un tableau c’est donc ranger les éléments d’un tableau en ordre croissant
ou décroissant. Il existe plusieurs méthodes de tri qui se différencient par leur complexité
d’exécution et leur complexité de compréhension pour le programmeur. Nous allons voir dans
ce chapitre les principaux algorithmes de tri de données. On ne fera que des tris en ordre
croissant.
4.2. La procédure échanger
Tous les algorithmes de tri utilisent une procédure qui permet d’échanger (de permuter)
la valeur de deux variables Dans le cas où les variables sont entières, La procédure
échanger est la suivante :
4.3. Tri par minimum successif (ou tri par sélection)
4.3.1. Principe
Le tri par minimum successif est un tri par sélection. Pour une place donnée, on sélectionne
l’élément qui doit y être positionné. De ce fait, si on parcourt le tableau de gauche à droite, on
positionne à chaque fois le plus petit élément qui se trouve dans le sous tableau droit. Ou plus
généralement : Pour trier le sous-tableau t[i..nbElements] il suffit de positionner au rang i le
plus petit élément de ce sous-tableau et de trier le sous-tableau t[i+1..nbElements].Par exemple,
pour trier <101, 115, 30, 63, 47, 20>, on va avoir les boucles suivantes :
i=1 <101, 115, 30, 63, 47, 20>, i=2 <20, 115, 30, 63, 47, 101>, i=3 <20, 30, 115, 63, 47, 101>
i=4 <20, 30, 47, 63, 115, 101>, i=5 <20,30, 47, 63, 115, 101>. Donc en sortie : <20, 30, 47, 63,
101, 155>
Il nous faut donc une fonction qui pour soit capable de déterminer le plus petit élément (en fait
l’indice du plus petit élément) d’un tableau à partir d’un certain rang.
4.3.2. Foncion indiceDuMinimum
4.3.3. Algorithme
L’algorithme de tri est donc :
4.4. Tri par échange
Le tri par échange est un algorithme de tri peu performant à cause de sa lenteur d'exécution.
C'est pourtant l'un des plus étudiés car c'est l'un des plus simples à comprendre. Le principe de
cet algorithme consiste à prendre chaque élément d'un tableau et le comparer à tous ses
suivants, lorsque l'ordre n'est pas respecté les deux éléments sont permutés.
Description
Le tableau suivant montre le fonctionnement de cet algorithme
La première ligne indique la position initiale des éléments du tableau avant le démarrage de
l'algorithme. Dans la première ligne de la première itération, l'algorithme compare le premier
élément du tableau T (nombre 7) avec le deuxième (nombre 4). Les éléments comparés sont
notés en gris. Dans ce cas, un échange est effectué entre ces deux éléments. On procède de la
même façon (c'est à dire le premier élément avec les autres) jusqu'à la fin de la première
itération. Lorsque l'on termine de parcourir le tableau la première fois (première boucle), on est
sûr d'avoir placé le premier élément à la bonne place. On parcourt donc de nouveau le tableau
pour placer le second élément, le troisième jusqu'au dernier.
4.5. Tri à Bulle (Bubble Sort)
Le principe de l'algorithme tri à Bulle (Bubble Sort) consiste à comparer successivement les
éléments adjacents, et les permuter si le premier élément est supérieur au second. L'opération
est relancée jusqu'à ce qu'il n'y ait plus de permutation possible (tous les nombres sont triés).
Description
L'algorithme contient deux boucles imbriquées. La boucle interne (la boucle Pour) permet de
comparer les éléments adjacents du tableau puis les échanger s'ils sont désordonnés. La boucle
externe, quant à elle, entraîne la boucle interne à se répéter jusqu'à ce qu'il n’ait plus aucune
permutation à effectuer (variable permut = Faux). Les éléments les plus grands remontent ainsi,
peu à peu vers les dernières places, ce qui explique la dénomination de tri à bulle. Le tableau
suivant montre le fonctionnement de cet algorithme.
La première ligne indique la position initiale des éléments du tableau T avant le démarrage de
l'algorithme. Chaque ligne indique le contenu du tableau T après chaque itération de la boucle.
Cet algorithme consiste à réduire à chaque fois l'intervalle de tri. C'est à dire qu'au début on
parcourt tout le tableau, on déplace l'élément le plus grand vers la droite. Après le premier
passage, le plus grand élément est assuré d'être à la fin du tableau, après le second passage, le
deuxième plus grand élément est en position, et ainsi de suite. C'est pourquoi la limite
supérieure dans la boucle interne doit être diminuée avec chaque passage.
Remarque :
Le principe du tri à bulles bidirectionnel est de parcourir la liste à trier dans un sens puis dans
l'autre donc en alternant le sens de passage à chaque tour. Cette technique augmente légèrement
la rapidité de l'algorithme.
4.6. Tri par insertion
Le tri par insertion est l'un des plus naturels : c'est le tri qu'on utilise intuitivement lorsqu'on
souhaite trier une liste d'objets manuellement, par exemple des dossiers, des cartes à jouer etc.
On prend un dossier et on le met à sa place parmi les dossiers déjà triés. Puis on recommence
avec le dossier suivant. Le principe de cet algorithme est de considérer que le tableau est divisé
en deux parties de tailles variables ; une dans laquelle les données sont triées et l'autre contenant
les données à trier. Au début de l'algorithme la partie non triée contient les autres éléments (le
reste du tableau). On prend un à un les éléments de la partie non triée du tableau et on l’insère
au bon endroit dans la partie triée du tableau en déplaçant tous les éléments qui le précède. Dès
que l'élément à insérer est plus grand qu'un des éléments de la partie triée du tableau, il n'y a
plus de déplacement, et l'élément est inséré dans la case laissé vacante par les éléments qui ont
été déplacé.
Description
Le tableau suivant montre le fonctionnement de cet algorithme.
Chaque ligne indique le contenu du tableau T après chaque itération de la boucle Pour, et la
valeur conservée par la variable echange. La première ligne indique la position initiale des
éléments du tableau T avant le démarrage de l'algorithme. La partie triée (note en gris) contient
une case, les cases restants ne sont pas triées (partie non triée). Dans le première itération (i =
2), le nombre 4 a été inséré au début du tableau. Cela nécessite le déplacement du nombre qui
le précède (7). On procède de la même façon jusqu'à la fin du tableau. Cet algorithme est utile
lorsque l'ensemble des éléments n'est pas disponible immédiatement. Imaginons par exemple
une application interactive où l’utilisateur fournit un élément à la fois et doit maintenir un
ensemble trié de ces éléments.
4.7. Tri par fusion
Le tri par fusion est une méthode de tri qui utilise le concept de "diviser pour régner". Il
s’agit de séparer le problème en plusieurs sous-problèmes similaires au problème initial. Le
principe utilisé est le suivant :
- Diviser : le problème en un certain nombre de sous- problème. Ici, la séquence de
éléments à trier en 2 sous-séquences de éléments, jusqu'à que chaque sous-séquence soit
de taille 1
- Régner : sur les sous- problèmes en les résolvant. Trier les 2 sous-séquences
récursivement à l’aide du tri fusion
- Combiner : les solutions des sous- problèmes en une solution unique. Fusionner les 2
sous-séquences triées pour produire la séquence triée.
Essayons de dérouler l’algorithme ci-dessus pour le tableau t[6, 5, 3, 1, 8, 7, 2, 4] en
appelant la procédure tri_fusion(t, 1, 8).
4.8. Tri rapide
Le tri rapide (en anglais QuickSort) est une méthode de tri inventée par C. Hoare basée sur la
méthode conception "diviser pour régner". Il n'est pas le plus rapide dans tous les cas, mais
il a la particularité d'être relativement rapide dans la plupart des situations. Le principe de cet
algorithme consiste à choisir un élément T[i] du tableau, appelé Pivot qui va servir à
partitionner ce tableau en deux parties. Toutes les valeurs d'une partie seront inférieures au
Pivot, les autres supérieurs. Ensuite, de façon récursive, l'algorithme va recommencer le tri
dans les deux sous tableaux ainsi obtenus. Le schéma suivant illustre le principe du tri rapide.
Nous choisissons ici comme Pivot l'élément du tableau dont l'indice est le plus petit.
Principe de la méthode
• Choisir un élément du tableau appelé pivot,
• Ordonner les éléments du tableau par rapport au pivot Appeler
récursivement le tri sur les parties du tableau
- à gauche et
- à droite du pivot.
Description
L'algorithme ci-dessus contient une fonction et une procédure qui prennent en entrée un
tableau d'entiers et deux entiers qui sont des bornes entre lesquelles il faut trier les valeurs.
L'exécution de cet algorithme commence par l'exécution de l'algorithme principal qui fait
appelle à la procédure Tri. La fonction Partition sert à partitionner le tableau en deux parties
et à mettre le Pivot à sa place définitif. Elle retourne en résultat la position finale du Pivot. Le
tableau ci-dessous montre une étape de partitionnement du tableau.
Chaque ligne représente une itération de la boucle TantQue du partitionnement du tableau.
Dans cette boucle les indices inf et sup sont respectivement incrémentés et décrémentés tant
qu'ils sont inférieurs (supérieurs) au Pivot. Ensuite la boucle effectue un échange entre deux
éléments du tableau. Les cellules en gris représentent le Pivot et les valeurs en gras sont celles
qui vont être permutées. La procédure Tri est constituée de l'appel à la fonction Partition et
de l'appel récursif à la procédure de tri dans les deux portions du tableau. Le tableau ci-
dessous illustre le fonctionnement de cet algorithme.
CHAPITRE 5 : LA NOTION DE COMPLEXITE
5.1. Introduction
La complexité d'un algorithme est la mesure du nombre d'opérations fondamentales qu'il
effectue sur un jeu de données. La complexité est exprimée comme une fonction de la taille
du jeu de données. La complexité d'un algorithme est souvent déterminée à travers une
description mathématique du comportement de cet algorithme. On note Dn l’ensemble des
données de taille n et T(d) le coût de l’algorithme sur la donnée d. On définit 3 types de
complexité :
- Complexité au meilleur : C'est le plus petit nombre d'opérations qu'aura à exécuter
l'algorithme sur un jeu de données de taille fixée, ici à n.
- Complexité au pire : C'est le plus grand
nombre d'opérations qu'aura à exécuter l'algorithme sur un jeu de données de taille
fixée, ici à n.
- Complexité en moyenne : C'est la moyenne des
complexités de l'algorithme sur des jeux de données de taille n.
C'est l'analyse pessimiste ou au pire qui est généralement adoptée. En effet, de nombreux
algorithmes fonctionnent la plupart du temps dans la situation la plus mauvaise pour eux.
• L’analyse au pire des cas donne une limite supérieure de la performance et elle
garantit qu'un algorithme ne fera jamais moins bien que ce qu'on a établi.
• Un algorithme est dit optimal si sa complexité est la complexité minimale parmi les
algorithmes de sa classe. Même si on s’intéresse quasi-exclusivement à la complexité en
temps des algorithmes. Il est parfois intéressant de s’intéresser à d’autres ressources, comme
la complexité en espace (taille de l’espace mémoire utilisé), la largeur de bande passante
requise, etc.
5.2. Notions mathématiques
La notation O est celle qui est le plus communément utilisée pour expliquer formellement les
performances d'un algorithme. Cette notation exprime la limite supérieure d'une fonction dans
un facteur constant. La notation O reflète la courbe ou l'ordre croissance d'un algorithme. Les
règles de la notation O sont les suivantes :
- Les termes constants : O(c) = O(1)
- Les constantes multiplicatives sont omises : O(cT ) = cO(T) = O(T)
- L'addition est réalisée en prenant le maximum : O(T1) + O(T2) = O(T1 + T2) =
max(O(T1);O(T2))
- La multiplication reste inchangée mais est parfois réécrite d'une façon plus compacte
: O(T1)O(T2) = O(T1T2)
Exemple : On suppose qu’on dispose d'un algorithme dont le temps d'exécution est décrit par
la fonction T(n) = 3n2+10n+10. L'utilisation des règles de la notation O nous permet de
simplifier en : O(T(n)) = O(3n2+10n+10)
= max(O(3n2); O(10n); O(10))
= max(3O(n2); 10O(n); O(1))
= max(O(n2); O(n); O(1))
= O(n2)
5.3. Classes de complexité
Les algorithmes usuels peuvent être classés en un certain nombre de grandes classes de
complexité. Les complexités les plus utilisées sont :
- Constante : O(1) Accéder au premier élément d'un ensemble de données
- Logarithmique : O(logn) Couper un ensemble de données en deux parties égales,
puis couper ces moitiés en deux parties égales, etc.
- Linéaire : O(n) Parcourir un ensemble de données
- Quasi-linéaire : O(nlogn) Couper répétitivement un ensemble de données en deux et
combiner les solutions partielles pour calculer la solution générale
- Quadratique : O(n2) Parcourir un ensemble de données en utilisant deux boucles
imbriquées
- Polynomiale : O(nP) Parcourir un ensemble de données en utilisant P boucles
imbriquées
- Exponentielle : O(2n) Générer tous les sous-ensembles possibles d'un ensemble de
données
5.4. Calcul de la complexité
5.4.1. Cas d'une instruction simple : écriture, lecture, affectation
Dans le cas d'une suite d'instructions simples, on considère la complexité maximale.
La complexité de cette séquence vaut max(O(1),O(1))=O(1).
5.4.2. Cas d'un traitement conditionnel
5.4.3. Cas d'un traitement itératif : Boucle Tant Que
5.4.4. Cas d'un traitement itératif : Boucle Pour
5.5. Exemples de calcul de la complexité
CHAPITRE 6 : LES TRAVAUX PRATIQUES DANS
UN LANGAGE PROFESSIONNEL (C OU C++)