Retour au cours

backend / algorithmes-structures-donnees

Patterns de résolution pour entretiens techniques

Leçon 241 exercice

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.

PatternSignal dans l'énoncéComplexité typique
Deux pointeursTableau trié, chercher une paire ou un tripletO(n) au lieu de O(n²)
Fenêtre glissanteSous-tableau ou sous-chaîne contigu avec une conditionO(n) au lieu de O(n²)
Pointeurs rapide/lentListe chaînée, cycle ou milieu à trouverO(n) en temps, O(1) en espace
Merge intervalsIntervalles qui se chevauchentO(n log n) pour le tri initial
Top KNe garder que les K plus grands/petits élémentsO(n log k) avec un tas
Recherche binaire sur la réponseEspace de réponses monotoneO(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

python
# --- 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êmes

Ré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

1 disponible
1

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é.

Résoudre l’exercice →