LeetCode 84. Largest Rectangle in Histogram

📊 결과


💻 내 코드

class Solution {
    public int largestRectangleArea(int[] heights) {
        Deque<Integer> heightStack = new ArrayDeque<Integer>();
        Deque<Integer> widthStack = new ArrayDeque<Integer>();
        Deque<Integer> stack = new ArrayDeque<Integer>();
        int max =0;
        int memo = 0;

        if(heights.length==1){
            return heights[0]; 
        }
        for(int i=0; i<heights.length; i++){
            int curr = heights[i];
            int jump = 0;
            if(!heightStack.isEmpty()){
                int prev = heightStack.peek();
              
                if(curr==prev && i!=heights.length-1){
                     memo++;
                }else{

                    if(curr<prev){
                        while(!heightStack.isEmpty()&& curr<prev){
                            int height = heightStack.pop();
                            int width = widthStack.pop()+memo;
                            max = Math.max(max, height * width);
                            if(!heightStack.isEmpty()){
                                prev = heightStack.peek();
                            }else{
                                prev = -1;
                            }
                            jump = width;
                        }
                    }

                    while(!widthStack.isEmpty()){
                        stack.push(widthStack.pop()+1+memo);
                    }
                    while(!stack.isEmpty()){
                        int module = stack.pop();
                        widthStack.push(module);
                    }
                    if(curr!=prev){
                        heightStack.push(curr);
                        widthStack.push(jump+1);
 
                    }
                    memo = 0;
                }

            }else{
                    heightStack.push(curr);
                    widthStack.push(1);
            }

        }

         while(!heightStack.isEmpty()){
            int h = heightStack.pop();
            int w = widthStack.pop();

            max=Math.max(max,h*w);
        }

    
        return max;

    }
}

📝 평가

✔ 잘한 점

  1. 단조 스택 접근: 스택을 활용하려는 시도
  2. 높이와 너비 추적: 별도 스택으로 정보 관리 시도
  3. 엣지 케이스 처리: 길이가 1인 경우 처리
  4. 끈기: 복잡한 문제를 끝까지 구현

✘ 심각한 문제점

  1. 과도한 복잡도:

    • 3개의 Deque 사용 (heightStack, widthStack, stack)
    • 불필요한 while 루프가 중첩
    • 849ms → 정상 풀이의 약 100배 느림
  2. 잘못된 로직:

     while(!widthStack.isEmpty()){
         stack.push(widthStack.pop()+1+memo);
     }
     while(!stack.isEmpty()){
         int module = stack.pop();
         widthStack.push(module);
     }
    
    • 이 부분이 매번 전체 스택을 재구성 → O(n²) 시간
  3. 복잡한 상태 관리:

    • memo, jump 변수의 역할 불명확
    • 같은 높이 처리 로직이 복잡함
  4. 불필요한 분기:

    • if(curr==prev && i!=heights.length-1) 조건이 불필요

✨ 최적화된 풀이

방법 1: 단조 증가 스택 (추천 ⭐⭐⭐⭐⭐)

핵심 아이디어: 인덱스만 저장하고, 높이가 감소할 때 면적 계산

class Solution {
    public int largestRectangleArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<>();
        int maxArea = 0;
        int n = heights.length;
        
        for (int i = 0; i < n; i++) {
            // 현재 높이가 스택의 높이보다 작으면
            while (!stack.isEmpty() && heights[stack.peek()] > heights[i]) {
                int height = heights[stack.pop()];
                int width = stack.isEmpty() ? i : i - stack.peek() - 1;
                maxArea = Math.max(maxArea, height * width);
            }
            stack.push(i);
        }
        
        // 스택에 남은 인덱스들 처리
        while (!stack.isEmpty()) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? n : n - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        
        return maxArea;
    }
}

시간복잡도: O(n) 공간복잡도: O(n) Runtime: 8-10ms ✔


방법 2: Sentinel 값 추가 (더 깔끔)

class Solution {
    public int largestRectangleArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1);  // Sentinel
        int maxArea = 0;
        
        for (int i = 0; i < heights.length; i++) {
            while (stack.peek() != -1 && 
                   heights[stack.peek()] > heights[i]) {
                int height = heights[stack.pop()];
                int width = i - stack.peek() - 1;
                maxArea = Math.max(maxArea, height * width);
            }
            stack.push(i);
        }
        
        while (stack.peek() != -1) {
            int height = heights[stack.pop()];
            int width = heights.length - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        
        return maxArea;
    }
}

