Colle 2 — Fusion et tri par comptage¶
Colle · MP2I · exercices 1 et 2 de la colle 2
Cours : Tri fusion · Complexité
Autres parties : Exercice 3. Arbres dans des tableaux
Les fonctions sont à écrire en C. Chaque question est suivie de tests qui doivent passer sans erreur.
Exercice 1 — Fusion de tableaux triés¶
On souhaite ici écrire une fonction fusion qui permet, étant donné deux
tableaux triés t1 et t2, de construire un tableau tf contenant les
éléments de t1 et de t2 mais globalement triés.
Ainsi, si t1 contient 0, 1, 4, 6 et t2 contient 1, 2, 3, 4, 5, le
tableau tf contiendra 0, 1, 1, 2, 3, 4, 4, 5, 6.
Question 1
Écrire la fonction fusion.
void fusion(int t1[], int n1, int t2[], int n2, int tf[])
{
// t1 contient n1 éléments, t2 contient n2 éléments
// et tf contient n1+n2 éléments indéterminés
}
Corrigé
void fusion(int t1[], int n1, int t2[], int n2, int tf[])
{
int a = 0;
int b = 0;
for(int i = 0; i < n1 + n2; i++)
{
if (a < n1 && (b == n2 || t1[a] < t2[b]))
{
tf[i] = t1[a];
a++;
}
else
{
tf[i] = t2[b];
b++;
}
}
}
Tests :
int t1[] = { 0, 1, 4, 6 };
int t2[] = { 1, 2, 3, 4, 5 };
int t[9];
fusion(t1, 4, t2, 5, t);
int tres[] = {0, 1, 1, 2, 3, 4, 4, 5, 6};
assert(memcmp(t, tres, sizeof(int) * 9) == 0);
Question 2
Prouver la correction de la fonction précédente. Il faudra non
seulement s'assurer que le tableau final est trié, mais aussi qu'il contient
exactement les éléments de t1 et de t2.
Corrigé
On va noter ici \(N = n_1+n_2\) et \(t[i..j]\) les éléments de \(t\) aux indices entre \(i\) et \(j\). On note aussi \(t1[n_1] = t2[n_2] = +\infty\).
On va prouver l'invariant
où \(\mathrm{trie}(i)\) signifie que \(tf[0..i-1]\) est trié, \(\mathrm{min12}(i,a,b)\) signifie que \(\forall k < i,\ tf[k] \le \min(t1[a],t2[b])\) et \(\mathrm{memes}(i,a,b)\) que \(tf[0..i-1]\) contient exactement les mêmes éléments que \(t1[0..a-1]\) et \(t2[0..b-1]\) réunis.
- Initialisation : au départ \(i=a=b=0\) et on a donc directement \(\mathrm{trie}(0)\), \(\mathrm{min12}(0,0,0)\) et \(\mathrm{memes}(0,0,0)\) car tout est vide.
- Hérédité : supposons \(I(i,a,b)\) vérifié au début de l'itération,
avec \(i < N\). Alors :
- Soit on est dans le premier cas car \(a < n_1\), \(b < n_2\) et \(t1[a] < t2[b]\). On va alors affecter \(t1[a]\) à \(tf[i]\), or \(\mathrm{min12}(i,a,b)\) est vérifié, donc pour \(k < i\) on a \(tf[k] \le \min(t1[a],t2[b]) = t1[a] = tf[i]\). Ainsi \(\mathrm{trie}(i+1)\) est vérifié. De plus, \(tf[i] = t1[a] \le t1[a+1]\) car \(t1\) est trié (ou \(t1[a+1] = +\infty\) si \(a+1 = n_1\)) et \(tf[i] < t2[b]\) par hypothèse. Donc on a \(\mathrm{min12}(i+1,a+1,b)\) vérifié. \(\mathrm{memes}(i+1,a+1,b)\) est vérifié directement car on a juste \(t1[a]\) comme nouvel élément et c'est celui qu'on a rajouté. Enfin, \(i+1 = (a+1) + b\) vu que \(i = a+b\), et \(a + 1 \le n_1\).
- Soit on est dans le premier cas car \(a < n_1\) et \(b = n_2\). Il suffit de reprendre le cas précédent en ayant en tête qu'on a posé \(t2[b] = +\infty\).
- Soit on est dans le second cas. Alors \(b < n_2\) : sinon, on aurait \(b = n_2\) et donc \(a = i - n_2 < n_1\), et on serait dans le premier cas. On a donc \(a = n_1\) ou \(t2[b] \le t1[a]\), c'est-à-dire, avec la convention \(t1[n_1] = +\infty\), \(t2[b] \le t1[a]\) dans tous les cas. Ce cas est alors symétrique des précédents en échangeant les rôles de \(t1\) et \(t2\) (et donc de \(a\) et de \(b\)).
L'invariant est bien vérifié en sortie d'itération.
Conclusion : on a \(I(N,a,b)\) en sortie, avec \(N = a+b\), \(a \le n_1\) et \(b \le n_2\). Comme \(N = n_1 + n_2\), forcément \(a = n_1\) et \(b = n_2\). Ainsi \(tf\) est trié et il contient les mêmes éléments que \(t1\) et \(t2\).
Question 3
Déterminer la complexité de cette fonction.
Corrigé
Pas de difficulté ici, on a une boucle for qui effectue un nombre
constant d'opérations élémentaires lors de chacune de ses \(N\) étapes. On est
donc en \(O(N) = O(n_1+n_2)\).
Exercice 2 — Tri par comptage¶
On considère ici une méthode de tri dite par comptage. On va supposer que les tableaux qu'on cherche à trier ne contiennent que des entiers entre \(0\) et \(N-1\) où \(N\) est fixé. L'idée est alors, dans un tableau de taille \(N\), de compter le nombre d'occurrences de chacun des entiers. Ensuite, on peut se servir de ce tableau d'occurrences pour en déduire le tableau trié.
On demande à ce que la complexité soit en \(O(N+n) = O(n)\) en supposant \(N\) constant, où \(n\) est la taille du tableau à trier.
Question 4
Écrire la fonction occurrences.
void occurrences(int t[], int n, int occ[], int N)
{
// t est un tableau de n entiers
// occ est un tableau de N entiers dont les valeurs sont
// non initialisées.
// on demande de placer dans occ[i] le nombre d'occurrences de i dans t
}
Corrigé
void occurrences(int t[], int n, int occ[], int N)
{
for(int i = 0; i < N; i++)
occ[i] = 0;
for(int i = 0; i < n; i++)
occ[t[i]]++;
}
Tests :
int t[] = { 0, 1, 2, 3, 0, 1, 2, 1 };
int occ[4];
occurrences(t, 8, occ, 4);
int res[] = { 2, 3, 2, 1 };
assert(memcmp(occ, res, sizeof(int) * 4) == 0);
Question 5
Écrire la fonction comptage.
#define N 100 // on définit ici une constante N qui vaut 100,
// ce qui permet de faire un tableau t[N]
void comptage(int t[], int n)
{
// t contient n entiers entre 0 et N-1
// trier t par comptage
}
Corrigé
void comptage(int t[], int n)
{
int occ[N];
occurrences(t, n, occ, N);
int a = 0;
for(int i = 0; i < N; i++)
{
for(int k = 0; k < occ[i]; k++)
{
t[a] = i;
a++;
}
}
}
Tests :
int t[] = { 0, 1, 2, 3, 0, 1, 2, 1 };
comptage(t, 8);
int res[] = { 0, 0, 1, 1, 1, 2, 2, 3 };
assert(memcmp(t, res, sizeof(int) * 8) == 0);
Question 6
Quelle est la complexité de ce tri ? Que pensez-vous de sa pertinence si on avait fixé \(2^{31}\) comme valeur de \(N\) ?
Corrigé
Le calcul du tableau des occurrences est en \(O(N+n) = O(n)\) en supposant
\(N\) constant. Le tri lui-même crée ce tableau puis effectue une boucle en
\(O(N+n)\) : \(N\) pour le premier for (rien que pour gérer les indices) et
\(n\) pour la somme de toutes les itérations du second for. Donc le tri est
globalement en \(O(N+n) =O(n)\).
Avec une très grande valeur de \(N\), il n'est plus pertinent de considérer \(N\) constant car \(n\) est sûrement d'un ordre de grandeur plus petit. En fait, \(N\) ne doit pas être d'un ordre de grandeur plus grand que \(n\) pour que ce tri soit efficace. De plus, pour une grande valeur de \(N\), le compromis espace-temps est peu favorable : on ne va pas allouer 8 Go (\(2^{31}\) entiers de 4 octets) pour trier un tableau !
Ce tri est donc bien plus adapté pour des petites valeurs de \(N\). Il reste alors très efficace et souvent plus pertinent qu'un tri en \(O(n \log n)\) comme le tri fusion.