IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours sur les Structures de Données


précédentsommairesuivant

1. Structures linéaires

1-1. Tableaux

La caractéristique fondamentale d'une structure linéaire est l'existence d'une fonction successeur qui à chaque élément de la structure - sauf éventuellement un élément particulier, le dernier - fait correspondre un autre élément de la structure.

Par exemple, un tableau est une structure de données basée sur la disposition contiguë d'un certain nombre d'éléments ayant le même type. Homogénéité et contiguïté rendent possible l'accès indexé aux éléments ; il en découle que, dans un tableau T, le successeur naturel de l'élément Ti est l'élément Ti+1. Un tableau est donc naturellement une structure linéaire.

Image non disponible
Figure 1 - Tableau

Les opérations les plus fréquentes sur les structures linéaires sont :

  • l'accès à un élément particulier (le premier élément, le dernier, le ie) ;
  • le parcours de la structure dans l'ordre déterminé par la fonction successeur.

Par exemple, vous avez déjà écrit d'innombrables fois le parcours d'un tableau T de n éléments :

 
Sélectionnez
for (i = 0; i < n; i++)
    « effectuer une certaine opération avec T[i] »

1-2. Listes chaînées

1-2-1. Notion

Dans un tableau, la relation successeur est implicite, elle découle de la contiguïté des composantes. Dans une liste chaînée, au contraire, la relation successeur est explicite : à chaque élément est accolée une information supplémentaire qui renvoie à son successeur, lequel n'a donc pas besoin d'être contigu, ni même voisin.

Image non disponible
Figure 2 - Liste chaînée

La notion de liste chaînée n'est pas liée à une manière concrète de matérialiser la relation successeur. Cependant, si le langage utilisé le permet, on choisit généralement d'associer deux principes :

  • les éléments sont référencés par leurs adresses dans la mémoire ;
  • chaque élément est alloué dynamiquement, au moment où il commence à être utile.

Ce qui, en C, donne les déclarations :

 
Sélectionnez
typedef ... OBJET; /* dépend du problème particulier considéré */
typedef struct maillon {
    OBJET info;
    struct maillon *suiv;
} MAILLON, *POINTEUR;
POINTEUR premier;

À retenir, trois points clés des listes chaînées :

  • chaque élément de la liste est formé de n+1 champs :

    • n champs constituant l'information portée par le maillon, dépendante du problème particulier considéré,
    • un champ supplémentaire qui est la matérialisation de la relation successeur ;
  • le dernier élément est signalé par une valeur conventionnelle du champ successeur (ex. : NULL) ;
  • on accède à la liste par l'adresse de son premier élément.

1-2-2. Insertion à la suite d'un élément donné

À titre de modèle, examinons l'opération la plus fondamentale et fréquente dans le traitement des listes chaînées : la création d'un maillon portant une certaine valeur X et son insertion dans une liste, à la suite d'un maillon donné.

Supposons que p ait pour valeur l'adresse de ce maillon (ne cherchons pas ici à savoir comment p a été déterminé). La séquence à exécuter est :

 
Sélectionnez
                                /* état initial: fig. 3 (a) */
q = malloc(sizeof(MAILLON));    /*     résultat: fig. 3 (b) */
q->info = X;                    /*     résultat: fig. 3 (c) */
q->suiv = p->suiv;              /*     résultat: fig. 3 (d) */
p->suiv = q;                    /*     résultat: fig. 3 (e) */
Image non disponible
Figure 3 (a)
Image non disponible
Figure 3 (b)
Image non disponible
Figure 3 (c)
Image non disponible
Figure 3 (d)
Image non disponible
Figure 3 (e)

1-2-3. Utilisation d'un allocateur particularisé

