cours15 - sbalev/processing101 GitHub Wiki

Tableaux

Des tableaux ? Pour quoi faire ?

Prenons le temps de revoir notre exemple de classe Voiture du cours 13. Nous avons développé pas mal d'efforts pour pouvoir dessiner facilement plusieurs voitures ... en l’occurrence trois :

Voiture v1;
Voiture v2;
Voiture v3;

Mais comment pourrions nous aller plus loin, et pourquoi pas, dessiner 100 ou 200 voitures ? En développant vos capacités à copier-coller, vous pourriez obtenir une programme commençant par :

Voiture v1;
Voiture v2;
Voiture v3;
Voiture v4;
Voiture v5;
Voiture v6;
Voiture v7;
Voiture v8;
Voiture v9;
Voiture v10;
Voiture v11;
Voiture v12;
Voiture v13;
Voiture v14;
Voiture v15;
Voiture v16;
Voiture v17;
Voiture v18;
Voiture v19;
Voiture v20;
Voiture v21;
Voiture v22;
Voiture v23;
Voiture v24;
Voiture v25;
Voiture v26;
Voiture v27;
Voiture v28;
Voiture v29;
Voiture v30;
Voiture v31;
Voiture v32;
Voiture v33;
Voiture v34;
Voiture v35;
Voiture v36;
Voiture v37;
Voiture v38;
Voiture v39;
Voiture v40;
Voiture v41;
Voiture v42;
Voiture v43;
Voiture v44;
Voiture v45;
Voiture v46;
Voiture v47;
Voiture v48;
Voiture v49;
Voiture v50;
Voiture v51;
Voiture v52;
Voiture v53;
Voiture v54;
Voiture v55;
Voiture v56;
Voiture v57;
Voiture v58;
Voiture v59;
Voiture v60;
Voiture v61;
Voiture v62;
Voiture v63;
Voiture v64;
Voiture v65;
Voiture v66;
Voiture v67;
Voiture v68;
Voiture v69;
Voiture v70;
Voiture v71;
Voiture v72;
Voiture v73;
Voiture v74;
Voiture v75;
Voiture v76;
Voiture v77;
Voiture v78;
Voiture v79;
Voiture v80;
Voiture v81;
Voiture v82;
Voiture v83;
Voiture v84;
Voiture v85;
Voiture v86;
Voiture v87;
Voiture v88;
Voiture v89;
Voiture v90;
Voiture v91;
Voiture v92;
Voiture v93;
Voiture v94;
Voiture v95;
Voiture v96;
Voiture v97;
Voiture v98;
Voiture v99;
Voiture v100;

Si vous ne manquez pas de courage, essayez de compléter le programme pour dessiner les 100 voitures. Bonne chance ! Mais n'attendez aucune aide ne notre part pour vous aider à travailler de cette façon.

En revanche, un tableau nous aidera à remplacer ces cent lignes de code par une seule. Au lieu d'avoir 100 variables, un tableau est une variable contenant une liste de variables.

À chaque fois qu'un programme nécessite de nombreuses instances d'un type de donnée il faut se demander si un tableau ne serait pas utile. Par exemple, un tableau pourrait être utilisé pour stocker les scores de 6 joueurs dans un jeu, une sélection de 10 couleurs dans un programme de dessin, ou encore une liste de poissons dans une simulation d'aquarium.

P.S : Le copier-coller c'est bien mais comment on change le numéro de chaque voiture ? L'horrible exemple ci-dessus a été obtenu à partir du croquis suivant :

PrintWriter fichier = createWriter("horribleExemple.txt");

for(int i = 1; i <= 100; i++) {
  fichier.println("Voiture v"+i+";");
}

fichier.flush();
fichier.close();
exit();

Une fois le programme exécuté allez dans le menu Croquis → Afficher le dossier ou faites Ctrl-K ou Cmd-K. Mais bien-sûr, vous n'aurez jamais besoin de ce programme !

Qu'est-ce qu'un tableau ?

Depuis le cours 4, vous savez qu'une variable est un nom qu'on donne à une zone mémoire (une adresse), où les données sont stockées. En d'autres termes, les variables permettent à un programme de conserver de l'information sur une période de temps donnée. Un tableau est exactement similaire, mais au lieu de pointer sur une donnée, le tableau pointe sur plusieurs :

Variable vs tableau

