스택(stack) 자료구조는 일상 생활에서 쌓인 접시나 책과 같은 것들을 떠올리면 이해하기 쉽습니다. 스택은 데이터를 순차적으로 저장하고 접근할 수 있는 자료구조로, 핵심 원칙은 가장 마지막에 추가된 데이터가 가장 먼저 제거된다는 것입니다. 이 원칙은 LIFO(Last In, First Out)라고도 불립니다.
스택에서는 주로 두 가지 연산을 수행합니다:
스택은 프로그래밍에서 다양한 문제를 해결하기 위해 사용되며, 예를 들어 함수 호출 스택, 괄호 짝 맞추기, 후위 표기법 계산 등에서 사용됩니다.
1) 함수 호출 스택:
프로그램에서 함수가 호출되면, 그 함수의 실행을 시작하기 전에 현재 실행 중인 함수의 정보와 다음 실행할 명령어의 주소가 스택에 저장됩니다. 이 정보를 '스택 프레임'이라고 합니다. 이후, 새로 호출된 함수가 실행되고 완료되면, 스택의 가장 위에 있는 스택 프레임이 제거되고, 이전 함수로 돌아갑니다. 이렇게 스택을 사용하면 프로그램의 실행 순서와 함수 호출 관계를 관리할 수 있습니다.예를 들어, 함수 A가 호출되어 실행 중일 때, 함수 B를 호출한다고 가정해봅시다. 이 때, 스택에는 함수 A의 스택 프레임이 저장되고, 그 위에 함수 B의 스택 프레임이 저장됩니다. 함수 B가 실행을 완료하면, 스택에서 함수 B의 스택 프레임이 제거되고, 함수 A로 돌아가서 실행을 이어갑니다.
2)괄호 짝 맞추기:
스택은 괄호 짝 맞추기 문제에서도 유용하게 사용됩니다. 예를 들어, 괄호가 올바르게 짝을 이루고 있는지 확인하는 알고리즘을 작성한다고 가정해봅시다. 이 경우, 스택을 사용하여 다음과 같이 처리할 수 있습니다.
- 입력 문자열을 순회하며 각 문자를 확인합니다.
- 여는 괄호('(')를 만나면, 스택에 여는 괄호를 푸시합니다.
- 닫는 괄호(')')를 만나면, 스택에서 팝하여 여는 괄호를 제거합니다. 이때, 스택이 비어있다면 짝이 맞지 않는 것으로 판단할 수 있습니다.
- 모든 문자를 확인한 후, 스택이 비어있다면 괄호가 올바르게 짝을 이룬 것입니다. 스택에 남아있는 괄호가 있다면 짝이 맞지 않는 것으로 판단합니다.
3) 후위 표기법(Postfix Notation)은 연산자를 피연산자 뒤에 위치시키는 수학 표기법으로, 컴퓨터에서 산술 연산을 수행할 때 효율적인 방법입니다. 후위 표기법 계산에서 스택이 어떻게 사용되는지 이해하기 쉽게 설명하겠습니다.
후위 표기법으로 주어진 수식을 계산하는 과정은 다음과 같습니다:
- 입력 문자열을 왼쪽에서 오른쪽으로 순회하면서 각 문자를 확인합니다.
- 피연산자(숫자)를 만나면 스택에 푸시합니다.
- 연산자(+, -, *, / 등)를 만나면 스택에서 팝을 통해 두 개의 피연산자를 꺼냅니다. 이때, 먼저 팝한 값이 오른쪽 피연산자, 나중에 팝한 값이 왼쪽 피연산자가 됩니다.
- 꺼낸 두 피연산자에 대해 해당 연산자를 적용한 결과를 다시 스택에 푸시합니다.
- 입력 문자열의 모든 문자를 확인한 후, 스택에 남아있는 값이 최종 계산 결과입니다.
간단한 예를 들어보겠습니다. 후위 표기법으로 주어진 수식이 "5 3 2 * + 4 -"라고 가정해봅시다. 이 수식을 계산하는 과정은 다음과 같습니다:
- 5를 스택에 푸시합니다. (스택: [5])
- 3을 스택에 푸시합니다. (스택: [5, 3])
- 2를 스택에 푸시합니다. (스택: [5, 3, 2])
- 연산자를 만나면, 스택에서 2와 3을 팝하고, 이 둘을 곱한 결과인 6을 스택에 푸시합니다. (스택: [5, 6])
- 연산자를 만나면, 스택에서 6과 5를 팝하고, 이 둘을 더한 결과인 11을 스택에 푸시합니다. (스택: [11])
- 4를 스택에 푸시합니다. (스택: [11, 4])
- 연산자를 만나면, 스택에서 4와 11을 팝하고, 이 둘을 뺀 결과인 7을 스택에 푸시합니다. (스택: [7])
이제 스택에 남아있는 값은 7이므로, 최종 계산 결과는 7입니다. 이렇게 스택을 사용하여 후위 표기법으로 주어진 수식을 효과적으로 계산할 수 있습니다. 스택을 사용함으로써 연산자 우선순위를 고려할 필요가 없고, 괄호를 사용하지 않아도 됩니다. 이로 인해 후위 표기법은 컴퓨터에서 산술 연산을 수행하는 데 효율적인 방법으로 활용됩니다.
앞서 예시에서처럼, 후위 표기법으로 표현된 수식을 계산할 때 스택을 사용하여 피연산자를 저장하고, 연산자가 나타날 때마다 스택에서 피연산자를 꺼내 계산한 뒤 결과를 다시 스택에 저장하는 과정을 반복합니다. 이 과정을 문자열의 끝까지 진행한 후 스택에 남아있는 값이 최종 계산 결과가 됩니다.
간단한 예시로, 책을 쌓아두는 것을 생각해보세요. 책을 차례대로 쌓으면서, 가장 마지막에 쌓은 책을 가장 먼저 꺼내어 읽습니다. 이것이 바로 스택의 동작 방식입니다.
장점:
단점:
스택은 탐색이나 정렬과 같은 작업에 적합하지 않은 이유는 주로 자료구조의 특성과 접근 방식 때문입니다. 스택은 LIFO(Last In First Out) 원칙에 따라 동작하는 자료구조로, 가장 마지막에 들어온 요소가 가장 먼저 나가게 됩니다. 이 특성 때문에 스택에서 발생하는 문제점들을 살펴보겠습니다.
- 제한된 접근: 스택에서는 가장 위에 있는 요소만 확인하거나 제거할 수 있습니다. 따라서 중간에 있는 요소에 접근하려면 상위 요소들을 모두 제거해야 합니다. 이는 탐색 작업에 있어 매우 비효율적입니다.
- 데이터 이동 비용: 스택에서 중간 요소에 접근하거나, 삽입 및 삭제를 수행하려면 상위 요소들을 제거하고 다시 스택에 넣는 작업이 필요합니다. 이 과정에서 발생하는 데이터 이동 비용이 크기 때문에, 스택은 정렬 작업에 비효율적입니다.
- 구조적 한계: 스택은 선형 구조로, 한쪽 끝에서만 작업을 수행할 수 있습니다. 따라서 탐색이나 정렬 작업을 수행할 때, 요소들 간의 상대적 위치를 변경하거나 한 번에 여러 요소에 접근하는 것이 어렵습니다.
이러한 이유로, 스택은 탐색이나 정렬 작업에 적합하지 않습니다. 이와 같은 작업을 수행할 때는 다른 자료구조를 사용하는 것이 더 효율적입니다. 예를 들어, 배열이나 연결 리스트는 임의의 위치에 있는 요소에 쉽게 접근할 수 있으며, 정렬 알고리즘을 적용하기에 더 적합한 구조를 가지고 있습니다.
적합한 상황:
부적합한 상황:
주의사항:
이처럼 스택은 특정 상황에는 매우 유용한 자료구조이지만, 다양한 작업을 수행하는데 있어서는 부적합한 경우도 있습니다. 사용할 작업의 요구사항에 따라 적절한 자료구조를 선택하는 것이 중요합니다.
Pyton
# 재귀 함수
def recursive(data):
if data < 0:
print ("ended")
else:
print(data)
recursive(data - 1)
print("returned", data)
recursive(4)
# 4
# 3
# 2
# 1
# 0
# ended
# returned 0
# returned 1
# returned 2
# returned 3
# returned 4
파이썬 리스트 기능에서 제공하는 메서드로 스택 사용해보기
append(push), pop 메서드 제공
data_stack = list()
data_stack.append(1)
data_stack.append(2)
data_stack
# [1, 2]
data_stack.pop()
# 2
data_stack.pop()
# 1
리스트 변수로 스택을 다루는 pop, push 기능 구현해보기 (pop, push 함수 사용하지 않고 직접 구현해보기)
stack_list = list()
def push(data):
stack_list.append(data)
def pop():
data = stack_list[-1]
del stack_list[-1]
return data
for index in range(10):
push(index)
pop()
# 9
pop()
# 8
pop()
# 7
pop()
# 6
JS
class Stack {
items = [];
// 스택에 새로운 요소를 추가
push(element) {
this.items.push(element);
}
// 스택에서 가장 위에 있는 요소를 제거하고 반환
pop() {
if (this.isEmpty()) {
return "Stack is empty";
}
const lastItem = this.items[this.items.length - 1];
this.items.length -= 1; // 렝스를 줄이면 내부적으로 마지막 요소 삭제됨
return lastItem;
}
// 스택의 가장 위에 있는 요소를 반환
peek() {
if (this.isEmpty()) {
return "Stack is empty";
}
return this.items[this.items.length - 1];
}
// 스택이 비어있는지 확인
isEmpty() {
return this.items.length === 0;
}
// 스택의 길이를 반환
size() {
return this.items.length;
}
// 스택의 모든 요소를 제거
clear() {
this.items = [];
}
// 스택의 요소를 문자열로 반환
toString() {
return this.items.toString();
}
}
// 사용 예시
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.peek()); // 출력: 3
console.log(stack.pop()); // 출력: 3
console.log(stack.size()); // 출력: 2