C++ STL 컨테이너 완벽 가이드
개요
STL(Standard Template Library) 컨테이너는 C++에서 데이터를 저장하고 관리하는 템플릿 클래스들입니다.
목차
시퀀스 컨테이너
vector
동적 배열, 가장 많이 사용되는 컨테이너
#include <vector>
// 생성
vector<int> vec; // 빈 벡터
vector<int> vec(10); // 크기 10, 기본값 0
vector<int> vec(10, 5); // 크기 10, 모든 값 5
vector<int> vec{1, 2, 3, 4, 5}; // 초기화 리스트
// 주요 함수
vec.push_back(10); // 끝에 추가
vec.pop_back(); // 끝에서 제거
vec.insert(vec.begin() + 2, 99); // 특정 위치에 삽입
vec.erase(vec.begin() + 2); // 특정 위치 삭제
// 크기 관련
vec.size(); // 현재 크기
vec.capacity(); // 할당된 용량
vec.reserve(100); // 용량 예약
vec.resize(20); // 크기 변경
vec.shrink_to_fit(); // 불필요한 메모리 해제
// 접근
vec[0]; // 인덱스 접근 (범위 검사 X)
vec.at(0); // 안전한 접근 (범위 검사 O)
vec.front(); // 첫 번째 요소
vec.back(); // 마지막 요소
vec.data(); // 내부 배열 포인터
시간복잡도: - 접근: O(1), 끝 삽입/삭제: O(1), 중간 삽입/삭제: O(n)
deque
양쪽 끝에서 빠른 삽입/삭제가 가능한 덱
#include <deque>
deque<int> dq;
// 양쪽 끝 조작
dq.push_front(1); // 앞에 추가
dq.push_back(2); // 뒤에 추가
dq.pop_front(); // 앞에서 제거
dq.pop_back(); // 뒤에서 제거
// vector와 유사한 인터페이스
dq[0] = 10;
dq.at(1) = 20;
dq.insert(dq.begin() + 1, 15);
시간복잡도: - 접근: O(1), 양쪽 끝 삽입/삭제: O(1), 중간 삽입/삭제: O(n)
list
이중 연결 리스트
#include <list>
list<int> lst{1, 2, 3, 4, 5};
// 삽입/삭제
lst.push_front(0); // 앞에 추가
lst.push_back(6); // 뒤에 추가
auto it = lst.begin();
advance(it, 2);
lst.insert(it, 99); // 특정 위치에 삽입
// 리스트 특화 연산
lst.remove(3); // 값 3인 모든 요소 제거
lst.remove_if([](int x) { return x > 3; }); // 조건 만족하는 요소 제거
lst.unique(); // 연속된 중복 제거
lst.sort(); // 정렬
lst.reverse(); // 뒤집기
// 두 리스트 합치기
list<int> other{10, 20, 30};
lst.merge(other); // 정렬된 두 리스트 병합
lst.splice(lst.begin(), other); // other의 모든 요소를 lst 앞으로 이동
시간복잡도: - 접근: O(n), 삽입/삭제: O(1) (위치를 알고 있을 때)
forward_list
단일 연결 리스트 (C++11)
#include <forward_list>
forward_list<int> flst{1, 2, 3, 4, 5};
// 앞쪽만 조작 가능
flst.push_front(0);
flst.pop_front();
// 특정 위치 다음에 삽입
auto it = flst.begin();
flst.insert_after(it, 99);
flst.erase_after(it);
array
고정 크기 배열 (C++11)
#include <array>
array<int, 5> arr{1, 2, 3, 4, 5};
// C 배열과 유사하지만 STL 인터페이스 제공
arr[0] = 10;
arr.at(1) = 20;
arr.fill(0); // 모든 요소를 0으로
연관 컨테이너
set
정렬된 고유 요소 집합
#include <set>
set<int> s{3, 1, 4, 1, 5, 9}; // 중복 제거되어 {1, 3, 4, 5, 9}
// 삽입/삭제
s.insert(2); // 삽입
s.erase(3); // 값으로 삭제
s.erase(s.find(4)); // 반복자로 삭제
// 검색
auto it = s.find(5); // 찾기
bool exists = s.count(5); // 존재 여부 (0 또는 1)
auto it = s.lower_bound(3); // 3 이상인 첫 번째 요소
auto it = s.upper_bound(3); // 3 초과인 첫 번째 요소
// 사용자 정의 비교 함수
set<int, greater<int>> desc_set; // 내림차순
multiset
정렬된 중복 허용 집합
#include <set>
multiset<int> ms{3, 1, 4, 1, 5, 9, 1}; // 중복 허용
ms.count(1); // 1의 개수 반환
auto range = ms.equal_range(1); // 1인 요소들의 범위
map
키-값 쌍의 정렬된 연관 배열
#include <map>
map<string, int> m;
// 삽입
m["apple"] = 5;
m.insert({"banana", 3});
m.emplace("orange", 7);
// 접근
int count = m["apple"]; // 없으면 기본값으로 생성
int count = m.at("apple"); // 없으면 예외 발생
// 검색
auto it = m.find("apple");
if (it != m.end()) {
cout << it->first << ": " << it->second << endl;
}
// 순회
for (const auto& [key, value] : m) { // C++17 구조화 바인딩
cout << key << ": " << value << endl;
}
multimap
키 중복을 허용하는 map
#include <map>
multimap<string, int> mm;
mm.insert({"fruit", 1});
mm.insert({"fruit", 2}); // 같은 키에 다른 값
auto range = mm.equal_range("fruit"); // 해당 키의 모든 값
무순서 연관 컨테이너 (C++11)
unordered_set
해시 테이블 기반 집합
#include <unordered_set>
unordered_set<int> us{1, 2, 3, 4, 5};
// set과 유사한 인터페이스, 하지만 정렬되지 않음
us.insert(6);
us.erase(3);
bool exists = us.count(4);
// 해시 테이블 정보
cout << "Bucket count: " << us.bucket_count() << endl;
cout << "Load factor: " << us.load_factor() << endl;
unordered_map
해시 테이블 기반 연관 배열
#include <unordered_map>
unordered_map<string, int> um;
um["key1"] = 100;
um["key2"] = 200;
// map과 유사하지만 더 빠른 평균 접근 시간
시간복잡도: - 평균: O(1), 최악: O(n)
컨테이너 어댑터
stack
LIFO (Last In, First Out) 스택
#include <stack>
stack<int> st;
st.push(1); // 추가
st.push(2);
st.push(3);
cout << st.top() << endl; // 최상단 요소 (3)
st.pop(); // 제거 (반환값 없음)
cout << st.size() << endl; // 크기
queue
FIFO (First In, First Out) 큐
#include <queue>
queue<int> q;
q.push(1); // 추가
q.push(2);
q.push(3);
cout << q.front() << endl; // 앞쪽 요소 (1)
cout << q.back() << endl; // 뒤쪽 요소 (3)
q.pop(); // 앞에서 제거
priority_queue
우선순위 큐 (최대 힙)
#include <queue>
priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(4);
cout << pq.top() << endl; // 최대값 (4)
pq.pop(); // 최대값 제거
// 최소 힙
priority_queue<int, vector<int>, greater<int>> min_pq;
// 사용자 정의 비교
struct Compare {
bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second; // second 기준 최소 힙
}
};
priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> custom_pq;
성능 비교
삽입/삭제 성능
Operation | vector | deque | list | set | unordered_set
--------------------|--------|-------|------|------|---------------
끝 삽입/삭제 | O(1) | O(1) | O(1) | - | -
앞 삽입/삭제 | O(n) | O(1) | O(1) | - | -
중간 삽입/삭제 | O(n) | O(n) | O(1) | - | -
요소 삽입/삭제 | - | - | - |O(log n)|O(1) avg
접근 성능
Operation | vector | deque | list | set | unordered_set
--------------------|--------|-------|------|------|---------------
인덱스 접근 | O(1) | O(1) | O(n) | - | -
요소 검색 | O(n) | O(n) | O(n) |O(log n)|O(1) avg
메모리 사용량
- vector: 연속 메모리, 가장 효율적
- deque: 블록 단위 메모리, 중간 정도
- list: 노드별 할당, 오버헤드 큼
- set/map: 트리 구조, 노드 오버헤드
- unordered_*: 해시 테이블, 버킷 오버헤드
선택 가이드
시퀀스 컨테이너 선택
// 일반적인 배열 대체 → vector
vector<int> general_use;
// 양쪽 끝 삽입/삭제 빈번 → deque
deque<int> buffer;
// 중간 삽입/삭제 빈번 → list
list<int> frequently_insert;
// 고정 크기 → array
array<int, 100> fixed_size;
연관 컨테이너 선택
// 정렬된 고유 요소 → set
set<int> unique_sorted;
// 정렬된 키-값 → map
map<string, int> sorted_dict;
// 빠른 검색 (정렬 불필요) → unordered_set/unordered_map
unordered_set<int> fast_lookup;
unordered_map<string, int> fast_dict;
// 중복 허용 → multiset/multimap
multiset<int> allow_duplicates;
어댑터 선택
// LIFO 필요 → stack
stack<int> undo_operations;
// FIFO 필요 → queue
queue<int> task_queue;
// 우선순위 처리 → priority_queue
priority_queue<int> urgent_tasks;
실전 예제
1. LRU 캐시 구현
class LRUCache {
int capacity;
list<pair<int, int>> cache;
unordered_map<int, list<pair<int, int>>::iterator> map;
public:
LRUCache(int capacity) : capacity(capacity) {}
int get(int key) {
auto it = map.find(key);
if (it == map.end()) return -1;
// 최근 사용으로 이동
cache.splice(cache.begin(), cache, it->second);
return it->second->second;
}
void put(int key, int value) {
auto it = map.find(key);
if (it != map.end()) {
// 기존 키 업데이트
it->second->second = value;
cache.splice(cache.begin(), cache, it->second);
} else {
// 새 키 추가
if (cache.size() >= capacity) {
// 가장 오래된 항목 제거
map.erase(cache.back().first);
cache.pop_back();
}
cache.emplace_front(key, value);
map[key] = cache.begin();
}
}
};
2. 단어 빈도 계산
map<string, int> count_words(const vector<string>& words) {
map<string, int> frequency;
for (const string& word : words) {
frequency[word]++;
}
return frequency;
}
// 빠른 버전
unordered_map<string, int> count_words_fast(const vector<string>& words) {
unordered_map<string, int> frequency;
for (const string& word : words) {
frequency[word]++;
}
return frequency;
}
3. 슬라이딩 윈도우 최대값
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq; // 인덱스 저장
vector<int> result;
for (int i = 0; i < nums.size(); i++) {
// 윈도우 밖의 요소 제거
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// 현재 요소보다 작은 요소들 제거
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
dq.push_back(i);
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
성능 최적화 팁
1. vector 최적화
// 미리 크기 예약
vector<int> vec;
vec.reserve(expected_size);
// emplace_back vs push_back
vec.emplace_back(args...); // 제자리 생성 (더 효율적)
vec.push_back(Type(args)); // 임시 객체 생성 후 복사
2. map vs unordered_map
// 정렬이 필요하거나 키가 복잡한 타입 → map
map<ComplexKey, Value> sorted_map;
// 단순 검색, 빠른 속도 → unordered_map
unordered_map<int, Value> hash_map;
3. 반복자 무효화 주의
vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
// 위험: 벡터 크기 변경으로 반복자 무효화
vec.push_back(6); // it가 무효화될 수 있음
// 안전한 방법: 인덱스 사용 또는 반복자 갱신
마지막 업데이트: 2025-06-24