informatique (MP2I/MPI) · Chapitre 7 · Application directe du cours
Problème — Le tableau associatif : un contrat, deux réalisations
Structures séquentielles : listes, piles, files · niveau 3 (difficile)
Énoncé
Le programme demande la « structure de tableau associatif ». On veut associer une valeur à une clé.
- Écrire le contrat.
- Le réaliser par une liste d'associations, puis par un tableau trié par clé.
- Compter, puis mesurer, le coût de écritures suivies de lectures.
- Aucune des deux n'est satisfaisante. Pourquoi, et que faut-il pour l'être ?
Corrigé
1. Le contrat.
| Opération | Rôle | Effet |
|---|---|---|
creer() | constructeur | un tableau associatif vide |
ecrire(t, c, v) | transformateur | associe à la clé , en écrasant |
lire(t, c) | accesseur | la valeur associée à , ou l'absence |
supprimer(t, c) | transformateur | retire l'association de clé |
Invariant : une clé apparaît au plus une fois. C'est lui qui donne son sens au mot « associatif », et c'est ecrire qui en est responsable — d'où la nécessité de chercher avant d'ajouter.
2. Les deux réalisations.
/* (a) Liste d'associations : on cherche, on modifie ou on ajoute en tete. */
typedef struct assoc_s { int cle; int val; struct assoc_s* suiv; } assoc;
assoc* ecrire(assoc* t, int c, int v) {
for (assoc* m = t; m != NULL; m = m->suiv) {
if (m->cle == c) { m->val = v; return t; } /* deja la */
}
assoc* n = malloc(sizeof(assoc));
n->cle = c; n->val = v; n->suiv = t;
return n;
}
/* (b) Tableaux paralleles TRIES par cle, recherche par dichotomie.
INVARIANT : cles[0] < cles[1] < ... < cles[n-1]. */
typedef struct { int* cles; int* vals; int n; int cap; } tabtri;
int place(const tabtri* t, int c) { /* premier i tel que cles[i] >= c */
int g = 0, d = t->n;
while (g < d) {
int m = g + (d - g) / 2;
if (t->cles[m] < c) { g = m + 1; } else { d = m; }
}
return g;
}
void ecrire(tabtri* t, int c, int v) {
int i = place(t, c);
if (i < t->n && t->cles[i] == c) { t->vals[i] = v; return; }
assert(t->n < t->cap);
for (int j = t->n; j > i; j = j - 1) { /* on ouvre le trou */
t->cles[j] = t->cles[j-1]; t->vals[j] = t->vals[j-1];
}
t->cles[i] = c; t->vals[i] = v; t->n = t->n + 1;
}
Noter que place est écrite pour rendre le point d'insertion même quand la clé est absente : une seule fonction sert à la lecture et à l'écriture, et la propriété « premier indice de clé » est son invariant de sortie.
3. Le compte et la mesure. On compte les sondes, c'est-à-dire les comparaisons de clés.
n = 8000, cles deux a deux distinctes
liste : 31 996 000 sondes a l'ecriture, 32 004 000 a la lecture
trie : 92 633 sondes a l'ecriture, 103 810 a la lecture
repere : n(n-1)/2 = 31 996 000 n log2 n = 103 726
La liste donne exactement sondes à l'écriture : chaque insertion parcourt tout ce qui précède, puisque aucune clé n'est déjà là. Le tableau trié donne sondes en lecture pour un repère théorique de : la dichotomie tient sa promesse à près.
4. Pourquoi aucune des deux ne convient.
lire | ecrire | Parcours ordonné | |
|---|---|---|---|
| Liste d'associations | non | ||
| Tableau trié | oui |
Le tableau trié cache un dans son écriture, et c'est le point du problème : la dichotomie trouve la place en , mais l'insertion décale ensuite cases. Compter les seules sondes le masque complètement — c'est pourquoi on ne mesure jamais une seule grandeur.
Il faut donc une structure où la recherche et l'insertion soient sous-linéaires. Deux réponses, et le livre les donne toutes les deux :
- la table de hachage (chapitre chap:hachage), en moyenne pour les deux, au prix de l'ordre : on ne peut plus parcourir les clés triées ;
- l'arbre binaire de recherche équilibré (chapitre chap:tas), garanti pour les deux, en conservant l'ordre.
Et c'est bien la thèse du chapitre précédent : le contrat n'a pas bougé d'une ligne entre les quatre réalisations. Ce qui change, c'est ce qu'on peut se permettre de répéter.