Aller au contenu principal

Pensée Algorithmique Abstraite : Choisir le Bon Algorithme

Introduction

La pensée algorithmique abstraite consiste à comprendre pourquoi choisir un algorithme plutôt qu'un autre, au-delà de simplement savoir comment ils fonctionnent.

C'est la différence entre:

  • MAUVAIS "Je connais le tri par insertion"
  • BON «Je sais quand utiliser le tri par insertion vs tri par sélection vs heapsort»

1. Les Questions Fondamentales

Avant de choisir un algorithme de tri, posez-vous:

A. Quelle est la taille des données?

TailleAlgorithme RecommandéRaison
< 10 élémentsTri par insertionSimple, peu d'overhead
10-1000Tri par insertion ou HeapsortBon équilibre
> 1000HeapsortO(n log n) nécessaire
Très grand (ne tient pas en mémoire)Tri externeUtilise le disque

B. Les données sont-elles déjà partiellement triées?

ÉtatAlgorithmeRaison
Presque triéTri par insertionO(n) dans le meilleur cas
Complètement aléatoireHeapsortO(n log n) garanti
Déjà triéTri à bulles ou Tri par insertionO(n) dans le meilleur cas

C. Ai-je des contraintes mémoire?

ContrainteAlgorithmeRaison
Mémoire limitée ou nonHeapsort, Tri par insertion, Tri à bulles, Tri par sélectionTous in-place — O(1) extra

D. Ai-je besoin de stabilité?

Stabilité: Préserve l'ordre relatif des éléments égaux.

// Données: [(Alice, 25), (Bob, 30), (Charlie, 25)]
// Tri par âge

// Tri stable:
// → [(Alice, 25), (Charlie, 25), (Bob, 30)]
// Alice reste avant Charlie (ordre préservé)

// Tri instable:
// → [(Charlie, 25), (Alice, 25), (Bob, 30)]
// Charlie peut passer avant Alice
BesoinAlgorithme
StableTri par insertion, Tri à bulles
Instable (pas grave)Heapsort, Tri par sélection

2. Matrice de Décision : Choisir son Algorithme de Tri

AlgorithmeMeilleur CasMoyenPireMémoireStableQuand l'Utiliser?
Tri à bullesO(n)O(n²)O(n²)O(1)OUIPédagogie, très petites listes déjà presque triées
Tri par sélectionO(n²)O(n²)O(n²)O(1)NONÉchanges coûteux, très petites listes
Tri par insertionO(n)O(n²)O(n²)O(1)OUIPetites listes, données presque triées
HeapsortO(n log n)O(n log n)O(n log n)O(1)NONGrandes listes, mémoire limitée, pire cas garanti

Pire cas garanti (Heapsort) : contrairement au tri par insertion ou au tri à bulles qui peuvent dégénérer à O(n²) selon l'ordre des données (ex. données inversées), le Heapsort est toujours O(n log n), peu importe si les données sont triées, inversées ou aléatoires. Son meilleur cas = son cas moyen = son pire cas. C'est utile dans les systèmes critiques où une dégradation soudaine des performances serait inacceptable.

Ce que Java fait en pratique (Arrays.sort)

Java ne choisit pas un seul algorithme fixe — il adapte sa stratégie selon la taille du tableau :

Taille du tableauAlgorithme utilisé par JavaRaison
≤ 1 élémentAucun tri (retour immédiat)Déjà trié par définition
2 à 47 élémentsTri par insertionOverhead faible, excellent sur petites listes
48 éléments et plusDual-Pivot QuicksortO(n log n) moyen, très rapide en pratique

Note : Pour les tableaux d'objets (ex. Integer[], String[]), Java utilise TimSort (hybride tri par insertion + tri fusion), qui est stable et très efficace sur les données partiellement triées.

Cette approche hybride illustre exactement la pensée algorithmique abstraite : il n'existe pas un seul meilleur algorithme, mais le meilleur algorithme selon le contexte.

3. Pensée Abstraite : Au-Delà de la Complexité

A. Localité en Cache

