자료구조란 데이터들과의 관계, 함수, 명령 등의 집합을 의미하며 데이터에 대해 효율적으로 접근, 수정 등 데이터의 처리를 효율적으로 할 수 있게 만든 구조를 의미한다.


탐색과 맨 뒤의 요소를 삭제하거나 삽입하는 데 O(1)이 걸리며,맨 뒤나 맨 앞이 아닌 요소를 삭제하고 삽입하는 데 O(n)의 시간이 걸린다.
vector<타입> 변수명;
int형 vector를 정의하고 싶다면 vector<int> 로 정의해야 한다.
#include <bits/stdc++.h>
using namespace std;
vector<int> v;
int main(){
for(int i = 1; i <= 10; i++)v.push_back(i);
for(int a : v) cout << a << " ";
cout << "\n";
v.pop_back();
for(int a : v) cout << a << " ";
cout << "\n";
v.erase(v.begin(), v.begin() + 3);
for(int a : v) cout << a << " ";
cout << "\n";
auto a = find(v.begin(), v.end(), 100);
if(a == v.end()) cout << "not found" << "\n";
fill(v.begin(), v.end(), 10);
for (int a : v) cout << a << " ";
cout << "\n";
v.clear();
cout << "아무것도 없을까?\n";
for (int a : v) cout << a << " ";
cout << "\n";
return 0;
}

vector의 뒤에서부터 요소를 더한다.
c.f 참고로 push_back()과 같은 기능을 하는 emplace_back()도 있다.(이게 더 빠르지만 시간 차이 별로 안 남)
vector의 맨 뒤의 요소를 지운다.
iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);
한 요소만을 지우면 erase(위치)로 쓰고, [from, to)로 지우고 싶다면 erase[from, to)를 쓴다.
vector의 메서드가 아닌 STL 함수이다.
vector 내의 요소들을 찾고 싶을 때, [from, to) 에서 value를 찾는다.
O(n)의 시간복잡도를 가진다.
vector의 모든 요소를 지운다.
vector 내의 value로 값을 할당하고 싶다면 fill을 써서 채운다.
보통 이를 '~~한 값으로 초기화'라고 부른다. [from, to) 구간에 value를 초기화 합니다.
C++11부터 범위기반 for 루프가 추가되어서 이를 쓸 수 있다. (vector만 쓸 수 있는 것은 아니고 순회할 수 있는 컨테이너인 vector, Array, map 등도 사용가능)
for ( range_declaration : range_expression )
loop_statement
for(<해당 컨테이너에 들어있는 타입> 임시변수명 : 컨테이너)
#include <bits/stdc++.h>
using namespace std;
vector<int> v{1, 2, 3};
int main(){
for (int a : v) cout << a << " ";
cout << "\n";
for (int i = 0; i < v.size(); i++) cout << v[i] << ' ';
cout << "\n";
vector<pair<int, int>> v2 = {{1, 2,}, {3, 4}};
for(pair<int, int> a : v2) cout << a.first << " ";
return 0;
}
만약 vector 안에 pair가 들어간다면 pair을 써서 범위기반 for루프를 써야 한다.
아래 두 코드는 vector<int>내의 요소를 탐색한다는 같은 의미이다.
for(int a : v)
for(int i = 0; i < v.size(); i++) v[i]
vector라고 해서 무조건 크기가 0인 빈 vector를 만들어 동적할당으로 요소를 추가하는 것은 아니다.
미리 크기를 정해놓거나 해당 크기에 대해 어떠한 값으로 초기화 해놓고 시작할 수도 있다.
다음은 5개의 요소를 담을 수 있는 vector를 선언하고 모든 값을 100으로 채운 코드이다.
#include <bits/stdc++.h>
using namespace std;
vector<int> v(5, 100);
int main(){
for (int a : v) cout << a << " ";
cout << "\n";
return 0;
}
아래처럼도 가능하다.
#include <bits/stdc++.h>
using namespace std;
vector<int> v{10, 20, 30, 40, 50};
int main(){
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
for (int i : v) {
cout << i << " ";
}
return 0;
}
vector를 이용한 2차원 배열을 만드는 3가지 방법이 있다.
#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> v; //1
vector<vector<int>> v2(10, vector<int>(10, 0)); //2
vector<int> v3[10]; //3
int main(){
for (int i = 0; i < 10; i++) {
vector<int> temp;
v.push_back(temp);
}
return 0;
}
v는 vector안에 vector가 들어가 있는 2차원 배열 타입을 선언한다.
그 이후 v에 temp라는 vector를 push_back해서 2차원 배열을 만든다.
v2는 10 * 10 짜리 크기의 2차원배열을 바로 만든다. 0으로 초기화까지 한다.
v3는 10개 짜리 배열을 선언한다. 이것도 v와 똑같은 2차원배열이다.