개선점:


방법 3: 배열에 0 추가 (가장 간결)

class Solution {
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        // 끝에 0을 추가하여 모든 막대 처리 보장
        int[] newHeights = new int[n + 1];
        System.arraycopy(heights, 0, newHeights, 0, n);
        
        Deque<Integer> stack = new ArrayDeque<>();
        int maxArea = 0;
        
        for (int i = 0; i <= n; i++) {
            while (!stack.isEmpty() && 
                   newHeights[stack.peek()] > newHeights[i]) {
                int height = newHeights[stack.pop()];
                int width = stack.isEmpty() ? i : i - stack.peek() - 1;
                maxArea = Math.max(maxArea, height * width);
            }
            stack.push(i);
        }
        
        return maxArea;
    }
}

방법 4: Divide and Conquer (이해용)

class Solution {
    public int largestRectangleArea(int[] heights) {
        return calculateArea(heights, 0, heights.length - 1);
    }
    
    private int calculateArea(int[] heights, int start, int end) {
        if (start > end) return 0;
        
        // 최소 높이 인덱스 찾기
        int minIndex = start;
        for (int i = start; i <= end; i++) {
            if (heights[i] < heights[minIndex]) {
                minIndex = i;
            }
        }
        
        // 최소 높이로 만들 수 있는 직사각형
        int currentArea = heights[minIndex] * (end - start + 1);
        
        // 왼쪽과 오른쪽 분할 정복
        int leftArea = calculateArea(heights, start, minIndex - 1);
        int rightArea = calculateArea(heights, minIndex + 1, end);
        
        return Math.max(currentArea, Math.max(leftArea, rightArea));
    }
}

시간복잡도: 평균 O(n log n), 최악 O(n²)


📊 성능 비교

방법 시간복잡도 공간복잡도 Runtime Memory 가독성 추천도
원본 코드 O(n²) O(n) 849ms 59.88MB
단조 스택 O(n) O(n) 8-10ms 56MB ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Sentinel O(n) O(n) 8-10ms 56MB ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
0 추가 O(n) O(n) 8-10ms 56.5MB ⭐⭐⭐⭐ ⭐⭐⭐⭐
D&C O(n log n) ~ O(n²) O(log n) 20-200ms 55MB ⭐⭐⭐ ⭐⭐

개선 효과: 849ms → 8ms = 약 100배 빠름! 🚀


💡 핵심 인사이트

알고리즘 이해

문제: heights = [2, 1, 5, 6, 2, 3]

단조 스택 처리 과정:

i=0: h=2
  stack: [0]
  
i=1: h=1 < 2
  pop(0): height=2, width=1, area=2
  stack: [1]
  
i=2: h=5
  stack: [1, 2]
  
i=3: h=6
  stack: [1, 2, 3]
  
i=4: h=2 < 6
  pop(3): height=6, width=1 (4-2-1), area=6
  pop(2): height=5, width=2 (4-1-1), area=10 ✓
  stack: [1, 4]
  
i=5: h=3
  stack: [1, 4, 5]
  
끝: 스택 정리
  pop(5): height=3, width=2 (6-4-1), area=6
  pop(4): height=2, width=4 (6-1-1), area=8
  pop(1): height=1, width=6, area=6

최대 면적: 10


왜 이 방법이 O(n)인가?

각 인덱스는:
- 정확히 1번 push
- 최대 1번 pop

총 연산: 2n = O(n)

핵심:

“스택에 남아있는 막대는 항상 증가하는 높이 순서”


너비 계산 공식

int width = stack.isEmpty() ? i : i - stack.peek() - 1;

설명:

예시:

heights = [2, 1, 5, 6, 2, 3]
           0  1  2  3  4  5

i=4에서 h=6을 pop:
- i = 4 (현재 위치)
- stack.peek() = 2 (인덱스 2의 h=5)
- width = 4 - 2 - 1 = 1 ✓

배운 점

  1. 과도한 설계는 독

    • 3개 스택 → 1개 스택으로 충분
    • 복잡한 상태 관리 불필요
  2. 단조 스택의 본질

     단조 증가 스택 유지
     → 감소하는 순간 = 직사각형 계산 시점
    
  3. 인덱스의 힘

    • 값 대신 인덱스 저장
    • 너비 계산 용이
    • 원본 배열 참조 가능
  4. Sentinel의 활용

    • 경계 조건 간소화
    • if-else 분기 제거

