Adloun

Corrigé Mines-Ponts 2025 Informatique I MPI — Partie 3 (Q19 à Q29) : Skip lists : implémentation et complexité en moyenne

Sujet officiel du concours Mines-Ponts, session 2025, épreuve Informatique I MPI. Corrigé rédigé par Ibrahim Alame.

Sujet officiel (PDF, site du concours)

Énoncé

Une skip list est une liste chaînée triée par clef (niveau ) dont certains nœuds portent aussi des pointeurs de niveaux supérieurs : un pointeur de niveau mène au nœud suivant qui possède lui aussi un niveau . Le premier nœud est une sentinelle, sans donnée utile, qui possède tous les niveaux. Idéalement le niveau contient un nœud sur deux, le niveau un sur quatre (figure 2) :

Recherche : partir de la sentinelle au niveau le plus haut, avancer tant que la clef du nœud suivant est inférieure à la clef cherchée, sinon descendre d'un niveau ; au niveau , la recherche s'achève (figure 3, recherche de "g") :

En pratique, chaque nœud est présent au niveau supérieur avec probabilité . Les structures :


struct slNoeud_s {
  char* clef;
  int valeur;

  struct slNoeud_s* suivant_par_niveau[];
};
typedef struct slNoeud_s slNoeud;

struct slListe_s {
  int niveau_actuel;
  slNoeud* sentinelle;
};
typedef struct slListe_s slListe;

MAX_NIVEAU est le nombre maximal de niveaux d'un nœud ; la sentinelle a une clef vide, une valeur nulle et MAX_NIVEAU+1 pointeurs (indices à MAX_NIVEAU) initialisés à NULL. Sont fournies slNoeud* sl_creer_noeud(int niveau, char* clef, int valeur) (pointeurs à NULL) et :


int sl_mystere() {
  int niveau = 0;
  while ((rand() % 2) == 1 && niveau < MAX_NIVEAU) {
    niveau++;
  }
  return niveau;
}

void sl_ajoute_valeur(slListe* liste, char* clef, int valeur) {
  int niveau = sl_mystere();
  slNoeud* nouveau_noeud = sl_creer_noeud(niveau, clef, valeur);

  slNoeud* noeuds_a_mettre_a_jour[MAX_NIVEAU + 1];
  slNoeud* courant = liste->sentinelle;

  // PARTIE A
  /* parcourt l'ensemble des niveaux de la liste pour trouver
     dans chaque niveau le noeud après lequel insérer
     le nouveau noeud */
  // FIN PARTIE A

  if (niveau > liste->niveau_actuel) {
    // PARTIE B
    /* si le niveau du nouveau noeud est plus grand
       que le niveau actuel de la liste
       préparer la sentinelle */
    // FIN PARTIE B
  }

  // PARTIE C
  /* insérer le nouveau noeud */
  // FIN PARTIE C
}

Q19. Écrire int sl_rechercher(slListe* liste, char* clef), qui suit l'algorithme ci-dessus et renvoie la valeur associée, ou si la clef est absente.
Q20. Propriétés de la valeur renvoyée par sl_mystere (rand() renvoie un entier entre et un entier au moins égal à ).
Q21. Écrire la partie A (les nœuds à mettre à jour vont dans noeuds_a_mettre_a_jour).
Q22. Écrire la partie B.  Q23. Écrire la partie C.
Q24. Les skip lists relèvent-elles de Monte-Carlo ou de Las Vegas ?
Q25. Complexité de la recherche dans une skip list idéale de taille (chaque niveau contient exactement la moitié du précédent, idéalement répartie).
Désormais chaque élément monte au niveau supérieur avec probabilité .
Q26. Espérance du nombre d'éléments au niveau .
Q27. Espérance du niveau le plus haut d'une skip list de taille (on admet ensuite qu'elle est proportionnelle à ).
Q28. Nombre moyen de nœuds parcourus horizontalement au niveau lors d'une recherche.
Q29. En déduire la complexité moyenne de la recherche.

Corrigé

