Solutions du TD-1, recherche en table

1  Liste des prénoms

Voici le programme solution. La fonction d'insertion est une insertion simple en table triée, suffisante pour le nombre somme toute réduit d'élèves de l'école.

L'algorithme à employer est décrit : ``insertion successive des prénoms'', il est même précisé que la table contient les prenoms triés.

Bon mais qu'est-ce qu'une table ? Une table est un tableau de chaîne (table) et un entier (next) qui indique la prochaine place libre dans la table.

Bon, comment insérer dans une table triée ? On part d'un squelette
  static void insertion(String nom) {

  }
Pour insérer dans la table le plus simple est de la parcourir, donc une boucle ``for'' :
static void insertion(String nom) {

   for (int i=0 ; i < next ; i++) {
     ...
  }
}
Ainsi, dans le corps de la boucle, on a i qui pointe dans la table à coup sûr. On ne s'en préocupera donc plus.

On va comparer nom à chaque élément de la table, jusqu'à arriver à trois situations différentes :
  1. nom est déjà dans la table.
  2. Il y a un prénom plus grand que nom dans la table.
  3. On sort de la table.
Dans le premier cas, il n'y a pas lieu de poursuivre le parcours. En effet, la table est une table des prénoms différents.
static void insertion(String nom) {

  for (int i=0 ; i < next ; i++) {
    int r = nom.compareTo(table[i]);
    if (r == 0)
      return;
     ....
  }
}
Le plus simple est de retourner immédiatement de la méthode insertion par l'instruction return.

Dans le deuxième cas, on est certain que nom n'est pas dans la table et on vient de trouver sa place (car la table est triée), il faut donc l'insérer. Ensuite on retournera de insertion comme dans le cas précédent :
static void insertion(String nom) {

  for (int i=0 ; i < next ; i++) {
    int r = nom.compareTo(table[i]);
    if (r == 0)
      return;
    else if (r < 0) {
       for (int j=next ; j > i ; j--) // Décaler la fin de la table
         table[j] = table[j-1];
       table[i] = nom;                // Mettre nom à sa place
       next++;                        // Un elt de plus dans la table
       return;                        // Fini pour ce nom
    }
  }
  ...
}
L'écriture de la boucle est terminée. En effet, si on a ni ``r == 0'', ni ``r > 0'', alors on a forcément ``r < 0'' c'est à dire que nom est plus petit que table[i] et que l'on peut passer à l'itération suivante.

Par conséquent, lorsque la table a été complètement parcourue, on est sûr que nom est strictement plus grand que tous les éléments de la table. Il faut donc l'insérer en fin de table :
static void insertion(String nom) {

  for (int i=0 ; i < next ; i++) {
    int r = nom.compareTo(table[i]);
    if (r == 0)
      return;
    else if (r < 0) {
       for (int j=next ; j > i ; j--) // Décaler la fin de la table
         table[j] = table[j-1];
       table[i] = nom;                // Mettre nom à sa place
       next++;                        // Un elt de plus dans la table
       return;                        // Fini pour ce nom
    }
  }
  table[next] = nom;
  next++;
}
(Notons que l'insertion en fin de table s'applique notamment lorsque la table est vide.)

Voila c'est tout. La fonction insertion présentée est relativement concise et simple. Remarquez en particulier :

2  Statistique des prénoms

2.1  Table des compteurs

Voici le source de notre classe Counter. Le choix de la fonction de hachage est critique pour l'efficacité. Il faut, autant que possible, que des prénoms différents donnent des clés de hachage différentes. (Sinon, on se retrouve avec une recherche linéaire dans une table non-triée).

Notre fonction est simple, elle additionne les codes des lettres des prénoms, en décalant d'un cran vers la gauche à chaque fois. Compte-tenu d'éventuels débordements de capacité, le résultat peut être négatif. On rend donc la valeur absolue de result sans se poser plus de questions.
  private static int hash (String s) {
        int result = 0;

        for (int i = 0 ; i < s.length() ; i++) {
            result *= 2;
            result += s.charAt(i);
        }
        return Math.abs (result);     
    }
Cette fonction hash suffit pour nos quelques prénoms. Si on écrivait une table de hachage pour des données plus générales, il faudrait probablement travailler plus.