Vous pouvez voir un tableau comme une liste de variables. Une liste est utile pour deux raisons importantes. D'abord elle conserve les éléments qu'elle contient. Mais aussi, elle se souvient de l'ordre de ces éléments. C'est un point crucial car dans de nombreux programmes cette information est tout aussi importante que les données elles-mêmes.

Dans un tableau, chaque élément de la liste possède un indice unique. Il s'agit d'un nombre entier qui désigne la position d'un élément dans la liste. Dans tous les cas, le nom du tableau désigne le tableau en entier, alors que chaque élément est identifié et accédé par sa position, donc son indice.

Les indices en Processing, et nous le verrons, en Java, commencent à zéro. Donc pour un tableau de 10 éléments les indices vont de 0 à 9 :

Indices

Bien qu'à première vue il puisse sembler plus intuitif de commencer à 1 (et certains langages de programmations font cela), nous commençons à zéro car techniquement le premier élément du tableau est à une distance nulle du début de ce tableau. Commencer à zéro rend aussi les opérations sur les tableaux (le fait d'exécuter une ou plusieurs lignes de code pour chaque élément du tableau) bien plus faciles à réaliser.


Exercice 15.1. Si vous avez un tableau de 1000 éléments, quel est l'intervalle valide d'indices pour ce tableau ? De .... à .... ?


Donald Knuth

Source xkcd

Déclarer et créer un tableau

Dans le cours 4 nous avons aussi appris que toute variable doit avoir un nom et un type. Les tableaux n'échappent pas à la règle. Cependant, la déclaration d'un tableau est légèrement différente. On dénote l'utilisation d'un tableau en plaçant des crochets ouvrant-fermant vides ([]) après le type de la variable tableau. Commençons avec la déclaration d'un tableau d'entiers :

int[] tableauDEntiers;

Le type de chaque case du tableau sera int. On sait que la variable tableauDEntiers identifie un tableau car on trouve [] après le type. Comme pour les autres variables le nom est totalement arbitraire.

L'une des propriétés fondamentales des tableaux est que ces derniers ont une taille fixe. Une fois la taille déterminée, le tableau ne pourra être redimensionné. Cependant, comme vous l'avez certainement constaté, pour l'instant nous n'avons pas indiqué la taille du tableau tableauDEntiers. En effet, pour l'instant le code donné déclare uniquement l'existence d'une variable identifiant un tableau, mais le tableau n'est pas encore créé. C'est lors de la création que la taille est indiquée.

Pour ce faire, nous utiliserons l'opérateur new que nous avons déjà rencontré pour créer des objets. Pour les classes, nous créions une nouvelle voiture, ou un nouveau Chip avec new. Ici nous créerons un nouveau tableau de taille donnée :

int[] tableauDEntiers = new int[42];

La déclaration ci-dessus spécifie la taille du tableau, ici 42 entiers (plus techniquement, cela indique à la machine combien de mémoire elle doit allouer). La taille d'un tableau est forcément un entier (il ne peut pas y avoir de demi-case pour un demi-élément ! Même si les données qu'on y stocke sont très petites ! N'insistez-pas). La taille peut être donnée par une constante littérale comme ci-dessus, mais toute expression s'évaluant à un entier peut convenir (2+2, x+1, etc.).


Exemple 15.1. Exemples de déclarations de tableaux.

float[] scores = new float[4];          // Une liste de 4 nombres réels.
Humain[] gens = new Humain[100];        // Une liste de 100 personnes.
int n = 50;
Voiture[] voitures = new Voiture[n];    // Utilisation d'une variable pour définir la taille.
Vaisseau[] vaisseaux = new Vaisseau[n*2 + 3];   // Une expression pour définir la taille.

Exercice 15.2. Écrivez une déclaration et création pour les tableaux suivants :

  1. 30 entiers,
  2. 100 nombres réels,
  3. 56 objets Chip,

Exercice 15.3. Quelles sont les déclarations et créations valides ? Pour les versions invalides, précisez pourquoi.

  1. int[] nombres = new int[10];
  2. float[] nombres = new float[5 + 6];
  3. int n = 5; float[] nombres = new int[n];
  4. float n = 5.2; Voiture[] voitures = new Voiture[n];
  5. int n = (5 * 6)/ 2; float[] nombres = new float[n + 5];
  6. int n = 5; Chip[] chips = new Chip[n * 10];

Nous avons appris à déclarer puis à créer un tableau. Mais quel est le contenu de ce dernier ?

