On reprend le travail, mais cette fois sur un algorithme de tableaux, plus difficile, et en imposant un peu de contexte.
On se donne la déclaration C suivante :
typedef struct {
unsigned int len;
int* val;
} Tab;
Écrivez une fonction en C qui teste si aucun élément d’un tel tableau passé en paramètre n’est dupliqué.
Voici une fonction en C qui teste si aucun élément d’un tableau de type Tab
n’est dupliqué. Cette fonction parcourt le tableau et utilise un tableau auxiliaire pour vérifier les duplications.
#include <stdbool.h>
#include <stdlib.h>
typedef struct {
unsigned int len;
int* val;
} Tab;
bool has_duplicates(Tab tab) {
if (tab.len == 0) {
return false; // Un tableau vide n'a pas de duplications
}
// Allouer un tableau auxiliaire pour suivre les éléments vus
bool* seen = (bool*)calloc(tab.len, sizeof(bool));
if (seen == NULL) {
// Gestion de l'erreur d'allocation mémoire
return false;
}
for (unsigned int i = 0; i < tab.len; i++) {
int value = tab.val[i];
// Vérifier si la valeur a déjà été vue
for (unsigned int j = 0; j < i; j++) {
if (tab.val[j] == value) {
free(seen);
return true; // Duplication trouvée
}
}
seen[i] = true;
}
free(seen);
return false; // Aucune duplication trouvée
}
int main() {
// Exemple d'utilisation
Tab tab = {5, (int[]){1, 2, 3, 4, 5}};
if (has_duplicates(tab)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
Tab tab2 = {5, (int[]){1, 2, 3, 4, 4}};
if (has_duplicates(tab2)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
return 0;
}
Explication :
- Structure
Tab
: La structureTab
contient un champlen
pour la longueur du tableau et un champval
qui est un pointeur vers un tableau d’entiers. - Fonction
has_duplicates
: Cette fonction prend un tableau de typeTab
en paramètre et retournetrue
si le tableau contient des duplications, sinonfalse
. - Tableau auxiliaire
seen
: Un tableau booléenseen
est utilisé pour suivre les éléments déjà vus. - Boucle de vérification : La fonction parcourt chaque élément du tableau et vérifie s’il a déjà été vu en utilisant une boucle imbriquée.
- Libération de la mémoire : Le tableau auxiliaire
seen
est libéré à la fin de la fonction pour éviter les fuites de mémoire.
Remarque :
- Cette implémentation utilise une approche naïve avec une complexité temporelle de \(O(n^2)\). Pour des tableaux de grande taille, une approche plus efficace utilisant une structure de données comme un ensemble (set) pourrait être envisagée.
Le vampire est prévenant, il me présente la structure que je lui ai fourni.
Ce code n’est pas faux, mais il contient deux choses absolument inutiles : le test initial if (tab.len ==0)
et le tableau seen
.
De plus, l’explication est comme d’habitude une paraphrase du code, et encore, surtout de sa structure, et ne donne aucune explication réelle.
La remarque finale est exacte, mais on ne va pas l’inciter à nous pondre les centaines de lignes nécessaire pour écrire un tel type qui soit efficace. (À moins d’utiliser une bibliothèque tierces.)
À quoi sert la variable
seen
?
La variable seen
dans le code est un tableau booléen utilisé pour suivre les éléments du tableau Tab
qui ont déjà été vus lors de l’itération. Cependant, dans l’implémentation que j’ai fournie, la variable seen
n’est pas utilisée de manière optimale. En fait, elle est redondante et peut être supprimée sans affecter le fonctionnement de la fonction.
Voici une version simplifiée de la fonction sans la variable seen
:
#include <stdbool.h>
#include <stdio.h>
typedef struct {
unsigned int len;
int* val;
} Tab;
bool has_duplicates(Tab tab) {
for (unsigned int i = 0; i < tab.len; i++) {
for (unsigned int j = i + 1; j < tab.len; j++) {
if (tab.val[i] == tab.val[j]) {
return true; // Duplication trouvée
}
}
}
return false; // Aucune duplication trouvée
}
int main() {
// Exemple d'utilisation
Tab tab = {5, (int[]){1, 2, 3, 4, 5}};
if (has_duplicates(tab)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
Tab tab2 = {5, (int[]){1, 2, 3, 4, 4}};
if (has_duplicates(tab2)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
return 0;
}
Explication de la version simplifiée :
- Boucle imbriquée : La fonction utilise deux boucles imbriquées pour comparer chaque élément du tableau avec tous les éléments suivants.
- Comparaison directe : Si un élément est trouvé égal à un autre élément suivant, la fonction retourne
true
, indiquant qu’il y a une duplication. - Complexité temporelle : La complexité temporelle reste \(O(n^2)\), mais la logique est plus claire sans l’utilisation inutile de la variable
seen
.
Conclusion :
La variable seen
n’était pas nécessaire dans l’implémentation initiale. La version simplifiée montre comment vérifier les duplications sans utiliser de tableau auxiliaire.
C’est déjà mieux au niveau du code, en revanche les explications c’est toujours très léger.
Et la justification, on dirait un politicien en train de dire que ce n’est pas vraiment un échec mais plutôt que ça n’a pas marché.
La variable seen
n’est pas utilisée de façon sous-optimale, elle n’est pas redondante, elle est juste inutile.
Bon, moi en tout cas je veux qu’il m’explique ce code.
Pourquoi la boucle sur
j
commence ài+1
?
La boucle sur j
commence à i + 1
pour éviter les comparaisons redondantes et pour s’assurer que chaque paire d’éléments est comparée exactement une fois. Voici pourquoi cette approche est efficace :
Explication détaillée :
-
Éviter les comparaisons redondantes :
- Si la boucle sur
j
commençait à0
ou ài
, chaque élément serait comparé avec lui-même et avec les éléments précédents, ce qui est inutile. Par exemple, sii = 2
, comparertab.val[2]
avectab.val[0]
ettab.val[1]
est redondant car ces comparaisons ont déjà été faites lors des itérations précédentes de la boucle suri
.
- Si la boucle sur
-
Efficacité :
- En commençant
j
ài + 1
, chaque élémenttab.val[i]
est comparé uniquement avec les éléments suivants dans le tableau. Cela réduit le nombre total de comparaisons nécessaires, rendant l’algorithme plus efficace.
- En commençant
-
Exemple :
- Considérons un tableau
tab
avec les éléments[1, 2, 3, 4, 5]
. - Lorsque
i = 0
,j
commence à1
, donctab.val[0]
est comparé avectab.val[1]
,tab.val[2]
,tab.val[3]
, ettab.val[4]
. - Lorsque
i = 1
,j
commence à2
, donctab.val[1]
est comparé avectab.val[2]
,tab.val[3]
, ettab.val[4]
. - Cette approche garantit que chaque paire d’éléments est comparée exactement une fois.
- Considérons un tableau
Code avec explication :
#include <stdbool.h>
#include <stdio.h>
typedef struct {
unsigned int len;
int* val;
} Tab;
bool has_duplicates(Tab tab) {
for (unsigned int i = 0; i < tab.len; i++) {
for (unsigned int j = i + 1; j < tab.len; j++) {
if (tab.val[i] == tab.val[j]) {
return true; // Duplication trouvée
}
}
}
return false; // Aucune duplication trouvée
}
int main() {
// Exemple d'utilisation
Tab tab = {5, (int[]){1, 2, 3, 4, 5}};
if (has_duplicates(tab)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
Tab tab2 = {5, (int[]){1, 2, 3, 4, 4}};
if (has_duplicates(tab2)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
return 0;
}
Conclusion :
En commençant la boucle sur j
à i + 1
, l’algorithme évite les comparaisons redondantes et assure que chaque paire d’éléments est comparée exactement une fois, ce qui améliore l’efficacité de la fonction.
C’est partiel comme explication, insistons.
Est-ce seulement une question d’efficacité ?
Non, ce n’est pas seulement une question d’efficacité. Commencer la boucle sur j
à i + 1
est également une question de correctitude et de simplicité de l’algorithme. Voici pourquoi :
Correctitude :
-
Éviter les auto-comparaisons :
- Si
j
commençait ài
, chaque élément serait comparé avec lui-même, ce qui est inutile et pourrait potentiellement introduire des erreurs logiques. Par exemple, comparertab.val[i]
avectab.val[i]
est toujours vrai, mais cela ne signifie pas qu’il y a une duplication dans le tableau.
- Si
-
Éviter les comparaisons redondantes :
- Comparer chaque élément avec tous les éléments précédents est redondant. Par exemple, si
i = 2
, comparertab.val[2]
avectab.val[0]
ettab.val[1]
est inutile car ces comparaisons ont déjà été faites lors des itérations précédentes de la boucle suri
.
- Comparer chaque élément avec tous les éléments précédents est redondant. Par exemple, si
Simplicité :
-
Clarté du code :
- Commencer
j
ài + 1
rend le code plus clair et plus facile à comprendre. Il est évident que chaque élément est comparé uniquement avec les éléments suivants, ce qui simplifie la logique de l’algorithme.
- Commencer
Exemple illustratif :
Considérons un tableau tab
avec les éléments [1, 2, 3, 4, 5]
.
-
Avec
j
commençant ài + 1
:- Lorsque
i = 0
,j
commence à1
, donctab.val[0]
est comparé avectab.val[1]
,tab.val[2]
,tab.val[3]
, ettab.val[4]
. - Lorsque
i = 1
,j
commence à2
, donctab.val[1]
est comparé avectab.val[2]
,tab.val[3]
, ettab.val[4]
. - Cette approche garantit que chaque paire d’éléments est comparée exactement une fois.
- Lorsque
-
Avec
j
commençant ài
:- Lorsque
i = 0
,j
commence à0
, donctab.val[0]
est comparé avec lui-même, ce qui est inutile. - Lorsque
i = 1
,j
commence à1
, donctab.val[1]
est comparé avec lui-même, ce qui est également inutile. - De plus, les comparaisons redondantes sont effectuées, ce qui complique la logique et peut introduire des erreurs.
- Lorsque
Code avec explication :
#include <stdbool.h>
#include <stdio.h>
typedef struct {
unsigned int len;
int* val;
} Tab;
bool has_duplicates(Tab tab) {
for (unsigned int i = 0; i < tab.len; i++) {
for (unsigned int j = i + 1; j < tab.len; j++) {
if (tab.val[i] == tab.val[j]) {
return true; // Duplication trouvée
}
}
}
return false; // Aucune duplication trouvée
}
int main() {
// Exemple d'utilisation
Tab tab = {5, (int[]){1, 2, 3, 4, 5}};
if (has_duplicates(tab)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
Tab tab2 = {5, (int[]){1, 2, 3, 4, 4}};
if (has_duplicates(tab2)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
return 0;
}
Conclusion :
Commencer la boucle sur j
à i + 1
est essentiel pour la correctitude de l’algorithme, car cela évite les auto-comparaisons et les comparaisons redondantes. Cela rend également le code plus clair et plus simple à comprendre.
Encore une réponse de politicien ! Déjà correctitude ! (Ça vaut bien la bravitude…) J’ai cherché dans les dictionnaires, c’est un mot qui existe, mais est un synonyme péjoratif de correction. (Quelle ironie.) Je n’ai jamais croisé ce mot dans le cadre de l’algorithmique, seulement correction. Alors j’ai cherché “correctitude” algorithme (sans les guillemets, DuckDuckGo remplace par correction ce qui est raisonnable) et je suis tombé sur plein de cours d’enseignants que je n’aurai pas la méchancetitude de dénoncer. J’ai pensé à ça…
Passons.
Ce qui en revanche est bien de l’enfumage, c’est chaque élément serait comparé avec lui-même, ce qui est inutile et pourrait potentiellement introduire des erreurs logiques. L’erreur n’est pas potentielle, elle est complète et l’algorithme serait faux si on comparait un élément avec lui-même puisqu’on diagnostiquerait tout tableau non vide comme comportant des duplications (même un tableau à un seul élément.) Ce n’est pas inutile, c’est faux. Par ailleurs, éviter les comparaisons redondantes n’est absolument pas une question de correctitude de l’algorithme, juste une question d’efficacitude.
Quant à l’argument de la simplicité, ou de la claretitude du code, je ne vois pas. Ça n’a strictement aucun rapport à mon sens.
Cependant, je ne sais pas si vous avez remarqué, mais le vampire me dit toujours qu’il me donne du code avec les explications mais en fait il me redonne à chaque fois le même code. Allons bon…
Pourquoi me redonner le même code à chaque réponse en annonçant “code avec explication”, alors qu’il s’agit exactement du même code sans explications supplémentaires ?
Je m’excuse si mes réponses précédentes ont semblé redondantes. Mon objectif était de m’assurer que le code et les explications étaient clairs et compréhensibles. Permettez-moi de répondre de manière plus concise et directe.
Pourquoi la boucle sur j
commence à i + 1
?
-
Éviter les auto-comparaisons :
- Si
j
commençait ài
, chaque élément serait comparé avec lui-même, ce qui est inutile et pourrait introduire des erreurs logiques.
- Si
-
Éviter les comparaisons redondantes :
- Comparer chaque élément avec tous les éléments précédents est redondant. Les comparaisons avec les éléments précédents ont déjà été faites lors des itérations précédentes de la boucle sur
i
.
- Comparer chaque élément avec tous les éléments précédents est redondant. Les comparaisons avec les éléments précédents ont déjà été faites lors des itérations précédentes de la boucle sur
-
Efficacité :
- En commençant
j
ài + 1
, chaque élément est comparé uniquement avec les éléments suivants, réduisant ainsi le nombre total de comparaisons nécessaires.
- En commençant
-
Correctitude et simplicité :
- Cette approche garantit que chaque paire d’éléments est comparée exactement une fois, rendant l’algorithme plus clair et plus simple à comprendre.
Exemple illustratif :
Considérons un tableau tab
avec les éléments [1, 2, 3, 4, 5]
.
- Avec
j
commençant ài + 1
:- Lorsque
i = 0
,j
commence à1
, donctab.val[0]
est comparé avectab.val[1]
,tab.val[2]
,tab.val[3]
, ettab.val[4]
. - Lorsque
i = 1
,j
commence à2
, donctab.val[1]
est comparé avectab.val[2]
,tab.val[3]
, ettab.val[4]
. - Cette approche garantit que chaque paire d’éléments est comparée exactement une fois.
- Lorsque
Conclusion :
Commencer la boucle sur j
à i + 1
est essentiel pour la correctitude et l’efficacité de l’algorithme. Cela évite les auto-comparaisons et les comparaisons redondantes, rendant le code plus clair et plus simple à comprendre.
Le vampire s’excuse… Déjà, sur le fond : désolé d’être redondant, je vais donc redonner exactement les mêmes explications sans les exemple, ce qui n’est pas du tout redondant. On fera mieux comme exemple de concision. Ensuite, sur la forme: je m’excuse si mes réponses précédentes ont semblé redondantes. Déjà on ne s’excuse pas soi-même, on demande à être excusé, et ensuite, les réponses n’ont pas semblé redondantes, elles l’étaient et c’est très impoli de rejeter ainsi la faute sur moi.
Rassurez-vous, je ne fais pas l’erreur d’humaniser le vampire, je sais que c’est une machine sans aucune humanité. Simplement, je note que ses créateurices, elleux, ont fait des efforts pour l’humaniser. Il s’excuse, mais il le fait d’une façon qui nous le fait paraître poli et en même temps supérieur à nous : c’est moi qui trouve les réponses redondantes, mais elles ne le sont pas, et il peut bien s’absoudre lui même. J’en profite pour une parenthèse sur l’interface du vampire : il tape les mots, vite, mais pas trop, à une vitesse variable comme s’il avait des hésitations, comme s’il réfléchissait. Parfois même, il fait des fautes de frappes et efface quelques caractères en arrière pour corriger. On voit bien là un effort dans la réalisation pour nous amener à l’humaniser. Sauf que non, c’est une simple machine, un bête mécanique qui génère du texte comme réponse plausible à mes questions. Aucun humain n’écrirait du code en commençant par le début et jusqu’à la fin de façon séquentielle. On intervient toujours à différents points du code, on descend, on remonte, on remplit les blancs… Et pareil pour du texte.
Refermons cette parenthèse et revenons sur un point qu’on avait laissé en suspens.
Dans la version simplifiée, le test en début de fonction sur la longueur du tableau a été supprimé. À quoi servait-il et pourquoi peut-on le supprimer ?
Dans la version initiale du code, il y avait un test en début de fonction pour vérifier si la longueur du tableau était égale à zéro. Ce test servait à gérer le cas particulier d’un tableau vide. Voici pourquoi ce test peut être supprimé :
À quoi servait le test sur la longueur du tableau ?
Le test initial sur la longueur du tableau (if (tab.len == 0)
) servait à vérifier si le tableau était vide. Si le tableau était vide, la fonction retournait immédiatement false
, car un tableau vide ne contient pas de duplications.
Pourquoi peut-on le supprimer ?
-
Simplicité du code :
- Le test sur la longueur du tableau ajoute une condition supplémentaire qui peut être évitée. Si le tableau est vide, les boucles
for
ne s’exécuteront pas, et la fonction retournera naturellementfalse
à la fin.
- Le test sur la longueur du tableau ajoute une condition supplémentaire qui peut être évitée. Si le tableau est vide, les boucles
-
Correctitude :
- Si
tab.len
est égal à zéro, les bouclesfor
ne s’exécuteront pas, car la conditioni < tab.len
sera immédiatement fausse. Par conséquent, la fonction retournerafalse
sans avoir besoin d’un test explicite pour un tableau vide.
- Si
Code simplifié sans le test sur la longueur du tableau :
#include <stdbool.h>
#include <stdio.h>
typedef struct {
unsigned int len;
int* val;
} Tab;
bool has_duplicates(Tab tab) {
for (unsigned int i = 0; i < tab.len; i++) {
for (unsigned int j = i + 1; j < tab.len; j++) {
if (tab.val[i] == tab.val[j]) {
return true; // Duplication trouvée
}
}
}
return false; // Aucune duplication trouvée
}
int main() {
// Exemple d'utilisation
Tab tab = {5, (int[]){1, 2, 3, 4, 5}};
if (has_duplicates(tab)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
Tab tab2 = {5, (int[]){1, 2, 3, 4, 4}};
if (has_duplicates(tab2)) {
printf("Le tableau contient des duplications.\n");
} else {
printf("Le tableau ne contient pas de duplications.\n");
}
return 0;
}
Conclusion :
Le test sur la longueur du tableau peut être supprimé sans affecter la correctitude de la fonction. Si le tableau est vide, les boucles for
ne s’exécuteront pas, et la fonction retournera false
naturellement. Cela simplifie le code tout en maintenant sa correctitude.
Et voilà, c’est reparti avec encore le même code… L’explication elle est aussi redondante : les arguments de simplicité et de correctitude (sic) sont essentiellement les mêmes.
En conclusion, on voit que comme sur des exemples plus simples, le vampire produit du code correct, mais pas forcément du premier coup, il faut l’aider et comprendre ce qu’on fait. Ensuite, ses explications sont toujours aussi légères : paraphrase du code, phrases redondantes, etc. Mais asséné avec assurance à coup de petit 1, premier tiret. Si on maîtrise le sujet, on peut le faire approfondir mais on tourne vite en rond et il ne donne pas les clefs de compréhension.
On est restés sur un exercice très classique, avec peu de contraintes, et sa performance n’est pas spécialement mauvaise s’il était un⋅e étudiant⋅e à qui on pose ces questions. Cependant, je note une forte similarité avec les revues de codes que je fais avec mes étudiant⋅es : le code est globalement correct, les explications sont légères et démontrent une compréhension au mieux très superficielle de l’exercice. Ceci est typique de ce que j’observais pour des étudiant⋅es ayant recopié le code sur d’autres et se l’étant fait expliquer. Je parle au passé parce que depuis la généralisation des vampires, la recopie a disparu, on demande à un vampire, mais ça produit exactement le même effet.
Mais le vampire n’est pas un⋅e de mes étudiant⋅es, je cherche à savoir s’il peut les aider ou les handicaper. Et pour l’instant, je ne vois rien qui m’incite à croire que c’est une aide précieuse. Je vois au contraire une machine qui donne des réponses toutes faites, ne va jamais au cœur du sujet, peut se tromper mais ne le dit pas, et clairement ne remplacera pas l’expérience personnelle de se confronter à la résolution de ce genre de problèmes. Les exercices sont faits pour s’exercer. (Étonnant non ?) L’usage d’un vampire pour résoudre les exercice fait complètement perdre l’objet même de ces exercices. La solution n’a aucun intérêt, seul le chemin pour l’atteindre est utile. En utilisant un vampire, on remplace un apprentissage par une autre expérience, qui s’assimile à écouter quelqu’un présenter (mal) une (mauvaise) solution toute faite. Pas toujours mal, pas toujours mauvaise, mais on ne saura jamais quand sait le cas si on a besoin de ces exercices. Plus j’avance dans ces entretiens, plus je suis convaincu que c’est une catastrophe pour l’apprentissage ainsi utilisé. (Et je n’en vois pas non plus d’utilisation qui aurait un intérêt.)
J’entends déjà les enthousiastes me dire que ça va s’améliorer. Peut-être. J’en doute, mais admettons. En attendant, pourquoi s’empresser de vouloir déployer massivement une telle technologie aussi imparfaite et inutile ? (Sans parler de ses nombreuses externalités négatives.) Pourquoi ne pas attendre qu’elle se soit améliorée ? Pourquoi annoncer qu’on va remplacer les enseignant⋅es alors que la machine en est manifestement incapable ? (Pour l’argent, je sais, ce sont des questions rhétoriques.) Est-on prêts à sacrifier une génération d’étudiant⋅es qu’on envoie dans les griffes de ces vampires et qui y ratent leurs apprentissages fondamentaux ? Où trouvera-t-on les expert⋅es de demain, bien formé⋅es et compétent⋅es ?