Stack
스택
LIFO(Last-In-First-Out) 자료구조. 마지막에 삽입된 데이터가 먼저 나가는 구조로, 함수 호출 스택, 실행 취소, DFS 등에 활용.
스택
LIFO(Last-In-First-Out) 자료구조. 마지막에 삽입된 데이터가 먼저 나가는 구조로, 함수 호출 스택, 실행 취소, DFS 등에 활용.
스택(Stack)은 LIFO(Last-In-First-Out, 후입선출) 원칙을 따르는 선형 자료구조입니다. 접시를 쌓아 올리는 것처럼 마지막에 추가된 데이터가 가장 먼저 제거됩니다. 데이터는 top(꼭대기)에서만 삽입(push)과 삭제(pop)가 이루어집니다. 이는 FIFO(First-In-First-Out) 구조인 큐(Queue)와 대비되는 특성입니다. 스택이라는 용어는 '쌓다'라는 의미에서 유래했으며, 가장 기본적이면서도 강력한 자료구조 중 하나입니다.
스택의 핵심 연산들은 모두 O(1) 상수 시간복잡도를 가집니다. push(삽입)는 top에 요소를 추가하고, pop(삭제)은 top 요소를 제거 후 반환합니다. peek/top은 top 요소를 제거 없이 확인만 합니다. isEmpty(비어있는지 확인)와 size(크기 반환)도 O(1)입니다. 배열이나 연결 리스트로 구현할 수 있는데, 배열 구현은 메모리가 연속적이라 캐시 효율이 좋고, 연결 리스트 구현은 동적 크기 조절이 자유롭습니다.
컴퓨터 과학에서 스택은 두 가지 중요한 영역에서 핵심적으로 사용됩니다. 콜 스택(Call Stack)은 함수 호출 시마다 스택 프레임(지역 변수, 반환 주소 등)을 쌓아 함수 실행 흐름을 관리합니다. 함수가 끝나면 프레임이 pop되어 호출자로 돌아갑니다. 메모리 스택은 프로그램 실행 시 지역 변수와 함수 매개변수를 저장하는 메모리 영역입니다. 힙(Heap)과 달리 자동으로 할당/해제되어 빠르지만 크기가 제한적입니다. 재귀 호출이 깊어지면 스택 메모리가 가득 차서 Stack Overflow가 발생합니다.
실무에서 스택은 다양한 분야에서 핵심적으로 활용됩니다. 실행 취소(Undo) 기능은 작업을 스택에 쌓고, Ctrl+Z 시 pop하여 이전 상태로 복원합니다. 괄호 검사는 여는 괄호를 push하고 닫는 괄호가 나올 때 pop하여 짝이 맞는지 확인합니다. DFS(깊이 우선 탐색)는 스택을 사용해 그래프/트리를 탐색합니다. 브라우저의 뒤로가기/앞으로가기, 수식의 후위 표기법 계산, 문자열 역순 처리, 재귀 함수의 반복문 변환 등에도 스택이 필수적으로 사용됩니다.
함수 호출 관리. 스택 프레임에 지역 변수, 매개변수, 반환 주소 저장. 재귀/함수 중첩의 핵심.
수식 계산에 사용. 중위(infix)를 후위(postfix)로 변환, 후위 표기법 계산에 활용.
프로그램 실행 시 지역 변수 저장. 힙보다 빠르지만 크기 제한. 자동 메모리 관리.
작업 이력 저장. 실행 취소/다시 실행 구현. 에디터, 그래픽 툴에서 필수.
| 연산 | 시간복잡도 | 설명 |
|---|---|---|
| push (삽입) | O(1) | top에 요소 추가 |
| pop (삭제) | O(1) | top에서 요소 제거 및 반환 |
| peek / top | O(1) | top 요소 확인 (제거 없음) |
| isEmpty | O(1) | 비어있는지 확인 |
| size | O(1) | 요소 개수 반환 |
| 검색 | O(n) | 특정 요소 찾기 (순차 탐색) |
# Python 스택 구현 예제
from typing import Any, List, Optional
class Stack:
"""리스트 기반 스택 구현"""
def __init__(self):
self._items: List[Any] = []
def push(self, item: Any) -> None:
"""스택의 top에 요소 추가 - O(1)"""
self._items.append(item)
def pop(self) -> Any:
"""스택의 top에서 요소 제거 및 반환 - O(1)"""
if self.is_empty():
raise IndexError("빈 스택에서 pop 시도")
return self._items.pop()
def peek(self) -> Any:
"""top 요소 확인 (제거 없음) - O(1)"""
if self.is_empty():
raise IndexError("빈 스택에서 peek 시도")
return self._items[-1]
def is_empty(self) -> bool:
"""스택이 비어있는지 확인 - O(1)"""
return len(self._items) == 0
def size(self) -> int:
"""스택의 크기 반환 - O(1)"""
return len(self._items)
def __len__(self) -> int:
return self.size()
def __repr__(self) -> str:
return f"Stack({self._items})"
# 사용 예제
if __name__ == "__main__":
stack = Stack()
stack.push("A")
stack.push("B")
stack.push("C")
print(f"스택: {stack}") # Stack(['A', 'B', 'C'])
print(f"top: {stack.peek()}") # C
print(f"크기: {len(stack)}") # 3
print(f"pop: {stack.pop()}") # C
print(f"pop: {stack.pop()}") # B
print(f"남은 스택: {stack}") # Stack(['A'])
# ========== 실무 활용 예제 ==========
# 1. 괄호 유효성 검사
def is_valid_parentheses(s: str) -> bool:
"""괄호가 올바르게 짝지어졌는지 확인"""
stack = []
pairs = {')': '(', '}': '{', ']': '['}
for char in s:
if char in '({[':
stack.append(char)
elif char in ')}]':
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
return len(stack) == 0
print(is_valid_parentheses("({[]})")) # True
print(is_valid_parentheses("([)]")) # False
# 2. 문자열 뒤집기
def reverse_string(s: str) -> str:
"""스택을 사용해 문자열 뒤집기"""
stack = list(s)
result = []
while stack:
result.append(stack.pop())
return ''.join(result)
print(reverse_string("Hello")) # olleH
# 3. DFS (깊이 우선 탐색) - 스택 활용
def dfs_iterative(graph: dict, start: str) -> List[str]:
"""스택을 사용한 반복적 DFS"""
visited = set()
stack = [start]
result = []
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
result.append(node)
# 역순으로 추가 (왼쪽 자식 먼저 방문하도록)
for neighbor in reversed(graph.get(node, [])):
if neighbor not in visited:
stack.append(neighbor)
return result
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
print(f"DFS 순회: {dfs_iterative(graph, 'A')}") # ['A', 'B', 'D', 'E', 'F', 'C']
# 4. 실행 취소 (Undo) 시스템
class UndoSystem:
"""간단한 Undo/Redo 시스템"""
def __init__(self):
self.undo_stack = []
self.redo_stack = []
self.current_state = ""
def execute(self, action: str):
"""작업 실행"""
self.undo_stack.append(self.current_state)
self.current_state = action
self.redo_stack.clear() # 새 작업 시 redo 스택 초기화
print(f"실행: {action}")
def undo(self) -> Optional[str]:
"""실행 취소"""
if not self.undo_stack:
print("더 이상 취소할 수 없습니다.")
return None
self.redo_stack.append(self.current_state)
self.current_state = self.undo_stack.pop()
print(f"취소 -> 현재 상태: {self.current_state or '(초기 상태)'}")
return self.current_state
def redo(self) -> Optional[str]:
"""다시 실행"""
if not self.redo_stack:
print("다시 실행할 작업이 없습니다.")
return None
self.undo_stack.append(self.current_state)
self.current_state = self.redo_stack.pop()
print(f"다시 실행 -> 현재 상태: {self.current_state}")
return self.current_state
editor = UndoSystem()
editor.execute("Hello")
editor.execute("Hello World")
editor.execute("Hello World!")
editor.undo() # Hello World
editor.undo() # Hello
editor.redo() # Hello World
// JavaScript 스택 구현 예제
class Stack {
constructor() {
this.items = [];
}
// 스택의 top에 요소 추가 - O(1)
push(item) {
this.items.push(item);
return this; // 메서드 체이닝 지원
}
// 스택의 top에서 요소 제거 및 반환 - O(1)
pop() {
if (this.isEmpty()) {
throw new Error("빈 스택에서 pop 시도");
}
return this.items.pop();
}
// top 요소 확인 (제거 없음) - O(1)
peek() {
if (this.isEmpty()) {
throw new Error("빈 스택에서 peek 시도");
}
return this.items[this.items.length - 1];
}
// 스택이 비어있는지 확인 - O(1)
isEmpty() {
return this.items.length === 0;
}
// 스택의 크기 - O(1)
get size() {
return this.items.length;
}
// 스택 초기화
clear() {
this.items = [];
}
// 배열로 변환
toArray() {
return [...this.items];
}
toString() {
return `Stack([${this.items.join(', ')}])`;
}
}
// 사용 예제
const stack = new Stack();
stack.push("A").push("B").push("C");
console.log(`스택: ${stack}`); // Stack([A, B, C])
console.log(`top: ${stack.peek()}`); // C
console.log(`크기: ${stack.size}`); // 3
console.log(`pop: ${stack.pop()}`); // C
console.log(`pop: ${stack.pop()}`); // B
console.log(`남은 스택: ${stack}`); // Stack([A])
// ========== 실무 활용 예제 ==========
// 1. 괄호 유효성 검사
function isValidParentheses(s) {
const stack = [];
const pairs = { ')': '(', '}': '{', ']': '[' };
for (const char of s) {
if ('({['.includes(char)) {
stack.push(char);
} else if (')}]'.includes(char)) {
if (stack.length === 0 || stack[stack.length - 1] !== pairs[char]) {
return false;
}
stack.pop();
}
}
return stack.length === 0;
}
console.log(isValidParentheses("({[]})")); // true
console.log(isValidParentheses("([)]")); // false
// 2. 브라우저 히스토리 (뒤로가기/앞으로가기)
class BrowserHistory {
constructor(homepage) {
this.backStack = [];
this.forwardStack = [];
this.current = homepage;
}
visit(url) {
this.backStack.push(this.current);
this.current = url;
this.forwardStack = []; // 새 페이지 방문 시 앞으로 가기 스택 초기화
console.log(`방문: ${url}`);
}
back() {
if (this.backStack.length === 0) {
console.log("뒤로 갈 수 없습니다.");
return this.current;
}
this.forwardStack.push(this.current);
this.current = this.backStack.pop();
console.log(`뒤로 -> ${this.current}`);
return this.current;
}
forward() {
if (this.forwardStack.length === 0) {
console.log("앞으로 갈 수 없습니다.");
return this.current;
}
this.backStack.push(this.current);
this.current = this.forwardStack.pop();
console.log(`앞으로 -> ${this.current}`);
return this.current;
}
}
const browser = new BrowserHistory("google.com");
browser.visit("youtube.com");
browser.visit("facebook.com");
browser.back(); // youtube.com
browser.back(); // google.com
browser.forward(); // youtube.com
// 3. DFS (깊이 우선 탐색) - 스택 활용
function dfsIterative(graph, start) {
const visited = new Set();
const stack = [start];
const result = [];
while (stack.length > 0) {
const node = stack.pop();
if (!visited.has(node)) {
visited.add(node);
result.push(node);
// 역순으로 추가
const neighbors = graph[node] || [];
for (let i = neighbors.length - 1; i >= 0; i--) {
if (!visited.has(neighbors[i])) {
stack.push(neighbors[i]);
}
}
}
}
return result;
}
const graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
};
console.log(`DFS 순회: ${dfsIterative(graph, 'A')}`); // A,B,D,E,F,C
// 4. 수식 계산기 (후위 표기법)
function evaluatePostfix(expression) {
const stack = [];
const tokens = expression.split(' ');
for (const token of tokens) {
if (!isNaN(token)) {
stack.push(parseFloat(token));
} else {
const b = stack.pop();
const a = stack.pop();
switch (token) {
case '+': stack.push(a + b); break;
case '-': stack.push(a - b); break;
case '*': stack.push(a * b); break;
case '/': stack.push(a / b); break;
}
}
}
return stack.pop();
}
// "3 + 4 * 2" -> 후위: "3 4 2 * +"
console.log(evaluatePostfix("3 4 2 * +")); // 11
// "( 5 - 3 ) * 4" -> 후위: "5 3 - 4 *"
console.log(evaluatePostfix("5 3 - 4 *")); // 8
// 5. Min Stack (최솟값 O(1) 조회 스택)
class MinStack {
constructor() {
this.stack = [];
this.minStack = []; // 각 시점의 최솟값 저장
}
push(val) {
this.stack.push(val);
const minVal = this.minStack.length === 0
? val
: Math.min(val, this.minStack[this.minStack.length - 1]);
this.minStack.push(minVal);
}
pop() {
this.stack.pop();
this.minStack.pop();
}
top() {
return this.stack[this.stack.length - 1];
}
getMin() {
return this.minStack[this.minStack.length - 1];
}
}
const minStack = new MinStack();
minStack.push(5);
minStack.push(2);
minStack.push(7);
console.log(`최솟값: ${minStack.getMin()}`); // 2
minStack.pop();
minStack.pop();
console.log(`최솟값: ${minStack.getMin()}`); // 5
"스택과 큐의 핵심 차이는 데이터 처리 순서입니다. 스택은 LIFO로 마지막에 들어온 게 먼저 나가서 함수 호출 스택이나 실행 취소(Undo)에 적합합니다. 큐는 FIFO로 먼저 들어온 게 먼저 나가서 작업 대기열이나 BFS 탐색에 씁니다. 예를 들어 브라우저 뒤로가기는 최근 페이지부터 돌아가니 스택을, 웹 서버 요청 처리는 들어온 순서대로 처리해야 하니 큐를 사용합니다. 시간복잡도는 둘 다 삽입/삭제 O(1)로 동일합니다."
"Stack Overflow 에러가 나는 이유는 재귀 함수의 base case가 누락되었거나, 재귀 깊이가 너무 깊어서 콜 스택이 가득 찼기 때문입니다. 해결 방법은 세 가지예요. 첫째, base case가 올바르게 정의되었는지 확인하세요. 둘째, 재귀를 반복문으로 변환하여 명시적 스택을 사용하세요. 셋째, 꼬리 재귀 최적화를 지원하는 언어라면 꼬리 재귀 형태로 바꾸세요. Python에서는 sys.setrecursionlimit()으로 제한을 늘릴 수 있지만, 근본적인 해결책은 아닙니다."
"이 DFS 함수가 재귀로 구현되어 있는데, 그래프 깊이가 예측 불가능하면 스택으로 바꾸는 게 안전합니다. 재귀는 코드가 깔끔하지만 호출마다 콜 스택에 프레임이 쌓여서 깊이가 수천을 넘으면 터질 수 있어요. 명시적 스택을 사용하면 힙 메모리를 쓰기 때문에 훨씬 깊게 탐색할 수 있습니다. 그리고 메모이제이션이 필요한 경우에도 명시적 스택이 관리하기 편합니다."
콜 스택의 크기는 제한되어 있습니다. 재귀 호출이 너무 깊거나 무한 재귀에 빠지면 스택이 가득 차서 프로그램이 중단됩니다. JavaScript는 브라우저마다 다르지만 수천~수만 번, Python은 기본 1000번이 한계입니다. 깊이가 예측 불가능하거나 큰 경우, 재귀 대신 명시적 스택과 반복문을 사용하세요.
많은 언어에서 재귀 깊이에 제한이 있습니다. Python에서는 sys.getrecursionlimit()으로 확인하고 sys.setrecursionlimit()으로 변경할 수 있지만, 시스템 스택 크기를 넘으면 여전히 크래시됩니다. 꼬리 재귀 최적화(TCO)를 지원하는 언어(Scheme, Scala 등)에서는 꼬리 재귀로 작성하면 스택 프레임을 재사용하여 이 문제를 해결할 수 있습니다.
스택 메모리는 자동 할당/해제되어 빠르지만 크기가 작습니다(보통 1~8MB). 대용량 데이터는 힙에 할당하세요. 지역 변수로 큰 배열을 선언하면 스택 오버플로우가 발생할 수 있습니다. 또한 스택에 저장된 값의 참조가 함수 종료 후에도 남아있으면 dangling pointer 문제가 발생할 수 있으니 주의하세요.