기본 특징
메모리 구조
Stack:
┌──────────────────┐
│ first (pointer) │ → 첫 번째 노드
│ last (pointer) │ → 마지막 노드
│ size │ → 요소 개수
└──────────────────┘
Heap:
┌────┬────┬────┐ ┌────┬────┬────┐ ┌────┬────┬────┐
│prev│data│next│ ↔ │prev│data│next│ ↔ │prev│data│next│
└────┴────┴────┘ └────┴────┴────┘ └────┴────┴────┘
정렬
std::list<int> myList = {3, 1, 4, 1, 5};
// ✅ list 전용 sort() 멤버 함수 사용 (O(n log n))
myList.sort();
// ❌ std::sort() 사용 불가 (Random Access Iterator 필요)
// std::sort(myList.begin(), myList.end()); // 컴파일 에러!
주요 연산 시간 복잡도
| 연산 | 시간 복잡도 | 비고 |
|---|---|---|
| Insertion/Deletion (위치를 알 때) | O(1) | 중간 삽입/삭제 효율적 |
Random Access ([], at()) | 불가능 | 순차 접근만 가능 |
| Search (find) | O(n) | 순차 탐색 |
| Sort | O(n log n) | 멤버 함수 sort() 사용 |
기본 특징
list보다 메모리 효율적 (포인터 하나만 필요)메모리 구조
Stack:
┌──────────────────┐
│ front (pointer) │ → 첫 번째 노드
└──────────────────┘
Heap:
┌────┬────┐ ┌────┬────┐ ┌────┬────┐
│data│next│ → │data│next│ → │data│next│ → nullptr
└────┴────┘ └────┴────┘ └────┴────┘
제한 사항
std::forward_list<int> fList = {1, 2, 3, 4, 5};
// ✅ 가능한 연산
fList.push_front(0);
fList.pop_front();
fList.insert_after(fList.begin(), 10);
// ❌ 불가능한 연산
// fList.push_back(6); // back 포인터 없음
// fList.size(); // size 저장 안 함 (O(n)으로 계산 가능)
// fList[2]; // Random Access 불가
성능 비교
| 연산 | vector | list | forward_list |
|---|---|---|---|
| Random Access | O(1) ✅ | O(n) ❌ | O(n) ❌ |
| Front Insertion/Deletion | O(n) | O(1) ✅ | O(1) ✅ |
| Back Insertion/Deletion | O(1) ✅ | O(1) ✅ | O(n) ❌ |
| Middle Insertion/Deletion | O(n) | O(1) ✅ | O(1) ✅ |
| find() | O(n) | O(n) | O(n) |
| 메모리 연속성 | 연속 ✅ | 분산 | 분산 |
| 캐시 효율 | 높음 ✅ | 낮음 | 낮음 |
실제 find() 성능
// 둘 다 O(n)이지만 실제로는 vector가 훨씬 빠름!
std::vector<int> vec(1000000);
std::list<int> lst(1000000);
// vector의 find: 메모리가 연속적 → 캐시 히트율 높음 ✅
auto it1 = std::find(vec.begin(), vec.end(), 999999);
// list의 find: 메모리가 분산 → 캐시 미스 많음 ❌
auto it2 = std::find(lst.begin(), lst.end(), 999999);
왜 vector가 더 빠를까? (캐시 지역성)
병렬 프로그래밍에서의 문제
// ❌ list는 병렬화가 어려움
// 각 노드가 분산되어 있어서 동시 접근 시 동기화 오버헤드 큼
std::list<int> lst;
// ✅ vector는 병렬화가 용이함
// 메모리가 연속적이라 분할하기 쉬움
std::vector<int> vec;
#pragma omp parallel for
for (int i = 0; i < vec.size(); i++) {
// 병렬 처리
}
선택 가이드
vector 사용 ✅list 고려forward_list기본 특징
deque (변경 가능)지원 컨테이너
// ✅ 기본 (deque)
std::stack<int> s1;
// ✅ vector로 구현
std::stack<int, std::vector<int>> s2;
// ✅ list로 구현
std::stack<int, std::list<int>> s3;
// ❌ forward_list는 불가 (back 접근 필요)
// std::stack<int, std::forward_list<int>> s4; // 컴파일 에러!
주요 연산
std::stack<int> s;
s.push(10); // O(1) - 삽입
s.pop(); // O(1) - 제거 (값 반환 안 함!)
int top = s.top(); // O(1) - 최상단 접근
bool empty = s.empty(); // O(1)
size_t sz = s.size(); // O(1)
성능 최적화가 필요한 경우
// 직접 구현 예시 (vector 기반)
template<typename T>
class FastStack {
private:
std::vector<T> data;
public:
void push(const T& value) {
data.push_back(value);
}
void pop() {
data.pop_back();
}
T& top() {
return data.back();
}
// ...
};
기본 특징
deque (변경 가능)지원 컨테이너
// ✅ 기본 (deque)
std::queue<int> q1;
// ✅ list로 구현
std::queue<int, std::list<int>> q2;
// ❌ vector는 불가 (front에서 pop이 O(n))
// std::queue<int, std::vector<int>> q3; // 컴파일 에러!
// ❌ forward_list는 불가 (back 접근 필요)
// std::queue<int, std::forward_list<int>> q4; // 컴파일 에러!
주요 연산
std::queue<int> q;
q.push(10); // O(1) - 뒤에 삽입
q.pop(); // O(1) - 앞에서 제거
int front = q.front(); // O(1) - 앞 요소 접근
int back = q.back(); // O(1) - 뒤 요소 접근
bool empty = q.empty(); // O(1)
size_t sz = q.size(); // O(1)
기본 특징
vector)시간 복잡도
| 연산 | 시간 복잡도 |
|---|---|
Insertion (push) | O(log n) |
Extraction (pop) | O(log n) |
Top 접근 (top) | O(1) ✅ |
힙 구조 (배열 기반)
인덱스: 0 1 2 3 4 5 6
값: [100, 50, 80, 30, 20, 60, 70]
트리 구조:
100 (idx=0)
/ \
50 80
/ \ / \
30 20 60 70
부모-자식 관계:
- 부모 노드: (idx - 1) / 2
- 왼쪽 자식: 2 * idx + 1
- 오른쪽 자식: 2 * idx + 2
힙 속성
사용 예시
#include <queue>
// ✅ Max Heap (기본)
std::priority_queue<int> maxHeap;
maxHeap.push(30);
maxHeap.push(100);
maxHeap.push(50);
std::cout << maxHeap.top(); // 100
// ✅ Min Heap (greater 사용)
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
minHeap.push(30);
minHeap.push(100);
minHeap.push(50);
std::cout << minHeap.top(); // 30
// ✅ 커스텀 비교 함수
auto cmp = [](int a, int b) { return a > b; }; // Min Heap
std::priority_queue<int, std::vector<int>, decltype(cmp)> customHeap(cmp);
인덱스 계산 예시
// 인덱스 3의 부모: (3 - 1) / 2 = 1
// 인덱스 1의 왼쪽 자식: 2 * 1 + 1 = 3
// 인덱스 1의 오른쪽 자식: 2 * 1 + 2 = 4
기본 특징
<algorithm> 헤더에 포함된 힙 관련 함수들vector)를 힙으로 변환/관리priority_queue와 동일한 내부 구조주요 함수
| 함수 | 시간 복잡도 | 설명 |
|---|---|---|
make_heap | O(n) | 범위를 힙으로 변환 |
push_heap | O(log n) | 마지막 요소를 힙에 삽입 |
pop_heap | O(log n) | 최대값을 마지막으로 이동 |
사용 예시
#include <algorithm>
#include <vector>
std::vector<int> vec = {30, 100, 50, 20, 80};
// ✅ 힙 생성 - O(n)
std::make_heap(vec.begin(), vec.end());
// vec: [100, 80, 50, 20, 30] (Max Heap)
// ✅ 최대값 확인
std::cout << vec.front(); // 100 (O(1))
// ✅ 최대값 제거
std::pop_heap(vec.begin(), vec.end()); // O(log n)
// 최대값을 맨 뒤로 이동
// vec: [80, 30, 50, 20, 100]
vec.pop_back(); // 실제 제거 - O(1)
// vec: [80, 30, 50, 20]
// ✅ 새 요소 추가
vec.push_back(90); // O(1)
std::push_heap(vec.begin(), vec.end()); // O(log n)
// vec: [90, 80, 50, 20, 30]
make_heap이 O(n)인 이유
Naive 방식 (위에서 아래로): O(n log n)
- 각 요소를 하나씩 삽입: n번
- 각 삽입마다 heapify: O(log n)
- 총 시간: O(n log n)
Floyd 알고리즘 (아래에서 위로): O(n) ✅
- 리프 노드는 이미 힙 속성 만족
- 아래에서 위로 올라가며 heapify
- 높이 h인 노드 개수: n / 2^(h+1)
- 각 노드의 작업량: O(h)
- 총 시간: Σ(n / 2^(h+1) * h) = O(n)
priority_queue vs heap 알고리즘
// priority_queue: 편리하지만 유연성 낮음
std::priority_queue<int> pq;
pq.push(10);
// 내부 데이터 직접 접근 불가
// heap 알고리즘: 유연하지만 수동 관리 필요
std::vector<int> vec = {10, 20, 30};
std::make_heap(vec.begin(), vec.end());
// 벡터에 직접 접근 가능 ✅
vec[0]; // 최대값 확인
언제 heap 알고리즘을 사용할까?
컨테이너 선택 가이드
1. 대부분의 경우: vector 사용 (캐시 효율, 랜덤 액세스)
2. 중간 삽입/삭제 빈번: list 고려
3. 메모리 최소화: forward_list
4. LIFO 필요: stack
5. FIFO 필요: queue
6. 우선순위 관리: priority_queue
7. 힙 직접 제어: heap 알고리즘
성능 최적화 팁
make_heap이 삽입보다 빠름 (O(n) vs O(n log n))코드없는 프로그래밍
C++ Standard Containers
std::list
std::forward_list
std::priority_queue
Heap Algorithms
Container Adapters