단조 스택 (Monotonic Stack)

스택의 원소를 단조 증가 또는 단조 감소로 유지해 Next Greater Element, 히스토그램 최대 넓이 등을 O(n)에 해결하는 기법을 설명합니다.

· 5 min read · PALDYN Team

지난 글에서 덱의 슬라이딩 윈도우 응용을 다뤘습니다. 이번에는 스택에 특수한 불변식(invariant)을 부여해 강력한 문제들을 선형 시간에 해결하는 **단조 스택(Monotonic Stack)**을 소개합니다. 이름이 생소하게 들릴 수 있지만, Next Greater Element 같은 고전 문제부터 히스토그램 최대 넓이까지 다양한 면접 단골 문제의 핵심 기법입니다.

단조 스택이란

일반 스택에 항상 단조 증가(또는 단조 감소) 순서를 유지하는 불변식을 추가한 것입니다.

  • 단조 감소 스택: 스택 바닥→top으로 갈수록 값이 감소 (top이 가장 작음)
  • 단조 증가 스택: 스택 바닥→top으로 갈수록 값이 증가 (top이 가장 큼)

새 원소를 push할 때 불변식이 깨지면, 깨지지 않을 때까지 pop합니다.

# 단조 감소 스택 유지
stack = []
for val in arr:
    while stack and stack[-1] < val:   # top이 현재값보다 작으면 pop
        stack.pop()
    stack.append(val)

응용 1 — Next Greater Element (NGE)

배열의 각 원소에 대해 오른쪽에서 처음 만나는 더 큰 값을 구합니다. 브루트 포스는 O(n²)이지만 단조 스택으로 **O(n)**에 해결됩니다.

단조 스택 — NGE

핵심 관찰: 새 원소 x가 스택 top보다 크면, top이 기다리던 “오른쪽의 첫 큰 값”이 x임을 의미합니다.

def next_greater(arr: list) -> list:
    n = len(arr)
    nge = [-1] * n      # 기본값 -1 (없음)
    stack = []          # 인덱스 저장

    for i, val in enumerate(arr):
        while stack and arr[stack[-1]] < val:
            idx = stack.pop()
            nge[idx] = val      # idx의 NGE는 val
        stack.append(i)

    return nge

print(next_greater([2, 1, 5, 3, 6, 4]))
# [5, 5, 6, 6, -1, -1]

응용 2 — 히스토그램 최대 직사각형

가로 폭 1짜리 막대들로 이루어진 히스토그램에서 만들 수 있는 가장 넓은 직사각형 넓이를 구합니다.

히스토그램 최대 직사각형

def largest_rectangle(heights: list) -> int:
    heights = heights + [0]   # sentinel: 끝에 0 추가해 잔여 원소 처리
    stack = []                 # 단조 증가 스택 (인덱스)
    max_area = 0

    for i, h in enumerate(heights):
        while stack and heights[stack[-1]] >= h:
            top = stack.pop()
            # 너비: 현재 인덱스 - 스택의 새 top - 1
            width = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, heights[top] * width)
        stack.append(i)

    return max_area

print(largest_rectangle([2, 1, 5, 6, 2, 3]))  # 10

응용 3 — 빗물 가두기 (Trapping Rain Water)

높이 배열이 주어졌을 때, 막대 사이에 고이는 총 빗물 양을 구합니다.

def trap_rain(height: list) -> int:
    stack = []
    water = 0

    for i, h in enumerate(height):
        while stack and height[stack[-1]] < h:
            bottom = stack.pop()
            if not stack:
                break
            left = stack[-1]
            width = i - left - 1
            bounded_h = min(height[left], h) - height[bottom]
            water += bounded_h * width
        stack.append(i)

    return water

print(trap_rain([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))  # 6

단조 스택 선택 가이드

문제 유형스택 종류pop 조건
오른쪽 첫 큰 값 (NGE)단조 감소top < 현재값
오른쪽 첫 작은 값 (NSE)단조 증가top > 현재값
왼쪽 첫 큰 값단조 감소 + 역순top < 현재값
히스토그램 최대 넓이단조 증가top >= 현재값

시간/공간 복잡도

시간: O(n) — 각 원소는 스택에 최대 1번 push, 1번 pop
공간: O(n) — 최악의 경우 스택에 n개 원소 (단조 정렬된 배열)

나이브 이중 루프 O(n²)를 O(n)으로 개선하는 핵심 기법이므로, 면접에서 “O(n)으로 풀 수 있냐”는 질문이 나오면 단조 스택을 가장 먼저 떠올려야 합니다.

정리

  • 단조 스택은 스택의 단조성 불변식을 유지해 선형 시간 내에 강력한 문제들을 해결합니다.
  • NGE/NSE, 히스토그램 넓이, 빗물 가두기는 모두 O(n) 단조 스택 패턴으로 풀립니다.
  • 각 원소의 push/pop이 최대 1회이므로 전체 복잡도는 항상 O(n)입니다.

지난 글:

다음 글: 스킵 리스트


읽어주셔서 감사합니다. 😊