Two Pointers et Sliding Window : optimisation pour tableaux
1. Présentation générale
Quand vous rencontrez un problème de parcours de tableau ou de chaîne où la solution brute-force s’exécute en O(n²) ou O(n³), les deux techniques two pointers et sliding window vous aideront généralement à réduire la complexité à O(n). Cet article explique l’idée centrale de chaque technique, accompagnée d’exemples Java concrets : two sum sur un tableau trié, vérification de palindrome, somme maximale d’une sous-suite dans une fenêtre fixe, et plus longue sous-chaîne sans caractère répétitif. Après la lecture, vous reconnaîtrez le motif et pourrez l’appliquer à de nombreux problèmes similaires.
2. Two Pointers : qu’est-ce que c’est
Two pointers est une technique utilisant deux variables d’index (pointeurs) se déplaçant sur une structure de données (généralement un tableau ou une chaîne triée) pour éviter les parcours imbriqués. Les deux pointeurs peuvent se déplacer :
- Même direction (fast/slow) : utilisée pour supprimer les éléments en double, détecter les cycles.
- Directions opposées (left/right des deux extrémités) : utilisée pour trouver une paire de somme égale à la cible, vérifier un palindrome.
2.1. Two pointers en directions opposées : trouver une paire dont la somme égale la cible
Problème classique : étant donné un tableau trié en ordre croissant, trouver deux nombres dont la somme égale la cible.
public class TwoSumSorted {
public int[] findPair(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return new int[]{numbers[left], numbers[right]};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[0];
}
}
Explication :
leftcommence au début du tableau,rightà la fin du tableau.- Si
sum < target, le nombre àleftest trop petit, donc on augmenteleftpour obtenir un nombre plus grand. - Si
sum > target, le nombre àrightest trop grand, donc on diminueright. - La boucle s’exécute en maximum n étapes car à chaque itération
leftourightse déplace d’un cran, donnant une complexité O(n) au lieu de O(n²) avec une approche à deux boucles imbriquées.
Test avec JUnit 5 + AssertJ :
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TwoSumSortedTest {
private final TwoSumSorted twoSum = new TwoSumSorted();
@Test
void shouldFindPairWhenSumMatchesTarget() {
int[] numbers = {1, 3, 5, 8, 11};
int[] result = twoSum.findPair(numbers, 13);
assertThat(result).containsExactly(5, 8);
}
}
Remarque : cette technique nécessite un tableau déjà trié. Si le tableau n’est pas trié, vous devez d’abord le trier (O(n log n)) ou utiliser
HashMappour maintenir O(n) sans tri.
2.2. Two pointers pour vérifier un palindrome
public class PalindromeChecker {
public boolean isPalindrome(String text) {
int left = 0;
int right = text.length() - 1;
while (left < right) {
if (text.charAt(left) != text.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
Les deux pointeurs partent des deux extrémités de la chaîne, comparent les caractères puis avancent vers le milieu. S’ils rencontrent une paire de caractères différents, ils renvoient false immédiatement — pas besoin de parcourir tout. C’est pourquoi cette technique est plus rapide qu’inverser la chaîne puis comparer (reverse().equals()), car elle évite de créer un objet StringBuilder intermédiaire.
3. Qu’est-ce que le Sliding Window ?
Le sliding window est une variante de two pointers, applicable aux problèmes concernant les sous-séquences contiguës (subarray/substring). Au lieu de recalculer à partir du début pour chaque position de départ, vous maintenez une « fenêtre » [left, right] et la mettez à jour progressivement au fur et à mesure que la fenêtre glisse, en réutilisant les résultats précédemment calculés.
Il y a deux types :
- Fixed-size window : la taille de la fenêtre est fixe (par exemple k éléments).
- Variable-size window : la taille s’adapte selon la condition du problème.
3.1. Fixed-size window : plus grande somme de sous-suite de taille k
public class MaxSumSubarray {
public int maxSum(int[] numbers, int windowSize) {
int windowSum = 0;
for (int i = 0; i < windowSize; i++) {
windowSum += numbers[i];
}
int maxSum = windowSum;
for (int right = windowSize; right < numbers.length; right++) {
windowSum += numbers[right] - numbers[right - windowSize];
maxSum = Math.max(maxSum, windowSum);
}
return maxSum;
}
}
Explication :
- La première boucle calcule la somme de la première fenêtre (de l’index 0 à
windowSize - 1). - À partir de là, chaque fois que vous décalez la fenêtre d’un pas vers la droite : vous ajoutez le nouvel élément (
numbers[right]) et soustrayez l’élément qui quitte la fenêtre (numbers[right - windowSize]). - Ainsi, chaque étape ne coûte que O(1) au lieu de recalculer la somme en O(k), pour une complexité totale O(n).
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MaxSumSubarrayTest {
private final MaxSumSubarray solver = new MaxSumSubarray();
@Test
void shouldReturnMaxSumOfWindowSizeThree() {
int[] numbers = {2, 1, 5, 1, 3, 2};
int result = solver.maxSum(numbers, 3);
assertThat(result).isEqualTo(9); // fenêtre [5, 1, 3]
}
}
3.2. Variable-size window : plus longue sous-chaîne sans caractère répétitif
Dans ce problème, la fenêtre s’adapte selon la condition « pas de caractère répétitif ».
import java.util.HashMap;
import java.util.Map;
public class LongestUniqueSubstring {
public int lengthOfLongestSubstring(String text) {
Map<Character, Integer> lastSeenIndex = new HashMap<>();
int left = 0;
int maxLength = 0;
for (int right = 0; right < text.length(); right++) {
char current = text.charAt(right);
if (lastSeenIndex.containsKey(current) && lastSeenIndex.get(current) >= left) {
left = lastSeenIndex.get(current) + 1;
}
lastSeenIndex.put(current, right);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
}
Explication :
rightavance toujours (élargit la fenêtre),leftsaute seulement quand on rencontre un caractère répétitif à l’intérieur de la fenêtre actuelle.lastSeenIndexenregistre la position d’apparition la plus récente de chaque caractère pour savoir oùleftdoit sauter.- La condition
lastSeenIndex.get(current) >= leftgarantit qu’on considère un caractère comme « répétitif » seulement s’il se trouve toujours à l’intérieur de la fenêtre actuelle, évitant queleftne recule incorrectement. - Chaque caractère est traité exactement une fois lors du déplacement de
right, donc la complexité totale est O(n).
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class LongestUniqueSubstringTest {
private final LongestUniqueSubstring solver = new LongestUniqueSubstring();
@Test
void shouldReturnLengthOfLongestUniqueSubstring() {
int result = solver.lengthOfLongestSubstring("abcabcbb");
assertThat(result).isEqualTo(3); // "abc"
}
@Test
void shouldHandleAllSameCharacters() {
int result = solver.lengthOfLongestSubstring("bbbbb");
assertThat(result).isEqualTo(1);
}
}
Sortie lors du débogage manuel avec "abcabcbb" :
right=0 'a' -> fenêtre="a" longueur=1
right=1 'b' -> fenêtre="ab" longueur=2
right=2 'c' -> fenêtre="abc" longueur=3
right=3 'a' -> répétitif, left=1 -> fenêtre="bca" longueur=3
right=4 'b' -> répétitif, left=2 -> fenêtre="cab" longueur=3
...
maxLength = 3
4. Comparaison et quand utiliser quelle technique
| Situation | Technique appropriée |
|---|---|
| Tableau trié, trouver une paire/triplet satisfaisant une condition de somme | Two pointers en directions opposées |
| Vérifier la symétrie (palindrome), inverser un tableau sur place | Two pointers en directions opposées |
| Supprimer les éléments en double dans un tableau trié | Two pointers même direction (fast/slow) |
| Subarray/substring de taille fixe k | Sliding window fixed-size |
| Subarray/substring satisfaisant une condition dynamique (pas de répétition, somme ≤ cible…) | Sliding window variable-size |
Point commun : les deux techniques évitent les recalculs sur les parties déjà traitées, en passant de O(n²) à O(n) en maintenant l’état (sum, set, map) lors du déplacement des pointeurs/fenêtres.
5. Erreurs courantes
- Oublier la condition d’arrêt
left < right: cela peut facilement causer une boucle infinie ou comparer un élément avec lui-même quandleft == right. - Appliquer two pointers à un tableau non trié en oubliant de le trier d’abord — le résultat sera complètement incorrect car la logique d’augmentation/diminution des pointeurs repose sur la monotonie du tableau.
- Dans le sliding window, oublier de mettre à jour l’état quand
leftse déplace : par exemple, oublier de supprimer l’ancien élément deHashMap/sum, ce qui fait que la fenêtre « se souvient » de données n’appartenant plus à la fenêtre. - Confondre la condition d’élargissement/rétrécissement de la fenêtre : avec variable-size window, vous devez clairement définir quelle condition fait avancer
rightet quelle condition fait avancerleft— si c’est inversé, la fenêtre ne se rétrécira jamais correctement.
Remarque : ces deux techniques ne conviennent pas quand le problème demande d’examiner toutes les combinaisons de sous-ensembles non contigus (par exemple subset sum) — dans ce cas, vous avez besoin du backtracking ou de la programmation dynamique, pas du sliding window.
6. Conclusion
Two pointers et sliding window sont deux outils fondamentaux pour résoudre les problèmes de tableaux/chaînes avec une complexité linéaire. Reconnaître le motif — un tableau trié où il faut trouver une paire, ou des subarray/substring contigus à optimiser — vous aide à choisir la bonne technique et à écrire du code concis et facile à tester comme les exemples ci-dessus. Pendant l’entraînement, essayez de redessiner l’état de left/right à chaque étape pour bien comprendre l’invariant avant de coder.