Trier des tableaux

Voici un autre sujet intéressant auquel le programmeur est forcément confronté un jour où l' autre.

Trier des tableaux



Trier des entiers

Avec l' algorithme "qsort"

La librairie C standard offre la fonction qsort() définie dans stdlib.h :

void qsort ( void * base, size_t num, size_t width, int (*fncompare)(const void *, const void *) );
Sort using quicksort algorithm.


Le principe est simple: en lui passant un tableau en paramètre, elle le trie et il n' y a plus qu'a lister le tableau pour voir ce qu'il contient.
Noter que le dernier paramètre de qsort() est un pointeur de fonction: il faut écrire cette fonction et lui passer son nom en paramètre. Ce procédé permet de créer des fonctions pour trier des entiers, des floats, de strings, ...

/* GNU example - Sort integers */
#include <stdio.h>
#include <stdlib.h>

int compare (const void * a, const void * b)
{
  return ( *(int*)a - *(int*)b );
}

int main ()
{
  int n;
  int i[15]={102,89,69,9,56,59,85,44,66,12,36,458,965,254,666};

  qsort (i, 15, sizeof(int), compare);

  for (n=0; n<15; n++)
    printf ("%d ",i[n]);
  puts("");

  return 0;
}



Gareth McCaughan a réécrit la fonction qsort(): source ici mais d'après mes tests sa fonction qsortG() est deux fois plus lente que qsort de la LibC.


Darel L. Finley a implémenté de façon remarquable l' algorithme quickSort:
//  quickSort
//
//  This public-domain C implementation by Darel R. Finley.
//  http://alienryderflex.com/quicksort/
//
//  * Returns true if sort was successful, or false if the nested
//    pivots went too deep, in which case your array will have
//    been re-ordered, but probably not sorted correctly.
//
//  * This function assumes it is called with valid parameters.
//
//  * Example calls:
//    quickSort(&myArray[0],5); // sorts elements 0, 1, 2, 3, and 4
//    quickSort(&myArray[3],5); // sorts elements 3, 4, 5, 6, and 7

#include <stdio.h>
#include <stdlib.h>

char quickSort(int *arr, int elements) {

  #define MAX_LEVELS 1000

  int  piv, beg[MAX_LEVELS], end[MAX_LEVELS], i=0, L, R ;

  beg[0]=0; end[0]=elements;
  while (i>=0) {
    L=beg[i]; R=end[i]-1;
    if (L<R) {
      piv=arr[L]; if (i==MAX_LEVELS-1) return 0;
      while (L<R) {
        while (arr[R]>=piv && L<R) R--; if (L<R) arr[L++]=arr[R];
        while (arr[L]<=piv && L<R) L++; if (L<R) arr[R--]=arr[L]; }
      arr[L]=piv; beg[i+1]=L+1; end[i+1]=end[i]; end[i++]=L; }
    else {
      i--; }}
  return 1; }

int main ()
{
  int n;
  int i[15]={102,89,69,9,56,59,85,44,66,12,36,458,965,254,666};

  n = quickSort(i, 15);

  for (n=0; n<15; n++)
    printf ("%d ",i[n]);
  puts("");

  return 0;
}


Avec l' algorithme "Counting sort"

/*
  Sort integers without qsort,
  using algorithm "counting sort"
  source: http://en.wikipedia.org/wiki/Counting_sort
*/
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

/* end is the last index + 1 */
void csort(int array[], const int end, const int max, const int min)
{
  int i;
  const int range = max-min+1;
  int count[range+1], scratch[end];

  for(i=0; i<range+1; i++)
    count[i] = 0;

  /* Set the value of count[i] to the number of
   * elements in array with value i+min-1. */
  for(i=0; i<end; i++) {
    int c = array[i]+1-min;
    count[c]++;
  }

  /* Update count[i] to be the number of
   * elements with value less than i+min. */
  for(i=1; i<range; i++)
    count[i] += count[i-1];

  /* Copy the elements of array into scratch in
   * stable sorted order. */
  for(i=(end-1); i>=0; i--) {
    int c = array[i]-min;
    int s = count[c];
    scratch[s] = array[i];
    /* Increment count so that the next element
     * with the same value as the current element
     * is placed into its own position in scratch. */
    count[c]++;
  }

  for(i=0; i<end; i++)
    array[i] = scratch[i];
}