Conventions. Un nœud « de niveau » possède les pointeurs d'indices à (comme la sentinelle, de niveau MAX_NIVEAU) ; il appartient aux listes des niveaux . Le champ niveau_actuel est le plus haut niveau utilisé (il vaut pour une liste vide) et la sentinelle pointe sur NULL à tous les niveaux supérieurs. On note l'ensemble des éléments présents au niveau : chaque est chaîné par ordre croissant de clef, et . Ces conventions sont celles du sujet : la sentinelle a MAX_NIVEAU pointeurs, le tableau noeuds_a_mettre_a_jour a MAX_NIVEAU cases, sl_mystere renvoie un niveau entre et MAX_NIVEAU, et le test niveau > liste->niveau_actuel de sl_ajoute_valeur suppose un niveau courant déjà défini, , pour une liste encore vide.

Q19.


int sl_rechercher(slListe* liste, char* clef) {
  assert(liste != NULL && clef != NULL);
  slNoeud* courant = liste->sentinelle;
  for (int i = liste->niveau_actuel; i >= 0; i = i - 1) {
    while (courant->suivant_par_niveau[i] != NULL
           && strcmp(courant->suivant_par_niveau[i]->clef, clef) < 0) {
      courant = courant->suivant_par_niveau[i];
    }
  }
  slNoeud* candidat = courant->suivant_par_niveau[0];
  if (candidat != NULL && strcmp(candidat->clef, clef) == 0) {
    return candidat->valeur;
  }
  return -1;
}

Invariant : à la fin du traitement du niveau , courant est le dernier élément de de clef strictement inférieure à clef, ou la sentinelle s'il n'y en a pas. Au départ du niveau , courant est ce nœud pour le niveau (ou la sentinelle au niveau le plus haut) ; il appartient aussi à , et aucun élément de situé avant lui ne peut être « le dernier de clef inférieure », puisque lui-même convient ; la boucle avance tant que le suivant au niveau a une clef inférieure, et s'arrête sur un suivant nul ou de clef clef : l'invariant est rétabli. Au niveau , qui contient tous les éléments triés, le seul élément qui puisse porter la clef est donc le suivant de courant : on le teste par strcmp. La sentinelle, dont la clef vide n'a pas de sens, n'est jamais comparée. Chaque boucle while termine, car elle avance dans une liste finie.

Exécuté sur la figure 2 reconstruite : les sept clefs donnent leurs valeurs, la recherche de "g" fait exactement les trois comparaisons de la descente de la figure 3 (, , puis ) suivies du test d'égalité final, et "h", "aa", la clef vide, une liste vide donnent .

Ce que le correcteur attend : descendre par niveaux en repartant du nœud atteint ; un code qui repart de la sentinelle à chaque niveau, ou qui ne parcourt que le niveau , n'exploite pas la structure et n'est pas meilleur qu'une liste chaînée.

Q20. sl_mystere compte les « succès » consécutifs d'un tirage à pile ou face (rand() % 2 == 1), en s'arrêtant au premier échec ou à MAX_NIVEAU. En supposant les tirages indépendants et rand() % 2 égal à avec probabilité (c'est exact si rand est uniforme et RAND_MAX pair, par exemple ), la valeur renvoyée vérifie :

suit donc une loi géométrique de paramètre tronquée en MAX_NIVEAU ; ce n'est pas exactement une loi géométrique, puisque est possible et que la valeur est plafonnée. . C'est exactement le tirage voulu : un nœud est au niveau avec probabilité , et monte au niveau supérieur avec probabilité . Exécuté ( tirages) : fréquences ; ; ; et moyenne .

100 % gratuit

La suite du corrigé est sur Adloun

Le corrigé rédigé en entier, question par question, et le tuteur pour poser des questions sur ce sujet.

Voir le corrigé complet, gratuitement

Inscription gratuite en 30 secondes (e-mail, nom d'utilisateur, mot de passe, classe ou profil), sans carte bancaire : arrivée directe sur le corrigé.

Toutes les parties de l'épreuve

Corrigé complet gratuit →