2차원 배열은 단순히 차원을 늘려서 만들면 된다.
#include <bits/stdc++.h>
using namespace std;
const int mxy = 3;
const int mxx = 4;
int a[mxy][mxx];
int main(){
for (int i = 0; i < mxy; i++) {
for (int j = 0; j < mxx; j++) {
a[i][j] = ( i + j );
}
}
//good
for (int i = 0; i < mxy; i++) {
for (int j = 0; j < mxx; j++) {
cout << a[i][j] << ' ';
}
cout << '\n';
}
//bad
for (int i = 0; i < mxx; i++) {
for (int j = 0; j < mxy; j++) {
cout << a[j][i] << ' ';
}
cout << '\n';
}
return 0;
}

위 good, bad 코드를 참고해라.
첫번째 차원 >> 2번째 차원 순으로 탐색하는게 성능이 좋다.
c.f
이는 C++에서 캐시를 첫번째 차원(여기서는 mxy)을 기준으로 하기 때문에 캐시관련 효율성 때문에 탐색을 하더라도 순서를 지켜가며 해주는게 좋다.
next, prev라는 포인터로 연결되어 이루어지며 중복을 허용한다.
데이터를 감싼 노드를 포인터로 연결해서 공간적인 효율성을 극대화시킨 자료구조이다.
삽입과 삭제가 O(1)이 걸리며 k번째 요소를 참조한다 했을 때는 O(k)가 걸린다.
노드의 구성 : 싱글연결리스트 기준
data +next(포인터, 노드와 노드를 잇게 만드는)
class Node{
public:
int data;
Node* next;
Node() {
data = 0;
next = NULL;
}
Node(int data) {
this->data = data;
this->next = NULL;
}
}
연결리스트에는 싱글연결리스트, 이중연결리스트, 원형연결리스트 3가지가 있다.
싱글연결리스트(Singly Linked List)는 next 포인터밖에 존재하지 않으며 한 방향으로만 데이터가 연결된다.

이중연결리스트(Doubly Linked List)는 prev, next 두개의 포인터로 양방향으로 데이터가 연결된다.

원형연결리스트(Circular Linked List)는 마지막 노드와 첫번째 노드가 연결되어 원을 형성한다.
싱글연결리스트, 이중영ㄴ결리스트로 이뤄진 2가지 타입이 있다.


C++에서는 list로 이중연결리스트를 쉽게 구현할 수 있다.
#include <bits/stdc++.h>
using namespace std;
list<int> a;
void print(list <int> a) {
for (auto it : a) cout << it << " ";
cout << '\n';
}
int main(){
for (int i = 1; i <= 3; i++) a.push_back(i);
for (int i = 1; i <= 3; i++) a.push_front(i);
auto it = a.begin();
it++;
a.insert(it, 1000);
print(a);
it = a.begin();
it++;
a.erase(it);
print(a);
a.pop_front();
a.pop_back();
print(a);
cout << a.front() << " : " << a.back() << '\n';
a.clear();
return 0;
}
리스트의 앞에서부터 value를 넣는다.
리스트의 뒤에서부터 value를 넣는다.
iterator insert (const_iterator position, const value_type& val);
요소를 몇번째에 삽입한다.
iterator erase (const_iterator position);
리스트의 idx번째 요소를 지운다.
첫번째 요소를 지운다.
맨 끝 요소를 지운다.
맨 앞 요소를 참조한다.
맨 뒤 요소를 참조한다.
모든 요소를 지운다.
c.f 랜덤접근과 순차적 접근
직접 접근이라고 하는 랜덤 접근(random access)은 동일한 시간에 배열과 같은 순차적인 데이터가 있을 때 임의의 인덱스에 해당하는 데이터에 접근할 수 있는 기능이다. 이는 데이터를 저장된 순서대로 검색해야 하는 순차적 접근(sequential access)과는 반대이다.