🎯 개선 후 코드

추천: Sentinel 방식 (가독성과 성능 모두 우수)

class Solution {
    public int largestRectangleArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1);  // Sentinel: 왼쪽 경계 역할
        int maxArea = 0;
        
        for (int i = 0; i < heights.length; i++) {
            // 현재 높이가 스택 top보다 낮으면
            // 스택의 막대들로 만들 수 있는 직사각형 계산
            while (stack.peek() != -1 && 
                   heights[stack.peek()] > heights[i]) {
                int height = heights[stack.pop()];
                int width = i - stack.peek() - 1;
                maxArea = Math.max(maxArea, height * width);
            }
            stack.push(i);
        }
        
        // 스택에 남은 막대들 처리
        while (stack.peek() != -1) {
            int height = heights[stack.pop()];
            int width = heights.length - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        
        return maxArea;
    }
}

/**
 * 시간복잡도: O(n) - 각 막대는 1번 push, 최대 1번 pop
 * 공간복잡도: O(n) - 스택 크기
 * 
 * Runtime: 8-10ms (상위 95%)
 * Memory: 56MB
 */

개선 효과:


📚 관련 개념

알고리즘 패턴

연관 개념

  1. 단조 증가 스택

     스택: [인덱스들]
     heights[스택의 인덱스들]은 증가 순서
        
     감소하는 순간 = 직사각형 완성
    
  2. 실무 활용

    • 건축 설계: 최대 공간 활용
    • 데이터 시각화: 차트 영역 계산
    • 이미지 처리: 최대 직사각형 탐지
    • 자원 할당: 최적 구간 찾기
  3. 관련 문제 패턴

    • Largest Rectangle
    • Maximal Rectangle (2D)
    • Trapping Rain Water

🎓 다음 단계

비슷한 문제

  1. LeetCode 85. Maximal Rectangle ⭐⭐⭐⭐⭐

    • 2D 배열에서 최대 직사각형 (이 문제의 확장)
  2. LeetCode 42. Trapping Rain Water ⭐⭐⭐⭐

    • 유사한 단조 스택 활용
  3. LeetCode 496. Next Greater Element I ⭐⭐

    • 단조 스택 기초
  4. LeetCode 739. Daily Temperatures ⭐⭐

    • 단조 스택 입문
  5. LeetCode 221. Maximal Square ⭐⭐⭐

    • DP 접근

심화 학습

  1. 2D 히스토그램 (#85)로 확장
  2. Divide and Conquer 버전 구현
  3. Segment Tree 활용 방법

연습 포인트 체크리스트


🔍 단계별 디버깅 예시

입력: [2, 1, 5, 6, 2, 3]

// 각 단계 출력
i=0, h=2: stack=[0], maxArea=0
i=1, h=1: pop 0, area=2*1=2, stack=[1], maxArea=2
i=2, h=5: stack=[1,2], maxArea=2
i=3, h=6: stack=[1,2,3], maxArea=2
i=4, h=2: 
  - pop 3, h=6, w=1, area=6, maxArea=6
  - pop 2, h=5, w=2, area=10, maxArea=10 
  - stack=[1,4], maxArea=10
i=5, h=3: stack=[1,4,5], maxArea=10

끝:
  - pop 5, h=3, w=2, area=6
  - pop 4, h=2, w=4, area=8
  - pop 1, h=1, w=6, area=6

답: 10

💭 문제 해결 사고 과정

Brute Force (시간초과):

for (int i = 0; i < n; i++) {
    int minHeight = heights[i];
    for (int j = i; j < n; j++) {
        minHeight = Math.min(minHeight, heights[j]);
        maxArea = Math.max(maxArea, minHeight * (j - i + 1));
    }
}
// O(n²)

최적화 사고:

  1. “모든 구간 확인? 중복 계산 많음”
  2. “각 막대를 높이로 하는 최대 직사각형만 찾으면?”
  3. “왼쪽/오른쪽으로 확장 가능한 범위는?”
  4. “단조 스택으로 범위 추적!”

🏷️ Keywords

#MonotonicStack #Histogram #LargestRectangle #Stack #DynamicProgramming #단조스택#히스토그램 #면적최대화 #Deque #IndexTracking #O(n)Algorithm #Sentinel
#LeetCodeHard#코딩테스트 #Hard #최적화