IN4R11
E4FR
I. INTRODUCTION 5
A. PRESENTATION GENERALE DU COURS 5
B. QUELQUES REGLES CRUCIALES A RESPECTER POUR UN ALGORITHME 5
1. LE PSEUDO LANGAGE 5
2. LA DEFINITION DES VARIABLES 5
3. AFFECTATION DES VARIABLES 5
4. LA DEFINITION D’UN ALGORITHME 5
5. LES STRUCTURES CONDITIONNELLES 6
6. LES STRUCTURES ITERATIVES 7
7. LES PROCEDURES & LES FONCTIONS 8
8. L’INDENTATION 9
9. NOTION DE COMPLEXITE D’UN ALGORITHME 9
C. RECURSIVITE 10
D. DIFFERENTS TYPES D’ALGORITHMES 12
1. ALGORITHME DE TYPE « DIVISER POUR REGNER » 12
2. ALGORITHMES GLOUTONS 12
II. BIBLIOGRAPHIE 13
III. RAPPELS SUR LE LANGAGE C 14
A. TYPES,CONSTANTES,VARIABLES 14
1. TYPES DE BASE 14
2. DEFINITION DES TYPES 15
B. EXPRESSIONS 15
1. EXPRESSIONS ARITHMETIQUES 15
2. EXPRESSIONS LOGIQUES 15
3. EXPRESSIONS RELATIONNELLES 16
4. EXPRESSIONS DE MANIPULATION DE BITS 16
5. OPERATEURS D’AFFECTATION 16
6. LES CONVERSIONS IMPLICITES 16
7. LES CONVERSIONS EXPLICITES (FORCEUR : CAST) 16
8. EXPRESSIONS CONDITIONNELLES 16
9. APPEL DE FONCTION 17
C. ENTREES SORTIES 17
1. LECTURE/ECRITURE D’UN CARACTERE 17
2. ECRITURE PREFORMATEE 17
3. LECTURE FORMATEE 17
D. INSTRUCTIONS 18
1. INSTRUCTION VIDE 18
2. INSTRUCTION EXPRESSION 18
3. INSTRUCTION COMPOSEE OU BLOC 18
4. INSTRUCTION SI …ALORS …SINON 18
5. INSTRUCTION CONDITIONNELLE SWITCH 18
6. INSTRUCTION ITERATION TANT QUE 19
7. INSTRUCTION POUR 19
8. INSTRUCTION ITERATIVE REPETER 19
9. INSTRUCTION DE SORTIE :BREAK 19
E. STRUCTURES 20
1. TABLEAUX 20
2. CHAINES DE CARACTERES 20
F. TYPES STRUCTURES 20
1. STRUCTURE 20
2. TYPE STRUCTURE 20
G. POINTEURS 21
1. DECLARATION 21
2. RECUPERATION D’ADRESSE 21
3. MODIFICATION DU CONTENU 21
4. ALLOCATION/LIBERATION 22
5. TABLEAU DE POINTEURS 23
6. NOTATION SPECIFIQUE DES POINTEURS SUR TYPE STRUCTURE 23
7. OPERATIONS 23
H. LA MODULARITE 24
1. PROCEDURE & FONCTIONS 24
2. VISIBILITE DES VARIABLES 25
I. PASSAGE PAR VALEUR 26
J. PASSAGE PAR REFERENCE/ADRESSE 27
K. LE MAKEFILE SOUS LINUX 27
IV. LES FICHIERS 29
A. ENREGISTREMENTS, CHAMPS ET CLE 29
B. TYPES DE FICHIERS : SEQUENTIEL, INDEXE ET DIRECT 29
C. MANIPULATION DES FICHIERS EN C 29
1. DECLARATION D’UN FICHIER EN C 29
2. OUVERTURE/FERMETURE D’UN FICHIER 30
3. LECTURE/ECRITURE DANS UN FICHIER 30
4. DEPLACEMENT DANS UN FICHIER 32
V. STRUCTURES DE DONNEES FONDAMENTALES ET LEUR REPRESENTATION 33
A. RAPPEL SUR LES POINTEURS 33
B. CHAINES DE CARACTERES 33
C. TYPE DE DONNEES ABSTRAIT 33
D. TABLEAUX 34
1. DEFINITION 34
2. PRIMITIVES 34
E. LISTES CHAINEES SIMPLES 36
1. DEFINITION 36
2. MISE EN OEUVRE 37
3. PRIMITIVES 39
F. LISTES DOUBLEMENT CHAINEES 42
1. DEFINITION 42
2. MISE EN ŒUVRE 42
3. PRIMITIVES 43
G. PILES 46
1. DEFINITION 46
2. MISE EN ŒUVRE 46
H. FILES 48
1. DEFINITION 48
2. MISE EN ŒUVRE 48
I. FILES CIRCULAIRES 48
1. DEFINITION 48
J. ARBRES BINAIRES 49
1. DEFINITION 49
2. TERMINOLOGIE 49
3. REPRESENTATION 50
4. PROPRIETES DES ARBRES 52
5. MISE EN ŒUVRE 53
6. PARCOURS 54
VI. ALGORITHMES FONDAMENTAUX EN RECHERCHE 61
A. SEQUENTIELLE 61
B. SEQUENTIELLE INDEXEE 61
C. ARBRES BINAIRES DE RECHERCHE (ABR) 61
1. DEFINITION 61
2. RECHERCHE 61
3. INSERTION 63
4. SUPPRESSION 64
I. Introduction
A. Présentation générale du cours
Les premiers algorithmes seront présentés sous forme de pseudo langage, un langage permettant de décrire les différentes instructions d’un programme de façon la plus lisible possible. Cette utilisation d’un pseudo langage pour concevoir un algorithme permet d’utiliser cet algorithme pour programmer dans n’importe quel langage connu.
Le passage d’un algorithme à un programme se fait en décrivant les structures de données qui vont être utilisées par le programme pour pouvoir correctement appliquer l’algorithme dans l’environnement de programmation. La description des structures de données est donc très importante, c’est pourquoi nous étudierons plusieurs types de structures de données fondamentales : leur définition, leur représentation, les opérations de base (primitives) que l’on peut effectuer dessus et tant que possible leur implantation.
B. Quelques règles cruciales à respecter pour un algorithme 1. Le pseudo langage
Pour décrire des algorithmes, on utilise un pseudo langage proche du français, qui permet d’avoir une suite d’instruction tout de suite compréhensible. Le langage de programmation n’influe donc pas sur l’algorithme (il n’y a aucun « bout » de code dedans) ; il n’intervient qu’au moment de l’implémentation.
2. La définition des variables
Toutes les variables devront être définies avant de commencer l’algorithme : type, nom, rôle qu’elle va jouer dans l’algorithme.
3. Affectation des variables
Dans les algorithmes, pour affecter une valeur à une variables on utilise le symbole suivant : « ». La variable se trouve à gauche de ce symbole, et la valeur à lui affecter à droite.
Par exemple, pour une variable « v » de type entier, à laquelle on veut affecter la valeur « 0 », on écrira : v 0
4. La définition d’un algorithme
Un algorithme sera toujours délimité par :
Début nom de l’algorithme
…
Fin (nom de l’algorithme)
5. Les structures conditionnelles
Il y a 2 structures conditionnelles (choix) :
Le choix simple « Si … Alors … Sinon » Le choix multiple
a) Le choix simple
Il teste une condition : « Si condition Alors ». Si celle-ci est vérifiée, le programme exécute une suite d’instruction. Lorsque la condition n’est pas vérifiée, s’il faut exécuter une autre suite d’instructions, il suffit de faire « Sinon suite d’instructions ». Il ne faut jamais oublier de « fermer » la structure conditionnelle : FinSi.
Si condition Alors Instruction 1 Instruction 2
…
Instruction N [
Sinon
Instruction 1
…
Instruction M ] Facultatif
FinSi
b) Le choix multiple
Le choix multiple compare différentes valeurs à celle d’une variable donnée : « Choix selon variable ». Si la valeur testée est égale à la valeur de la variable, le programme exécute la suite d’instructions qui correspond à la valeur : « valeur N : suite d’instructions ». Si aucune valeur ne correspond, il est possible d’exécuter une suite d’instructions à la fin du choix :
« Autrement … Suite d’instructions ».
Choix selon variable
Valeur 1 : Suite d’instructions Valeur 2 : Suite d’instructions
…
Valeur N : Suite d’instructions Autrement
[
Suite d’instructions ) Facultatif
FinChoix
6. Les structures itératives
Il y a trois types de structures itératives (ou boucles) : La boucle « Pour »
La boucle « Tant que » La boucle « Répéter »
a) La boucle « Pour »
La boucle Pour est très utilisée quand on connaît le nombre exact de traitements à effectuer.
Pour compteur allant de début à fin (par pas de pas) faire Instruction 1
Instruction 2
…
Instruction N FinPour
b) La boucle « Tant que »
La boucle « Tant Que » permet à l’utilisateur d’agir sur un (ou plusieurs) élément qu’il a initialisé auparavant. Puisque le test est fait au départ de la boucle, la boucle peut ne pas être exécutée (condition non vérifiée dès le départ).
Initialiser(élément) (il faut que l’élément – ou les éléments – utilisé dans la condition d’arrêt de la boucle soit initialisé avant la boucle)
Tant que condition sur élément vérifiée faire Instruction 1
Instruction 2
…
Traitement(élément) (il faut que l’élément – ou les éléments – utilisé dans la condition d’arrêt de la boucle soit traité pendant la boucle pour que la boucle ne soit pas infinie).
…
Instruction N FinTantQue
c) La boucle « Répéter »
La boucle « Répéter » s’apparente à la boucle « Tant Que ». Les différences résident dans le fait que comme la condition d’arrêt de la boucle « Répéter » est à la fin, le programme est assuré de traiter au moins un élément.
Répéter
Instruction 1 Instruction 2
…
Traitement(élément) (il faut que l’élément – ou les éléments – utilisé dans la condition d’arrêt de la boucle soit traité pendant la boucle pour que la boucle ne soit pas infinie).
…
Instruction N
Jusqu’à ce que condition sur élément vérifiée (pas de FinRépéter, le mot jusqu’à suffit)
7. Les procédures & les fonctions
Les procédures et les fonctions sont des petits algorithmes dans l’algorithme, il est donc normal qu’ils se présentent de la façon suivante :
Pour une procédure :
Début nom de la procédure( type, nom, E/S type nom, E/S, …) Variables locales : type, nom
…
Suite d’instructions
…
Fin (nom de la procédure)
Pour une fonction :
type retourné nom de la fonction( type, nom, E/S type nom, E/S, …) Variables locales : type, nom
…
Suite d’instructions
…
retourne(résultat) Fin (nom de la fonction)
Remarque : Dans la pratique, le fait d’énumérer tous les paramètres de façon exhaustive (type, nom, E/S) est lourde, et on aura souvent tendance à omettre le paramètre E/S (voire même quelquefois le paramètre de type si le nom de variable est assez explicite).
8. L’indentation
L’indentation est très importante dans un algorithme, elle permet de le rendre lisible et de déterminer du premier coup d’œil les délimitations des différents blocs d’instructions. Elle consiste à décaler les instructions qui se trouvent dans une structure conditionnelle, dans une structure itérative ou dans une fonction.
Exemple :
Début test_2_nombres
Variables :
Entier nombre1, nombre2 // nombres à tester
Entier supérieur(entier n1 (E), entier n2 (E)) Si n1>n2 Alors
Retourne n1 Sinon
Retourne n2 FinSi
Fin supérieur
Lire(nombre1) Lire(nombre2)
Ecrire(supérieur(nombre1, nombre2)) // Appel à la fonction Fin Teste_2_nombres
9. Notion de complexité d’un algorithme
« L’efficacité (efficiency) d’un algorithme se mesure en fonction d’un paramètre N qui caractérise la quantité d’éléments traités par l’algorithme. Ces éléments peuvent être les instructions coûteuses d’une partie de code (ou au contraire les opérations élémentaires), les valeurs d’un tableau, les enregistrements d’un fichier, les constituants d’une liste, les nœuds d’un arbre ou encore les traits (ou les surfaces) dessinés par un algorithme faisant intervenir des aspects graphiques. Bien entendu, plus le temps d’exécution d’un algorithme est petit, plus son efficacité est grande.
La complexité (computationnal complexity) caractérise l’efficacité d’un algorithme et fournit une estimation de son coût en temps processeur. Cette estimation est en général suffisante pour les applications non critiques en temps, c’est-à-dire n’appartenant pas au domaine des applications temps réel dur. Elle ne tient cependant pas compte d’aspects (importants) de génération de code, d’utilisation de ressources physiques, etc.
La complexité est la mesure classique largement utilisée, mais une précision importante
La complexité dans le pire des cas (worst case), qui définit le coût maximal possible
La complexité en moyenne (average case), qui définit le coût moyen
La complexité dans le meilleur des cas (best case), qui définit le coût minimal possible
» Rappel des différentes classes de complexité caractérisant les algorithmes : Constante : O(1)
Logarithmique : O(log(N)) Linéaire : O(N)
Quasi linéaire : O(N.log(N)) Quadratique : O(N²)
Cubique : O(N3) Exponentielle : O(2N)
Rappel :
La notation g = O(f) équivaut à écrire :
Il existe une constante K et un entier N0 tels que : g(N) < K.f(N) pour N > N0
C. Récursivité
La récursivité est une notion essentielle en algorithmique. Elle permet de faire des algorithmes facilement appréhendables et très courts en général. Elle est très utilisée pour les différents algorithmes de tri appliquant la méthode « diviser pour régner ».
Le principe est le même que pour les suites mathématiques. Une fonction récursive doit avoir :
Une condition d’arrêt (qui correspond aux valeurs u0 ou u1 …), appelée aussi cas trivial
Un appel à elle-même dans le corps de la fonction (correspond à la définition des suites : un = un-1+un-2), cela correspond au cas général
Un exemple simple éclairera tout cela. Prenons par exemple la fonction mathématique factorielle, on peut définir cette fonction de la façon suivante :
0 ! = 1
N ! = (1 x 2 x 3 x … x N-1) x N = (N-1) ! x N Ou encore :
u0 = 1 un = n x un-1
Voici un algorithme récursif résolvant N ! : Entier Factorielle(Entier N)
Si N = 0 Alors condition d’arrêt Retourner(1)
Sinon
Retouner(N x factorielle(N-1)) appel récursif FinSi
Fin Factorielle
Ce type de fonction n’est pas réservé uniquement à des calculs mathématiques. On en verra quelques exemples par la suite.
Il existe plusieurs types de récursivité : La récursivité simple
La récursivité multiple La récursivité mutuelle La récursivité terminale
La récursivité simple est une récursivité comme vue précédemment.
La récursivité multiple fait plusieurs appels récursifs dans la même fonction. Par exemple pour calculer la suite de Fibonacci: Un = Un-1 + Un-2.
La récursivité mutuelle est une récursivité « croisée » : une fonction F1 fait appel à une fonction F2, celle-ci faisant appel à la fonction F1. Par exemple, pour déterminer si un nombre est pair ou impair, on peut écrire :
Booléen Impair(Entier n) Si n=0 Alors
Retourner(Faux) Sinon
Retourner(Pair(n-1)) FinSi
Fin Impair
Booléen Pair(Entier n) Si n=0 Alors
Retourner(Vrai) Sinon
Retourner(Impair(n-1)) FinSi
Fin Pair
On peut tester cet algorithme :
Pour Pair(5) : Impair(4) Pair(3) Impair(2) Pair(1) Impair(0) Faux Pour Pair(4) : Impair(3) Pair(2) Impair(1) Pair(0) Vrai
Pour Impair(5) : Pair(4) Impair(3) Pair(2) Impair(1) Pair(0) Vrai
La récursivité terminale est de la forme suivante : Type Fonction(…)
Condition d’arrêt Retourner Fonction( …) Fin Fonction
On peut assimiler ce type de récursivité à une pile : la fonction va calculer une suite de valeurs, pour ne prendre que celle qui est sur le bas de la pile.
Par exemple, écrivons une fonction récursive prenant 2 paramètres entiers : n1 et n2 : Entier fonction(Entier n1, Entier n2)
Si n1<=1 Alors Retourner(n2) Sinon
Retourner(fonction(n1-1,n1*n2) FinSi
Fin fonction
On peut tester cet algorithme :
Pour fonction(4,2) fonction(3,8) fonction(2,24) fonction(1,48) = 48
L’évaluation de la récursivité par le programme va se faire dans le sens des flèches. Il est très facile de dérécursifier ce type de fonction.
D. Différents types d’algorithmes
1. Algorithme de type « diviser pour régner »
Le problème est divisé au départ pour traiter un petit nombre de données. Une fois ces données traitées séparément, on récupère l’ensemble des données traitées pour recommencer l’opération jusqu’à obtenir le résultat souhaité.
2. Algorithmes gloutons
Les algorithmes dits gloutons sont des algorithmes qui vont « prendre des décisions » optimales localement dans un problème, c’est-à-dire, qu’à un moment donné, l’algorithme va prendre un décision optimale, mais que cette décision n’aboutira pas forcément à une solution optimale à la fin de l’algorithme. Nous en verrons quelques exemples : codage de Huffman, Dijkstra et Prim.
II. Bibliographie
Algorithmes en langage C – Robert Sedgewick (InterEdition) Maîtrise des Algorithmes en C – Kyle Loudon (O’Reilly)
Algorithmique et structures de données en langage C – Leendert Ammeraal (InterEdition)
Algorithmes et structures de données avec Ada, C++ et Java – Abdelali Guerid / Pierre Breguet / Henri Röthlisberger (Presses Polytechniques et universitaires romandes)
Au cœur de Java 2 Notions fondamentales – Cay S. Horstmann & Gary Cornell (Campus Press)
III. Rappels sur le langage C
A. Types, Constantes, Variables 1. Types de base
a) Les entiers
int Nombre entier, signé, codé sur 16 bits (2 octets) sur PC short Nombre entier, signé, codé sur 8 bits (1 octet)
song Nombre entier, signé, codé sur 32 bits (4 octets) unsigned int Nombre entier non signé codé sur 16 bits sur PC unsigned short Nombre entier non signé codé sur 8 bits
unsigned long Nombre entier non signé codé sur 32 bits b) Les réels
float Nombre réel, codé sur 32 bits (4 octets) double Nombre réel, codé sur 64 bits (8 octets)
c) Les caractères
char Caractère, codé sur 8 bits (valeurs de -127 à 128)
unsigned char Valeurs entières de 0 à 255 permettant le codage de l’ASCII étendu
d) Le type void
void Type des sous programmes qui ne renvoient aucune valeur (procédures)
e) Le type des valeurs booléennes
Pas de Boolean Type qui prend 2 valeurs Vrai (true) ou Faux (false). Il s’agit en fait d’un nombre entier. La valeur Faux correspond à la valeur 0, et Vrai aux valeurs entières non nulles.
f) Les types énumérés
enum Type qui permet de donner une liste de valeurs qui peuvent être prises par une variable de ce type. L’utilisation se fait de la sorte : enum jour {lundi, mardi, mercredi,…} (ici le type est enum jour)
2. Définition des types
Une définition de type que l’on veut désigner par un identificateur se fait grâce à une déclaration typedef.
Exemple : typedef enum {FAUX,VRAI} BOOL
Cela permet de remplacer des types complexes par un nom, et en cas de changement, il suffit de changer une ligne dans la définition du type au lieu de la changer dans toutes les occurrences du programme.
a) Les constantes Les constantes sont en fait des macros :
#define NOM_CONST valeur_const1
#define NOM_CONST2 valeur_const2
#define NOM_CONST3 (NOM_CONST1*NOM_CONST2)
b) Les variables
Une déclaration de variable est de la forme suivante :
Type nom_var (on peut aussi utiliser une classe d’allocation en mémoire).
B. Expressions
1. Expressions arithmétiques
a) Opérateurs sur les entiers/caractères Opérateurs unaires : +,-
Opérateurs binaires : +,-,*, /,%
b) Opérateurs sur les réels Opérateurs unaires : +,-
Opérateurs binaires : +,-,*, /
2. Expressions logiques
Les opérateurs sont : !, &&, ||
3. Expressions relationnelles
Les opérateurs sont : !=, ==, <,<=,>,>=
4. Expressions de manipulation de bits
~ Complément à 1 (unaire)
<< Décalage à gauche
>> Décalage à Droite
& Et bit à bit
| Ou inclusif bit à bit
^ Ou exclusif bit à bit
5. Opérateurs d’affectation
L’opérateur d’affectation de base est l’opérateur « = ». Il existe une autre forme d’affectation : <op>= où <op> peut prendre les valeurs : +, -, *, /, %, >>, <<, &, ^, |.
6. Les conversions implicites
float doubleint float int long
char / short / enum int
7. Les conversions explicites (forceur : cast)
On dit qu’on « cast », on force le type d’une variable quand on convertit explicitement un type vers un autre :
Exemple : si on veut transformer un double en int, il suffit d’indiquer devant celui-ci (int), par exemple pour x, entier et y réel, on pourra écrire une opération de la forme :
x = 10*(int)y
8. Expressions conditionnelles
Elle utilise l’opérateur ternaire « ? » : Test ? Valeur1 :Valeur2 Si le test est vrai, on prendra la valeur1, sinon ce sera la valeur2.
Par exemple pour calculer le maximum entre 2 nombres a et b : max = (a>b) ?a :b ;
9. Appel de fonction
Pour faire un appel de fonction il suffit d’écrire le nom de la fonction et de passer correctement ses différents paramètres. Par exemple, int maxi = max(a,b) ; (si il existe une fonction prototypée int max(int,int)). Si une fonction n’a pas de paramètres, il faut quand même utiliser les parenthèses « () ».
C. Entrées sorties
#include <stdio.h>
1. Lecture/Ecriture d’un caractère
getchar() ; putchar() ;
2. Ecriture préformatée
Printf("texte + formatage",expression1,expression2, …) ;
Cela permet d’afficher des expressions en les formatant (nombre de chiffres après la virgule, entier, réel, caractère, chaîne de caractères). Pour ce faire on utilise dans le texte pour chaque expression apparaissant dans la 2ème partie un caractère spécial « % » suivi d’une lettre :
d/i entiers signés en décimal o non signés en octal u non signés en décimal x/X signés en hexadécimal c caractère
s chaîne de caractères f réels sous forme décimal e/E réels sous forme exponentielle
Il est possible de rajouter des paramètres pour le formatage entre le caractère « % » et la lettre désignant la conversion à effectuer. Ces paramètres sont des nombres permettant de savoir combien de caractères vont être affichés .
3. Lecture formatée
Scanf(" texte + formatage ",&var1,&var2, …) ;
Même caractéristiques que le Printf (lecture) mais cette fois-ci, affecte des valeurs aux variables var1, var2, … en fonction du formatage donné dans la première partie du scanf(). On utilise le caractère spécial « & » pour les variables dans le scanf() sauf pour les chaînes de caractères (on verra pourquoi par la suite).
D. Instructions
1. Instruction vide
« ; » dans une boucle par exemple
2. Instruction expression
Toute expression qui se termine par un « ; » devient une instruction. Le « ; » est un terminateur d’instruction.
3. Instruction composée ou Bloc
Les accolades { } permettent de définir un bloc, c’est-à-dire regrouper des déclarations et des instructions. Cela s’avère très utile quand la syntaxe impose une seule instruction. On peut alors utiliser un bloc (comme pour les instructions Si/Alors/Sinon que l’on verra par la suite).
Attention : Les déclarations faites dans un bloc ne sont valables que dans ce bloc.
4. Instruction Si … Alors … Sinon
if (Test)
Instruction [else
Instruction) facultatif
Il suffit de remplacer l’instruction par un bloc pour pouvoir exécuter plusieurs instructions dans chaque cas … On obtient alors :
if (Test) {
Instruction1 ; Instruction2 ;
… }
[ else {
Instruction1 ; Instruction2 ;
… }] facultatif
En général, on écrira toujours cette instruction sous forme de blocs (même pour une seule instruction), cela permet de mieux voir à quel if correspond un else.
5. Instruction conditionnelle Switch
switch(variable) {
case valeur1 : suite d’instructions break;
case valeur2 : suite d’instructions break;
default : suite d’instructions }
Les instructions break permettent de ne pas passer par la suite des instructions du switch et de sortir directement après la parenthèse fermante.
6. Instruction itération Tant Que
while(Test) Instruction ;
De la même façon que pour l’instruction If/Then/Else, on utilise très souvent un bloc : while(Test) {
Suite d’instructions }
7. Instruction Pour
for(expression1 ;expression2 ;expression3) Instruction ;
Avec un bloc, on obtient :
for(expression1 ;expression2 ;expression3) { Suite d’instruction
}
En général, cette boucle est utilisée de la façon suivante :
for(initialisation compteur ; test compteur ; incrémentation/décrémentation compteur)
8. Instruction itérative Répéter
do
Instruction while(Test);
Avec un bloc, on obtient : do {
Suite d’instructions }
while(Test) ;
9. Instruction de sortie : Break
Elle permet de sortir pour l’instruction switch mais elle est aussi utile pour sortir des boucles (sort d’un niveau de boucle quand elle est appelée, c’est-à-dire qu’elle arrête la boucle
en court ; si 2 boucles sont imbriquées et que l’instruction break est appelée dans la 2ème boucle, on sortira pour revenir dans la première boucle)
E. Structures
Ex : Tableau, structure, union.
Les variables d’un type structuré ont plus d’un composant.
Chacun des composants est d’un type simple ou structuré. Au plus bas niveau, les composants sont de type simple.
Ils sont utilisés de la même manière que les types simples mais diffèrent d’eux par la désignation des composants.
Ils permettent une meilleure lisibilité, une meilleure organisation des données.
1. Tableaux
Collection ordonnée de valeurs qui ont toutes le même type.
Type nom_var[nb_valeurs] ;
La variable indique l’adresse de la première valeur contenue dans le tableau.
On ne peut pas copier un tableau directement comme une variable, il faut recréer une variable tableau avec le même nombre de valeurs et copier les valeurs une par une (tab’[i] = tab[i]).
2. Chaînes de caractères
Une chaîne de caractères se déclare de la façon suivante : char chaine[255] ; ou char * chaine ; Pour utiliser une chaine de caractères déclarée sous la forme « char *chaine », il faut lui allouer de la mémoire avec malloc (cf. pointeur par la suite).
Une chaîne est un tableau de caractères se terminant par le caractère \0 (caractère de fin de chaîne). Comme c’est un tableau, la variable indique l’adresse de la première valeur, c’est un « pointeur » sur le premier caractère, il n’y a donc pas besoin de passer l’argument par adresse au scanf() (pas de « et commercial : & » cf. plus tard passage par adresse).
F. Types structurés 1. Structure
Déclaration d’une structure : struct nom_struct {
}
Utilisation d’une structure : struct nom_struct nom_var ;
2. Type structuré
Déclaration d’un type structuré : typedef struct {
} nom_type ;
Utilisation d’un type structuré : nom_type nom_var ;
G. Pointeurs
1. Déclaration
Les pointeurs sont des types de données particuliers. Ils permettent d’accéder à une zone mémoire. On les déclare de la façon suivante :
Type* nom_point ;
On peut s’apercevoir qu’un pointeur est attaché à un type bien spécifique. Un pointeur adressera logiquement une valeur du type qui le définit, c’est-à-dire qu’il pointera sur une zone mémoire contenant une valeur du type Type.
Le type Type peut être un type simple, structuré ou une structure.
Schéma :
int* p ; int i ;
i=0 ;
p = &i ;
2. Récupération d’adresse
Quand on ne veut faire pointer le pointeur sur rien, on utilise la valeur NULL. Il suffit de l’affecter de la manière suivante : nom_point = NULL ;
On peut récupérer l’adresse d’une variable grâce à l’opérateur & : &nom_var va retourner l’adresse de nom_var, donc un « pointeur » sur nom_var. Si nom_var et nom_point ont le même type, on pourra écrire : nom_point = &nom_var. On fera ainsi pointer nom_point sur l’adresse de nom_var.
3. Modification du contenu
… 0 … p
Adresse mémoire de i
Algorithmique et Structures de données 22/66
On pourra ensuite accéder au contenu de nom_point par *nom_point et ainsi, changer la valeur de nom_var, par exemple, pour nom_var de type int (donc int* nom_point), si j’écris
*nom_point = 5 ; nom_var prendra automatiquement la valeur 5.
Cette propriété permet d’utiliser les pointeurs dans les fonctions pour modifier des variables externes à la fonction. On verra cette propriété dans peu de temps (passage par adresse/référence).
4. Allocation/Libération
Pour les fonctions qui vont suivre il est nécessaire d’inclure la bibliothèque <stdlib.h>.
a) Allocation
Si on veut utiliser un pointeur pour désigner une adresse mémoire non encore alloué, il faut réserver l’espace mémoire nécessaire.
On réalise cette allocation mémoire avec la fonction malloc : nom_point = (Type*)malloc(sizeof(Type)) ;
La fonction malloc renvoie une adresse déclarée comme void* ; il faut donc utiliser un forceur pour la convertir dans le même type que le pointeur. La fonction sizeof() détermine la taille à allouer en fonction du type de donnée qui lui est passé en paramètre. Cela permet d’allouer un espace mémoire qui correspond toujours au type que va adresser le pointeur.
Schéma :
int *p ;
p = (int*)malloc(sizeof(int)) ;
b) Libération
La libération de la zone mémoire précédemment utilisée se fera par le biais de la fonction free :
free(nom_point) ;
La fonction free libère l’espace mémoire adressé par le pointeur nom_point. Cette zone mémoire pourra alors être réutilisée par la suite lors de la déclaration d’autres variables. Le pointeur pointe toujours sur l’adresse ; pour éviter les mauvaises manipulations il est prudent de remettre le pointeur à NULL après un free.
Exemple :
p
int
pEntier (1)
1
int* pEntier ;
pEntier=(int*)malloc(sizeof(int)) ; (1) *pEntier=1;
(2) free(pEntier);
(3) pEntier=NULL;
5. Tableau de pointeurs
Il est possible de définir des tableaux de pointeurs : la déclaration est la même que pour des tableaux de valeur : Type* nom_point[xx].
Chaque case du tableau est alors un pointeur avec une adresse spécifique.
6. Notation spécifique des pointeurs sur type structuré
Pour accéder à un élément d’un type structuré, on utilise l’opérateur « . ». Lorsqu’un pointeur adresse un type structuré, il est possible de simplifier la notation :
« (*p).var » s’écrit « p var »
7. Opérations
a) Affectation entre 2 pointeurs
Lorsque 2 pointeurs pointent sur le même type de données, il est possible de les affecter l’un à l’autre.
Exemple :
int* p1 ; int* p2 ; int nb ;
nb=12 ; p1 = &nb ; p2 = p1;
p1
p2
12
b) Décalage d’adresse
L’opération nom_point+=i ; décale l’adresse de nom_point de i*taille(type). On peut ainsi utiliser cette propriété pour se déplacer dans des chaînes de caractères : char* chaine.
*chaine représente le premier caractère de la chaîne.
*(chaine+1) représente le 2ème caractère de la chaîne …
Jusqu’à ce que *(chaine + i) soit égal à \0, caractère de terminaison de la chaîne de caractères.
Exemple : char* pChaine ;
… // Allocation mémoire, remplissage de la chaîne par « test » pChaine=pChaine+1 ; // le pointeur pointe sur e maintenant
H. La modularité
1. Procédure & fonctions
Pour alléger la programmation, permettre une lecture simple d’un programme et surtout pour avoir une « réutilisabilité » maximum, il est bon de scinder un programme en modules.
Ces modules sont des procédures ou des fonctions. Les procédures sont des modules qui ne renvoient pas d’informations. Les fonctions renvoient une information dépendante (en général) des paramètres qui lui sont passés. On rappelle donc brièvement leur déclaration :
Procédure :
void Nom_Procédure(Type1 var1, Type2 var2, …) {
}
Fonction :
Type Nom_Fonction(Type1 var1, Type2 var2, …) //le type est un type simple, un type structuré, une structure, un pointeur, mais pas de tableau ni de chaîne de caractères
{
return … ; }
Pour pouvoir utiliser ces modules dans votre programme il faut que vous donniez les prototypes des fonctions de la façon suivante :
void Nom_Procédure(Type1, Type2 , …) ; Type Nom_Fonction(Type2, Type2, …) ;
Les noms des variables utilisées dans la déclaration des paramètres n’est pas obligatoirement le même que celui des variables utilisées à l’extérieur de la fonction.
Considérons une fonction F déclarée de la façon suivante : int F(int a, int b) ;
On peut l’appeler de différentes manières : F(1,2) ;
int x=1 ; int y=2 ; F(x,y) ; int x=1 ; F(x,2) ;
Lors de l’appel de la fonction la valeur de la variable passée en paramètre sera copiée dans la variable déclarée pour la fonction.
Par exemple, dans le premier cas : a 1 et b 2 Dans le deuxième cas : a x et b y
Dans le troisième cas : a x et b 2
2. Visibilité des variables
Ainsi, les variables utilisées pour passer les paramètres d’une fonction ne peuvent pas être modifiées. On dit des variables déclarées dans un block de programme que leur portée est locale. On peut résumer cette propriété de façon « visuelle » en prenant comme repère les accolades : les variables déclarées derrière une accolade ouvrante ne sont utilisables que jusqu’à l’accolade fermante correspondante.
Si des variables sont déclarées dans l’en-tête du programme principal, elles ont une portée globale, elles peuvent être utilisées partout dans le programme et donc être modifiées n’importe quand.
Exemple : {
int i ; {
int j ; }//on ne voit plus j {
int k ; {
int l ; }//on ne voit plus l {
int m;
{
int n ; }//on ne voit plus n }//on ne voit plus m }//on ne voit plus k
}//on ne voit plus i
I. Passage par valeur
Lorsqu’on appelle une fonction avec un paramètre, la valeur du paramètre est copiée dans une variable locale; ce n’est pas la donnée passée que l’on manipule, mais une copie de cette donnée.
Exemple :
int factorielle(int n) {
… }
…
int x = 5 ; factorielle(x) ;
Quand on fait l’appelle à factorielle avec la variable x comme paramètre, la valeur de x est copiée dans la variable locale n. On ne peut donc pas changer la valeur de x dans la fonction.
Ce type de passage par paramètre s’appelle le passage par valeur. Pour pouvoir modifier une variable passée dans les paramètres d’une fonction, il faut utiliser un passage par adresse (appelé aussi passage par référence).
J. Passage par référence/adresse
La modification d’une variable dans un module du programme nécessite qu’elle soit déclarée en tant que variable globale. C’EST FAUX.
L’utilisation des pointeurs permet de pallier à ce problème. En effet, une variable de type primitif est en faite une case mémoire allouée automatiquement par le programme (taille fixe), et il est donc possible de récupérer l’adresse mémoire de cette case. Cette adresse mémoire est en fait un pointeur sur la valeur, et on l’obtient en ajoutant un signe « & » (« et commercial ») devant le nom de la variable.
Pour modifier une variable dans une fonction il faut donc passer l’adresse mémoire (référence) de cette variable à la fonction. La fonction pourra alors accéder à la valeur NON COPIEE de la variable (on passe un pointeur sur la valeur, le pointeur est copié, pas ce qu’il pointe).
Récapitulons :
Déclaration d’une variable : Type nom_var
Accès à son adresse mémoire (référence) : &nom_var Déclaration d’une fonction avec passage par adresse :
Type fonction(Type *nom_var_fonc) ;
Accès à la valeur de la variable dans la fonction : *nom_var_fon
K. Le makefile sous Linux
Le programme utilisé pour exécuter des « Makefile » est le programme Make, intégré à tous les systèmes Unix.
Les options de make sont les suivantes :
–k : permet de continuer la compilation après la première erreur trouvée. Ce qui permet de repérer toutes les erreurs dès la première compilation.
-f <fichier> : permet d’utiliser un fichier « makefile » à utiliser pour la compilation. Par défaut l’exécution de make recherche un fichier makefile ou Makefile.
-h : aide sur le programme make.
Pour ajouter une ligne de commentaire, il suffit d’utiliser le caractère «#».
Un fichier makefile se présente comme suit : cible : dépendances
règles
Les dépendances représentent les fichiers dont on a besoin pour créer la cible.
Les règles sont en fait l’opération de compilation pour créer la cible : ici gcc –c … ou gcc –o …
Exemple:
mon_programme : principal.o outil.o outil_age.o
gcc -o mon_programme principal.o outil.o outil_age.o principal.o : principal.c principal.h
gcc -c principal.c outil.o : outil.c outil.h
gcc - c outil.c
outil_age.o : outil_age.c outil_age.h
gcc -c outil_age.c
IV. Les fichiers
A. Enregistrements, champs et clé
Un enregistrement peut être référencé par différents champs : nom, date, taille, type. On utilise un de ces champs pour les classer. Ce champ est alors appelé clé. Il permet de faire du tri ou de la recherche parmi les enregistrements.
B. Types de fichiers : séquentiel, indexé et direct
Accès séquentiel indexé : Quand on doit parcourir, lors d'une recherche, tout ou partie des informations d'un fichier pour obtenir ce que l'on souhaite. Pour aller plus vite, on peut indexer le fichier et cela donne un « Accès Séquentiel Indexé », qui fut pendant longtemps la forme la plus achevée de gestion de base de données.
Contraire : accès direct.
Accès direct : Quand on peut obtenir tout de suite la bonne information, sans avoir à lire les informations qui la précèdent, mais il faut alors savoir exactement où se trouvent les données (et les données ne doivent pas dépendre les unes des autres).
Le temps d'accès est constant, sinon on a seulement un accès aléatoire. Contraire : accès séquentiel.
C. Manipulation des fichiers en C
La manipulation des fichiers en langage C nécessite le chargement d’une bibliothèque d’E/S : stdio.h. Le chargement se fait au début du programme comme suit :
#include <stdio.h>
1. Déclaration d’un fichier en C
En C, comme dans tous les langages évolués, la manipulation des fichiers se fait par l’utilisation d’un pointeur sur le fichier. On utilise le type FILE définit dans la bibliothèque stdio. La déclaration pour un pointeur de fichier est donc la suivante :
FILE *fichier ;
2. Ouverture/Fermeture d’un fichier
a) Ouverture
Pour ouvrir, un fichier, on utilise la fonction fopen qui renvoie un pointeur de fichier.
Cette fonction prend comme paramètres le chemin du fichier et les paramètres de lecture/écriture.
Les paramètres de lecture/écriture ont 2 significations : Le mode d’accès au fichier :
o "r": lecture seule (message d’erreur si le fichier n’existe pas)
o "w": écriture dans un nouveau fichier (si le fichier existe déjà, il est effacé) o "a": ajout à la fin du fichier (si le fichier n’existe pas, il est créé)
o "r+": lecture et écriture(message d’erreur si le fichier n’existe pas)
o "w+": création d’un nouveau fichier et ouverture en lecture/écriture (si le fichier existe déjà, il est effacé)
o "a+": ouverture en lecture et ajout (si le fichier n’existe pas, il est créé) Le type de fichier :
o "b": binaire o "t": texte
FILE * fopen(const char *filename,const char *acces_mode) ; Exemples :
fichier = fopen("c:\temp\text.txt","rt");
fichier = open("c:\temp\text.txt","w+ t");
b) Fermeture
La fermeture d’un fichier doit être faite avant de quitter un programme, sinon le fichier n’est pas utilisable. La fermeture se fait de façon simple, avec la fonction fclose, qui prend en paramètre le pointeur de fichier. La fonction renvoie une erreur si le pointeur de fichier n’est pas initialisé (0 sinon).
int fclose(FILE *fichier) ;
3. Lecture/Ecriture dans un fichier
a) LectureLa lecture dans un fichier peut se faire de différente façon :
soit on va rechercher des données que l’on a écrites de façon structurée dans le fichier, dans ce cas, on va préférer la fonction fscanf ou fread
soit on lit le fichier séquentiellement, « caractère » par « caractère » (octet par octet) et on préférera fgetc.
La fonction fscanf s’utilise exactement comme la fonction scanf. Elle prend en paramètre le pointeur de fichier, puis les paramètres habituels de scanf : format du flux à lire, variables à mettre à jour. La fonction fscanf renvoie le nombre d’éléments correctement lus.
int fscanf(FILE *fichier, const char *format_string, …) Exemple :
fscanf(fichier, "Date %d-%d-%d",&jour,&mois,&annee) ;
La fonction fread prend en paramètre le buffer où sont stockées les éléments lus, la taille en octet de chaque élément, le nombre (maximum) d’éléments à lire et le pointeur de fichier.
La fonction fread renvoie le nombre d’éléments lus.
int fread(void *buffer,int size, int count, FILE *fichier) Exemple :
Nb_elements = fread(buffer,10,5,fichier) ;
La fonction fgetc renvoie un caractère (un octet) à partir de la position courante dans le fichier. La position courante dans le fichier est alors automatiquement incrémentée d’un octet.
Si on lit à la fin du fichier, la fonction fgetc renvoie la constante EOF (end of file).
int fgetc(FILE *fichier) Exemple :
caractere = fgetc(fichier);
b) Ecriture
De la même façon que pour la lecture, on peut :
Soit écrire des données formatées avec les fonctions fprintf et fwrite Soit écrire caractère par caractère avec fputc
La fonction fprintf permet d’écrire des données formatées à la façon du printf dans un fichier. Elle prend comme paramètres le pointeur de fichier, le format du flux à écrire et les variables à utiliser. Elle renvoie le nombre de caractères écrits.
int fprintf(FILE *fichier,const char *format_string, …) Exemple :
Nb_caractere = fprintf(fichier,"Date %d-%d-%d",jour,mois,annee) ;
La fonction fwrite s’utilise comme la fonction fread ; les paramètres sont les mêmes : buffer à écrire dans le fichier, taille d’un élément du buffer, nombre d’éléments dans le buffer, pointeur de fichier. fwrite renvoie le nombre d’éléments écrits.
int fwrite(void *buffer,int size, int count,FILE *fichier) Exemple:
Nb_elt = fwrite(buffer,10,5,fichier);
La fonction fputc permet d’écrire un caractère dans le fichier. Elle prend comme paramètres le caractère à écrire et le pointeur de fichier. Elle renvoie le caractère à écrire en cas de succès, la constante EOF sinon.
int fputc(int c, FILE * fichier) Exemple :
Resultat = fputc("A",fichier) ;
4. Déplacement dans un fichier
Pour savoir où l’on se trouve dans un fichier, il suffit de faire appel à la fonction ftell.
Elle renvoie la position courante par rapport au début du fichier (position en octet). Elle prend en paramètre le pointeur de fichier.
long ftell(FILE *fichier) Exemple :
Num_octet = ftell(fichier) ;
Pour décaler la position courante par rapport à une origine donnée dans un fichier, on peut utiliser la fonction fseek. Elle prend en paramètres le pointeur de fichier, le décalage voulu (en nombre d’octets) et le point d’origine du décalage. On utilise 3 constantes pour définir le point d’origine du décalage :
SEEK_SET (0) : à partir du début du fichier SEEK_CUR (1) : à partir de la position courante SEEK_END (2) : à partir de la fin du fichier
La fonction retourne 0 en cas de succès, une valeur non nulle sinon.
int fseek(FILE *fichier,long offset,int origin) Exemple :
Error = fseek(fichier,255,SEEK_SET);
V. Structures de données fondamentales et leur représentation
A. Rappel sur les pointeurs
Un pointeur est une référence à un espace mémoire dans lequel se trouve l’information (entier, caractère, structure de données créée par l’utilisateur, …).
La déclaration d’un pointeur se fait de la façon suivante : Entier *pointeur (ici on
déclare un pointeur sur un espace mémoire contenant un entier)
L’étoile « * » caractérise le pointeur. Elle permet aussi d’accéder à l’information contenue dans le bloc mémoire indiqué par le pointeur.
Ainsi pour récupérer la valeur de l’entier, il suffit d’appeler
« *pointeur ».
En général les pointeurs sont utilisés avec des structures de données plus complexes comme nous le verrons par la suite. Schématiquement, ils sont représentés par une flèche.
En Java, l’utilisation des pointeur est totalement transparente, il n’y jamais besoin de déclarer de pointeur comme ci-dessus. Le fait de créer un objet crée un pointeur (instance de la classe).
B. Chaînes de caractères
Une des structures les plus simples à appréhender est la chaîne de caractères. Elle se compose d’une suite de caractères. C’est une structure dynamique, on peut lui donner en entrée n’importe quel nombre de caractères. La chaîne de caractères est une suite de pointeurs, celle- ci pouvant être modifiée à tout moment (ajout/suppression de plusieurs pointeurs, insertion de pointeur, … tout cela de façon transparente).
C. Type de données abstrait
Un type de données abstrait est défini par un type de données et l’ensemble des opérations permettant d’accéder à ces données et à faire des opérations de façon transparente pour l’utilisateur. On verra que toutes les structures que l’on étudiera par la suite peuvent être considérés comme des types de données abstraits.
mémoire
pointeur 12
4a12c abcd
D. Tableaux
1. Définition
Les tableaux sont des structures de données à taille fixe. A la création du tableau, il faut donner le nombre d’éléments maximum. Cependant, malgré cet inconvénient majeur, il est possible d’accéder à ses éléments via leurs indices dans le tableau. L’accès aux éléments est donc très rapide. Dans cette structure se cache en fait une suite finie de pointeurs, auxquels on accède de façon totalement transparente.
2. Primitives
Dans la suite on supposera que le nombre d’éléments dans le tableau est N, que le nombre d’élément maximum est NbMax. La déclaration du tableau se fera de la manière suivante :
Début Tableau
Variables : Entier N
Entier tab[NbMax]
N 0 ;
… Fin Tableau
Tous les algorithmes qui suivront sont des fonctions qui doivent être contenues dans l’algorithme précédent.
Tableau de 13 éléments : 8 seulement son remplis
Index : 0 1 2 3 4 5 6 7 8 9 10 11 12
5 3 7 8 4 6 1 7
a) Déplacement
Le déplacement d’un élément nécessite le déplacement de tous les éléments qui sont compris entre l’indice actuel de l’élément et l’indice de l’élément une fois déplacé.
Cela demande donc un nombre d’opérations qui dépend de la distance à parcourir par l’élément choisi.
L’algorithme en découlant est le suivant : Début Modification(indice_départ,indice_fin)
Variables locales : Entier i Entier info
info tab[indice_depart]
Si indice_depart > indice_fin Alors
Pour i allant de indice_depart à indice_fin+1 par pas de -1 faire tab[i] tab[i-1]
FinPour Sinon
Pour i allant de indice_depart à indice_fin-1 faire tab[i] tab[i+1]
FinPour FinSi
tab[indice_fin] info Fin Modification
4
Index : 0 1 2 3 4 5 6 7 8 9 10 11 12
5 3 7 8 4 6 1 7
Index : 0 1 2 3 4 5 6 7 8 9 10 11 12
5 3 7 8
4
6 1 7
Déplacement de l’élément « 4 » de l’indice 4 à l’indice 1
Index : 0 1 2 3 4 5 6 7 8 9 10 11 12
5 3 7 8 6 1 7
b) Insertion
De la même façon, pour insérer un élément à l’indice i, il faut décaler vers la droite tous les éléments du tableau de l’indice i à l’indice N (N+1 étant le nombre d’éléments contenu dans le tableau), cela demande donc un nombre d’opérations qui dépend de la distance à laquelle va se trouver l’élément à ajouter de la fin du tableau.
L’algorithme s’écrit donc :
Début InsertionAprès(indice,info) Variables locales :
Entier i
Pour i allant de N à indice+1 par pas de -1 faire tab[i+1]tab[i]
FinPour
tab[indice] info NN+1
Fin InsertionAprès
c) Suppression
De la même façon, la suppression d’un élément à l’indice i dans un tableau nécessite le décalage de tous les indices > i vers la gauche pour ne pas laisser une case vide. Le nombre d’opérations pour la suppression dépend donc de la distance entre la fin du tableau et l’indice de l’élément à supprimer.
On peut le vérifier en écrivant l’algorithme : Début Suppression(indice)
Variables locales : Entier i
Pour i allant de indice à N-1 faire tab[i]tab[i+1]
FinPour
NN-1 Fin Suppression
E. Listes chaînées simples
1. Définition
La liste chaînée organise l’information séquentiellement comme dans la structure de tableau que l’on a pu voir précédemment. Chaque maillon/nœud de la chaîne contient une information et un lien sur le nœud suivant.
On représentera la liste de la façon suivante :
L’avantage de cette structure par rapport à la structure tableau est qu’elle est dynamique, on peut ajouter et retirer des éléments de la liste à notre guise, en ayant une gestion mémoire optimale. Il n’est donc pas nécessaire de connaître à l’avance la taille maximale de la liste.
2. Mise en oeuvre
Une liste chaînée est une suite de pointeurs sur des objets contenant l’information. La structure de base d’une liste chaînée est la suivante :
En programmation procédurale, il faut utiliser les pointeurs explicites:
Structure Noeud info
Nœud *Nœud_suivant Fin Structure
En POO (Java), la liste fera l’objet d’une classe : Classe Nœud
Variables : info
Nœud nœud_suivant Fin Classe
8 4 2 1
8 4 2 1
Liste simplement chaînée tête
On peut remarquer que les déclarations ne sont pas très éloignées l’une de l’autre, cependant, il y a des différences à l’utilisation (plus besoin de pointeur pour la POO) :
Pour appeler le nœud suivant dans le premier cas : Nœud *tete
(*tete).noeud_suivant ou encore tete->noeud_suivant
Pour appeler le nœud suivant dans le deuxième cas (Java):
Nœud tete = new Nœud() tete.noeud_suivant
C’est ainsi qu’on peut comprendre que les pointeurs sont complètement implicites en Java.
Par la suite on désignera le nœud suivant par suivant(nœud) pour ne pas avoir à faire de distinction entre la POO et la programmation procédurale.
La déclaration d’une liste simplement chaînée se fera de la manière suivante : Début ListeSimple
Variables : Nœud tete tete NIL
… Fin ListeSimple
Tous les algorithmes qui suivront sont des fonctions qui doivent être contenues dans l’algorithme précédent.
3. Primitives
a) Déplacement
Comme on peut le voir sur le schéma précédent, déplacer un nœud dans une liste consiste à changer 3 liens, quelque soit l’endroit où se trouve ce nœud.
L’algorithme qui permet cette modification est le suivant :
Début Modification(tete,nœud_depart,nœud_fin) // on suppose que nœud_depart est le nœud juste avant celui qu’on veut déplacer, et nœud_fin, le nœud derrière lequel on veut mettre le nœud choisi. Si le nœud fin est vide, on déplace devant la tête de la liste
Variables locales :
Nœud nœud_choisi Si nœud_depart est vide Alors
nœud_choisi tete
tete suivant(nœud_choisi) Sinon
nœud_choisi suivant(nœud_depart)
suivant(nœud_depart) suivant(nœud_choisi) FinSi
Si nœud_fin est vide Alors
suivant(nœud_choisi) tete tete nœud_choisi
Sinon
suivant(nœud_choisi) suivant(nœud_fin) suivant(nœud_fin ) nœud_choisi
FinSi Fin Modification
8 4 2 1
8 4 2 1
2 8 4 1
Déplacement d’un nœud dans la liste 3
3
3
b) Suppression
Comme on peut le voir sur le schéma précédent, supprimer un nœud dans une liste
consiste modifier 1 lien (il faudra supprimer de la mémoire le nœud supprimer si on n’utilise pas Java).
L’algorithme qui permet cette modification est le suivant :
Début Suppression(tete,noeud) // on suppose que « nœud » est le nœud juste avant celui qu’on veut supprimer
Variables locales : Nœud pointeur Nœud aEffacer Si nœud est vide Alors aEffacer tete
tete suivant(aEffacer) Libere(aEffacer)
Sinon
aEffacer suivant(noeud)
suivant(noeud) suivant(aEffacer) Libere(aEffacer)
FinSi Fin Suppression
8 4 2 1
8 4 2 1
8 4 1
Suppression d’un nœud dans la liste
c) Insertion
Comme on peut le voir sur le schéma ci-dessus, insérer un nœud dans une liste consiste modifier 2 liens.
L’algorithme qui permet cette modification est le suivant :
Début InsérerAprès(tete,nœud,noeudAInsérer) // si nœud est vide, on insère avant la tete
Si nœud est vide Alors
suivant(noeudAInserer) tete tete noeudAInserer
Sinon
suivant(noeudAInsérer) suivant(nœud) suivant(nœud) noeudAInsérer
FinSi Fin Suppression
8 4 2 1
8 4 2 1
8 4
Insertion d’un nœud dans la liste 9
2 9 1