On reprend le travail, mais cette fois sur un algorithme de tableaux, plus difficile, et en imposant un peu de contexte.

Entretien réalisé le 05/03/2025.

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 :

  1. Structure Tab : La structure Tab contient un champ len pour la longueur du tableau et un champ val qui est un pointeur vers un tableau d’entiers.
  2. Fonction has_duplicates : Cette fonction prend un tableau de type Tab en paramètre et retourne true si le tableau contient des duplications, sinon false.
  3. Tableau auxiliaire seen : Un tableau booléen seen est utilisé pour suivre les éléments déjà vus.
  4. 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.
  5. 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 :

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 :

  1. Boucle imbriquée : La fonction utilise deux boucles imbriquées pour comparer chaque élément du tableau avec tous les éléments suivants.
  2. 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.
  3. 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 :

  1. É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, si i = 2, comparer tab.val[2] avec tab.val[0] et tab.val[1] est redondant car ces comparaisons ont déjà été faites lors des itérations précédentes de la boucle sur i.
  2. Efficacité :

    • En commençant j à i + 1, chaque élément tab.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.
  3. Exemple :

    • Considérons un tableau tab avec les éléments [1, 2, 3, 4, 5].
    • Lorsque i = 0, j commence à 1, donc tab.val[0] est comparé avec tab.val[1], tab.val[2], tab.val[3], et tab.val[4].
    • Lorsque i = 1, j commence à 2, donc tab.val[1] est comparé avec tab.val[2], tab.val[3], et tab.val[4].
    • Cette approche garantit que chaque paire d’éléments est comparée exactement une fois.

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 :

  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 potentiellement introduire des erreurs logiques. Par exemple, comparer tab.val[i] avec tab.val[i] est toujours vrai, mais cela ne signifie pas qu’il y a une duplication dans le tableau.
  2. Éviter les comparaisons redondantes :

    • Comparer chaque élément avec tous les éléments précédents est redondant. Par exemple, si i = 2, comparer tab.val[2] avec tab.val[0] et tab.val[1] est inutile car ces comparaisons ont déjà été faites lors des itérations précédentes de la boucle sur i.

Simplicité :

  1. 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.

Exemple illustratif :

Considérons un tableau tab avec les éléments [1, 2, 3, 4, 5].

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…

Photo d'une grande affiche en train d'être installée sur un pignon d'immeuble, et sur laquelle il est écrit "We trained Copilot on your code. That's why it sucks"

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 ?

  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.
  2. É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.
  3. 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.
  4. 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].

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 ?

  1. 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 naturellement false à la fin.
  2. Correctitude :

    • Si tab.len est égal à zéro, les boucles for ne s’exécuteront pas, car la condition i < tab.len sera immédiatement fausse. Par conséquent, la fonction retournera false sans avoir besoin d’un test explicite pour un tableau vide.

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 ?