19. STL 컨테이너(STL Container)
1. 키워드
- STL 컨테이너(STL Container)
- 컨테이너(Container)
- 시퀀스 컨테이너(Sequence Container)
vector
,deque
,list
,forward_list
컨테이너- 연관 컨테이너(Associative Container)
set
,multiset
,map
,multimap
컨테이너- 순서가 지정되지 않은(Unordered) 연관 컨테이너
unordered_set
,unordered_multiset
,unordered_map
,unordered_multimap
- 컨테이너 어댑터(Container Adapter)
stack
,queue
,priority_queue
2. 컨테이너
- STL에서 컨테이너는 같은 타입의 여러 객체를 저장하는 일종의 집합이라 할 수 있다.
- 컨테이너는 클래스 템플릿으로, 컨테이너 변수를 선언할 때 컨테이너에 포함할 요소의 타입을 명시할 수 있다.
- 컨테이너에는 복사 생성과 대입을 할 수 있는 타입의 객체만을 저장할 수 있다.
- 또한, 컨테이너는 요소의 추가 및 제거를 포함한 다양한 작업을 도와주는 여러 멤버 함수를 포함하고 있다.
- 참고로 컨테이너의 각 요소에는 반복자를 사용하여 접근할 수 있다.
1) 컨테이너의 종류
- STL에서 컨테이너는 자료를 저장하는 방식과 관리하는 방식에 따라 여러 가지 형태로 나뉠 수 있다.
- STL 컨테이너는 크게 다음과 같이 세 가지 유형으로 구분된다.
1] 시퀀스 컨테이너
2] 연관 컨테이너
3] 컨테이너 어댑터
컨테이너 종류 | 설명 | 컨테이너 |
---|---|---|
시퀀스 컨테이너 | 데이터를 선형으로 저장하며, 특별한 제약이나 규칙이 없는 가장 일반적인 컨테이너 | vector , deque , list , forwad_list |
연관 컨테이너 | 데이터를 일정 규칙에 따라 조직화하여 저장하고 관리하는 컨테이너 | set , multiset , map , multimap |
컨테이너 어댑터 | 간결함과 명료성을 위해 인터페이스를 제한한 시퀀스나 연관 컨테이너의 변형 단, 반복자를 지원하지 않으므로 STL 알고리즘에서는 사용할 수 없음 |
stack , queue , priority_queue |
3. 시퀀스 컨테이너
- 시퀀스 컨테이너는 데이터를 선형으로 저장하며, 특별한 제약이나 규칙이 없는 가장 일반적인 컨테이너이다.
- 시퀀스 컨테이너에서는 삽입된 요소의 순서가 그대로 유지된다.
1) 시퀀스 컨테이너의 요구 사항
- STL에서 시퀀스 컨테이너는 기본 컨테이너의 개념에 다음과 같은 요구 사항을 추가하여 정의한다.
1] 모든 요소가 직선 순서대로 배치되어 있어야 하는 즉, 첫 번째 요소와 마지막 요소를 제외한 나머지 요소들은 반드시 앞뒤로 인접한 요소를 하나씩 가지고 있어야 한다.
2] 반복자가 최소한 순방향 반복자 이상이어야 하는데, 이것은 반복자가 이동할 때마다 요소들의 순서가 변하지 않음을 보장해 주는 것이다.
3] 시퀀스 컨테이너의 요소들은 명확한 순서를 가지므로, 특정 위치를 참조하는 연산이 가능해야 한다.
2) 시퀀스 컨테이너의 종류
- STL에서는 시퀀스 컨테이너로 다음과 같은 클래스 템플릿을 제공한다.
1] vector
2] deque
3] list
4] forward_list
(1) vector
vector
컨테이너는 동적 배열의 클래스 템플릿 표현이라 할 수 있다.vector
객체는 요소가 추가되거나 삭제될 때마다 자동으로 메모리를 재할당하여 크기를 동적으로 변경한다.- 이 컨테이너는
vector
헤더 파일에 정의되어 있으며, 임의 접근을 제공하는 가장 간단한 시퀀스 컨테이너이다.
vector
컨테이너는 다음과 같이 선언한다.
- 템플릿 인수로는
vector
컨테이너에 저장될 요소의 타입을 전달한다. - 생성자 인수로는
vector
컨테이너의 초기 크기를 전달하며, 생략하면 요소를 가지지 않는 빈vector
를 생성한다.
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
int main(void) {
vector<int> vc = {10, 20, 30}; // vector 객체의 선언 및 초기화
cout << "현재 벡터의 크기는 " << vc.size() << "입니다." << endl;
vc.push_back(40); // vector 요소의 추가
cout << "현재 벡터의 크기는 " << vc.size() << "입니다." << endl;
cout << "현재 벡터의 네 번째 요소는 " << vc[3] << "입니다." << endl;
cout << "현재 벡터의 모든 요소는 다음과 같습니다:" << endl;
copy(vc.begin(), vc.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 현재 벡터의 크기는 3입니다.
// 현재 벡터의 크기는 4입니다.
// 현재 벡터의 네 번째 요소는 40입니다.
// 현재 벡터의 모든 요소는 다음과 같습니다:
// 10 20 30 40
- 위의 코드에서 사용된 STL 함수는 다음과 같다.
1] size()
- 컨테이너에 저장된 요소의 개수를 반환함
2] push_back()
- 컨테이너의 마지막 요소로 해당 데이터를 추가함
3] begin()
- 컨테이너의 첫 번째 요소를 가리키는 반복자를 반환함
4] end()
- 컨테이너의 마지막 요소 바로 다음을 가리키는 반복자를 반환함
(2) deque
deque
컨테이너는 양쪽에 끝이 있는 큐이다.- 이 컨테이너는 컨테이너의 양 끝에서 빠르게 요소를 삽입하거나 삭제할 수 있다.
deque
컨테이너는deque
헤더 파일에 정의되어 있다.
deque
객체는vector
객체와 마찬가지로 임의 접근과 동적 크기의 장점을 가지며,vector
로는 할 수 없는 전방 삽입과 전방 삭제도 빠르게 수행할 수 있다.
#include <deque>
#include <iostream>
#include <iterator>
using namespace std;
int main(void) {
deque<int> dq = {20}; // deque 객체의 선언 및 초기화
dq.push_back(30); // 요소의 후방 삽입
dq.push_front(10); // 요소의 전방 삽입
cout << "현재 데큐의 모든 요소는 다음과 같습니다:" << endl;
copy(dq.begin(), dq.end(), ostream_iterator<int>(cout, " "));
cout << endl
<< "현재 데큐의 첫 번째 요소는 " << dq.front() << "입니다." << endl;
dq.pop_front(); // 요소의 전방 삭제
cout << "현재 데큐의 모든 요소는 다음과 같습니다:" << endl;
copy(dq.begin(), dq.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 현재 데큐의 모든 요소는 다음과 같습니다:
// 10 20 30
// 현재 데큐의 첫 번째 요소는 10입니다.
// 현재 데큐의 모든 요소는 다음과 같습니다:
// 20 30
- 위의 코드에서 사용된 STL 함수는 다음과 같다.
1] push_front()
- 컨테이너의 첫 번째 요소로 해당 데이터를 추가함
2] front()
- 컨테이너의 첫 번째 요소를 반환함
3] pop_front()
- 컨테이너의 첫 번째 요소를 삭제함
(3) list
list
컨테이너는 이중 연결 리스트(Doubly Linked List)의 클래스 템플릿 표현이라 할 수 있다.- 이 컨테이너는 컨테이너의 모든 요소에서 양방향 접근, 빠른 삽입과 삭제를 할 수 있지만, 임의 접근은 할 수는 없다.
list
컨테이너는list
헤더 파일에 정의되어 있다.
vector
객체는 임의 접근을 통한 빠른 접근이 장점이며,list
객체는 요소들의 빠른 삽입과 삭제가 장점이다.- 또한,
list
를 구성하는 링크는 포인터이므로, 다음과 같은 특정 작업을 링크만 재배치하는 것으로 아주 빠르게 수행할 수 있다.
1] swap()
- 두 요소의 위치를 서로 바꿈
2] reverse()
list
전체의 요소의 위치를 역순으로 변경함
3] sort()
list
전체의 요소를 정렬함
4] unique()
- 같은 값이 인접해 있을 경우, 그 값들을 하나로 단일화함
5] merge()
- 두 정렬된
list
를 합병함
6] splice()
- 두
list
를 연결하거나, 한 쪽list
로 이동시킴
- 다음의 코드는
unique()
함수를 사용하여list
에서 서로 인접한 위치에 있는 동일한 값들을 제거하는 것이다.
#include <iostream>
#include <iterator>
#include <list>
using namespace std;
int main(void) {
list<int> ls = {1, 2, 2, 3, 4, 3, 5, 5}; // list 객체의 선언 및 초기화
// ls.sort(); // 1, 2, 3, 4, 5
ls.unique(); // 1, 2, 3, 4, 3, 5
cout << "현재 리스트의 모든 요소는 다음과 같습니다:" << endl;
copy(ls.begin(), ls.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 현재 리스트의 모든 요소는 다음과 같습니다:
// 1 2 3 4 3 5
- 위의 코드에서
unique()
함수만을 사용하면, 서로 인접해 있지 않은 두 개의3
은 제거되지 않는다. - 따라서
unique()
함수를 사용하기 전에sort()
함수를 먼저 사용하여 정렬하면,list
내의 모든 중복값을 제거할 수 있다.
(4) forward_list
forward_list
컨테이너는 단방향 연결 리스트(Singly Linked List)의 클래스 템플릿 표현이라 할 수 있다.- C++11부터 추가된 이 컨테이너는 모든 요소에서 순방향으로 접근할 수는 있지만, 역방향으로 접근할 수는 없다.
- 따라서
forward_list
컨테이너에서는 오직 순방향 반복자만을 사용한다.
list
객체와 비교하면forward_list
객체는 더 적은 메모리를 가지고 간편하게 사용할 수 있는 장점이 있다.
#include <forward_list>
#include <iostream>
#include <iterator>
using namespace std;
int main(void) {
forward_list<int> fls01 = {10, 20, 400, 30}; // forward_list 객체의 선언 및 초기화
forward_list<int> fls02 = {40, 50};
forward_list<int>::iterator itr;
fls01.remove(400); // 값이 400인 모든 요소를 삭제함.
cout << "현재 순방향 리스트의 모든 요소는 다음과 같습니다:" << endl;
copy(fls01.begin(), fls01.end(), ostream_iterator<int>(cout, " "));
cout << endl;
itr = fls01.begin(); // fls01의 첫 번째 요소를 가리키도록 반복자를 초기화함
fls01.splice_after(itr, fls02); // fls02의 모든 요소를 fls01의 첫 번째 요소 다음에 삽입함
cout << "fls01: ";
copy(fls01.begin(), fls01.end(), ostream_iterator<int>(cout, " "));
cout << endl
<< "fls02: ";
copy(fls02.begin(), fls02.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 현재 순방향 리스트의 모든 요소는 다음과 같습니다:
// 10 20 30
// fls01: 10 40 50 20 30
// fls02:
- 위의 코드에서 사용된 STL 함수는 다음과 같다.
1] remove()
- 전달된 값과 같은 값을 가지는 요소를 컨테이너에서 모두 삭제함
2] splice_after()
- 해당 요소를 원본
forward_list
에서 삭제하고, 대상forward_list
의 지정된 위치에 삽입함
- 따라서 위의 코드에서
splice_after()
함수가 호출된 후에fls02
에는 어떠한 요소도 저장되어 있지 않게 된다.
4. 연관 컨테이너
- 연관 컨테이너는 키(Key)와 값(Value)처럼 관련있는 데이터를 하나의 쌍으로 저장하는 컨테이너이다.
- 키와 값을 이용한 연관 컨테이너는 요소들에 대한 빠른 접근을 제공해 준다.
- 하지만 연관 컨테이너는 삽입되는 요소의 위치를 지정할 수는 없다.
- 이러한 연관 컨테이너는 보통 균형 잡힌 이진 탐색 트리(Balanced Binary Search Table)나 해시 테이블(Hash Table)을 이용하여 구현한다.
1) 연관 컨테이너의 종류
- STL에서는 연관 컨테이너로 다음과 같은 클래스 템플릿을 제공한다.
1] set
2] multiset
3] map
4] multimap
(1) set
과 multiset
set
컨테이너는 저장하는 데이터 그 자체를 키로 사용하는 가장 단순한 연관 컨테이너이다.- 이 컨테이너는
vector
와 달리 오름차순으로 정렬된 위치에 요소를 삽입하므로 검색 속도가 매우 빠르다.
set
에서는 키는 유일해야 하므로, 키의 중복을 허용하지 않는다.- 하지만
multiset
은 키의 중복을 허용하므로, 같은 값을 여러 번 저장할 수 있다. - 이 두 컨테이너는 모두
set
헤더 파일에 정의되어 있다.
set
컨테이너는 다음과 같이 선언한다.
- 템플릿 인수로는
set
컨테이너에 저장될 키의 타입을 전달한다.
- 다음의 코드는 반복자의 범위를 매개변수로 사용하는
set
컨테이너의 생성자를 사용하는 것이다.
#include <iostream>
#include <iterator>
#include <set>
using namespace std;
int main(void) {
int arr[5] = {10, 20, 30, 40, 50}; // 배열 생성 및 초기화
set<int> st(arr, arr + 3); // 배열의 일부 요소를 가지고 set 컨테이너를 생성함
cout << "현재 집합의 모든 요소는 다음과 같습니다." << endl;
copy(st.begin(), st.end(), ostream_iterator<int>(cout, " "));
cout << endl;
st.insert(60); // 요소의 추가
st.insert(70); // 요소의 추가
st.erase(20); // 요소의 삭제
cout << "현재 집합의 모든 요소는 다음과 같습니다." << endl;
copy(st.begin(), st.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 현재 집합의 모든 요소는 다음과 같습니다.
// 10 20 30
// 현재 집합의 모든 요소는 다음과 같습니다.
// 10 30 60 70
- 위의 코드에서 사용된 STL 함수는 다음과 같다.
1] insert()
- 컨테이너에 해당 데이터를 추가함
2] erase()
- 전달된 값과 같은 값을 가지는 요소를 컨테이너에서 모두 삭제함
(2) map
과 multimap
map
컨테이너는 키와 값의 쌍으로 데이터를 관리하는 진정한 연관 컨테이너이다.- 이 컨테이너는
set
컨테이너와 마찬가지로 정렬된 위치에 요소를 삽입하므로 검색 속도가 매우 빠르다.
map
에서는 키는 유일해야 하므로, 키의 중복을 허용하지 않는다.- 따라서 하나의 키에 하나의 값만이 연결될 수 있다.
- 하지만
multimap
은 값의 중복을 허용하므로, 하나의 키가 여러 개의 값과 연결될 수 있다. - 이 두 컨테이너는 모두
map
헤더 파일에 정의되어 있다.
map
컨테이너는 다음과 같이 선언한다.
- 템플릿 인수로는
map
컨테이너에 저장될 키의 타입과 값의 타입을 전달한다.
- 다음의 코드는
map
컨테이너에 요소를 추가하는 방법을 알려주는 것이다.
#include <iostream>
#include <iterator>
#include <map>
using namespace std;
int main(void) {
map<string, int> mp;
mp.insert(pair<string, int>("국어", 80)); // 익명의 pair 객체를 생성하여 추가함
mp["수학"] = 100; // 첨자 연산자를 이용하여 추가함
cout << "현재 맵의 모든 요소는 다음과 같습니다." << endl;
map<string, int>::iterator it;
for (it = mp.begin(); it != mp.end(); it++) {
cout << it->first << ": " << it->second << endl;
}
return 0;
}
// 현재 맵의 모든 요소는 다음과 같습니다.
// 국어: 80
// 수학: 100
map
컨테이너 요소의 키는first
라는 멤버 변수에, 값은second
라는 멤버 변수에 각각 저장되어 있다.
2) 순서가 지정되지 않은 연관 컨테이너
- 순서가 지정되지 않은 연관 컨테이너는 순서가 지정된 기존의 연관 컨테이너와 같은 동작을 한다.
- 기존의 연관 컨테이너는 트리 구조를 기반으로 동작하지만, C++11부터 추가된 이 컨테이너는 해시 테이블을 기반으로 동작하게 된다.
- 따라서 요소의 추가, 삭제 속도가 빨라졌으며, 다양한 검색 알고리즘을 사용할 수 있게 되었다.
- 순서가 지정된 연관 컨테이너는 양방향 반복자를 지원하지만, 이 컨테이너는 순방향 반복자만을 지원한다.
- C++11부터 추가된 순서가 지정되지 않은 연관 컨테이너는 다음과 같다.
1] unordered_set
2] unordered_multiset
3] unordered_map
4] unordered_multimap
5. 컨테이너 어댑터
- 컨테이너 어댑터란 기존 컨테이너의 인터페이스를 제한하여 만든 기능이 제한되거나 변형된 컨테이너를 의미한다.
- 이러한 컨테이너 어댑터는 각각의 기초가 되는 클래스의 인터페이스를 제한하여, 특정 형태의 동작만을 수행하도록 한다.
- 단, 반복자를 지원하지 않으므로 STL 알고리즘에서는 사용할 수 없다.
1) 컨테이너 어댑터의 종류
- STL에서는 컨테이너 어댑터로 다음과 같은 클래스 템플릿을 제공한다.
1] stack
2] queue
3] priority_queue
(1) stack
stack
컨테이너는vector
클래스의 인터페이스를 제한하여, 전형적인 스택 메모리 구조의 인터페이스를 제공한다.stack
컨테이너는stack
헤더 파일에 정의되어 있다.
- 스택 메모리 구조는 선형 메모리 공간에 데이터를 저장하면서 후입선출(LIFO)의 시멘틱을 따르는 자료 구조이다.
- 즉, 가장 나중에 저장된 데이터가 가장 먼저 인출되는 구조이다.
stack
컨테이너는 스택 메모리 구조를 표현하기 위해 다음과 같은 멤버 함수를 제공한다.
1] empty()
- 스택이 비어 있으면
true
를, 비어 있지 않으면false
를 반환함
2] size()
- 스택 요소의 총 개수를 반환함
3] top()
- 스택의 제일 상단에 있는(제일 마지막으로 저장된) 요소에 대한 참조를 반환함
4] push()
- 스택의 제일 상단에 요소를 삽입함
5] pop()
- 스택의 제일 상단에 있는 요소를 삭제함
- 다음의 코드는
stack
컨테이너를 사용하여 10진수를 2진수로 변환하는 것이다.
#include <iostream>
#include <stack>
using namespace std;
int main(void) {
int decimal = 123;
stack<int> st;
// 10진수를 2진수로 변환
do {
st.push(decimal % 2);
decimal /= 2;
} while (decimal);
// 스택의 모든 요소를 인출
while (!st.empty()) {
cout << st.top();
st.pop();
}
cout << endl;
return 0;
}
// 1111011
- 참고로
stack
컨테이너는 요소에 대한 임의 접근을 허용하지 않을 뿐만 아니라, 스택을 순회하는 반복자도 허용하지 않는다.
(2) queue
queue
컨테이너는deque
클래스의 인터페이스를 제한하여, 전형적인 큐 메모리 구조의 인터페이스를 제공한다.queue
컨테이너는queue
헤더 파일에 정의되어 있다.
- 큐 메모리 구조는 선형 메모리 공간에 데이터를 저장하면서 선입선출(FIFO)의 시멘틱을 따르는 자료 구조이다.
- 즉, 가장 먼저 저장된 데이터가 가장 먼저 인출되는 구조이다.
queue
컨테이너는 큐 메모리 구조를 표현하기 위해 다음과 같은 멤버 함수를 제공한다.
1] empty()
- 큐가 비어 있으면
true
를, 비어 있지 않으면false
를 반환함
2] size()
- 큐 요소의 총 개수를 반환함
3] front()
- 큐의 맨 앞에 있는(제일 먼저 저장된) 요소에 대한 참조를 반환함
4] back()
- 큐의 맨 뒤에 있는(제일 나중에 저장된) 요소에 대한 참조를 반환함
5] push()
- 큐의 맨 뒤에 요소를 삽입함
6] pop()
- 큐의 맨 앞의 요소를 삭제함
- 다음의 코드는
queue
컨테이너를 사용하여 정해진 수만큼 피보나치 수열을 출력하는 것이다.
#include <iostream>
#include <queue>
using namespace std;
int main(void) {
int n = 20; // 20개의 피보나치 수열을 출력함
queue<int> que;
que.push(0); // 초기값인 0과 1을 저장함
que.push(1);
// 피보나치 수열
for (int i = 2; i < n; i++) {
int temp = que.front();
cout << temp << " ";
que.pop();
que.push(temp + que.front());
}
cout << endl;
return 0;
}
// 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
(3) priority_queue
priority_queue
컨테이너는queue
와는 달리 큐의 맨 앞의 요소로 가장 먼저 저장된 요소가 아닌, 가장 큰 값을 지닌 요소가 위치하게 된다.- 또한,
queue
가deque
클래스를 기반으로 하는 것과 달리,priority_queue
는vector
클래스를 기반으로 한다.
- 하지만 사용할 수 있는 멤버 함수는
queue
컨테이너와 같다. priority_queue
컨테이너는queue
헤더 파일에 정의되어 있다.
- 다음의 코드는
priority_queue
의 동작 방식을 보여주는 것이다.
#include <iostream>
#include <queue>
using namespace std;
int main(void) {
priority_queue<int> pq;
pq.push(10);
pq.push(20);
pq.push(100);
pq.push(3);
// 우선 순위 큐의 모든 요소를 인출
while (!pq.empty()) {
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
return 0;
}
// 100 20 10 3
- 위의 코드에서 볼 수 있듯이
priority_queue
의 요소는 언제나 값의 내림차순으로 정렬되게 된다.