int main(int argc, char* argv[])
{
  int n;
  int i[15]={102,89,69,9,56,59,85,44,66,12,36,458,965,254,666};

  csort(i, 15, 965, 9);

  for (n=0; n<15 ; n++)
   printf("%d ", i[n]);
  puts("");

  return 0;
}



Un désavantage pour cette fonction: il faut passer des arguments additionnels comme les valeurs minimales et maximales du tableau (comme si on les connaissait à l' avance)...
Il faut aussi passer le nombre d' éléments du tableau. Aurait pu être évité facilement si la fonction avait inclué un :

  #include <stdio.h>

  int main()
  {
    int i[]={102,89,69,9,56,59,85,44,66};

    int count = sizeof (i) / sizeof (int);
    printf("nombre d' éléments: %d ", count);

    return 0;
  }

Astucieux non? En effet un tableau d'entiers contient n fois la taille d' un int. Donc connaissant sa taille totale et la taille d'un int, obtient le nombre d' éléments. Que les valeurs soient remplies ou pas! Ce calcul prend en compte la taille allouée au tableau sans se préoccupper de ce qu'il contient!


Avec mon algorithme:

Comme vous vous en doutez je vous invite à écrire votre algorithme de triage d' entiers. Voici le mien.

Le tableau de valeurs à trier est parcouru une fois afin de remplir un tableau d'index de valeurs triées. Avant d'insérer un élément, trois cas de figure sont possibles:
  • La valeur de l' index à insérer est supérieure ou égale au dernier élément du tableau de valeurs triées: ce sera le maximum. A ce titre, il est inséré en dernière position.
  • La valeur est inférieure ou égale au premier élément du tableau de valeurs triées: ce sera le minimum. A cet effet, décaler les éléments suivants puis insérer le nouvel élément en première position.
  • La valeur n'est ni le maximum, ni le maximum: parcourir le tableau de valeurs triées jusqu'à ce qu'une valeur rencontrée lui soit supérieure. Décaler vers la droite les éléments en partant de cette n-ième position puis insérer l' élément à la position n.
Voici un exemple concret, pas à pas.

Partant de ce tableau d' entiers contenant les valeurs suivantes,
Tableau à trier:
Index012345
Valeur812610142

Le but consiste à passer les index de ce tableau vers un tableau temporaire qui lui, contiendra les index triés. Au départ, ce tableau est vide.
Tableau temporaire:
012345
index
Valeur

L' algorithme commence par mettre le premier élément dans le tableau temporaire.
Tableau temporaire:
012345
index0
Valeur8

A présent une boucle va traiter le tableau de valeurs à trier, en partant du second élément du tableau puisque le premier est déjà inséré.

La seconde valeur à insérer: index=1 valeur=12.
Comme 12 est supérieur à 8, 12 est inséré en tant que dernier élément du tableau de valeurs triées:
012345
index01
Valeur812

Le troisième élément (index: 2 valeur: 6) a une valeur qui est inférieure à la valeur minimale contenue dans le tableau de valeurs triées: chaque élément est décalé vers la droite ...
012345
index01
Valeur812

012345
index01
Valeur812

... Puis l' élément est inséré:
012345
index201
Valeur6812

Le quatrième élément à traiter (index: 3 valeur: 10) n'est ni le minimum, ni le maximum: il faut parcourir le tableau de valeurs triées jusqu'à ce qu'un élément qui lui est supérieur soit trouvé. En l' occurence c'est 12, qui a l'index 2. Les éléments ayant un index supérieur ou égal à 2 sont décalés vers la droite ...
012345
index201
Valeur6812

Et le nouveau est inséré à la position 2:
012345
index2031
Valeur681012

Cette explication pas à pas a présenté les trois cas de figure. Et ainsi de suite jusqu'au dernier élément du tableau de valeurs à trier.

Voici le code C qui transpose l' algorithme en syntaxe C. Les trois cas de figures sont traités avec un if ... else if ... else bien qu'un switch aurait pu convenir.
De plus, la fonction de tri prend le tableau à trier en paramètre, pour éviter de déclarer le tableau en variable globale. La fonction trie le tableau comme expliqué ci-dessus, puis substitue les valeurs du tableau à trier avec celles du tableau de valeurs triées:

  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>

  void sort_tableau_int(int *t, int COUNT)
  {
    int i, j, k, nb=0; // nombre d' éléments vus
    int tabi[COUNT];
    int tabv[COUNT];

    // Ajoute le premier
    tabi[nb++] = 0;

    for (i=1; i<COUNT; i++)
    {
      if (t[i] >= t[tabi[i-1]]) // Maximum: met en dernier (nb)
        tabi[nb++]= i;
      else if (t[i] <= t[tabi[0]] ) // Minimum: décale et met en premier
      {
        for (j=nb-1; j>-1; j--)
          tabi[j+1] = tabi[j];
        tabi[0] = nb++;
      }
      else // ni min, ni max
      {
        for (j=1; j<nb; j++) //du 1er au nb ième
        {
          if (t[tabi[j]]>=t[i]) //s'il est supérieur,
          {
            for (k=nb-1; k>j-1; k--) //décale
              tabi[k+1]= tabi[k];
            tabi[j] = nb++; //et insère
            break;
          }
        }
      }
    } // for

    // remplit tabv avec les index de tabi et les valeurs de t
    for(i=0; i<COUNT; i++)
      tabv[i] = t[tabi[i]];
    // pour copier tableau tabv vers original
    for(i=0; i<COUNT; i++)
      t[i] = tabv[i];
  }

  int main (void)
  {
    int n;
    int i[15]={102,89,69,9,56,59,85,44,66,12,36,458,965,254,666};

    // trie le tableau ...
    sort_tableau_int(i, 15);

    // ... vérifie son contenu
    for (n=0; n<15; n++)
      printf ("%d ", i[n]);
    printf ("\n");

    return 0;
  }





Mesure de ces cinq algorithmes/fonctions

Les codes ont été mesurés:
  • en interne: seule la fonction de tri a été mesurée
  • sur le même tableau d'entiers
  • selon la stricte même procédure
  • toutes les procédures main() sont identiques

Les temps sont en microsecondes. Chaque source a été testée 20 fois.
Source Temps Moyenne +rapide +lent Sortie
Sur un petit tableau de 10 entiers
pt-bxint.c temps 6.40 6 7 sortie
pt-counting-sort.c temps 27.95 25 31 sortie
pt-qsort.c temps 14.95 14 16 sortie
pt-qsort-Gareth.c temps 45.90 44 48 sortie
pt-quickSort-Finley.c temps 10.95 8 16 sortie
Sur un grand tableau de 1000 entiers
gd-bxint.c temps 6021.15 5917 7377 sortie
gd-counting-sort.c temps 97.30 94 102 sortie
gd-qsort.c temps 762.95 745 807 sortie
gd-qsort-Gareth.c temps 326.40 323 359 sortie
gd-quickSort-Finley.c temps 268.95 261 324 sortie
outils de test

Sur un tableau de dix entiers, bxint.c donne le tempo. qsort() se comporte bien.
Mais sur un grand tableau de mille entiers, Counting Sort brille alors que bxint s'expulse.

C'est la conséquence du logiciel sommaire écrit pour parer à toutes les situations, comme la fonction qsort() de la LibC qui s'en sort moyennement dans toutes les situations, sachant que pour une situation donnée (petit ou grand tableau), certains font mieux.

Comme les programmeurs écrivent du logiciel dédié, qui colle au mieux à son environnement et à une situation donnée, ils préfèrent une fonction plutôt qu'une autre. Imaginez le cas où un programme doit trier dix mille petits tableaux. Employer une fonction générique ralentirait considrablement l'ensemble, dommage si c'est au moment où le robot doit prendre une décision alors qu'il descend un escalier!

Trier des doubles


  /* qsort: trier un tableau de doubles */
  #include <stdio.h>
  #include <stdlib.h>

  int compare_doubles (const void *a, const void *b)
  {
    const double *da = (const double *) a;
    const double *db = (const double *) b;  
    return (*da > *db) - (*da < *db);
  }

  int main ()
  {
    int n, size=5;
    double vals[] = { 40.5, 40.22, 100, 90.455, 20.11 };

    for (n=0; n<size; n++)
      printf ("%.2lf ", vals[n]);
    puts("");

    qsort (vals, size, sizeof (double), compare_doubles);

    for (n=0; n<size; n++)
      printf ("%.2lf ", vals[n]);
    puts("");

    return 0;
}

REFERENCES


(en) Liste d' algorithmes (Wikipedia)
(en) Counting Sort (Wikipedia)
(en) Quick Sort (Darel R. Finley)
(en) GNU qsort()