vector, Array와 같은 배열은 랜덤접근이 가능해서 k번째 요소에 접근할 때 O(1)이 걸린다.
연결리스트, 스택, 큐는 순차적 접근만이 가능해서 k번째 요소에 접근할 때 O(k)이 걸린다.
c.f 배열과 연결리스트 비교 (면접에서 자주 나옴)

c.f 레드 - 블랙트리
레드블랙트리는 균형 이진 검색 트리이며 삽입, 삭제, 수정, 탐색에 O(logN)의 시간복잡도가 보장된다.

[] 로 해당 키로 직접 참조할 수 있다.#include <bits/stdc++.h>
using namespace std;
map<string, int> mp;
string a[] = {"주홍철", "이승철", "박종선"};
int main(){
for (int i = 0; i < 3; i++) {
mp.insert({a[i], i+1});
mp[a[i]] = i + 1;
}
cout << mp["kundol"] << '\n';
mp["kundol"] = 4;
cout << mp.size() << '\n';
mp.erase("kundol");
auto it = mp.find("kundol");
if (it == mp.end()) {
cout << "못 찾겠다\n";
}
mp["kundol"] = 100;
it = mp.find("kundol");
if(it != mp.end()) {
cout << (*it).first << " : " << (*it).second<< '\n';
}
for (auto it :mp) {
cout << (it).first << " : " << (it).second << '\n';
}
for (auto it = mp.begin(); it != mp.end(); it++) {
cout << (*it).first << " : " << (*it).second << '\n';
}
mp.clear();
return 0;
}

map에다 {key, value}를 집어 넣는다.
대괄호[]를 통해 key에 해당하는 value를 할당한다.
대괄호[]를 통해 key를 기반으로 map에 있는 요소를 참조한다.
map에 있는 요소들의 개수를 반환한다.
해당 키에 해당하는 요소를 지운다.
map에서 해당 key를 가진 요소를 찾아 해당 이터레이터를 반환한다. 만약 못찾을 경우 mp.end(), 해당 map의 end() 이터레이터를 반환한다.
범위기반 for루프로 map에 있는 요소들을 순회한다. map을 순회할 때는 key는 first, value는 second로 참조가 가능하다.
map에 있는 요소들을 이터레이터로 순회할 수 있다.
map에 있는 요소들을 다 제거한다.
다음 코드처럼 map의 경우 해당 인덱스에 참조만 해도 맵의 요소가 생기게 된다.
만약 map에 해당 키에 해당하는 요소가 없다면 0 또는 빈문자열로 초기화가 되어 할당된다. (int는 0, string이나 char은 빈 문자열로 할당됩니다.)
할당하고 싶지 않아도 대괄호([])로 참조할 경우 자동으로 요소가 추가가 되기 때문에 조심해야 한다.
#include <bits/stdc++.h>
using namespace std;
map<int, int> mp;
map<string, string> mp2;
int main(){
ios_base::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
cout << mp[1] << '\n';
cout << mp2["aaa"] << '\n';
for(auto i : mp) cout << i.first << " " << i.second;
cout << '\n';
for(auto i : mp2) cout << i.first << " " << i.second;
return 0;
}
/*
0
1 0
aaa %
*/
맵에 요소가 있는지 없는지를 확인하고 맵에 요소를 할당하는 로직은 다음처럼 구축 가능하다.
#include <bits/stdc++.h>
using namespace std;
map<int, int> mp;
map<string, string> mp2;
int main(){
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
if(mp[1] == 0){
mp[1] = 2;
}
for(auto i : mp) cout << i.first << " " << i.second;
return 0;
}
/*
1 2
*/
다만 앞의 코드는 문제에서 해당 키에 0이 아닌 값이 들어갈 때 활용이 가능하다.
이미 if문 안에 mp[1] == 0을 해버린 순간 mp[1] = 0이 할당되어버리기 때문이다.