Initialiser un tableau

Une première façon de remplir un tableau est de "coder en dur" les valeurs pour chaque case du tableau :


Exemple 15.2. Initialiser les éléments du tableau un par un.

int[] trucs = new int[3];
trucs[0] = 8;   // Le premier élément contient la valeur 8,
trucs[1] = 3;   // Le second la valeur 3,
trucs[2] = 1;   // Le dernier la valeur 1.

Comme vous pouvez le constater, on accède à un élément individuel du tableau en spécifiant son indice, en commençant à zéro. La syntaxe pour cela est constituée du nom du tableau, suivi de l'indice entre crochets.

Une autre option consiste à entrer la liste des valeurs lors de la création :


Exemple 15.3. Initialiser un tableau avec une liste de valeurs.

int[] trucs = {1, 5, 8, 9, 4, 5};   // Créé un tableau de 6 entiers, avec leurs valeurs.
float[] machins = {1.2, 3.5, 2.0, 3.4123, 9.9}; // Créé un tableau de 5 réels avec leurs valeurs.

Notez que dans ce dernier cas on n'utilise pas l'opérateur new.

Mais ces deux options sont finalement très peu utilisées, et nous n'en verrons que très rarement des exemples par la suite. En fait, aucune des deux façons de faire ne résout le problème posé au début. Imaginez initialiser chaque élément d'un tableau de 100 ou 1000, ou... 1000000 éléments !

La solution à nos soucis consiste à itérer sur les éléments du tableau. Ding, dong ! Une alarme vient de se déclencher dans votre esprit (depuis le début du cours, on l'espère).

Opérations sur les tableaux

Considérez le problème suivant :

  • (A) Créez un tableau de 1000 nombres réels.
  • (B) Initialisez chacun des éléments par une valeur réelle aléatoire entre 0 et 10.

Pour la partie A, nous savons déjà faire :

float[] valeurs = new float[1000];

Mais ce que nous voulons éviter pour la partie B, c'est :

valeurs[0] = random(0, 10);
valeurs[1] = random(0, 10);
valeurs[2] = random(0, 10);
valeurs[3] = random(0, 10);
... etc. etc.

Décrivons en français ce que nous voudrions réaliser :

Pour chaque nombre n de 0 à 99, initialiser le n-ième élément stocké dans le tableau par une valeur aléatoire entre 0 et 10. Traduit en code, nous aurions :

int n = 0;
valeurs[n] = random(0, 10);
n = n + 1;
valeurs[n] = random(0, 10);
n = n + 1;
valeurs[n] = random(0, 10);
n = n + 1
valeurs[n] = random(0, 10);
... etc. etc.

Nous n'avons pas réglé le problème, mais en utilisant une variable, nous avons préparé le terrain pour l'utilisation d'une boucle while :

int n = 0;
while(n < 1000) {
  valeurs[n] = random(0, 10);
  n = n + 1;
}

Et une boucle for nous permettrait d'être encore plus concis :


Exemple 15.4. Initialiser un tableau avec une boucle for.

for (int n = 0; n < 1000; n++) {
  valeurs[n] = random(0, 10);
}

Ce qui prenait 1000 lignes de code nous en prend désormais trois !

Nous pouvons utiliser la même technique pour tout type d'opération sur les tableaux. Par exemple nous pourrions prendre un tableau et doubler la valeur de chacun des éléments (par convention, nous utiliserons une variable i, pour itération, plutôt que n, qui désigne plus souvent un nombre d'éléments) :


Exemple 15.5. Une opération sur un tableau.

for (int i = 0; i < 1000; i++) {
  valeurs[i] = valeurs[i] * 2;
}

Mais il reste un problème avec l'exemple précédent. La valeur « codée en dur » 1000. Vous commencez à vous en douter, « codé en dur » est une quasi insulte dans le monde de la programmation. Comme nous ne voulons pas être tournés en ridicule, et comme nous désirons être de meilleurs programmeurs, nous devons toujours nous questionner sur l'existence de valeurs littéralement écrites dans le programme. Dans notre cas, que se passerait-il si nous avions voulu traiter un tableau de 2000 éléments ? Si nous avons un programme très long avec de nombreuses opérations sur les tableaux, il nous faudra changer chaque occurrence de la constante littérale 1000 dans notre programme. C'est une porte grande ouverte pour de jolis bugs.

