Implémentation et gestion des listes chaînées simples en langage C

Pourquoi privilégier les listes chaînées ?

Dans l'apprentissage du C, le tableau est généralement la première structure de données utilisée pour stocker des collections. Cependant, les tableaux imposent des contraintes majeures : - Ils occupent un bloc de mémoire contigu.

  • Leur taille doit être définie à la compilation ou lors de l'allocation initiale, ce qui manque de flexibilité.
  • Si l'accès à un élément est rapide ($O(1)$), l'insertion ou la suppression au milieu du tableau nécessite de décaler les éléments, entraînant une complexité de $O(n)$.

La liste chaînée pallie ces limitations. Elle est idéale pour les situations où le nombre d'éléments fluctue fréquemment, car elle permet des modifications structurelles efficaces. ### Architecture et cnocepts fondamentaux

Contrairement aux tableaux, une liste chaînée n'est pas stockée de manière linéaire en mémoire. Elle se compose de nœuds reliés entre eux par des références. - Le Nœud : C'est l'unité de base. Il contient un champ de données (le contanu utile) et un champ pointeur (l'adresse du nœud suivant).

  • La Tête (Head) : Un pointeur pointant vers le premier élément. Si la liste est vide, ce pointeur est nul.
  • La Queue (Tail) : Le dernier élément de la liste, dont le pointeur vers le "suivant" est égal à NULL.

Avantages et compromis

L'utilisation des listes chaînées offre plusieurs bénéfices : 1. Flexibilité mémoire : Les nœuds peuvent être dispersés n'importe où dans le tas (heap). 2. Allocation dynamique : On peut ajouter ou retirer des éléments à la volée selon les besoins réels du programme, évitant ainsi le gaspillage de ressources. 3. Opérations d'insertion/suppression : Une fois l'emplacement trouvé, modifier la structure ne demande que la mise à jour de quelques pointeurs ($O(1)$).

Le revers de la médaille réside dans l'accès aux données. Pour lire le $n$-ième élément, il est impossible d'utiliser un index direct comme avec un tableau ; il faut parcourir la liste depuis le début, ce qui donne une complexité de recherche en $O(n)$. ### Structure de données en C

Voici comment définir un nœud pour une liste chaînée simple stockant des entiers : ``` typedef struct Element { int donnee; struct Element *suivant; } Element, *Liste;


### Opérasions de création

Il existe deux stratégies principales pour construire une liste : l'insertion par la tête ou par la queue. #### 1. Insertion par la tête

Cette méthode place chaque nouvel élément au début de la liste. ```
void insererEnTete(Liste *L, int nombre) {
    Liste nouveau;
    for (int i = 0; i < nombre; i++) {
        nouveau = (Liste)malloc(sizeof(Element));
        nouveau->donnee = rand() % 100;
        nouveau->suivant = *L;
        *L = nouveau;
    }
}

2. Insertion par la queue

Cette méthode conserve l'ordre de création en ajoutant les éléments à la fin. ``` void insererEnQueue(Liste *L, int nombre) { Liste nouveau, dernier; *L = NULL; dernier = NULL;

for (int i = 0; i < nombre; i++) {
    nouveau = (Liste)malloc(sizeof(Element));
    nouveau->donnee = rand() % 100;
    nouveau->suivant = NULL;

    if (*L == NULL) {
        *L = nouveau;
    } else {
        dernier->suivant = nouveau;
    }
    dernier = nouveau;
}

}


### Recherche d'un élément

Pour extraire une valeur à une position spécifique, nous utilisons un curseur qui parcourt la chaîne. ```
int obtenirElement(Liste L, int position, int *valeur) {
    int compteur = 0;
    Liste curseur = L;

    while (curseur && compteur < position) {
        curseur = curseur->suivant;
        compteur++;
    }

    if (!curseur) return 0; // Échec

    *valeur = curseur->donnee;
    return 1; // Succès
}

Insertion à une position donnée

L'insertion nécessite de manipuler les pointeurs du nœud précédent pour intégrer le nouveau maillon. ``` int insererAilleurs(Liste *L, int position, int valeur) { Liste courant = *L; int j = 0;

while (courant && j < position - 1) {
    courant = courant->suivant;
    j++;
}

if (!courant) return 0;

Liste maille = (Liste)malloc(sizeof(Element));
maille->donnee = valeur;
maille->suivant = courant->suivant;
courant->suivant = maille;

return 1;

}


### Suppression d'un nœud

Pour supprimer un élément, on court-circuite le nœud cible en reliant son prédécesseur à son successeur, puis on libère la mémoire. ```
int supprimerElement(Liste *L, int position) {
    Liste prec = *L, cible;
    int j = 0;

    while (prec->suivant && j < position - 1) {
        prec = prec->suivant;
        j++;
    }

    if (!(prec->suivant)) return 0;

    cible = prec->suivant;
    prec->suivant = cible->suivant;
    free(cible);

    return 1;
}

Libération de la mémoire

Il est crucial de libérer chaque nœud individuellement pour éviter les fuites de mémoire lors de la destruction de la liste. ``` void viderListe(Liste *L) { Liste temp, curseur; curseur = *L;

while (curseur) {
    temp = curseur->suivant;
    free(curseur);
    curseur = temp;
}
*L = NULL;

}

Étiquettes: C structures de données Gestion de la mémoire algorithmique Listes chaînées

Publié le 4 juillet à 05h20