Académique Documents
Professionnel Documents
Culture Documents
Objectifs du cours
Ce cours d’algorithmique et structures de données permettra à l’étudiant de :
- Comprendre et maitriser les différentes structures et organisations de
données en algorithmique.
- Comprendre les notions de complexité des algorithmes et d’identifier les
meilleurs algorithmes.
- Faire l’analyse expérimentale et l’analyse théorique d’un algorithme.
- Maîtriser les structures de données particulières piles et files d’attente où
les insertions et suppressions d’éléments peuvent se faire en tête ou en
queue de liste.
- Maîtriser la notion de récursivité et développer des procédures récursives.
- Comprendre et maîtriser les structures dynamiques simples (les listes
chainées unidirectionnelles, bidirectionnelles et circulaires) et les structures
avancées (arbres et graphes).
Pré-requis
Pour bien appréhender ce cours, l’étudiant doit avoir suivi et assimilé :
- Le cours de logique de programmation ;
- Quelques notions d’analyse mathématique.
Modes d’évaluation
Les différentes évaluations retenues dans le cadre de ce cours sont :
- Présence aux cours ;
- Travail dirigé ;
- Interrogation et examens.
La moyenne annuelle représente la moitié de points, soit 10 points, et l’examen
également la moitié de points, soit 10 points.
Plan du cours
Chap. I. Généralités sur les algorithmes et complexité
I.1. Généralités sur les algorithmes
I.2. Notion de complexité d’un algorithme
I.3. Analyse des algorithmes
I.4. Exercices
I.5. Travail dirigé
Chap. II. Les Piles, les Files d’attente et la récursivité
II.1. Généralités
II.2. Les piles
II.3. Les files d’attente
II.4. La récursivité
Chap. III. Les algorithmes de tri
III.1. Définition
III.2. Tri par sélection
III.3. Tri par insertion
III.4. Tri par bulles
III.5. Tri rapide
III.6. Exercices
Chap. IV. Les listes chaînées
IV.1. Notions sur les pointeurs
IV.2. Généralités sur les listes chaînées
IV.3. Définition d’une liste chaînée
IV.4. Représentation en mémoire des listes chaînées
IV.5. Opérations dans une liste chaînée
Module A
Module B
Module C
Oui Non
Condition
Module A Module B
Condition Non
Module A
Oui
Oui
Déterminer la Non
mention (E, TB, B, …)
d’un étudiant en Oui
fonction de sa
moyenne Non
Oui
Non
I.1.4. Exercices
a. Ecrire un algorithme qui lit le nom et l’âge d’une personne, puis affiche « X est
majeur (e) » si l’âge est supérieur ou égal à 18, sinon « X est mineur (e) » dans le cas
contraire (X étant le nom lu).
b. Un service de bureautique facture 50Fc par copie l’impression des vingt premières
copies d’un document, 40Fc par copie les trente suivantes et 30Fc par copie au-delà.
Ecrivez un algorithme qui calcule et affiche le montant à payer correspondant à un
nombre de copies donné. Dressez un ordinogramme y relatif.
Soit l’illustration suivante :
Par exemple, si un algorithme écrit dans une fonction prend en entrée un tableau de
n éléments, la complexité de l’algorithme sera une estimation du nombre total
d’opérations de base nécessaires pour l’algorithme, en fonction de n. Plus n sera
grand, plus il faudra d’opérations. La nature de l’algorithme sera différente selon que
sa complexité sera plutôt de l’ordre de n, de n2, de n3, ou bien de 2n. Le temps de
calcul pris par l’algorithme ne sera pas le même.
Une opération de base pourra être une affectation, un test, une incrémentation, une
addition, une multiplication, etc.
I.2.2. Les fondements mathématiques de l’algorithmique
L’analyse des algorithmes s’appuie sur les fonctions mathématiques. Les principales
fonctions mathématiques que l’on retrouve en algorithmique sont : la fonction
constante, la fonction logarithme, la fonction linéaire, la fonction quadratique, la
fonction n log n, la fonction cubique, les autres polynômes et la fonction
exponentielle.
a. La fonction constante
f (n) = c (c est constante)
Quelle que soit la valeur de n, f (n) = c. La fonction constante est importante en
algorithmique, car elle permet de caractériser le nombre d’étapes nécessaires pour
effectuer une opération à l’aide d’un ordinateur.
b. La fonction logarithme
Un aspect important dans l’analyse des algorithmes est la présence quasi
permanente de la fonction logarithme.
; pour b > 1.
La base la plus intéressante pour les ordinateurs est la base 2. Dans les calculateurs,
on utilise aussi souvent .
c. La fonction linéaire
f (n) = n
Cette fonction apparait dans les algorithmes chaque fois que nous devons réaliser
une simple opération pour chacun de n éléments d’un ensemble. Par exemple, pour
l’algorithme de recherche linéaire, il est question de comparer une donnée x à
chaque élément d’un tableau (ou fichier) de n éléments (ou enregistrements), ce qui
exigera n comparaisons.
Le temps nécessaire à l’exécution de cet algorithme est donc proportionnel au
nombre de comparaisons. En supposant que chaque élément du champ de
comparaison possède la même probabilité d’être retenu.
d. La fonction quadratique
f (n) = n2
La raison principale pour laquelle la fonction quadratique apparait dans l’analyse des
algorithmes est due au fait qu’il y a plusieurs algorithmes qui exploitent des boucles
imbriquées. Soit n opérations pour la boucle interne, n x n opérations pour la boucle
externe, ce qui fait n2 opérations en tout.
La fonction quadratique peut également apparaitre dans l’analyse des algorithmes
lorsque dans le contexte de boucles imbriquées, la première itération nécessite une
opération, la seconde nécessite deux opérations, la troisième nécessite trois
opérations et ainsi de suite. Le nombre d’opérations est alors :
1 + 2 + 3 + ... + (n − 2) + (n − 1) + n
C’est le total d’opérations exécutées par la boucle externe lorsque le nombre
d’opérations de la boucle interne croit de 1 à chaque opération de la boucle externe.
e. La fonction
Cette fonction croit un peu plus vite que la fonction linéaire et moins vite que la
fonction quadratique. En diverses occasions, il est possible de ramener le temps
nécessaire pour résoudre un problème d’une fonction quadratique à une fonction
. Ce qui peut représenter un gain non négligeable de temps.
g. La fonction exponentielle
Une autre fonction souvent utilisée dans l’analyse des algorithmes est la fonction
exponentielle
f (n) = bn, avec b > 0
b est la base et n l’exposant. La base qui apparait le plus souvent dans l’analyse des
algorithmes est la base 2. Si par exemple nous avons une boucle qui effectue une
opération, puis double le nombre d’opérations à chaque itération, alors le nombre
d’opérations effectuées à la nieme itération est 2n.
Remarque (quelques sommations)
La sommation suivante apparait de nombreuses fois dans l’analyse des algorithmes :
Un polynôme f (n) de degré d dont les coefficients sont a0,a1, …, ad peut être écrit
sous la forme ci-dessous :
calcul pris par l’algorithme (sur une machine donnée) est directement lié à ce nombre
d’opérations.
En fait, il est hors de question de calculer exactement le nombre d’opérations
engendrées par l’application d’un algorithme. On cherche seulement un ordre de
grandeur. L’ordre de grandeur asymptotique est l’ordre de grandeur lorsque la taille
des données devient très grande.
taille de l’entrée comme paramètre principal. Dans ce contexte, tout algorithme peut
être étudié ou analysé, soit expérimentalement, soit théoriquement.
I.3.1. Analyse expérimentale
Une fois qu’un algorithme est implémenté, nous pouvons étudier son temps
d’exécution en fonction de la taille de l’entrée en mesurant expérimentalement ce
temps d’exécution. Des telles mesures sont possibles grâce aux fonctions disponibles
dans les langages de programmation et les systèmes d’exploitation.
Les mesures expérimentales ont trois limitations principales :
- Les expériences ne peuvent être faites que sur un nombre limité d’entrées
(d’autres entrées pouvant se révéler importantes sont laissées de côté) ;
- Il est difficile de comparer les temps d’exécution expérimentaux de deux
algorithmes, sauf si les expériences ont été menées sur les mêmes
environnements (Hardware et Software) ;
- On est obligé d’implémenter et d’exécuter un algorithme en vue d’étudier ses
performances. Cette dernière limitation est celle qui requiert le plus de temps
lors d’une étude expérimentale d’un algorithme.
Cette méthodologie associe à chaque algorithme étudié une fonction f(n), qui
caractérise son temps d’exécution en fonction de la taille n de l’entrée. Cette fonction
s’obtient par un ajustement approprié des données expérimentales.
Exemple : Considérons l’algorithme de tri par insertion qui a été implémenté en Java.
Pour mesurer le temps d’exécution, on fait tourner le programme pour des entrées
allant de 1000 à 400000 nombres entiers générés de manière aléatoire. Ce qui donne
lieu au tableau suivant :
200000 80261
300000 180171
400000 322896
Ce tableau génère la figure suivante qui est une parabole, que l’on peut exprimer par
une équation du type .
Ce qui nous fait dire que le temps d’exécution du tri par insertion est du type (n2),
c’est-à-dire quadratique.
I.3.2. Analyse théorique
L’analyse expérimentale est valable, mais présente des limites. Si nous souhaitons
analyser un algorithme particulier sans procéder à des expériences sur son temps
d’exécution, nous pouvons effectuer l’analyse de son pseudo-code. Il s’agit de
l’analyse théorique également appelée analyse des opérations primitives ou analyse
de la complexité. Ici, on s’appuie sur des opérations de base telles que :
- assigner une valeur à une variable ;
- effectuer une opération arithmétique (exemple : additionner deux nombres) ;
- comparer deux nombres ;
- indexer un tableau ;
- suivre la référence d’un objet ;
En fait, une opération primitive correspond à une instruction de bas-niveau avec un
temps d’exécution constant. Pour déterminer le temps d’exécution d’un algorithme,
il suffit de compter le nombre d’opérations primitives exécutées et pour chaque
opération primitive de multiplier ce nombre par son temps d’exécution constant.
6. Somme ← somme + i + j C4
7. Renvoyer somme C5 1
8. Fin
Le coût de l'exécution de l'algorithme dépend donc de n. On a ainsi :
Top 1 AAA
FFF 2 BBB
EEE 3 CCC
DDD Top 4 DDD
CCC 5 EEE
BBB 6 FFF
AAA 7
N
AAA BBB CCC DDD EEE FFF ….
1 2 3 4 5 6 N
Top
2) Dans une liste AVAIL (pour available, c-à-d disponible), les nœuds libres sont
retirés uniquement de la tête de la liste et que les nœuds libérés étaient insérés
dans cette liste toujours par la tête. La liste AVAIL est implémentée sous forme
d’une pile.
II.2.2. Représentation physique d’une pile
Une pile peut être représentée soit par une liste simplement chaînée, c'est-à-dire une
liste monodirectionnelle, soit par un tableau linéaire.
Considérons une pile sous la forme du tableau linéaire de l’élément au sommet de la
pile et une variable MAX donnera le nombre maximum d’éléments qui peuvent être
accommodés par la pile. TOP = NULL indique que la pile est vide. Dans le cas ci-après
TOP = 3, la pile comporte trois éléments, XXX, YYY, ZZZ et MAX = 8, il reste encore sur
la pile de la place pour 5 éléments.
PILE (STA)
XXX YYY ZZZ
1 2 3 4 5 6 7 8
Top 3 Max 8
E sera retiré de la file d’attente avant F parce que E était placé avant F, mais il doit
attendre le retrait de C et D.
II.3.2. Représentation des files d’attente
Les files d’attente peuvent être représentées en mémoires de diverses manières,
généralement sous forme de listes monodirectionnelles ou des tableaux linéaires.
Nos files seront représentées par un tableau linéaire Q accompagné de deux
pointeur : F, contenant l’adresse de son premier élément, et R, contenant celle de
son dernier élément. Si F = NULL, la file est vide pour un tableau Q à N éléments.
Chaque fois, qu’un élément est supprimé, la valeur F est incrémentée de 1, ce qui
peut être implémenté par F = F + 1.
De même, chaque fois qu’un élément est ajouté, la valeur de R est incrémentée de 1,
ce qui donne R = R + 1.
C'est-à-dire qu’après N insertions, l’élément de queue occupera Q * N + ou en d’autres
termes, le dernier élément de la file d’attente occupera la fin du tableau.
B. Algorithme de suppression
Dans cette procédure qui supprime le premier élément d’une file d’attente et
l’affecte à la variable ITEM, il faut d’abord commencer par tester s’il n’y a pas
dépassement de capacité négatif (UNDERFLOW), on cherche à savoir si la file
d’attente est vide ou non.
SUPP ( Q, F, R, ITEM)
1. * File d’attente déjà vide ? +
Si F = NULL alors Ecrire UNDERFLOW et Quitter
2. Poser ITEM = Q [ F ]
3. [ Trouver nouvelle valeur de F ]
Si F = R alors * file d’attente à un élément au départ +
Poser F = NULL et R = NULL
Sinon si F = N alors
Poser F = 1
Sinon Poser F = F + 1
[ fin de la structure Si ]
4. Quitter
II.3.4. DEQUE
Une deque est une liste linéaire où les éléments peuvent être ajoutés ou supprimés à
l’une ou l’autre extrémité mais pas au milieu.
Il existe plusieurs manières de représenter une deque en mémoire. On supposera que
notre deque est représentée par un tableau circulaire muni des pointeurs L et H
pointant ses deux extrémités. Les éléments se disposent en partant de l’extrémité
gauche et vont jusqu’à l’extrémité droite. Le terme circulaire signifie que DEQUE [ 1 ]
vient après DEQUE [ N ]
DEQUE
L=4
H=7
A B C D
1 2 3 4 5 6 7 8
II.4. La récursivité
II.4.1. Généralités
La récursive est un concept qui joue un rôle important en informatique. De nombreux
algorithmes sont en effet décrits de manière plus descriptive en termes de
récursivité.
Considérons une procédure P qui comprend une instruction Call (appel) sur elle-
même, soit une instruction Call sur une autre procédure pouvant d’ailleurs entraîner
une instruction Call de retour sur la procédure initiale P. Dans ces conditions, P
constitue une procédure récursive.
Pour empêcher le programme de tourner indéfiniment, une procédure récursive doit
posséder les deux propriétés suivantes :
(1) Il doit exister certains critères de base pour lesquels la procédure ne s’appelle
pas elle-même.
(2) Chaque fois que la procédure s’appelle elle-même, elle doit être plus proche
de ses critères de base.
Une procédure récursive est dite bien définie si elle possède ces deux critères.
On dit qu’une fonction est définie de manière récursive si sa définition fait référence à
elle-même. Pour que cette fonction ne soit pas circulaire, elle doit posséder les deux
propriétés suivantes :
a. Il doit exister les valeurs de base ou arguments pour lesquelles la fonction ne
se réfère pas à elle-même.
b. Chaque fois que la fonction se réfère à elle-même, son argument doit être plus
proche d’une valeur de base.
II.4.2. Exemple
Calcul de n!
On sait que pour tout entier positif n, on a : n! = n (n-1)!
On peut définir cette fonction factorielle d’une manière récursive.
(1) si n = 0, alors n! = 1
(2) si n 0, alors n! = n (n -1)!
Cette définition est-elle récursive puisqu’elle se réfère à elle-même quand elle
applique (n-1)! ?
(1) – la valeur de n ! est donnée implicitement par n = 0 (par conséquent, 0 est
une valeur de base) ;
(2) – la valeur de n ! pour n arbitraire est définie en fonction d’une plus petite n
plus voisine de la valeur de base 0. Donc la définition n’est pas circulaire.
C'est-à-dire la procédure est bien définie.
Calculons 3 ! en utilisant la définition récursive.
(1) 3! = 3.2!
(2) 2! = 2.1!
(3) 1! = 1.0!
(4) 0 ! = 1 (valeur de base de la définition récursive)
(5) 1 ! = 1. 1 = 1
(6) 2 ! = 2.1 = 2
(7) 3 ! = 3.2 = 6
Etape (1) : définit 3! en fonction de 2!
Etape (2) : définit 2! en fonction de 1!
Etape (3) : définit 1! en fonction de 0!
Etape (4) : évalue 0! implicité puisque 0 est la valeur de base de la définition
récursive.
Etape 5 à 7 : nous repartons en arrière, en utilisant 0! pour évaluer 1!, qui nous sert à
évaluer 2! et ainsi de suite jusqu’à 3!
II.4.3. Procédures de calcul
A. Première procédure de calcul de la fonction n !
FACTO (FAC, N) calcule n ! et affecte le résultat à FAC
1. Si N = 0 alors Poser FAC = 1 et Return
2. Poser FAC = 1 [initialiser FAC pour la bouche]
3. Répéter pour K = N à 1 (décroit de 1)
Poser FAC = K*FAC
[Fin de la boucle]
4. Fin
B. Deuxième procédure de calcul de la fonction n!
FACTO (FAC, N) calcul n! et affecte le résultat à variable FAC
1. Si N = 0 alors Poser FAC = 1 et Return
2. Call FACTO (FAC, N-1)
3. Poser FAC = N*FAC
4. Fin
Prof. BATUBENGA MWAMBA Nz. J.D. 25
Cours d’Algorithmique et Structures de données
Cette procédure itérative est beaucoup plus efficace que la procédure récursive
précédente.
II.4.5. Schémas itératifs - schémas récursifs
Bien que les algorithmes récursifs se présentent souvent sous une forme assez
simple, il n’est pas toujours aisé pour le programmeur de le concevoir car ils
nécessitent un assez grand effort d’abstraction. Un problème se prête
particulièrement bien à une démarche récursive lorsqu’il peut être décomposé en
une suite d’actions dont l’une est un sous-problème semblable au problème d’origine
mais appliqué à un environnement réduit.
Toute la difficulté consiste alors à trouver la solution correspondant à
l’environnement minimum qui ne fera plus appel à la récursivité.
La récursivité est coûteuse, mais donne des solutions qui s’expriment de manière plus
concise que les solutions itératives correspondantes.
Cependant, si la solution itérative est simple à formuler, il est déconseillé d’utiliser
une solution récursive en raison du coût en temps d’exécution de la gestion des
environnements successifs.
Il existe une sorte de problèmes pour lesquels il est difficile de donner une solution
non récursive. Ce sont les problèmes où la recherche de la solution se fait par essais
successifs, comme dans le parcours d’un labyrinthe : il faut essayer un chemin et, s’il
ne conduit pas à la solution, revenir en arrière et essayer un nouveau chemin. Les
applications intéressantes sont en recherche opérationnelle (parcours de graphe) et
en intelligence artificielle.
Il existe de méthodes de traductions d’une procédure récursive en une procédure
non récursive.
II.4.6. Traduction d’une procédure récursive en une procédure non récursive
On suppose P une procédure récursive telle qu’un appel à P ne peut provenir que de
P. P peut être traduite en une procédure non récursive de la manière suivante :
(1) Considérons une pile STY pour chaque paramètre Y ;
(2) Une pile STX pour chaque variable locale.
(3) Une variable locale AD et une pile STAD pour ranger les adresses de retour.
Chaque fois qu’apparaîtra un appel récursif à P, les valeurs actuelles des paramètres
et des variables locales seront empilées sur les piles correspondantes pour
traitements ultérieurs et à chaque retour récursif sur P, les valeurs des paramètres et
Avec [n/2] le plancher de n/2, c'est-à-dire le plus grand entier qui ne soit pas
supérieur à n/2 .
a) Calculer L (25)
b) Quel est l’objet de cette fonction ?
Solution
a) L (25) = L (12) +1
= [L (6) + 1 ] + 1 = L (6) + 2
= [L (3) + 1] + 2 = L (3) + 3
= [ L (1) + 1 ] + 3 = L (1) + 4
=0+4=4
b) Chaque fois que a est divisé par 2, la valeur de L augmente de 1. Il en résulte
que L est le plus entier tel que .
C'est-à-dire L = [log2n]
3. Soient j et k deux entiers et soit f (j, k) définie récursivement par :
6 3 7 2 3 5
Le maximum de ce tableau est 7. Plaçons le 7 en dernière position par un échange.
6 3 5 2 3 7
reste à trier
Le 7 est à sa position définitive ; il ne reste plus qu’à trier les cinq premiers éléments.
Le maximum de ces cinq éléments est 6. Plaçons-le à la fin par un échange :
3 3 5 2 6 7
reste à trier
Prof. BATUBENGA MWAMBA Nz. J.D. 31
Cours d’Algorithmique et Structures de données
Le maximum parmi les nombres restant à trier est 5. Plaçons-le en dernière position
par un échange :
3 3 2 5 6 7
reste à trier
Enfin le maximum parmi les nombres à trier est 3. On le place en dernier et le tableau
est trié.
2 3 3 5 6 7
III.2.2 Algorithme de tri par sélection
L’algorithme de tri par sélection (par exemple sur des nombres entiers) est le
suivant :
PROCEDURE TriSelection(entier *T, entier n)
début
entier k, i, imax, temp;
pour k ←n-1 à 1 pas -1 faire
/* recherche de l’indice du maximum : */
imax←0;
pour i←1 à k pas 1 faire
si T[imax] < T[i] alors imax←i;
fin faire
/* échange : */
temp←T*k+;
T*k+←T*imax+;
T*imax+←temp;
fin faire
fin
III.2.3. Estimation du nombre d’opérations
• Coût des échanges.
Le tri par sélection sur n nombres fait n−1 échanges, ce qui fait 3(n−1) affectations.
• Coût des recherches de maximum :
– On recherche le maximum parmi n éléments : au plus 4(n−1) opérations. (C’est le
nombre d’itérations de la boucle sur i pour k fixé égal à n-1)
– On recherche ensuite le maximum parmi n−1 éléments : au plus 4(n−2) tests.
6 3 4 2 3 5
En triant les deux premiers éléments on obtient :
3 6 4 2 3 5
trié
En insérant le troisième élément à sa place dans la liste triée :
3 4 6 2 3 5
trié
En insérant le quatrième élément à sa place dans la liste triée :
2 3 4 6 3 5
trié
Prof. BATUBENGA MWAMBA Nz. J.D. 33
Cours d’Algorithmique et Structures de données
2 3 3 4 6 5
trié
En insérant le sixième élément à sa place dans la liste triée :
2 3 3 4 5 6
Pour insérer un élément à sa place, on doit décaler les éléments déjà triés qui sont
plus grands.
III.3.2 Algorithme de tri par insertion
L’algorithme de tri par insertion est le suivant :
PROCEDURE TriInsertion(entier *T, entier n)
Début
entier k, i, v;
pour k ←1 à n-1 pas 1 faire
v←T*k+;
i←k-1;
/* On décale les éléments pour l’insertion */
tant que i≥0 et v < T*i+ faire
T*i+1+←T*i+;
i←i-1;
fin faire
/* On fait l’insertion proprement dite */
T*i+1+←v;
fin faire
fin
III.3.3. Estimation du nombre d’opérations
L’indice k varie de 1 à n−1, soit n−1 valeurs différentes. Pour chaque valeur de k,
l’indice i prend au plus k valeurs différentes. Le total du nombre d’opérations est
donc au plus de :
3(n−1) + 4(1 + 2 + 3 + ··· + (n−2) + (n−1))
6 3 4 2 3 5
3 6 4 2 3 5
3 4 6 2 3 5
3 4 2 6 3 5
3 4 2 3 6 5
3 4 2 3 5 6
On fait une deuxième passe :
3 2 4 3 5 6
3 2 3 4 5 6
2 3 3 4 5 6
III.4.2 Algorithme du tri par bulles
L’algorithme du tri par bulles est le suivant :
PROCEDURE TriBulle(entier *T,entier n)
Début
entier i, k, temp;
/* pour chaque passe */
pour k ←n-1 à 1 pas -1 faire
/* on fait remonter le plus grand */
pour i←1 à k pas 1 faire
si T[i] < T[i-1] alors
/* échange de T[i-1] et T[i] */
temp←T*i+;
T*i+←T*i-1];
T[i-1+←temp;
fin si
fin faire
fin faire
fin
III.4.3. Estimation du nombre d’opérations
La variable k prend n−1 valeurs différentes. Pour chacune de ces valeurs, la variable i
prend k valeurs différentes. Le nombre total d’opérations est plus petit que :
2(n−1) + 5(1 + 2 + 3 + ··· + (n−2) + (n−1))
Pour faire cela, on introduit un indice i, initialisé à imin, et un indice j, initialisé à imax-
1. On fait ensuite remonter l’indice i jusqu’au premier élément supérieur à v, et on
fait redescendre j jusqu’au premier élément inférieur à v. On échange alors les
éléments d’indices i et j. On itère ce procédé tant que i≤j. À la fin, on place la valeur
pivot en position i.
fin faire
tant que j>=imin et T[j] >= v faire
j←j-1;
fin faire
si i<j alors /* échange */
temp←T*i+;
T*i+←T*j+;
T*j+←temp;
fin si
fin faire
T*imax+←T*i+; T*i+←v; /* On place la valeur pivot en i */
retourner i; /* renvoie l’endroit de la séparation */
fin
III.6. Exercices
1. On se propose de trier une liste de noms de manière à les ranger par ordre
alphabétique. On utilise la méthode de tri insertion ; écrire un programme réalisant
ce tri.
2. Soit une liste linéaire suivante : 60, 43, 10, 50, 32, 55, 40, 66, 77, 54, 99, 80, 88.
a) Appliquer l’algorithme de tri insertion pour ranger cette liste par ordre croissant.
b) Appliquer l’algorithme de tri sélection moyennant aménagement pour ranger
cette liste par ordre décroissant.
3. Soit une pile P1 contenant solution une liste non triée de côtes de 10 étudiants.
Ecrire un algorithme qui crée une autre pile P2 contenant la liste triée des éléments
de P1.
4. Soit un tri rapide dont l’idée est : choisir un élément pivot au hasard dans un
tableau, on partitionne celui-ci en deux sous-tableaux tels que les éléments du
premier soient plus petits que le pivot, et les éléments du second plus grands, et on
appelle récursivement la procédure de tri sur ces sous-tableaux. Le cas d’arrêt est
obtenu quand les tableaux sont de taille 0 ou 1, et donc naturellement triés.
Appliquer ce tri rapide à la liste suivante : 32, 51, 27, 85, 66, 77, 23, 13, 46, 57.
Les éléments d’un tableau sont comme des cases rangées successivement dans la
mémoire centrale. Ils sont numérotés par des indices.
Par exemple, les éléments du tableau linéaire tab déclaré ci-dessus, qui comporte 100
éléments, ont des indices 0, 1, 2, ..., 99 ; soit tab*0+, tab|1+, tab*2+, …, tab*99+.
Et les éléments du tableau tabs qui comporte 50 lignes et 40 colonnes, ont des
indices *0, 0+, *0, 1+, …, *1, 0+, *1, 1+, … *49, 39+
Les indices des éléments d’un tableau commencent à 0 et non pas à 1. Par
conséquent, les éléments d’un tableau linéaire à N éléments ont leurs indices allant
de 0 à N−1. L’accès dans ce cas à un élément d’indice supérieur ou égal à N
provoquera systématiquement un résultat faux, et généralement une erreur
mémoire (erreur de segmentation). Il est de même pour un tableau binaire à M lignes
et N colonnes, dont les éléments ont leurs indices allant de (0, 0) à (M-1, N-1). L’accès
dans ce cas à un élément d’indice de ligne supérieur ou égal à M et/ou d’indice de
colonne supérieur ou égal à N provoquera systématiquement un résultat faux (erreur
mémoire). On parle de dépassement de tableaux.
Plus généralement, l’élément d’indice i dans le tableau linéaire tab est noté tab[i],
tandis que l’élément de la ligne i et colonne j dans le tableau tabs est noté tabs[i, j].
Les indices i et j doivent impérativement être des nombres entiers positifs ou nuls. Ils
peuvent aussi être le résultat de toute une expression comme dans tab[3*m+2], où m
est un entier.
Pour parcourir un tableau linéaire, on le balaye avec une boucle Pour (car le nombre
de cases est connu à l’avance) ; et pour un tableau binaire, on le balaye avec deux
boucles Pour imbriquées.
Exercice : Ecrire un algorithme qui lit les effectifs de personnes souffrant d’une de 5
maladies données, dans chacune de 24 communes de Kinshasa. Ensuite, il détermine
l’effectif total par maladie pour l’ensemble de la ville de Kinshasa.
IV.1.2. Structures
Une structure est un type qui permet de stocker plusieurs données, de même type ou
de types différents, dans une même variable de type structure. Une structure est
composée de plusieurs champs, chaque champ correspondant à une donnée.
Exemple : déclaration d’une structure Point qui contient trois champs x, y et z de réel.
struct point
{ /* déclaration de la structure */
Réel x, y, z; /* trois champs x, y, z se déclarent comme des variables */
/* mais on ne peut pas initialiser les valeurs */
};
La déclaration d’une variable de type struct point se fait ensuite comme pour une
autre variable :
struct point P; /* Déclaration d’une variable P */
Utilisation d’une structure
Une fois la variable déclarée, on accède aux données x, y, z du point P par un point.
Ces données sont désignées dans le programme par P.x, P.y, P.z. Ces données P.x, P.y,
P.z, ici de type réel, sont traitées comme n’importe quelle autre donnée de type réel
dans le programme.
Notons que l’on pourrait rajouter d’autres données des types que l’on souhaite à la
suite des données x, y, z dans la structure. Voici un exemple de programme avec une
structure point.
Début
struct point
{ /* déclaration de la structure */
Réel x, y, z;
};/* ne pas oublier le point-virgule */
struct point P;
Afficher("Veuillez entrer les coordonnées d’un point 3D :");
Lire(P.x, P.y, P.z);
Afficher("L’homothétie de centre O et de rapport 3");
Afficher("appliquée à ce point donne :’’);
Afficher(3*P.x, 3*P.y, 3*P.z);
Fin
IV.1.3. Pointeur
La mémoire centrale d’un ordinateur est composée d’un très grand nombre d’octets.
Chaque octet est repéré par un numéro appelé adresse de l’octet. Chaque variable
dans la mémoire occupe des octets contigus, c’est-à-dire des octets qui se suivent.
Par exemple, un float occupe 4 octets qui se suivent. L’adresse de la variable est
l’adresse de son premier octet.
L’adresse d’une variable peut elle-même être mémorisée dans une autre variable. Les
variables dont les valeurs sont des adresses des autres vraibles s’appellent des
pointeurs. Nous notons ici une variable pointeur par PTR.
À partir d'un type t quelconque, on peut définir un typepointeur_sur_t. Les variables
du type pointeur_sur_t contiendront, sous forme d'une adresse mémoire, un mode
d'accès au contenu de la variable de type t, appelée variable pointée. Celle-ci
n'aura pas d'identificateur, mais sera accessible uniquement par l'intermédiaire de
son pointeur. Nous notons ici une variable pointée par le pointeur PTR avec
l’identificateur *PTR+ ou PTR^.
Quelques syntaxes :
Déclarer un pointeur : PTR : pointeur_sur_t ou ^t.
Créer une variable dynamique de type t : allouer(PTR)
Libérer la place occupée par la variable dynamique créée : desallouer(PTR)
Affecter une valeur à la variable pointée : PTR^ ← <expression de type t>
Lire une valeur de type t et la mettre dans la variable pointée : lire(ptr^)
Afficher la valeur présente dans la zone pointée : écrire(ptr^)
Exemple :
L'algorithme suivant n'a comme seul but que de faire comprendre la
manipulation des pointeurs. À droite les schémas montrent l'état de la mémoire en
plus de ce qui s'affiche à l'écran.
Variables
ptc : ^chaîne de caractères
Début
allouer(ptc)
ptc^ ←'chat'
écrire(ptc^)
allouer(ptx1)
lire(ptx1^)
ptx2 ←ptx1
écrire(ptx2^)
ptx2^ ←ptx1^ + ptx2^
écrire(ptx1^, ptx2^)
ptx1 ←Nil
écrire(ptx2^)
désallouer(ptx2^)
ptx2 ←Nil
ptc ←Nil
Fin
Exemple :
Algorithme Pointeur2;
Structure fiche {
Nom : Chaine de caractères
Note : entier
}
pfiche : ^fiche
var p, q, Meilleur : pfiche
Début
Allouer(p)
Allouer(q)
p^.nom ← “Pascal”
p^.note ← 18
q^.nom ← “Blaise”
q^.note ← 16
Si p^.note > q^.note alors Meilleur ← p sinon Meilleur ← q
Ecrire(“Le meilleur est : “, Meilleur^.nom)
Fin
On peut aussi définir un type structuré avec un ou plusieurs champs qui soient du
type pointeur qui pointe vers le même type structure. Ceci permet d’implémenter
des structures comme des listes chaînées, arbres et graphes.
Exemple :
Structure Maillon {
valeur : Entier
suivant : ^Maillon
}
IV.2. Généralités sur les listes chaînées
Une liste est une suite d’un nombre variable d’objets de même type, appelés
éléments de la liste. Il s’agit d’une collection linéaire d’éléments d’information. Elle
est enregistrée sur un support adressable et il existe une action simple permettant de
passer d’un élément à l’élément suivant s’il existe.
Deux points sont importants dans ce qui précède :
1. Le fait que la liste soit constituée d’un nombre variable d’éléments implique que
ceux-ci puissent être crées ou supprimés en fonction des besoins du traitement :
ce sont des objets dynamiques.
2. La liste est enregistrée sur un support adressable : pour désigner un élément, on
utilisera en général un pointeur. Le support pourra être la mémoire centrale ou
un périphérique à accès sélectif.
Les tableaux constituent un moyen de rangement de données avant leurs
traitements. Ils présentent certains inconvénients. Par exemple, il est relativement
cher d’insérer et de supprimer des éléments dans un tableau. De plus comme un
tableau occupe généralement un bloc d’espace mémoire, il n’est pas possible d’en
doubler ou d’en tripler simplement la taille quand cela est nécessaire. Pour cette
raison, on dit que les tableaux sont des listes définies comme des structures des
données statistiques. Comment représenter une liste dans une mémoire adressable
où il faut pouvoir y effectuer différents traitements tels que :
- accéder au premier élément (tête de liste) ;
- accéder aux divers autres éléments ;
- supprimer ou ajouter des éléments.
Une première manière de ranger une liste en mémoire est d’intégrer à chaque
élément de la liste un champ appelé lien ou pointeur qui contient l’adresse de
l’élément suivant de la liste. Dans ces conditions, les éléments successifs de la liste
n’ont plus besoin d’occuper en mémoire des positions adjacentes. Cela rend plus
facile l’adjonction et la suppression.
Il en résulte que si le problème posé est de rechercher des éléments dans la liste pour
les insérer ou les supprimer, comme dans le cas du traitement de texte, il vaut mieux
ranger les données dans une liste utilisant des pointeurs que dans un tableau. Ce type
de structure est appelé liste chaînée. Les listes circulaires et bidirectionnelles (ou
doublement chaînées) sont les généralités naturelles des listes chaînées.
Une deuxième manière de ranger une liste en mémoire est la représentation
contiguë. Une liste contiguë est une liste dont les éléments sont adjacents sur le
support adressable. Nous n’insisterons pas sur cette représentation, car elle est peu
utilisée à cause de sa structure et des limitations de traitement qui en découle.
NAME
ou
START
X
(2) (1)
INFO LINK
1
2
3 O 6
4 T 0
START 9
5
6 11
7 X 10
8
9 N 3
10 I 4
11 E 7
12
LINKPTR, nous devons effectuer deux tests. D’abord, il faut voir si nous n’avons pas
atteint la fin de la liste, c'est-à-dire contrôler si PTR= NULL, sinon, nous cherchons si
INFOPTR = ITEM.
Algorithme
1. Poser PTR = START
2. Répéter étape 3 tant que PTR NULL
3. Si ITEM = INFO PTR alors :
Poser LOC = PTR, et Exit
Si non
Poser PTR= LINKPTR PTR pointe sur le nœud suivant
Fin de la structure if
Fin de boucle de l’étape2
4. Poser LOC= NULL La recherche est un échec
5. Exit
Remarque : La fonction de complexité de cet algorithme est la même que celle de
l’algorithme de recherche linéaire dans les tableaux linéaires. Dans le cas le plus
favorable, f (n) = 0 (n), dans le cas courant f (n) = 0 (n/2)
B.LISTE est ordonnée.
On cherche toujours ITEM en faisant défiler LISTE, en utilisant un pointeur PTR et en
comparant ITEM au contenu de INFO PTR de chaque nœud de LISTE, pris un à un.
On arrête le processus dès que ITEM excède à INFOPTR.
L’algorithme suivant trouve la position LOC du nœud où ITEM apparaît pour la
première fois dans LIST :
Algorithme :
1. Poser PTR = START
2. Repeat étape 3 tant que PTR NULL :
3. Si ITEM INFO PTR, alors :
Poser PTR = LINKPTR PTR pointe sur le nœud suivant
sinon si ITEM = INFOPTR, alors :
Poser LOC = PTR, et Exit Succès
Sinon :
Poser LOC = NULL, et Exit ITEM est supérieur à INFOPTR
Fin de structure if
*Fin de boucle de l’étape 2+
4. Poser Loc = NULL
5. Exit.
Remarque : La complexité est toujours la même que celle dans autres algorithmes
de recherche linéaire. C'est-à-dire le temps d’exécution dans le cas le plus
défavorable et proportionnel à n : f (n) = 0(n), n nombre d’élément de LISTE, dans le
cas le plus courant f (n) = 0 (n/2).
IV.4.3. Insertion dans une liste chaînée
Soit LISTE, une liste chaînée de nœuds successifs A et B. on désire insérer un nœud N
entre A et B.
START
Nœud A Nœud B
X
xXX
(Avant insertion)
START
Nœud A Nœud B
X
Nœud N (Après insertion)
On note que le nœud A pointe à présent sur le nœud N et le nœud N pointe sur le
nœud B. notons que les cases mémoires inutilisées dans les tableaux sont aussi liées,
de manière à former une liste chaînée utilisant AVAIL (pour available : disponible)
comme variable pointeur de liste. La liste de mémoire disponible sera aussi appelée
la liste AVAIL. Une telle structure de données sera souvent désignée en écrivant
LIST (INFO, LINK, START, AVAIL). La figure de l’insertion se présente de la manière
suivante.
START
Nœud A Nœud B
X
AVAIL
Nœud N
X
START
Liste des emplacements libres
AVAIL
A) Algorithme 1
On suppose que notre liste chaînée ne soit pas obligatoirement ordonnée et qu’il n’y
ait aucune raison que le nœud nouveau soit inséré en une position particulière
donnée dans la liste. Il s’agit d’une insertion en tête de liste.
Cet algorithme insère ITEM en tête de la liste dont il devient alors le premier nœud.
ALGO 1 (INFO, LINK, START, AVAIL, ITEM)
1. Si AVAIL= NULL, alors WRITE OVER FLOW, et Exit
2. Poser NEW= AVAIL et AVAIL= LINK [AVAIL]
*Extraction du premier nœud dans la liste AVAIL+
3. Poser INFO [NEW] = ITEM [copie des données nouvelles dans le
nœud nouveau+
4. Poser LINK [NEW] = START [le nouveau nœud pointe maintenant
sur le nœud précédemment en première position+.
5. Poser START = NEW [modification de START qui pointe maintenant
sur le nœud nouveau+
6. Exit.
START
X
NEW
Insertion de ITEM au début d’une liste
ITEM
B. Algorithme 2
Il s’agit d’une insertion après un nœud donnée. Supposons que nous ayons la valeur
LOC, où LOC est la position d’un nœud A dans une liste chaînée LIST ou LOC= NULL.
L’algorithme qui suit insère ITEM dans LIST de manière à ce que ITEM suive le nœud A
ou, lorsque LOC = NULL, ITEM soit le premier nœud. Soit le nœud nouveau NEW (N).
Si LOC = NULL, N est inséré comme premier nœud (c’est-à-dire en tête de liste) dans
LIST, c’est le cas de l’algorithme 1. Si non, le Nœud N pointera sur le nœud B (qui à
l’origine suivait le nœud A) en vertu de l’affectation LINK *NEW+ = LINK [LOC] et le
nœud A pointera sur le nœud nouveau N par suite de l’affectation LINK *LOC+ = NEW.
Cet algorithme insère ITEM comme premier nœud de position LOC ou insère ITEM
comme premier nœud de la liste si LOC= NULL.
SAVE PTR
START
X
On obtient aussi un algorithme qui insère ITEM dans une liste chaînée ordonnée.
ALGO (INFO, LINK, START, AVAIL, ITEM)
1. *Application de l’algorithme 3 pour déterminer la position du nœud
précédent ITEM]
CALL ALGO 3 (INFO, LINK, START, ITEM, LOC)
2. *Application de l’algorithme 2 pour insérer ITEM après le nœud en
position LOC]
CALL ALGO 2 (INFO, LINK, START, AVAIL, LOC, ITEM).
3. Exit.
D. Exemple : Considérons l’exemple d’un hôpital qui comporte 12 lits dont 9 sont
occupés. On suppose que la liste est rangée dans le tableau linéaire BED et LINK,
c'est-à-dire le patient dont le lit est K est affecté à BED [K].
On a une liste alphabétique des patients avec la liste AVAIL. Supposons que JEAN doit
y être ajouté. Nous allons appliquer ALGO ou plus précisément ALGO 3 d’abord et
l’algorithme ALGO ensuite. Il convient de noter que ITEM= JEAN et INFO=BED.
a. ALGO 3 (BED, LINK, START, ITEM, LOC)
1. Puisque START NULL, le contrôle est transféré à 2
2. Puisque BED [5] = AKELA JEAN, le contrôle est transféré à 3
3. SAVE = 5 et PTR = LINK [5] = 3
4. On exécute étapes ci-après :
BED (3) = DIANA JEAN, SAVE = 3 et PTR = LINK [3]=11
BED (11)= FATAKI JEAN, SAVE = 11 et PTR = LINK [11]=8
BED (8) = GABY JEAN, SAVE = 8 et PTR = LINK[8]=1
Puisque BED (1) = KIBOKO JEAN, nous avons LOC = SAVE = 8 et Return.
b. ALGO 2 ( BED, LINK, START, AVAIL, LOC, ITEM) [Ici LOC = 8]
1. Puisque AVAIL NULL, le contrôle est transféré à 2
2. NEW=10 et AVAIL= LINK [10]=2
3. BED [10]= JEAN
4. Puisque LOC NULL nous avons
LINK [10] = LINK [8] = 1 et LINK [8] = NEW=10
5. Exit
Après insertion de JEAN, sur la liste des patients, les trois pointeurs AVAIL, LINK [10]
et LINK [8] ont été modifié.
Soit LIST, une liste en mémoire sous la forme LIST (INFO, LINK, START, AVAIL) et N un
nœud compris entre les nœuds A et B. on se propose de supprimer le nœud N dans la
liste chaînée. La suppression est exécutée dès que le champ de pointeur de nœud
suivant le nœud A est modifié de manière à ce qu’il pointe sur le nœud B.
Par conséquent, lorsqu’on supprime un nœud, il est nécessaire de garder la trace de
l’adresse du nœud qui précède immédiatement le nœud qui va être supprimé.
START
Nœud A Nœud N Nœud B
X
A
Avant suppression
START
Nœud A Nœud N Nœud B
A A X
Après suppression
Il convient de souligner que dès qu’un nœud est supprimé dans une liste, il sera
incorporé en tête de la liste AVAIL. On a les opérations suivantes :
(1) Le champ de pointeur de nœud suivant le nœud A pointe sur B, sur lequel pointait
précédemment le nœud N.
(2) Le champ de pointeur de nœud suivant le nœud N pointe maintenant sur le nœud
originellement premier de la liste d’espace libre, sur lequel pointait
précédemment AVAIL.
(3) AVAIL pointe sur le nœud supprimé.
N.B : Cas particuliers :
a) Si le nœud N supprimé est le premier nœud de la liste, START pointeur sur le nœud
B.
b) Si le nœud N supprimé est le dernier nœud de la liste, le nœud A contiendra le
pointeur Null.
START
Nœud Nœud
Nœud N
A B X
X
Exemple :
Considérons la liste des malades d’un hôpital (IV. 3), supposons que GABY soit sort de
l’hôpital. Il en résulte que BED (8) est vide. Par conséquent, pour que la liste chaînée
existe toujours, il faut exécuter les trois modifications suivantes des champs de
pointeurs :
LINK [11] = 10 LINK [8] = 2 AVAIL = 8
La première signifie que FATAKI (II) pointe sur JEAN.
La deuxième et la troisième modification ajoutent le lit vide à liste AVAIL. Avant
d’effectuer la suppression, il fait trouver le nœud BED *8+ supprimé.
IV.4.6. Algorithmes de suppression
Il existe une diversité d’algorithme de suppression dans les listes chaînées qui
répondent à des besoins nombreux. Nous nous proposons d’étudier ici deux
algorithmes :
START
Nœud 1 Nœud 2 Nœud 3
trace de la position du nœud précédent en utilisant une variation SAVE comme dans
les autres cas déjà étudiés.
SAVE = PTR et PTR = LINK [PTR]
Le défilement va continuer tant que INFO [PTR]
INFO [PTR] ITEM, en d’autres termes si INFO *PTR+ = ITEM, le défilement est
terminé. PTR contient alors la position LOC du nœud N et SAVE, la position LP du
nœud précédent N. notons que l’instruction INFO *START+ = ITEM signifie que N est le
premier nœud. La procédure PRO détermine la position LOC du premier nœud N qui
contient ITEM est la position LP du nœud précédent N. si ITEM n’appartient pas dans
la liste, la procédure pose LOC = NULL, si ITEM apparaît dans le premier nœud, elle
pose LP = NULL.
PRO ( INFO, LINK, START, ITEM, LOC, LP).
1. [liste vide ?] si START = NULL alors
Poser LOC = NULL et LP = NULL et Return
[Fin de la structure Si]
2. *ITEM dans le premier nœud ?] si INFO [START] = ITEM alors
Poser LOC = START et LP = NULL et Return.
[Fin de la structure Si]
3. Poser SAVE = START et PTR = LINK [START]
[Initialisation du pointeur]
4. Répéter les étapes 5 et 6 tant que PTR NULL
5. Si INFO [PTR] = ITEM alors
LOC = PTR et LP = SAVE, et Return
[Tin de la direction if]
6. Poser SAVE = PTR et PTR = LINK [PTR]
[Mise à jour des pointeurs]
[Fin de la boucle de l’étape 4+
7. Poser LOC = NULL [Echec de la recherche]
8. Return
Avec cette procédure, nous allons présenter un algorithme dont l’objet est de
supprimer dans une liste chaînée le premier nœud N qui contient une information
donnée ITEM. L’algorithme DEL 2 exécute cette opération.
DEL2 (INFO, LINK, START, AVAIL, ITEM).
1. [Utiliser PRO pour trouver les positions de N et du nœud qui le précède+
CALL PRO (INFO, LINK, START, ITEM, LOC, LP)
2. si LOC = NULL alors Ecrire : ITEM n’est pas dans la liste, et exit.
3. [Supprimer le nœud+
Si LP = NULL, alors :
Poser START = LINK *START+ *supprimer le premier nœud+
Sinon :
Poser LINK [LP] = LINK [LOC] [supprime le Nœud N en général+
[Fin de la structure Si]
4. *Renvoyer le nœud supprimé à la liste AVAIL]
Poser LINK [LOC] = AVAIL et AVAIL = LOC
5. Exit
Exemple :
BED LINK
1 KIBOKO 7
START 5 2 6
3 DIAMA 11
4 MUTOMBO 12
5 AKELA 3
AVAIL 6 0
8
7 LUBOYA 4
8 2
9 SULUA 0
10 JEAN 1
11 FATAKI 10
12 NDEMA 9
X
2. Une liste circulaire à en-tête est une liste à en-tête dont le dernier nœud pointe
sur le nœud d’en-tête.
START
B. Propriétés
1. Le nœud dans une liste à en-tête représente un nœud ordinaire, pas le nœud
d’en-tête. Par conséquent, le premier nœud dans une liste à en-tête est le
nœud qui suit le nœud d’en-tête et sa position est LINK [START] et non START
comme dans une liste ordinaire.
2. Dans l’algorithme qui utilise une variable pointeur pour faire défiler une liste
circulaire à en-tête, on commence par PTR = LINK [START] et non PTR = START
et on termine par PTR = START et non PTR = NULL.
3. Les listes circulaires à en-tête sont plus fréquemment utilisées que les listes
chaînées ordinaires parce qu’elles rendent de nombreuses opérations plus
faciles à formuler et à implémenter. Les raisons sont :
a. le pointeur vide n’est pas utilisé et, par conséquent, tous les pointeurs
contiennent des adresses valides. (on supprime le vide)
b. chaque nœud ordinaire possède un antécédent ou prédécesseur et il en
résulte que le premier nœud ne constitue plus un cas particulier.
c. Recherche dans une liste circulaire à en-tête
c1. L’algorithme ci-après détermine la position LOC du premier nœud LIST qui
contient ITEM quand LIST est une liste chaînée circulaire à en-tête.
CHA1 [INFO, LINK, START, LOC]
1. Poser PTR = LINK [START]
2. Répéter tant que INFO [PTR] ITEM et PTR NULL
Poser PTR = LINK [PTR]
[Le nœud PTR pointe sur le nœud suivant+
3. Si INFO [PTR] = ITEM, alors :
Poser LOC = PTR
Sinon
Poser LOC = NULL
[Fin de structure Si]
4. Exit.
Remarque : Dans la boucle de recherche (étape 2) les deux testes qui contrôlent la
boucle n’étaient pas exécutés au même moment dans l’algorithme relatif aux listes
ordinaires, car dans une liste ordinaire INFO *PTR+ n’est pas défini quand PTR = NULL.
(PTR = START)
c2. L’algorithme ci-après trouve les positions LOC du premier nœud N qui
contient ITEM et LP du nœud précédent N qui contient ITEM et LP du
nœud précédent N quant LIST est une liste circulaire à en-tête.
CHA2 [INFO, LINK, START, ITEM, LP)
1. Poser SAVE = START et PTR = LINK [START]
[Initialisation du pointeur]
2. Répéter tant que INFO [PTR] ITEM et PTR START
Poser SAVE = PTR et PTR = LINK [PTR]
[Mise à jour du pointeur]
[Fin de la boucle]
3. Si INFO [PTR] = ITEM, alors
Poser LOC = PTR et LP = SAVE
Sinon : Poser LOC = NULL et LP = SAVE
[Fin de la structure Si]
4. Exit
c3. L’algorithme suivant supprime le premier nœud N qui contient ITEM quand
LIST est une liste circulaire à en-tête.
1. *utiliser la procédure CHA2 pour trouver les positions de N et du nœud
qui le précède]
2. Si LOC = NULL, alors WITE : ITEM n’est pas dans la liste et Exit.
3. LINK *LP+ = LINK *LOC+ *supprimer le nœud+
4. *Renvoyer le nœud supprimé à la liste AVAIL+
Poser LINK [LOC] = AVAIL et AIVAIL = LOC
5. Exit
IV.4.8. Liste circulaire
Une liste chaînée dont le dernier nœud pointe sur le premier nœud au lieu de
contenir le pointeur vide, est appelée une liste circulaire.
En effet, une liste simplement chaînée ne permet pas, à partir d’un élément,
d’accéder directement à ceux qui le précèdent. Pour remédier à cet inconvénient, on
peut faire pointer le dernier élément sur le premier et obtenir ainsi une liste
circulaire qu’on appelle aussi liste en anneau.
START
START
Coefficient Exposant
0 -1 2 8 -5 7 -3 2 4 0
Cela entraîne qu’il est possible de supprimer N dans la liste sans avoir à n’en
parcourir aucune partie.
Une liste bidirectionnelle est un ensemble d’éléments d’informations appelés nœuds
où chacun d’entre eux est divisé en trois parties :
(1) Un champ d’information INFO qui contient les données de N
(2) Un champ de pointeur FORW qui contient la position du nœud suivant
dans la liste.
(3) Un champ de pointeur BACK qui contient la position du nœud précédent
dans la liste.
Ce type de liste nécessite également deux pointeurs de liste : FIRST, qui pointe sur le
premier nœud de la liste, et LAST, qui pointe sur le dernier.
Le pointeur vide apparaît dans le champ FORW du dernier nœud de la liste ainsi que
le champ BACK du premier nœud de la liste.
Nœud N
En utilisant la variable FIRST et le champ de pointeur FORW, nous pouvons, comme
auparavant, parcourir une liste bidirectionnelle dans le sens direct habituel.
De plus, en utilisant la variable LAST et le champ de pointeur BACK, nous pouvons
maintenant la parcourir également dans le sens rétrograde.
Soit LOC A et LOC B les positions de deux nœuds A et B.
FORW [LOC A] = LOC B si et seulement si
BACK [LOC B] = LOC A c'est-à-dire le nœud B suit le nœud A si seulement si le nœud A
précède le nœud B.
B. Représentation en mémoire
Les listes bidirectionnelles peuvent être rangées en mémoire sous forme de tableaux
linéaires de la même manière que les listes monodirectionnelles, à cela qu’elles
nécessitent deux tableaux de pointeurs, FORW et BACK au lieu d’un seul, LINK, ainsi
que deux pointeurs de liste, FIRT et LAST, au lieu d’un START.
La liste AVAIL de l’espace mémoire disponible dans le tableau sera toujours rangée
sous la forme d’une liste monodirectionnelle faisant appel à FORW comme champ de
pointeur, puisque nous ne supprimons et nous n’insérons de nœud qu’en début de la
liste AVAIL.
C. Exemple
On reprend l’exemple de 9 patients qui sont réparties entre 12 lits dans une salle
d’hôpital. La liste alphabétique peut être organisée en une liste bidirectionnelle. Les
valeurs de FIRST et du pointeur FORW sont respectivement, les même que celles de
START et du tableau LINK et par conséquent, la liste peut être parcourue selon l’ordre
alphabétique direct. En outre, l’utilisation de LAST et du tableau de pointeur BACK
permet de parcourir cette liste selon l’ordre alphabétique inverse c'est-à-dire que
LAST pointe sur SULUA dont le champ de pointeur BACK pointe sur NDEMA, dont le
champ de pointeur pointe sur MUITOMBO et ainsi de suite.
Date Lieu
B C
D E G H
F J K
A chaque nœud d'un arbre binaire T est affecté un numéro de niveau. La hauteur
d'un arbre binaire est le nombre de nœuds qui constituent le plus long chemin de la
racine à une feuille.
5
4 4
1 1
3
La hauteur de cet arbre est 5 et on fait figurer dans chaque nœud la hauteur associée.
Un arbre binaire complet est un arbre binaire de hauteur k possédant 2 k - 1 nœuds.
Puisqu'il n'a pas de trous, un tel arbre peut être représenté par un tableau de 2 k - 1
éléments, tout simplement en rangeant les valeurs des nœuds niveau par niveau.
Autrement dit, un arbre binaire est considéré comme complet si toutes ses feuilles
sont au même niveau et que chacun de ses nœuds internes à deux enfants.
L'exemple ci-après représente l'arbre complet T15 (arbre à 15 nœuds) de hauteur 4.
Un arbre binaire presque complet est un arbre binaire dont tous les niveaux sont
pleins sauf le dernier, qui ne comporte que les p nœuds de gauche (un tel arbre n'est
pas complet, mais au moins il n'a pas de trous). Dans une représentation par tableau
définie comme pour les arbres binaires complets, il ne remplit donc que le début du
tableau.
Un arbre binaire T est appelé arbre pair ou encore arbre binaire étendu si chacun de
ses nœuds N possède soit 0, soit 2 enfants.
Dans ces conditions, les nœuds qui possèdent 2 enfants sont appelés, nœuds internes
et ceux qui en possèdent 0 nœuds, sont externes.
V.2.3. Représentation des arbres binaires en mémoire
On peut représenter un arbre binaire de 2 manières :
La première et la plus courante s'appelle « représentation chaînée », elle est
analogue à la représentation en mémoire des listes chaînées.
La seconde qui utilise un tableau unique est appelée « représentation
séquentielle de T ».
Dans n'importe quelle représentation d'un nœud, l'utilisation doit avoir un accès
direct à la racine R de T et un nœud N quelconque de T.
A. Représentation séquentielle d'un arbre
Soit T, un arbre complet. Il existe une manière très efficace de définir la
représentation séquentielle. Cette représentation utilise un tableau linéaire T en
mémoire :
a) La racine R de T est rangée en TREE [1]
b) Si un nœud occupe TREE *K+, son enfant de gauche est rangé en TREE *2*K+ et
K /
K=1
45 1 45
2 22
K=2 K=3 3 77
22 77 4 11
K=4 K=5 K=6
5 30
11 30 90
6 90
9 15
15 88
25 10 25
12 88
B C
D E G H
F J K
Remarque :
Dans la pratique, il n'y aura qu'un élément d'information à chaque nœud N de l'arbre
T. Un enregistrement entier pourrait être rangé en un nœud N.
En d'autres termes, INFO peut être en réalité un tableau linéaire d'enregistrements
ou un ensemble des tables parallèles.
Puisque dans un arbre binaire les nœuds peuvent être supprimés ou insérés, nous
supposerons implicitement que les adresses vides dans le tableau INFO, LEFT, RIGHT
forment une liste chaînée avec un pointeur AVAIL.
Généralement, on range le pointeur de AVAIL dans le tab.LEFT ; Toute adresse
invalide peut être choisi comme adresse NULL.
V.3. Opérations sur les arbres binaires
V.3.1. Défilement des arbres binaires
Il existe trois manières de faire défiler un arbre T de racine R.
a) Pré-ordre, c'est-à-dire commencer par le nœud (N), le sous-arbre de gauche
(G) et enfin le sous arbre de droite (D), d'où NGD ou NLR.
1) Traiter la racine R.
2) Faire défiler le sous-arbre de gauche de R
3) Faire défiler le sous-arbre de droite de R
b) In-ordre, c'est-à-dire commencer par le sous-arbre de gauche (G), le nœud (N),
et enfin le sous arbre de droite (D), d'où GND ou LNR.
1) Faire défiler le sous-arbre de gauche de R
2) Traiter la racine R.
3) Faire défiler le sous-arbre de droite de R
c) Post-ordre, c'est-à-dire commencer par le sous arbre de gauche, le sous arbre
de droite, enfin le nœud N, d'où GDN ou LRN.
1) Faire défiler le sous-arbre de gauche de R
2) Faire défiler le sous-arbre de droite de R
3) Traiter la racine R.
Remarquons que dans tous les algorithmes précédents sont définis récursivement
dans la mesure où ils entraînent le défilement dans un ordre donné de sous arbres.
La différence entre les trois algorithmes résident dans le choix du moment où la
racine R est traitée.
Exemple
Considérons l'arbre binaire T ci-après :
Aucun n’autre nœud n'est traité, car K n'a pas d'enfant de gauche
(7) Recherche ascendante, dépiler l’élément C de STACK, et poser PTR = C ⇒ on a
STACK = 0
Puisque PTR ≠ NULL, on retourne à l’étape (1) de l’algorithme.
(8) Descendre le chemin le plus à gauche de la racine avec PTR = C
(i) Traiter C et empiler son enfant de droite F sur STACK, on a STACK = 0, F
(ii) Traiter E (pas d’enfant de droite)
Aucun n’autre nœud n'est traité, car K n'a pas d'enfant de gauche
(9) Recherche ascendante, dépiler l’élément F de STACK, et poser PTR = C ⇒ on a
STACK = 0
Puisque PTR ≠ NULL, on retourne à l’étape (1) de l’algorithme.
(10) Descendre le chemin le plus à gauche de la racine avec PTR = F
(i) Traiter F (Pas d’enfant de droite)
Aucun n’autre nœud n'est traité, car F n'a pas d'enfant de gauche
(11) Recherche ascendante, dépiler l’élément NULL au sommet de STACK, et poser
PTR = NULL. Comme PTR = NULL, donc fin de l'algorithme.
Résultat : En regardant les points 2, 4, 6, 8 et 10, on a les résultats suivants A, B, D, G,
H, K, C, E, F.
Algorithme (de défilement suivant l'option pré-ordonné)
On part d'un tableau T rangé en mémoire. L'algorithme ci-après opère un défilement
pré-ordonné de T. On utilisera un tableau STACK pour ranger temporairement les
adresses de nœuds.
PRE (INFO, LEFT, RIGHT, ROOT)
1. Poser TOP = 1, STACK = NULL, PTR = ROOT
2. Répéter les étapes 3 à 5 tant que PTR ≠ NULL
3. Appliquer PROCESS à INFO [PTR]
(Process Operation qui consiste à traiter chaque élément du tableau)
4. Si RIGHT *PTR+ ≠ NULL, Empiler sur STACK
Poser TOP = TOP + 1, STACK [TOP] = RIGHT [PTR]
(Fin de si)
5. Si LEFT *PTR+ ≠ NULL, alors Poser PTR = LEFT *PTR+
Sinon, dépiler STACK
PTR = STACK [TOP] ; TOP = TOP - 1
(Fin de si)
6. EXIT
Remarque
L'opération PROCESS dans l'algorithme de défilement peut faire appel à certaines
variables qui doivent être initialisé avant qu'elle ne soit appliquée sur tout élément
du tableau.
B. Défilement in- ordre
On utilisera également un pointeur PTR qui contiendra (initialement) l'adresse du
nœud N. Ainsi, qu'un tableau STACK où seront rangées les adresses de nœud en vue
de leur traitement ultérieur.
Avec cet algorithme, un nœud n'est traité que lorsqu'il est dépilé de STACK.
Démarche à suivre :
Empiler initialement NULL sur STACK comme indicateur puis initialiser PTR =
racine ROOT (c-à-d PTR = ROOT). Répéter ensuite les étapes suivantes jusqu'à
ce que NULL soit dépilé de STACK.
a) Descendre le chemin le plus à gauche ayant PTR comme racine en
empilant chaque nœud N sur STACK et en stoppant (chaque fois qu'un
nœud N sans enfant de gauche est empilé sur STACK)
b) Recherche ascendante Dépiler et traiter les nœuds empilés sur STACK, Si
NULL est dépilé, alors fin de l'algorithme.
Si un nœud N possédant un enfant de droite R(N) est traité, alors Poser
PTR = R(N). En affectant PTR = RIGHT [PTR] et retourner à l'étape a.
On retiendra ici qu'un nœud N n'est traité que quand il est dépilé de
STACK. L'exemple ci-après permettra de mieux comprendre l'algorithme
qui va suivre.
1. STACK = 0, PTR = A
2. Descendre le chemin le plus à gauche de A en empilant le nœud ABDGK (on
aura) STACK = 0, A, B, D, G, K.
3. Recherche ascendante ;
Les nœuds K, G et D sont dépilés et traiter, ce qui laisse STACK = 0, A, B.
Ensuite, arrêter les traitements à D car D est un enfant de gauche.
Puis initialiser PTR = H enfant de droite de D.
4. Descendre le chemin le plus à gauche de la racine PTR = H, en empilant les
nœuds H et L sur STACK. Alors STACK = 0, A, B, H, L ; (L n'a pas d'enfants de
gauche).
5. Les nœuds L et H sont dépilés et traités, ce qui laisse STACK, 0, A, B. Arrêter le
traitement à H, car H a un enfant de gauche L.
Puis initialiser PTR = M enfant de droite de H.
6. Descendre le chemin le plus à gauche M, en empilant les nœuds M : STACK = 0,
A, B, M. Aucun autre nœud n'est empiler, car M n'a pas d'enfants de gauche.
7. *Recherche ascendante+, Les nœuds M, B et A sont dépilés et traités, ce qui
laisse STACK = 0.
Aucun entre élément de STACK n'est dépiler, car A possède un enfant de
gauche.
Initialiser PTR = C enfant de droite de A.
8. Descendre le chemin le plus à gauche de la racine C, en empilant les nœuds C
et F sur STACK. Alors STACK = 0, C, F.
9. *Recherche ascendante+. Le nœud E est dépilé et traité. Comme E n'a pas
d'enfants de droite, C est dépilé et traité et comme C n'a pas d'enfants de
droite, (l'élément suivant), NULL est dépilé. D'où, la fin de l'algorithme.
Résultat du défilement d'après les étapes 3, 5, 7, 0 les nœuds sont traités dans l'ordre
ci-après K, G, D, L, H, M, B, A, E, C.
Algorithme
ln (lNFO, LEFT, RIGHT, ROOT)
1. Poser TOP = 1, STACK [1] = NULL, PTR = ROOT
2. Répéter tant que PTR ≠ NULL
a) Poser TOP = TOP + 1 et STACK [TOP] = PTR
b) Poser PTR = LEFT [PTR]
(Fin de la bouche)
3. Poser PTR = STACK (TOP) et TOP = TOP - 1
4. Répéter les étapes 5 à 7 tant que PTR ≠ NULL
5. Appliquer PROCESS à INFO [PTR]
14 56
8 23
45 82
18 70
14 56
8 23
45 82
18 70
20
La procédure ci-après (FIND) détermine l'adresse L du nœud ITEM dans l'arbre T ainsi
que l'adresse PAR u parent de ITEM.
Il convient de faire remarquer que si :
1. L = NULL et PAR = NULL, c'est-à-dire l'arbre est vide.
2. L ≠ NULL et PAR = NULL, ITEM est la racine de l'arbre.
3. L = NULL et PAR ≠ NULL, ITEM n'est pas dans l'arbre et il peut être inséré à
l'arbre T en tant qu'un enfant du nœud N d'adresse PAR.
L'algo de recherche et d'insertion s'appuiera sur la procédure suivante qui détermine
les adresses d'un ITEM et de son parent.
FIND [INFO, LEFT, RIGHT, ROOT, ITEM, PAR, L]
1. [Arbre vide ?]
Si ROOT = NULL, alors poser L = NULL et PAR = NUL et Return
2. [ITEM en racine ?]
Si ITEM = INFO (ROOT), alors poser L = ROOT et PAR = NUL ; Return
3. [Initialiser les pointeurs PTR et SAVE]
Si ITEM < INFO (ROOT) alors
Poser PTR = LEFT (ROOT) et SAVE = ROOT
Sinon
Poser PTR = RIGHT [ROOT] et SAVE = ROOT
[Fin de si]
4. Répéter les étapes 5 et 6 tant que PTR ≠ NUL
5. [ITEM trouvé ?]
Si ITEM = INFO (PTR), alors L = PTR et PAR = SAVE ; Return
6. Si ITEM < INFO (PTR), alors
Poser SAVE = PTR et PTR = LEFT (PTR)
Sinon
Poser SAVE = PTR et PTR = RIGHT [PTR]
[Fin de Si]
[Fin de la boucle 4]
7. [Echec] Poser L = NULL et PAR = SAVE
8. EXIT.
Représentation linéaire
Supposons que nous supprimons le nœud 44 de l'arbre T. Ce nœud n'a pas d'enfant.
La suppression est effectuée en affectant simplement NULL au fils de droite de 44.
3. Supposons maintenant que nous supprimons le nœud 25 qui a deux enfants (15 et
50). On sait que le nœud 33 est le successeur IN-ordonné du nœud 25. La suppression
est affectée en enlevant tout d'abord le nœud 33 puis en le substituant au nœud 25.
En mémoire de l'ordinateur, ce remplacement est effectué en modifiant tout
simplement le pointeur, pas en déplaçant le contenu d'un nœud d'une adresse à une
autre.
Le nœud 75 reste toujours la valeur de INFO *7+. En ce moment, l'arbre devient :
B. Algorithme de suppression
Soit un arbre binaire T en mémoire et un ITEM d'information donnée. Cet algorithme
supprime ITEM dans l'arbre T.
1. (Trouver les adresses de ITEM et de son parent, c'est-à-dire en la faire)
CALL FIND [INFO, LEFT, RIGHT, ROOT, ITEM, LOC, PAR]
2. [Est-ce qu'item est dans cet arbre là ?] Si LOC = NUL alors
Ecrire « ITEM n'est pas sur l'arbre » et sortir
3. Si RIGHT *LOC+ ≠ NULL et LEFT *LOC+ ≠ NULL
Alors CALL SUP 2 [INFO, LEFT, RIGHT, ROOT, LOC, PAR]
Sinon CALL SUP 1 [INFO, LEFT, RIGHT, ROOT, LOC, PAR]
[fin de si]
4. (respecter le nœud supprimé à la liste AVAIL. On le fait en :)
Poser LEFT [LOC] = AVAIL et AVAIL = LOC
5. Sortir
V.4. Les arbres généralisés
V.4.1. Généralités
Un arbre généralisé en défini comme un ensemble non vide T des éléments appelés
nœuds tels que T comporte un élément particulier R appelé racine de T. Les autres
éléments de T forment un ensemble ordonné de 0 ou plusieurs arbres disjoints T1, T2,
T3, …, Tn. Dans ces conditions, on dit que T1, T2, T3, ... , Tn sont des sous- arbres de la
racine R, et les racines de T1, T2, ... , Tn sont appelés successeurs de R.
Si N est un nœud qui a pour successeurs S1, S2, S3, ..., Sn. N est dit parent de Si et les Si
sont appelés enfants de N. Les Si sont dits frères les uns des autres.
Les enfants d'un même nœud sont ordonnés de gauche à droite. Un arbre généralisé
peut être vu généralement comme un graphe connexe sans cycle. Il est orienté et
réuni sur un ensemble fini X, tel que.
1. Il y a dans X un nœud distingué appelé « racine » de l'arbre que nous notons
habituellement N.
2. Les autres nœuds, s'il y en a, sont partitionnés en N sous-ensembles disjoints
T1, T2, T3, ..., Tn qui sont alors tous des arbres des racines respectives T 1, T2, T3,
... , Tn ; Si l'on désire les noter ainsi, tel que , il existe un arc
d'origine N et d'extrémité Ti. Ces arbres sont appelés les sous-ensembles de X.
Remarque
1. Une feuille est un nœud qui n'a pas de fils. On l'appelle aussi un nœud
terminal. C'est un sommet de degré 1. Un nœud intérieur appelé encore un
Prof. BATUBENGA MWAMBA Nz. J.D. 94
Cours d’Algorithmique et Structures de données
Cet exemple nous montre un arbre généralisé, car la racine A possède 3 enfants A, B,
C, D ; et C possède 3 enfants G, H et J.
Il est possible de ramener cet arbre sous forme d'un arbre binaire afin de pouvoir
étudier les différentes opérations sur cette structure de données.
V.4.2. Binarisation d'un arbre généralisé
Soit T un arbre généralisé, dans le souci de faciliter les traitements de données dans
cette structure, nous pouvons lui associer un arbre binaire unique T'.
Nous pouvons lui associer un arbre binaire unique de la manière suivante :
1. Les nœuds de l'arbre binaire T' seront les mêmes que ceux de l'arbre T ;
2. La racine de T' sera la même racine de T ;
3. Si N est un nœud quelconque de l'arbre binaire T', l'enfant de gauche de N
dans T' sera le premier enfant du nœud N dans l'arbre généralisé T. Et l'enfant
de droite de N dans T' sera le frère suivant de N dans l'arbre généralisé T.
V.4.3. Exercice
Soit l'arbre généralisé T ensemble ci-après. D déterminer l'arbre binaire associé T'
Solution
V.4.4. Remarques
a. L'importance de cette banalisation réside du fait que certains algorithmes
applicables aux arbres binaires peuvent maintenant être appliqués aux arbres
généralisés, Si n est une constante entière, un arbre est dit n- aire quand le
nombre maximum de fils d'un même nœud est n.
Un arbre binaire est un arbre dont chaque nœud au plus 2 fils.
b. Un arbre binaire n'est pas un cas particulier d'un arbre généralisé. Les arbres
binaires et les arbres généralisés sont physiquement des objets différents. Voici les
deux différences fondamentales qui les séparent.
(1) Un arbre binaire T' peut être vide, mais un arbre généralisé T est non vide.
(2) Soit un nœud N qui ne possède qu'un enfant. Dans un arbre binaire cet enfant
est repéré soit comme enfant de gauche, soit comme un enfant de droite ;
dans un arbre généralisé, les distinctions de cet arbre n'existent pas. Cette
différence peut être illustrée par les arbres T1 et T2 ci-après :
On peut considérer que les arbres T1 et T2 sont différents puisque B est l'enfant
gauche de A dans T1 et l'enfant droite de A dans T2 mais les arbres généralisés T1 et
T2 sont identiques (tandis que les arbres binaires T1 et T2 ne sont pas identiques).
V.4.5. Définition
On appelle forêt F, un ensemble ordonnée des zéros ou plus d'arbres distincts. Si on
supprime la racine ROOT dans un arbre généralisé « T », on obtient la forêt F
constituée par l'ensemble de sous- arbres du ROOT. Réciproquement, si F est une
forêt, nous pouvons lui adjoindre un nœud ROOT de manière à former un arbre
généralisé T dont ROOT est la racine où les sous- arbres de ROOT sont les arbres
originaires de F.
V.4.6. Représentation en mémoire
Soit T un arbre généralisé, il sera représenté en mémoire sous forme d'une liste
chaînée à trois tableaux parallèles : INFO, CHILD, SIBL, plus de variables pointeurs
ROOT et AVAIL (pour tes nœuds qui sont vides). Chaque nœud N de T correspond à
une adresse K telle que :
(1) INFO *K+ contient l'information du nœud N ;
(2) CHILD [K] contient la position du premier enfant de N (position enfant de
gauche). La condition CHILD (KI = û, indique que le nœud N n'a pas d'enfants,
(3) SIBL IK) contient l'adresse du prochain frère de N.
La condition SIBL (K) = 0, indique que le N est le dernier enfant de son parent.
La racine ROOT contient l'adresse R de T. On note que chaque nœud N de
l'arbre T quel que soit le nombre de ces enfants contiendra exactement 3
champs, On peut ainsi représenter une forêt F d'arbre T1, T2, T3, .., Tn en
mémoire de l'ordinateur. Il faut supposer que tous les nœuds racines sont
frères (c'est-à-dire si A1, A2, A3 … : nœuds racines).
Prof. BATUBENGA MWAMBA Nz. J.D. 97
Cours d’Algorithmique et Structures de données