LeetCode 155. Min Stack

📊 결과


💻 내 코드

class MinStack {
    Deque<Integer> minStack;
    Deque<Integer> minDeque;
    Deque<Integer> stack;
    int min = 0;
    int last = 0;
    public MinStack() {
        this.minStack = new ArrayDeque<Integer>();
        this.minDeque = new ArrayDeque<Integer>();
        this.stack = new ArrayDeque<Integer>();
        this.min = 2147483647;
        minDeque.addLast(min);
    }
    
    public void push(int val) {
        
        if(val<=minDeque.peekLast()){
            minDeque.addLast(val);
        }else{
            while(!minDeque.isEmpty() && minDeque.peekLast()<val){
        
                stack.addLast(minDeque.removeLast());    
            }
            minDeque.addLast(val);

            while(!stack.isEmpty()){
                minDeque.addLast(stack.removeLast());         
            }
        }
        minStack.addLast(val);
    }
    
    public void pop() {
    
        int del = minStack.removeLast();

        if(del==minDeque.peekLast()){
            minDeque.removeLast();
        }else{
             while(!minDeque.isEmpty() && minDeque.peekLast()!=del){
                stack.addLast(minDeque.removeLast());    
            }
            minDeque.removeLast();
            while(!stack.isEmpty()){
                minDeque.addLast(stack.removeLast());         
            }
        }

    }
    
    public int top() {
        return minStack.peekLast();
    }
    
    public int getMin() {
        return minDeque.peekLast();
    }
}

📝 평가

✔ 잘한 점

  1. 문제 의도 파악: 최솟값을 O(1)에 찾아야 한다는 핵심 이해
  2. 별도 자료구조 시도: minDeque를 통해 최솟값 추적 시도
  3. 정답 도출: 느리지만 모든 테스트 케이스 통과
  4. 끈기: 40분간 복잡한 로직을 완성

✦ 개선점

  1. 심각한 성능 문제: 732ms (하위 5%)

    • push/pop 시마다 minDeque를 재정렬하는 과도한 연산
  2. 불필요한 복잡도:

    • 3개의 Deque 사용 (minStack, minDeque, stack)
    • while 루프로 재정렬 → O(n) 시간 소요
  3. 사용하지 않는 변수: min, last 선언 후 미사용

  4. 잘못된 접근:

    • minDeque를 정렬된 상태로 유지하려 함 → 불필요!
    • 최솟값만 추적하면 되는데 전체 순서 유지 시도
  5. 메모리 낭비:

    • 추가 Deque(stack) 사용
    • 불필요한 초기화 값

✨ 최적화된 풀이

방법 1: Two Stack 방법 (추천 ⭐⭐⭐⭐⭐)

핵심 아이디어: 각 시점의 최솟값만 저장하는 별도 스택 유지

class MinStack {
    private Deque<Integer> stack;
    private Deque<Integer> minStack;  // 각 시점의 최솟값 저장
    
    public MinStack() {
        stack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        stack.push(val);
        // 현재 val과 이전 최솟값 중 작은 값을 minStack에 push
        if (minStack.isEmpty()) {
            minStack.push(val);
        } else {
            minStack.push(Math.min(val, minStack.peek()));
        }
    }
    