만약 문제에서 0이 들어가는 것을 비교하기 귀찮다면 다음 코드를 기반으로 작성해라.
#include <bits/stdc++.h>
using namespace std;
map<int, int> mp;
map<string, string> mp2;
int main(){
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
if(mp.find(1) == mp.end()){
mp[1] = 2;
}
for(auto i : mp) cout << i.first << " " << i.second;
return 0;
}
/*
1
2
*/
map은 정렬이 되는 반면 unordered_map은 정렬이 되지 않은 map이며 메서드는 map과 동일하다.
#include<bits/stdc++.h>
using namespace std;
unordered_map<string, int> umap;
int main(){
umap["bcd"] = 1;
umap["aaa"] = 1;
umap["aba"] = 1;
for(auto it : umap){
cout << it.first << " : " << it.second << '\n';
}
return 0;
}
/*
aba : 1
aaa : 1
bcd : 1
*/
문제에 정렬이 필요로 하지 않은 문제에는 unordered_map을 써도 될 것 같지만 제출해보면 시간초과가 나기도 한다. (가장 최악의 경우 O(N)이기 때문)
➡️ 되도록 map을 쓰는 것을 권장한다.
#include <bits/stdc++.h>
using namespace std;
int main(){
set<pair<string, int>> st;
st.insert({"test", 1});
st.insert({"test", 1});
st.insert({"test", 1});
st.insert({"test", 1});
cout <<st.size() <<"\n";
set<int> st2;
st2.insert(2);
st2.insert(1);
st2.insert(2);
for(auto it : st2){
cout << it << '\n';
}
return 0;
}
/*
1
1
2
*/
어떤 것을 사용하는게 더 좋은가? set을 사용해도 되고, unique와 erase를 사용해도 된다.
vector에다가 담아야 하는 로직이 있다고 가정.
set을 사용할 경우,
1. 중복된 배열 vector 생성됨
2. set을 사용해서 중복 제거
3. 다시 새로운 vector를 만들어 요소를 집어넣음
➡️ 새로운 vector, set 2개의 자료구조가 '더' 만들어진다.
➡️ u nique()와 erase()를 사용한다면 그냥 해당 중복된 배열 vector를 기반으로 재탕해서 사용해도 된다.
두 개의 로직을 C++ 벤치마크로 테스팅
https://perfbench.com/
#include <bits/stdc++.h>
using namespace std;
void A(){
vector<int> v;
int n = 1e5;
for(int i = 1; i < n; i++){
v.push_back(i);
v.push_back(n - i);
}
sort(v.begin(), v.end());
v.erase(unique(v.begin(),v.end()),v.end());
}
void B(){
vector<int> v;
int n = 1e5;
for(int i = 1; i < n; i++){
v.push_back(i);
v.push_back(n - i);
}
set<int> st;
for(int i : v){
st.insert(i);
}
vector<int> nv;
for (int i : st) {
nv.push_back(i);
}
}
void test_latency(size_t iteration) {
PROFILE_START("A");
A();
PROFILE_STOP("A");
PROFILE_START("B");
B();
PROFILE_STOP("B");
}
int main() {
const size_t warmups = 1000;
const size_t tests = 100;
PROFILE_RUN_ALL(warmups, tests, test_latency(__loop);)
return 0;
}


#include <bits/stdc++.h>
using namespace std;
multiset<int> s;
int main() {
for(int i = 5; i >= 1; i--){
s.insert(i);
s.insert(i);
}
for(int it : s) cout << it << " ";
cout << '\n';
return 0;
}
/* 1 1 2 2 3 3 4 4 5 5 */

스택은 주로 문자열 폭발, 아름다운 괄호만들기, 짝찾기 키워드를 기반으로 이루어진 문제에서 쓰일 수 있다.
또한, “교차하지 않고” 라는 문장이 나오면 스택을 사용해야 하는 것은 아닐까? 염두해야 함
#include<bits/stdc++.h>
using namespace std;
stack<string> stk;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
stk.push("김");
stk.push("혜");
stk.push("주");
stk.push("화");
stk.push("이");
stk.push("팅");
while(stk.size()) {
cout << stk.top() << "\n";
stk.pop();
}
return 0;
}
/*
팅
이
화
주
혜
김
*/
해당 value를 스택에 추가한다.
가장 마지막에 추가한(가장 위에 있는) 요소를 지운다.
가장 마지막에 있는(가장 위에 있는) 요소를 참조한다.
스택의 크기

#include <bits/stdc++.h>
using namespace std;
queue<int> q;
int main(){
for(int i = 1; i <= 10; i++) q.push(i);
while(q.size()){
cout << q.front() << ' ';
q.pop();
}
return 0;
}
/*
1 2 3 4 5 6 7 8 9 10
*/
value를 큐에 추가한다.
가장 앞에 있는 요소를 제거한다.
큐의 크기
가장 앞에 있는 요소를 참조한다.
queue는 앞에서만 끄집어낼 수 있다면 deque은 앞뒤로 삽입, 삭제, 참조가 가능한 자료구조입니다.

