ITSkillsCenter
Carrière & Entretien

Reconnaître les patterns d’algorithmes en entretien : deux pointeurs, fenêtre glissante, parcours

14 دقائق للقراءة
Guide principal : Réussir les entretiens techniques : algorithmes, complexité et system design. Ce tutoriel approfondit une compétence précise du parcours.

Le scénario qui fait perdre la plupart des candidats n’est pas le problème trop dur : c’est l’énoncé jamais vu devant lequel on ne sait pas par où commencer. On relit, on bloque, le chronomètre tourne. Pourtant, la grande majorité des exercices d’entretien se rangent dans une poignée de familles, et chaque famille a son schéma de résolution. Apprendre à reconnaître ces schémas, c’est transformer un problème inconnu en un problème déjà à moitié résolu.

Dans ce tutoriel, vous allez construire un classeur mental de quatre schémas qui couvrent une part énorme des exercices : les deux pointeurs, la fenêtre glissante, les pointeurs lent et rapide, et le parcours en largeur. Pour chacun, vous verrez l’indice qui le déclenche, le squelette de code, et un problème type résolu en Python. À la fin, vous saurez classer un énoncé en quelques secondes au lieu de partir tête baissée.

Ce que vous allez apprendre

  • Identifier, à la lecture d’un énoncé, lequel des quatre schémas s’applique.
  • Écrire le squelette de chaque schéma de mémoire, sans hésiter sur les bornes.
  • Annoncer la complexité de chaque solution, et expliquer pourquoi elle bat la force brute.
  • Éviter les pièges de bornes et de cas limites propres à chaque schéma.

Ce que vous allez construire

Au fil des étapes, vous résoudrez quatre problèmes représentatifs — une paire de somme cible, la plus longue sous-chaîne sans répétition, la détection de cycle dans une liste chaînée, et le comptage d’îlots sur une grille. Le vrai livrable n’est pas ces quatre solutions, mais le réflexe de classement que vous garderez pour tous les exercices suivants.

Prérequis

  • Python 3.9 ou plus récent installé (les exemples utilisent une syntaxe standard).
  • Être à l’aise avec les boucles, les listes et les dictionnaires.
  • Connaître la notation big-O. Si ce n’est pas le cas, lisez d’abord le rappel sur la complexité.
  • Temps estimé : environ 35 minutes.

Étape 1 — Adopter le réflexe : classer avant de coder

Avant la moindre ligne de code, prenez l’habitude de poser une question : « à quelle famille ce problème appartient-il ? » Cette seconde de recul oriente toute la suite et vous évite de réinventer une roue qui existe déjà. Les indices sont souvent dans le vocabulaire de l’énoncé. Un tableau trié invite aux deux pointeurs. Une sous-chaîne ou un sous-tableau contigu avec une contrainte invite à la fenêtre glissante. Une liste chaînée où l’on cherche un cycle ou un milieu invite aux pointeurs lent et rapide. Une grille, un réseau ou un plus court chemin en nombre d’étapes invite au parcours en largeur.

Gardez en tête que ces schémas ont un point commun : ils remplacent une exploration coûteuse par un balayage intelligent qui ne visite chaque élément qu’une fois ou deux. C’est exactement ce qui fait passer une solution du temps quadratique au temps linéaire. Voyons-les un par un.

Étape 2 — Les deux pointeurs : converger depuis les extrémités

Le schéma des deux pointeurs place un curseur à chaque extrémité d’un tableau trié, puis les rapproche selon une condition. Il s’applique dès qu’on cherche une paire, un encadrement ou une symétrie dans une séquence ordonnée. Prenons le problème classique : trouver dans un tableau trié deux nombres dont la somme vaut une cible.

La force brute testerait toutes les paires, en temps quadratique. Mais comme le tableau est trié, on peut être plus malin : si la somme des deux bouts est trop petite, on avance le curseur de gauche pour augmenter la somme ; si elle est trop grande, on recule celui de droite. Chaque élément n’est visité qu’une fois.

def paire_somme(nums, cible):
    gauche, droite = 0, len(nums) - 1
    while gauche < droite:
        somme = nums[gauche] + nums[droite]
        if somme == cible:
            return (gauche, droite)
        if somme < cible:
            gauche += 1
        else:
            droite -= 1
    return None

