backend / algorithmes-structures-donnees
Patterns de résolution pour entretiens techniques
Explication
Ce que vous allez apprendre
- Reconnaître, à la lecture d'un énoncé, quel pattern algorithmique classique s'applique
- Appliquer le pattern "deux pointeurs" sur un tableau trié pour éviter une double boucle
- Appliquer le pattern "fenêtre glissante" pour des problèmes de sous-tableaux ou sous-chaînes contigus
- Reconnaître un problème "top K" et savoir pourquoi un tas bat un tri complet dans ce cas
- Identifier un espace de réponses monotone pour appliquer la recherche binaire sur la réponse, même hors d'un contexte de recherche classique
Dans quel contexte ?
Une candidate passe un entretien technique et reçoit un énoncé qu'elle n'a jamais vu : « trouver la plus petite sous-chaîne contenant tous les caractères d'un motif donné ». Plutôt que de paniquer, elle reconnaît la structure du problème — un sous-tableau contigu qui doit satisfaire une condition — et applique directement le pattern "fenêtre glissante" appris en cours, sans avoir jamais résolu cet exercice précis auparavant. C'est exactement l'objectif de cette leçon : ne pas mémoriser des solutions par cœur, mais reconnaître des structures de problèmes récurrentes.
D'abord, pourquoi reconnaître un "pattern" change tout
Après avoir étudié de nombreux algorithmes séparément, on réalise qu'un grand nombre de problèmes, en apparence différents, se résolvent en fait avec la même poignée de techniques réutilisables. Reconnaître rapidement "quel pattern s'applique ici" fait souvent la différence entre trouver une solution en quelques minutes et rester bloqué.
Prérequis
Cette leçon est une synthèse : elle suppose connus les tas (leçon 12), la recherche binaire (leçon 5) et les listes chaînées (leçon 2).
Pattern 1 : les deux pointeurs
Sur un tableau trié, resserrer deux index vers le centre évite de tester toutes les paires possibles une par une.
Pattern 2 : la fenêtre glissante
Maintenir une plage [gauche, droite] qui avance sans jamais revenir en arrière convient à tout problème sur des sous-tableaux ou sous-chaînes contigus.
Pattern 3 : les pointeurs rapide/lent
Deux pointeurs avançant à des vitesses différentes dans une structure chaînée permettent de trouver un milieu ou un cycle, sans jamais compter la longueur au préalable.
| Pattern | Signal dans l'énoncé | Complexité typique |
|---|---|---|
| Deux pointeurs | Tableau trié, chercher une paire ou un triplet | O(n) au lieu de O(n²) |
| Fenêtre glissante | Sous-tableau ou sous-chaîne contigu avec une condition | O(n) au lieu de O(n²) |
| Pointeurs rapide/lent | Liste chaînée, cycle ou milieu à trouver | O(n) en temps, O(1) en espace |
| Merge intervals | Intervalles qui se chevauchent | O(n log n) pour le tri initial |
| Top K | Ne garder que les K plus grands/petits éléments | O(n log k) avec un tas |
| Recherche binaire sur la réponse | Espace de réponses monotone | O(n log(plage de réponses)) |
Pattern 4 : merge intervals
Trier d'abord les intervalles, puis les fusionner en un seul passage, résout la plupart des problèmes de chevauchement d'intervalles.
Pattern 5 : top K
Un tas est préférable à un tri complet dès que seul un sous-ensemble des éléments extrêmes importe réellement.
Pattern 6 : la recherche binaire sur la réponse
Cette technique s'applique dès qu'un espace de réponses possibles est "monotone" (une capacité suffisante le reste au-delà), même si le problème ne ressemble pas du tout à une recherche classique.
Astuce
Face à un nouvel énoncé, posez-vous ces questions dans l'ordre : les données sont-elles triées ou peuvent-elles l'être facilement (deux pointeurs) ? Est-ce un sous-tableau contigu (fenêtre glissante) ? Une structure chaînée (pointeurs rapide/lent) ? Ne cherche-t-on que les K meilleurs éléments (tas) ?
Ce qui compte le plus : la méthode, pas la mémorisation
Face à un problème inconnu, la démarche prime sur la connaissance par cœur de chaque pattern : clarifier les contraintes, écrire d'abord une solution brute même lente pour valider le raisonnement, puis identifier quel pattern s'applique.
Un dernier réflexe à ne jamais oublier
Tester systématiquement les cas limites (tableau vide, un seul élément, doublons) évite les erreurs les plus fréquentes.
Vers la suite
Ces patterns généraux couvrent la majorité des problèmes courants. Les prochaines leçons plongent dans des techniques plus spécialisées, à commencer par la recherche de motifs dans du texte avec KMP.
Commandes & code
Patterns de résolution pour entretiens techniques
# --- Pattern 1 : Deux pointeurs (two pointers) -- tableau trié, palindromes, paires ---
def paire_somme_cible(tableau: list[int], cible: int) -> tuple[int, int] | None:
# O(n) au lieu de O(n^2) en force brute -- exige un tableau TRIÉ
gauche, droite = 0, len(tableau) - 1
while gauche < droite:
total = tableau[gauche] + tableau[droite]
if total == cible:
return (gauche, droite)
elif total < cible:
gauche += 1 # augmenter la somme
else:
droite -= 1 # diminuer la somme
return None
assert paire_somme_cible([1, 2, 4, 7, 11, 15], 15) == (2, 4) # 4 + 11
def est_palindrome(s: str) -> bool:
gauche, droite = 0, len(s) - 1
while gauche < droite:
if s[gauche] != s[droite]:
return False
gauche += 1; droite -= 1
return True
# --- Pattern 2 : Fenêtre glissante (sliding window) -- sous-tableaux/sous-chaînes contigus ---
def plus_longue_sous_chaine_sans_repetition(s: str) -> int:
# O(n) : la fenêtre [gauche, droite] ne contient jamais de doublon
vus = {}
gauche = 0
max_longueur = 0
for droite, caractere in enumerate(s):
if caractere in vus and vus[caractere] >= gauche:
gauche = vus[caractere] + 1 # on saute directement après la dernière occurrence
vus[caractere] = droite
max_longueur = max(max_longueur, droite - gauche + 1)
return max_longueur
assert plus_longue_sous_chaine_sans_repetition("abcabcbb") == 3 # "abc"
# --- Pattern 3 : Fast & slow pointers -- détection de cycle, milieu de liste ---
def trouver_milieu_liste(tete) -> object:
lent = rapide = tete
while rapide and rapide.suivant:
lent = lent.suivant
rapide = rapide.suivant.suivant
return lent # quand rapide atteint la fin, lent est exactement au milieu
# --- Pattern 4 : Merge intervals -- fusionner des intervalles chevauchants ---
def fusionner_intervalles(intervalles: list[tuple[int, int]]) -> list[tuple[int, int]]:
if not intervalles:
return []
intervalles_tries = sorted(intervalles)
resultat = [intervalles_tries[0]]
for debut, fin in intervalles_tries[1:]:
dernier_debut, dernier_fin = resultat[-1]
if debut <= dernier_fin: # chevauchement -> fusionner
resultat[-1] = (dernier_debut, max(dernier_fin, fin))
else:
resultat.append((debut, fin))
return resultat
assert fusionner_intervalles([(1, 3), (2, 6), (8, 10), (15, 18)]) == [(1, 6), (8, 10), (15, 18)]
# --- Pattern 5 : Top K éléments -- toujours envisager un tas plutôt qu'un tri complet ---
import heapq
from collections import Counter
def k_elements_plus_frequents(tableau: list, k: int) -> list:
compteur = Counter(tableau)
return [element for element, _ in heapq.nlargest(k, compteur.items(), key=lambda x: x[1])]
# --- Pattern 6 : Recherche binaire sur la réponse (binary search on answer) ---
def capacite_minimale_expedition(poids: list[int], jours: int) -> int:
# Minimiser la capacité du bateau pour livrer tous les colis en <= jours, dans l'ordre
def jours_necessaires(capacite: int) -> int:
jours_utilises, charge_actuelle = 1, 0
for p in poids:
if charge_actuelle + p > capacite:
jours_utilises += 1
charge_actuelle = 0
charge_actuelle += p
return jours_utilises
gauche, droite = max(poids), sum(poids)
while gauche < droite:
milieu = (gauche + droite) // 2
if jours_necessaires(milieu) <= jours:
droite = milieu # capacité suffisante -- on tente plus petit
else:
gauche = milieu + 1
return gauche
assert capacite_minimale_expedition([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5) == 15
# --- Pattern 7 : BFS/DFS sur grille -- pattern très fréquent (îles, labyrinthes, jeux) ---
def chemin_le_plus_court_grille(grille: list[list[int]], depart: tuple, arrivee: tuple) -> int:
from collections import deque
lignes, colonnes = len(grille), len(grille[0])
file = deque([(depart, 0)])
visites = {depart}
while file:
(i, j), distance = file.popleft()
if (i, j) == arrivee:
return distance
for di, dj in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
ni, nj = i + di, j + dj
if (0 <= ni < lignes and 0 <= nj < colonnes
and grille[ni][nj] == 0 and (ni, nj) not in visites):
visites.add((ni, nj))
file.append(((ni, nj), distance + 1))
return -1
# --- Méthode générale face à un problème inconnu en entretien ---
# 1. Clarifier les contraintes (taille des entrées, doublons, valeurs négatives, tri déjà fait ?)
# 2. Commencer par une solution brute (force brute), même O(n^2)/O(n^3) -- valider la logique d'abord
# 3. Identifier le pattern (deux pointeurs ? fenêtre glissante ? DP ? graphe ? tas ?)
# 4. Optimiser en fonction du pattern identifié, en expliquant le compromis temps/espace
# 5. Tester sur les cas limites : tableau vide, un seul élément, doublons, valeurs extrêmesRésumé
- Reconnaître le PATTERN (deux pointeurs, fenêtre glissante, top-K, merge intervals, BFS/DFS grille, binary search on answer) accélère radicalement la résolution.
- Deux pointeurs exige généralement des données triées ; fenêtre glissante s'applique aux sous-tableaux/sous-chaînes contigus.
- La recherche binaire ne se limite pas à chercher une valeur : elle s'applique à tout espace de réponses monotone (minimiser une capacité, un temps, etc.).
- Méthode systématique en entretien : clarifier -> solution brute -> identifier le pattern -> optimiser -> tester les cas limites.
Exercices pratiques
Mission : reconnaître le bon pattern en entretien technique chronométré
Objectif : Identifier le pattern adapté à trois énoncés inconnus et justifier pourquoi la solution naïve serait insuffisante.
Contexte
Tu passes un entretien technique de 45 minutes avec trois questions enchaînées, chacune formulée différemment mais correspondant à un pattern vu dans la leçon. Le recruteur ne te dira jamais explicitement quel pattern utiliser : c'est à toi de le reconnaître à partir de la structure de l'énoncé, pas du vocabulaire employé.