using namespace std;
int main(){
deque<int> dq;
dq.push_front(1);
dq.push_back(2);
dq.push_back(3);
cout << dq.front() << "\n";
cout << dq.back() << "\n";
cout << dq.size() << "\n";
dq.pop_back();
dq.pop_front();
cout << dq.size() << "\n";
return 0;
}
/*
1
3
3
1
*/
구조체라 불리는 struct는 C++에서 제공하는 자료구조가 아닌 개발자의 커스텀한 자료구조를 의미한다.
커스텀하게 정렬을 추가하고 싶거나 문제에서 여러개의 변수(ex - a, b, c, d, e)가 들어간 자료구조가 필요하다면 struct를 사용해야 한다.

예를 들어 int 타입의 2개의 멤버변수, double 타입의 3개의 멤버 변수가 필요하다고 하자.
c.f 멤버변수란 클래스 또는 구조체 내부의 변수이자 메소드 밖에 있는 변수를 뜻함
Ralo라는 int 타입의 2개의 멤버변수, double 타입의 3개의 멤버 변수를 가진 간단한 구조체를 형성한 것을 볼 수 있다. #include<bits/stdc++.h>
using namespace std;
struct Ralo{
int a, b;
double c, d, e;
};
void print(Ralo ralo){
cout << ralo.a << " " << ralo.b << " " << ralo.c << " " << ralo.d << " " << ralo.e << '\n';
}
int main(){
Ralo ralo = {1, 1, 1, 1, 1};
print(ralo);
vector<Ralo> ret;
ret.push_back({1, 2, 3, 4, 5});
ret.push_back({1, 2, 3, 4, 6});
ret.push_back({});
ret.push_back({1, 3});
for(Ralo ralo : ret){
print(ralo);
}
return 0;
}
/*
1 1 1 1 1
1 2 3 4 5
1 2 3 4 6
0 0 0 0 0
1 3 0 0 0
*/
간단한 구조체를 집어넣은 vector를 정렬한다면 어떻게?
a를 1순위로, b를 2순위로 오름차순으로 정렬하고 싶다?
지금은 아무것도 집어넣지 않을 경우 0 또는 빈 문자열이 들어가게 되는데 초기값을 설정하고 싶다면?
➡️ 요구사항이 많아지게 되면 조금은 복잡하게 구조체를 구축해야 한다.
다음 코드는 조금은 복잡한 Point라는 구조체를 정의한다.
구조체를 기반으로 정렬하는 연산, 그리고 초기값 설정이 필요하다면 다음과 같은 형태로 구조체를 정의해야 한다.
struct Point{
int y, x;
Point(int y, int x) : y(y), x(x){}
Point(){y = -1; x = -1; }
bool operator < (const Point & a) const{
if(x == a.x) return y < a.y;
return x < a.x;
}
};
하나씩 살펴보자.
int y, x;
Point(int y, int x) : y(y), x(x){}
c.f java의 constructor 매직메서드
class의 constructor라는 매직메서드를 생각하면 된다. 이 구조체를 기반으로 객체를 생성할 때 y, x를 받아 생성한다라는 의미이다.