Fort heureusement, Processing nous fournit un moyen élégant de connaître la taille d'un tableau dynamiquement, en utilisant la notation pointée que nous avons déjà rencontré au cours 13. L'attribut length est défini pour chaque tableau et nous pouvons y accéder en écrivant le nom du tableau, un point, et length :


Exemple 15.6. Une opération sur un tableau en utilisant length.

for (int i = 0; i < valeurs.length; i++) {
  valeurs[i] = 0;
}

Ici nous remettons le tableau à zéro en affectant à chaque case la valeur 0.


Exercice 15.4. Nous considérons un tableau de 10 entiers :

int[] nombres = {5, 4, 2, 7, 6, 8, 5, 2, 8, 14};

Écrivez le code réalisant les opérations suivantes (notez que le nombre d'indices varie, ce n'est pas parce que [___] n'est pas explicitement indiqué qu'il ne faut pas utiliser de crochets).

  • Mettez au carré chaque nombre.

    for (int i ___; i < ___; i++) {
      ___[i] = ___ * ___;
    }
  • Ajoutez un nombre aléatoire entre 0 et 10 à chaque nombre.

    __________{
       ___ += int(___);
       ___
     }
  • Ajoutez à chaque nombre le nombre qui le suit directement dans le tableau. Sautez la dernière case !

    for (int i = 0; i < ___; i++) {
        ___ += ___ [___];
    }
  • Calculez la somme de tous les nombres du tableau.

    ___ ___ = ___;
    for (int i = 0; i < nombres.length; i++) {
        ___ += ___;
    }

Un exemple simple : snake

Une tâche qui peut sembler triviale, programmer une traînée qui suit la souris, n'est pas si facile qu'il y parait. La solution requiert un tableau qui permettra de sauvegarder l'historique des positions de la souris. En fait, nous allons utiliser deux tableaux, l'un pour les positions en X l'autre pour celles en Y. Arbitrairement nous allons choisir de stocker les 50 dernières positions.

Pour commencer nous allons déclarer deux tableaux :

int[] xpos = new int[50];
int[] ypos = new int[50];

Ensuite, dans setup(), nous devons initialiser les deux tableaux. Comme au début du programme la souris n'aura pas encore été déplacée, nous allons les remplir de zéros :

for (int i = 0; i < xpos.length; i++) {
  xpos[i] = 0;
  ypos[i] = 0;
}

À chaque passage dans draw() nous devons mettre à jour le tableau avec la position actuelle de la souris. Choisissons de placer la position la plus récente à la fin du tableau. La taille du tableau étant 50, les indices vont de 0 à 49. Donc la position la plus récente de la souris sera aux indices 49 des deux tableaux, soit xpos.length - 1 :

xpos[xpos.length - 1] = mouseX;
ypos[ypos.length - 1] = mouseY;

Maintenant vient la partie difficile. Nous souhaitons conserver les 50 dernières positions. Si nous stockons la position courante à la fin nous écrasons l'avant-dernière position. Nous souhaitons donc décaler tous les éléments du tableau. Si la souris était à (10,10) à l'image précédente et est maintenant à (15,15), nous souhaitons que (10, 10) soit désormais stocké dans l'avant-dernière case des tableaux. La solution est de décaler tous les éléments d'un indice avant de stocker la position de la souris à la fin.

Décalage

L'élément 49 bouge à la position 48, le 48 à la position 47, etc. Nous pouvons réaliser ceci en itérant sur le tableau et en copiant dans la case actuelle, la valeur de la case suivante. Attention cependant, nous devons nous arrêter à l'avant-dernière case, puisque pour la case 49, il n'y a pas de case 50 à copier. Voici le code pour réaliser ceci :

for (int i = 0; i < xpos.length - 1; i++) {
  xpos[i] = xpos[i + 1];
  ypos[i] = ypos[i + 1];
}

Finalement, la partie la plus facile, nous pouvons utiliser cette historique des positions de la souris pour dessiner une série de cercles. Pour tous les éléments des tableaux xpos et ypos, nous dessinons un cercle :

for (int i = 0; i < xpos.length; i++) {
  noStroke();
  fill(255);
  ellipse(xpos[i], ypos[i], 32, 32);
}

Enfin, pour rendre les choses plus jolies, nous pourrions lier la taille et la couleur des ellipses à leur ancienneté, c'est-à-dire à leur position et donc indice dans les tableaux. Plus les indices sont proches de zéro plus les éléments sont vieux :

