C++ set 자료구조 사용하는 방법을 알아보자😚
안녕하세요! 두두코딩 입니다 ✋
오늘은 Set의 자료구조에 대해 알아보겠습니다.
이전 포스팅을 참고하시면 더 이해가 편합니다!
🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
중복된 값을 삽입할 경우
Set 같은 경우 고유한 값을 특정한 순서에 따라 저장하는 컨테이너이다. 고유한 값이 들어온다는 전제가 있지만, 만약 개발자가 동일한 값을 입력했을 때 어떻게 될까?
결론적으로는 아무일도 일어나지 않는다😵
set
내부에선 어떻게 처리할지 알아보자.
위의 그림과 같이, set
에서는 데이터를 삽입할 때, 하나의 pair 형태로 반환한다. pair
의 첫번째 값은 해당 위치를 가리키는 반복자이고, 두 번째 인자 값은 삽입 성공유무를 판단하는 bool
변수이다.
#include <iostream>
#include <set>
using namespace std;
int main()
{
set<int> s;
s.insert(30);
s.insert(40);
s.insert(20);
s.insert(10);
s.insert(45);
s.insert(25);
// 동일한 값을 넣었을 때
auto ret = s.insert(20);
// 두번째 인자로 성공유무 판단.
// 실패라는 것은 중복된 값이라는 것.
if (ret.second == false)
cout << "duplicated value" << endl;
}
위의 예시처럼, 값이 동일하게 들어올 경우, 반환 값을 받아 두번째 값으로 확인해보면, 중복 유무를 판단할 수 있다.
auto ret = s.insert(20);
// 원래 형태
pair<set<int>::iterator, bool> ret = s.insert(20);
위의 코드에서 보이는 것과 같이, 원래 형태는 복잡하다. 따라서, 간단하게 auto
로 쓰고, 원형을 한번 눈에 익히도록 하자.
중복된 값을 허용하는 set
그렇다면 set
에서는 중복된 값을 넣을 수 없을까?
set
자료구조가아닌 multiset
자료구조를 사용하면 중복을 사용할 수 있다. 위의 코드를 multiset
으로 변경해보자.
#include <iostream>
#include <set>
using namespace std;
int main()
{
// set -> multiset으로 변경
// set<int> s;
multiset<int> s;
s.insert(30);
s.insert(40);
s.insert(20);
s.insert(10);
s.insert(45);
s.insert(25);
// 동일한 값을 넣었을 때
auto ret = s.insert(20);
// multiset 일경우 중복된 값이 무조건 들어가기때문에
// 성공유무를 판단할 필요가 없다.
// if (ret.second == false)
// cout << "duplicated value" << endl;
auto p = begin(s);
while( p != end(s) )
{
cout << *p << endl;
++p;
}
}
위의 코드와 같이, set
을 multiset
으로 변경해서 사용해보자. 중요한 특징은 multiset
같은 경우 중복된 값을 무조건 허용하기 때문에, set
과 달리 pair형식으로 반환값을 전달받는 것이 아닌 iterator
만 전달받는다.
따라서, second
로 중복 유무를 검사하는 부분을 주석으로 막고 테스틀해야한다.
사용자 정의 타입을 set에 넣기
우리는 프로그램을 개발하면서 기본 데이터 타입만 사용하는것이 아니라, 때론 사용자 정의 타입 (class)를 만들어 사용한다. 하지만 set
은 사용자 정의 타입이 있는지도 알지 못한다.
아래의 예시를 통해 알아보자.
#include <iostream>
#include <set>
using namespace std;
struct Point
{
int x,y;
Point(int a = 0, int b = 0) : x(a), y(b) {}
};
int main()
{
set<Point> s;
s.insert(Point(1,1));
s.insert(Point(2,2));
s.insert(Point(3,3));
}
위의 코드를 수행했을 때, set
에서는 사용자 정의 타입을 알지못하는 상태에서 비교한다. 사실 set
에서는 어떻게 비교를 해야하는지도 몰라 아래와 같은 에러를 출력한다.
...
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/set:675:25: note: in instantiation of member function 'std::__1::__tree<Point, std::__1::less<Point>, std::__1::allocator<Point>>::__insert_unique' requested here
{return __tree_.__insert_unique(_VSTD::move(__v));}
^
set.cc:16:4: note: in instantiation of member function 'std::__1::set<Point, std::__1::less<Point>, std::__1::allocator<Point>>::insert' requested here
s.insert(Point(1,1));
^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/utility:592:1: note: candidate template ignored: could not match 'pair<type-parameter-0-0, type-parameter-0-1>' against 'const Point'
operator< (const pair<_T1,_T2>& __x, const pair<_T1,_T2>& __y)
^
1 error generated.
이런 set
자료구조에 사용자 정의 타입을 넣으려면 어떻게 해야할까?
해결하는 방법은 함수 객체 제공 과 < 연산자 재정의 두 가지를 활용하면 된다.
🌱 함수 객체 제공
함수 객체를 제공하는 것은 템플릿 단위전략 패턴을 활용하는 방법으로, set
에서 해당 타입을 비교할 때, 내가 전달한 비교함수로 해줘~ 라고 요청하는 것이다.
// 함수 객체 만들기
struct Pointless
{
// 편의상 x만 비교합니다.
bool operator()(const Point& p1, const Point& p2) const
{
return p1.x < p2.x;
}
};
int main()
{
set<Point, Pointless> s;
s.insert(10);
...
}
위의 코드와 같이, Pointless
함수 객체를 만들어, set
의 두번째 인자로 넘겨주게 되면 된다. set
은 두번째 인자를 전달받아 해당 비교 객체를 통해 비교하도록 한다.
🌱 < 연산자 재정의
위의 방법보다 더 쉽게 구현할 수 있는 방법이다. 바로 < 연산자 를 재정의해두면 set
이 알아서 사용하도록 한다.
struct Point
{
int x,y;
Point(int a = 0, int b = 0) : x(a), y(b) {}
// < 연산자 재정의
// 편의상 x 값만 비교
bool operator<(const Point& p) const
{
return x < p.x;
}
};
위와 같이, 사용자 정의 타입에 operator<
를 넣어둘 경우 set
은 자동적으로 비교할 수 있다.
사용자 정의 타입 상등연산
그렇다면 우리는 set
을 잘 사용하기 위해 어떤 비교연산들을 제공해야할까?
당연히 insert()
를 위해 < 연산자
와 > 연산자
두개를 함수 객체나 연산자 재정의로 제공해야한다.
만약 우리가 동일한 값을 입력하거나 find()
함수를 사용하기 위해서 ==
연산을 제공해야할까? 생각해보자🤔
결론적으로는 ”==” 연산자는 제공할 필요가 없다 이다.
set
내부적으로는 상등을 조사하는 방법이 있다.
// set 내부 과정 간략화
if (newelem < oldelem) left
else if (oldelem < newelem) right
else // 모두 같다고 판단..
위의 코드는 상등을 구하는 부분을 간략화한 것이다. set
내부에서는 “<” 연산과 “>” 연산을 통해 비교를 하고, 나머지는 상등으로 취급하기 때문에 ==
연산은 굳이 필요하지가 않다.
==
연산은 만들어도 사용하지 않는다는 점을 기억하자!