Point(){y = -1; x = -1; }
bool operator < (const Point & a) const{
if(x == a.x) return y < a.y;
return x < a.x;
}
구조체를 기반으로 만들어진 객체들끼리 비교해야 하는 경우가 있다.(ex : PointA < PointB) 이 때 비교하는 "기준"을 잡는다. 1순위는 x, 2순위는 y를 기반으로 크고 작음을 판단하는 코드이다.
이 비교는 단순하게 if문으로 비교할 때도 정의해야하지만 정렬할 때도 정의해야 한다. 정렬이란 요소들을 비교해가며 정렬하는 것이기 때문이다. 앞의 코드를 기반으로 정렬하면 x가 1순위, y가 2순위로 “오름차순"으로 정렬된다.
만약 {1, 2}, {2, 3}이 만나면 어떻게 되는가? x는 서로 같지 않기 대문에 바로 밑의 return문으로 내려가게 되고 x를 비교해서 {1, 2}가 더 작기 때문에 {1, 2}와 {2, 3}을 비교했을 때 {1, 2}가 더 작다는 결론을 내리게 된다. 만약 x가 같다면 이와 같은 논리로 y를 기반으로 비교해서 해당 요소가 더 작다는 결론을 내릴 것이다.
if(x == a.x) return y < a.y;
return x < a.x;
구조체끼리도 크고 작음을 비교할 때는 이렇게 어떠한 멤버변수를 기준으로 할 것인지 등을 정해야 한다.
왜냐하면 operator +, -, *, / 등의 의미를 변경하지 않으며 그저 오퍼레이터의 대상이 바뀌는 것 뿐이기 때문이다. 교안에서는 <라는 연산자를 오버로딩하는데 이는 그냥 int, float의 기본타입이 아니라 좀 더 복잡한 어떠한 객체 struct 를 기반으로 확장하는 것 뿐이기 때문이다.
앞의 코드를 보면 < 오퍼레이터를 기준으로 struct를 구축된 것을 볼 수 있다. 이를 기반으로 이러한 struct를 구현한 변수들을 sort를 하는 상황이 생긴다. 이 때문에 저 struct의 오퍼레이터는 >가 아니라 < 오퍼레이터를 기준으로 설정되어야 한다.
sort()함수 자체가 < 오퍼레이터를 기준으로 정렬하기 때문이다.
c.f 참고로 struct 내의 오퍼레이터 오버로딩 하지 않고 compare 함수를 만들어서 할 수도 있다. 이 때 sort를 사용한다면 동일하게 < 오퍼레이터를 기준으로 compare함수를 정의해야 한다.
string으로 이뤄진 배열을 정렬한 코드
#include <bits/stdc++.h>
using namespace std;
bool compare(string a, string b){
if(a.size() == b.size()) return a < b;
return a.size() < b.size();
}
int main(){
ios::sync_with_stdio(0); cin.tie(0);
string a[3] = {"111", "222", "33"};
sort(a, a + 3, compare);
for(string b : a) cout << b << " ";
return 0;
}
/*
33 111 222
*/
숫자를 담고 있지만 string으로 설정된 변수들을 정렬할 때 앞의 코드와 같은 compare()함수를 써야 한다.
string은 서로 비교할 때 왼쪽에서부터 아스키코드순으로 비교한다.
왼쪽에서부터 아스키코드순으로 비교하기 때문에 “33”이 “111”보다 더 크다고 인식해버린다. 그래서 항상 숫자로 이루어진 문자열을 비교할 때는 사이즈확인 로직을 넣는게 중요합니다.
Ralo struct를 compare 함수를 통해 정렬한 코드
#include <bits/stdc++.h>
using namespace std;
struct Ralo{
int a, b;
};
bool compare(Ralo A, Ralo B){
if(A.a == B.a) return A.b < B.b;
return A.a < B.a;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0);
Ralo a[3] = {{1, 2}, {1, 3}, {0, 4}};
sort(a, a + 3, compare);
for(Ralo A : a) cout << A.a << " : " << A.b << "\n";
return 0;
}
/*
0 : 4
1 : 2
1 : 3
*/
1순위로 Ralo의 a를 오름차순으로, 2순위로 Ralo의 b를 오름차순으로 정렬되는 코드이다.
자, 이제 3개의 멤버변수인 y, x, z가 필요하다고 해보자.
x를 1순위로 오름차순으로 정렬하고, y가 2순위로 내림차순, z가 3순위로 오름차순 정렬이라는 문제가 있다면?
struct Point{
int y, x, z;
Point(int y, int x, int z) : y(y), x(x), z(z){}
Point(){y = -1; x = -1; z = -1; }
bool operator < (const Point & a) const{
if(x == a.x) {
if(y == a.y) return z < a.z;
return y > a.y;
}
return x < a.x;
}
};
이렇게 위와 같이 구조체를 구축한다. 참고로 초기값은 -1이 아니라 달느 수를 집어넣어도 된다.
vector에다 Point라는 struct를 넣어서 정렬하는 모습이다.
#include <bits/stdc++.h>
using namespace std;
struct Point {
int y, x;
};
bool cmp(const Point &a, const Point &b) {
return a.x > b.x;
}
vector<Point> v;
int main(){
for (int i = 10; i >= 1; i--) {
v.push_back({i, 10 - i});
}
sort(v.begin(), v.end(), cmp);
for (auto it : v) cout << it.y << " : " << it.x << "\n";
return 0;
}
/*
1 : 9
2 : 8
3 : 7
4 : 6
5 : 5
6 : 4
7 : 3
8 : 2
9 : 1
10 : 0
*/
c.f
커스텀한 자료구조를 만들 때 보통 class와 struct를 쓰지만 코딩테스트에서는 struct만 알아도 충분하다. 둘의 차이는 struct의 멤버변수는 기본적으로 public이며 상속이 안되고, class의 멤버변수는 기본적으로 private이며 상속이 된다.

