STL의 기본인 Vector와 Pair에 대해서 보도록 합니다.
Vector
vector
헤더 안에 작성되어 있습니다.
std::vector is a sequence container that encapsulates dynamic size arrays.
레퍼런스 사이트에 의하자면, vector는 sequence container로서
dynamic size array를 캡슐화 하여 제공한다고 합니다.
여기서 sequence container는 순차 컨테이너 로 번역될 수 있는데, 순차적으로 데이터를 저장하는 Array를 생각하면 가장 정확할 것 같습니다. 즉, vector는 dynamic size array, 동적 크기 확장을 지원하는 배열 자료구조가 되겠네요
Vector는 배열과 큰 차이가 없기 때문에 쉽게 사용을 검토해 볼 수 있으면서, std::sort 알고리즘 함수를 편리하게 사용할 수 있기 때문에 사용할수록 편리해집니다.
배열과 큰 차이가 없이 순차적으로 배치되어 있다는 특징 때문에, 기존 stl의 순회 방법인 iterator를 사용한 접근 방법 외에도 braket operator [] 를 사용해 random access를 O(1)에 할 수 있습니다.
이는 다시 말해 일반 배열과 완전히 동일한 방법으로도 사용할 수 있다는 것입니다.
Vector's member function.
front(), back()
배열의 앞 원소와 뒷 원소를 가져오는 멤버 펑션입니다.
각각 V[0]
, V[V.size()-1]
과 동일합니다.
시간복잡도는 O(1)
입니다.
size()
배열의 크기를 반환합니다.
굳이 계산하자면 sizeof(V) / sizeof(V[0])
정도가 됩니다.
시간복잡도는 O(1)
입니다.
push_back()
배열의 뒤에 원소를 추가하는 멤버 펑션입니다.
배열의 크기가 1 증가하며, 값도 동시에 들어갑니다.
V.push_back(12345);
와 같이 사용합니다.
시간복잡도가 Amortized O(1)
으로 꽤나 복잡한 편입니다. Vector 컨테이너의 구현을 위해 어쩔수 없이 이러한 시간복잡도를 가지게 되었는데, 설명이 굉장히 길게 필요하다 보니 아마 별도의 글로 분리할 것 같습니다.
pop_back()
배열의 뒤 마지막 원소를 삭제합니다.
배열의 크기가 1 감소하지만, 배열의 capacity == 메모리 용량은 줄어들지 않습니다.
시간복잡도는 O(1)
입니다.
begin(), end()
배열의 앞 iterator와 뒷 iterator를 가져오는 멤버 펑션입니다.
*it를 통해 출력해보면 각각 V[0]
, V[V.size()]
와 동일합니다.
V[V.size()-1]
을 참조하는 rbegin() 과V[-1]
을 참조하는 rend() 도 존재합니다.
iterator의 축약 구문을 사용하면 Vector에 대한 출력을 굉장히 편리하게 진행할 수 있습니다.
시간복잡도는 O(1)
입니다.
int main(){
vector<int> V;
V.push_back(1);
V.push_back(2);
V.push_back(3);
V.push_back(4);
V.push_back(5);
for(int k : V){
cout << k << "\n";
}
}
Vector의 사용처
기존의 배열이 사용하던 거의 모든 곳에서 그 역할을 대체 할 수 있으며, 대체 했을 경우 iterator를 사용 가능하게 되므로 일반 배열보다 정렬이나 최대/최솟값 등을 구하기가 압도적으로 편해집니다.
개인적으로는 추후 다루게 될 <Algorithm> 헤더의 함수들과 사용하기 편하게 하기 위해서
#define all(v) v.begin(), v.end()
매크로 상수 정의를 통해서 "처음부터 끝까지" 에 대한 정의를 미리 해두고 있습니다.
이 경우 정렬을 할 때 sort(all(V))
로 정렬할 수 있어 매우 편리합니다.
Pair
std::pair는 별다른 멤버 함수가 크게 존재하지 않는 아주 간단한 형태의 자료구조입니다. 기본으로
- == operator
- > operator
- < operator
등의 비교 연산자가 적용되어 있어서 비교가 가능하면서, 두 연관성 있는 값을 서로 묶어줍니다. 이는 백준 문제 해결에 있어서 대단히 유용한 특성을 가지게 되는데, 예를 들어서 {x,y} 좌표쌍을 pair에 저장하면 변수 하나에 담아 비교할 수 있어 대단히 편하게 됩니다.
pair<int,int> A = {3,5};
와 같이 사용합니다.
Vector + Pair
Vector와 Pair를 같이 사용해 정렬을 하면 문제를 굉장히 쉽게 해결할 수 있는데, 대표적인 유형이 Silver V
에 상주하고 있는 다양한 기준에 맞게 정렬하는 문제들입니다.
x좌표에 따라 정렬하고, x좌표가 같으면 y좌표에 따라서 정렬을 해야하는 문제입니다. 심지어 N이 1e5로 O(NlogN) 이상의 정렬을 사용해야 합니다.
편의상 std::sort를 통해 한번에 정렬 해주도록 하고, Vector와 Pair를 사용하지 않는 소스코드는 아래와 같습니다.
void fastio(){ cin.sync_with_stdio(0); cin.tie(0); cout.tie(0); }
struct st{
int x;
int y;
};
bool operator<(st A, st B){
if(A.x != B.x) return A.x < B.x;
return A.y < B.y;
}
st arr[123456];
int main(){
fastio();
int N;
cin >> N;
for(int i=0; i<N; i++){
cin >> arr[i].first >> arr[i].second;
}
sort(arr, arr+N);
for(int i=0; i<N; i++){
cout << arr[i].first << " " << arr[i].second << "\n";
}
return 0;
}
보통 위 소스코드와 같이 < 연산자에 대한 오버로딩을 하거나, 혹은 동일한 역할을 하는 함수를 Functor로 std::sort의 세 번째 인자로 넘겨주는 방식으로 구현합니다.
이제, 이 구현을 Vector + Pair 조합으로 해보도록 하겠습니다.
vector<pair<int,int>> V;
int main(){
fastio();
int N;
cin >> N;
for(int i=0; i<N; i++){
int a,b;
cin >> a >> b;
V.push_back({a,b});
}
sort(V.begin(), V.end());
for(pair<int,int> now : V){
cout << now.first << " " << now.second << "\n";
}
return 0;
}
iterator 가 되면서 출력을 위한 for문이 Range-Based For loop 이 되었고, 연산자가 기본으로 지정되어있기 때문에 따로 오버로딩 해줄 필요성이 없어지게 됩니다.
참고로
- 역순 정렬 : 음수를 붙여 저장한후 정렬하고 다시 양수로 바꾸어 출력합니다.
- 세가지 값 이상 : pair<int,pair<int,int>> 와 같이 pair를 중첩 사용합니다
- first/second : define 구문으로 x,y로 바꿔 사용할 수 있습니다
#define x first
#define y second
등을 통해 보다 편리하게 더 많은 문제들을 해결할 수 있습니다.
추후 그래프 알고리즘에서 노드 번호와 가중치를 동시에 저장하는 등의 필요에 의해 std::pair를 사용하게 되므로, 반드시 익혀둘 필요가 있습니다.
해결 가능한 문제들
- boj 10814 나이순 정렬
- boj 10825 국영수
- boj 2535 아시아 정보올림피아드
이외 여러 정렬 기준을 적용해야 하는 Silver V~Silver III
수준의 문제들 을 쉽게 해결해 볼 수 있습니다.