본문 바로가기

개발일지

C++ STL 셋(SET), 맵(MAP)

C++ 에는 시퀀스 컨테이너들 (vector, list, deque)  3종류 외에 다른 종류의 컨테이너인 연관 컨테이너(associative container) 의 셋(set) 과 맵 (map) 에 대해서 알아보겠습니다.

 

연관 컨테이너는 시퀀스 컨테이너와는 다르게 키(key) - 값(value) 구조를 가집니다. 

 

위 처럼 연관 컨테이너는 키를 바탕으로 이에 대응되는 값을 얻을 수 있는 구조 입니다.

 

#include <iostream>
#include <set>

using namespace std;

template <typename T>
void print_set(set<T>& s) {
  // 셋의 모든 원소들을 출력하기
  cout << "[ ";
  for (typename set<T>::iterator itr = s.begin(); itr != s.end(); ++itr) {
    cout << *itr << " ";
  }
  cout << " ] " << endl;
}
int main() {
  set<int> s;
  s.insert(10);
  s.insert(50);
  s.insert(20);
  s.insert(40);
  s.insert(30);

  cout << "순서대로 정렬되서 나온다" << endl;
  print_set(s);

  cout << "20 이 s 의 원소인가요? :: ";
  auto itr = s.find(20);
  if (itr != s.end()) {
    cout << "Yes" << endl;
  } else {
    cout << "No" << endl;
  }

  cout << "25 가 s 의 원소인가요? :: ";
  itr = s.find(25);
  if (itr != s.end()) {
    cout << "Yes" << endl;
  } else {
    cout << "No" << endl;
  }
}

set의 경우 insert() 함수가 어디에 추가할지 정보없이 추가되며, 삽입된 원소가 순서대로 자동으로 정렬되어 있습니다.

시퀀스 컨테이너가 상자 하나에 원소를 한 개 씩 담고, 각 상자에 번호를 매긴 것이라면, 셋은 그냥 큰 상자 안에 모든 원소들을 쑤셔 넣은 것이라 보면 됩니다. 그 상자 안에 원소가 어디에 있는지는 중요한게 아니고,그 상자 안에 원소가 '있냐/없냐' 만이 중요한 정보입니다.

 

