Le tri par dénombrement.
Le tri par dénombrement, souvent appelé tri linéaire en référence à sa classe de complexité en temps, est un tri qui opère uniquement sur des listes de valeurs entières avec une faible dispersion, quantité définie par \begin{equation} \Delta(L):=1-\frac{\#\{x_i\mid x_i\in L\}}{\text{max}\,L -\text{min}\,L + 1} \end{equation} Le numérateur de la fraction est le nombre de valeurs distinctes dans la liste \(L\) et le dénominateur est l'étendue de la liste, i.e. la taille de l'intervalle entier délimité par les valeurs minimale et maximale de la liste \(L\). Nous préciserons dans la section Complexité la raison de cette hypothèse. D'après ce théorème sur la complexité des tris comparatifs, nous savons que si ce tri a une complexité linéaire, il opère nécessairement sans faire de comparaisons sur les termes de la liste.L'algorithme opère en deux phases. La première phase consiste à parcourir la liste \(L\) afin de dénombrer les occurrences de chaque valeur. Par exemple la liste \(L=[{\color{#FF0}7},3,4,{\color{#FF0}7},4,{\color{#FF0}7}]\) contient une fois la valeur 3, 2 fois la valeur 4 et 3 fois la valeur \({\color{#FF0}7}\). Pour ranger ces nombres d'occurrences, on utilise une table auxiliaire \(H\) qui fait office d'histogramme*.(*) Un histogramme est une table contenant le nombre d'occurences de chaque valeur d'une liste triée, la valeur servant de clé d'entrée de la table. L'idéal serait de pouvoir utiliser les valeurs \(v\) comme clés d'indexation de l'histogramme \(H\), mais les tables sont indexées avec des entiers positifs et les valeurs de la liste peuvent être négatives. Il faut donc trouver un palliatif. Il suffit de translater les valeurs \(v\) de \(\delta:= 1 -\text{min}\, L\) où \(\text{min}\, L\) est la plus petite valeur dans la liste \(L\) pour que \(\text{min}\, L\) soit associée à l'index 1. Dans notre exemple la plus petite valeur étant 3, il faut décaler toutes les valeurs de la liste de \(\delta=-2\) positions. Donc \(H=[1_3,2_4,0_5,0_6,3_{\color{#FF0}7}]\) (nous avons indiqué les valeurs correspondantes de la liste en indice). L'exercice suivant propose une autre alternative pour l'indexation.
À l'issue de la première phase, pour chaque valeur \(v\) dans la liste on dispose de son nombre d'occurrences à la position \(v + \delta\) dans l'histogramme \(H\). La deuxième phase consiste à parcourir \(H\) avec une variable d'indexation \(i\) et à ranger exactement \(H[i]\) fois la valeur \(i -\delta\) dans la liste \(L\). Dans notre exemple, \(H=[1_3,2_4,0_5,0_6,3_{\color{#FF0}7}]\), on range donc successivement 1 fois la valeur 3 puis 2 fois la valeur 4 et 3 fois la valeur 7 dans \(L\) pour obtenir la liste triée \(L=[3,4,4,7,7,7]\).
Vous pouvez observer ci-dessous la valeur de l'histogramme pour une liste \(L\) de votre choix (les valeurs \(v\) sont limitées aux entiers \(-15\leq v\leq 15\) pour limiter la taille de l'affichage) :
On peut à présent écrire l'algorithme TriDenombrement. Par hypothèse, la liste \(L\) contient des entiers relatifs. Dans un premier temps, il faut trouver la valeur minimale \(\text{min}\,\) et la valeur maximale \(\max\) dans la liste \(L\) (instruction #8) afin de déterminer l'espace nécessaire pour ranger les nombres d'occurrences dans l'histogramme \(H\). Notons que la recherche de ces deux valeurs peut se faire en une seule passe sur la liste \(L\), il est donc préférable de ne pas faire appel aux fonctions Min et Max des langages de programmation quand elles existent. Il suffit ensuite de parcourir \(L\) une seconde fois pour incrémenter les valeurs de l'histogramme. Dans un deuxième temps, on parcourt cette fois la liste \(H\) et on range chaque valeur \(v := i-\delta \) exactement \(H[i]\) fois dans la liste \(L\).
TriDenombrement(@L) données L: liste d'entiers variables i,j,min,max,delta: entiers H: table d'entiers debut (min,max) ← MinMax(L) Allouer(H, max - min + 1, 0) delta ← 1 - min i ← 1 TQ (i ≤ #L) FAIRE H[L[i] + delta] ← H[L[i] + delta] + 1 i ← i + 1 FTQ i ← 1 j ← 1 TQ (i ≤ #L) FAIRE SI (H[j] > 0) ALORS L[i] ← j - delta H[j] ← H[j] - 1 i ← i + 1 SINON j ← j + 1 FSI FTQ FIN
Le tri par répartition.
Il s'agit d'une variante du tri par dénombrement qui s'avère plus versatile. Ce tri calque exactement le mode opératoire d'un facteur dans un centre de tri postal. Le facteur dispose d'une armoire de tri (cf. table ci-dessous) constituée d'autant de casiers (de listes) qu'il y a d'adresses dans sa tournée.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Avant de commencer sa tournée, le facteur récupère les enveloppes à distribuer, i.e. les éléments de la liste \(L\), et range chaque enveloppe \(L[i]\) dans le casier correspondant à l'adresse sur l'enveloppe. Une fois toutes les enveloppes réparties dans les casiers, le facteur n'a plus qu'à les rassembler dans l'ordre des casiers (les concaténer) pour commencer sa tournée.
Si la liste est définie sur un ensemble \(V\) quelconque plutôt que sur des entiers, on peut généraliser le tri en se donnant une fonction d'adressage \(\text{adr}:V\rightarrow{\mathbf N}\) qui va renvoyer l'adresse \(\text{adr}(v)\) du casier dans lequel il faut ranger la valeur \(v\) de la liste. Si la liste contient des entiers naturels, il suffit de choisir comme fonction d'adressage, l'application identique \(v\mapsto v\).
L'écriture de l'algorithme de répartition des mots de la liste à l'aide des casiers ne pose aucune difficulté particulière. Notons que nous travaillons directement sur la liste en entrée qui est vidée au fur et mesure que ses éléments sont répartis dans les différents casiers. Après la répartition, la liste \(L\) est vide et on la reconstitue en concaténant les casiers dans l'ordre naturel. On peut bien entendu réaliser la même opération en renvoyant une liste différente de celle d'entrée. L'intérêt de cette écriture est que cette répartition se fait in situ. L'algorithme auxiliaire Enfiler(L,x) rajoute l'élément \(x\) à la fin de la liste \(L\), opération que l'on peut toujours réaliser en \(\Theta(1)\) avec une structure ad hoc. L'algorithme auxiliaire Concaténer(L,L') concatène deux listes et cette opération peut elle aussi être réalisée avec un coût constant \(\Theta(1)\). Le chapitre Listes, piles, files étudie précisément les questions de complexité liées à l'utilisation du modèle de données liste selon les multiples structures que l'on peut lui associer. L'algorithme Eteter(L) renvoie l'élément au début de la liste et l'élimine de la liste.
TriRépartition(@L,adr) données L: liste d'élément de V adr: fonction d'adressage V → N variables Casiers: tableau de listes amax, i: entiers v: élément de V debut amax ← Max(L,adr) Allouer(Casiers, amax, []) TQ (L != []) FAIRE v ← Eteter(L); Enfiler(Casiers[adr(v)], v) FTQ i ← 1 TQ (i ≤ #Casiers) FAIRE L ← Concaténer(L,Casiers[i]); i ← i + 1 FTQ FIN
Complexité
Commençons par l'algorithme TriDenombrement. Notons \(n:=\#L\) la taille de la liste considérée. Le calcul du minimum et du maximum coûte une passe sur la liste, soit \(\Theta(n\)) et l'allocation de l'histogramme \(\Theta(m)\) où \(m\) désigne l'étendue de la liste \(L\). La construction de l'histogramme demande une nouvelle passe sur la liste et à donc un coût de \(\Theta(n)\). La reconstruction de la liste dans la boucle #18 est facile à analyser globalement, on vide chacun des \(H[i]\) et on sait que \[\sum_{i=1}^{\max L - \text{min}\, L +1}H[i] = n\] donc la reconstruction de la liste coûte elle aussi \(\Theta(n)\). L'analyse de la complexité de l'algorithme TriRéparition est identique si l'on suppose que l'appel de la fonction d'adressage \(\text{adr}\) a un coût constant \(\Theta(1)\). On en conclut le théorème :
Travaux pratiques
(2) Écrivez une fonction tliste Histogramme(tliste L) qui renvoie l'histogramme des valeurs contenues dans la liste \(L\). Le nombre d'occurences de la valeur minimale doit être rangée à l'index 0 de l'histogramme à l'aide d'une translation (cf. cours).
(3) Enfin, écrivez une fonction void TriDenombrement(tliste L) qui effectue le tri de la liste \(L\) à l'aide de l'algorithme du tri par dénombrement.
Indication : le type tliste est une structure C contenant un tableau et sa taille.(4) Comparez la complexité moyenne du tri par dénombrement avec celle du tri par tas empiriquement en traitant des listes de type permutations aléatoires d'ordre \(n\). Pour cela, tracez la courbe représentative de la fonction comptant le nombre d'accès à un terme d'une liste (\(L\) ou histogramme) pour le tri par dénombrement et celle de la fonction comptant le nombre de comparaisons pour le tri par tas avec gnuplot.
Pour générer une liste de type permutation aléatoire, utilisez cette suggestion d'un précédent TP.
typedef struct TCELL{ int valeur; struct TCELL *suiv; } TCELL; typedef TCELL *TLISTE;(1) Écrivez les fonctions TLISTE *InsQueue(TLISTE *L, int valeur) et TLISTE *InsTete(TLISTE *L, int valeur) qui rajoutent respectivement la valeur entière valeur à la queue et à la tête de la liste chaînée L, et renvoient cette nouvelle liste. NB. La liste L peut-être initialement vide (NULL).
(2) Écrivez une fonction TLISTE SupTete(TLISTE *L) qui renvoie la liste ne contenant que la cellule en tête de la liste L en la supprimant de cette liste. Écrivez une fonction TLISTE Rabouter(TLISTE L1, L2) qui concatène la liste L2 à la liste L1 et renvoie cette liste (on ne crée donc pas de nouvelle liste. il n'y a aucune cellule créée.
(3) Écrivez une fonction TLISTE *Repartir(TLISTE *L) qui renvoie un tableau de listes chaînées indexé par les valeurs entières (à une translation près) contenues dans la liste L et qui répartit les différentes cellules L dans les listes correspondantes du tableau. La liste L doit donc être vide à la fin de la répartition.
(4) À l'aide des fonctions précédentes, écrivez une fonction void TriRepartition(TLISTE L) qui réalise le tri par répartition.