c.f
힙은 완전이진트리로 최소힙 또는 최대힙이 있으며 삽입, 삭제, 탐색, 수정에 대해 O(logN)의 시간복잡도를 갖는다. 최대 힙은 루트 노드에 최대값이 있고, 최소 힙은 루트 노드에 최소값이 있는 힙을 말한다.

greater<타입> 을 써서 오름차순, less<타입>을 써서 내림차순으로 바꿀 수 있다. priority_queue<타입>을 쓰면 해당 타입에 대한 내림차순으로 설정된다.size(), top(), pop(), push()가 있다. #include <bits/stdc++.h>
using namespace std;
priority_queue<int, vector<int>, greater<int> > pq; //오름차순
priority_queue<int> pq2; // 내림차순
priority_queue<int, vector<int>, less<int> > pq3; // 내림차순
int main(){
for(int i = 5; i >= 1; i--){
pq.push(i); pq2.push(i); pq3.push(i);
}
while(pq.size()){
cout << pq.top() << " : " << pq2.top() << " : " << pq3.top() << '\n';
pq.pop(); pq2.pop(); pq3.pop();
}
return 0;
}
/*
1 : 5 : 5
2 : 4 : 4
3 : 3 : 3
4 : 2 : 2
5 : 1 : 1
*/
int 뿐만 아니라 구조체(struct) 등 다른 자료구조를 넣어서 할 수 있다.
#include <bits/stdc++.h>
using namespace std;
struct Point {
int y, x;
Point(int y, int x) : y(y), x(x) {}
Point() {y = -1; x = -1;}
bool operator < (const Point & a) const {
return x > a.x;
}
};
priority_queue<Point> pq;
int main(){
pq.push({1, 1});
pq.push({2, 2});
pq.push({3, 3});
pq.push({4, 4});
pq.push({5, 5});
pq.push({6, 6});
cout << pq.top().x << "\n";
return 0;
}
/*
1
*/
앞의 코드를 보면 분명 < 연산자에 x > a.x를 했기 때문에 분명 내림차순으로 정렬되어 6이 출력이 되어야 하는데 1이 출력된다.
➡️ 이는 우선순위큐에 커스텀 정렬을 넣을 때 반대로 넣어야 하는 특징 때문이다.
반대로 해보자.
#include <bits/stdc++.h>
using namespace std;
struct Point {
int y, x;
Point(int y, int x) : y(y), x(x) {}
Point() {y = -1; x = -1;}
bool operator < (const Point & a) const {
return x < a.x;
}
};
priority_queue<Point> pq;
int main(){
pq.push({1, 1});
pq.push({2, 2});
pq.push({3, 3});
pq.push({4, 4});
pq.push({5, 5});
pq.push({6, 6});
cout << pq.top().x << "\n";
return 0;
}
/*
6
*/
x > a.x가 x < a.x로 바뀐 모습이다. 우선순위큐에 커스텀 정렬을 넣을 때는 반대라고 생각하면 된다. 가장 최소를 끄집어 내고 싶은 로직이라면 >, 최대라면 < 이런식으로 설정하면 된다.
다음 코드처럼도 가능하다.
#include <bits/stdc++.h>
using namespace std;
struct Point {
int y, x;
};
struct cmp {
bool operator() (Point a, Point b) {
return a.x < b.x;
}
};
priority_queue<Point, vector<Point>, cmp> pq;
int main(){
pq.push({1, 1});
pq.push({2, 2});
pq.push({3, 3});
pq.push({4, 4});
pq.push({5, 5});
pq.push({6, 6});
cout << pq.top().x << "\n";
return 0;
}
/*
6
*/
지금까지 다룬 자료구조의 최악의 시간복잡도를 정리한다.
| 자료구조 | 참조 | 탐색 | 삽입 | 삭제 |
|---|---|---|---|---|
| 배열 | O(1) | O(n) | O(n) | O(n) |
| 스택 | O(n) | O(n) | O(1) | O(1) |
| 큐 | O(n) | O(n) | O(1) | O(1) |
| 연결리스트 | O(n) | O(n) | O(1) | O(1) |
| 맵 | O(logn) | O(logn) | O(logn) | O(logn) |