LES TABLEAUX STATIQUES


tableaux unidimensionnels

generalites

Un tableau permet de regrouper dans une structure plusieurs valeurs scalaires de meme type. Pour permettre une maintenance aisee du programme, la dimension doit etre definie par une constante. C'est cette derniere qui sera utilisee pour les tests de depassement (peu de compilateurs le font automatiquement, ils le feraient par exemple a l'interieur d'une boucle alors que le test sur la valeur finale serait suffisant). La taille reellement utilisee peut etre inferieure ou egale a la dimension du tableau, les composantes au dela de la taille utilisee peuvent etre mises a 0 mais cela n'a aucun interet, sauf si on interdit le 0 dans le tableau (cas des caracteres, en C).

Nous allons travailler dans un premier temps sur des tableaux de flottants. Nous utiliserons des tableaux dont le premier indice est 0 puisque c'est la seule possibilite en C, et la solution optimale dans les autres langages puisque necessitant moins de calculs dans le code compile. Si tous les tableaux utilises ont la meme dimension, on peut definir de maniere globale :

#define composante float
#define dim_tus 100
typedef composante type_tus[dim_tus];
Ces declarations sont necessaires dans les langages effectuant un controle tres strict des types de donnees (comme le Pascal) et refusant de combiner des tableaux de tailles differentes. En C, nous pouvons preparer des fonctions sur tous tableaux de reels, quelle que soit leur dimension :
#define composante float
typedef composante type_tus[]; /* ou typedef composante *type_tus */
C'est cette seconde declaration que nous utiliserons dans les exemples suivants.

Remarque : dans notre cas il peut etre interessant de definir un tableau comme une structure regroupant le tableau, sa dimension et sa taille effective.

fonctions de base (fichier inclus base_tus)

Les fonctions de base sur les tableaux unidimensionnels sont l'initialisation du tableau, l'ajout d'une valeur en fin du tableau, et l'affichage de tout le tableau :
void init_tus(type_tus tab,int *taille,int dim)
 {
  int i;
  for(i=0;i<dim;i++) tab[i]=0;
  *taille=0;
 }
L'initialisation a 0 n'est pas necessaire dans la plupart des cas. La taille est passee par adresse puisque modifiee par la fonction init_tus.
int ajoute_val_tus(type_tus tab,int *taille,int dim,composante val)
/* retourne 0 si tout s'est bien passe, 1 si erreur */
 {
  if (*taille>=dim)
   {
    puts("depassement de capacite du tableau");
    return(1);
   }
  /* le else n'est pas necessaire du fait du return precedent */
  tab[(*taille)++]=val;
  return(0);
 }

