RAPPEL SUR LES POINTEURS
ET LES LISTES CHAINÉES
Assuré par Mme Hanene
Ghazouani
Les pointeurs
• En algorithmique, les pointeurs ne sont pas des
structures ou des types de données intrinsèques, comme
dans des langages comme C ou C++, mais ils sont
souvent utilisés de manière conceptuelle pour expliquer
comment manipuler directement des éléments de
structures de données (comme des tableaux, des listes
chaînées, etc.) via des références.
Qu'est-ce qu'un pointeur en algorithmique ?
• En algorithmique, un pointeur est une variable ou une
référence qui permet de se référer directement à un
emplacement de mémoire. Le pointeur ne contient pas la
valeur de la donnée, mais l'adresse de la donnée (le lieu
où elle est stockée).
• L'idée est de travailler sur des adresses plutôt que de
copier des valeurs directement, ce qui peut rendre les
algorithmes plus efficaces en termes de mémoire et de
temps.
Concepts clés en algorithmique liés aux
pointeurs
1. Références et adresses : Un pointeur fait référence à
l'emplacement où une donnée est stockée, plutôt qu'à la
donnée elle-même. Cela permet de manipuler les éléments
dans leur emplacement d'origine sans les copier.
2. Allocation dynamique : L'usage de pointeurs est
souvent lié à l'allocation dynamique de mémoire.
Par exemple, en travaillant avec des structures comme des
listes chaînées, on crée des éléments dans des zones de
mémoire spécifiques et on les relie avec des pointeurs.
Concepts clés en algorithmique liés aux
pointeurs
3 . Modification de variables via pointeurs : Un pointeur peut être
utilisé pour modifier directement la valeur d'une variable à l'endroit où elle
est stockée en mémoire. Ceci est utile dans des algorithmes où l'on doit
modifier plusieurs éléments d'une structure sans créer de copies.
4. Structures de données avec pointeurs : Les pointeurs sont
essentiels pour la mise en œuvre de certaines structures de données
comme :
Listes chaînées : Chaque nœud d'une liste contient une donnée et un
pointeur vers le nœud suivant.
Arbres binaires : Chaque nœud contient une donnée et deux
pointeurs, chacun vers un enfant (gauche et droit).
Exemple avec une liste chaînée :
Voici un exemple d'usage de pointeurs dans une liste chaînée simple.
Créer une structure de liste chaînée et permettre l'ajout de nouveaux éléments à la liste.
Structure Noeud
entier valeur
Noeud pointeurVersSuivant
Procédure ajouterEnTete(Liste, valeur)
NouveauNoeud ← allouer Noeud
NouveauNoeud.valeur ← valeur
NouveauNoeud.pointeurVersSuivant ← Liste.tete
Liste.tete ← NouveauNoeud
Procédure afficherListe(Liste)
NoeudCourant ← Liste.tete
Tant que NoeudCourant ≠ NULL afficher NoeudCourant.valeur
NoeudCourant ← NoeudCourant.pointeurVersSuivant
Début Liste ← allouer Liste
Liste.tete ← NULL
ajouterEnTete(Liste, 10)
ajouterEnTete(Liste, 20)
ajouterEnTete(Liste, 30)
afficherListe(Liste)
Explication
• Structure de liste chaînée :
• Chaque Noeud contient une valeur et un pointeur vers le suivant.
• On alloue dynamiquement un nouveau nœud lorsqu'on souhaite
ajouter un élément à la liste.
• ajouterEnTête() :
• Cette procédure crée un nouveau nœud et le place en tête de la
liste. Elle modifie directement la liste via des pointeurs.
• afficherListe() :
• Cette procédure traverse la liste en suivant les pointeurs de
chaque nœud jusqu'à la fin (quand le pointeur est NULL).
Pointeurs et performances
• L'utilisation des pointeurs améliore l'efficacité des
algorithmes dans plusieurs cas, car elle permet d'éviter de
copier des grandes quantités de données. Les structures
dynamiques comme les listes chaînées, les piles, et les
arbres reposent principalement sur les pointeurs pour
gérer la mémoire de manière flexible et dynamique.
• Ainsi, en algorithmique, les pointeurs permettent de
gérer la mémoire de façon plus fine et de manipuler
des structures complexes de manière plus efficace.
Pointeurs et performances
• Les pointeurs sont un concept fondamental en
programmation, surtout dans des langages comme C, C+
+, et d'autres langages basés sur des bas niveaux de
mémoire.
Principaux concepts liés aux pointeurs
• Déclaration d'un pointeur : Pour déclarer un pointeur, il faut utiliser le
symbole * avant le nom de la variable. Le type du pointeur doit
correspondre au type de la variable qu'il pointe.
int *ptr; // Déclare un pointeur à un entier
• Initialisation d'un pointeur : Un pointeur peut être initialisé en stockant
l'adresse mémoire d'une variable avec l'opérateur &.
int a = 10; int *ptr = &a; // Le pointeur 'ptr' pointe vers l'adresse de 'a'
• Déréférencement d'un pointeur : Déréférencer un pointeur signifie
accéder à la valeur stockée à l'adresse mémoire pointée par le pointeur.
Pour cela, on utilise l'opérateur *.
• int valeur = *ptr; // Accède à la valeur de 'a' à travers le pointeur
Principaux concepts liés aux pointeurs
• Pointeur nul : Un pointeur peut être initialisé à une valeur spéciale
appelée "pointeur nul" (généralement NULL ou nullptr en C++
moderne). Cela signifie qu'il ne pointe vers aucune adresse valide.
int *ptr = NULL;
• Pointeur et tableaux : Les tableaux et les pointeurs sont
étroitement liés. Le nom d'un tableau est en réalité l'adresse de
son premier élément, donc on peut traiter un tableau comme un
pointeur.
int arr[3] = {1, 2, 3}; int *ptr = arr; // 'ptr' pointe vers le premier
élément de 'arr'
Exercice :
Voici un petit exercice avec pointeurs en C, accompagné de la solution.
Écrivez un programme qui utilise des pointeurs pour échanger les valeurs de
deux variables entières.
• Objectif :
Utiliser des pointeurs pour modifier les valeurs de variables en dehors de la
fonction.
• Instructions :
1. Déclarez deux variables entières a et b et initialisez-les avec des valeurs.
2. Créez une fonction echanger(int *x, int *y) qui échange les valeurs de a et
b en utilisant des pointeurs.
3. Affichez les valeurs de a et b avant et après l'appel de la fonction
d'échange.
Solution
Algorithme EchangerParPointeurs
• Entrée : Deux entiers X et Y
• Sortie : Les valeurs échangées de X et Y
Fonction Echanger(Adresse a, Adresse b)
Début
Temp ← *a
*a ← *b
*b ← Temp
Fin
Début
Lire X, Y
Afficher ("Avant l'échange : X =", X, " Y =", Y ) // Appel de la fonction en passant les adresses
Echanger(Adresse(X), Adresse(Y))
Afficher ("Après l'échange : X =", X, " Y =", Y)
Fin
Solution
#include <stdio.h> // Fonction pour échanger deux entiers en utilisant des pointeurs
void echanger(int *x, int *y)
{
int temp = *x; // Sauvegarder la valeur pointée par x
*x = *y; // Copier la valeur de y dans x
*y = temp; // Copier la valeur de temp (ancienne valeur de x) dans y
}
int main() {
int a = 5;
int b = 10;
printf("Avant l'échange :\n");
printf("a = %d, b = %d\n", a, b); // Appel de la fonction echanger pour inverser les valeurs de a et b
echanger(&a, &b);
printf("Après l'échange :\n");
printf("a = %d, b = %d\n", a, b);
return 0; }
Explication du programme
• Fonction echanger(int *x, int *y) :
• La fonction prend deux pointeurs en paramètres.
• Un troisième entier temporaire temp est utilisé pour stocker la
valeur de *x.
• Ensuite, la valeur de *y est attribuée à *x, et enfin, temp est affecté
à *y. Cela échange les valeurs.
• Fonction main() :
• Deux variables entières a et b sont initialisées avec 5 et 10
respectivement.
• Les adresses de a et b sont passées à la fonction echanger(), qui
modifie directement les valeurs à ces adresses.
• Le programme affiche les valeurs avant et après l'échange.
Sortie du programme
• Avant l'échange : a = 5, b = 10
• Après l'échange : a = 10, b = 5
Exercice :
Écrire un algorithme qui permet d'inverser les éléments d'un tableau en utilisant des
pointeurs. Vous devez manipuler directement les adresses des éléments du tableau
au lieu de passer par des indices classiques.
Objectif :
Utiliser des pointeurs pour échanger les valeurs des éléments d’un tableau, de sorte
que le premier élément devienne le dernier, le deuxième devienne l'avant-dernier, etc.
Instructions :
• Demandez à l'utilisateur la taille du tableau.
• Créez un tableau dynamique (simulé en algorithmique) pour stocker les éléments.
• Écrivez une procédure qui utilise des pointeurs pour inverser les éléments du
tableau.
• Affichez le tableau avant et après l'inversion.
Solution Procédure inverserTableau(Tableau,
taille) gauche ← 0 // Pointeur vers le
• Début // Demander la taille du début du tableau droite ← taille - 1 //
tableau afficher "Entrez la taille du Pointeur vers la fin du tableau
tableau : « Tant que gauche < droite // Échanger les
• lire taille éléments pointés par gauche et droite
• // Déclarer un tableau dynamique temp ← Tableau[gauche]
(simulé) et demander à l'utilisateur Tableau[gauche] ← Tableau[droite]
d'entrer les éléments Tableau ← Tableau[droite] ← temp // Déplacer les
allouer tableau de taille éléments pointeurs vers le centre du tableau
gauche ← gauche + 1
• Pour i ← 0 à taille - 1
droite ← droite – 1
• afficher "Entrez l'élément ", i + 1, " : FinTantQue
« FinProcédure
• lire Tableau[i] // Appeler la procédure d'inversion
• FinPour inverserTableau(Tableau, taille)
• // Afficher le tableau avant inversion // Afficher le tableau après inversion
afficher "Tableau après inversion :"
• afficher "Tableau avant inversion :" Pour i ← 0 à taille - 1
• Pour i ← 0 à taille – 1 afficher Tableau[i]
• afficher Tableau[i] FinPour
• FinPour Fin
Explication
• Tableau dynamique :
• L'utilisateur entre la taille du tableau, et un tableau est créé (alloué
dynamiquement dans une implémentation réelle).
• Les éléments sont ensuite saisis par l'utilisateur et stockés dans le tableau.
• Procédure inverserTableau :
• Deux pointeurs, gauche et droite, sont utilisés pour parcourir le tableau. gauche
commence au début du tableau, et droite commence à la fin.
• Dans chaque itération, les éléments aux positions gauche et droite sont
échangés.
• Les pointeurs se rapprochent l’un de l’autre à chaque étape (le pointeur gauche
est incrémenté, et droite est décrémenté), jusqu’à ce qu'ils se rencontrent ou se
croisent, ce qui marque la fin de l'inversion.
• Affichage du tableau :
• Le tableau est affiché une première fois avant l'inversion.
• Après avoir appelé la procédure inverserTableau, le tableau est affiché une
seconde fois avec les éléments dans l'ordre inversé.
Exemple d'exécution
• Entrée :
• Entrez la taille du tableau : 5 Entrez l'élément 1 : 10
Entrez l'élément 2 : 20 Entrez l'élément 3 : 30 Entrez
l'élément 4 : 40 Entrez l'élément 5 : 50
Exemple d'exécution
• Sortie:
• Tableau avant inversion : 10 20 30 40 50
• Tableau après inversion : 50 40 30 20 10
Conclusion
• Cet exercice montre comment utiliser des pointeurs
conceptuels pour inverser les éléments d'un tableau en
algorithmique. L'approche utilisant des pointeurs est plus
efficace que de copier les valeurs dans un tableau
temporaire, car elle évite l'utilisation de mémoire
supplémentaire et minimise les opérations inutiles.
LES LISTES CHAINÉES
Definition
• Les listes chaînées (ou listes simplement chaînées) sont
une structure de données fondamentale en algorithmique,
particulièrement utile lorsque la taille des données n'est
pas fixe ou lorsque des insertions et suppressions
fréquentes sont nécessaires. Contrairement aux tableaux,
qui ont une taille fixe, une liste chaînée est une structure
dynamique où chaque élément (appelé nœud) est relié
au suivant par un pointeur.
Concepts de base d'une liste chaînée :
• Nœud (ou maillon) : Chaque élément d'une liste chaînée est
appelé un nœud. Un nœud contient :
• Une donnée (ou valeur) : l'élément stocké.
• Un pointeur vers le nœud suivant dans la liste.
• Pointeur vers la tête de la liste : Une liste chaînée
commence par un pointeur vers le premier nœud, appelé la
tête de la liste. Si la liste est vide, ce pointeur est NULL.
• Pointeur nul (NULL) : Le dernier nœud de la liste ne pointe
vers aucun autre nœud, donc son pointeur vers le suivant est
NULL, ce qui indique la fin de la liste.
Schéma d'une liste chaînée :
• [tête] --> [nœud1: valeur | pointeur] --> [nœud2: valeur |
pointeur] --> NULL
Structure d'un nœud de la liste chaînée
• Chaque nœud contient deux éléments :
• Valeur : La donnée stockée dans le nœud.
• PointeurVersSuivant : Un lien vers le nœud suivant dans
la liste.
Opérations principales sur une liste chaînée
• Insertion :
• On peut insérer un nouvel élément au début, à la fin ou à une position
donnée dans la liste.
• L'insertion au début est souvent l'opération la plus simple et rapide.
• Suppression :
• On peut supprimer un élément à partir de la tête, à une position donnée, ou
à la fin de la liste.
• Il faut ajuster les pointeurs pour maintenir la chaîne de nœuds.
• Parcours :
• Parcourir une liste chaînée consiste à traverser tous les nœuds un par un
en suivant les pointeurs, jusqu'à atteindre NULL.
• Recherche :
• On peut chercher un élément en parcourant la liste et en comparant les
valeurs de chaque nœud.
Insertion en tête d'une liste chaînée
Procédure insérerEnTête(Liste, valeur)
• NouveauNoeud ← allouer Noeud
• NouveauNoeud.valeur ← valeur
• NouveauNoeud.suivant ← Liste.tete
• Liste.tete ← NouveauNoeud
Insertion à la fin d'une liste chaînée :
• Procédure insérerEnFin(Liste, valeur)
NouveauNoeud ← allouer Noeud
NouveauNoeud.valeur ← valeur
NouveauNoeud.suivant ← NULL
Si Liste.tete = NULL
Liste.tete ← NouveauNoeud Sinon
NoeudCourant ← Liste.tete
Tant que NoeudCourant.suivant ≠ NULL
NoeudCourant ← NoeudCourant.suivant
FinTantQue
NoeudCourant.suivant ← NouveauNoeud
FinSi
Suppression d'un élément en tête
Procédure supprimerEnTête(Liste)
Si Liste.tete ≠ NULL
NoeudTemporaire ← Liste.tete
Liste.tete ← Liste.tete.suivant
libérer NoeudTemporaire
FinSi
Parcours d'une liste chaînée :
• Procédure parcourirListe(Liste)
NoeudCourant ← Liste.tete
Tant que NoeudCourant ≠ NULL
afficher NoeudCourant.valeur
NoeudCourant ← NoeudCourant.suivant
FinTantQue
Avantages d'une liste chaînée :
• Flexibilité : La taille d'une liste chaînée peut changer
dynamiquement, ce qui la rend plus flexible qu'un tableau à
taille fixe.
• Insertion/Suppression rapides : Ajouter ou supprimer un
élément en tête ou à une position donnée (lorsque l'élément
est trouvé) est rapide et ne nécessite pas de déplacement
des autres éléments, contrairement aux tableaux.
• Utilisation efficace de la mémoire : La mémoire n'est
allouée que lorsqu'un nouvel élément est ajouté.
Inconvénients d'une liste chaînée
• Accès séquentiel : Contrairement aux tableaux, où l'accès à
un élément est direct via un indice, accéder à un élément dans
une liste chaînée nécessite de parcourir la liste depuis la tête.
• Espace mémoire supplémentaire : Chaque nœud nécessite
de l'espace supplémentaire pour stocker le pointeur vers le
nœud suivant.
• Temps de parcours : Les opérations comme la recherche d'un
élément prennent plus de temps (linéaire) car il faut parcourir la
liste à partir de la tête.
Variantes de listes chaînées :
• Liste doublement chaînée : Chaque nœud contient deux
pointeurs : un vers le nœud suivant et un autre vers le
nœud précédent. Cela permet de parcourir la liste dans
les deux sens.
• Liste circulaire : Dans une liste circulaire, le dernier
nœud pointe vers le premier, formant un cycle.