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();
}
}
심각한 성능 문제: 732ms (하위 5%)
불필요한 복잡도:
사용하지 않는 변수: min, last 선언 후 미사용
잘못된 접근:
메모리 낭비:
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)
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];
}
}
아이디어: 최솟값이 바뀔 때만 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 크기가 작아짐
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());
}
이 코드는:
올바른 접근:
과잉 설계(Over-engineering)
스택의 특성 활용
스택은 LIFO → 가장 최근 상태가 중요
각 시점의 최솟값을 함께 저장하면
pop 시 자동으로 이전 최솟값으로 복원!
Trade-off 이해
“동기화된 스택” 패턴:
메인 스택: 실제 값 저장
보조 스택: 메타데이터 저장 (최솟값, 최댓값 등)
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
*/
개선 효과:
Stack 확장
실무 활용
최적화 기법
LeetCode 716. Max Stack ⭐⭐ (Premium)
LeetCode 895. Maximum Frequency Stack ⭐⭐⭐⭐
LeetCode 901. Online Stock Span ⭐⭐
LeetCode 1172. Dinner Plate Stacks ⭐⭐⭐⭐⭐
LeetCode 295. Find Median from Data Stream ⭐⭐⭐⭐
단계별 리팩토링:
// Step 1: 불필요한 변수 제거
// min, last 제거
// Step 2: Deque 개수 줄이기
// stack 제거 (임시 저장용 불필요)
// Step 3: 정렬 로직 제거
// minDeque 재정렬 while 루프 삭제
// Step 4: 핵심 로직으로 단순화
// 각 push마다 현재 최솟값만 minStack에 저장
교훈:
“복잡한 해결책이 떠오르면, 더 단순한 방법이 있는지 의심하라”
#MinStack #Stack #Design #TwoStack #AuxiliaryDataStructure #MetadataTracking
#SynchronizedStack #SpaceTimeTradeoff #MonotonicStack #DataStructureDesign
#알고리즘#LeetCodeMedium #코딩테스트 #자료구조 #성능개선