셋에는 find(0 함수가 제공되며, 이 find() 함수를 통해 이 셋에 원소가 존재하는지 아닌지 확인할 수 있습니다.

또한 셋의 중요한 특징으로 바로 셋 안에는 중복된 원소들이 없다는 점이 있습니다.

set은 순서대로 정렬된 원소를 순서대로 찾아주기때문에 중복된 원소가 있을 수 없습니다.

 

참고로 시퀀스 컨테이너들과 마찬가지로 set 역시 범위 기반 for 문을 지원합니다. 원소들의 접근 순서는 반복자를 이용해서 접근하였을 때와 동일합니다.

 

다만, 이렇게 set의 경우 순서대로 정렬해야하기때문에 직접 만든 class 객체를 저장하기 위해서는 서로의 순서를 비교할수 있는 operator< 를 오버로딩해주어야 합니다.

 

#include <iostream>
#include <set>
#include <string>

using namespace std;

template <typename T>
void print_set(set<T>& s) {
  // 셋의 모든 원소들을 출력하기
  for (const auto& elem : s) {
    cout << elem << " " << endl;
  }
}
class Todo {
  int priority;
  string job_desc;

 public:
  Todo(int priority, string job_desc)
      : priority(priority), job_desc(job_desc) {}

  bool operator<(const Todo& t) const {
    if (priority == t.priority) {
      return job_desc < t.job_desc;
    }
    return priority > t.priority;
  }

  friend ostream& operator<<(ostream& o, const Todo& td);
};

ostream& operator<<(ostream& o, const Todo& td) {
  o << "[ 중요도: " << td.priority << "] " << td.job_desc;
  return o;
}
int main() {
  set<Todo> todos;

  todos.insert(Todo(1, "농구 하기"));
  todos.insert(Todo(2, "수학 숙제 하기"));
  todos.insert(Todo(1, "프로그래밍 프로젝트"));
  todos.insert(Todo(3, "친구 만나기"));
  todos.insert(Todo(2, "영화 보기"));

  print_set(todos);

  cout << "-------------" << endl;
  cout << "숙제를 끝냈다면!" << endl;
  todos.erase(todos.find(Todo(2, "수학 숙제 하기")));
  print_set(todos);
}
 

 

중요도에 맞게 순서를 정렬하여주는 operator< 연산자를 오버로딩 하였기때문에 중요도를 순서로 원소값을 저장한 set 입니다. (중요도가 같다면 피연산자가 우선순위이다)

 

 

맵 (map)

맵은 셋과 거의 똑같은 자료 구조 입니다. 다만 셋의 경우 키만 보관했지만, 맵의 경우 키에 대응되는 값(value) 까지도 같이 보관하게 됩니다.

 

 

#include <iostream>
#include <map>
#include <string>

using namespace std;

template <typename K, typename V>
void print_map(map<K, V>& m) {
  // 맵의 모든 원소들을 출력하기
  for (auto itr = m.begin(); itr != m.end(); ++itr) {
    cout << itr->first << " " << itr->second << endl;
  }
}

int main() {
  map<string, double> pitcher_list;

  // 참고로 2017년 7월 4일 현재 투수 방어율 순위입니다.

  // 맵의 insert 함수는 pair 객체를 인자로 받습니다.
  pitcher_list.insert(pair<string, double>("박세웅", 2.23));
  pitcher_list.insert(pair<string, double>("해커 ", 2.93));
  pitcher_list.insert(pair<string, double>("피어밴드 ", 2.95));

  // 타입을 지정하지 않아도 간단히 make_pair 함수로
  // pair 객체를 만들 수 도 있습니다.
  pitcher_list.insert(make_pair("차우찬", 3.04));
  pitcher_list.insert(make_pair("장원준 ", 3.05));
  pitcher_list.insert(make_pair("헥터 ", 3.09));

  // 혹은 insert 를 안쓰더라도 [] 로 바로
  // 원소를 추가할 수 있습니다.
  pitcher_list["니퍼트"] = 3.56;
  pitcher_list["박종훈"] = 3.76;
  pitcher_list["켈리"] = 3.90;

  print_map(pitcher_list);

  cout << "박세웅 방어율은? :: " << pitcher_list["박세웅"] << endl;
}

 

출력값

 

insert() 함수는 pair객체로 전달해야합니다.

해당 객체에는 2가지의 값이 각각 key와 value 값으로 되어있는 객체입니다.

 다만 인자타입을 매번 지정해주지않고 아래와 같이도 사용가능합니다.

pitcher_list.insert(std::make_pair("헥터 ", 3.09));

또는 insert를 사용하지 않아도 원소를 추가할 수도 있습니다.

맵의 경우 operator[] 를 이용해서 새로운 원소를 추가할 수 도 있습니다 (만일 해당하는 키가 맵에 없다면). 만일 키가 이미 존재하고 있다면 값이 대체될 것입니다.

pitcher_list["니퍼트"] = 3.56;

또한 key 값으로 맞는 value 값을 찾을 수도 있습니다.

std::cout << "박세웅 방어율은? :: " << pitcher_list["박세웅"] << std::endl;

 

없는 값을 참조하였으니 오류가 발생해야 정상인데 오히려 값을 돌려주었네요.이는 [] 연산자가, 맵에 없는 키를 참조하게 되면, 자동으로 값의 디폴트 생성자를 호출해서 원소를 추가해버리기 때문입니다.

 

double 의 디폴트 생성자의 경우 그냥 변수를 0 으로 초기화 해버립니다. 따라서 되도록이면 find 함수로 원소가 키가 존재하는지 먼저 확인 후에, 값을 참조하는 것이 좋습니다. 아래는 find 함수를 이용해서 안전한게 키에 대응되는 값을 찾는 방법입니다.

 

#include <iostream>
#include <map>
#include <string>

using namespace std;

template <typename K, typename V>
void print_map(const map<K, V>& m) {
  // kv 에는 맵의 key 와 value 가 pair 로 들어갑니다.
  for (const auto& kv : m) {
    cout << kv.first << " " << kv.second << endl;
  }
}

int main() {
  map<string, double> pitcher_list;

  pitcher_list["오승환"] = 3.58;
  cout << "류현진 방어율은? :: " << pitcher_list["류현진"] << endl;

  cout << "-----------------" << endl;
  print_map(pitcher_list);
}

 

 

 

마지막으로 짚고 넘어갈 점은 맵 역시 셋 처럼 중복된 원소를 허락하지 않는다는 점입니다. 이미, 같은 키가 원소로 들어 있다면 나중에 오는 insert 는 무시됩니다.

 

이미 같은 키를 가지는 원소가 있다면 그 insert 작업은 무시됩니다. 만약에, 원소에 대응되는 값을 바꾸고 싶다면 insert 를 하지 말고, [] 연산자로 대응되는 값을 바꿔주면 됩니다.

 

#include <iostream>
#include <map>
#include <string>

using namespace std;

template <typename K, typename V>
void print_map(const map<K, V>& m) {
  // kv 에는 맵의 key 와 value 가 pair 로 들어갑니다.
  for (const auto& kv : m) {
    cout << kv.first << " " << kv.second << endl;
  }
}

int main() {
  map<string, double> pitcher_list;

  // 맵의 insert 함수는 pair 객체를 인자로 받습니다.
  pitcher_list.insert(pair<string, double>("박세웅", 2.23));
  pitcher_list.insert(pair<string, double>("박세웅", 2.93));

  print_map(pitcher_list);

  // 2.23 이 나올까 2.93 이 나올까?
  cout << "박세웅 방어율은? :: " << pitcher_list["박세웅"] << endl;
}