print(paire_somme([2, 7, 11, 15], 9))  # (0, 1)

Le résultat affiché est (0, 1), car nums[0] + nums[1] vaut bien 9. La boucle s’arrête dès que les curseurs se croisent, ce qui donne un temps linéaire en O(n) et un espace constant en O(1). Le piège à connaître : ce schéma exige un tableau trié ; si l’énoncé fournit des données en vrac, soit on les trie d’abord, soit on bascule sur une table de hachage.

Étape 3 — La fenêtre glissante : un sous-tableau qui respire

La fenêtre glissante maintient un intervalle contigu — la fenêtre — qu’on étend par la droite et qu’on rétracte par la gauche selon une contrainte. Elle brille sur les problèmes de sous-chaîne ou de sous-tableau contigu : plus longue séquence sans répétition, plus petite fenêtre couvrant une condition, somme maximale d’une taille donnée. Le cas emblématique est la plus longue sous-chaîne sans caractère répété.

L’idée : on avance la borne droite caractère par caractère ; dès qu’un caractère déjà présent dans la fenêtre réapparaît, on déplace la borne gauche juste après sa précédente occurrence. Un dictionnaire mémorise la dernière position de chaque caractère.

def plus_longue_sans_repetition(s):
    derniere_position = {}
    debut = 0
    meilleur = 0
    for fin, caractere in enumerate(s):
        if caractere in derniere_position and derniere_position[caractere] >= debut:
            debut = derniere_position[caractere] + 1
        derniere_position[caractere] = fin
        meilleur = max(meilleur, fin - debut + 1)
    return meilleur

print(plus_longue_sans_repetition("abcabcbb"))  # 3

Sur l’entrée "abcabcbb", la plus longue sous-chaîne sans répétition est "abc", de longueur 3, et c’est bien la valeur retournée. Chaque caractère est traité une seule fois, d’où un temps linéaire en O(n). Le piège classique : oublier de vérifier que la position mémorisée est bien dans la fenêtre courante (le test >= debut), sinon on rétracte la fenêtre à tort.

Il existe en réalité deux variétés de fenêtre glissante, et savoir les distinguer évite bien des erreurs. La fenêtre de taille fixe garde toujours la même largeur : on la fait glisser d’un cran en ajoutant l’élément qui entre et en retirant celui qui sort, comme dans le calcul d’une somme maximale sur k éléments. La fenêtre de taille variable, elle, s’étire et se contracte selon une condition : on étend la borne droite tant que la contrainte tient, et on rétracte la gauche dès qu’elle est violée. Le problème de la plus longue sous-chaîne sans répétition relève de cette seconde variété. Demandez-vous toujours, en lisant l’énoncé, si la taille de la fenêtre est imposée ou si elle dépend d’une condition : la réponse dicte la structure de votre boucle.

Étape 4 — Les pointeurs lent et rapide : détecter un cycle

Ce schéma fait avancer deux curseurs à des vitesses différentes dans une séquence chaînée. Le lent fait un pas, le rapide en fait deux. S’il existe un cycle, le rapide finit toujours par rattraper le lent ; s’il n’y en a pas, le rapide atteint la fin. C’est l’algorithme de détection de cycle de Floyd, et il se reconnaît dès qu’un énoncé parle de boucle dans une liste chaînée ou de recherche du milieu.

class Noeud:
    def __init__(self, valeur):
        self.valeur = valeur
        self.suivant = None

def a_un_cycle(tete):
    lent = rapide = tete
    while rapide and rapide.suivant:
        lent = lent.suivant
        rapide = rapide.suivant.suivant
        if lent is rapide:
            return True
    return False

La fonction renvoie True dès que les deux curseurs coïncident, ce qui ne peut arriver qu’en présence d’un cycle. L’élégance du procédé tient à son espace constant en O(1) : pas besoin de mémoriser les nœuds visités dans un ensemble, contrairement à l’approche naïve. Le temps reste linéaire. Si vous voulez revoir comment se manipule une structure chaînée, le tutoriel implémenter une liste chaînée en Python pose les bases.

Étape 5 — Le parcours en largeur : explorer une grille