Ensuite, la transformation d'une chaîne en un index de table demande de comparer la clé à trouver avec les clés de la table, jusqu'à la trouver elle ou une place libre. C'est le travail de la methode privée getIndex qui est appellée tant par set que par get.
private static int getIndex (String s) {

        int h = hash(s) % size;
        int i = h; // indice de debut de la recherche

        //        System.err.println("Hash for " + s + " is " + h);
        do {
            if (name[i] == null) { // une place libre
                name[i] = s;
                count[i] = 0;
                return i;
            }
            if (s.equals(name[i])) // de'ja` dans la table
                return i;
            i = (i + 1) % size;    // aller voir plus loin
        } while (i != h);
        // un tour complet n'a rien donne'
        throw new Error ("Table de hachage pleine");
    }
Ce code appelle deux commentaires un rouge et un rose:

2.2  Utilisation de la table

Voici le programme solution. Notez qu'il diffère peu du programme précédent. Essentiellement la méthode insertion commence par augmenter de un le compte du prénom inséré. Cette technique fonctionne lors de la toute première insertion d'un prénom, en raison de la spécification de Couter.get (String key) qui dit qu'une association de key à zéro est créée si aucune n'existe déjà.
  static void insertion(String nom) {
    // Ajuster le compteur
    Counter.set(nom,Counter.get(nom)+1);

      ...

  }

  public static void main (String [] args) {
    // Initialiser la table des compteurs
    Counter.init (Eleve.prenom.length);

  }  

3  Belle statistique des prénoms

Voici le programme solution. L'insertion est maintenant réalisée par dichotomie et le tri choisi est quicksort. Voici notre fonction compare :
    static boolean compare (String s1, String s2) {
        int c1 = Counter.get(s1);
        int c2 = Counter.get(s2);

        if (c1 == c2)
            return s1.compareTo(s2) < 0;
        else
            return c1 > c2;
    }
La fonction compare renvoie true si s1 doit être affiché avant s2. C'est à dire si : En termes savants, on parle d'ordre lexicographique sur la paire formée du compte et du prénom.

L'insertion par recherche dichotomique n'est pas évidente à programmer juste. Voici la nôtre :
  static void insertion(String nom) {

 // recherche dichotomique dans la table de 0 a` next-1
        int g=0;
        int d=next;
        int m;
        int r;

        while (d-g > 0) { // d-g est la taille de la table
            m = g+(d-g)/2; // m est dans la table
            r = nom.compareTo(table[m]);
            if (r < 0)
                d = m;
            else if (r > 0)
                g = m+1;
            else
                return; // nom est dans la table
        }
   ....
Par convention, la recherche se fait dans la table, de g à d-1, noté comme la sous-table table[g... d[). De cette façon, d-g est la longeur de la table. La boucle n'est donc exécutée que si la sous-table courante n'est pas vide et m qui est g plus la partie entière de la demi-longuer de la sous-table courante est toujours dans la table.

Correction

On compare le nom recherché nom à table[m] (qui se trouve être au milieu de la sous-table) Il y a trois cas :

Terminaison

La longueur de la table table[g... d[ décroit strictement à chaque tour de boucle.

D'une itération de boucle à l'autre on passe de la sous-table table[g... d[ à l'une ou l'autre des sous-tables table[m+1... d[ ou table[g... m[, avec m = g + (d-g)/2. Remarquons d'abord que m est toujours strictement plus grand que g et strictement plus petit que d, sauf dans le cas d'une sous-table de taille un où m est égal à g. Or, c'est m+1 et non m qui peut être affecté à g pour le tour suivant.

Insertion

La sous-table courante finira donc par être vide si nom n'est pas déjà dans table et on pourra l'insérer. Reste à savoir où.

Il est simple de voir que les deux invariants suivants sont vrais initialement (les sous-tables concernées sont vides) et conservés par la boucle :
  1. Tous les éléments de table[0... g[ sont plus petits que nom.
  2. Tous les éléments de table[d... next[ sont plus grands que nom.
En sortie de boucle on a g == d, on doit donc insérer nom à l'indice g :
      // g est la place de nom dans la table

        for (int i = next ;  i > g ; i--)
            table[i] = table[i-1];
        table[g] = nom;
        next++;
    }

4  Culture

Il y a deux ans, vos prédecesseurs ont planché sur le même problème, mais en utilisant une technique bien différente.

La librairie Java contient une classe Hashtable qui réalise les tables de hachage. Voici une classe Counter qui utilise la librairie Java. Découvrez les joies de la programmation orientée object et en particulier, la classe Object des objets, la classe Integer des entiers-objets et comment on passe de l'un à l'autre (par un cast).
Ce document a été traduit de LATEX par HEVEA.