Deux opérations se présentent très souvent ensemble : l'allocation dynamique d'une structure pointée (l'appel de malloc) et le remplissage des champs de la structure ainsi créée. Il est clair et pratique de les réunir en une fonction spécifique, une sorte de version personnalisée de malloc :

 
Sélectionnez
POINTEUR maillon(OBJET i, POINTEUR s) {
    POINTEUR p = malloc(sizeof(MAILLON));
    assert(p != NULL);
    p->info = i;
    p->suiv = s;
    return p;
}

Cette fonction dépend étroitement de la structure utilisée (ses arguments formels en reproduisent exactement les champs), mais elle est indépendante de l'usage qu'on en fait dans une application. Dans la suite, chaque fois qu'une structure sera déclarée, nous supposerons que nous avons écrit l'allocateur correspondant.

Avec un tel allocateur, les quatre lignes de la page précédente s'écrivent bien plus simplement :

 
Sélectionnez
p->suiv = maillon(X, p->suiv);

1-2-4. Faut-il utiliser un tableau ou une liste chaînée ?

Cette question se pose au programmeur dans de nombreuses applications. Lorsque le choix est possible, on doit considérer les points suivants.

Avantages des tableaux par rapport aux listes :

  • accès indexé « direct ». C'est la définition même des tableaux : l'accès au ie élément se fait en un temps indépendant de i, alors que dans une liste chaînée ce temps est de la forme k × i (car il faut exécuter i fois l'opération p = p->suiv) ;
  • pas de surencombrement. La relation successeur étant implicite (définie par la contiguïté des composantes), il n'y a pas besoin d'espace supplémentaire pour son codage. Alors que dans une liste chaînée l'encombrement de chaque maillon est augmenté de la taille du pointeur suivant(1).

Avantages des listes par rapport aux tableaux :

  • efficacité et souplesse dans la définition de la relation successeur. Cette relation étant explicite, on peut la modifier aisément (les maillons des listes chaînées peuvent être réarrangés sans avoir à déplacer les informations portées). Autre aspect de la même propriété : un même maillon peut appartenir à plusieurs listes (cf. § 1.5Application aux matrices creuses (listes orthogonales)) ;
  • encombrement total selon le besoin, si on utilise, comme dans les exemples précédents, l'allocation dynamique des maillons. Le nombre de maillons d'une liste correspond au nombre d'éléments effectivement présents, alors que l'encombrement d'un tableau est fixé d'avance et constant.

NOTE. Une conséquence de l'encombrement constant d'un tableau est le risque de saturation : l'insertion d'un élément échoue parce que toutes les cases du tableau sont déjà occupées. Bien sûr, ce risque existe aussi dans le cas des listes chaînées (il se manifeste par l'échec de la fonction malloc), mais il correspond alors à la saturation de la mémoire de l'ordinateur ; c'est un événement grave mais rare. Alors que le débordement des tableaux est beaucoup plus fréquent, car il ne traduit qu'une erreur d'appréciation de la part du programmeur (si le tableau est statique) ou de l'utilisateur (si le tableau est dynamique).

1-2-5. Le parcours des listes, NULL et le coup de la sentinelle

Le parcours des listes est une opération qui revient fréquemment dans les applications. Par exemple, le parcours d'une liste à la recherche d'un maillon portant une information x donnée alors que l'on n'est pas sûr qu'un tel maillon existe, peut se programmer ainsi :

 
Sélectionnez
POINTEUR trouver(OBJET x, POINTEUR p) { /* rend p tel que p->info == x ou NULL */
    while (p != NULL && p->info != x)
        p = p->suiv;
    return p;
}

Supposons avoir à écrire un programme où cette opération est critique (autrement les considérations suivantes sont sans intérêt). Nous devons essayer de rendre la boucle while ci-dessus, qui est déjà très simple, encore moins onéreuse.

C'est le moment de nous rappeler que la seule caractéristique de NULL que nous utilisons est le fait d'être une adresse conventionnelle distincte de toutes les autres adresses manipulées. Par conséquent, n'importe quelle adresse pourra jouer le rôle de NULL, à la condition qu'elle ait fait l'objet d'une convention préalable, connue et suivie par la partie de notre programme qui a construit la liste. Par exemple, l'adresse NULLBIS d'un maillon créé par nos soins :

 
Sélectionnez
MAILLON laFinDeToutesLesListes;
#define NULLBIS (&laFinDeToutesLesListes)

Il est clair qu'un programme correct (ne contenant pas initialement les deux noms ci-dessus) dans lequel on a ensuite ajouté les deux lignes précédentes, puis remplacé par NULLBIS chaque occurrence de NULL, est encore correct. Par exemple (ceci suppose que, comme dit précédemment, en construisant la liste on a utilisé NULLBIS à la place de NULL) :

 
Sélectionnez
POINTEUR trouver(OBJET x, POINTEUR p) { /* rend p t.q. p->info == x ou NULLBIS */
    while (p != NULLBIS && p->info != x)
        p = p->suiv;
    return p;
}

Pour le moment nous n'avons rien gagné. Mais cela devient intéressant quand on s'aperçoit qu'on peut améliorer la fonction trouver en logeant dans le maillon pointé par NULLBIS une valeur « sentinelle » :

 
Sélectionnez
POINTEUR trouver(OBJET x, POINTEUR p) { /* rend p t.q. p->info == x ou NULLBIS */
    NULLBIS->info = x;
    while (p->info != x) /* maintenant on est sûr de "trouver" x */
        p = p->suiv;
    return p;
}

Dans la boucle (la seule partie qui doit nous intéresser, du point de vue de l'efficacité) nous avons remplacé deux comparaisons par une seule. On peut estimer que la boucle a été réduite de 2/3.

1-2-6. La structure de liste vue de haut

Des sections précédentes se dégage une notion de liste assez terre à terre, faisant penser plus à une collection de trucs et astuces pour économiser l'espace ou gagner du temps qu'à une structure de données tant soit peu abstraite. Pour rehausser le débat, voici comment on peut présenter les listes d'une manière plus formelle, comme cela se fait dans les langages Lisp, Prolog, etc. où elles sont les structures de données fondamentales.

DÉFINITION

Soit X un ensemble. Une liste d'éléments de X est définie récursivement comme : un symbole conventionnel, appelé la liste vide, ou bien, un couple ( i , L ) où i ∈ X et L est une liste d'éléments de X.

Le rapport avec ce que nous appelions liste précédemment devient évident si on décide qu'une liste sera représentée dans un programme par une adresse : soit NULL, représentant la liste vide, soit l'adresse d'un maillon à deux champs représentant une paire ( i , L ) :

Image non disponible
Figure 4. Autour de la notion de liste

De cette définition récursive des listes dérive immédiatement une définition récursive de l'algorithme de leur parcours : pour parcourir une liste L il faut :

  • si L est la liste vide, ne rien faire ;
  • si L = ( i , L' ), exploiter i puis parcourir L'.

Ici, « exploiter » représente une opération qui dépend de l'ensemble X et du problème particulier considéré. Ce qui donne le programme :

 
Sélectionnez
void parcours(POINTEUR liste) {
    if (liste != NULL) {
        EXPLOITER(liste->info);
        parcours(liste->suiv);
    }
}

1-2-7. Les listes ordonnées

Une liste ordonnée est une liste dont les maillons ont un champ clé (éventuellement le champ info tout entier), dont les valeurs appartiennent à un ensemble totalement ordonné, et la liste vérifie :

 
Sélectionnez
si p->suiv existe alors p->cle ≤ p->suiv->cle

Les listes ordonnées se rencontrent très fréquemment, nous en verrons des exemples dans les pages suivantes. L'ajout d'un maillon nouveau dans une telle structure met en évidence une qualité des listes : l'insertion d'un élément au milieu de la liste ne requiert pas le déplacement « physique » des éléments qui seront derrière le nouveau venu.

L'insertion d'un élément dans une liste ordonnée se fait en trois temps (les deux premiers sont interchangeables) :

  • rechercher la place dans la liste du nouvel élément ;
  • allouer le nouveau maillon et remplir ses champs ;
  • raccrocher le nouvel élément à la liste, ce qui se fait de deux manières, selon que :

    • la liste est vide ou le nouvel élément se place à la tête de la liste (c.-à-d. : le nouvel élément ne sera le successeur d'aucun maillon),
    • la liste n'est pas vide et le nouvel élément ne se place pas en tête.

Ce qui donne, en supposant que la liste est repérée par un pointeur teteDeListe :

 
Sélectionnez
POINTEUR pr, pc, nouveau;
pc = teteDeListe; /* pc parcourt la liste, pr court après pc */
while (pc != NULL && pc->cle < x) {
    pr = pc;
    pc = pc->suiv;
}
nouveau = maillon(x, autres informations, pc);
if (pc == teteDeListe)
    teteDeListe = nouveau;
else
    pr->suiv = nouveau;

1-2-8. Le célèbre maillon « bidon »

On peut abolir la différence de traitement qui existe entre l'insertion d'un maillon en tête d'une liste et l'insertion à une autre place (voir le programme précédent), en convenant que toute liste commencera par un maillon « technique », sans signification pour l'application. Ainsi, d'un point de vue pratique, une liste ne sera jamais vide et, lors de l'insertion d'un maillon, il n'y aura qu'un seul cas à considérer.

Si on procède ainsi, une liste ne sera pas représentée par l'adresse de son premier maillon utile mais par l'adresse d'un maillon sans signification, voire - si on programme en C - par ce maillon sans signification lui-même. À la place de :

 
Sélectionnez
POINTEUR teteDeListe;
Image non disponible
Figure 5 (a). Liste habituelle

on aura :

 
Sélectionnez
MAILLON maillonBidon;
Image non disponible
Figure 5 (b). Liste commençant par un maillon sans signification

Voici le programme de recherche/insertion qui correspond à une telle liste. Comme prévu, la deuxième partie de l'insertion se simplifie bien :

 
Sélectionnez
POINTEUR pr, pc;
…
pr = &maillonBidon;
pc = maillonBidon.suiv;
while (pc != NULL && pc->cle < x) {
    pr = pc;
    pc = pc->suiv;
}
pr->suiv = maillon(x, /* autres informations */, pc);

On peut trouver cette manière de gérer les listes préférable, mais ce n'est pas une évidence. Elle ne simplifie que le texte du programme, dont l'exécution n'est ni plus ni moins rapide que celle de la version précédente, car nous n'avons pas modifié la boucle while qui, du point de vue de l'efficacité, est la seule partie à considérer. D'autre part, cette simplification se paye par un encombrement supplémentaire de chaque liste, puisque nous avons remplacé un pointeur par un maillon tout entier. Ce surencombrement ne peut être tenu pour négligeable que lorsqu'on doit gérer un petit nombre de listes ou bien des listes faites de maillons dont la partie info est de petite taille.

VARIANTE

Ayant introduit le maillon sans signification, qui garantit qu'une liste a toujours au moins un élément, nous pouvons récrire le programme précédent à l'aide d'un seul pointeur :

 
Sélectionnez
POINTEUR pr;
…
pr = &maillonBidon;
while (pr->suiv != NULL && pr->suiv->cle < x)
    pr = pr->suiv;
pr->suiv = maillon(x, autres informations, pr->suiv);

Il est difficile de dire si cette version du programme est plus ou moins rapide que la précédente, car dans la boucle, nous avons supprimé une affectation mais nous avons ajouté deux indirections.

1-3. Piles

Nous avons défini les tableaux et les listes chaînée par leur constitution (en répondant à la question « comment cela est fait ? »). Au contraire, les piles et les queues se définissent par leur comportement (en répondant à la question « qu'est-ce que cela fait ») ; on dit que ce sont des structures de données abstraites.

Une pile est une structure de données dynamique (des éléments y sont introduits, puis extraits) ayant la propriété que, lors d'une extraction, l'élément extrait est celui qui a y été introduit le plus récemment. On dit « dernier entré, premier sorti » ou, en bon français, LIFO (« Last In First Out »).

C'est une propriété abstraite, qui n'impose pas une constitution particulière. En fait, on réalise aussi bien des piles à partir de tableaux qu'à partir de listes chaînées.

1-3-1. Réalisation d'une pile à l'aide d'un tableau

 
Sélectionnez
typedef ... OBJET; /* dépend du problème */
#define MAXPILE ... /* selon estimation du besoin */
OBJET pile[MAXPILE];
int niveau;

void initialiser(void)
{
    niveau = 0;
}

void empiler(OBJET x)
{
    assert(niveau < MAXPILE);
    pile[ niveau++ ] = x;
}

OBJET depiler(void)
{
    assert(niveau > 0);
    return pile[ --niveau ];
}

1-3-2. Réalisation d'une pile à l'aide d'une liste chaînée

 
Sélectionnez
typedef struct maillon
{
    OBJET info;
    struct maillon *suiv;
} MAILLON, *POINTEUR;

POINTEUR maillon(OBJET i, POINTEUR s)
{
    POINTEUR p;
    p = malloc(sizeof(MAILLON));
    assert(p != NULL);
    p->info = i;
    p->suiv = s;
    return p;
}

POINTEUR pile;

void initialiser(void)
{
    pile = NULL;
}

void empiler(OBJET x)
{
    pile = maillon(x, pile);
}

OBJET depiler(void)
{
    POINTEUR p;
    OBJET r;
    assert(pile != NULL);
    p = pile;
    pile = pile->suiv;
    r = p->info;
    free(p);
    return r;
}

1-4. Queues

Une queue, ou file d'attente, est une structure de données dynamique telle que, lors d'une extraction, l'élément extrait est celui qui s'y trouve depuis le plus longtemps. On dit « premier entré, premier sorti » ou encore FIFO (« First In First Out »).

1-4-1. Réalisation d'une queue à l'aide d'une liste chaînée

Image non disponible
Figure 6. Queue réalisée par une liste chaînée
 
Sélectionnez
POINTEUR premier, dernier;

void initialiser(void)
{
    premier = NULL;
}

void entrer(OBJET x)
{
    POINTEUR p;
    p = maillon(x, NULL);
    if (premier == NULL)
        premier = p;
    else
        dernier->suiv = p;
    dernier = p;
}

OBJET sortir(void)
{
    POINTEUR p;
    OBJET r;
    assert(premier != NULL);
    p = premier;
    premier = premier->suiv;
    r = p->info;
    free(p);
    return r;
}

1-4-2. Réalisation d'une queue à l'aide d'un tableau

Pour implanter une queue nous nous donnerons deux indices, appelés premier et dernier, et nous utiliserons un tableau circulaire, obtenu à partir d'un tableau ordinaire en décidant que la première case du tableau fait suite à la dernière.

D'un point de vue technique, il suffira de modifier légèrement l'opération « passage au successeur ». Pour un tableau ordinaire, cette opération se traduit par l'incrémentation d'un indice :

 
Sélectionnez
i = i + 1;

Pour avoir un tableau circulaire, nous faisons une incrémentation modulo la taille du tableau :

 
Sélectionnez
i = (i + 1) % TAILLE_TABLEAU;
Image non disponible
Figure 7 (a) Queue réalisée par un tableau circulaire

Attention, premier n'est pas nécessairement devant dernier :

Image non disponible
Figure 7 (b) Queue réalisée par un tableau circulaire
 
Sélectionnez
#define MAXQUEUE 10
OBJET queue[MAXQUEUE];
int premier, dernier, nombre;
#define SUCCESS(i) (((i) + 1) % MAXQUEUE)

void initialiser(void)
{
    nombre = 0;
    premier = dernier = 0;
}

void entrer(OBJET x)
{
    assert(nombre < MAXQUEUE);
    nombre++;
    queue[dernier] = x;
    dernier = SUCCESS(dernier);
}

OBJET sortir(void)
{
    OBJET r;
    assert(nombre > 0);
    nombre--;
    r = queue[premier];
    premier = SUCCESS(premier);
    return r;
}

Remarque : on peut être surpris par la présence du compteur nombre, croyant que la position relative de premier et dernier suffit pour déceler qu'une queue est vide ou pleine. Or, il y a un petit piège : la figure 8 montre une queue avec deux éléments, que l'on fait sortir successivement, et une autre avec deux cases libres, que l'on remplit.

Image non disponible Image non disponible

On constate que la position relative de premier et dernier, lorsque la queue est vide, est la même que celle qui correspond à une queue pleine. D'où la nécessité de prendre des mesures supplémentaires, comme veiller à ce qu'il y ait toujours une case libre ou bien utiliser un compteur du nombre d'éléments.

1-5. Application aux matrices creuses (listes orthogonales)

Un avantage important des listes chaînées est de permettre de donner à une même collection d'informations plusieurs structures de liste, selon plusieurs critères. Par exemple, une collection de fiches décrivant les abonnés à un certain service peut être organisée de telle manière que chaque élément appartienne à plusieurs listes : la liste des abonnés qui sont dans la même tranche d'âge, la liste des abonnés qui ont le même type de profession, la liste des abonnés qui habitent dans la même région, etc.

Nous allons considérer un exemple concret d'une telle organisation : les listes orthogonales, utilisées pour la représentation économique des matrices creuses, ou matrices contenant « beaucoup » de zéros(2). L'idée est de maintenir de telles matrices en ne représentant que les coefficients non nuls. Plus précisément (voyez la figure 9) chaque valeur ai,j telle que ai,j ≠ 0 occupe un maillon appartenant à deux listes chaînées : la liste des coefficients non nuls de la ligne i (ordonnée par rapport aux indices de colonne, nous verrons l'intérêt d'un tel arrangement) et la liste des coefficients non nuls de la colonne j (ordonnée par rapport aux indices de ligne). Il y a donc une liste, éventuellement vide, pour chaque ligne et une liste pour chaque colonne.

Ces maillons pourront être déclarés de la manière suivante :

 
Sélectionnez
typedef struct maillon
{
    double val;
    short il;           /* indice de la ligne */
    short ic;           /* indice de la colonne */
    struct maillon *sl; /* suivant dans la même ligne */
    struct maillon *sc; /* suivant dans la même colonne */
} MAILLON, *POINTEUR;
Image non disponible
Figure 9. Représentation d'une matrice creuse

Supposons que les matrices qui nous intéressent aient n lignes et n colonnes. Une matrice sera alors déterminée par la donnée de deux tableaux de n pointeurs chacun : le tableau des têtes des lignes et celui des têtes des colonnes :

 
Sélectionnez
#define N … /* dépend du problème considéré */

typedef struct matrice
{
    POINTEUR ligne[N], colonne[N];
} MATRICE;

Supposons que, comme le suggère la figure 9, les champs val, sl et sc ont la même taille, qui est le double de celle des champs il et ic (si val avait une taille supérieure, ce dispositif serait encore plus avantageux). Soit p le nombre de coefficients non nuls. Exprimé en nombre de fois la taille d'un pointeur, l'encombrement de notre structure est 2n + 4p.

La place occupée par la matrice ordinaire correspondante aurait été n2. L'affaire est donc rentable dès que

kitxmlcodelatexdvpn < \frac{n^2 - 2n}{4} \approx \frac{n^2}{4}finkitxmlcodelatexdvp

Examinons à présent la manipulation de telles structures. Comme on pouvait s'y attendre, la création d'un élément est un peu compliquée, mais les autres opérations sont effectuées avec une efficacité égale ou supérieure à celle des matrices habituelles ; à titre d'exemple nous présentons le produit.

Ajout d'un élément. L'ajout d'un coefficient revient à faire deux fois l'insertion d'un maillon dans une liste ordonnée. Notez que l'utilisation d'une sentinelle (NULL a été remplacé par NULLBIS, l'adresse d'un maillon ayant le plus grand entier comme numéro de ligne et de colonne) simplifie les deux boucles while qui constituent l'essentiel du travail :

 
Sélectionnez
MAILLON finDesListes = { 0, INT_MAX, INT_MAX, NULL, NULL };
#define NULLBIS (&finDesListes)

void insertion(double a, int i, int j, MATRICE *m)
{
    POINTEUR pr, pc, q = maillon(a, i, j, NULLBIS, NULLBIS);
    pc = m->ligne[i];
    while (pc->ic < j) /* rappel: NULLBIS->ic == INT_MAX */
        pr = pc, pc = pc->sl;
    q->sl = pc;

    if (pc == m->ligne[i])
        m->ligne[i] = q;
    else
        pr->sl = q;

    pc = m->colonne[j];
    while (pc->il < i) /* rappel: NULLBIS->il == INT_MAX */
        pr = pc, pc = pc->sc;
    q->sc = pc;

    if (pc == m->colonne[j])
        m->colonne[j] = q;
    else
        pr->sc = q;
}

Multiplication de deux matrices. Le programme obtenu, malgré la complexité des structures mises en œuvre, est plus rapide que celui qui utilise la structure usuelle des matrices, car dans la boucle la plus profonde, il n'y a pas d'accès complexe du type t[i][j] :

 
Sélectionnez
void produit(MATRICE *a, MATRICE *b, MATRICE *c)
{
    int i, j;
    double v;
    POINTEUR pa, pb;
    for (i = 0; i < N; i++)
        c->ligne[i] = c->colonne[i] = NULLBIS;
    for (i = 0; i < N; i++)
        for (j = 0; j < N; j++)
        {
            v = 0;
            pb = b->colonne[j];
            for (pa = a->ligne[i]; pa != NULLBIS; pa = pa->sl)
            {
                while (pb->il < pa->ic)
                    pb = pb->sc;
                if (pb->il == pa->ic)
                    v += pa->val * pb->val;
            }
            if (v != 0)
                insertion(v, i, j, c);
        }
}

précédentsommairesuivant
Néanmoins, ce surencombrement est bien moins important qu'il n'y paraît, car contrairement à ce que suggèrent nos dessins, le champ info est souvent assez volumineux, ce qui rend la taille du champ suiv négligeable devant celle du maillon tout entier.
De telles matrices ne sont pas aussi marginales qu'on pourrait le croire. Par exemple, une méthode classique pour représenter un graphe, comme un réseau routier, dont les sommets sont numérotés de 1 à n, consiste à introduire la matrice (ai,j) définie par ai,j = la longueur de l'arc joignant le sommet i au sommet j, si cet arc existe, 0 sinon. Cette matrice possède n2 coefficients. Or, voyez une carte routière, le nombre d'arêtes du graphe, c'est-à-dire le nombre de ai,j non nuls, est « de l'ordre de n » (c.-à-d. majoré par k × n avec, souvent, k ridiculement petit).

Copyright © 2014 Henri Garreta. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.