Dès qu’un problème évoque des cases voisines, un réseau ou un plus court chemin en nombre d’étapes, un graphe se cache derrière, et le parcours en largeur est souvent la bonne réponse. Il explore les voisins niveau par niveau à l’aide d’une file. Le problème type est le comptage d’îlots : dans une grille de terres et d’eaux, combien de groupes de terres connectées existe-t-il ?

On parcourt la grille ; chaque fois qu’on tombe sur une terre non encore visitée, on lance un parcours en largeur qui marque toute l’île, et on incrémente le compteur. La file utilise collections.deque, qui offre un retrait en tête en temps constant — un point que la documentation officielle de Python garantit.

from collections import deque

def nombre_ilots(grille):
    if not grille:
        return 0
    lignes, colonnes = len(grille), len(grille[0])
    vus = set()
    compte = 0

    def bfs(depart_r, depart_c):
        file = deque([(depart_r, depart_c)])
        vus.add((depart_r, depart_c))
        while file:
            r, c = file.popleft()
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nr, nc = r + dr, c + dc
                if (0 <= nr < lignes and 0 <= nc < colonnes
                        and grille[nr][nc] == "1" and (nr, nc) not in vus):
                    vus.add((nr, nc))
                    file.append((nr, nc))

    for r in range(lignes):
        for c in range(colonnes):
            if grille[r][c] == "1" and (r, c) not in vus:
                bfs(r, c)
                compte += 1
    return compte

carte = [
    ["1", "1", "0", "0"],
    ["1", "0", "0", "1"],
    ["0", "0", "1", "1"],
]
print(nombre_ilots(carte))  # 2

Le résultat est 2 : un premier îlot en haut à gauche, un second en bas à droite. On marque chaque case visitée dans l’ensemble vus pour ne jamais la recompter, ce qui garantit un temps linéaire en la taille de la grille, soit O(lignes × colonnes). Le piège récurrent est l’oubli du marquage avant l’ajout dans la file : sans lui, une même case entre plusieurs fois et le programme tourne en rond. Pour aller plus loin sur ce schéma, voyez implémenter le parcours en largeur sur un graphe.

Pourquoi un seul balayage suffit

Ces quatre schémas partagent une intuition profonde qui mérite d’être nommée, car elle vous aidera à en inventer de nouveaux. Dans une solution en force brute, on recommence souvent un travail déjà fait : pour chaque élément, on re-parcourt tout le reste. Les schémas efficaces suppriment cette redondance en conservant, d’un pas à l’autre, l’information déjà calculée. Les deux pointeurs ne reviennent jamais en arrière ; la fenêtre glissante réutilise la somme précédente au lieu de la recalculer ; les pointeurs lent et rapide n’ont besoin d’aucune mémoire annexe ; le parcours en largeur marque chaque case pour ne jamais la revisiter.

Cette idée porte un nom en analyse d’algorithmes : l’amortissement. Même si, à un instant donné, une étape semble coûteuse — la borne gauche de la fenêtre peut avancer de plusieurs crans d’un coup — le coût total sur l’ensemble du parcours reste linéaire, parce que chaque élément n’est franchi qu’un nombre constant de fois au total. Quand vous justifiez la complexité d’une de ces solutions à l’examinateur, c’est exactement cet argument qu’il attend : non pas « la boucle interne est constante », ce qui serait faux, mais « chaque élément entre et sort de la fenêtre au plus une fois, donc le total est linéaire ». Savoir formuler ce raisonnement vous distingue nettement.

Étape 6 — Construire son arbre de décision

Vous avez maintenant quatre schémas. La compétence finale consiste à les choisir vite. Entraînez-vous à dérouler ce petit arbre de décision à chaque énoncé : la donnée est-elle une séquence triée où l’on cherche une paire ou un encadrement ? Deux pointeurs. S’agit-il d’un sous-tableau ou d’une sous-chaîne contiguë sous contrainte ? Fenêtre glissante. Manipule-t-on une liste chaînée avec une notion de cycle ou de milieu ? Pointeurs lent et rapide. Parle-t-on de cases voisines, de réseau, de plus court chemin en étapes ? Parcours en largeur. Ce réflexe, répété sur quelques dizaines de problèmes, devient automatique et vous fait gagner les minutes décisives de l’entretien.