for (int i = 0; i < xpos.length; i++) {
  noStroke();
  fill(255 - i * 5);
  ellipse(xpos[i], ypos[i], i, i);
}

Et voici le code complet.


Exemple 15.6. Snake

int[] xpos = new int[50];
int[] ypos = new int[50];

void setup() {
  size(800, 800);

 // Initialiser les positions à 0
 for (int i = 0; i < xpos.length; i++) {
   xpos[i] = 0;
   ypos[i] = 0;
 }
}

void draw() {
  background(255);

  // Décaler les valeurs à gauche
  for (int i = 0; i < xpos.length - 1; i++) {
    xpos[i] = xpos[i + 1];
    ypos[i] = ypos[i + 1];
  }

  // La nouvelle position vient à la fin du tableau
  xpos[xpos.length - 1] = mouseX;
  ypos[ypos.length - 1] = mouseY;

  // Dessiner tout
  noStroke();
  for (int i = 0; i < xpos.length; i++) {
    fill(255 - i * 5);
    ellipse(xpos[i], ypos[i], i, i);
  }
}

Nous referons plus tard le snake, mais sans tableaux !


Exercice 15.5. Créez un tableau et initialisez-le avec les 20 premiers éléments de la suite de Fibonacci. Les deux premiers éléments seront 0 et 1. Chaque élément à partir du troisième est la somme des deux précédents.

Facultatif (pour les plus courageux) : Essayez de dessiner la spirale de Fibonacci.

Spirale de Fibonacci


Exercice 15.6. On veut vérifier si notre « dé électronique », le générateur de nombres aléatoires random(), n'est pas pipé, c'est-à-dire si la distribution des nombres générés est uniforme. Le sketch suivant génère des nombres aléatoires entre 0 et 99 et compte le nombre d’occurrences de chaque nombre.

int[] nbOcc = new int[100];

void setup() {
  size(800, 400);

  // On initialise tout à zéro
  for (int i = 0; i < nbOcc.length; i++) {
    nbOcc[i] = 0;
  }
}

void draw() {
  // On génère un nombre aléatoire
  int r = int(random(nbOcc.length));

  // On met à jour le nombre de ses occurrences
  nbOcc[r]++;

  // TODO : Tracer l'histogramme
}

Complétez le sketch pour tracer le nombre d'occurrences sous forme d'histogramme. La hauteur de chaque barre est égale au nombre d'occurrences du nombre correspondant.

Histogramme

Exercice 15.7. DataViz 101

On peut initialiser un tableau en lisant les valeurs à partir d'un fichier. Le fichier notes.txt contient vos notes de l'examen de Processing. Créez un nouveau sketch et à l'intérieur de son répertoire, créez un sous-répertoire data. Téléchargez ce fichier dedans. On peut lire le contenu de ce fichier ainsi :

String[] sNotes = loadStrings("notes.txt");

Maintenant le tableau sNotes contient autant d'éléments qu'il y a des lignes dans notre fichier. Chaque ligne est une chaîne de caractères (String). On veut traiter les notes comme nombres et c'est pourquoi nous allons convertir ce tableau en tableau d'entiers :

int[] notes = int(sNotes);
  1. Calculez et affichez la moyenne des notes.

  2. On veut visualiser la distribution des notes. Commencez par créer un tableau distrib de 21 éléments (de 0 à 20). Mettez dedans le nombre d'étudiants qui ont obtenu la note correspondante (distrib[j] = nombre d'étudiants qui ont obtenu la note j).

  3. Calculez la valeur maximale du tableau distrib et placez-la dans une variable eMax (nombre maximal d'étudiants qui ont obtenu la même note).

  4. Tracez l'histogramme de la distribution. Utilisez la fonction map() pour établir une correspondance entre nombre d'étudiants [0, eMax] et coordonnées y [height, 0].

  5. Ajoutez des étiquettes sur les axes, lignes horizontales pour plus de lisibilité et une ligne verticale rouge correspondant à la note moyenne.

    Histogramme

  6. Parfois l'interactivité permet de rendre les données plus lisibles. Ajoutez des éléments d'interactivité, par exemple : lorsque la souris survole une barre, la barre change de couleur en on affiche le pourcentage d'étudiants ayant obtenu la note correspondante.


⚠️ **GitHub.com Fallback** ⚠️