Concept: Les algorithmes qui accèdent à des données proches en mémoire sont plus rapides.

// Mauvaise localité (sauts aléatoires)
for (int i = 0; i < n; i++) {
access(array[random()]); // Cache miss!
}

// Bonne localité (accès séquentiel)
for (int i = 0; i < n; i++) {
access(array[i]); // Cache hit!
}

Impact:

  • Tri à bulles, Tri par insertion : Excellente localité (accès séquentiels)
  • Tri par sélection : Bonne localité
  • Heapsort : Mauvaise localité (sauts aléatoires dans le tas)

Conclusion: Même complexité O(n log n), le Heapsort est souvent plus lent que prévu à cause des défauts de cache.

B. Nombre de Comparaisons vs Nombre d'Échanges

AlgorithmeComparaisonsÉchanges
Tri par sélectionBeaucoup (O(n²))Peu (O(n))
Tri par insertionPeu (si presque trié)Beaucoup
Tri à bullesBeaucoup (O(n²))Beaucoup
HeapsortO(n log n)O(n log n)

Conséquence:

  • Si les comparaisons sont coûteuses (ex. comparer de gros objets ou des chaînes longues) → Tri par sélection : il fait beaucoup de comparaisons, mais seulement O(n) échanges (un seul échange par passe — il cherche le minimum sur toute la portion restante, puis l'échange une fois).
  • Si les échanges sont coûteux (ex. déplacer de très gros objets en mémoire) → là aussi, Tri par sélection : c'est l'algorithme qui minimise le nombre d'échanges parmi ceux vus dans ce cours.

C. Parallélisation

Les algorithmes de tri non récursifs se parallélisent généralement peu ou pas :

AlgorithmeParallélisable?Raison
Tri à bullesPartielPasses indépendantes sur sous-tableaux
Tri par sélectionNonSéquentiel par nature
Tri par insertionNonSéquentiel par nature
HeapsortPartielConstruction du tas partiellement parallélisable

Note : Pour des besoins de parallélisation intensive, les APIs Java (Arrays.parallelSort()) sont préférables.

Ce que Java fait en pratique (Arrays.parallelSort)

Arrays.parallelSort() exploite le framework Fork/Join de Java pour tirer parti des processeurs multi-cœurs :

ÉtapeCe qui se passe
DivisionLe tableau est découpé en sous-tableaux d'environ 8 192 éléments (seuil interne)
Tri localChaque sous-tableau est trié en parallèle avec Arrays.sort() (Dual-Pivot Quicksort ou TimSort selon le type)
FusionLes sous-tableaux triés sont fusionnés en parallèle (tri fusion) jusqu'à obtenir le tableau complet
// Tri séquentiel — utilise un seul cœur
Arrays.sort(tableau);

// Tri parallèle — utilise tous les cœurs disponibles via Fork/Join
Arrays.parallelSort(tableau);
AspectArrays.sortArrays.parallelSort
AlgorithmeDual-Pivot Quicksort / TimSortFork/Join + Dual-Pivot Quicksort / TimSort + fusion parallèle
Complexité tempsO(n log n)O(n log n) — mais divisé par le nb de cœurs
Complexité mémoireO(log n) à O(n)O(n) — copies pour la fusion parallèle
IntérêtPetits tableaux ou machine mono-cœurGrands tableaux (> ~100 000 éléments) sur machine multi-cœur

À retenir : parallelSort est plus rapide sur de très grands tableaux, mais consomme plus de mémoire (O(n)) à cause des copies nécessaires à la fusion. Pour des petits tableaux, sort reste préférable car le coût de coordination des threads dépasse le gain.

4. Cas Pratiques : Quelle Décision Prendre?

Cas 1: Trier une Petite Liste d'Utilisateurs

Contexte:

  • 50 utilisateurs
  • Tri par nom
  • Appelé une fois au démarrage

Réflexion:

  • Taille petite → Complexité pas critique
  • Appelé rarement → Performance pas critique
  • Simplicité importante

Choix: Collections.sort() (tri intégré Java, optimal pour les petites listes)

users.sort(Comparator.comparing(User::getName));

Pourquoi pas autre chose?

  • MAUVAIS Heapsort manuel : Over-engineering pour 50 éléments
  • MAUVAIS Tri à bulles manuel : Moins performant que le tri intégré

Cas 2: Trier 1 Million de Nombres

Contexte:

  • 1 000 000 d'entiers
  • Données aléatoires
  • Appelé fréquemment
  • Mémoire disponible

Réflexion:

  • Grande taille → O(n log n) nécessaire
  • Données aléatoires → Heapsort garanti O(n log n)
  • Performance critique → Utiliser algo optimisé

Choix: Heapsort (O(n log n) garanti, in-place)

heapSort(numbers); // O(n log n), O(1) mémoire
// Ou le tri intégré Java :
Arrays.sort(numbers);

Pourquoi pas autre chose?

  • MAUVAIS Tri par insertion : O(n²) trop lent
  • MAUVAIS Tri à bulles : O(n²) inexploitable pour un million d'éléments

Cas 3: Trier des Événements par Date

Contexte:

  • Liste d'événements déjà chronologiques
  • Nouvelle insertion → besoin de re-trier
  • Besoin de stabilité (même date = même ordre)

Réflexion:

  • Presque trié → Insertion Sort O(n)
  • Stabilité requise → Tri par insertion
  • Petite modification → Pas besoin de tout re-trier

Choix: Insertion manuelle

public void insertEvent(Event newEvent) {
int i = events.size() - 1;
events.add(newEvent);

// Insertion sort sur le dernier élément
while (i >= 0 && events.get(i).getDate().after(newEvent.getDate())) {
events.set(i + 1, events.get(i));
i--;
}
events.set(i + 1, newEvent);
}

Pourquoi pas autre chose?

  • MAUVAIS Re-trier toute la liste : Gaspillage (O(n log n))
  • BON Insertion : O(n) suffisant

Cas 4: Mémoire Très Limitée (Embarqué)

Contexte:

  • Système embarqué (Arduino, etc.)
  • 10 Ko de RAM
  • 500 éléments à trier

Réflexion:

  • Mémoire critique → In-place requis
  • Pire cas garanti → Heapsort (O(n log n) toujours)

Choix: Heapsort

// In-place, O(n log n) garanti, O(1) mémoire
heapSort(array);

Pourquoi pas autre chose?

  • MAUVAIS Tri par insertion : O(n²), trop lent pour 500 éléments si performance critique
  • MAUVAIS Tri à bulles : O(n²), plus lent que Heapsort

5. Méthodologie : Comment Penser Comme un Algorithmicien

Étape 1: Caractériser le Problème

Posez les bonnes questions:

  1. Quelle est la taille des données? (n = ?)
  2. Quelle est la distribution? (aléatoire, presque trié, etc.)
  3. Quelle est la contrainte principale? (temps, mémoire, stabilité)
  4. À quelle fréquence le tri est-il appelé?
  5. Les données tiennent-elles en mémoire?

Étape 2: Éliminer les Mauvais Choix

Exemple: Trier 10 000 éléments

  • MAUVAIS O(n²) : Trop lent → Éliminer Tri à bulles, Tri par insertion, Tri par sélection
  • BON Reste : Heapsort (O(n log n) garanti, in-place)

Étape 3: Comparer les Finalistes

CritèreTri par insertionHeapsortTri à bulles
Temps moyenO(n²)O(n log n)O(n²)
MémoireO(1)O(1)O(1)
StabilitéOuiNonOui
Meilleur casO(n) si presque triéO(n log n)O(n) si presque trié
ImplémentationSimpleMoyenneSimple

Étape 4: Décider

Si: Données petites ou presque triées → Tri par insertion Si: Grandes données, performance garantie → Heapsort Si: Besoin de stabilité + petite liste → Tri par insertion ou Tri à bulles Si: Priorité à l'implémentation simple → Tri par insertion

6. Trade-offs Avancés

A. Tri Indirect

Problème: Objets très gros (1 Mo chacun), coûteux à déplacer.

Solution: Trier les indices, pas les objets.

// Au lieu de trier les objets directement
BigObject[] objects = ...;
Arrays.sort(objects); // MAUVAIS - Beaucoup de copies

// Trier les indices
Integer[] indices = {0, 1, 2, 3, ...};
Arrays.sort(indices, (a, b) -> objects[a].compareTo(objects[b])); // BON
// Accès: objects[indices[i]]

Trade-off: Complexité accrue, mais beaucoup moins de copies.

B. Tris hybrides (pour information)

Les bibliothèques standard comme Java et C++ utilisent des tris hybrides qui combinent plusieurs algorithmes pour optimiser les performances. Ces algorithmes dépassent le cadre de ce cours (ils font appel à de la récursion ou des techniques avancées), mais il est utile de savoir qu’ils existent.

  • Timsort (Java/Python) : combine Insertion Sort et une variante de fusion.
  • Introsort (C++) : hybride qui bascule vers Heapsort si nécessaire.

Ces solutions sont optimisées pour les données réelles, c’est pourquoi Collections.sort() et Arrays.sort() sont généralement les meilleurs choix en Java.

C. Tri Externe (Données > RAM)

Problème: 100 Go de données, 8 Go de RAM.

Solution: Tri externe

  1. Diviser les données en blocs qui tiennent en RAM
  2. Trier chaque bloc en mémoire (avec Heapsort ou Tri par insertion)
  3. Fusionner les blocs triés sur disque

Trade-off: Plus lent (I/O disque), mais fonctionne.

7. Utiliser l'IA pour Analyser les Algorithmes

Prompt: Comparer des Algorithmes

J'ai une liste de 50 000 objets Person avec attributs: name, age, city.
Je dois trier par age, puis par name (si même age).

Compare Tri par insertion, Tri par sélection et Heapsort pour ce cas:
1. Lequel est le plus adapté?
2. Quels sont les trade-offs?
3. La stabilité est-elle importante ici?

Prompt: Optimiser un Tri

Mon tri est lent sur ces données:
- 10 000 événements
- Presque triés (nouveaux ajouts en fin)
- Tri par timestamp

Quel algorithme utiliser? Explique pourquoi.

8. Au-Delà du Tri : Penser Abstraitement

Principe Général

Cette pensée abstraite s'applique à tous les algorithmes:

Recherche:

  • Données triées → Recherche binaire O(log n)
  • Données non triées → HashMap O(1) ou recherche linéaire O(n)

Parcours de Graphe:

  • Chemin le plus court → Dijkstra
  • Tous les chemins → DFS/BFS

Structure de Données:

  • Accès fréquent par index → ArrayList
  • Insertions/suppressions fréquentes → LinkedList
  • Recherche par clé → HashMap

La Méta-Compétence

"Savoir choisir le bon outil pour le bon problème est plus important que connaître tous les outils."

Avec l'IA:

  • L'IA peut générer n'importe quel algorithme
  • VOUS devez savoir lequel demander
  • La compréhension des trade-offs est irremplaçable

9. Checklist de Décision

Avant de choisir un algorithme:

  • J'ai défini les contraintes (temps, mémoire, stabilité)
  • J'ai caractérisé les données (taille, distribution)
  • J'ai identifié le critère le plus important
  • J'ai éliminé les mauvais choix
  • J'ai comparé les finalistes
  • J'ai vérifié s'il existe une solution standard (Collections.sort)
  • J'ai considéré le trade-off complexité/performance

Conclusion

La pensée algorithmique abstraite est la capacité à:

  1. Analyser : Comprendre le problème et ses contraintes
  2. Comparer : Évaluer les options disponibles
  3. Décider : Choisir la meilleure solution pour le contexte
  4. Justifier : Expliquer pourquoi ce choix

Cette compétence ne peut pas être automatisée par l'IA - c'est VOTRE jugement qui fait la différence entre un code qui fonctionne et un code optimal.