Maximum dans une fenêtre glissante avec une deque monotone
Maintenez une deque décroissante d’indices pour répondre en O(1) par élément aux requêtes de maximum dans une fenêtre et résoudre le problème sliding-window-maximum en O(n).
Maximum dans une fenêtre glissante avec une deque monotone est une leçon DSA Interview Prep gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DSA Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DSA Interview Prep comprend 4 leçons au total.
Problème du maximum d'une fenêtre glissante
Le problème du maximum d'une fenêtre glissante (LeetCode 239) fournit un tableau et une taille de fenêtre k. Lorsque la fenêtre se déplace de gauche à droite, une position à la fois, renvoyez l'élément maximal de chaque fenêtre. Une approche par force brute calcule le maximum de chaque fenêtre de k éléments en O(k), soit O(nk) au total, ce qui est trop lent pour les grandes valeurs de k.
La solution par file monotone à double extrémité atteint O(n) au total en maintenant une file décroissante d'indices. Le début contient toujours l'indice du maximum de la fenêtre courante, ce qui fournit des requêtes de maximum en O(1) tout en permettant des opérations aux deux extrémités.
from collections import deque
# Brute force O(nk) for comparison
def sliding_max_brute(nums, k):
return [max(nums[i:i+k]) for i in range(len(nums) - k + 1)]
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print('Input:', nums, 'k=', k)
print('Expected: [3, 3, 5, 5, 6, 7]')
print('Brute: ', sliding_max_brute(nums, k))File monotone à double extrémité : idée clé
Maintenez une file monotone décroissante à double extrémité qui stocke des indices (et non des valeurs). L'invariant est le suivant : nums[deque[0]] >= nums[deque[1]] >= ... >= nums[deque[-1]]. Avant d'ajouter l'indice i :
- Retirez les indices expirés du début : si
deque[0] <= i - k, l'indice a quitté la fenêtre. - Retirez de la fin les indices associés à de plus petites valeurs : tant que
nums[deque[-1]] <= nums[i], ces indices ne peuvent plus jamais être le maximum d'une fenêtre future (ils sont situés plus à gauche et leurs valeurs sont plus petites) ; éliminez-les donc.
Après ces opérations, ajoutez i à la fin. Le début fournit toujours le maximum de la fenêtre courante.
from collections import deque
def sliding_window_max(nums, k):
dq = deque() # stores indices; values are decreasing
result = []
for i, n in enumerate(nums):
# 1. Remove indices outside the current window
while dq and dq[0] <= i - k:
dq.popleft()
# 2. Remove indices with smaller values from the back
while dq and nums[dq[-1]] <= n:
dq.pop()
dq.append(i)
# 3. Record max when first full window is complete
if i >= k - 1:
result.append(nums[dq[0]]) # front = max of current window
return result
nums = [1, 3, -1, -3, 5, 3, 6, 7]
print(sliding_window_max(nums, 3)) # [3, 3, 5, 5, 6, 7]Suivi de la file à double extrémité étape par étape
Suivons [1, 3, -1, -3, 5, 3, 6, 7] avec k=3 :
- i=0 (1) : dq=[0]
- i=1 (3) : pop 0 (1<3), dq=[1]
- i=2 (-1) : -1<3, donc conservez, dq=[1,2]. Fenêtre [1,3,-1], maximum=nums[1]=3
- i=3 (-3) : -3<-1, dq=[1,2,3]. Vérifiez le début : 1 > 3-3=0, OK. Maximum de la fenêtre=3
- i=4 (5) : pop 3,2,1 (tous plus petits), dq=[4]. Le début 4 > 4-3=1, OK. Maximum=5
- i=5 (3) : 3<5, dq=[4,5]. Le début 4 > 5-3=2, OK. Maximum=5
- i=6 (6) : pop 5,4 (tous deux plus petits), dq=[6]. Maximum=6
- i=7 (7) : pop 6, dq=[7]. Maximum=7
from collections import deque
def sliding_window_max_trace(nums, k):
dq = deque()
result = []
for i, n in enumerate(nums):
while dq and dq[0] <= i - k:
print(f' Remove expired index {dq[0]} from front')
dq.popleft()
while dq and nums[dq[-1]] <= n:
print(f' Remove smaller index {dq[-1]} (val={nums[dq[-1]]}) from back')
dq.pop()
dq.append(i)
print(f'i={i} n={n}: dq={list(dq)} vals={[nums[j] for j in dq]}')
if i >= k - 1:
win_max = nums[dq[0]]
result.append(win_max)
print(f' Window {nums[max(0,i-k+1):i+1]} -> max={win_max}')
return result
nums = [1, 3, -1, -3, 5, 3, 6, 7]
result = sliding_window_max_trace(nums, 3)
print('Result:', result)Pourquoi chaque élément est ajouté et retiré au plus une fois
La garantie O(n) repose sur le même raisonnement amorti que pour la pile monotone : chaque indice est ajouté à la file exactement une fois et retiré (soit du début lorsqu'il expire, soit de la fin lorsqu'il est remplacé) au plus une fois. Le nombre total d'opérations sur la file dans toute la boucle est donc au plus de 2n.
Les boucles while internes n'augmentent pas la complexité globale : tout retrait effectué dans ces boucles est compensé par l'ajout précédent. C'est le même raisonnement que pour la pile monotone, étendu à une file à double extrémité qui permet les retraits aux deux extrémités.
from collections import deque
def sliding_window_max_instrumented(nums, k):
dq = deque()
result = []
front_pops = back_pops = pushes = 0
for i, n in enumerate(nums):
while dq and dq[0] <= i - k:
dq.popleft(); front_pops += 1
while dq and nums[dq[-1]] <= n:
dq.pop(); back_pops += 1
dq.append(i); pushes += 1
if i >= k - 1:
result.append(nums[dq[0]])
print(f'n={len(nums)}: pushes={pushes}, front_pops={front_pops}, back_pops={back_pops}')
print(f'Total deque ops = {pushes + front_pops + back_pops} <= 3n = {3*len(nums)}')
return result
import random; random.seed(0)
nums = [random.randint(-100, 100) for _ in range(20)]
sliding_window_max_instrumented(nums, 5)Minimum d'une fenêtre glissante
Le minimum d'une fenêtre glissante est le pendant symétrique : maintenez une file monotone croissante à double extrémité (effectuez un pop à la fin lorsque le nouvel élément est plus petit que celui situé à la fin). Le début contient toujours le minimum de la fenêtre courante. Toutes les autres étapes sont identiques à la version du maximum — il suffit d'inverser le sens de la comparaison.
Les problèmes qui demandent le minimum d'une fenêtre glissante apparaissent souvent comme des sous-problèmes au sein d'algorithmes plus vastes. Par exemple, le coût minimal pour déplacer des marchandises le long d'un itinéraire comportant k arrêts intermédiaires peut nécessiter le minimum d'une fenêtre glissante sur des tableaux DP.
from collections import deque
def sliding_window_min(nums, k):
dq = deque() # increasing monotonic deque
result = []
for i, n in enumerate(nums):
while dq and dq[0] <= i - k:
dq.popleft() # expired
while dq and nums[dq[-1]] >= n:
dq.pop() # pop larger values from back
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]]) # front = min
return result
nums = [1, 3, -1, -3, 5, 3, 6, 7]
print('Max k=3:', sliding_window_min.__name__, '->', end=' ')
print(sliding_window_min(nums, 3)) # [-1, -3, -3, -3, 3, 3]
from collections import deque
def sliding_window_max(nums, k):
dq = deque(); result = []
for i, n in enumerate(nums):
while dq and dq[0] <= i-k: dq.popleft()
while dq and nums[dq[-1]] <= n: dq.pop()
dq.append(i)
if i >= k-1: result.append(nums[dq[0]])
return result
print('Max k=3:', sliding_window_max(nums, 3)) # [3,3,5,5,6,7]Problème des sauts VI : DP avec une file monotone à double extrémité
Le problème des sauts VI (LeetCode 1696) est un exemple classique de combinaison de DP et d'une file monotone à double extrémité. Étant donné un tableau et une longueur maximale de saut k, en partant de l'indice 0, à chaque étape vous avancez de 1 à k positions et ajoutez le score de la case d'arrivée. Maximisez le score total. La récurrence DP est dp[i] = nums[i] + max(dp[i-k], ..., dp[i-1]). Un maximum sur une fenêtre glissante du tableau DP donne un total en O(n).
Ce modèle — une récurrence DP où chaque case dépend du maximum d'une fenêtre de taille fixe parmi les cases précédentes — apparaît fréquemment et nécessite toujours une file monotone à double extrémité.
from collections import deque
def max_result(nums, k):
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
dq = deque([0]) # indices of max dp values in current window
for i in range(1, n):
# Remove expired indices
while dq and dq[0] < i - k:
dq.popleft()
# dp[i] = nums[i] + max dp in window [i-k, i-1]
dp[i] = nums[i] + dp[dq[0]]
# Maintain decreasing deque on dp values
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
dq.append(i)
return dp[n - 1]
print(max_result([1,-1,-2,4,-7,3], 2)) # 7: path 1->4->3
print(max_result([10,-5,-2,4,0,3], 3)) # 17: path 10->4->3
print(max_result([1,-5,-20,4,-1,3,-6,-3], 2)) # 0Maximum d'une fenêtre glissante : alternative avec un arbre de segments
Pour les problèmes où la taille de la fenêtre varie (et n'est pas fixée à k), la file monotone à double extrémité ne s'applique pas directement. Utilisez plutôt une table clairsemée pour les requêtes statiques de maximum sur des plages, en O(1) par query après un prétraitement en O(n log n), ou un arbre de segments pour les mises à jour dynamiques, avec O(log n) par query. Toutefois, pour les fenêtres glissantes de taille k fixe, la file est imbattable en O(n).
Lors des entretiens, préférez toujours la file monotone à double extrémité en O(n) à l'arbre de segments en O(n log n) lorsque la taille de la fenêtre est constante. Mentionnez le compromis : la file ne peut pas gérer des tailles de fenêtre ou des mises à jour arbitraires, tandis que les arbres de segments le peuvent.
# Sparse table for static RMQ (range maximum query)
import math
def build_sparse_table(arr):
n = len(arr)
LOG = int(math.log2(n)) + 1 if n else 1
table = [[0]*n for _ in range(LOG)]
table[0] = arr[:]
j = 1
while (1 << j) <= n:
for i in range(n - (1 << j) + 1):
table[j][i] = max(table[j-1][i], table[j-1][i + (1 << (j-1))])
j += 1
return table
def query(table, l, r):
k = int(math.log2(r - l + 1))
return max(table[k][l], table[k][r - (1 << k) + 1])
arr = [1, 3, -1, -3, 5, 3, 6, 7]
table = build_sparse_table(arr)
k = 3
result = [query(table, i, i + k - 1) for i in range(len(arr) - k + 1)]
print('Sparse table result:', result) # [3, 3, 5, 5, 6, 7]Plus long sous-tableau de 1 après suppression d'un élément
LeetCode 1493 : étant donné un tableau binaire, trouvez la longueur du plus long sous-tableau de 1 après avoir supprimé exactement un élément (qui peut être un 0 ou un 1). Il s'agit d'un problème de fenêtre glissante. Maintenez une fenêtre contenant au plus un 0. Lorsque la fenêtre contient plus d'un 0, réduisez-la par la gauche.
Ce problème utilise le modèle de fenêtre glissante de taille variable, et non une file à double extrémité. Toutefois, en l'associant à la technique de la fenêtre maximale : après avoir trouvé toutes les fenêtres valides, la longueur maximale est la réponse. La suppression d'un élément signifie que nous autorisons exactement un 0 dans notre fenêtre de 1.
def longest_subarray(nums):
left = 0
zeros = 0
max_len = 0
for right in range(len(nums)):
if nums[right] == 0:
zeros += 1
while zeros > 1:
if nums[left] == 0:
zeros -= 1
left += 1
# Window [left, right] has at most 1 zero
# After deleting one element, length = right - left (not +1, since we delete one)
max_len = max(max_len, right - left)
return max_len
print(longest_subarray([1,1,0,1])) # 3: delete the 0
print(longest_subarray([0,1,1,1,0,1,1,0,1])) # 5
print(longest_subarray([1,1,1])) # 2: must delete one 1Comparaison entre file à double extrémité, file et pile
Comprendre quand utiliser chaque conteneur est essentiel pour les entretiens :
- Pile (liste) : LIFO, accès à une seule extrémité. À utiliser pour DFS, l'analyse syntaxique d'expressions et les problèmes de piles monotones.
- File (file à double extrémité avec appendleft/popleft) : FIFO, ajout à une extrémité et retrait à l'autre. À utiliser pour BFS et la planification des tâches.
- File à double extrémité : les deux extrémités sont accessibles en O(1). À utiliser pour les fenêtres glissantes avec expiration (retrait au début) et pour l'invariant monotone (retrait à la fin). Le maximum d'une fenêtre glissante est le problème classique des files à double extrémité.
collections.deque de Python est l'outil adapté aux trois cas. Utilisez append/pop pour le comportement d'une pile et append/popleft ou appendleft/pop pour le comportement d'une file ou d'une file à double extrémité.
from collections import deque
# deque as stack
stack = deque()
stack.append(1); stack.append(2); stack.append(3)
print('Stack pop:', stack.pop()) # 3 (LIFO)
# deque as queue
queue = deque()
queue.append(1); queue.append(2); queue.append(3)
print('Queue pop:', queue.popleft()) # 1 (FIFO)
# deque as sliding window with front expiry + back monotonic
dq = deque()
nums = [3, 1, 4, 1, 5, 9, 2, 6]
k = 3
for i, n in enumerate(nums):
while dq and dq[0] <= i - k: dq.popleft() # expire front
while dq and nums[dq[-1]] <= n: dq.pop() # maintain back
dq.append(i)
if i >= k - 1:
print(f'Window {nums[max(0,i-k+1):i+1]}: max={nums[dq[0]]}')Plus court sous-tableau dont la somme est au moins K : file à double extrémité + sommes préfixes
Plus court sous-tableau dont la somme est au moins K (LeetCode 862) est un problème avancé qui combine les sommes préfixes et une file monotone à double extrémité. Construisez les sommes préfixes, puis utilisez une file pour trouver, pour chaque extrémité droite, la somme préfixe la plus à gauche qui satisfait prefix[right] - prefix[left] >= k. La file conserve des sommes préfixes croissantes (effectuez un pop à la fin pour conserver l'ordre croissant) et effectue des pops au début pour recueillir les réponses valides.
C'est l'un des problèmes de fenêtre glissante les plus difficiles, car il implique des nombres négatifs (ce qui exclut une simple méthode à deux pointeurs) et exige que la file serve à la fois de structure monotone et de mécanisme d'expiration.
from collections import deque
def shortest_subarray(nums, k):
n = len(nums)
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + nums[i]
dq = deque() # monotonic increasing deque of indices into prefix
result = float('inf')
for right in range(n + 1):
# Pop from front: valid subarrays ending at `right`
while dq and prefix[right] - prefix[dq[0]] >= k:
result = min(result, right - dq.popleft())
# Pop from back: maintain increasing deque
while dq and prefix[dq[-1]] >= prefix[right]:
dq.pop()
dq.append(right)
return result if result != float('inf') else -1
print(shortest_subarray([1], 1)) # 1
print(shortest_subarray([1, 2], 4)) # -1
print(shortest_subarray([2, -1, 2], 3)) # 3
print(shortest_subarray([84,-37,32,40,95], 167)) # 3Stratégie d'entretien pour les problèmes de files à double extrémité
Identifiez un problème de file monotone à double extrémité grâce aux indices suivants : (1) vous avez besoin du maximum ou du minimum d'une fenêtre glissante de taille fixe, (2) vous avez besoin d'une récurrence DP dp[i] = f(nums[i], max(dp[i-k..i-1])), ou (3) vous avez besoin de l'indice valide le plus proche qui satisfait une condition monotone.
Lors des entretiens, codez proprement la solution avec une file : importez deque, maintenez les deux invariants (expiration au début, monotonie à la fin) et renvoyez les résultats à partir de l'indice k-1. Mentionnez toujours la complexité temporelle O(n) et l'espace O(k) occupé par la file (au plus k indices stockés simultanément), puis comparez-la à la force brute en O(nk) pour montrer l'amélioration.
from collections import deque
# Clean, interview-ready template
def sliding_window_max_template(nums, k):
if not nums or k == 0:
return []
dq = deque() # monotonic decreasing, stores indices
result = []
for i in range(len(nums)):
# Invariant 1: remove expired indices (outside window)
while dq and dq[0] < i - k + 1:
dq.popleft()
# Invariant 2: remove indices with smaller values (useless)
while dq and nums[dq[-1]] < nums[i]:
dq.pop()
dq.append(i)
# Record result once first full window is established
if i >= k - 1:
result.append(nums[dq[0]])
return result
# Complexity: O(n) time, O(k) space
print(sliding_window_max_template([1,3,-1,-3,5,3,6,7], 3))
print(sliding_window_max_template([1], 1))
print(sliding_window_max_template([], 3))Vérification rapide
Évaluez votre compréhension des concepts de Structures de données & algorithmes — préparation aux entretiens de programmation de cette leçon.
Récapitulatif de la leçon
Dans cette leçon, vous avez appris : une file monotone décroissante à double extrémité conserve le maximum de la fenêtre à son début tout en éliminant de sa fin les éléments plus petits que les nouveaux arrivants, les indices expirés sont retirés du début lorsqu'ils sortent des limites de la fenêtre, et chaque indice est ajouté puis retiré au plus une fois, ce qui donne O(n) au total avec un espace O(k) pour la file. Ensuite, vous allez résoudre le problème de la rétention d'eau de pluie en utilisant à la fois la pile monotone et l'approche à deux pointeurs.
Questions Fréquemment Posées
La leçon « Maximum dans une fenêtre glissante avec une deque monotone » est-elle gratuite ?
Oui — le texte complet de « Maximum dans une fenêtre glissante avec une deque monotone » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DSA Interview Prep, passe à CoddyKit PRO. Le cours DSA Interview Prep comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Maximum dans une fenêtre glissante avec une deque monotone » ?
Maintenez une deque décroissante d’indices pour répondre en O(1) par élément aux requêtes de maximum dans une fenêtre et résoudre le problème sliding-window-maximum en O(n). Tu pratiques DSA Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer DSA Interview Prep ?
Aucune expérience préalable n'est requise. DSA Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.
Combien de temps prend la leçon « Maximum dans une fenêtre glissante avec une deque monotone » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon DSA Interview Prep ?
Oui. Chaque leçon DSA Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Pile monotone : croissante ou décroissante
- Plus grand rectangle dans un histogramme
- Maximum dans une fenêtre glissante avec une deque monotone
- Accumulation d’eau de pluie : pile et deux pointeurs