    public void pop() {
        stack.pop();
        minStack.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

동작 예시:

push(-2):
  stack:    [-2]
  minStack: [-2]  ← 최솟값 -2

push(0):
  stack:    [-2, 0]
  minStack: [-2, -2]  ← 여전히 -2가 최소

push(-3):
  stack:    [-2, 0, -3]
  minStack: [-2, -2, -3]  ← 새로운 최솟값 -3

getMin() → -3

pop():
  stack:    [-2, 0]
  minStack: [-2, -2]  ← -3 제거, 이전 최솟값 -2로 복원

getMin() → -2

시간복잡도: 모든 연산 O(1) ✔ 공간복잡도: O(n)


방법 2: Single Stack with Pair (메모리 최적화)

class MinStack {
    private Deque<int[]> stack;  // [값, 현재까지의 최솟값]
    
    public MinStack() {
        stack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        if (stack.isEmpty()) {
            stack.push(new int[]{val, val});
        } else {
            int currentMin = Math.min(val, stack.peek()[1]);
            stack.push(new int[]{val, currentMin});
        }
    }
    
    public void pop() {
        stack.pop();
    }
    
    public int top() {
        return stack.peek()[0];
    }
    
    public int getMin() {
        return stack.peek()[1];
    }
}

방법 3: 최적화된 공간 - 변화 시에만 저장

아이디어: 최솟값이 바뀔 때만 minStack에 저장

class MinStack {
    private Deque<Integer> stack;
    private Deque<Integer> minStack;
    
    public MinStack() {
        stack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        stack.push(val);
        // 최솟값이 갱신될 때만 minStack에 push
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }
    
    public void pop() {
        int val = stack.pop();
        // pop된 값이 최솟값이었다면 minStack도 pop
        if (val == minStack.peek()) {
            minStack.pop();
        }
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

장점: 최솟값이 자주 바뀌지 않으면 minStack 크기가 작아짐


방법 4: Single Stack - 차이값 저장 (고급)

class MinStack {
    private Deque<Long> stack;
    private long min;
    
    public MinStack() {
        stack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        if (stack.isEmpty()) {
            stack.push(0L);
            min = val;
        } else {
            // 현재 최솟값과의 차이를 저장
            stack.push((long)val - min);
            if (val < min) {
                min = val;
            }
        }
    }
    
    public void pop() {
        long diff = stack.pop();
        if (diff < 0) {
            // 음수면 이전 최솟값 복원
            min = min - diff;
        }
    }
    
    public int top() {
        long diff = stack.peek();
        if (diff < 0) {
            return (int)min;
        }
        return (int)(min + diff);
    }
    
    public int getMin() {
        return (int)min;
    }
}

장점: 스택 1개만 사용 (공간 O(n)) 단점: Long 타입 필요 (overflow 방지), 이해하기 어려움


📊 성능 비교

방법 push pop top getMin 공간 Runtime 가독성 추천도
원본 코드 O(n) O(n) O(1) O(1) O(n) 732ms ⭐⭐
Two Stack O(1) O(1) O(1) O(1) O(2n) 4-5ms ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Pair Stack O(1) O(1) O(1) O(1) O(2n) 4-5ms ⭐⭐⭐⭐ ⭐⭐⭐⭐
최적화 공간 O(1) O(1) O(1) O(1) O(n)~O(2n) 4-5ms ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
차이값 저장 O(1) O(1) O(1) O(1) O(n) 4-5ms ⭐⭐ ⭐⭐⭐

💡 핵심 인사이트

왜 원본 코드가 느린가?

문제점 분석:

// push 시 minDeque 재정렬
while(!minDeque.isEmpty() && minDeque.peekLast()<val){
    stack.addLast(minDeque.removeLast());    
}
minDeque.addLast(val);
while(!stack.isEmpty()){
    minDeque.addLast(stack.removeLast());         
}

이 코드는:

  1. minDeque를 정렬된 상태로 유지하려 함
  2. 삽입마다 최악의 경우 O(n) 시간 소요
  3. 불필요한 작업: 최솟값만 알면 되는데 전체 순서 유지

올바른 접근:


배운 점

  1. 과잉 설계(Over-engineering)

    • 문제: “최솟값을 O(1)에 찾기”
    • 잘못된 해석: “항상 정렬된 상태 유지”
    • 올바른 해석: “각 시점의 최솟값만 기록”
  2. 스택의 특성 활용

     스택은 LIFO → 가장 최근 상태가 중요
     각 시점의 최솟값을 함께 저장하면
     pop 시 자동으로 이전 최솟값으로 복원!
    
  3. Trade-off 이해

    • 시간 vs 공간
    • 가독성 vs 최적화
    • Two Stack: 가독성 최고, 공간 2배
    • 차이값: 공간 절약, 복잡함

핵심 개념

“동기화된 스택” 패턴:

메인 스택: 실제 값 저장
보조 스택: 메타데이터 저장 (최솟값, 최댓값 등)

push → 두 스택 모두 push
pop  → 두 스택 모두 pop

이 패턴은 다양한 문제에 응용 가능:


🎯 개선 후 코드

추천: Two Stack 방식 (가독성과 성능의 완벽한 균형)

class MinStack {
    private Deque<Integer> stack;     // 실제 값 저장
    private Deque<Integer> minStack;  // 각 시점의 최솟값 저장
    
    public MinStack() {
        stack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        stack.push(val);
        
        // 현재 최솟값과 비교하여 minStack에 저장
        if (minStack.isEmpty()) {
            minStack.push(val);
        } else {
            minStack.push(Math.min(val, minStack.peek()));
        }
    }
    
    public void pop() {
        stack.pop();
        minStack.pop();  // 동기화
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

/**
 * 시간복잡도: 모든 연산 O(1)
 * 공간복잡도: O(2n) = O(n)
 * 
 * Runtime: 4ms (상위 95%)
 * Memory: 44MB
 */

개선 효과:


📚 관련 개념

알고리즘 패턴

연관 개념

  1. Stack 확장

    • Min Stack (이 문제)
    • Max Stack
    • Min Max Stack
    • Median Stack
  2. 실무 활용

    • 브라우저 히스토리 (뒤로가기 + 현재 URL)
    • 텍스트 에디터 (Undo + 현재 커서 위치)
    • 게임 체크포인트 (상태 저장 + 점수)
  3. 최적화 기법

    • Space-Time Tradeoff
    • Lazy Evaluation
    • Compression (차이값 저장)

🎓 다음 단계

비슷한 문제

  1. LeetCode 716. Max Stack ⭐⭐ (Premium)

    • Min Stack의 반대 버전
  2. LeetCode 895. Maximum Frequency Stack ⭐⭐⭐⭐

    • 빈도수가 가장 높은 원소 pop
  3. LeetCode 901. Online Stock Span ⭐⭐

    • 단조 스택 활용
  4. LeetCode 1172. Dinner Plate Stacks ⭐⭐⭐⭐⭐

    • 여러 스택 관리
  5. LeetCode 295. Find Median from Data Stream ⭐⭐⭐⭐

    • 두 Heap으로 중앙값 찾기

심화 학습

  1. Max Stack 구현하기
  2. Min Max Stack 동시 지원
  3. getMedian() O(1) 구현 시도
  4. Thread-Safe MinStack 구현

연습 포인트 체크리스트


🔍 원본 코드 개선 과정

단계별 리팩토링:

// Step 1: 불필요한 변수 제거
// min, last 제거

// Step 2: Deque 개수 줄이기
// stack 제거 (임시 저장용 불필요)

// Step 3: 정렬 로직 제거
// minDeque 재정렬 while 루프 삭제

// Step 4: 핵심 로직으로 단순화
// 각 push마다 현재 최솟값만 minStack에 저장

교훈:

“복잡한 해결책이 떠오르면, 더 단순한 방법이 있는지 의심하라”


🏷️ Keywords

#MinStack #Stack #Design #TwoStack #AuxiliaryDataStructure #MetadataTracking
#SynchronizedStack #SpaceTimeTradeoff #MonotonicStack #DataStructureDesign
#알고리즘#LeetCodeMedium #코딩테스트 #자료구조 #성능개선