단조 덱을 사용한 슬라이딩 윈도우 최댓값
인덱스의 감소형 덱을 유지해 원소당 O(1)에 윈도우 최댓값 질의에 답하고, O(n) 시간에 슬라이딩 윈도우 최댓값 문제를 해결합니다.
단조 덱을 사용한 슬라이딩 윈도우 최댓값은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
슬라이딩 윈도 최댓값 문제
슬라이딩 윈도 최댓값 문제(LeetCode 239)는 배열과 윈도 크기 k를 제공합니다. 윈도가 왼쪽에서 오른쪽으로 한 칸씩 이동할 때 각 윈도의 최댓값을 출력하세요. 완전 탐색 방식은 k개의 원소로 이루어진 각 윈도의 최댓값을 O(k)에 계산하므로 전체 O(nk)가 되며, k가 큰 경우에는 너무 느립니다.
단조 덱(양방향 대기열) 해법은 인덱스의 감소 덱을 유지하여 전체 O(n)을 달성합니다. 앞쪽에는 항상 현재 윈도의 최댓값 인덱스가 있으므로 O(1)에 최댓값을 질의할 수 있고, 앞과 뒤 양쪽에서 연산할 수 있습니다.
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))단조 덱: 핵심 아이디어
단조 감소 덱에는 값이 아니라 인덱스를 저장하고 유지하세요. 불변식은 nums[deque[0]] >= nums[deque[1]] >= ... >= nums[deque[-1]]입니다. 인덱스 i를 추가하기 전에 다음을 수행합니다.
- 만료된 인덱스 제거: 앞쪽에서 제거합니다.
deque[0] <= i - k이면 해당 인덱스는 윈도를 벗어난 것입니다. - 더 작은 인덱스 제거: 뒤쪽에서 제거합니다.
nums[deque[-1]] <= nums[i]인 동안 해당 인덱스들은 앞으로 어떤 윈도에서도 최댓값이 될 수 없으므로 버립니다(왼쪽에 있고 더 작기 때문입니다).
이 연산을 마친 뒤 i를 뒤쪽에 넣으세요. 앞쪽에는 항상 현재 윈도의 최댓값이 있습니다.
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]덱을 단계별로 추적하기
k=3일 때 [1, 3, -1, -3, 5, 3, 6, 7]을 추적해 보겠습니다.
- i=0 (1): dq=[0]
- i=1 (3): pop 0 (1<3), dq=[1]
- i=2 (-1): -1<3이므로 유지, dq=[1,2]. 윈도 [1,3,-1], 최댓값=nums[1]=3
- i=3 (-3): -3<-1, dq=[1,2,3]. 앞쪽 확인: 1 > 3-3=0, OK. 윈도 최댓값=3
- i=4 (5): pop 3,2,1 (모두 더 작음), dq=[4]. 앞쪽 4 > 4-3=1, OK. 최댓값=5
- i=5 (3): 3<5, dq=[4,5]. 앞쪽 4 > 5-3=2, OK. 최댓값=5
- i=6 (6): pop 5,4 (둘 다 더 작음), dq=[6]. 최댓값=6
- i=7 (7): pop 6, dq=[7]. 최댓값=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)각 원소가 최대 한 번씩 추가되고 제거되는 이유
O(n)이 보장되는 이유는 단조 스택에서와 같은 분할 상환 분석에 있습니다. 각 인덱스는 덱에 정확히 한 번 추가되고, 만료될 때 앞쪽에서 제거되거나 더 큰 원소로 대체될 때 뒤쪽에서 제거되며, 최대 한 번만 제거됩니다. 전체 반복문에서 덱 연산은 최대 2n번입니다.
내부 while 반복문은 전체 복잡도를 증가시키지 않습니다. 이 반복문에서 수행되는 pop은 앞서 원소를 추가할 때 이미 비용을 치른 것으로 볼 수 있기 때문입니다. 단조 스택과 같은 논리이지만, 양쪽 끝에서 제거할 수 있는 덱으로 확장한 것입니다.
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)슬라이딩 윈도 최솟값
슬라이딩 윈도 최솟값은 대칭적인 대응 개념입니다. 단조 증가 덱을 유지하고, 새 원소가 뒤쪽 원소보다 작으면 뒤쪽에서 pop합니다. 앞쪽에는 항상 현재 윈도의 최솟값이 있습니다. 나머지 단계는 최댓값을 구하는 방식과 동일하며, 비교 방향만 반대로 바꾸면 됩니다.
슬라이딩 윈도 최솟값을 묻는 문제는 더 큰 알고리즘 안의 하위 문제로 자주 등장합니다. 예를 들어 중간 정류장을 k개 거쳐 경로를 따라 물건을 옮기는 최소 비용을 구하려면 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]점프 게임 VI: 단조 덱을 사용한 DP
점프 게임 VI(LeetCode 1696)는 DP와 단조 덱을 결합하는 대표적인 예입니다. 배열과 최대 점프 크기 k가 주어지고, 인덱스 0에서 시작하여 매번 1~k칸 앞으로 점프하면서 도착한 칸의 점수를 더합니다. 전체 점수를 최대로 만드세요. DP 점화식은 dp[i] = nums[i] + max(dp[i-k], ..., dp[i-1])입니다. DP 배열에 슬라이딩 윈도 최댓값을 적용하면 전체 O(n)에 해결할 수 있습니다.
각 칸이 고정 크기 윈도에 있는 이전 칸들의 최댓값에 의존하는 DP 점화식이라는 이 패턴은 자주 등장하며, 항상 단조 덱을 사용해야 합니다.
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)) # 0슬라이딩 윈도 최댓값: 세그먼트 트리 대안
윈도 크기가 고정된 k가 아니라 가변적인 문제에는 단조 덱을 직접 적용할 수 없습니다. 대신 전처리에 O(n log n)을 사용하고 각 질의에 O(1)이 걸리는 정적 구간 최댓값 질의에는 희소 테이블을 사용하고, O(log n)의 질의 시간으로 동적 갱신을 처리하려면 세그먼트 트리를 사용하세요. 하지만 k가 고정된 슬라이딩 윈도에서는 O(n)의 덱을 능가할 수 없습니다.
면접에서는 윈도 크기가 일정할 때 O(n log n)의 세그먼트 트리보다 O(n)의 단조 덱을 항상 우선하세요. 다음과 같은 장단점도 언급하세요. 덱은 임의의 윈도 크기나 갱신을 처리할 수 없지만, 세그먼트 트리는 처리할 수 있습니다.
# 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]원소 하나를 삭제한 후의 가장 긴 1 부분 배열
LeetCode 1493: 이진 배열이 주어질 때, 원소를 정확히 하나 삭제한 후(0 또는 1을 삭제할 수 있음) 1로 이루어진 가장 긴 부분 배열의 길이를 구하세요. 이는 슬라이딩 윈도 문제입니다. 0을 최대 하나 포함하는 윈도를 유지하세요. 윈도에 0이 두 개보다 많아지면 왼쪽에서 줄이세요.
이 문제는 덱이 아니라 가변 크기 슬라이딩 윈도 패턴을 사용합니다. 그러나 이를 최대 윈도 기법과 결합할 수 있습니다. 유효한 모든 윈도를 찾은 뒤 최댓값인 길이가 답이 됩니다. '원소 하나를 삭제한다'는 조건은 1로 이루어진 윈도에 0을 정확히 하나 포함할 수 있게 한다는 뜻입니다.
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 1덱, 큐, 스택 비교
면접에서는 각 자료구조를 언제 사용할지 이해하는 것이 핵심입니다.
- 스택(list): LIFO, 한쪽 끝에서만 접근합니다. DFS, 표현식 구문 분석, 단조 스택 문제에 사용합니다.
- 큐(appendleft/popleft를 사용하는 덱): FIFO, 한쪽 끝에서 넣고 다른 쪽 끝에서 꺼냅니다. BFS, 작업 일정 관리에 사용합니다.
- 덱: 양쪽 끝에 O(1)로 접근할 수 있습니다. 만료 처리가 있는 슬라이딩 윈도(앞쪽에서 제거)와 단조 불변식(뒤쪽에서 제거)에 사용합니다. 슬라이딩 윈도 최댓값은 대표적인 덱 문제입니다.
파이썬의 collections.deque는 세 가지 모두에 사용하는 도구입니다. 스택 동작에는 append/pop을 사용하고, 큐나 덱 동작에는 append/popleft 또는 appendleft/pop을 사용하세요.
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]]}')K 이상 합을 갖는 가장 짧은 부분 배열: 덱과 누적 합
K 이상 합을 갖는 가장 짧은 부분 배열(LeetCode 862)은 누적 합과 단조 덱을 결합하는 고급 문제입니다. 누적 합을 만든 다음 덱을 사용하여 각 오른쪽 끝점에 대해 prefix[right] - prefix[left] >= k를 만족하는 가장 왼쪽 누적 합을 찾습니다. 덱은 누적 합을 증가하는 순서로 유지하고(증가 순서를 유지하기 위해 뒤쪽에서 제거), 유효한 답을 모으기 위해 앞쪽에서 pop합니다.
음수가 포함되므로 단순한 투 포인터를 사용할 수 없고, 덱이 단조 구조와 만료 처리 수단의 역할을 모두 해야 하기 때문에 가장 어려운 슬라이딩 윈도 문제 중 하나입니다.
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)) # 3덱 문제를 위한 면접 전략
다음 신호가 보이면 단조 덱 문제인지 확인하세요. (1) 고정 크기의 슬라이딩 윈도에서 최댓값 또는 최솟값이 필요하거나, (2) dp[i] = f(nums[i], max(dp[i-k..i-1])) 형태의 DP 점화식이 필요하거나, (3) 단조 조건을 만족하는 가장 가까운 유효 인덱스가 필요할 때입니다.
면접에서는 덱 해법을 깔끔하게 작성하세요. deque를 가져오고, 두 가지 불변식(앞쪽 만료 처리와 뒤쪽 단조성)을 유지한 뒤 인덱스 k-1부터 결과를 반환하세요. 항상 O(n) 시간 복잡도와 덱에 필요한 O(k) 공간(한 번에 최대 k개의 인덱스 저장)을 언급하고, O(nk) 완전 탐색과 비교하여 개선된 점을 보여 주세요.
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))빠른 확인
이 레슨에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보세요.
레슨 복습
이 레슨에서 배운 내용은 다음과 같습니다. 단조 감소 덱은 새로 들어온 원소보다 작은 원소를 뒤쪽에서 버리면서 앞쪽에 윈도 최댓값을 유지합니다. 만료된 인덱스는 윈도 경계 밖으로 벗어나면 앞쪽에서 제거됩니다. 또한 각 인덱스는 최대 한 번 추가되고 제거되므로 전체 시간 복잡도는 O(n)이고 덱 공간은 O(k)입니다. 다음에는 단조 스택과 투 포인터 방식을 모두 사용해 빗물을 가두는 문제를 해결합니다.
자주 묻는 질문
“단조 덱을 사용한 슬라이딩 윈도우 최댓값” 강의는 무료인가요?
네 — “단조 덱을 사용한 슬라이딩 윈도우 최댓값” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“단조 덱을 사용한 슬라이딩 윈도우 최댓값”에서 뭘 배우나요?
인덱스의 감소형 덱을 유지해 원소당 O(1)에 윈도우 최댓값 질의에 답하고, O(n) 시간에 슬라이딩 윈도우 최댓값 문제를 해결합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“단조 덱을 사용한 슬라이딩 윈도우 최댓값” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 단조 스택: 증가형과 감소형
- 히스토그램에서 가장 큰 직사각형
- 단조 덱을 사용한 슬라이딩 윈도우 최댓값
- 빗물 받기: 스택과 투 포인터