void affiche_tus(type_tus tab,int taille)
 {
  int i;
  for(i=0;i<taille;i++) printf("%dieme valeur :
%f\n",i+1,tab[i]);
 }

Les declarations globales et fonctions ci-dessus etant recopiees dans le fichier "base_tus", nous pouvons ecrire le programme suivant (ex_tus) :
#include <stdio.h>
#include "base_tus.inc"
#define dim 100
void main(void)
 {
  int nb;
  composante v,t[dim];
  init_tus(t,&nb,dim);
  do
   {
    printf("entrez la %dieme note (fin si <0 ou >20) :",nb+1);
    scanf("%f",&v);
    if(v<0||v>20) break; /* on aurait pu le mettre 
                                dans la condition du while */
   }
  while (!ajoute_val_tus(t,&nb,dim,v));
  affiche_tus(t,nb);
 } 
Exercice (tusexo_a) : modifier ce programme pour qu'il calcule la moyenne et affiche, pour chaque note, l'ecart avec la moyenne (ce qui necessite l'utilisation d'un tableau car il faut d'abord calculer la moyenne puis utiliser les notes memorisees auparavant).

manipulations dans les tableaux (mani_tus)

L'insertion et la suppression de composantes d'un tableau necessitent des decalages des autres composantes du tableau:
void suppr_tus(type_tus tab,int *taille,int position)
 {
  int i;
  if(position>=*taille||position<0)return;
  (*taille)--;
  for(i=position;i<*taille;i++)tab[i]=tab[i+1];
 }
On peut remarquer que la suppression de la derniere composante n'entraine aucun decalage.
int insert_tus(type_tus tab,int *taille,int dim,int position,composante val)
/* retourne 0 si pas d'erreur */
 {
  int i;
  if(position<0)return(1);
  if(position>*taille)position=*taille;
  if (*taille>=dim)
   {
    puts("depassement de capacite du tableau");
    return(2);
   }
  for(i=*taille;i>position;i--)tab[i]=tab[i-1];
  tab[position]=val;
  (*taille)++;
  return(0);
 }

Le decalage doit se faire par indice decroissant (par indice croissant, on recopierait progressivement la composante a l'indice position dans tout le reste du tableau).

Les rotations sont egalement des manipulations frequentes sur les tableaux :

void rot_gauche_tus(type_tus tab,int taille)
 {
  composante tampon;
  int i;
  tampon=tab[0];
  for(i=1;i<taille;i++)tab[i-1]=tab[i];
  tab[taille-1]=tampon;
 }
void rot_droite_tus(type_tus tab,int taille)
 {
  composante tampon;
  int i;
  tampon=tab[taille-1];
  for(i=taille-1;i>0;i--)tab[i]=tab[i-1];
  tab[0]=tampon;
 }
Exercice (tusexo_b) : faire un programme permettant de tester ces fonctions, a l'aide d'un menu permettant d'essayer dans n'importe quel ordre ces fonctions.

tris

generalites (valables egalement pour d'autres types de donnees)

Les exemples qui suivent traitent des tableaux de flottants, les methodes etant identiques pour tout type de composante a condition d'y definir une relation d'ordre (par exemple, pour des chaines de caracteres on utilisera strcmp au lieu de <, > et =). Mais il ne faut pas oublier que l'efficacite de l'algorithme depend egalement des types de donnees traitees : une comparaison de chaines de caracteres etant relativement longue, les algorithmes effectuant beaucoup de tests seront moins efficaces qu'avec des flottants. De meme, les tableaux trop gros pour entrer en memoire devront etre traites sur support externe, rendant l'acces aux donnees (plusieurs millisecondes) bien plus lent que les tests ou calculs (micro voire nanosecondes). Dans les cas plus complexes, comme par exemple les tableaux de structures, on appelle clef le champ servant pour le tri (par exemple le nom pour un tableau contenant nom, prenom, adresse,...). Dans ce cas, les calculs sur les clefs seront souvent plus rapides que les deplacements des structures entieres. Les methodes de tris presentees ici sont souvent utilisables egalement avec les autres types de donnees, mais les conclusions sur leur efficacite varieront. On qualifiera de stable un tri laissant dans le l'ordre initial les elements de clef identique (par exemple, un stock saisi au fur et a mesure des arrivees de materiel, le classement par ordre alphabetique gardant les materiels de meme nom dans leur ordre d'arrivee sera stable). Tous les algorithmes decrits ici sont stables, a condition d'y prendre garde (scruter le tableau du debut vers la fin et non l'inverse par exemple). Dans ce chapitre, nous noterons N le nombre de composantes du tableau (appele taille auparavant).

le tri bulle

Cet algorithme est relativement connu, bien qu'il soit rarement efficace (en termes de temps de calcul, le tri est neanmoins correct, bien evidement). Il consiste a balayer tout le tableau, en comparant les elements adjacents et les echangeant s'ils ne sont pas dans le bon ordre. Un seul passage ne deplacera un element donne que d'une position, mais en repetant le processus jusqu'a ce plus aucun echange ne soit necessaire, le tableau sera trie. (bull_tus)
void tri_bulle_tus(type_tus tab, int N)
 {
  int ok,i;
  composante tampon;
  do
   {
    ok=1; /* vrai */
    for(i=1;i<N;i++) if(tab[i-1]>tab[i])
     {
      ok=0;
      tampon=tab[i-1];
      tab[i-1]=tab[i];
      tab[i]=tampon;
     }
   }
  while(!ok);
 }
Ce tri va necessiter un grand nombre de deplacements d'elements, il est donc inutilisable dans les cas ou ces deplacements sont couteux en temps. Il va necessiter N-1 boucles principales dans le cas ou le dernier element doit etre place en premier. Le nombre de boucles internes maximal est donc de l'ordre de (N-1)2. Il peut par contre etre interessant quand le tableau initial est deja pre-trie, les elements n'etant pas disposes trop loin de leur position finale (par exemple classement alphabetique ou les elements sont deja tries par leur premiere lettre).

Plutot que de deplacer un element dans une position meilleure que la precedente mais neanmoins mauvaise, les deux algorithmes qui suivent tentent de deplacer les elements directement en bonne position.

le tri par insertion

Plutot que de deplacer les elements d'une position, on peut prendre un element apres l'autre dans l'ordre initial, et le placer correctement dans les elements precedents deja tries, comme on le fait lorsque l'on classe ses cartes a jouer apres la donne (inse_tus) :
void tri_insertion_tus(type_tus tab, int N)
 {
  int pt,ppg; /* position testee, premier plus grand */
  composante tampon;
  for(pt=1;pt<N;pt++)
   {
    ppg=0;
    while(tab[ppg]<=tab[pt]&&ppg<pt)ppg++;
    if(ppg<pt)
     {
      tampon=tab[pt];
      suppr_tus(tab,&N,pt);
      insert_tus(tab,&N,32767,ppg,tampon); /* je suis sur de ne pas
              depasser la dimension puisque je viens de supprimer un
              element */
     }
   }
 }
Le nombre maximal de boucles internes est descendu a N(N-1)/2, on a au maximum N-1 couples de suppression/insertion mais qui eux effectuent en moyenne environ N/2 echanges d'elements, ce qui amene un maximum d'echanges de l'ordre de N2, ce qui n'est pas satisfaisant. On peut neanmoins ameliorer l'implantation de cet algorithme en optimisant la recherche de la position d'un element dans la partie deja triee, par dichotomie par exemple (voir chapitre sur les recherches), ainsi qu'en ameliorant le couple suppression/insertion (ne decaler que les elements entre la position finale et la position initiale, par une rotation a droite). On remplace la boucle principale par :
  for(pt=1;pt<N;pt++)
   {
    dpg=pt-1;
    tampon=tab[pt];
    while(tab[dpg]>tampon&&dpg>=0)
       {tab[dpg+1]=tab[dpg];dpg--;}
    tab[dpg+1]=tampon;
   }

Le tri par insertion peut etre interessant pour des tableaux ayant deja ete tries, mais ou l'on a rajoute quelques nouveaux elements en fin de tableau (dans ce cas il faut ameliorer l'implantation pour decouvrir rapidement le premier element mal place, puis utiliser l'algorithme complet pour les elements restants). Dans les autres cas, il sera plutot reserve aux types de donnees permettant une insertion rapide (listes chainees par exemple).

le tri par selection

Le but est desormais de deplacer chaque element a sa position definitive. On recherche l'element le plus petit. Il faut donc le placer en premier. Or cette position est deja occupee, on se propose donc d'echanger les deux elements. Il ne reste plus qu'a repeter l'operation N fois (sele_tus):
void tri_selection_tus(type_tus tab, int N)
 {
  int pd,pp,i; /* place definitive, plus petit */
  composante tampon;
  for(pd=0;pd<N-1;pd++)
   {
    pp=pd;
    for(i=pp+1;i<N;i++) if(tab[i]<tab[pp])pp=i;
    tampon=tab[pp];
    tab[pp]=tab[pd];
    tab[pd]=tampon;
   }
 }
Chaque echange met un element en position definitive, l'autre par contre est mal place. Mais aucun echange n'est inutile. Un element qui a ete bien place ne sera plus teste par la suite. Le nombre de boucles internes est environ N(N-1)/2, ce qui est meilleur que le tri bulle, mais toujours de l'ordre de N2. Par contre le nombre de deplacements d'elements est au maximum de 2(N-1), la moitie etant des deplacements necessaires, ce qui est faible pour un fichier en desordre total, mais n'est pas optimal pour les fichiers dont la premiere partie est deja classee (et grande par rapport a la taille totale). Une amelioration possible serait d'essayer de placer le second element de l'echange dans une position pas trop mauvaise, a condition que cette recherche ne soit pas elle meme plus gourmande en temps.

le tri shell

C'est une amelioration du tri par insertion : au lieu d'effectuer une rotation de tous les elements entre la position initiale et finale (ou du moins meilleure) d'un element, on peut faire des rotations par pas de P, ce qui rendra le fichier presque trie (chaque element sera a moins de P positions de sa position exacte). On repete ce tri pour P diminuant jusqu'a 1. Une suite possible pour P est de finir par 1, les pas precedents etant de 4, 13, 40, 121, 364, 1093... (Pi=3*Pi-1 +1). D'autres suites sont evidement possibles, a condition de prendre des valeurs qui ne soient pas multiples entre elles (pour ne pas toujours traiter les memes elements et laisser de cote les autres, par exemple les puissances successives de 2 ne traiteraient que les positions paires, sauf au dernier passage. Exemple d'implantation (shel_tus) :
void tri_shell_tus(type_tus tab, int N)
 {
  int pt,dpg,P; /* position testee,dernier plus grand */
  composante tampon;
  for(P=1;P<=N/9;P=3*P+1); /* calcul de P initial (<=N/9) */
  for(;P>0;P/=3) /* inutile de soustraire 1 car division entiere */
   {
    for(pt=P;pt<N;pt++)
     {
      dpg=pt-P;
      tampon=tab[pt];
      while(tab[dpg]>tampon&&dpg>=P-1) 
                {tab[dpg+P]=tab[dpg];dpg-=P;}
      tab[dpg+P]=tampon;
     }
   }
 }
L'interet de ce tri, bien qu'il ait une boucle autour du tri par insertion, est qu'il cree rapidement un fichier presque trie, le dernier tri par insertion sera donc beaucoup plus rapide. Il est en general plus rapide que le tri par insertion pour les fichiers completement melanges, mais pour certains tableaux et pour certaines suites de P, il peut etre bien plus mauvais que les autres tris.

le tri rapide (Quick Sort)

Ce tri est recursif. On cherche a trier une partie du tableau, delimitee par les indices gauche et droite. On choisit une valeur de ce sous-tableau (une valeur mediane serait ideale, mais sa recherche ralentit plus le tri que de prendre aleatoirement une valeur, par exemple la derniere), que l'on appelle pivot. Puis on cherche la position definitive de ce pivot, c'est a dire qu'on effectue des deplacements de valeurs de telle sorte que tous les elements avant le pivot soient plus petits que lui, et que toutes celles apres lui soient superieures, mais sans chercher a les classer pour accelerer le processus. Puis on rappelle recursivement le tri de la partie avant le pivot, et de celle apres le pivot. On arrete la recursivite sur les parties a un seul element, qui est donc necessairement triee. (Quick_tus)
void tri_rapide_tus(type_tus tab,int gauche,int droite)
 {
  int g,d;
  composante tampon,val;
  if(droite<=gauche)return; /* fin de recursivite 
                         si tableau d'une seule case a trier */
  /* choix du pivot : on prend par exemple la valeur de droite */
  val=tab[droite];
  g=gauche-1;
  d=droite;
  do
   {
    while(tab[++g]<val); /* g pointe le premier element
              (a gauche) plus grand (ou egal) que le pivot */
    while(tab[--d]>val); /* d pointe le premier element
              (par la droite) plus petit (ou egal) que le pivot */
    if(g<d) /* si g et d ne se sont pas rencontres, on
              echange les contenus de g et d et on recommence */
        {tampon=tab[g];tab[g]=tab[d];tab[d]=tampon;}
   }
  while(g<d); /* on sort quand g a rencontre d, alors tous les
      elements a gauche de g sont <= au pivot, tous ceux
      a droite de d sont >= */
/* on place le pivot en position g (d serait aussi possible), donc dans sa
   bonne position (tous ceux a gauche sont <=, a droite sont >=) */
  tampon=tab[g];tab[g]=tab[droite];tab[droite]=tampon;
  /* il ne reste plus qu'a trier les deux parties, a droite
     et a gauche du pivot */
  tri_rapide_tus(tab,gauche,g-1);
  tri_rapide_tus(tab,g+1,droite);
 }

On appelle le tri d'un tableau complet par : tri_rapide_tus(tableau, 0, N-1). On peut remarquer que cette implantation du tri n'est pas stable, mais peut l'etre en gerant les egalites avec le pivot, mais en ralentissant le tri. On effectue dans la boucle deux tests (g<d), on peut supprimer le second par une boucle infinie et un break dans le if. Ce tri fait en moyenne 2Nlog(N) boucles, sur des fichiers bien melanges, mais N2 sur un fichier deja trie (et dans ce cas la profondeur de recursivite est N, ce qui est prohibitif). Mais ces boucles sont longues et gourmandes en memoire du fait de la recursivite: chaque appel de fonction est assez long, il faut memoriser l'etat actuel. On peut optimiser le tri en remplacant la recursivite par des boucles (obligatoire si le langage utilise n'est pas recursif), ce qui evite d'empiler des adresses, mais la gestion des variables locales doit etre remplacee par gestion par pile (voir plus loin) pour memoriser les sous-tris en attente, ce qui permettra d'accelerer le tri mais necessite une programmation complexe (rappel : la recursivite sera automatiquement supprimee par le compilateur, cette transformation par le programmateur peut etre plus efficace).

Plutot que d'arreter la recursivite sur des sous-tableaux de taille 1, on peut s'arreter avant (entre 5 et 25 en general) pour eviter une profondeur de recursivite trop importante. Le fichier est alors presque trie, on peut alors effectuer un tri par insertion qui dans ce cas sera tres rapide. Une autre amelioration possible est de mieux choisir le pivot. la solution ideale est de trouver a chaque fois la valeur mediane du sous-tableau a trier, mais sa recherche precise rend le tri plus lent que sans elle. Une solution quelquefois utilisee est de prendre par exemple trois valeurs, pour en prendre la valeur mediane, par exemple tab[droite], tab[gauche] et tab[(droite+gauche)/2] (dans le cas d'un fichier parfaitement melange, le choix de trois positions n'a pas d'importance, mais dans des fichiers presque tries le choix ci-dessus est plus judicieux). La totalite de ces ameliorations peut apporter un gain de l'ordre de 20% par rapport a la version de base.

le tri par creation

Lorsqu'il est necessaire de disposer simultanement du tableau initial et du tableau trie, on peut recopier le tableau initial puis effectuer un tri sur la copie, ou adapter un des algorithmes precedents. Par exemple, a partir du tri par selection, l'algorithme consiste a rechercher l'element le plus petit, le copier en premiere position du tableau final, rechercher le suivant, le placer en seconde position, etc... En cas d'elements identiques, il y a lieu de marquer les elements deja choisis, par exemple a l'aide d'un troisieme tableau d'indicateurs (le tri est alors stable), ou suivant l'exemple (crea_tus) ci-dessous (ceci n'est qu'un exemple, d'autres possibilites existent) :
int le_suivant(type_tus ti, int taille, int precedent)
 {
  int pos,i;
/* 1) recherche du premier egal au precedent */
  pos=precedent+1;
  while(pos<taille&&ti[pos]!=ti[precedent])pos++;
  if(pos<taille)return(pos);
/* 2) sinon, recherche du suivant mais different dans tout le tableau */
  pos=0;
  while(ti[pos]<=ti[precedent])pos++; /* le 1er > precedent */
  for(i=pos+1;i<taille;i++) /*le plus petit dans la suite */
    if(ti[i]>ti[precedent]&&ti[i]<ti[pos])pos=i;
  return(pos);
 }

void tri_creation_tus(type_tus ti, type_tus tf, int taille)
/* ti tableau initial, tf tableau final (trie) */
 {
  int i,imin=0;
  for(i=1;i<taille;i++)if(ti[i]<ti[imin])imin=i;
  tf[0]=ti[imin];
  for(i=1;i<taille;i++)
   {
    imin=le_suivant(ti,taille,imin);
    tf[i]=ti[imin];
   }
 }

On peut remarquer que ce tri minimise le nombre de copies des elements, mais necessite beaucoup de comparaisons de clefs (en particulier un element deja selectionne sera encore compare par la suite). Ceci peut etre acceptable pour un fichier sequentiel a grands champs, sur bande par exemple, mais dont les clefs peuvent etre stockees completement en memoire. On verra d'autres propositions dans le cas des fichiers.

d'autres tris

Suivant les donnees a trier, il peut etre plus efficace de construire un algorithme de tri specifique. Par exemple, si le tableau contient un grand nombre de valeurs similaires (exemple : gestion annuelle d'un stock ou la plupart des articles entrent et sortent plusieurs fois par jour), on peut utiliser l'algorithme simple (par creation) consistant a rechercher l'element le plus petit, compter le nombre de ces elements, les mettre dans le tableau destination, et repeter l'operation jusqu'a la fin du fichier destination. C'est le tri par comptage. Dans le cas ou le nombre de clefs differentes est suffisamment faible, on peut utiliser un tableau de compteurs, ce qui permet d'effectuer le comptage en un seul balayage du fichier.

Dans la cas ou les clefs sont bornees (c'est a dire comprises entre un minimum et un maximum connus a l'avance) et en nombre fini, on peut utiliser le tri basique : par exemple si toutes les clefs sont des entiers entre 000 et 999, on peut separer le tableau en 10 parties en fonction des centaines, puis recursivement traiter les dizaines puis les unites (tri en base 10). Evidement, un tri en base 2 sera plus efficace sur ordinateur : on part a gauche ,on avance jusqu'a trouver un nombre commencant par 1, puis par la droite jusqu'a trouver un nombre commencant par 0, les echanger et continuer jusqu'a croisement des deux cotes. Puis on recommence (recursivement par exemple) sur le bit suivant, jusqu'a tri complet. Pour trier des clefs alphabetiques, on peut effectuer un tri en base 26, sur les N premiers caracteres (N pouvant valoir 2 ou 3 par exemple), le fichier est alors presque trie. Il est alors plus efficace d'effectuer un tri par insertion (passe de finition) plutot que de repeter le tri basique jusqu'a tri complet.

Le tri par fusion utilise un algorithme de fusion de deux tableaux tries en un seul plus grand, appele recursivement sur les deux moities du tableau, jusqu'a une taille de tableau de 1 (ou plus, avec un tri specifique pour petits tableaux, par exemple par echange sur des sous-tableaux de 3 elements)

recherches

On a souvent besoin de rechercher, dans un grand tableau, la position d'un element donne. Un point particulier a ne pas oublier pour tous les algorithmes est le traitement du cas ou l'element cherche n'est pas dans le tableau. Une autre caracteristique importante d'un algorithme de recherche est son comportement desire en cas d'elements identiques (doit-il donner le premier, le dernier, tous ?).

la recherche sequentielle

Il suffit de lire le tableau progressivement du debut vers la fin. Si le tableau n'est pas trie, arriver en fin du tableau signifie que l'element n'existe pas, dans un tableau trie le premier element trouve superieur a l'element recherche permet d'arreter la recherche, de plus cette position correspond a celle ou il faudrait inserer l'element cherche pour garder un tableau trie. Une recherche sur un tableau trie necessitera en moyenne N/2 lectures, mais on se rapprochera de N pour un fichier non trie avec beaucoup de recherches d'elements inexistants.
int rech_sequentielle_tus(type_tus tab, int N, composante val)
/* rend -1 si val non trouvee, premiere occurence trouvee sinon */
 {
  int i;
  for(i=0;i<N;i++)if(tab[i]==val)return(i);
  return(-1);
 }

la dichotomie

Dans le cas d'un tableau trie, on peut limiter le nombre de lectures a log(N)+1, en cherchant a limiter l'espace de recherche. On compare la valeur cherchee a l'element central du tableau, si ce n'est pas la bonne, un test permet de trouver dans quelle moitie du tableau on trouvera la valeur. On continue recursivement jusqu'a un sous-tableau de taille 1. Il vaut bien mieux implanter cet algorithme de maniere iterative, car la fonction se rappelle jusqu'a trouver la position desiree, puis seulement on effectue les depilages, alors que l'on n'a plus besoin des etats intermediaires qui ont ete memorises par la recursivite puisque le probleme est resolu.
int rech_dichotomie_tus(type-tus tab, int N, composante val)
 {
  int g,m,d; /* gauche, milieu, droite */
  g=0;d=N;
  while (g<=d)
   {
    m=(g+d)/2; /* division entiere */
    if(val<tab[m])d=m-1;
    else if(val>tab[m])g=m+1;
    else return(m)
   }
  return(-1);
 }

L'utilisation de la variable m permet d'eviter plusieurs calculs de (g+d)/2. Dans certains cas il peut etre interessant de prevoir egalement une memorisation de tab[m], mais pas pour les tableaux puisqu'ils permettent d'acces direct. L'ordre des tests est important, l'egalite ayant statistiquement moins de chances, elle doit etre traitee en dernier (donc faire le maximum de tests dans le cas le moins probable). Si on avait traite l'egalite en premier, tous les autres cas auraient necessite deux tests, l'egalite puis la selection de la partie droite ou gauche. En cas de multiples elements correspondants a la valeur cherchee, on retourne la position de l'un d'eux, mais pas necessairement le premier ou le dernier. Pour retourner le premier par exemple, il suffit de rajouter une boucle testant l'egalite vers la gauche, a n'effectuer qu'une seule fois bien evidement, lorsque une valeur adequate a ete localisee. On peut ameliorer l'algorithme en comparant val a tab[g] et tab[d], pour estimer la position recherchee plutot que de la supposer au milieu, comme on effectue une recherche dans un dictionnaire : m=g+(int)((val-tab[g])*(d-g)/(tab[d]-tab[g])). Attention, ce calcul se fait sur des composantes (par exemple des flottants), ce qui est toujours plus long que de simples calculs sur des entiers.

calculs mathematiques

Les tableaux unidimensionnels permettent de resoudre simplement divers problemes mathematiques. Nous allons en traiter certains.

calcul vectoriel

L'utilisation des tableaux statiques est bien indiquee pour le calcul vectoriel. En effet, toutes les variables seront de meme dimension. Il est aise de prevoir une bibliotheque de fonctions vectorielles comportant toutes les operations de base (multiplication par un reel, produit scalaire, produit vectoriel, produit mixte, norme...). Un grand nombre de problemes geometriques se resoudront bien plus facilement par calcul vectoriel que de maniere parametrique.

polynomes

Une maniere (mais il y en a d'autres) de representer un polynome est le tableau de ses coefficients. Par exemple f(x)=4x3+x+2 sera represente par le tableau 2,1,0,4 (en mettant le coefficient de xi en position i). L'evaluation d'un polynome (c'est a dire le calcul de sa valeur pour un x donne) ne doit pas utiliser de fonction puissances mais uniquement des multiplications successives, puisque toutes les puissances intermediaires sont necessaires. La somme de polynomes est triviale (somme des coefficients), le produit a peine plus complique (a moins que l'on ait besoin d'une optimisation poussee, dans ce cas on peut reduire d'1/4 le nombre de multiplications mais avec une complexite accrue). La resolution de l'equation f(x)=0 (recherche de racines) peut se faire par dichotomie par exemple (il faut alors donner le premier intervalle de recherche), en cas de racines multiples on en trouve une au hasard. Une recherche de racines plus efficace necessite des algorithmes complexes, rarement universels (c'est a dire que dans certains cas ils ont un resultat deplorable, voire faux ou plantage de l'ordinateur en cas de divergence) qui ne seront pas traites ici, mais une litterature abondante existe sur ce sujet.

L'interpolation polynomiale correspond elle a la recherche d'une courbe polynomiale passant par N+1 points donnes : P(xi)=yi pour i entre 0 et N. La solution la plus simple consiste a choisir le polynome de Lagrange (d'ordre N):

N


N

P(x)=
Sigma
( yj
Pi
(x-xi)/(xj-xi) )

j=0

i=0

i!=j


qui est le plus rapide a determiner mais donne des resultats decevants pour N assez grand (100 par exemple), puisque les seules conditions imposees sont des points de passage, on obtient (souvent pres des points extremes) une courbe assez "folklorique" entre certains points. Une methode souvent plus satisfaisante est l'utilisation des "splines", qui cherche parmi les multiples polynomes d'ordre N passant par N+1 points celui qui minimise une quadratique (en fait, correspond a la minimisation de l'energie de deformation d'une poutre passant par ces points, d'ou le nom de spline, "latte" en anglais) (voir mon support de cours sur l'infographie). Ou alors on peut utiliser une approximation (polynome d'ordre M<N) passant au voisinage des points (par exemple moindres carres).

tableaux multidimensionnels

Un tableau a N dimensions est en fait un tableau unidimensionnel de tableaux de N-1 dimensions. Tout ce qui a ete presente auparavant reste donc valable. La decision d'utiliser des tableaux multidimensionnels doit etre bien reflechie : ces tableaux necessitant beaucoup de memoire, il faut evaluer le ratio de composantes utiles par rapport aux places memoire utilisees. Par exemple un tableau 10x10x10 utilisant dans chacune des 3 directions une seule fois les 10 memoires prevues, les autres fois on s'arrete en moyenne a 7 (si ce n'est pas moins), reserve 1000 memoires, dont seulement 352 utiles (7x7x7+3+3+3).

Outre les tableaux de chaines de caracteres, les tableaux multidimensionnels les plus utilises sont les matrices Les algorithmes de base du calcul matriciel (base_mat) sont la mise a 0, la recopie, l'addition et le produit de matrices, qui sont simples a mettre en place (le produit entraine neanmoins N3 multiplications, des algorithmes plus performants existent mais ne commencent a etre rentables que pour N tres grand, entre 10000 et un million !).

Par contre, le probleme de l'inversion d'une matrice est plus complexe. Elle est utilisee principalement pour la resolution de N equations a N inconnues, representees par une matrice NxN. La methode de Gauss est certainement la plus simple a mettre en oeuvre, bien que pouvant poser probleme dans certains cas particuliers. On utilise en fait la methode de resolution d'un systeme d'equations A.X=B par substitution. Nous allons le preciser sur un exemple :


1

1
-2



x1



2



1
3
-4

*

x2

=

6



-1
-2
6



x3



-1




A




X



B

On utilise le fait que de remplacer une ligne du systeme d'equations par une combinaison lineaire entre elle et d'autres lignes ne modifie pas le resultat, pour eliminer le premier element de la seconde ligne. En soustrayant la premiere ligne a la seconde on obtient :


1

1
-2



x1



2



0
2
-2

*

x2

=

4



-1
-2
6



x3



-1

Puis on elimine les deux premiers elements de la troisieme ligne (on ajoute la premiere puis on ajoute 1/2 fois la seconde) :


1

1
-2



x1



2



0
2
-2

*

x2

=

4



0
0
1



x3



1

On peut desormais resoudre le systeme (en commencant par le bas) : x3=1, donc (seconde ligne) 2x2-2.1=4, donc x2=3, donc (premiere ligne) x1+3-2.1=2 donc x1=1.

void gauss_triangulation_simpliste(type_mat A,type_mat B, int N)
/* A de taille (N,N), B (N,1). On aurait pu gagner de la place en prenant B
   tableau unidimensionnel, ou meme le rajouter en colonne N de A */
 {
  int ce,l,c;
  composante coef;
  for(ce=0;ce<N-1;ce++) /* ce=col a effacer (dans les lignes SUIVANTES) */
    for(l=ce+1;l<N;l++) /* pour chaque ligne au dessous */
     {
      coef=A[l][ce]/A[ce][ce];
      A[l][ce]=0; /* normalement pas besoin de le calculer */
      for(c=ce+1;c<N;c++) A[l][c]-=coef*A[ce][c];
      B[l][0]-=coef*B[ce][0];
     }
 }

Si ces substitutions font apparaitre une ligne de 0, soit le systeme admet une infinite de solutions (0=0, une ligne est une combinaison lineaire des autres), soit aucune (0=N). Mais cette methode n'est pas directement applicable pour des coefficients reels (ou du moins flottants). En effet, si les substitutions precedentes ont amene un coefficient nul, on obtient une division par zero. S'il est proche de 0, l'utilisation de ce coefficient pour en annuler d'autres necessitera un multiplicateur tres grand, ce qui entrainera une multiplication importante de l'erreur inherente a l'utilisation de reels et donc un resultat faux (en fait on se trouvera en presence de coefficients d'ordre de grandeur tres different, et donc les petits deviendront negligeables devant l'erreur sur les tres grands). La solution est de ne pas traiter les lignes dans l'ordre mais pour une colonne donnee, choisir pour celle qui aura son premier terme non nul celle dont ce terme est le plus grand (on l'appelle le pivot). Il suffit alors d'echanger la ligne que l'on veut traiter avec la ligne contenant le pivot (echanger des lignes d'un systeme d'equations ne modifie pas la solution) (gauss):
void gauss_triangulation(type_mat A,type_mat B, int N)
 {
  int ce,l,c,lpivot;
  composante coef;
  for(ce=0;ce<N-1;ce++) /* ce=colonne a effacer */
   {
/*Recherche du pivot le + grand possible (dans les lignes qui restent)*/
    lpivot=ce;
    for(l=ce+1;l<N;l++)if(fabs(A[l][ce])>fabs(A[lpivot][ce]))lpivot=l;
/*Echange de la ligne du pivot et de la ligne ce (ne pas oublier B)*/
    for(c=ce;c<N;c++) /*tous les termes devant ce sont nuls*/
      {coef=A[ce][c];A[ce][c]=A[lpivot][c];A[lpivot][c]=coef;}
    coef=B[ce][0];B[ce][0]=B[lpivot][0];B[lpivot][0]=coef;
/*Suite de l'algorithme, comme le cas simpliste */
    for(l=ce+1;l<N;l++) /* pour chaque ligne au dessous */
     {
      coef=A[l][ce]/A[ce][ce];
      A[l][ce]=0;
      for(c=ce+1;c<N;c++)
        A[l][c]-=coef*A[ce][c];
      B[l][0]-=coef*B[ce][0];
     }
   }
 }
Une fois la matrice A triangulee, il ne reste plus qu'a determiner la matrice colonne solution X:
void gauss_resolution(type_mat A,type_mat B,type_mat X,int N)
 {
  int l,c;
  composante tampon;
  for(l=N-1;l>=0;l--)
   {
    tampon=B[l][0];
    for(c=l+1;c<N;c++)tampon-=A[l][c]*X[c][0];
    X[l][0]=tampon/A[l][l];
   }
 }
Remarque : on pourrait retourner le resultat dans B (si l'utilisateur desire le garder, il lui suffirait de le copier avant). Ceci economise le tableau X et le tampon.

D'autres algorithmes existent, mais ils ne sont plus efficaces que Gauss que pour de tres grosses matrices. Mais souvent les grosses matrices possedent des proprietes particulieres necessitant d'utiliser d'autres structures de donnees que les tableaux a 2 dimensions (en cas de matrices triangulaires, symetriques, bandes, en "ligne de ciel", creuses..., voir paragraphe 11.3.2), pour lesquelles Gauss n'est pas la meilleure solution, car la triangulation supprime ces proprietes.

conclusions

Les tableaux unidimensionnels statiques sont d'une utilisation simple. Ils permettent un acces direct (donc quasi immediat) a une donnee dont on connait la position. Les seules manipulations de base rapides sont l'insertion et la suppression en fin du tableau. La dimension du tableau doit etre connue (ou du moins maximisee) des la phase d'ecriture du programme, ces tableaux sont donc interessants dans le cas de dimensions petites ou presque constantes.