Pièges fréquents

Symptôme Cause probable Correctif
Les deux pointeurs renvoient un résultat faux Le tableau n’était pas trié Trier d’abord, ou passer à une table de hachage
La fenêtre glissante rétracte trop Position mémorisée hors de la fenêtre prise en compte Tester que la position est supérieure ou égale au début
Boucle infinie sur la liste chaînée Condition d’arrêt incomplète Vérifier à la fois le curseur rapide et son suivant
Le parcours visite deux fois la même case Marquage après ajout dans la file, pas avant Ajouter à l’ensemble des vus au moment de l’empilement
Complexité qui reste quadratique Un parcours imbriqué caché dans le corps de boucle Vérifier que chaque élément n’est touché qu’un nombre constant de fois

S’adapter au contexte ouest-africain

La plupart des entretiens techniques de la région se passent à distance, sur un éditeur de code partagé, parfois sans coloration syntaxique ni autocomplétion. Entraînez-vous donc à écrire ces squelettes de mémoire, sans l’aide de votre environnement habituel : ouvrez un éditeur en ligne minimal et tapez chaque schéma à blanc. Côté entraînement quotidien, beaucoup de plateformes d’exercices fonctionnent correctement même sur une connexion modeste ; téléchargez les énoncés quand le réseau est bon et travaillez hors ligne pendant les coupures. Un forfait data rechargé par mobile money en secours évite la panique si la fibre du quartier lâche en pleine séance.

Récapitulatif

Vous disposez désormais d’un classeur de quatre schémas qui couvrent une large part des exercices d’entretien. Vous savez repérer l’indice qui déclenche chacun, écrire son squelette, et annoncer sa complexité. Surtout, vous avez intégré le réflexe qui fait la différence : classer le problème avant d’écrire la moindre ligne. C’est cette habitude, plus que la mémoire des solutions, qui vous fera avancer face à un énoncé inconnu.

Aide-mémoire

Indice dans l’énoncé Schéma Complexité visée
Tableau trié, paire ou encadrement Deux pointeurs O(n) temps, O(1) espace
Sous-tableau ou sous-chaîne contiguë Fenêtre glissante O(n) temps
Liste chaînée, cycle ou milieu Pointeurs lent et rapide O(n) temps, O(1) espace
Grille, réseau, plus court chemin en étapes Parcours en largeur O(V + E) temps

À vous de jouer

Voici un défi pour ancrer la fenêtre glissante : écrivez une fonction qui renvoie la somme maximale d’un sous-tableau contigu de taille exactement k. Tentez-la avant de regarder la solution.

Voir une solution
def somme_max_fenetre(nums, k):
    if len(nums) < k:
        return None
    somme = sum(nums[:k])
    meilleur = somme
    for i in range(k, len(nums)):
        somme += nums[i] - nums[i - k]
        meilleur = max(meilleur, somme)
    return meilleur

print(somme_max_fenetre([2, 1, 5, 1, 3, 2], 3))  # 9

On calcule la somme de la première fenêtre, puis on la fait glisser en ajoutant l’élément qui entre et en retirant celui qui sort, sans tout recalculer. Le résultat est 9, la somme de la fenêtre [5, 1, 3]. La complexité reste linéaire en O(n).

Tutoriels frères

Pour aller plus loin

Foire aux questions

Dois-je connaître d’autres schémas que ces quatre ?
Ces quatre couvrent une part majeure des exercices, mais d’autres existent : récursion avec retour sur trace, recherche binaire sur la réponse, programmation dynamique. Les quatre présentés ici sont les plus rentables à maîtriser en premier.

Comment savoir si ma solution est assez efficace ?
Annoncez sa complexité et comparez-la à la taille de l’entrée. Si l’énoncé évoque des entrées de grande taille et que votre solution est quadratique, cherchez un schéma linéaire — c’est presque toujours le signal qu’il en existe un.

Faut-il apprendre ces squelettes par cœur ?
Apprenez la logique, pas le texte. Réécrivez chaque schéma plusieurs fois à blanc jusqu’à ce que la structure vienne naturellement ; c’est la compréhension du mécanisme qui vous sauvera sur une variante inattendue.

مشاركة
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité